File Coverage

blib/lib/POE/Component/Client/HTTP.pm
Criterion Covered Total %
statement 317 377 84.0
branch 95 142 66.9
condition 36 46 78.2
subroutine 30 32 93.7
pod 1 1 100.0
total 479 598 80.1


line stmt bran cond sub pod time code
1             package POE::Component::Client::HTTP;
2             # vim: ts=2 sw=2 expandtab
3             $POE::Component::Client::HTTP::VERSION = '0.949';
4 21     21   15362469 use strict;
  21         56  
  21         942  
5             #use bytes; # for utf8 compatibility
6              
7 21     21   142 use constant DEBUG => 0;
  21         44  
  21         1348  
8 21     21   115 use constant DEBUG_DATA => 0;
  21         46  
  21         911  
9              
10 21     21   106 use Carp qw(croak carp);
  21         39  
  21         1351  
11 21     21   18757 use HTTP::Response;
  21         726517  
  21         932  
12 21     21   21888 use Net::HTTP::Methods;
  21         116553  
  21         935  
13 21         2331 use Socket qw(
14             sockaddr_in inet_ntoa
15             getnameinfo NI_NUMERICHOST NI_NUMERICSERV
16 21     21   336 );
  21         40  
17              
18 21     21   15989 use POE::Component::Client::HTTP::RequestFactory;
  21         77  
  21         790  
19 21     21   131 use POE::Component::Client::HTTP::Request qw(:states :fields);
  21         47  
  21         151  
20              
21             BEGIN {
22 21     21   150 local $SIG{'__DIE__'} = 'DEFAULT';
23              
24             #TODO: move this to Client::Keepalive?
25             # Allow more finely grained timeouts if Time::HiRes is available.
26 21         42 eval {
27 21         106 require Time::HiRes;
28 21         240 Time::HiRes->import("time");
29             };
30             }
31              
32 21         197 use POE qw(
33             Driver::SysRW Filter::Stream
34             Filter::HTTPHead Filter::HTTPChunk
35             Component::Client::Keepalive
36 21     21   4433 );
  21         43  
