File Coverage

blib/lib/Perlbal/ClientHTTPBase.pm
Criterion Covered Total %
statement 397 493 80.5
branch 130 226 57.5
condition 62 103 60.1
subroutine 41 48 85.4
pod 7 21 33.3
total 637 891 71.4


line stmt bran cond sub pod time code
1             ######################################################################
2             # Common HTTP functionality for ClientProxy and ClientHTTP
3             # possible states:
4             # reading_headers (initial state, then follows one of two paths)
5             # wait_backend, backend_req_sent, wait_res, xfer_res, draining_res
6             # wait_stat, wait_open, xfer_disk
7             # both paths can then go into persist_wait, which means they're waiting
8             # for another request from the user
9             #
10             # Copyright 2004, Danga Interactive, Inc.
11             # Copyright 2005-2007, Six Apart, Ltd.
12              
13             package Perlbal::ClientHTTPBase;
14 22     22   137 use strict;
  22         43  
  22         851  
15 22     22   130 use warnings;
  22         91  
  22         812  
16 22     22   111 no warnings qw(deprecated);
  22         57  
  22         961  
17              
18 22     22   128 use Sys::Syscall;
  22         131  
  22         1198  
19 22     22   130 use base "Perlbal::Socket";
  22         46  
  22         3881  
20 22     22   21227 use HTTP::Date ();
  22         127781  
  22         1163  
21 22         227 use fields ('service', # Perlbal::Service object
22             'replacement_uri', # URI to send instead of the one requested; this is used
23             # to instruct _serve_request to send an index file instead
24             # of trying to serve a directory and failing
25             'scratch', # extra storage; plugins can use it if they want
26              
27             # reproxy support
28             'reproxy_file', # filename the backend told us to start opening
29             'reproxy_file_size', # size of file, once we stat() it
30             'reproxy_fh', # if needed, IO::Handle of fd
31             'reproxy_file_offset', # how much we've sent from the file.
32              
33             'post_sendfile_cb', # subref to run after we're done sendfile'ing the current file
34              
35             'requests', # number of requests this object has performed for the user
36              
37             # service selector parent
38             'selector_svc', # the original service from which we came
39             'is_ssl', # Is this socket SSL attached (restricted operations)
40 22     22   198 );
  22         60  
41              
42 22     22   2663 use Fcntl ':mode';
  22         55  
  22         9054  
43 22     22   143 use Errno qw(EPIPE ECONNRESET);
  22         48  
  22         1170  
44 22     22   123 use POSIX ();
  22         49  
  22         902578  
