File Coverage

blib/lib/POE/Component/Server/HTTP.pm
Criterion Covered Total %
statement 54 279 19.3
branch 0 94 0.0
condition 0 41 0.0
subroutine 18 35 51.4
pod 0 11 0.0
total 72 460 15.6


line stmt bran cond sub pod time code
1             package POE::Component::Server::HTTP;
2 4     4   1169217 use strict;
  4         10  
  4         225  
3 4     4   16766 use Socket qw(inet_ntoa);
  4         10155  
  4         7038  
4 4     4   248500 use HTTP::Date;
  4         295759  
  4         275  
5 4     4   1009 use HTTP::Status;
  4         10405  
  4         1830  
6 4     4   31 use File::Spec;
  4         8  
  4         111  
7 4     4   21 use Exporter ();
  4         8  
  4         102  
8 4     4   19 use vars qw(@ISA @EXPORT $VERSION);
  4         6  
  4         329  
9             @ISA = qw(Exporter);
10              
11 4     4   21 use constant RC_WAIT => -1;
  4         7  
  4         295  
12 4     4   21 use constant RC_DENY => -2;
  4         5  
  4         283  
13             @EXPORT = qw(RC_OK RC_WAIT RC_DENY);
14              
15 4     4   4088 use POE qw(Wheel::ReadWrite Driver::SysRW Session Filter::Stream Filter::HTTPD);
  4         97691  
  4         25  
16 4     4   297770 use POE::Component::Server::TCP;
  4         72514  
  4         178  
17 4     4   54 use Sys::Hostname qw(hostname);
  4         10  
  4         351  
18              
19             $VERSION = "0.09";
20              
21 4     4   3350 use POE::Component::Server::HTTP::Response;
  4         12  
  4         115  
22 4     4   6125 use POE::Component::Server::HTTP::Request;
  4         11  
  4         117  
23 4     4   2460 use POE::Component::Server::HTTP::Connection;
  4         11  
  4         120  
24              
25 4     4   25 use constant DEBUG => 0;
  4         8  
  4         247  
26              
27 4     4   23 use Carp;
  4         8  
  4         19826  
