File Coverage

blib/lib/HTTP/Server/Singlethreaded.pm
Criterion Covered Total %
statement 67 258 25.9
branch 19 122 15.5
condition 0 6 0.0
subroutine 12 17 70.5
pod 0 5 0.0
total 98 408 24.0


line stmt bran cond sub pod time code
1             package HTTP::Server::Singlethreaded;
2              
3             BEGIN{
4 1 50   1   5647 eval ( $ENV{OS}=~/win/i ? <
5              
6             sub BROKENSYSWRITE(){1}
7              
8             WIN
9              
10             sub BROKENSYSWRITE(){0}
11              
12             NOTWIN
13              
14             }
15              
16              
17 1     1   21 use 5.006;
  1         3  
  1         49  
18 1     1   5 use strict;
  1         2  
  1         27  
19 1     1   9 use warnings;
  1         2  
  1         50  
20 1         384 use vars qw/
21              
22             %Static
23             %Function
24             %CgiBin
25             %Path
26              
27              
28             $DefaultMimeType
29             %MimeType
30              
31             @Port
32             $Timeout
33             $MaxClients
34             $ServerType
35             $VERSION
36             $RequestTally
37             $uid $gid $forkwidth @kids
38             $WebEmail
39             $StaticBufferSize
40             @Cport
41             @Caddr
42             @Sport
43             @Saddr
44 1     1   5 /;
  1         1  
45              
46             sub DEBUG() {
47             # 1
48             0
49             };
50              
51             $RequestTally = 0;
52             $StaticBufferSize ||= 50000;
53              
54             # file number of request
55             my $fn;
56             # arrays indexed by $fn
57             my @Listeners; # handles to listening sockets
58             my @PortNo; # listening port numbers indexed by $fn
59             my @Clients; # handles to client sockets
60             my @inbuf; # buffered information read from clients
61             my @outbuf; # buffered information for writing to clients
62             my @LargeFile; # handles to large files being read, indexed by
63             # $fn of the client they are being read for
64             my @continue; # is there a continuation defined for this fn?
65             my @PostData; # data for POST-style requests
66              
67             #lists of file numbers
68             my @PollMe; #continuation functions associated with empty output buffers
69              
70             $VERSION = '0.12';
71              
72             # default values:
73             $ServerType ||= __PACKAGE__." $VERSION (Perl $])";
74             @Port or @Port = (80,8000);
75             $Timeout ||= 5;
76             $MaxClients ||= 10;
77             $DefaultMimeType ||= 'text/plain';
78             keys(%MimeType ) or
79             @MimeType{qw/txt htm html jpg gif png/} =
80             qw{text/plain text/html text/html image/jpeg image/gif image/png};
81              
82             sub Serve();
83             # use IO::Socket::INET;
84 1     1   1057 use Socket qw(:DEFAULT :crlf);
  1         3657  
  1         938  
85             BEGIN{
86 1     1   7 use Fcntl;
  1         2  
  1         252  
87             # determine if O_NONBLOCK is available,
88             # for use in fcntl($l, F_SETFL, O_NONBLOCK)
89 1     1   2 eval{
90             # print "O_NONBLOCK is ",O_NONBLOCK,
91             # " and F_SETFL is ",F_SETFL,"\n";
92 1     1   9 no warnings; O_NONBLOCK; F_SETFL;
  1         2  
  1         93  
  1         2  
  1         2  
93             };
94 1 50       4 if ($@){
95 0         0 warn "O_NONBLOCK is broken, but a workaround is in place.\n";
96 0         0 eval'sub BROKEN_NONBLOCKING(){1}';
97             }else{
98 1         716 eval'sub BROKEN_NONBLOCKING(){0}';
99             };
100             }
101              
102             sub makeref($){
103 0 0   0 0 0 ref($_[0]) ? $_[0] : \$_[0]
104             };
105              
106             sub import(){
107              
108 1     1   11 DEBUG and print __PACKAGE__," import called\n";
109              
110 1         2 shift; # we don't need to know __PACKAGE__
111              
112             # DYNAMIC RECONFIGURATION SECTION
113 1         2 my %args = @_;
114 1         2 DEBUG and do{
115             print "$_ is $args{$_}\n" foreach sort keys %args
116              
117             };
118 1 50       5 exists $args{port} and *Port = $args{port};
119 1 50       3 exists $args{timeout} and *Timeout = $args{timeout};
120 1 50       3 exists $args{maxclients} and *MaxClients = $args{maxclients};
121 1 50       3 exists $args{static} and *Static = $args{static};
122 1 50       11 exists $args{function} and *Function = $args{function};
123 1 50       4 exists $args{cgibin} and *CgiBin = $args{cgibin};
124 1 50       3 exists $args{servertype} and *ServerType = $args{servertype};
125 1 50       4 exists $args{webemail} and *WebEmail = makeref($args{webemail});
126 1 50       4 exists $args{path} and *Path = $args{path};
127              
128 1 50       4 @Port or die __PACKAGE__." invoked with empty \@Port array";
129              
130 1         2 @Listeners = ();
131 1         2 for (@Port) {
132 2         3 my $l;
133 2 50       963 socket($l, PF_INET, SOCK_STREAM,getprotobyname('tcp'))
134             || die "socket: $!";
135 2         3 unless (BROKEN_NONBLOCKING){
136 2 50       12 fcntl($l, F_SETFL, O_NONBLOCK)
137             || die "can't set non blocking: $!";
138             };
139 2 50       14 setsockopt($l, SOL_SOCKET,
140             SO_REUSEADDR,
141             pack("l", 1))
142             || die "setsockopt: $!";
143 0         0 bind($l, sockaddr_in($_, INADDR_ANY))
144 2 50       8 || do {warn "bind: $!";next};
  0         0  
145 2 50       54 listen($l,SOMAXCONN)
146             || die "listen: $!";
147 2 50       5 if (defined $l){
148 2         57 print "bound listener to $_\n";
149 2         6 $PortNo[fileno($l)] = $_;
150 2         11 push @Listeners,$l;
151             }else{
152 0         0 print "Could not bind listener to $_\n";
153             };
154             } ;
155              
156 1 50       4 @Listeners or die __PACKAGE__." could not bind any listening sockets among @Port";
157              
158             ###########################################################################
159             # uncomment the following if so desired
160             ###########################################################################
161             # if($defined $uid){
162             # $> = $< = $uid
163             # };
164             #
165             # if($defined $gid){
166             # $) = $( = $gid
167             # };
168             #
169             # if($defined $forkwidth){
170             # my $pid; my $i=0;
171             # while (++$i < $forkwidth){
172             # $pid = fork or last;
173             # unshift @kids, $pid
174             # };
175             # unless($kids[0] ){
176             # @kids=();
177             # };
178             # $forkwidth = "$i of $forkwidth";
179             # };
180             # END{ kill 'TERM', $_ for @kids };
181             ############################################################################
182              
183              
184              
185 1         3 for (keys %Function){
186 0 0       0 die "$Function{$_} is not a coderef"
187             unless (ref $Function{$_} eq 'CODE');
188 0         0 $Path{$_} = $Function{$_};
189             }
190 1         3 for (keys %Static){
191 0 0       0 die "path $_ already defined" if exists $Path{$_};
192 0         0 $Path{$_} = "STATIC $Static{$_}";
193             }
194 1         2 for (keys %CgiBin){
195 0 0       0 die "path $_ already defined" if exists $Path{$_};
196 0         0 $Path{$_} = "CGI $CgiBin{$_}";
197             }
198              
199             {
200             # import Serve into caller's package
201 1     1   6 no strict;
  1         2  
  1         1296  
  1         3  
202 1         2 *{caller().'::Serve'} = \&Serve;
  1         1710  
203             }
204              
205              
206             };
207              
208             my %RCtext =(
209             100=> 'Continue',
210             101=> 'Switching Protocols',
211             200=> 'OK',
212             201=> 'Created',
213             202=> 'Accepted',
214             203=> 'Non-Authoritative Information',
215             204=> 'No Content',
216             205=> 'Reset Content',
217             206=> 'Partial Content',
218             300=> 'Multiple Choices',
219             301=> 'Moved Permanently',
220             302=> 'Found',
221             303=> 'See Other',
222             304=> 'Not Modified',
223             305=> 'Use Proxy',
224             306=> '(Unused)',
225             307=> 'Temporary Redirect',
226             400=> 'Bad Request',
227             401=> 'Unauthorized',
228             402=> 'Payment Required',
229             403=> 'Forbidden',
230             404=> 'Not Found',
231             405=> 'Method Not Allowed',
232             406=> 'Not Acceptable',
233             407=> 'Proxy Authentication Required',
234             408=> 'Request Timeout',
235             409=> 'Conflict',
236             410=> 'Gone',
237             411=> 'Length Required',
238             412=> 'Precondition Failed',
239             413=> 'Request Entity Too Large',
240             414=> 'Request-URI Too Long',
241             415=> 'Unsupported Media Type',
242             416=> 'Requested Range Not Satisfiable',
243             417=> 'Expectation Failed',
244             500=> 'Internal Server Error',
245             501=> 'Not Implemented',
246             502=> 'Bad Gateway',
247             503=> 'Service Unavailable',
248             504=> 'Gateway Timeout',
249             505=> 'HTTP Version Not Supported'
250             );
251              
252              
253             our @Moustache; # per-fn %_ references
254              
255             sub dispatch(){
256             # based on the request, which is in $_,
257             # figure out what to do, and do it.
258             # return a numeric resultcode in $ResultCode
259             # and data in $Data
260              
261 0     0 0   if(DEBUG){
262             print "Request on fn $fn:\n${_}END_REQUEST\n";
263             };
264              
265             # defaults:
266 0           *_ = $Moustache[$fn] = {
267             Data => undef,
268             ResultCode => 200
269             };
270              
271 0           $continue[$fn] = undef;
272              
273             # rfc2616 section 5.1
274             /^(\w+) (\S+) HTTP\/(\S+)\s*(.*)$CRLF$CRLF/s
275 0 0         or do { $_{ResultCode} = 400;
  0            
276 0           return <
277             Content-type:text/plain
278              
279             This server only accepts requests that
280             match the perl regex
281             /^(\\w+) (\\S+) HTTP\\/(\\S+)/
282              
283             EOF
284             };
285 0           @_{qw/
286             REQUEST_METHOD REQUEST_URI HTTPver RequestHeader
287             REMOTE_ADDR REMOTE_PORT SERVER_ADDR SERVER_PORT/
288             } = (
289             $1,$2,$3,$4,
290             $Caddr[$fn], $Cport[$fn],
291             $Saddr[$fn], $Sport[$fn]
292             );
293 0           if(DEBUG){for( sort keys %_ ){
294             print "$_ is $_{$_}\n";
295             }};
296              
297             # REQUEST_URI is
298             # equivalent to SCRIPT_NAME . PATH_INFO . '?' . QUERY_STRING
299              
300 0           my $shortURI;
301 0           ($shortURI ,$_{QUERY_STRING}) = $_{REQUEST_URI}=~m#(/[^\?]*)\??(.*)$#;
302 0           $shortURI =~ s/%(..)/chr hex $1/ge; # RFC2616 sec. 3.2
  0            
303 0 0         if (uc($_{REQUEST_METHOD}) eq 'POST'){
304 0           $_{POST_DATA} = $PostData[$fn];
305             };
306              
307 0           my @URIpath = split '/',$shortURI,-1;
308 0           my @Castoffs;
309             my $mypath;
310 0           while (@URIpath){
311 0           $mypath = join '/',@URIpath;
312 0           DEBUG and warn "considering $mypath\n";
313 0 0         if (exists $Path{$mypath}){
314 0           $_{SCRIPT_NAME} = $mypath;
315 0           print "PATH $mypath is $Path{$mypath}";
316 0           $_{PATH_INFO} = join '/', @Castoffs;
317 0           print " and PATH_INFO is $_{PATH_INFO}\n";
318 0 0         if (ref $Path{$mypath}){
319 0           my $DynPage;
320 0           eval {
321 0           $DynPage = &{$Path{$mypath}};
  0            
322             };
323 0 0         $@ or return $DynPage;
324 0           $_{ResultCode} = 500;
325 0           return <
326             Content-type:text/plain
327              
328             Internal server error while processing routine
329             for $mypath:
330              
331             $@
332             EOF
333             };
334 0 0         if ($Path{$mypath} =~/^STATIC (.+)/){
335 0           my $FILE;
336 0           my $filename = "$1/$_{PATH_INFO}";
337 0           print "filename: $filename\n";
338 0           $filename =~ s/\/\.\.\//\//g; # no ../../ attacks
339 0           my ($ext) = $filename =~ /\.(\w+)$/;
340 0   0       my $ContentType = $MimeType{$ext}||$DefaultMimeType;
341             # unless (-f $filename and -r _ ){
342 0 0         unless(open $FILE, "<", $filename){
343 0           $_{ResultCode} = 404;
344 0           return <
345             Content-type: text/plain
346              
347             Could not open $filename for reading
348             $!
349              
350             for $mypath: $Path{$mypath}
351              
352             Request:
353              
354             $_
355              
356             EOF
357             };
358             # range will go here when supported
359 0           my $size = -s $filename;
360 0           my $slurp;
361 0           my $read = sysread $FILE, $slurp, $StaticBufferSize ;
362              
363 0 0         if ($read < $size){
364 0           $LargeFile[$fn] = $FILE;
365             };
366              
367 0           return "Content-type: $ContentType\n\n$slurp";
368              
369             };
370 0           $_{ResultCode} = 404;
371 0           return <
372             Content-type:text/plain
373              
374             This version of Singlethreaded does not understand
375             how to serve
376              
377             $mypath
378              
379             $Path{$mypath}
380              
381             Responsible person: $WebEmail
382              
383             We received this request:
384              
385             $_
386              
387             EOF
388             };
389 0 0         if((length $URIpath[$#URIpath]) > 0){
390 0           unshift @Castoffs, pop @URIpath;
391             }else{
392 0           $URIpath[$#URIpath] = '/'
393             };
394             };
395              
396              
397 0           $_{ResultCode} = 404;
398 0           <
399             Content-type:text/plain
400              
401             $$ $RequestTally handling fileno $fn
402              
403             apparently this Singlethreaded server does not
404             have a default handler installed at its
405             virtual root.
406              
407             Castoffs: [@Castoffs]
408              
409             Responsible person: [$WebEmail]
410              
411             $_
412              
413             EOF
414              
415             };
416              
417              
418             sub HandleRequest(){
419 0     0 0   $RequestTally++;
420 0           print "Handling request $RequestTally on fn $fn\n";
421 0           DEBUG and warn "Inbuf:\n$inbuf[$fn]\n";
422 0           *_ = \delete $inbuf[$fn]; # tight, huh? (the scalar slot)
423            
424 0           my $dispatchretval = dispatch;
425 0 0         $dispatchretval or return undef;
426 0           $outbuf[$fn]=<
427             HTTP/1.1 $_{ResultCode} $RCtext{$_{ResultCode}}
428             Server: $ServerType
429             Connection: close
430             EOF
431             # *_ = $Moustache[$fn]; # also, the hash slot -- this is done in &dispatch, never mind
432 0           HandleDRV($dispatchretval);
433 0           DEBUG and warn "Outbuf:\n$outbuf[$fn]\n";
434             };
435             sub HandleDRV{
436 0     0 0   my $dispatchretval = shift;
437 0 0         @_ and $dispatchretval = [$dispatchretval,shift]; # support old-style
438 0           $continue[$fn] = undef;
439 1 0   1   7 { no warnings; length $_{Data} and $outbuf[$fn] .= $_{Data}; }
  1         2  
  1         1852  
  0            
  0            
440 0 0         if(ref($dispatchretval)){
441 0           $continue[$fn] = $dispatchretval;
442              
443             }else{
444 0           $outbuf[$fn].=$dispatchretval
445             }
446              
447             };
448              
449             my $client_tally = 0;
450             sub Serve(){
451 0     0 0   DEBUG and print "L: (@Listeners) C: (@Clients)\n";
452 0           my ($rin,$win,$ein,$rout,$wout,$eout);
453 0           my $nfound;
454              
455 0           BEGIN_SERVICE:
456              
457             # support for continuation coderefs to empty outbufs
458             @PollMe = grep {
459 0           $fn = $_;
460 0           DEBUG and warn "polling $_";
461 0 0         if ( $continue[$_] ) {
462 0           *_ = $Moustache[$_]; # the hash slot
463 0           DEBUG and warn "still working with $_";
464 0           $_{Data} = '';
465 0           HandleDRV( &{$continue[$_]} );
  0            
466 0           $continue[$_];
467             }
468             } @PollMe;
469              
470              
471              
472             # poll for new connections?
473 0           my $Accepting = ($client_tally < $MaxClients);
474 0           $rin = $win = $ein = '';
475 0 0         if($Accepting){
476 0           for(@Listeners){
477 0           $fn = fileno($_);
478 0           vec($rin,$fn,1) = 1;
479 0           vec($win,$fn,1) = 1;
480 0           vec($ein,$fn,1) = 1;
481             };
482             };
483              
484              
485 0           my @Outs;
486             my @CompleteRequests;
487             # list all clients in $ein and $rin
488             # list connections with pending outbound data in $win;
489 0           for(@Clients){
490 0           $fn = fileno($_);
491 0           vec($rin,$fn,1) = 1;
492 0           vec($ein,$fn,1) = 1;
493 0 0         if( length $outbuf[$fn]){
494 0           vec($win,$fn,1) = 1;
495 0           push @Outs, $_;
496             }
497             };
498              
499             # Select.
500 0           $nfound = select($rout=$rin, $wout=$win, $eout=$ein, $Timeout);
501 0 0         $nfound > 0 or return;
502 0           my $Services = 0; # goes true when writing outbound bytes
503             # accept new connections
504 0 0         if($Accepting){
505 0           for(@Listeners){
506 0           my $paddr;
507 0 0         vec($rout,fileno($_),1) or next;
508             # relies on listeners being nonblocking
509             # thanks, thecap
510             # (at http://www.perlmonks.org/index.pl?node_id=6535)
511             #BLAH if (BROKEN_NONBLOCKING){ # this is a constant so the unused one
512             #BLAH # will be optimized away
513             #BLAH acc:
514             #BLAH $paddr=accept(my $NewServer, $_);
515             #BLAH if ($paddr){
516             #BLAH $fn =fileno($NewServer);
517             #BLAH ($Cport[$fn], my $iaddr) = sockaddr_in($paddr);
518             #BLAH $Caddr[$fn] = inet_ntoa($iaddr);
519             #BLAH $inbuf[$fn] = $outbuf[$fn] = '';
520             #BLAH print "Accepted $NewServer (",
521             #BLAH $fn,") ",
522             #BLAH ++$client_tally,
523             #BLAH "/$MaxClients on $_ ($fn) port $PortNo[fileno($_)]\n";
524             #BLAH push @Clients, $NewServer;
525             #BLAH
526             #BLAH
527             #BLAH }
528             #BLAH
529             #BLAH # select again to see if there's another
530             #BLAH # client enqueued on $_
531             #BLAH my $rvec;
532             #BLAH vec($rvec,fileno($_),1) = 1;
533             #BLAH select($rvec,undef,undef,0);
534             #BLAH vec($rvec,fileno($_),1) and goto acc;
535             #BLAH
536             #BLAH }else{ # WORKING NON_BLOCKING
537 0           while ($paddr=accept(my $NewServer, $_)){
538 0           $fn =fileno($NewServer);
539 0           $continue[$fn] = undef;
540 0           $Moustache[$fn] = {};
541 0           $inbuf[$fn] = $outbuf[$fn] = '';
542 0           ($Cport[$fn], my $iaddr) = sockaddr_in($paddr);
543 0           $Caddr[$fn] = inet_ntoa($iaddr);
544              
545 0           my $mysockaddr = getsockname($NewServer);
546 0           ($Sport[$fn], $iaddr) = sockaddr_in($mysockaddr);
547 0           $Saddr[$fn] = inet_ntoa($iaddr);
548              
549 0           print "Accepted $NewServer (",
550             $fn,") ",
551             ++$client_tally,
552             "/$MaxClients on $_ ($fn) port $PortNo[fileno($_)]\n";
553 0           push @Clients, $NewServer;
554              
555 0           BROKEN_NONBLOCKING and last; # much simpler
556             }
557             #BLAH }
558             }
559             } # if accepting connections
560              
561             # Send outbound data from outbufs
562 0           my $wlen;
563 0           for my $OutFileHandle (@Outs){
564 0           $fn = fileno($OutFileHandle);
565 0 0 0       ((defined $fn) and vec($wout,$fn,1)) or next;
566 0           $Services++;
567 0           $wlen = syswrite $OutFileHandle, $outbuf[$fn], (BROKENSYSWRITE ? 1 : length($outbuf[$fn]));
568 0 0         if(defined $wlen){
569 0           DEBUG and print "wrote $wlen of ",length($outbuf[$fn])," to ($fn)\n";
570 0           substr $outbuf[$fn], 0, $wlen, '';
571            
572 0 0         if(
573             length($outbuf[$fn]) < $StaticBufferSize
574             ){
575             # then we would like to add some more to our outbuf
576 0 0         if(
    0          
577             # support for chunking large files (not HTTP1.1 chunking, just
578             # reading as we go
579             defined($LargeFile[$fn])
580             ){
581 0           my $slurp;
582 0           my $read = sysread $LargeFile[$fn], $slurp, $StaticBufferSize ;
583             # zero for EOF and undef on error
584 0 0         if ($read){
585 0           $outbuf[$fn].= $slurp;
586             }else{
587 0 0         print "sysread error: $!" unless defined $read;
588 0           delete $LargeFile[$fn];
589             };
590             }elsif(
591             # support for continuation coderefs
592             $continue[$fn]
593             ){
594 0           *_ = $Moustache[$fn]; # the hash slot
595 0           $_{Data} = '';
596 0           HandleDRV( &{$continue[$fn]} );
  0            
597 0 0         length ($outbuf[$fn]) or push @PollMe, $fn;
598 0           next;
599             };
600             }
601             }else{
602 0           warn "Error writing to socket $OutFileHandle ($fn): $!";
603 0           $outbuf[$fn] = '';
604             }
605              
606             # rewrite this when adding keepalive support
607 0 0         length($outbuf[$fn]) or close $OutFileHandle;
608             }
609              
610             # read incoming data to inbufs and list inbufs with complete requests
611             # close bad connections
612 0           for(@Clients){
613 0 0         defined($fn = fileno($_)) or next;
614 0 0         if(vec($rout,$fn,1)){
615              
616 0           my $char;
617 0           sysread $_,$char,64000;
618 0 0         if(length $char){
619 0           DEBUG and print "$fn: read [$char]\n";
620 0           $inbuf[$fn] .= $char;
621             # CompleteRequest or not?
622 0 0         if($inbuf[$fn] =~
    0          
623             /^POST .*?Content-Length: ?(\d+)[\015\012]+(.*)$/is){
624 0           DEBUG and print "posting $1 bytes\n";
625 0 0         if(length $2 >= $1){
626 0           push @CompleteRequests, $fn;
627 0           $PostData[$fn] = $2;
628             }else{
629 0           if(DEBUG){
630             print "$fn: Waiting for $1 octets of POST data\n";
631             print "$fn: only have ",length($2),"\n";
632             }
633             }
634             }elsif(substr($inbuf[$fn],-4,4) eq "\015\012\015\012"){
635 0           push @CompleteRequests, $fn;
636 0           }elsif(DEBUG){
637             print "Waiting for request completion. So far have\n[",
638             $inbuf[$fn],"]\n";
639              
640             };
641             }else{
642 0           print "Received empty packet on $_ ($fn)\n";
643 0           print "CLOSING fd $fn\n";
644 0 0         close $_ or print "error on close: $!\n";
645 0           $client_tally--;
646 0           print "down to $client_tally / $MaxClients\n";
647             };
648             }
649 0 0         if(vec($eout,$fn,1)){
650             # close this one
651 0           print "error on $_ ($fn)\n";
652 0           print "CLOSING fd $fn\n";
653 0 0         close $_ or print "error on close: $!\n";
654             };
655             }
656              
657             # prune @Clients array
658              
659 0           @Clients = grep { defined fileno($_) } @Clients;
  0            
660 0           $client_tally = @Clients;
661 0           DEBUG and print "$client_tally / $MaxClients\n";
662              
663             # handle complete requests
664             # (outbound data will get written next time)
665 0           for $fn (@CompleteRequests){
666              
667 0           HandleRequest
668              
669             };
670              
671 0 0         $Services and goto BEGIN_SERVICE; # keep selecting while we actually do something
672              
673              
674             };
675              
676              
677              
678              
679             1;
680             __END__