#!/usr/bin/perl -w # Filename: httpdave # Author: David Ljung Madison # See License: http://MarginalHacks.com/License/ my $VERSION= 0.8; # Description: A simple web server use strict; use Socket; use IO::File; # ######################### # Changelog # ######################### # Need to thread/fork/whatever? # # 2005-01-27: v0.8 # shtml (Server Side Includes) support! # # 2004-10-24: v0.7 # Cookie support # # 2004-04-19: v0.6 # Better mime-types handling # # 2003-05-19: v0.5 # Environment vars, other stuff # # ??: v 0.4 # Real POST support, lots of other stuff # # 2000-06-23: v 0.3 # Handles paths and default path safely # Added support for images # Added directory listings (with sorting!) # # -------------------------------------------- # - Taken over by Dave http://DaveSource.com - # -------------------------------------------- # # Original: pure perl httpd. (c) Paul Tchistopolskii 1998, 99 # http://www.pault.com e-mail: paul@qub.com # # 1998-04-29: v0.2 # 'Support' for 'POST'. It was useful for debugging uploads. # # 1998-11-27: v0.1 # Supports only .htm(l) and .cgi in one directory # Not all ENV's are set. No 'index.html'. Only 'GET' yet. # ################################################## # Variables ################################################## my $PORT = $ARGV[0] || 3001; my $HOSTNAME = `hostname`; chomp $HOSTNAME; my $DOMAIN = "$HOSTNAME:$PORT"; my $ROOT = '.'; my $PERL = '/usr/bin/perl'; my $ICONS = "/icons"; my @DEFAULT_FILES = qw(index.html index.shtml index.cgi index.htm); my $HTML = '\.s?html?$'; my $BUFSIZE = 512; chdir $ROOT || die("Couldn't go to root directory [$ROOT]\n"); $ROOT = `pwd`; chomp($ROOT); # Setup ENV variables my $path = $ENV{PATH}; # Save path? my $home = $ENV{HOME}; #my $path = "/sbin:/usr/sbin:/bin:/usr/bin"; # Or, use this - more secure undef %ENV; $ENV{PATH} = $path; $ENV{DOCUMENT_ROOT} = $ROOT; $ENV{HTTP_HOST} = $HOSTNAME; $ENV{SERVER_ADDR} = $HOSTNAME; # Should be I.P. number, actually $ENV{SERVER_NAME} = $HOSTNAME; $ENV{SERVER_PORT} = $PORT; $ENV{SERVER_SOFTWARE} = "$0 $VERSION"; $ENV{SERVER_SIGNATURE} = "
$ENV{SERVER_SOFTWARE}
\n"; $ENV{SERVER_PROTOCOL} = "HTTP/1.0"; # I must confess I don't know if it's 1.1.. # Example variables for SUPERKLUDGE rename testing below my $SUPERKLUDGE=0; # currently off, so don't worry about it.. (always off for .shtml) my @DOMAINS = qw(Daveola DavidLjung GetDave DaveSource GetBert GetMadison DavePics DaveFAQ SaintVitus EveryScene DaveDomain imwriter ParkerLjung); # Where mime types are kept my $MIME_TYPES = '/usr/local/lib/mime-types'; $MIME_TYPES = '/usr/lib/mime-types' unless -r $MIME_TYPES; $MIME_TYPES = '/etc/mime-types' unless -r $MIME_TYPES; $MIME_TYPES = "$home/.mime-types" unless -r $MIME_TYPES; $MIME_TYPES = "$home/.mime.types" unless -r $MIME_TYPES; # Other types not found in mime-types my $EXTRA_MIME_TYPES = <) { chomp; s/#.*//; next unless /\S/; my ($post,$type) = split(/\s+/,$_,2); $MIME_TYPES{$post} = $type unless $MIME_TYPES{$post}; } close MIME_TYPES; foreach ( split(/\n/,$EXTRA_MIME_TYPES) ) { s/^\s+//; s/#.*//; next unless /\S/; my ($post,$type) = split(/\s+/,$_,2); $MIME_TYPES{$post} = $type unless $MIME_TYPES{$post}; } } init_types(); sub type { my ($post) = @_; $post =~ s/.*\.//; $MIME_TYPES{$post} || "text/plain"; } ######################### # Errors ######################### sub logmsg { print STDERR scalar localtime, ": $$: @_\n"; } # Just close our output end of the pipe so whatever we were doing will stop sub broken_pipe { my ($sig) = @_; close STDOUT; } $SIG{'PIPE'} = 'broken_pipe'; my %codes = ( '200', 'OK', '201', 'Created', '202', 'Accepted', '204', 'No Content', '301', 'Moved Permanently', '302', 'Moved Temporarily', '304', 'Not Modified', '400', 'Bad Request', '401', 'Unauthorized', '403', 'Forbidden', '404', 'Not Found', '500', 'Internal Server Error', '501', 'Not Implemented', '502', 'Bad Gateway', '503', 'Service Unavailable', ); sub logerr ($$$) { my ($code, $detail, $extra) = @_; my $msg = "$code " . $codes{$code}; logmsg "$detail : $msg"; print "HTTP/1.0 $msg\nContent-type: text/html\n"; print $extra if $extra; print "\n"; print "HttpDave : $detail : $msg\n"; } ######################### # Requests ######################### my $SHTML_ERR = undef; # http://httpd.apache.org/docs/mod/mod_include.html sub shtml { my ($file,$url,$element,$attr,$Val) = @_; my $val = $Val; $val =~ s/^["'](.*)["']$/$1/g; if ($element eq "exec" && $attr eq "cmd") { chdir $1 if $file =~ m|^(.+/)[^/]+$|; my $out = `$val`; chdir $ROOT || die("Couldn't go to root directory [$ROOT]\n"); return $out || ""; } return $ENV{$val} if $element eq "echo" && $attr eq "var"; if ($element eq "include" && ($attr eq "file" || $attr eq "virtual")) { flush_out(); # Kludge: Need to flush STDOUT first my $include = $val; if ($attr eq 'file') { $include =~ s#(^|/)\.\.(/|$)#/#g; # No '..' in path my $dir = ($file !~ m|^(.+/)[^/]+$|) ? '.' : $1; $include = "$dir/$include"; $include =~ s|/+|/|g; } else { my $path = $url =~ (m|(.+)/[^/]+$|) ? $1 : '/'; $include = "$path/$include" unless $include =~ m|^/|; ($include, undef) = clean_url($include); } dump_file($include); return ""; } return last_mod($val) if $element eq "flastmod" && $attr eq "file"; if ($element eq "config" && $attr eq "errmsg") { $SHTML_ERR = $val; return ""; } # Unsupported shtml constructs: # # # # # # # fsize file/virtual # if (grep($element eq $_, qw(set if elif else endif config))) { return "[unsupported shtml directive: $element]"; } return $SHTML_ERR || "[an error occurred while processing this directive: $element, $attr=$val]"; } sub flush_out { my $sel = select(STDOUT); my $save = $|; $|=1; $|=$save; select($sel); } sub dump_file ($$$) { my ($file,$url,$args) = @_; my $type = type($file); if ($type && $url) { print "HTTP/1.0 200 OK\n"; print "Content-type: $type\n\n"; } #my $search = $1 if ($type =~ /text/ && $args =~ m|\?/=(.+)|); my $in = new IO::File; $in->open("< $file") || return 0; my $ssi = ($file =~ /\.shtml?$/) ? 1 : 0; # Handle normal files. Just dump them. if (!$ssi && !($SUPERKLUDGE && $url)) { ## I don't use this, although it only happened if $search was defined and only until the search was found.. # # Search hack. You can automatically jump to any regexp in a page # # with this weird ugliness: page.html?/=#/ # if (defined $search) { # while (<$in>) { # if (/($search)/) { # # Actually - we should make sure we aren't in an html tag. Rats. # print "$`$1$'"; # last; # } # print; # } # } my $a; flush_out(); # Kludge: Need to flush STDOUT first # Fast. while (sysread($in,$a,$BUFSIZE)) { syswrite(STDOUT, $a); } $in->close; return 1; } # Handle Server Side Includes if ($ssi) { # shtml environment vars $ENV{DOCUMENT_NAME} = ($file =~ m|.*/([^/]+)$|) ? $1 : $file; $ENV{LAST_MODIFIED} = last_mod($file); # Not including: DATE_GMT, DATE_LOCAL, DOCUMENT_URI, USER_NAME while (<$in>) { # Not quite right, doesn't handle multi-lines s//shtml($file,$url,$1,$2,$3)/eg; print; } $in->close; undef $SHTML_ERR; return 1; } # Superkludge. Weirdness for just my setup... while (<$in>) { # # Search support in SUPERKLUDGE format # if (defined $search && /($search)/) { # print "$`$1$'"; # undef $search; # next; # } # Dave kludges # Look for href=/... and try to fixup fake domain if (/href=\//) { my ($pre,$post) = ($`,$'); # Assume domain is current URL directory my $domain = $url; $domain =~ s|(.)/.*|$1|; $_ = $pre."href=".$domain."/".$post; } s|http://63\.204\.157\.4||ig; my $re; # Convert specific domains to local directories foreach $re ( @DOMAINS ) { $re = lc($re); s|http://(www\.)?$re\.com|/$re\.com|ig; } print; } $in->close; return 1; } # Sort the directory contents # Pointers: [$file,$img,$alt,$mod,$size]); sub dir_contents { my ($dir,$method) = @_; # By last mod if ($method eq "M") { return $a->[3] <=> $b->[3] unless ($a->[3] == $b->[3]); } # By size if ($method eq "S") { return $a->[4] <=> $b->[4] unless (-d "$dir/$a->[0]" || -d "$dir/$b->[0]" || $a->[4] == $b->[4]); return -1 if (-d "$dir/$a->[0]" && ! -d "$dir/$b->[0]"); return 1 if (! -d "$dir/$a->[0]" && -d "$dir/$b->[0]"); } # By name return $a->[0] cmp $b->[0]; } sub cat_directory($$$) { my ($dir,$url,$args) = @_; print "HTTP/1.0 200 OK\n"; print "Content-type: text/html\n\n"; my $sort_by = ($args =~ /^\?(.)/) ? $1 : "N"; my $blank = (-f "$ICONS/blank.gif") ? "\"" : " "; my $pdir = "/"; # Print directory, broken down if ($dir ne ".") { my @dir = split("/",$dir); my @tmp; foreach my $d ( @dir ) { push(@tmp,$d); $pdir .= "$d/"; } } print < Index of $dir

$pdir

END_OF_DIR_HEADER # Get all the filenames (ignore .files) opendir(DIR, $dir) || return logerr 403,"Can't read directory $dir: $!",undef; my @files = grep(!/^\./, readdir(DIR)); closedir DIR; # Build up the file information for the directory listing my @file_info; foreach my $file ( @files ) { my ($img,$alt) = ("unknown.gif","   "); ($img,$alt)=("image2.gif","img") if ($file =~ /\.(gif|jpe?g|tiff?)$/); ($img,$alt)=("text.gif","TXT") if ($file =~ /\.s?html?$/); ($img,$alt)=("compressed.gif","   ") if ($file =~ /\.(g?z|Z)$/); ($img,$alt)=("folder.gif","DIR") if (-d "$dir/$file"); # # Last modified # my $mod=(stat("$dir/$file"))[9]; # Size my $size=(stat(_))[7]; push(@file_info, [$file,$img,$alt,last_mod(),$size]); } my $parent = ["..","back.gif","DIR",last_mod("$dir/.."),(stat(_))[7]]; # Sort @file_info = sort { dir_contents($dir,$sort_by) } @file_info; # Print it out foreach my $finfo ( $parent, @file_info ) { my ($file,$img,$alt,$last_mod,$size) = @$finfo; my $name=$file; $name="Parent Directory" if ($file eq ".."); $name=substr($name,0,30)."..>" if length($name)>33; my $type = ((-f "$ICONS/$img") ? "[$alt] " : "[$alt] "); my $url = $file; $url .= "/" if -d "$dir/$url"; # Size $size=int($size/1024) || 1; my $unit="k"; ($size,$unit)=(int($size/1024),"M") if ($size > 1024); ($size,$unit)=(int($size/1024),"G") if ($size > 1024); #printf "%6s", (-d "$dir/$file") ? "-" : $size.$unit; $size = (-d "$dir/$file") ? "-" : $size.$unit; # Print print < END_DIR_ENTRY } print "
$blank Name Last modified Size Description
$type $name $last_mod $size  
\n\n\n"; return 1; } my $tcp = getprotobyname('tcp'); socket(Server, PF_INET, SOCK_STREAM, $tcp) || die "socket: $!"; setsockopt(Server, SOL_SOCKET, SO_REUSEADDR, pack("l", 1)) || die "setsockopt: $!"; bind(Server, sockaddr_in($PORT, INADDR_ANY)) || die "bind: $!"; listen(Server,SOMAXCONN) || die "listen: $!"; logmsg "server started on port $PORT"; my $addr; my @inetaddr; sub clean_url { my ($path) = @_; my $args = ""; # Remove ?args (the second one is for intra-file searching) ($path,$args)=($`,$1) if ($path =~ m|([\?&][^/]+)$| || $path =~ m|(\?/=[^/]+)|); # Remove all // and /./ #$path="/$path"; # Temporarily make matches easier while ($path =~ s|/\.?/|/|) {} $path =~ s|/\.$||; # Remove all */dir/../* while ($path =~ s|/[^/]+/\.\./|/|) {} # SECURITY: Make sure we didn't have too many /../ # Example: /bob/../../../../../../etc/passwd $path =~ s|^/\.\./|/|; # /../blah -> /blah $path = "/" if $path eq "/.."; # /.. -> / $path =~ s|[^/]+/\.\.$||; # */some/.. -> * # SECURITY: Remove any leading / $path =~ s|^/||g; $path = $path || "."; # We need to redirect them if they asked for a directory and didn't # specify the end '/' (otherwise they won't know how to resolve links) if (-d $path && $path !~ m|/$| && $path ne ".") { logerr 301,"The document has moved here.","Location: http://$DOMAIN/$path/"; return undef; } return ($path,$args) unless -d $path; # Default file (index.html) foreach my $def ( @DEFAULT_FILES ) { return ($path."/$def",$args) if -f "$path/$def"; } ($path,$args); } sub last_mod { my ($file) = @_; my $mod = $file ? (stat($file))[9] : (stat(_))[9]; my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($mod); $mon=(qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec))[$mon]; sprintf("%0.2d-$mon-%s %0.2d:%0.2d ",$mday+1,$year+1900,$hour,$min); } for ( ; $addr = accept(Client,Server); close Client) { my($undef, $undef2, $inetaddr) = unpack('S n a4 x8', $addr); @inetaddr = unpack('C4', $inetaddr); logmsg "incoming connection from: " , join(".", @inetaddr); *STDIN = *Client; *STDOUT = *Client; $_ = ; my ($method, $url, $proto, $garbage) = split; my %info; while ( ) { last if (/^\s*$/); s/[\r\n]//g; $info{uc($1)} = $2 if (/^(\S+):\s*(.*)$/); } if ($garbage ne '') { logerr 400, $_,undef; } else { $url =~ s/%([\dA-Fa-f]{2})/chr(hex($1))/eg; # unescape. logmsg "Req: mthd=$method, url=$url, prot=$proto"; if ($method ne 'GET' && $method ne 'POST') { logerr 501, $method,undef; } else { $ENV{QUERY_STRING} = ""; # if ($url =~ /(.*)\.cgi\?(.*)/) { # $url = "$1.cgi"; # $ENV{QUERY_STRING} = $2; # } ($url,$ENV{QUERY_STRING}) = ($1,$2) if $url =~ /(.*)\?(.*)/; my ($file,$args) = clean_url($url); next unless $file; $ENV{REQUEST_URI} = $url; $ENV{SERVER_PROTOCOL} = $proto; $ENV{REQUEST_METHOD} = $method; $ENV{REMOTE_ADDR} = join('.', @inetaddr); $ENV{REMOTE_HOST} = join('.', @inetaddr); $ENV{HTTP_USER_AGENT} = $info{'USER-AGENT'}; $ENV{HTTP_REFERER} = $info{REFERER} if defined $info{REFERER}; $ENV{HTTP_COOKIE} = $info{COOKIE} if defined $info{COOKIE}; $ENV{CONTENT_LENGTH} = $info{'CONTENT-LENGTH'} if defined $info{'CONTENT-LENGTH'}; if ( not -e $file ) { logerr 404, $file,undef; } else { if ( $file =~ m/\.cgi$/ ) { logmsg "Executing '$file'"; $ENV{SCRIPT_URI} = "http://$DOMAIN/$file"; # Kludgy.. $ENV{SCRIPT_URL} = $url; $ENV{SCRIPT_NAME} = $file; $ENV{SCRIPT_FILENAME} = $file; # Should be abs path my @res; if ($method eq "POST") { #my $data = ; #my $len = length($data); #print STDERR "Content-Length lied! [$info{'CONTENT-LENGTH'} vs actual $len]\n" #if (defined $info{'CONTENT-LENGTH'} && #$info{'CONTENT-LENGTH'} != $len); #$ENV{'CONTENT_LENGTH'} = $len; my $data; read STDIN,$data,$info{'CONTENT-LENGTH'}; $! = 0; @res = `echo \Q$data\E | $file`; } else { $! = 0; @res = `$file`; } if ($? & 127) { logerr 500, $file,"Couldn't run cgi: [$!]\n"; } else { logmsg "Sending results..."; if (@res && $res[0] =~ /^Location:\s/) { print "HTTP/1.1 301 Moved Permanently\n"; } else { print "HTTP/1.0 200 OK\n"; } # More info to print?? (http/1.1?) #HTTP/1.1 200 OK #Date: Sun, 02 Jul 2000 02:25:17 GMT #Server: httpdave/$version (Unix) #Connection: close print @res; } } elsif (-d $file) { logmsg "Dumping directory contents '$file'"; cat_directory($file,$url,$args) || logerr 500,$file,undef; } else { logmsg "Dumping '$file'"; dump_file($file,$url,$args) || logerr 500,$file,undef; } #{ # } elsif ( $file =~ m/$HTML/ ) { # logmsg "Dumping '$file'"; # cat $file || logerr 500,$file,undef; # } else { # logerr 501, $file,undef; # } } } } close STDIN; close STDOUT; }