#!/usr/bin/perl # ppcgid - pure perl cgi server # See the bottom of this file for the POD documentation. Search for the # string '=head'. our $VERSION=0.92; use strict; use POSIX; use IO::Socket; use IO::Select; use IO::String; use IO::File; use LWP::UserAgent; use Getopt::Long; ##### CONFIGURE HERE ##### $ENV{'SERVER_SOFTWARE'} = 'Apache'; my $WRITE_TIMEOUT = 20; # time to wait to complete transmission my $READ_TIMEOUT = 5; # time to wait for get request my $MAX_LINE = 1024; # maximum size of a single header/get line my $HANDLER = '/var/www/handler.pl'; # perl cgi program to handle requests my $LOCAL_ADDR = '0.0.0.0'; # listen address my $LOCAL_PORT = 80; # listen port my $STATUS_HOST = 'noakai[.]com$'; # respond to STATUS only if host matches this my $DEBUG = 0; # modules for your handler program use CGI::Carp; use CGI qw(-oldstyle_urls -nph); ########################## ##### OVERRIDE CONF ###### GetOptions("port=i"=>\$LOCAL_PORT, "handler=s"=>\$HANDLER, "debug"=>\$DEBUG); select STDERR; $|=0; select STDOUT; $|=0; my $main_socket = new IO::Socket::INET(Proto=>'tcp', LocalAddr=>$LOCAL_ADDR, LocalPort=>$LOCAL_PORT, Listen => 1, Reuse=>1, Timeout=>5); die "startup failed: $!\n" unless $main_socket; my $read_set = new IO::Select(); my $write_set = new IO::Select(); $read_set->add($main_socket); my $start_time = time(); my $ok_count = 0; my $req_count = 0; 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', ); my $WIN32 = ($^O =~ /Win32/); $SIG{'TERM'} = \&sig_term; $SIG{'INT'} = \&sig_term; #$SIG{'HUP'} = \&sig_hup; my $handler_time; my $handler_coderef; my $handler_codeerr; while (1) { #Infinite loop # select() blocks until a socket is ready to be read or written my ($new_readable, $new_writable, $new_error) = IO::Select->select($read_set, $write_set, $read_set, undef); # If it comes here, there is at least one handle # to read from or write to. For the moment, worry only about # the read side. my $sock; foreach $sock (@$new_readable) { if ($sock == $main_socket) { my $new_sock = $sock->accept(); #logmsg("incoming connection from: " , $new_sock->peerhost()); # Add it to the list, and go back to select because the # new socket may not be readable yet. if ($new_sock) { ++$req_count; if ($WIN32) { ioctl($new_sock, 0x8004667e, pack("I", 1)); } else { fcntl($new_sock, F_SETFL(), O_NONBLOCK()); } $read_set->add($new_sock); *$new_sock{HASH}->{time} = time(); } } else { # It is an ordinary client socket, ready for reading. my $nodata = 1; my $buf; my $bytes_read = sysread($sock, $buf, 4096); if ($bytes_read == 0) { $read_set->remove($sock); if (!*$sock{HASH}->{resp}) { logmsg("connection closed before response", $sock); close_sock($sock); } next; } if (*$sock{HASH}->{buf}) { $buf = *$sock{HASH}->{buf} . $buf; delete *$sock{HASH}->{buf}; } my @lines = split(/(?:\r\n|\n)/,$buf,-1); if ($lines[$#lines]) { # partial line *$sock{HASH}->{buf} = $lines[$#lines]; } # last element is either empty or a partial line splice @lines, $#lines, 1; foreach $buf (@lines) { # already queued reponse, discard input next if *$sock{HASH}->{resp}; chomp($buf); if (length($buf) > $MAX_LINE) { http_error($sock, 400, "too much data on request"); close_sock($sock); } elsif (!*$sock{HASH}->{url}) { my ($method, $url, $proto, $garbage) = split(/\s+/,$buf); if ($garbage) { http_error($sock, 400, "garbage request"); } else { #logmsg("mthd=$method, url=$url, prot=$proto", $sock); $url =~ s/%([\dA-Fa-f]{2})/chr(hex($1))/eg; # unescape. *$sock{HASH}->{url} = $url; # note: you can start processing the requests here # instead of waiting for the headers, if you want } } elsif ($buf =~ /^\s*$/) { # blank line triggers processing # or you can trigger it earlier and display an error here process_request($sock); # http_error($sock, 404, "no host specified"); } else { my ($header, $value) = ($buf =~ /^([^: ]+)\s*:\s*(.*)$/); # todo, make the accepted header list configurable # so we store as many headers as we need, and never more if (lc($header) eq 'host') { *$sock{HASH}->{host} = lc($value); # logmsg("Host: " . *$sock{HASH}->{host}); # process_request($sock); } if (lc($header) eq 'referer') { *$sock{HASH}->{referer} = lc($value); } } } } } foreach $sock ($write_set->can_write(0)) { #logmsg("writable"); if (*$sock{HASH}->{close}) { close_sock($sock); } elsif (*$sock{HASH}->{resp}) { my $nb = syswrite($sock, *$sock{HASH}->{resp}, length(*$sock{HASH}->{resp}), *$sock{HASH}->{resp_offset}); if (!$nb) { logmsg("socket failed to write", $sock); close_sock($sock); } else { *$sock{HASH}->{resp_offset}+= $nb; if (*$sock{HASH}->{resp_offset} >= length(*$sock{HASH}->{resp})) { #free buffer delete *$sock{HASH}->{resp}; #logmsg("done writing, close socket on next writable"); *$sock{HASH}->{close} = 1; } } } else { logmsg("no data for response", $sock); close_sock($sock); } } foreach $sock (@$new_error) { die "main socket died" if ($sock == $main_socket); logmsg("error on socket read", $sock); close_sock($sock); } foreach $sock ($read_set->handles()) { next if ($sock == $main_socket); my $duration = (time() - *$sock{HASH}->{time}); if (*$sock{HASH}->{resp}) { if ($duration > $WRITE_TIMEOUT) { logmsg("write timeout", $sock); close_sock($sock); } } else { if ($duration > $READ_TIMEOUT) { #logmsg("read timeout"); close_sock($sock); } } } } sub logmsg { my ($msg, $sock, $code) = @_; my $time = time(); my $localtime = localtime($time); $code = 0 if !$code; my ($url, $ip, $host, $dur); if ($sock) { $url = *$sock{HASH}->{url}; $ip = $sock->peerhost(); $host = *$sock{HASH}->{host}; $dur = $time-*$sock{HASH}->{time}; } print STDERR "$localtime\t$msg\t$ip\t$host\t$url\t$dur\n"; } sub http_error { my ($sock, $code, $detail) = @_; my $msg = "$code " . $codes{$code}; logmsg($detail, $sock, $code); *$sock{HASH}->{resp} .= "HTTP/1.0 $msg\nContent-type: text/html\n\n"; *$sock{HASH}->{resp} .= "HTTPD : $detail : $msg\n"; $write_set->add($sock); # 148.235.173.22 - - [23/May/2005:10:43:32 -0700] "GET /invest.php HTTP/1.1" 301 212 "-" "-" "www.jordantrust.com" } sub process_request { my ($sock) = @_; if (*$sock{HASH}->{url} eq '/STATUS' && *$sock{HASH}->{host} =~ /$STATUS_HOST/i) { *$sock{HASH}->{resp} .= "HTTP/1.0 200 OK\nContent-type: text/plain\n\n"; *$sock{HASH}->{resp} .= "Url : " . *$sock{HASH}->{url} . "\n"; *$sock{HASH}->{resp} .= "Host : " . *$sock{HASH}->{host} . "\n"; *$sock{HASH}->{resp} .= "Readers : " . $read_set->count() . "\n"; *$sock{HASH}->{resp} .= "Writers : " . $write_set->count() . "\n"; my $uptime = (time() - $start_time); my $req_ps = sprintf("%2.2f", 1.0*$req_count/$uptime); my $ok_ps = sprintf("%2.2f", 1.0*$ok_count/$uptime); *$sock{HASH}->{resp} .= "Uptime : " . (time() - $start_time) . " sec\n"; *$sock{HASH}->{resp} .= "Hits All: $req_count ($req_ps/sec)\n"; *$sock{HASH}->{resp} .= "Hits OK : $ok_count ($ok_ps/sec)\n"; } else { my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat($HANDLER); if ($mtime > $handler_time) { open IN, $HANDLER; local $/ = undef; my $code = ; close IN; $handler_coderef = eval "sub {$code};"; if (!$handler_coderef) { $handler_codeerr = $@; $handler_codeerr =~ s/\n/ /g; } $handler_time = $mtime; } if ($handler_coderef) { my $io = IO::String->new(*$sock{HASH}->{resp}); select($io); $ENV{REQUEST_URI} = *$sock{HASH}->{url}; $ENV{REMODE_ADDR} = $sock->peerhost(); $ENV{HTTP_REFERER} = *$sock{HASH}->{referer}; $ENV{HTTP_HOST} = *$sock{HASH}->{host}; &$handler_coderef; logmsg("ok",$sock); ++$ok_count; } else { http_error($sock, 500, "invalid handler ($handler_codeerr)"); } } $write_set->add($sock); #logmsg "added socket to write set"; } sub close_sock { my ($sock) = @_; %{*$sock{HASH}} = (); $read_set->remove($sock); $write_set->remove($sock); close($sock); } sub sig_term { logmsg("terminated by user"); exit(0); } __END__ =head1 NAME ppcgid - This is a pure perl single-threaded trivial httpd server =head1 VERSION This document refers to version 0.92 of ppcgid =head1 REQUIREMENTS It needs the following perl Modules: POSIX IO::Socket IO::Select IO::String LWP::UserAgent CGI CGI::Carp =head1 DESCRIPTION Start from your inittab or init.d. STDERR gets tab-delimited logs. ppcgid 2>/var/log/ppcgid.log& You're responsible for actually handling the HTTP requests by writing a perl CGI application. $HANDLER='/path/to/cgi.pl'; The application should be a valid nph cgi program. Your program should not be written to block on IO, or you will defeat the speed of ppcgid. Creative use of caching and hash tables should help you. Don't bother re-use-ing any of the required modules, since your program is running in the same namespace. In fact, you're way better off use-ing stuff at the top of the ppcgid main, rather than your module. If you want a webserver that you can tweak for your perl web application and can handle a high load, this is fine. It ran 400 requests per second with a very complex CGI program on a 500MHZ box. The same program hung on mod_perl under the same load, so I use ppcgid now for that app. It also gracefully degrades under a load. It also supports multiple timeouts, which all web servers should. =head1 BUGS Report any bug to: 'Erik Aronesty' =head1 AUTHOR Erik Aronesty erik@q32.com =head1 COPYRIGHT License: GNU GPL http://www.fsf.org/copyleft/gpl.html Copyright (C) 2002 Erik Aronesty This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA =cut