File Coverage

blib/lib/IOMux/HTTP/Service.pm
Criterion Covered Total %
statement 27 127 21.2
branch 0 44 0.0
condition 0 11 0.0
subroutine 9 25 36.0
pod 7 13 53.8
total 43 220 19.5


line stmt bran cond sub pod time code
1             # Copyrights 2011 by Mark Overmeer.
2             # For other contributors see ChangeLog.
3             # See the manual pages for details on the licensing terms.
4             # Pod stripped from pm file by OODoc 1.07.
5 1     1   1267 use warnings;
  1         2  
  1         35  
6 1     1   7 use strict;
  1         3  
  1         44  
7              
8             package IOMux::HTTP::Service;
9 1     1   5 use vars '$VERSION';
  1         2  
  1         242  
10             $VERSION = '0.11';
11              
12 1     1   6 use base 'IOMux::HTTP';
  1         2  
  1         131  
13              
14 1     1   7 use Log::Report 'iomux-http';
  1         2  
  1         8  
15              
16 1     1   911 use HTTP::Request ();
  1         2  
  1         16  
17 1     1   5 use HTTP::Response ();
  1         2  
  1         25  
18 1     1   6 use HTTP::Status;
  1         1  
  1         556  
19 1     1   7 use Socket;
  1         2  
  1         2792  