28              
29             my %default_headers = (
30             "Server" => "POE HTTPD Component/$VERSION ($])",
31             );
32              
33             sub new {
34 0     0 0   my $class = shift;
35 0           my $self = bless {@_}, $class;
36 0 0         $self->{Headers} = { %default_headers, ($self->{Headers} ? %{$self->{Headers}}: ())};
  0            
37              
38 0 0         $self->{TransHandler} = [] unless($self->{TransHandler});
39 0 0         $self->{ErrorHandler} = {
40             '/' => \&default_http_error,
41             } unless($self->{ErrorHandler});
42 0 0         $self->{PreHandler} = {} unless($self->{PreHandler});
43 0 0         $self->{PostHandler} = {} unless($self->{PostHandler});
44              
45 0 0         if (ref($self->{ContentHandler}) ne 'HASH') {
46 0 0         croak "You need a default content handler or a ContentHandler setup"
47             unless(ref($self->{DefaultContentHandler}) eq 'CODE');
48 0           $self->{ContentHandler} = {};
49 0           $self->{ContentHandler}->{'/'} = $self->{DefaultContentHandler};
50             }
51 0 0         if (ref $self->{ErrorHandler} ne 'HASH') {
52 0 0         croak "ErrorHandler must be a hashref or a coderef"
53             unless(ref($self->{ErrorHandler}) eq 'CODE');
54 0           $self->{ErrorHandler}={'/' => $self->{ErrorHandler}};
55             }
56              
57             # DWIM on these handlers
58 0           foreach my $phase (qw(PreHandler PostHandler)) {
59             # NOTE: we want the following 2 cases to fall through to the last case
60 0 0         if('CODE' eq ref $self->{$phase}) { # CODE to { / => [ CODE ]}
61 0           $self->{$phase}={'/' => [$self->{$phase}]};
62             }
63 0 0         if('ARRAY' eq ref $self->{$phase}) { # ARRAY to { / => ARRAY }
64 0           $self->{$phase}={'/' => $self->{$phase}};
65             }
66 0 0         if('HASH' eq ref $self->{$phase}) { # check all hash keys
67 0           while(my($path, $todo)=each %{$self->{$phase}}) {
  0            
68 0 0         if('CODE' eq ref $todo) {
69 0           $self->{$phase}{$path}=[$todo];
70 0           next;
71             }
72 0 0         next if 'ARRAY' eq ref $todo;
73 0           croak "$phase\->{$path} must be an arrayref";
74             }
75 0           next;
76             }
77 0           croak "$phase must be a hashref";
78             }
79              
80 0 0         $self->{Hostname} = hostname() unless($self->{Hostname});
81              
82 0           my $alias = "PoCo::Server::HTTP::[ID]";
83 0           my $tcp_alias = $alias . "::TCP";
84             my $session = POE::Session->create(
85             inline_states => {
86             _start => sub {
87 0     0     my $id=$_[SESSION]->ID;
88 0           $alias =~ s/\[ID\]/$id/;
89 0           $tcp_alias =~ s/\[ID\]/$id/;
90 0           $_[KERNEL]->alias_set($alias);
91             },
92 0     0     _stop => sub { },
93             accept => \&accept,
94             input => \&input,
95             execute => \&execute,
96             error => \&error,
97             shutdown => sub {
98 0     0     my ($kernel, $session, $heap) = @_[KERNEL, SESSION, HEAP];
99 0           $kernel->call($tcp_alias, "shutdown");
100 0           $kernel->alias_remove($alias);
101             },
102             },
103 0           heap => { self => $self }
104             );
105              
106              
107             POE::Component::Server::TCP->new(
108             Port => $self->{Port},
109             Address => $self->{Address},
110             Alias => $tcp_alias,
111             Error => sub {
112 0     0     $poe_kernel->post($session, 'error', @_[ARG0..ARG2]);
113             },
114             # ClientError => sub {
115             # $poe_kernel->post($session, 'error', @_[ARG0..ARG2]);
116             # },
117             Acceptor => sub {
118 0     0     $poe_kernel->post($session,'accept',@_[ARG0..ARG2]);
119 0           });
120              
121 0           return { httpd => $alias, tcp => $tcp_alias };
122             }
123              
124             sub handler_queue {
125 0     0 0   return [qw(
126             TransHandler
127             Map
128             PreHandler
129             ContentHandler
130             Send
131             PostHandler
132             Cleanup
133             )];
134             }
135              
136             sub error_queue {
137 0     0 0   return [qw(
138             Map
139             ErrorHandler
140             PostHandler
141             Cleanup
142             )];
143             }
144              
145             # Set up queue for handling this request
146             sub rebuild_queue {
147 0     0 0   my( $self, $handlers) = @_;
148 0           my $now = $handlers->{Queue}[0]; # what phase are we about to do?
149              
150 0 0 0       if (not $now) { # this means we are post Cleanup
    0 0        
    0 0        
      0        
      0        
151             # (which could be keep-alive)
152 0           DEBUG and warn "Error post-Cleanup!";
153             # we need Map to turn set up ErrorHandler
154 0           $handlers->{Queue} = ['Map', 'ErrorHandler', 'Cleanup'];
155             # Note : sub error set up fake request/response objects, etc
156             }
157             elsif ($now eq 'TransHandler' or $now eq 'Map' or
158             $now eq 'PreHandler' or $now eq 'ContentHandler' or
159             $now eq 'Send' or $now eq 'PostHandler') {
160              
161 0           $handlers->{Queue}=$self->error_queue;
162             }
163             elsif ($now eq 'Cleanup') {
164             # we need Map to turn set up ErrorHandler
165 0           unshift @{$handlers->{Queue}}, 'Map', 'ErrorHandler';
  0            
166             }
167              
168             # clear these lists, so that Map builds new ones
169 0           $handlers->{PostHandler} = [];
170 0           $handlers->{PreHandler} = [];
171             }
172              
173             sub accept {
174 0     0 0   my ($socket,$remote_addr, $remote_port) = @_[ARG0, ARG1, ARG2];
175 0           my $self = $_[HEAP]->{self};
176 0           my $connection = POE::Component::Server::HTTP::Connection->new();
177 0           $connection->{remote_ip} = inet_ntoa($remote_addr);
178 0           $connection->{remote_addr} = getpeername($socket);
179 0           $connection->{local_addr} = getsockname($socket);
180              
181 0           $connection->{handlers} = {
182 0           TransHandler => [@{$self->{TransHandler}}],
183             PreHandler => [],
184             ContentHandler => undef,
185             PostHandler => [],
186             # IMHO, Queue should be set in 'input' --PG
187             Queue => $self->handler_queue,
188             };
189              
190 0           my $wheel = POE::Wheel::ReadWrite->new(
191             Handle => $socket,
192             Driver => POE::Driver::SysRW->new,
193             Filter => POE::Filter::HTTPD->new(),
194             InputEvent => 'input',
195             FlushedEvent => 'execute',
196             ErrorEvent => 'error'
197             );
198 0           DEBUG and warn "Accept remote_ip=$connection->{remote_ip} id=", $wheel->ID;
199              
200 0           $_[HEAP]->{wheels}->{$wheel->ID} = $wheel;
201 0           $_[HEAP]->{c}->{$wheel->ID} = $connection
202             }
203              
204              
205             sub input {
206 0     0 0   my ($request,$id) = @_[ARG0, ARG1];
207              
208 0           DEBUG and warn "Input id=$id uri=", $request->uri->as_string;
209 0           bless $request, 'POE::Component::Server::HTTP::Request';
210 0           my $c = $_[HEAP]->{c}->{$id};
211 0           my $self = $_[HEAP]->{self};
212              
213 0 0         if ($request->uri) {
214 0           $request->uri->scheme('http');
215 0           $request->uri->host($self->{Hostname});
216 0           $request->uri->port($self->{Port});
217             }
218 0           $request->{connection} = $c;
219              
220 0           my $response = POE::Component::Server::HTTP::Response->new();
221              
222 0           $response->{connection} = $c;
223              
224 0           $c->{wheel} = $_[HEAP]->{wheels}->{$id};
225              
226 0           $c->{request} = $request;
227 0           $c->{response} = $response;
228 0           $c->{session} = $_[SESSION];
229 0           $c->{my_id} = $id;
230 0           $poe_kernel->yield('execute',$id);
231             }
232              
233             sub error {
234 0     0 0   my ($op, $errnum, $errstr, $id) = @_[ARG0..ARG3];
235 0 0         unless ( $_[HEAP]->{c}{$id} ) {
236 0           warn "Error $op $errstr ($errnum) happened after Cleanup!\n";
237 0           return;
238             }
239 0           my $c = $_[HEAP]->{c}->{$id};
240 0           my $self = $_[HEAP]->{self};
241              
242 0           DEBUG and warn "$$: HTTP error op=$op errnum=$errnum errstr=$errstr id=$id\n";
243 0 0 0       if ($op eq 'accept') {
    0          
244 0           die "$$: HTTP error op=$op errnum=$errnum errstr=$errstr id=$id\n";
245             }
246             elsif ($op eq 'read' or $op eq 'write') {
247             # connection closed or other error
248              
249             ## Create some temporary objects if needed
250 0 0         unless($c->{request}) {
251 0           my $request = POE::Component::Server::HTTP::Request->new(
252             ERROR => '/'
253             );
254 0           $request->{connection} = $c;
255 0           $c->{request}=$request;
256             }
257 0           $c->{request}->header(Operation => $op);
258 0           $c->{request}->header(Errnum => $errnum);
259 0           $c->{request}->header(Error => $errstr);
260              
261 0 0         unless ($c->{response}) {
262 0           my $response = POE::Component::Server::HTTP::Response->new();
263 0           $response->{connection} = $c;
264 0           $c->{response}=$response;
265             }
266 0   0       $c->{session} ||= $_[SESSION];
267 0   0       $c->{my_id} ||= $id;
268 0   0       $c->{wheel} ||= $_[HEAP]{wheels}{$id};
269              
270             # mark everything hence forth as an error
271 0           $c->{request}->is_error(1);
272 0           $c->{response}->is_error(1);
273              
274             # and rebuild the queue
275 0           $self->rebuild_queue($c->{handlers});
276 0           $poe_kernel->yield('execute',$id);
277             }
278             }
279              
280             sub default_http_error {
281 0     0 0   my ($request, $response) = @_;
282              
283 0           my $op = $request->header('Operation');
284 0           my $errstr = $request->header('Error');
285 0           my $errnum = $request->header('Errnum');
286 0 0 0       return if $errnum == 0 and $op eq 'read'; # socket closed
287              
288 0           warn "Error during HTTP $op: $errstr ($errnum)\n";
289             }
290              
291              
292             sub execute {
293 0     0 0   my $id = $_[ARG0];
294 0           my $self = $_[HEAP]->{self};
295 0           my $connection = $_[HEAP]->{c}->{$id};
296 0           my $handlers = $connection->{handlers};
297              
298 0           my $response = $connection->{response};
299 0           my $request = $connection->{request};
300              
301 0           my $state;
302             HANDLERS:
303 0           while (1) {
304 0           $state = $handlers->{Queue}->[0];
305 0           DEBUG and warn "Execute state=$state id=$id";
306              
307 0 0 0       if ($state eq 'Map') {
    0          
    0          
    0          
    0          
308 0 0         $self->state_Map( $request->uri ? $request->uri->path : '',
309             $handlers, $request );
310 0           shift @{$handlers->{Queue}};
  0            
311 0           next;
312             }
313             elsif ($state eq 'Send') {
314 0           $self->state_Send( $response, $_[HEAP]->{wheels}->{$id} );
315 0           shift @{$handlers->{Queue}};
  0            
316 0           last;
317             }
318             elsif ($state eq 'ContentHandler' or
319             $state eq 'ErrorHandler') {
320             # this empty sub should really make a 404
321 0   0 0     my $sub = $handlers->{ $state } || sub {};
  0            
322              
323             # XXX: we should wrap this in an eval and return 500
324 0           my $retvalue = $sub->($request, $response);
325 0           shift @{$handlers->{Queue}};
  0            
326 0 0         if ($retvalue == RC_WAIT) {
327 0 0         if( $state eq 'ErrorHandler') {
328 0           warn "ErrorHandler is not allowed to return RC_WAIT";
329             }
330             else {
331 0           last HANDLERS;
332             }
333             }
334 0           next;
335             }
336             elsif ($state eq 'Cleanup') {
337 0 0 0       if (not $response->is_error and $response->streaming()) {
338 0           $_[HEAP]->{wheels}->{$id}->set_output_filter(POE::Filter::Stream->new() );
339 0           unshift(@{$handlers->{Queue}},'Streaming');
  0            
340 0           next HANDLERS;
341             }
342              
343 0           delete($response->{connection});
344 0           delete($request->{connection});
345              
346             # under HTTP/1.1 connections are always kept alive, unless
347             # there's a Connection: close present
348 0           my $close = 1;
349 0 0         if ( $request->protocol eq 'HTTP/1.1' ) {
350 0           $close = 0; # keepalive
351             # It turns out the connection field can contain multiple
352             # comma separated values
353 0           my $conn = $request->header('Connection');
354 0 0         $close = 1 if qq(,$conn,) =~ /,\s*close\s*,/i;
355             }
356              
357 0 0         unless ($close) {
358 0           DEBUG and warn "Keepalive connection still active";
359             # Breaking encapsulation causes immolation --richardc
360             # We'll need a new POE::Filter::HTTPD
361 0           $_[HEAP]{wheels}{$id}[2] = (ref $_[HEAP]{wheels}{$id}[2])->new;
362              
363             # IMHO, Queue should be set in 'input' --PG
364 0           $handlers->{Queue} = $self->handler_queue;
365             }
366             else {
367 0           DEBUG and warn "Close connection";
368 0           delete($connection->{handlers});
369 0           delete($connection->{wheel});
370 0           delete($_[HEAP]->{c}->{$id});
371 0           delete($_[HEAP]->{wheels}->{$id});
372             }
373 0           last HANDLERS;
374             }
375             elsif ($state eq 'Streaming') {
376 0           $self->{StreamHandler}->($request, $response);
377 0           last HANDLERS;
378             }
379              
380             DISPATCH: # this is used for {Trans,Pre,Post}Handler
381 0           while (1) {
382 0           my $handler = shift(@{$handlers->{$state}});
  0            
383 0 0         last DISPATCH unless($handler);
384 0           my $retvalue = $handler->($request,$response);
385              
386 0 0         if ($retvalue == RC_DENY) {
    0          
387 0           last DISPATCH;
388             }
389             elsif ($retvalue == RC_WAIT) {
390 0           last HANDLERS;
391             }
392             }
393              
394 0           shift @{$handlers->{Queue}};
  0            
395 0 0         last unless(0 != @{$handlers->{Queue}});
  0            
396             }
397             }
398              
399             sub state_Map {
400 0     0 0   my $self = shift;
401 0           my $path = shift;
402 0           my $handlers = shift;
403 0           my $request = shift;
404 0           my $filename;
405 0           (undef, $path,$filename) = File::Spec->splitpath($path);
406 0           my @dirs = File::Spec->splitdir($path);
407 0           pop @dirs;
408              
409 0           DEBUG and warn "dirs=", join ',', @dirs;
410              
411 0           my @check;
412             my $fullpath;
413 0           foreach my $dir (@dirs) {
414 0           $fullpath .= $dir.'/';
415 0           push @check, $fullpath;
416             }
417              
418 0 0         push(@check, "$check[-1]$filename") if($filename);
419              
420 0           DEBUG and warn "check=", join ',', @check;
421              
422 0           my @todo;
423 0 0         unless ($request->is_error) {
424 0           @todo=qw(PreHandler ContentHandler PostHandler);
425             }
426             else {
427 0           @todo=qw(ErrorHandler PostHandler);
428             }
429              
430 0           foreach my $path (@check) {
431 0           foreach my $phase (@todo) {
432 0 0         next unless exists($self->{$phase}->{$path});
433 0 0         if ('ARRAY' eq ref $self->{$phase}{$path}) {
434 0           push @{$handlers->{$phase}}, @{$self->{$phase}->{$path}};
  0            
  0            
435             }
436             else {
437 0           $handlers->{$phase}=$self->{$phase}->{$path};
438             }
439             }
440             }
441 0           require Data::Dumper if DEBUG;
442 0           DEBUG and warn "Map ", Data::Dumper::Dumper( $handlers );
443             }
444              
445             sub state_Send {
446 0     0 0   my $self = shift;
447 0           my $response = shift;
448 0           my $wheel = shift;
449              
450 0           $response->header(%{$self->{Headers}});
  0            
451 0 0         unless ($response->header('Date')) {
452 0           $response->header('Date',time2str(time));
453             }
454 0 0 0       if (!($response->header('Content-Lenth')) && !($response->streaming())) {
455 4     4   42 use bytes;
  4         10  
  4         38  
456 0           $response->header('Content-Length',length($response->content));
457             }
458              
459 0           $wheel->put($response);
460             }
461              
462             1;
463             __END__