root/trunk/lib/Perlbal/ClientHTTPBase.pm

Revision 816, 31.0 kB (checked in by hachi, 10 months ago)

need to call end_proxy_request during keepalives

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