20              
21              
22             my $conn_id = 'C0000000';
23              
24             sub init($)
25 0     0 0   { my ($self, $args) = @_;
26 0   0       $args->{name} ||= ++$conn_id;
27              
28 0           $self->SUPER::init($args);
29 0           $self->{IMHS_requests} = [];
30 0 0         $self->{IMHS_handler} = $args->{handler}
31             or error __x"service {name} is started without handler callback"
32             , name => $self->name;
33 0           $self->{IMHS_session} = {};
34 0           $self->{IMHS_sent} = [];
35 0           $self;
36             }
37              
38             #---------------------
39              
40 0     0 1   sub client() {shift->{IMHS_client}}
41 0     0 0   sub session() {shift->{IMHS_session}}
42 0     0 1   sub msgsSent(){shift->{IMHS_sent}}
43              
44             # called when this object gets connected to the mux
45             sub mux_init($)
46 0     0 1   { my ($self, $mux) = @_;
47 0           $self->SUPER::mux_init($mux);
48              
49 0           my $peername = $self->socket->peername;
50 0           my ($port, $addr) = unpack_sockaddr_in $peername;
51 0           my $ip = inet_ntoa $addr;
52 0           my $host; # would be nice to have a async dnslookup here
53 0           my %client = (port => $port, ip => $ip, host => $host);
54 0           $self->{IMHS_client} = \%client;
55             }
56              
57             sub headerArrived($)
58 0     0 0   { my $self = shift;
59 0           HTTP::Request->parse(shift);
60             }
61              
62             sub bodyComponentArrived($$)
63 0     0 0   { my ($self, $req, $refdata) = @_;
64              
65 0           my $headers = $req->headers;
66 0   0       my $te = lc($headers->header('Transfer-Encoding') || '8bit');
67 0 0         return $self->SUPER::bodyComponentArrived($req, $refdata)
68             if $te eq '8bit';
69              
70 0 0         if($te ne 'chunked')
71 0           { trace "Unsupported transfer encoding $te";
72 0           return $self->errorResponse($req, RC_NOT_IMPLEMENTED);
73             }
74              
75 0 0         my ($starter, $len) = $$refdata =~ m/^((\S+)\r?\n)/ or return;
76 0 0         if($len !~ m/^[0-9a-fA-F]+$/)
77 0           { trace "Bad chunk header $len";
78 0           return $self->errorResponse($req, RC_BAD_REQUEST);
79             }
80              
81 0           my $need = hex $len;
82 0           my $chunk_length = length($starter) + $need + 2;
83             return # need more data for chunck
84 0 0         if length($$refdata) < $chunk_length;
85            
86 0 0         if($need!=0)
87 0           { $req->add_content(substr $$refdata, length($starter), $need, '');
88 0           return; # get more chunks
89             }
90              
91 0 0         return if $$refdata !~ m/\n\r?\n/; # need footer
92 0           my ($footer) = $$refdata =~ s/^0+\r?\n(.*?\r?\n)\r?\n//;
93 0           my $header = $req->headers;
94             HTTP::Message->parse($footer)->headers
95 0     0     ->scan(sub { $header->push_header(@_)} );
  0            
96              
97 0           $header->_header('Content-Length' => length ${$req->content_ref});
  0            
98 0           $header->remove_header('Transfer-Encoding');
99 0           $req;
100             }
101              
102             sub messageArrived($;$)
103 0     0 0   { my ($self, $req, $resp) = @_;
104              
105 0 0         if(my $waiting = shift @{$self->{IMHS_sent}})
  0            
106             { # try to continue on track
107 0           my ($resp, $cb, $session) = @$waiting;
108 0           return $cb->($self, $resp, $resp->code, $req, $session);
109             }
110              
111 0 0         $self->shutdown(0) # shutdown on low-level errors
112             if $resp;
113              
114 0 0         unless($resp)
115             { # Auto-reply to "Expect" requests
116 0           my $headers = $req->headers;
117 0 0         if(my $expect = $headers->header('Expect'))
118 0 0         { $resp = lc $expect ne '100-continue'
119             ? $self->errorResponse($req, RC_EXPECTATION_FAILED)
120             : $self->errorResponse($req, RC_CONTINUE);
121             }
122             }
123              
124 0           my $queue = $self->{IMHS_requests};
125 0           push @$queue, [$req, $resp];
126             # trace "new queued ".$req->uri.'; ql='.@$queue;
127              
128             # handler initiated by first request in queue, then auto-continues
129 0 0         $self->nextRequest
130             if @$queue==1;
131             }
132              
133             # This is the most tricky part: each connection may have multiple
134             # requests queued. If the handler returns a response object, the
135             # the response succeeded. Otherwise, other IO will need to be performed:
136             # we simply stop. When the other IO has completed, it will call this
137             # function again, to resolve the other requests.
138              
139             sub nextRequest()
140 0     0 0   { my $self = shift;
141 0           my $queue = $self->{IMHS_requests};
142 0           my $starter = $self->{IMHS_handler};
143              
144             #trace "nextRequest: ".join(',', map {$_->[0]->uri} @$queue);
145 0           while(@$queue)
146 0           { my $first = $queue->[0];
147 0           my ($req, $resp) = @$first;
148 0 0         if($resp)
149 0           { info "response already prepared: ".$req->uri;
150 0     0     $self->sendResponse($resp, sub {} );
  0            
151             }
152             else
153 0           { info "initiate new session: ".$req->uri;
154 0           $starter->($self, $req, $self->{IMHS_session});
155             }
156 0           shift @$queue;
157             }
158             }
159              
160             #--------------
161              
162             sub sendResponse($$;$)
163 0     0 1   { my ($self, $resp, $user_cb, $session) = @_;
164 0           $resp->protocol('HTTP/1.1');
165 0           push @{$self->{IMHS_sent}}, [$resp, $user_cb, $session];
  0            
166 0     0     $self->sendMessage($resp, sub {
167             # message send completed
168 0           });
169             }
170              
171              
172             sub makeResponse($$$;$)
173 0     0 1   { my ($self, $req, $status, $header, $content) = @_;
174 0           my $resp = HTTP::Response->new($status, status_message($status), $header);
175 0           $resp->request($req);
176              
177 0 0         $content or return $resp;
178              
179 0 0         if(ref $content eq 'CODE') { $resp->content($content) }
  0 0          
180 0           elsif(ref $content eq 'SCALAR') { $resp->content_ref($content) }
181 0           else { $resp->content_ref(\$content) }
182              
183 0           $resp;
184             }
185              
186              
187             sub errorResponse($$;$)
188 0     0 1   { my ($self, $req, $status, $text) = @_;
189 0 0 0       my $descr = defined $text && length $text ? "\n

$text

" : '';
190 0           my @headers = ('Content-Type' => 'text/html');
191 0           my $message = status_message $status;
192              
193 0           $self->makeResponse($req, $status, \@headers, \<<__CONTENT);
194             $status $message
195            

$status $message

$descr
196            
197             __CONTENT
198             }
199              
200              
201             sub redirectResponse($$$;$)
202 0     0 1   { my ($self, $req, $status, $location, $content) = @_;
203 0 0         is_redirect $status
204             or panic "Status '$status' is not redirect";
205              
206 0           my @headers = (Location => $location);
207 0 0 0       if(defined $content && length $content)
208 0 0         { my $ct = $content =~ m/^\s*\
209 0           push @headers, 'Content-Type' => $ct;
210             }
211              
212 0           $self->makeResponse($req, $status, \@headers, $content);
213             }
214              
215             #---------------------
216              
217              
218             1;