37              
38             # The Internet Assigned Numbers Authority (IANA) acts as a registry
39             # for transfer-coding value tokens. Initially, the registry contains
40             # the following tokens: "chunked" (section 3.6.1), "identity" (section
41             # 3.6.2), "gzip" (section 3.5), "compress" (section 3.5), and
42             # "deflate" (section 3.5).
43              
44             # FIXME - Haven't been able to test the compression options.
45             # Comments for each filter are what HTTP::Message use. Methods
46             # without packages are from Compress::Zlib.
47              
48             # FIXME - Is it okay to be mixing content and transfer encodings in
49             # this one table?
50              
51             my %te_possible_filters = (
52             'chunked' => 'POE::Filter::HTTPChunk',
53             'identity' => 'POE::Filter::Stream',
54             # 'gzip' => 'POE::Filter::Zlib::Stream', # Zlib: memGunzip
55             # 'x-gzip' => 'POE::Filter::Zlib::Stream', # Zlib: memGunzip
56             # 'x-bzip2' => 'POE::Filter::Bzip2', # Compress::BZip2::decompress
57             # 'deflate' => 'POE::Filter::Zlib::Stream', # Zlib: uncompress / inflate
58             # 'compress' => 'POE::Filter::LZW', # unsupported
59             # FIXME - base64 = MIME::Base64::decode
60             # FIXME - quoted-printable = Mime::QuotedPrint::decode
61             );
62              
63             my %te_filters;
64              
65             while (my ($encoding, $filter) = each %te_possible_filters) {
66 21     21   213 eval "use $filter";
  21     21   44  
  21         398  
  21         133  
  21         54  
  21         317  
67             next if $@;
68             $te_filters{$encoding} = $filter;
69             }
70              
71             # The following defaults to 'chunked,identity' which is technically
72             # correct but arguably useless. It also stomps on gzip'd transport
73             # because in the World Wild Web, Accept-Encoding is used to indicate
74             # gzip readiness, but the server responds with 'Content-Encoding:
75             # gzip', completely outside of TE encoding.
76             #
77             # Done this way so they appear in order of preference.
78             # FIXME - Is the order important here?
79              
80             #my $accept_encoding = join(
81             # ",",
82             # grep { exists $te_filters{$_} }
83             # qw(x-bzip2 gzip x-gzip deflate compress chunked identity)
84             #);
85              
86             my %supported_schemes = (
87             http => 1,
88             https => 1,
89             );
90              
91              
92             #------------------------------------------------------------------------------
93             # Spawn a new PoCo::Client::HTTP session. This basically is a
94             # constructor, but it isn't named "new" because it doesn't create a
95             # usable object. Instead, it spawns the object off as a separate
96             # session.
97              
98             sub spawn {
99 21     21 1 112547 my $type = shift;
100              
101 21 50       130 croak "$type requires an even number of parameters" if @_ % 2;
102              
103 21         115 my %params = @_;
104              
105 21         71 my $alias = delete $params{Alias};
106 21 100 66     188 $alias = 'weeble' unless defined $alias and length $alias;
107              
108 21         55 my $bind_addr = delete $params{BindAddr};
109 21         55 my $cm = delete $params{ConnectionManager};
110              
111 21         243 my $request_factory = POE::Component::Client::HTTP::RequestFactory->new(
112             \%params
113             );
114              
115 21 50       105 croak(
116             "$type doesn't know these parameters: ",
117             join(', ', sort keys %params)
118             ) if scalar keys %params;
119              
120             POE::Session->create(
121             inline_states => {
122             _start => \&_poco_weeble_start,
123             _stop => \&_poco_weeble_stop,
124 68     68   15108853 _child => sub { },
125              
126             # Public interface.
127 21         851 request => \&_poco_weeble_request,
128             pending_requests_count => \&_poco_weeble_pending_requests_count,
129             'shutdown' => \&_poco_weeble_shutdown,
130             cancel => \&_poco_weeble_cancel,
131              
132             # Client::Keepalive interface.
133             got_connect_done => \&_poco_weeble_connect_done,
134              
135             # ReadWrite interface.
136             got_socket_input => \&_poco_weeble_io_read,
137             got_socket_flush => \&_poco_weeble_io_flushed,
138             got_socket_error => \&_poco_weeble_io_error,
139              
140             # I/O timeout.
141             got_timeout => \&_poco_weeble_timeout,
142             remove_request => \&_poco_weeble_remove_request,
143             },
144             heap => {
145             alias => $alias,
146             factory => $request_factory,
147             cm => $cm,
148             is_shut_down => 0,
149             bind_addr => $bind_addr,
150             },
151             );
152              
153 21         3893 undef;
154             }
155              
156              
157             sub _poco_weeble_start {
158 21     21   5869 my ($kernel, $heap) = @_[KERNEL, HEAP];
159              
160 21         118 $kernel->alias_set($heap->{alias});
161              
162             # have to do this here because it wants a current_session
163 21 50       967 $heap->{cm} = POE::Component::Client::Keepalive->new(
    100          
164             timeout => $heap->{factory}->timeout,
165             ($heap->{bind_addr} ? (bind_address => $heap->{bind_addr}) : ()),
166             ) unless ($heap->{cm});
167             }
168              
169              
170             sub _poco_weeble_stop {
171 21     21   2018020 my $heap = $_[HEAP];
172 21         76 my $request = delete $heap->{request};
173              
174 21         157 foreach my $request_rec (values %$request) {
175 0         0 $request_rec->remove_timeout();
176 0         0 delete $heap->{ext_request_to_int_id}->{$request_rec->[REQ_HTTP_REQUEST]};
177             }
178              
179 21         356 DEBUG and warn "Client::HTTP (alias=$heap->{alias}) stopped.";
180             }
181              
182              
183             sub _poco_weeble_pending_requests_count {
184 7     7   6885 my ($heap) = $_[HEAP];
185 7   50     27 my $r = $heap->{request} || {};
186 7         30 return scalar keys %$r;
187             }
188              
189              
190             sub _poco_weeble_request {
191             my (
192 60     60   517824 $kernel, $heap, $sender,
193             $response_event, $http_request, $tag, $progress_event,
194             $proxy_override
195             ) = @_[KERNEL, HEAP, SENDER, ARG0, ARG1, ARG2, ARG3, ARG4];
196              
197 60         387 my $scheme = $http_request->uri->scheme;
198 60 100 66     5646 unless (
199             defined($scheme) and
200             exists $supported_schemes{$scheme}
201             ) {
202 1         25 my $rsp = HTTP::Response->new(
203             400 => 'Bad Request', [],
204             "\n"
205             . "Error: Bad Request\n"
206             . "\n"
207             . "

Error: Bad Request

\n"
208             . "Unsupported URI scheme: '$scheme'\n"
209             . "\n"
210             . "\n"
211             );
212 1         121 $rsp->request($http_request);
213 1 50       19 if (ref($response_event) eq 'POE::Component::Client::HTTP::Request') {
214             # This happens during redirect.
215 0         0 $response_event->postback->($rsp);
216             } else {
217 1         10 $kernel->post($sender, $response_event, [$http_request, $tag], [$rsp]);
218             }
219 1         155 return;
220             }
221              
222 59         222 my $host = $http_request->uri->host;
223 59 50 33     3654 unless (defined $host and length $host) {
224 0         0 my $rsp = HTTP::Response->new(
225             400 => 'Bad Request', [],
226             "\n"
227             . "Error: Bad Request\n"
228             . "\n"
229             . "

Error: Bad Request

\n"
230             . "URI contains no discernable host.\n"
231             . "\n"
232             . "\n"
233             );
234 0         0 $rsp->request($http_request);
235 0 0       0 if (ref($response_event) eq 'POE::Component::Client::HTTP::Request') {
236 0         0 $response_event->postback->($rsp);
237             } else {
238 0         0 $kernel->post($sender, $response_event, [$http_request, $tag], [$rsp]);
239             }
240 0         0 return;
241             }
242              
243 59 50       259 if ($heap->{is_shut_down}) {
244 0         0 my $rsp = HTTP::Response->new(
245             408 => 'Request timed out (component shut down)', [],
246             "\n"
247             . "Error: Request timed out (component shut down)" </td> </tr> <tr> <td class="h" > <a name="248">248</a> </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td >   </td> <td class="s"> . "\n"
249             . "\n"
250             . "

Error: Request Timeout

\n"
251             . "Request timed out (component shut down)\n"
252             . "\n"
253             . "\n"
254             );
255 0         0 $rsp->request($http_request);
256 0 0       0 if (ref($response_event) eq 'POE::Component::Client::HTTP::Request') {
257 0         0 $response_event->postback->($rsp);
258             } else {
259 0         0 $kernel->post($sender, $response_event, [$http_request, $tag], [$rsp]);
260             }
261 0         0 return;
262             }
263              
264 59 100       189 if (defined $proxy_override) {
265 6         86 POE::Component::Client::HTTP::RequestFactory->parse_proxy($proxy_override);
266             }
267              
268 59         590 my $request = $heap->{factory}->create_request(
269             $http_request, $response_event, $tag, $progress_event,
270             $proxy_override, $sender
271             );
272 59         326 $heap->{request}->{$request->ID} = $request;
273 59         217 $heap->{ext_request_to_int_id}->{$http_request} = $request->ID;
274              
275 59         116 my @timeout;
276 59 50       293 if ($heap->{factory}->timeout()) {
277 59         222 @timeout = (
278             timeout => $heap->{factory}->timeout()
279             );
280             }
281              
282 59         197 eval {
283             # get a connection from Client::Keepalive
284             #
285             # TODO CONNECT - We must ask PCC::Keepalive to establish an http
286             # socket, not https. The initial proxy interactin is plaintext?
287              
288 59         347 $request->[REQ_CONN_ID] = $heap->{cm}->allocate(
289             scheme => $request->scheme,
290             addr => $request->host,
291             port => $request->port,
292             context => $request->ID,
293             event => 'got_connect_done',
294             @timeout,
295             );
296             };
297 59 50       1140284 if ($@) {
298 0         0 delete $heap->{request}->{$request->ID};
299 0         0 delete $heap->{ext_request_to_int_id}->{$http_request};
300              
301             # we can reach here for things like host being invalid.
302 0         0 $request->error(400, $@);
303             }
304             }
305              
306              
307             sub _poco_weeble_connect_done {
308 56     56   1112072 my ($heap, $response) = @_[HEAP, ARG0];
309              
310 56         166 my $connection = $response->{'connection'};
311 56         144 my $request_id = $response->{'context'};
312              
313             # Can't handle connections if we're shut down.
314             # TODO - How do we still get these? Were they previously queued or
315             # something?
316 56 50       275 if ($heap->{is_shut_down}) {
317 0         0 _internal_cancel(
318             $heap, $request_id, 408, "Request timed out (request canceled)"
319             );
320 0         0 return;
321             }
322              
323 56 100       247 if (defined $connection) {
324 53         90 DEBUG and warn "CON: request $request_id connected ok...";
325              
326 53         204 my $request = $heap->{request}->{$request_id};
327 53 50       298 unless (defined $request) {
328 0         0 DEBUG and warn "CON: ignoring connection for canceled request";
329 0         0 return;
330             }
331              
332 53         517 my $block_size = $heap->{factory}->block_size;
333              
334             # get wheel from the connection
335 53         312 my $new_wheel = $connection->start(
336             Driver => POE::Driver::SysRW->new(BlockSize => $block_size),
337             InputFilter => POE::Filter::HTTPHead->new(),
338             OutputFilter => POE::Filter::Stream->new(),
339             InputEvent => 'got_socket_input',
340             FlushedEvent => 'got_socket_flush',
341             ErrorEvent => 'got_socket_error',
342             );
343              
344 53         22305 DEBUG and warn "CON: request $request_id uses wheel ", $new_wheel->ID;
345              
346             # Add the new wheel ID to the lookup table.
347 53         545 $heap->{wheel_to_request}->{ $new_wheel->ID() } = $request_id;
348              
349 53         339 $request->[REQ_CONNECTION] = $connection;
350              
351             # SSLify needs us to call it's function to get the "real" socket
352 53         101 my $peer_addr;
353 53 50       417 if ( $request->scheme eq 'http' ) {
354 53         1808 $peer_addr = getpeername($new_wheel->get_input_handle());
355             } else {
356 0         0 my $socket = $new_wheel->get_input_handle();
357 0         0 $peer_addr = getpeername(POE::Component::SSLify::SSLify_GetSocket($socket));
358             }
359              
360 53 50       1623 if (defined $peer_addr) {
361 53         1456 my ($error, $address, $port) = getnameinfo(
362             $peer_addr, NI_NUMERICHOST | NI_NUMERICSERV
363             );
364              
365 53 50       278 if ($error) {
366 0         0 $request->[REQ_PEERNAME] = "error: $error";
367             }
368             else {
369 53         220 $request->[REQ_PEERNAME] = "$address.$port";
370             }
371             }
372             else {
373 0         0 $request->[REQ_PEERNAME] = "error: $!";
374             }
375              
376 53         439 $request->create_timer($heap->{factory}->timeout);
377 53         298 $request->send_to_wheel;
378             }
379             else {
380 3         6 DEBUG and warn(
381             "CON: Error connecting for request $request_id --- ", $_[SENDER]->ID
382             );
383              
384 3   100     29 my ($operation, $errnum, $errstr) = (
385             $response->{function},
386             $response->{error_num} || '??',
387             $response->{error_str}
388             );
389              
390 3         6 DEBUG and warn(
391             "CON: request $request_id encountered $operation error " .
392             "$errnum: $errstr"
393             );
394              
395 3         5 DEBUG and warn "I/O: removing request $request_id";
396 3         21 my $request = delete $heap->{request}->{$request_id};
397 3         16 $request->remove_timeout();
398 3         11 delete $heap->{ext_request_to_int_id}->{$request->[REQ_HTTP_REQUEST]};
399              
400             # Post an error response back to the requesting session.
401 3         16 $request->connect_error($operation, $errnum, $errstr);
402             }
403             }
404              
405              
406             sub _poco_weeble_timeout {
407 3     3   6827598 my ($kernel, $heap, $request_id) = @_[KERNEL, HEAP, ARG0];
408              
409 3         9 DEBUG and warn "T/O: request $request_id timed out";
410              
411             # Discard the request. Keep a copy for a few bits of cleanup.
412 3         9 DEBUG and warn "I/O: removing request $request_id";
413 3         18 my $request = delete $heap->{request}->{$request_id};
414              
415 3 50       23 unless (defined $request) {
416 0         0 die(
417             "T/O: unexpectedly undefined request for id $request_id\n",
418 0         0 "T/O: known request IDs: ", join(", ", keys %{$heap->{request}}), "\n",
419             "...",
420             );
421             }
422              
423 3         8 DEBUG and warn "T/O: request $request_id has timer ", $request->timer;
424 3         111 $request->remove_timeout();
425 3         23 delete $heap->{ext_request_to_int_id}->{$request->[REQ_HTTP_REQUEST]};
426              
427             # There's a wheel attached to the request. Shut it down.
428 3 50       42 if ($request->wheel) {
429 3         34 my $wheel_id = $request->wheel->ID();
430 3         36 DEBUG and warn "T/O: request $request_id is wheel $wheel_id";
431              
432             # Shut down the connection so it's not reused.
433 3         1161 $request->wheel->shutdown_input();
434 3         485 delete $heap->{wheel_to_request}->{$wheel_id};
435             }
436              
437              
438 3         8 DEBUG and do {
439             die( "T/O: request $request_id is unexpectedly zero" )
440             unless $request->[REQ_STATE];
441             warn "T/O: request_state = " . sprintf("%#04x\n", $request->[REQ_STATE]);
442             };
443              
444             # Hey, we haven't sent back a response yet!
445 3 50       14 unless ($request->[REQ_STATE] & (RS_REDIRECTED | RS_POSTED)) {
446              
447             # Well, we have a response. Isn't that nice? Let's send it.
448 3 100       16 if ($request->[REQ_STATE] & (RS_IN_CONTENT | RS_DONE)) {
449 1         8 _finish_request($heap, $request);
450 1         13 return;
451             }
452              
453             # Post an error response back to the requesting session.
454 2         5 DEBUG and warn "I/O: Disconnect, keepalive timeout or HTTP/1.0.";
455 2 50       16 $request->error(408, "Request timed out") if $request->[REQ_STATE];
456 2         18 return;
457             }
458             }
459              
460              
461             sub _poco_weeble_io_flushed {
462 55     55   30328 my ($heap, $wheel_id) = @_[HEAP, ARG0];
463              
464             # We sent the request. Now we're looking for a response. It may be
465             # bad to assume we won't get a response until a request has flushed.
466 55         194 my $request_id = $heap->{wheel_to_request}->{$wheel_id};
467 55 50       207 if (not defined $request_id) {
468 0         0 DEBUG and warn "!!!: unexpectedly undefined request ID";
469 0         0 return;
470             }
471              
472 55         78 DEBUG and warn(
473             "I/O: wheel $wheel_id (request $request_id) flushed its request..."
474             );
475              
476 55         183 my $request = $heap->{request}->{$request_id};
477              
478             # Read content to send from a callback
479 55 100       253 if ( ref $request->[REQ_HTTP_REQUEST]->content() eq 'CODE' ) {
480 3         44 my $callback = $request->[REQ_HTTP_REQUEST]->content();
481              
482 3         29 my $buf = eval { $callback->() };
  3         10  
483              
484 3 100       34 if ( $buf ) {
485 2         12 $request->wheel->put($buf);
486              
487             # reset the timeout
488             # Have to also reset REQ_START_TIME or timer ends early
489 2         159 $request->remove_timeout;
490 2         11 $request->[REQ_START_TIME] = time();
491 2         18 $request->create_timer($heap->{factory}->timeout);
492              
493 2         7 return;
494             }
495             }
496              
497 53         2608 $request->[REQ_STATE] ^= RS_SENDING;
498 53         215 $request->[REQ_STATE] = RS_IN_HEAD;
499              
500             # XXX - Removed a second time. The first time was in version 0.53,
501             # because the EOF generated by shutdown_output() causes some servers
502             # to disconnect rather than send their responses.
503             # $request->wheel->shutdown_output();
504             }
505              
506              
507             sub _poco_weeble_io_error {
508 19     19   19711 my ($kernel, $heap, $operation, $errnum, $errstr, $wheel_id) =
509             @_[KERNEL, HEAP, ARG0..ARG3];
510              
511 19         32 DEBUG and warn(
512             "I/O: wheel $wheel_id encountered $operation error $errnum: $errstr"
513             );
514              
515             # Drop the wheel.
516 19         63 my $request_id = delete $heap->{wheel_to_request}->{$wheel_id};
517              
518             # There was no corresponding request? Nothing left to do here.
519             # We might have got here because the server sent EOF after we were done processing
520             # the request, and deleted it from our cache. ( notes for RT#50231 )
521 19 50       247 return unless $request_id;
522              
523 19         29 DEBUG and warn "I/O: removing request $request_id";
524 19         225 my $request = delete $heap->{request}->{$request_id};
525 19         142 $request->remove_timeout;
526 19         82 delete $heap->{ext_request_to_int_id}{$request->[REQ_HTTP_REQUEST]};
527              
528             # Otherwise the remote end simply closed. If we've got a pending
529             # response, then post it back to the client.
530 19         31 DEBUG and warn "STATE is ", $request->[REQ_STATE];
531              
532             # Except when we're redirected. In this case, the connection was but
533             # one step towards our destination.
534 19 50       98 return if ($request->[REQ_STATE] & RS_REDIRECTED);
535              
536             # If there was a non-zero error, then something bad happened. Post
537             # an error response back, if we haven't posted anything before.
538 19 50       65 if ($errnum) {
539 0 0       0 if ($operation eq "connect") {
540 0         0 $request->connect_error($operation, $errnum, $errstr);
541 0         0 return;
542             }
543              
544 0 0       0 unless ($request->[REQ_STATE] & RS_POSTED) {
545 0         0 $request->error(400, "$operation error $errnum: $errstr");
546             }
547 0         0 return;
548             }
549              
550             # We seem to have finished with the request. Send back a response.
551 19 100 100     265 if (
552             $request->[REQ_STATE] & (RS_IN_CONTENT | RS_DONE) and
553             not $request->[REQ_STATE] & RS_POSTED
554             ) {
555 16         63 _finish_request($heap, $request);
556 16         79 return;
557             }
558              
559             # We have already posted a response, so this is a remote keepalive
560             # timeout or other delayed socket shutdown. Nothing left to do.
561 3 100       15 if ($request->[REQ_STATE] & RS_POSTED) {
562 2         4 DEBUG and warn "I/O: Disconnect, remote keepalive timeout or HTTP/1.0.";
563 2         12 return;
564             }
565              
566             # We never received a response.
567 1 50       4 if (not defined $request->[REQ_RESPONSE]) {
568             # Check for pending data indicating a LF-free HTTP 0.9 response.
569 1         7 my $lines = $request->wheel->get_input_filter()->get_pending();
570 1         3 my $text = join '' => @$lines;
571 1         2 DEBUG and warn "Got ", length($text), " bytes of data without LF.";
572              
573             # If we have data, build and return a response from it.
574 1 50       7 if ($text =~ /\S/) {
575 1         2 DEBUG and warn(
576             "Generating HTTP response for HTTP/0.9 response without LF."
577             );
578 1         14 $request->[REQ_RESPONSE] = HTTP::Response->new(
579             200, 'OK', [
580             'Content-Type' => 'text/html',
581             'X-PCCH-Peer' => $request->[REQ_PEERNAME],
582             ], $text
583             );
584 1         168 $request->[REQ_RESPONSE]->protocol('HTTP/0.9');
585 1         13 $request->[REQ_RESPONSE]->request($request->[REQ_HTTP_REQUEST]);
586 1         10 $request->[REQ_STATE] = RS_DONE;
587 1         8 $request->return_response;
588 1         7 return;
589             }
590              
591             # No data received. This is an incomplete response.
592 0         0 $request->error(400, "Incomplete response - $request_id");
593 0         0 return;
594             }
595              
596             # We haven't built a proper response, and nothing returned by the
597             # server can be turned into a proper response. Send back an error.
598             # Changed to 406 after considering rt.cpan.org 20975.
599             #
600             # 10.4.7 406 Not Acceptable
601             #
602             # The resource identified by the request is only capable of
603             # generating response entities which have content characteristics
604             # not acceptable according to the accept headers sent in the
605             # request.
606              
607 0         0 $request->error(406, "Server response is Not Acceptable - $request_id");
608             }
609              
610              
611             #------------------------------------------------------------------------------
612             # Read a chunk of response. This code is directly adapted from Artur
613             # Bergman's nifty POE::Filter::HTTPD, which does pretty much the same
614             # in the other direction.
615              
616             sub _poco_weeble_io_read {
617 129     129   1243665 my ($kernel, $heap, $input, $wheel_id) = @_[KERNEL, HEAP, ARG0, ARG1];
618 129         372 my $request_id = $heap->{wheel_to_request}->{$wheel_id};
619              
620 129         165 DEBUG and warn "I/O: wheel $wheel_id got input...";
621 129         167 DEBUG_DATA and warn (ref($input) ? $input->as_string : _hexdump($input));
622              
623             # There was no corresponding request? Nothing left to do here.
624             #
625             # We might have got here because the server sent EOF after we were
626             # done processing the request, and deleted it from our cache. (
627             # notes for RT#50231 )
628 129 100       372 return unless defined $request_id;
629              
630 121         294 my $request = $heap->{request}->{$request_id};
631 121 50       428 return unless defined $request;
632 121         156 DEBUG and warn(
633             "REQUEST $request_id is $request <",
634             $request->[REQ_HTTP_REQUEST]->uri(), ">"
635             );
636              
637             # Reset the timeout if we get data.
638 121         628 $kernel->delay_adjust($request->timer, $heap->{factory}->timeout);
639              
640 121 50       13533 if ($request->[REQ_STATE] & RS_REDIRECTED) {
641 0         0 DEBUG and warn "input for request that was redirected";
642 0         0 return;
643             }
644              
645              
646             # The very first line ought to be status. If it's not, then it's
647             # part of the content.
648 121 100       349 if ($request->[REQ_STATE] & RS_IN_HEAD) {
649 50 50       161 if (defined $input) {
650 50         270 $input->request ($request->[REQ_HTTP_REQUEST]);
651             #warn(
652             # "INPUT for ", $request->[REQ_HTTP_REQUEST]->uri,
653             # " is \n",$input->as_string
654             #)
655             }
656             else {
657             #warn "NO INPUT";
658             }
659              
660             # FIXME: LordVorp gets here without $input being a HTTP::Response.
661             # FIXME: This happens when the response is HTTP/0.9 and doesn't
662             # include a status line. See t/53_response_parser.t.
663 50         747 $request->[REQ_RESPONSE] = $input;
664 50         361 $input->header("X-PCCH-Peer", $request->[REQ_PEERNAME]);
665              
666             # TODO CONNECT - If we've got the headers to a CONNECT request,
667             # then we can switch to the actual request. This is like a faux
668             # redirect where the socket gets reused.
669             #
670             # 1. Switch the socket to SSL.
671             # 2. Switch the request from CONNECT mode to regular mode, using
672             # the method proposed in PCCH::Request.
673             # 3. Send the original request via PCCH::Request->send_to_wheel().
674             # This puts the client back into the RS_SENDING state.
675             # 4. Reset any data/state so it appears we never went through
676             # CONNECT.
677             # 5. Make sure that PCC::Keepalive will discard the socket when
678             # we're done with it.
679             # 6. Return. The connection should proceed as normal.
680             #
681             # I think the normal handling for HTTP errors will cover the case
682             # of CONNECT failure. If not, we can refine the implementation as
683             # needed.
684              
685             # Some responses are without content by definition
686             # FIXME: #12363
687             # Make sure we finish even when it isn't one of these, but there
688             # is no content.
689 50 100 100     3676 if (
      100        
      66        
690             $request->[REQ_HTTP_REQUEST]->method eq 'HEAD'
691             or $input->code =~ /^(?:1|[23]04)/
692             or (
693             defined($input->content_length())
694             and $input->content_length() == 0
695             )
696             ) {
697 8 100       381 if (_try_redirect($request_id, $input, $request)) {
698 2         8 my $old_request = delete $heap->{request}->{$request_id};
699 2         7 delete $heap->{wheel_to_request}->{$wheel_id};
700 2 50       8 if (defined $old_request) {
701 2         4 DEBUG and warn "I/O: removed request $request_id";
702 2         11 $old_request->remove_timeout();
703 2         11 delete $heap->{ext_request_to_int_id}{$old_request->[REQ_HTTP_REQUEST]};
704 2         5 $old_request->[REQ_CONNECTION] = undef;
705             }
706 2         13 return;
707             }
708 6         16 $request->[REQ_STATE] |= RS_DONE;
709 6         29 $request->remove_timeout();
710 6         23 _finish_request($heap, $request);
711 6         478 return;
712             }
713             else {
714             # If we have content length, and it's more than the maximum we
715             # requested, then fail without bothering with the content.
716 42 100 100     4233 if (
      100        
717             defined($heap->{factory}->max_response_size())
718             and defined($input->content_length())
719             and $input->content_length() > $heap->{factory}->max_response_size()
720             ) {
721 2         16 _internal_cancel(
722             $heap, $request_id, 406,
723             "Response content length " . $input->content_length() .
724             " is greater than specified MaxSize of " .
725             $heap->{factory}->max_response_size() .
726             ". Use range requests to retrieve specific amounts of content."
727             );
728 2         14 return;
729             }
730              
731 40         173 $request->[REQ_STATE] |= RS_IN_CONTENT;
732 40         65 $request->[REQ_STATE] &= ~RS_IN_HEAD;
733             #FIXME: probably want to find out when the content from this
734             # request is in, and only then do the new request, so we
735             # can reuse the connection.
736 40 100       194 if (_try_redirect($request_id, $input, $request)) {
737 2         8 my $old_request = delete $heap->{request}->{$request_id};
738 2         6 delete $heap->{wheel_to_request}->{$wheel_id};
739 2 50       8 if (defined $old_request) {
740 2         6 DEBUG and warn "I/O: removed request $request_id";
741 2         9 delete $heap->{ext_request_to_int_id}{$old_request->[REQ_HTTP_REQUEST]};
742 2         11 $old_request->remove_timeout();
743 2         10 $old_request->close_connection();
744             }
745 2         11 return;
746             }
747              
748             # RFC 2616 14.41: If multiple encodings have been applied to an
749             # entity, the transfer-codings MUST be listed in the order in
750             # which they were applied.
751              
752 38         623 my ($filter, @filters);
753              
754             # Transfer encoding.
755              
756 38         453 my $te = $input->header('Transfer-Encoding');
757 38 100       1480 if (defined $te) {
758 4         35 my @te = split(/\s*,\s*/, lc($te));
759              
760 4   100     39 while (@te and exists $te_filters{$te[-1]}) {
761 3         5 my $encoding = pop @te;
762 3         11 my $fclass = $te_filters{$encoding};
763 3         33 push @filters, $fclass->new();
764             }
765              
766 4 100       13 if (@te) {
767 2         10 $input->header('Transfer-Encoding', join(', ', @te));
768             }
769             else {
770 2         9 $input->header('Transfer-Encoding', undef);
771             }
772             }
773              
774             # Content encoding.
775              
776 38         346 my $ce = $input->header('Content-Encoding');
777 38 100       1255 if (defined $ce) {
778 1         5 my @ce = split(/\s*,\s*/, lc($ce));
779              
780 1   33     10 while (@ce and exists $te_filters{$ce[-1]}) {
781 0         0 my $encoding = pop @ce;
782 0         0 my $fclass = $te_filters{$encoding};
783 0         0 push @filters, $fclass->new();
784             }
785              
786 1 50       4 if (@ce) {
787 1         4 $input->header('Content-Encoding', join(', ', @ce));
788             }
789             else {
790 0         0 $input->header('Content-Encoding', undef);
791             }
792             }
793              
794 38 50       206 if (@filters > 1) {
    100          
795 0         0 $filter = POE::Filter::Stackable->new( Filters => \@filters );
796             }
797             elsif (@filters) {
798 3         6 $filter = $filters[0];
799             }
800             else {
801             # Punt if we have no specified filters.
802 35         193 $filter = POE::Filter::Stream->new;
803             }
804              
805             # do this last, because it triggers a read
806 38         535 $request->wheel->set_input_filter($filter);
807             }
808 38         4423 return;
809             }
810              
811             # We're in a content state.
812 71 100       229 if ($request->[REQ_STATE] & RS_IN_CONTENT) {
813 67 50 33     247 if (ref($input) and UNIVERSAL::isa($input, 'HTTP::Response')) {
814             # there was a problem in the input filter
815             # $request->close_connection;
816             }
817             else {
818 67         293 $request->add_content($input);
819             }
820             }
821              
822             # POST response without disconnecting
823 71 100 100     677 if (
824             $request->[REQ_STATE] & RS_DONE and
825             not $request->[REQ_STATE] & RS_POSTED
826             ) {
827 18         88 $request->remove_timeout;
828 18         96 _finish_request($heap, $request);
829             }
830              
831             }
832              
833              
834             #------------------------------------------------------------------------------
835             # Generate a hex dump of some input. This is not a POE function.
836              
837             sub _hexdump {
838 0     0   0 my $data = shift;
839              
840 0         0 my $dump;
841 0         0 my $offset = 0;
842 0         0 while (length $data) {
843 0         0 my $line = substr($data, 0, 16);
844 0         0 substr($data, 0, 16) = '';
845              
846 0         0 my $hexdump = unpack 'H*', $line;
847 0         0 $hexdump =~ s/(..)/$1 /g;
848              
849 0         0 $line =~ tr[ -~][.]c;
850 0         0 $dump .= sprintf( "%04x %-47.47s - %s\n", $offset, $hexdump, $line );
851 0         0 $offset += 16;
852             }
853              
854 0         0 return $dump;
855             }
856              
857              
858             # Check for and handle redirect. Returns true if redirect should
859             # occur, or false if there's no redirect.
860              
861             sub _try_redirect {
862 48     48   114 my ($request_id, $input, $request) = @_;
863              
864 48 100       235 if (my $newrequest = $request->check_redirect) {
865 4         9 DEBUG and warn(
866             "Redirected $request_id ", $input->code, " to <",
867             $newrequest->uri, ">"
868             );
869 4         6 my @proxy;
870 4 100       18 if ($request->[REQ_USING_PROXY]) {
871 1         6 push @proxy, (
872             'http://' . $request->host . ':' . $request->port . '/'
873             );
874             }
875              
876             $poe_kernel->yield(
877 4         26 request =>
878             $request,
879             $newrequest,
880             "_redir_".$request->ID,
881             $request->[REQ_PROG_POSTBACK],
882             @proxy
883             );
884              
885 4         308 return 1;
886             }
887              
888 44         707 return;
889             }
890              
891              
892             # Complete a request. This was moved out of _poco_weeble_io_error(). This is
893             # not a POE function.
894              
895             sub _finish_request {
896 41     41   78 my ($heap, $request) = @_;
897              
898 41         181 my $request_id = $request->ID;
899 41         71 if (DEBUG) {
900             carp "XXX: calling _finish_request(request id = $request_id)";
901             }
902              
903             # XXX What does this do?
904 41         213 $request->add_eof;
905              
906             # KeepAlive: added the RS_POSTED flag
907 41         100 $request->[REQ_STATE] |= RS_POSTED;
908              
909 41 50       154 my $wheel_id = defined $request->wheel ? $request->wheel->ID : "(undef)";
910 41         361 DEBUG and warn "Wheel from request is ", $wheel_id;
911             # clean up the request
912 41         164 my $address = "$request->[REQ_HOST]:$request->[REQ_PORT]";
913              
914 41         67 DEBUG and warn "address is $address";
915              
916 41         163 return _clear_req_cache( $heap, $request_id );
917             }
918              
919              
920             sub _poco_weeble_remove_request {
921 0     0   0 my ($kernel, $heap, $request_id) = @_[KERNEL, HEAP, ARG0];
922              
923 0         0 return _clear_req_cache( $heap, $request_id );
924             }
925              
926              
927             # helper subroutine to remove a request from our caches
928              
929             sub _clear_req_cache {
930 41     41   79 my ($heap, $request_id) = @_;
931              
932 41         123 my $request = delete $heap->{request}->{$request_id};
933 41 100       135 return unless defined $request;
934              
935 24         30 DEBUG and warn "I/O: removed request $request_id";
936              
937 24         102 $request->remove_timeout();
938 24         85 delete $heap->{ext_request_to_int_id}{$request->[REQ_HTTP_REQUEST]};
939 24 50       80 if (my $wheel = $request->wheel) {
940 24         205 delete $heap->{wheel_to_request}->{$wheel->ID};
941             }
942              
943             # If the response wants us to close the connection, regrettably do
944             # so. Only matters if the request is defined.
945 24 50       153 if ($request->[REQ_CONNECTION]) {
946 24 50       170 if (defined(my $response = $request->[REQ_RESPONSE])) {
947 24         98 my $connection_header = $response->header('Connection');
948 24 100 100     1038 if (defined $connection_header and $connection_header =~ /\bclose\b/) {
949 14         18 DEBUG and warn "I/O: closing connection on server's request";
950 14         70 $request->close_connection();
951             }
952             }
953             }
954              
955 24         284 return;
956             }
957              
958              
959             # Cancel a single request by HTTP::Request object.
960              
961             sub _poco_weeble_cancel {
962 2     2   618 my ($kernel, $heap, $request) = @_[KERNEL, HEAP, ARG0];
963 2         13 my $request_id = $heap->{ext_request_to_int_id}{$request};
964 2 50       9 return unless defined $request_id;
965 2         12 _internal_cancel(
966             $heap, $request_id, 408, "Request timed out (request canceled)"
967             );
968             }
969              
970              
971             sub _internal_cancel {
972 6     6   26 my ($heap, $request_id, $code, $message) = @_;
973              
974 6         24 my $request = delete $heap->{request}{$request_id};
975 6 50       42 return unless defined $request;
976              
977 6         14 DEBUG and warn "CXL: canceling request $request_id";
978 6         38 $request->remove_timeout();
979 6         134 delete $heap->{ext_request_to_int_id}{$request->[REQ_HTTP_REQUEST]};
980              
981 6 100       38 if ($request->wheel) {
982 3         30 my $wheel_id = $request->wheel->ID;
983 3         31 DEBUG and warn "CXL: Request $request_id canceling wheel $wheel_id";
984 3         11 delete $heap->{wheel_to_request}{$wheel_id};
985             }
986              
987 6 100       27 if ($request->[REQ_CONNECTION]) {
988 3         15 DEBUG and warn "I/O: Closing connection during internal cancel";
989 3         22 $request->close_connection();
990             }
991             else {
992             # Didn't connect yet; inform connection manager to cancel
993             # connection request.
994              
995 3         20 $heap->{cm}->deallocate($request->[REQ_CONN_ID]);
996             }
997              
998 6 50       9544 unless ($request->[REQ_STATE] & RS_POSTED) {
999 6         560 $request->error($code, $message);
1000             }
1001             }
1002              
1003              
1004             # Shut down the entire component.
1005             sub _poco_weeble_shutdown {
1006 19     19   2470564 my ($kernel, $heap) = @_[KERNEL, HEAP];
1007              
1008 19         69 $heap->{is_shut_down} = 1;
1009              
1010 19         5291 my @request_ids = keys %{$heap->{request}};
  19         130  
1011 19         81 foreach my $request_id (@request_ids) {
1012 2         41 _internal_cancel(
1013             $heap, $request_id, 408, "Request timed out (component shut down)"
1014             );
1015             }
1016              
1017             # Shut down the connection manager subcomponent.
1018 19 50       192 if (defined $heap->{cm}) {
1019 19         46 DEBUG and warn "CXL: Client::HTTP shutting down Client::Keepalive";
1020 19         153 $heap->{cm}->shutdown();
1021 19         79236 delete $heap->{cm};
1022             }
1023              
1024             # Final cleanup of this component.
1025 19         103 $kernel->alias_remove($heap->{alias});
1026             }
1027              
1028             1;
1029              
1030             __END__