45              
46             # hard-code defaults can be changed with MIME management command
47             our $MimeType = {qw(
48             css text/css
49             doc application/msword
50             gif image/gif
51             htm text/html
52             html text/html
53             jpg image/jpeg
54             js application/x-javascript
55             mp3 audio/mpeg
56             mpg video/mpeg
57             pdf application/pdf
58             png image/png
59             tif image/tiff
60             tiff image/tiff
61             torrent application/x-bittorrent
62             txt text/plain
63             zip application/zip
64             )};
65              
66             # ClientHTTPBase
67             sub new {
68              
69 72     72 1 208 my Perlbal::ClientHTTPBase $self = shift;
70 72         201 my ($service, $sock, $selector_svc) = @_;
71 72 100       342 $self = fields::new($self) unless ref $self;
72 72         7924 $self->SUPER::new($sock); # init base fields
73              
74 72         243 $self->{service} = $service;
75 72         257 $self->{replacement_uri} = undef;
76 72         191 $self->{headers_string} = '';
77 72         223 $self->{requests} = 0;
78 72         198 $self->{scratch} = {};
79 72         176 $self->{selector_svc} = $selector_svc;
80 72         172 $self->{is_ssl} = 0;
81              
82 72         718 $self->state('reading_headers');
83              
84 72         474 $self->watch_read(1);
85 72         2295 return $self;
86             }
87              
88             sub new_from_base {
89 24     24 0 49 my $class = shift;
90 24         63 my Perlbal::ClientHTTPBase $cb = shift; # base object
91 24         97 Perlbal::Util::rebless($cb, $class);
92              
93 24         186 $cb->handle_request;
94 24         71 return $cb;
95             }
96              
97             sub close {
98 69     69 1 142 my Perlbal::ClientHTTPBase $self = shift;
99              
100             # don't close twice
101 69 50       562 return if $self->{closed};
102              
103             # could contain a closure with circular reference
104 69         759 $self->{post_sendfile_cb} = undef;
105              
106             # close the file we were reproxying, if any
107 69 50       260 CORE::close($self->{reproxy_fh}) if $self->{reproxy_fh};
108              
109             # now pass up the line
110 69         466 $self->SUPER::close(@_);
111             }
112              
113             # given the response headers we just got, and considering our request
114             # headers, determine if we should be sending keep-alive header
115             # information back to the client
116             sub setup_keepalive {
117 208     208 0 604 my Perlbal::ClientHTTPBase $self = $_[0];
118 208         601 print "ClientHTTPBase::setup_keepalive($self)\n" if Perlbal::DEBUG >= 2;
119              
120             # now get the headers we're using
121 208         897 my Perlbal::HTTPHeaders $reshd = $_[1];
122 208         508 my Perlbal::HTTPHeaders $rqhd = $self->{req_headers};
123 208         409 my $override_value = $_[2];
124              
125             # for now, we enforce outgoing HTTP 1.0
126 208         1100 $reshd->set_version("1.0");
127              
128             # if we came in via a selector service, that's whose settings
129             # we respect for persist_client
130 208   66     1659 my $svc = $self->{selector_svc} || $self->{service};
131 208   100     1058 my $persist_client = $svc->{persist_client} || 0;
132 208 100       675 $persist_client = $override_value if defined $override_value;
133 208         328 print " service's persist_client = $persist_client\n" if Perlbal::DEBUG >= 3;
134              
135             # do keep alive if they sent content-length or it's a head request
136 208   100     1590 my $do_keepalive = $persist_client && $rqhd->req_keep_alive($reshd);
137 208 100       840 if ($do_keepalive) {
138 149         783 print " doing keep-alive to client\n" if Perlbal::DEBUG >= 3;
139 149         520 my $timeout = $self->{service}->{persist_client_idle_timeout};
140 149         627 $reshd->header('Connection', 'keep-alive');
141 149 50       1013 $reshd->header('Keep-Alive', $timeout ? "timeout=$timeout, max=100" : undef);
142             } else {
143 59         99 print " doing connection: close\n" if Perlbal::DEBUG >= 3;
144             # FIXME: we don't necessarily want to set connection to close,
145             # but really set a space-separated list of tokens which are
146             # specific to the connection. "close" and "keep-alive" are
147             # just special.
148 59         350 $reshd->header('Connection', 'close');
149 59         219 $reshd->header('Keep-Alive', undef);
150             }
151             }
152              
153             # overridden here from Perlbal::Socket to use the service value
154             sub max_idle_time {
155 5     5 0 16 my Perlbal::ClientHTTPBase $self = shift;
156 5 50       35 if ($self->state eq 'persist_wait') {
157 0         0 return $self->{service}->{persist_client_idle_timeout};
158             } else {
159 5         9687 return $self->{service}->{idle_timeout};
160             }
161             }
162              
163             # Called when this client is entering a persist_wait state, but before we are returned to base.
164 28     28 0 50 sub persist_wait {
165            
166             }
167              
168             # called when we've finished writing everything to a client and we need
169             # to reset our state for another request. returns 1 to mean that we should
170             # support persistence, 0 means we're discarding this connection.
171             sub http_response_sent {
172 208     208 0 438 my Perlbal::ClientHTTPBase $self = $_[0];
173              
174             # close if we're supposed to
175 208 100 66     1714 if (
      66        
176             ! defined $self->{res_headers} ||
177             ! $self->{res_headers}->res_keep_alive($self->{req_headers}) ||
178             $self->{do_die}
179             )
180             {
181             # do a final read so we don't have unread_data_waiting and RST
182             # the connection. IE and others send an extra \r\n after POSTs
183 59         265 my $dummy = $self->read(5);
184              
185             # close if we have no response headers or they say to close
186 59         2374 $self->close("no_keep_alive");
187 59         54981 return 0;
188             }
189              
190             # if they just did a POST, set the flag that says we might expect
191             # an unadvertised \r\n coming from some browsers. Old Netscape
192             # 4.x did this on all POSTs, and Firefox/Safari do it on
193             # XmlHttpRequest POSTs.
194 149 100       748 if ($self->{req_headers}->request_method eq "POST") {
195 75         198 $self->{ditch_leading_rn} = 1;
196             }
197              
198             # now since we're doing persistence, uncork so the last packet goes.
199             # we will recork when we're processing a new request.
200 149         951 $self->tcp_cork(0);
201              
202             # reset state
203 149         245948 $self->{replacement_uri} = undef;
204 149         453 $self->{headers_string} = '';
205 149         494 $self->{req_headers} = undef;
206 149         1835 $self->{res_headers} = undef;
207 149         741 $self->{reproxy_fh} = undef;
208 149         440 $self->{reproxy_file} = undef;
209 149         678 $self->{reproxy_file_size} = 0;
210 149         346 $self->{reproxy_file_offset} = 0;
211 149         2981 $self->{read_buf} = [];
212 149         616 $self->{read_ahead} = 0;
213 149         331 $self->{read_size} = 0;
214 149         851 $self->{scratch} = {};
215 149         411 $self->{post_sendfile_cb} = undef;
216 149         1046 $self->state('persist_wait');
217              
218 149         814 $self->persist_wait;
219              
220 149 100       2146 if (my $selector_svc = $self->{selector_svc}) {
221 64 50       370 if (! $selector_svc->run_hook('return_to_base', $self)){
222 64         1055 $selector_svc->return_to_base($self);
223             }
224             }
225              
226             # NOTE: because we only speak 1.0 to clients they can't have
227             # pipeline in a read that we haven't read yet.
228 149         1722 $self->watch_read(1);
229 149         7454 $self->watch_write(0);
230 149         3284 return 1;
231             }
232              
233             sub reproxy_fh {
234 39     39 0 73 my Perlbal::ClientHTTPBase $self = shift;
235              
236             # setter
237 39 50       116 if (@_) {
238 39         77 my ($fh, $size) = @_;
239 39         134 $self->state('xfer_disk');
240 39         83 $self->{reproxy_fh} = $fh;
241 39         78 $self->{reproxy_file_offset} = 0;
242 39         82 $self->{reproxy_file_size} = $size;
243              
244 39   33     193 my $is_ssl_webserver = ( $self->{service}->{listener}->{sslopts} &&
245             ( $self->{service}->{role} eq 'web_server') );
246              
247 39 50       93 unless ($is_ssl_webserver) {
248             # call hook that we're reproxying a file
249 39 50       321 return $fh if $self->{service}->run_hook("start_send_file", $self);
250             # turn on writes (the hook might not have wanted us to)
251 39         193 $self->watch_write(1);
252 39         1714 return $fh;
253             } else { # use aio_read for ssl webserver instead of sendfile
254              
255 0 0       0 print "webserver in ssl mode, sendfile disabled!\n"
256             if $Perlbal::DEBUG >= 3;
257              
258             # turn off writes
259 0         0 $self->watch_write(0);
260             #create filehandle for reading
261 0         0 my $data = '';
262             Perlbal::AIO::aio_read($self->reproxy_fh, 0, 2048, $data, sub {
263             # got data? undef is error
264 0 0   0   0 return $self->_simple_response(500) unless $_[0] > 0;
265              
266             # seek into the file now so sendfile starts further in
267 0         0 my $ld = length $data;
268 0         0 sysseek($self->{reproxy_fh}, $ld, &POSIX::SEEK_SET);
269 0         0 $self->{reproxy_file_offset} = $ld;
270             # reenable writes after we get data
271 0         0 $self->tcp_cork(1); # by setting reproxy_file_offset above,
272             # it won't cork, so we cork it
273 0         0 $self->write($data);
274 0         0 $self->watch_write(1);
275 0         0 });
276 0         0 return 1;
277             }
278             }
279              
280 0         0 return $self->{reproxy_fh};
281             }
282              
283             sub event_read {
284 88     88 1 125539 my Perlbal::ClientHTTPBase $self = shift;
285              
286 88         236 $self->{alive_time} = $Perlbal::tick_time;
287              
288             # see if we have headers?
289 88 50       446 die "Shouldn't get here! This is an abstract base class, pretty much, except in the case of the 'selector' role."
290             if $self->{req_headers};
291              
292 88         10051 my $hd = $self->read_request_headers;
293 88         1042 $self->handle_request;
294             }
295              
296             sub handle_request {
297 112     112 0 283 my Perlbal::ClientHTTPBase $self = shift;
298 112         256 my Perlbal::HTTPHeaders $hd = $self->{req_headers};
299              
300 112 100       601 return unless $hd;
301              
302 89         499 $self->check_req_headers;
303              
304 89 50       1045 return if $self->{service}->run_hook('start_http_request', $self);
305              
306             # we must stop watching for events now, otherwise if there's
307             # PUT/POST overflow, it'll be sent to ClientHTTPBase, which can't
308             # handle it yet. must wait for the selector (which has as much
309             # time as it wants) to route as to our subclass, which can then
310             # re-enable reads.
311 89         536 $self->watch_read(0);
312              
313             my $select = sub {
314             # now that we have headers, it's time to tell the selector
315             # plugin that it's time for it to select which real service to
316             # use
317 89     89   653 my $selector = $self->{'service'}->selector();
318 89 50       477 return $self->_simple_response(500, "No service selector configured.")
319             unless ref $selector eq "CODE";
320 89         392 $selector->($self);
321 89         3204 };
322              
323 89         1557 my $svc = $self->{'service'};
324 89 50       1449 if ($svc->{latency}) {
325 0         0 Danga::Socket->AddTimer($svc->{latency} / 1000, $select);
326             } else {
327 89         259 $select->();
328             }
329             }
330              
331             sub reproxy_file_done {
332 39     39 0 65 my Perlbal::ClientHTTPBase $self = shift;
333 39 50       160 return if $self->{service}->run_hook('reproxy_fh_finished', $self);
334             # close the sendfile fd
335 39         660 CORE::close($self->{reproxy_fh});
336 39         85 $self->{reproxy_fh} = undef;
337 39 100       139 if (my $cb = $self->{post_sendfile_cb}) {
338 8         15 $cb->();
339             } else {
340 31         258 $self->http_response_sent;
341             }
342             }
343              
344             # client is ready for more of its file. so sendfile some more to it.
345             # (called by event_write when we're actually in this mode)
346             sub event_write_reproxy_fh {
347 39     39 0 65 my Perlbal::ClientHTTPBase $self = shift;
348              
349 39         279 my $remain = $self->{reproxy_file_size} - $self->{reproxy_file_offset};
350 39 50       197 $self->tcp_cork(1) if $self->{reproxy_file_offset} == 0;
351 39         312 $self->watch_write(0);
352              
353 39 50       992 if ($self->{is_ssl}) { # SSL (sendfile does not do SSL)
354 0 0       0 return if $self->{closed};
355 0 0       0 if ($remain <= 0) { #done
356 0         0 print "REPROXY SSL done\n" if Perlbal::DEBUG >= 2;
357 0         0 $self->reproxy_file_done;
358 0         0 return;
359             }
360             # queue up next read
361 0         0 Perlbal::AIO::set_file_for_channel($self->{reproxy_file});
362 0 0       0 my $len = $remain > 4096 ? 4096 : $remain; # buffer size
363 0         0 my $buffer = '';
364             Perlbal::AIO::aio_read(
365             $self->{reproxy_fh},
366             $self->{reproxy_file_offset},
367             $len,
368             $buffer,
369             sub {
370 0 0   0   0 return if $self->{closed};
371             # we have buffer to send
372 0         0 my $rv = $_[0]; # arg is result of sysread
373 0 0 0     0 if (!defined($rv) || $rv <= 0) { # read error
374             # sysseek is called after sysread so $! not valid
375 0         0 $self->close('sysread_error');
376 0         0 print STDERR "Error w/ reproxy sysread\n";
377 0         0 return;
378             }
379 0         0 $self->{reproxy_file_offset} += $rv;
380 0         0 $self->tcp_cork(1); # by setting reproxy_file_offset above,
381             # it won't cork, so we cork it
382 0         0 $self->write($buffer); # start socket send
383 0         0 $self->watch_write(1);
384             }
385 0         0 );
386 0         0 return;
387             }
388              
389             # cap at 128k sendfiles
390 39 50       117 my $to_send = $remain > 128 * 1024 ? 128 * 1024 : $remain;
391              
392             my $postread = sub {
393 39 50   39   119 return if $self->{closed};
394              
395 39         214 my $sent = Perlbal::Socket::sendfile($self->{fd},
396             fileno($self->{reproxy_fh}),
397             $to_send);
398             #warn "to_send = $to_send, sent = $sent\n";
399 39         12241 print "REPROXY Sent: $sent\n" if Perlbal::DEBUG >= 2;
400              
401 39 50       131 if ($sent < 0) {
402 0 0       0 return $self->close("epipe") if $! == EPIPE;
403 0 0       0 return $self->close("connreset") if $! == ECONNRESET;
404 0         0 print STDERR "Error w/ sendfile: $!\n";
405 0         0 $self->close('sendfile_error');
406 0         0 return;
407             }
408 39         101 $self->{reproxy_file_offset} += $sent;
409              
410 39 50       502 if ($sent >= $remain) {
411 39         187 $self->reproxy_file_done;
412             } else {
413 0         0 $self->watch_write(1);
414             }
415 39         238 };
416              
417             # TODO: way to bypass readahead and go straight to sendfile for common/hot/recent files.
418             # something like:
419             # if ($hot) { $postread->(); return ; }
420              
421 39 50       297 if ($to_send < 0) {
422 0         0 Perlbal::log('warning', "tried to readahead negative bytes. filesize=$self->{reproxy_file_size}, offset=$self->{reproxy_file_offset}");
423             # this code, doing sendfile, will fail gracefully with return
424             # code, not 'die', and we'll close with sendfile_error:
425 0         0 $postread->();
426 0         0 return;
427             }
428              
429 39         165 Perlbal::AIO::set_file_for_channel($self->{reproxy_file});
430 39         231 Perlbal::AIO::aio_readahead($self->{reproxy_fh},
431             $self->{reproxy_file_offset},
432             $to_send, $postread);
433             }
434              
435             sub event_write {
436 39     39 1 2469 my Perlbal::ClientHTTPBase $self = shift;
437              
438             # Any HTTP client is considered alive if it's writable.
439             # if it's not writable for 30 seconds, we kill it.
440             # subclasses can decide what's appropriate for timeout.
441 39         83 $self->{alive_time} = $Perlbal::tick_time;
442              
443             # if we're sending a filehandle, go do some more sendfile:
444 39 50       143 if ($self->{reproxy_fh}) {
445 39         156 $self->event_write_reproxy_fh;
446 39         395 return;
447             }
448              
449             # otherwise just kick-start our write buffer.
450 0 0       0 if ($self->write(undef)) {
451             # we've written all data in the queue, so stop waiting for
452             # write notifications:
453 0         0 print "All writing done to $self\n" if Perlbal::DEBUG >= 2;
454 0         0 $self->watch_write(0);
455             }
456             }
457              
458             # this gets called when a "web" service is serving a file locally.
459             sub _serve_request {
460 46     46   86 my Perlbal::ClientHTTPBase $self = shift;
461 46         91 my Perlbal::HTTPHeaders $hd = shift;
462              
463 46         147 my $rm = $hd->request_method;
464 46 50 66     436 unless ($rm eq "HEAD" || $rm eq "GET") {
465 0         0 return $self->_simple_response(403, "Unimplemented method");
466             }
467              
468 46   66     308 my $uri = Perlbal::Util::durl($self->{replacement_uri} || $hd->request_uri);
469 46         179 my Perlbal::Service $svc = $self->{service};
470              
471             # start_serve_request hook
472 46 50       196 return 1 if $self->{service}->run_hook('start_serve_request', $self, \$uri);
473              
474             # don't allow directory traversal
475 46 100 66     459 if ($uri =~ m!/\.\./! || $uri !~ m!^/!) {
476 1         5 return $self->_simple_response(403, "Bogus URL");
477             }
478              
479             # double question mark means to serve multiple files, comma separated after the
480             # questions. the uri part before the question mark is the relative base directory
481             # TODO: only do this if $uri has ?? and the service also allows it. otherwise
482             # we don't want to mess with anybody's meaning of '??' on the backend service
483 45 100       185 return $self->_serve_request_multiple($hd, $uri) if $uri =~ /\?\?/;
484              
485             # chop off the query string
486 36         90 $uri =~ s/\?.*//;
487              
488 36 50       133 return $self->_simple_response(500, "Docroot unconfigured")
489             unless $svc->{docroot};
490              
491 36         125 my $file = $svc->{docroot} . $uri;
492              
493             # update state, since we're now waiting on stat
494 36         155 $self->state('wait_stat');
495              
496             Perlbal::AIO::aio_stat($file, sub {
497             # client's gone anyway
498 36 50   36   627 return if $self->{closed};
499 36 100       125 unless (-e _) {
500 2 50       11 return if $self->{service}->run_hook('static_get_poststat_file_missing', $self);
501 2         25 return $self->_simple_response(404);
502             }
503              
504 34         137 my $mtime = (stat(_))[9];
505 34         227 my $lastmod = HTTP::Date::time2str($mtime);
506 34   100     1620 my $ims = $hd->header("If-Modified-Since") || "";
507              
508             # IE sends a request header like "If-Modified-Since: ; length="
509             # so we have to remove the length bit before comparing it with our date.
510             # then we save the length to compare later.
511 34         54 my $ims_len;
512 34 50 66     142 if ($ims && $ims =~ s/; length=(\d+)//) {
513 0         0 $ims_len = $1;
514             }
515              
516 34   66     112 my $not_mod = $ims eq $lastmod && -f _;
517              
518 34         48 my $res;
519 34         52 my $not_satisfiable = 0;
520 34 100       105 my $size = -s _ if -f _;
521              
522             # extra protection for IE, since it's offering the info anyway. (see above)
523 34 50 33     112 $not_mod = 0 if $ims_len && $ims_len != $size;
524              
525 34         175 my ($status, $range_start, $range_end) = $hd->range($size);
526              
527 34 100       332 if ($not_mod) {
    50          
    50          
528 1         5 $res = $self->{res_headers} = Perlbal::HTTPHeaders->new_response(304);
529             } elsif ($status == 416) {
530 0         0 $res = $self->{res_headers} = Perlbal::HTTPHeaders->new_response(416);
531 0 0       0 $res->header("Content-Range", $size ? "bytes */$size" : "*");
532 0         0 $res->header("Content-Length", 0);
533 0         0 $not_satisfiable = 1;
534             } elsif ($status == 206) {
535             # partial content
536 0         0 $res = $self->{res_headers} = Perlbal::HTTPHeaders->new_response(206);
537             } else {
538 33 50       169 return if $self->{service}->run_hook('static_get_poststat_pre_send', $self, $mtime);
539 33         164 $res = $self->{res_headers} = Perlbal::HTTPHeaders->new_response(200);
540             }
541              
542             # now set whether this is keep-alive or not
543 34         146 $res->header("Date", HTTP::Date::time2str());
544 34 100       408 $res->header("Server", "Perlbal") if $self->{service}{server_tokens};
545 34         114 $res->header("Last-Modified", $lastmod);
546              
547 34 100       193 if (-f _) {
    50          
548             # advertise that we support byte range requests
549 27         87 $res->header("Accept-Ranges", "bytes");
550              
551 27 100 66     144 unless ($not_mod || $not_satisfiable) {
552 26         179 my ($ext) = ($file =~ /\.(\w+)$/);
553 26 50 33     258 $res->header("Content-Type",
554             (defined $ext && exists $MimeType->{$ext}) ? $MimeType->{$ext} : "text/plain");
555              
556 26 50       70 unless ($status == 206) {
557 26         81 $res->header("Content-Length", $size);
558             } else {
559 0         0 $res->header("Content-Range", "bytes $range_start-$range_end/$size");
560 0         0 $res->header("Content-Length", $range_end - $range_start + 1);
561             }
562             }
563              
564             # has to happen after content-length is set to work:
565 27         199 $self->setup_keepalive($res);
566              
567 27 50       119 return if $self->{service}->run_hook('modify_response_headers', $self);
568              
569 27 100 100     255 if ($rm eq "HEAD" || $not_mod || $not_satisfiable) {
      66        
570             # we can return already, since we know the size
571 2         13 $self->tcp_cork(1);
572 2         57 $self->state('xfer_resp');
573 2         32 $self->write($res->to_string_ref);
574 2         18 $self->write(sub { $self->http_response_sent; });
  2         114  
575 2         16 return;
576             }
577              
578             # state update
579 25         101 $self->state('wait_open');
580              
581             Perlbal::AIO::aio_open($file, 0, 0, sub {
582 25         54 my $rp_fh = shift;
583              
584             # if client's gone, just close filehandle and abort
585 25 50       92 if ($self->{closed}) {
586 0 0       0 CORE::close($rp_fh) if $rp_fh;
587 0         0 return;
588             }
589              
590             # handle errors
591 25 50       69 if (! $rp_fh) {
592             # couldn't open the file we had already successfully stat'ed.
593             # FIXME: do 500 vs. 404 vs whatever based on $!
594 0         0 return $self->close('aio_open_failure');
595             }
596              
597 25         87 $self->state('xfer_disk');
598 25         245 $self->tcp_cork(1); # cork writes to self
599 25         562 $self->write($res->to_string_ref);
600              
601             # seek if partial content
602 25 50       89 if ($status == 206) {
603 0         0 sysseek($rp_fh, $range_start, &POSIX::SEEK_SET);
604 0         0 $size = $range_end - $range_start + 1;
605             }
606              
607 25         64 $self->{reproxy_file} = $file;
608 25         141 $self->reproxy_fh($rp_fh, $size);
609 25         253 });
610              
611             } elsif (-d _) {
612 7         56 $self->try_index_files($hd, $res, $uri);
613             }
614 36         840 });
615             }
616              
617             sub _serve_request_multiple {
618 9     9   15 my Perlbal::ClientHTTPBase $self = shift;
619 9         16 my ($hd, $uri) = @_;
620              
621 9         21 my @multiple_files;
622             my %statinfo; # file -> [ stat fields ]
623              
624             # double question mark means to serve multiple files, comma
625             # separated after the questions. the uri part before the question
626             # mark is the relative base directory
627 9         63 my ($base, $list) = ($uri =~ /(.+)\?\?(.+)/);
628              
629 9 100       47 unless ($base =~ m!/$!) {
630 1         9 return $self->_simple_response(500, "Base directory (before ??) must end in slash.")
631             }
632              
633             # and remove any trailing ?.+ on the list, so you can do things like cache busting
634             # with a ?v= at the end of a list of files.
635 8         25 $list =~ s/\?.+//;
636              
637 8         21 my Perlbal::Service $svc = $self->{service};
638 8 50       35 return $self->_simple_response(500, "Docroot unconfigured")
639             unless $svc->{docroot};
640              
641 8         37 @multiple_files = split(/,/, $list);
642              
643 8 100       35 return $self->_simple_response(403, "Multiple file serving isn't enabled") unless $svc->{enable_concatenate_get};
644 7 50       19 return $self->_simple_response(403, "Too many files requested") if @multiple_files > 100;
645 7 50       19 return $self->_simple_response(403, "Bogus filenames") if grep { m!(?:\A|/)\.\./! } @multiple_files;
  14         49  
646              
647 7         13 my $remain = @multiple_files + 1; # 1 for the base directory
648 7         17 my $dirbase = $svc->{docroot} . $base;
649 7         17 foreach my $file ('', @multiple_files) {
650             Perlbal::AIO::aio_stat("$dirbase$file", sub {
651 21     21   31 $remain--;
652 21 100       275 $statinfo{$file} = $! ? [] : [ stat(_) ];
653 21 100 66     109 return if $remain || $self->{closed};
654 7         35 $self->_serve_request_multiple_poststat($hd, $dirbase, \@multiple_files, \%statinfo);
655 21         167 });
656             }
657             }
658              
659             sub _serve_request_multiple_poststat {
660 7     7   11 my Perlbal::ClientHTTPBase $self = shift;
661 7         20 my ($hd, $basedir, $filelist, $stats) = @_;
662              
663             # base directory must be a directory
664 7 100 100     73 unless (S_ISDIR($stats->{''}[2] || 0)) {
665 1         6 return $self->_simple_response(404, "Base directory not a directory");
666             }
667              
668             # files must all exist
669 6         9 my $sum_length = 0;
670 6         8 my $most_recent_mod = 0;
671 6         12 my $mime; # undef until set, or defaults to text/plain later
672 6         14 foreach my $f (@$filelist) {
673 12         22 my $stat = $stats->{$f};
674 12 100 100     62 unless (S_ISREG($stat->[2] || 0)) {
675 1 50       5 return if $self->{service}->run_hook('concat_get_poststat_file_missing', $self);
676 1         6 return $self->_simple_response(404, "One or more file does not exist");
677             }
678 11 50 66     89 if (!$mime && $f =~ /\.(\w+)$/ && $MimeType->{$1}) {
      66        
679 6         26 $mime = $MimeType->{$1};
680             }
681 11         21 $sum_length += $stat->[7];
682 11 100       40 $most_recent_mod = $stat->[9] if
683             $stat->[9] >$most_recent_mod;
684             }
685 5   50     17 $mime ||= 'text/plain';
686              
687 5         27 my $lastmod = HTTP::Date::time2str($most_recent_mod);
688 5   100     147 my $ims = $hd->header("If-Modified-Since") || "";
689              
690             # IE sends a request header like "If-Modified-Since: ; length="
691             # so we have to remove the length bit before comparing it with our date.
692             # then we save the length to compare later.
693 5         7 my $ims_len;
694 5 50 66     36 if ($ims && $ims =~ s/; length=(\d+)//) {
695 0         0 $ims_len = $1;
696             }
697              
698             # What is -f _ doing here? don't we detect the existence of all files above in the loop?
699 5   66     19 my $not_mod = $ims eq $lastmod && -f _;
700              
701 5         7 my $res;
702 5 100       16 if ($not_mod) {
703 1         6 $res = $self->{res_headers} = Perlbal::HTTPHeaders->new_response(304);
704             } else {
705 4 50       16 return if $self->{service}->run_hook('concat_get_poststat_pre_send', $self, $most_recent_mod);
706 4         20 $res = $self->{res_headers} = Perlbal::HTTPHeaders->new_response(200);
707 4         13 $res->header("Content-Length", $sum_length);
708             }
709              
710 5         17 $res->header("Date", HTTP::Date::time2str());
711 5 50       33 $res->header("Server", "Perlbal") if $self->{service}{server_tokens};
712 5         15 $res->header("Last-Modified", $lastmod);
713 5         13 $res->header("Content-Type", $mime);
714             # has to happen after content-length is set to work:
715 5         23 $self->setup_keepalive($res);
716 5 50       19 return if $self->{service}->run_hook('modify_response_headers', $self);
717              
718 5 100 66     18 if ($hd->request_method eq "HEAD" || $not_mod) {
719             # we can return already, since we know the size
720 1         6 $self->tcp_cork(1);
721 1         28 $self->state('xfer_resp');
722 1         7 $self->write($res->to_string_ref);
723 1     1   9 $self->write(sub { $self->http_response_sent; });
  1         29  
724 1         34 return;
725             }
726              
727 4         34 $self->tcp_cork(1); # cork writes to self
728 4         97 $self->write($res->to_string_ref);
729 4         19 $self->state('wait_open');
730              
731             # gotta send all files, one by one...
732 4         13 my @remain = @$filelist;
733             $self->{post_sendfile_cb} = sub {
734 12 100   12   28 unless (@remain) {
735 4         20 $self->write(sub { $self->http_response_sent; });
  4         109  
736 4         42 return;
737             }
738              
739 8         12 my $file = shift @remain;
740 8         17 my $fullfile = "$basedir$file";
741 8         17 my $size = $stats->{$file}[7];
742              
743             Perlbal::AIO::aio_open($fullfile, 0, 0, sub {
744 8         8 my $rp_fh = shift;
745              
746             # if client's gone, just close filehandle and abort
747 8 50       20 if ($self->{closed}) {
748 0 0       0 CORE::close($rp_fh) if $rp_fh;
749 0         0 return;
750             }
751              
752             # handle errors
753 8 50       19 if (! $rp_fh) {
754             # couldn't open the file we had already successfully stat'ed.
755             # FIXME: do 500 vs. 404 vs whatever based on $!
756 0         0 return $self->close('aio_open_failure');
757             }
758              
759 8         65 $self->{reproxy_file} = $file;
760 8         31 $self->reproxy_fh($rp_fh, $size);
761 8         44 });
762 4         28 };
763 4         12 $self->{post_sendfile_cb}->();
764             }
765              
766             sub check_req_headers {
767 295     295 0 532 my Perlbal::ClientHTTPBase $self = shift;
768 295         743 my Perlbal::HTTPHeaders $hds = $self->{req_headers};
769              
770 295 50       1774 if ($self->{service}->trusted_ip($self->peer_ip_string)) {
771 0   0     0 my @ips = split /,\s*/, ($hds->header("X-Forwarded-For") || '');
772              
773             # This list may be empty, and that's OK, in that case we should unset the
774             # observed_ip_string, so no matter what we'll use the 0th element, whether
775             # it happens to be an ip string, or undef.
776 0         0 $self->observed_ip_string($ips[0]);
777             }
778              
779 295         1375 return;
780             }
781              
782             sub try_index_files {
783 15     15 0 25 my Perlbal::ClientHTTPBase $self = shift;
784 15         32 my ($hd, $res, $uri, $filepos) = @_;
785              
786             # make sure this starts at 0 initially, and fail if it's past the end
787 15   100     64 $filepos ||= 0;
788 15 50       20 if ($filepos >= scalar(@{$self->{service}->{index_files} || []})) {
  15 100       92  
789 6 100       27 unless ($self->{service}->{dirindexing}) {
790             # just inform them that listing is disabled
791 5         39 $self->_simple_response(200, "Directory listing disabled");
792 5         27 return;
793             }
794              
795             # ensure uri has one and only one trailing slash for better URLs
796 1         7 $uri =~ s!/*$!/!;
797              
798             # open the directory and create an index
799 1         4 my $body = "";
800 1         3 my $file = $self->{service}->{docroot} . $uri;
801              
802 1         4 $res->header("Content-Type", "text/html");
803 1         51 opendir(D, $file);
804 1         62 foreach my $de (sort readdir(D)) {
805 4 100       89 if (-d "$file/$de") {
806 2         8 $body .= "$de
\n";
807             } else {
808 2         8 $body .= "$de
\n";
809             }
810             }
811 1         15 closedir(D);
812              
813 1         4 $body .= "";
814 1         7 $res->header("Content-Length", length($body));
815 1         8 $self->setup_keepalive($res);
816              
817 1         4 $self->state('xfer_resp');
818 1         6 $self->tcp_cork(1); # cork writes to self
819 1         27 $self->write($res->to_string_ref);
820 1         15 $self->write(\$body);
821 1     1   7 $self->write(sub { $self->http_response_sent; });
  1         33  
822 1         6 return;
823             }
824              
825             # construct the file path we need to check
826 9         29 my $file = $self->{service}->{index_files}->[$filepos];
827 9         36 my $fullpath = $self->{service}->{docroot} . $uri . '/' . $file;
828              
829             # now see if it exists
830             Perlbal::AIO::aio_stat($fullpath, sub {
831 9 50   9   46 return if $self->{closed};
832 9 100       63 return $self->try_index_files($hd, $res, $uri, $filepos + 1) unless -f _;
833              
834             # at this point the file exists, so we just want to serve it
835 1         4 $self->{replacement_uri} = $uri . '/' . $file;
836 1         7 return $self->_serve_request($hd);
837 9         86 });
838             }
839              
840             sub _simple_response {
841 34     34   79 my Perlbal::ClientHTTPBase $self = shift;
842 34         75 my ($code, $msg) = @_; # or bodyref
843              
844 34         215 my $res = $self->{res_headers} = Perlbal::HTTPHeaders->new_response($code);
845              
846 34         68 my $body;
847 34 100 66     249 if ($code != 204 && $code != 304) {
848 33         141 $res->header("Content-Type", "text/html");
849 33         142 my $en = $res->http_code_english;
850 33 100       310 $body = "

$code" . ($en ? " - $en" : "") . "

\n";
851 33 100       124 $body .= $msg if $msg;
852 33         160 $res->header('Content-Length', length($body));
853             }
854              
855 34 100       737 $res->header('Server', 'Perlbal') if $self->{service}{server_tokens};
856              
857 34         168 $self->setup_keepalive($res);
858              
859 34         243 $self->state('xfer_resp');
860 34         316 $self->tcp_cork(1); # cork writes to self
861 34         1007 $self->write($res->to_string_ref);
862 34 100       135 if (defined $body) {
863 33 50 33     305 unless ($self->{req_headers} && $self->{req_headers}->request_method eq 'HEAD') {
864             # don't write body for head requests
865 33         109 $self->write(\$body);
866             }
867             }
868 34     34   399 $self->write(sub { $self->http_response_sent; });
  34         1242  
869 34         433 return 1;
870             }
871              
872              
873             sub send_response {
874 17     17 0 34 my Perlbal::ClientHTTPBase $self = shift;
875              
876 17         60 $self->watch_read(0);
877 17         322 $self->watch_write(1);
878 17         650 return $self->_simple_response(@_);
879             }
880              
881             sub send_full_response {
882 1     1 0 107 my Perlbal::ClientHTTPBase $self = shift;
883 1         2 my $code = shift;
884 1   50     5 my $headers = shift || [];
885 1 50       5 my $bref = ref($_[0]) eq 'SCALAR' ? shift : \shift;
886 1   50     11 my $options = shift || {};
887              
888 1         7 my $res = $self->{res_headers} = Perlbal::HTTPHeaders->new_response($code);
889              
890 1         4 while (@$headers) {
891 2         7 my ($name, $value) = splice @$headers, 0, 2;
892 2         8 $res->header($name, $value);
893             }
894              
895 1 50 33     10 if ($code == 204 || $code == 304) {
    50          
896 0         0 $res->header('Content-Length', undef);
897 0         0 $bref = \undef;
898             } elsif (defined $$bref) {
899 1         5 $res->header('Content-Length', length($$bref));
900             }
901              
902 1 50       8 $res->header('Server', 'Perlbal') if $self->{service}{server_tokens};
903             # $res->header('Date', # We should do this
904              
905 1         8 $self->setup_keepalive($res, $options->{persist_client});
906              
907 1         11 $self->state('xfer_resp');
908 1         10 $self->tcp_cork(1); # cork writes to self
909 1         28 $self->write($res->to_string_ref);
910              
911 1 50 33     20 if (defined $$bref && $self->{req_headers} && $self->{req_headers}->request_method ne 'HEAD') {
      33        
912             # don't write body for head requests
913 1         5 $self->write($bref);
914             }
915              
916 1     1   116 $self->write(sub { $self->http_response_sent; });
  1         32  
917 1         9 return 1;
918             }
919              
920             # method that sends a 500 to the user but logs it and any extra information
921             # we have about the error in question
922             sub system_error {
923 0     0 0   my Perlbal::ClientHTTPBase $self = shift;
924 0           my ($msg, $info) = @_;
925              
926             # log to syslog
927 0           Perlbal::log('warning', "system error: $msg ($info)");
928              
929             # and return a 500
930 0           return $self->send_response(500, $msg);
931             }
932              
933 0     0 1   sub event_err { my $self = shift; $self->close('error'); }
  0            
934 0     0 1   sub event_hup { my $self = shift; $self->close('hup'); }
  0            
935              
936             sub _sock_port {
937 0     0     my $name = $_[0];
938 0           my $port = eval { (Socket::sockaddr_in($name))[0] };
  0            
939 0 0         return $port unless $@;
940             # fallback to IPv6:
941 0           return (Socket6::unpack_sockaddr_in($name))[0];
942             }
943              
944             sub as_string {
945 0     0 1   my Perlbal::ClientHTTPBase $self = shift;
946              
947 0           my $ret = $self->SUPER::as_string;
948 0 0         my $name = $self->{sock} ? getsockname($self->{sock}) : undef;
949 0 0         my $lport = $name ? _sock_port($name) : undef;
950 0           my $observed = $self->observed_ip_string;
951 0 0         $ret .= ": localport=$lport" if $lport;
952 0 0         $ret .= "; observed_ip=$observed" if defined $observed;
953 0           $ret .= "; reqs=$self->{requests}";
954 0           $ret .= "; $self->{state}";
955              
956 0           my $hd = $self->{req_headers};
957 0 0         if (defined $hd) {
958 0   0       my $host = $hd->header('Host') || 'unknown';
959 0           $ret .= "; http://$host" . $hd->request_uri;
960             }
961              
962 0           return $ret;
963             }
964              
965             1;
966              
967             # Local Variables:
968             # mode: perl
969             # c-basic-indent: 4
970             # indent-tabs-mode: nil
971             # End: