| 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: |
||||||
| 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= |
||||||
| 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: |
||||||
| 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: |