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