]>
Commit | Line | Data |
---|---|---|
57f93db1 DM |
1 | package PVE::HTTPServer; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | use Socket qw(IPPROTO_TCP TCP_NODELAY SOMAXCONN); | |
6 | use POSIX qw(strftime EINTR EAGAIN); | |
7 | use Fcntl; | |
8 | use File::stat qw(); | |
9 | use AnyEvent::Strict; | |
10 | use AnyEvent::Util qw(guard fh_nonblocking WSAEWOULDBLOCK WSAEINPROGRESS); | |
11 | use AnyEvent::Handle; | |
12 | use AnyEvent::TLS; | |
13 | use AnyEvent::IO; | |
14 | use AnyEvent::HTTP; | |
15 | use Fcntl (); | |
16 | use Compress::Zlib; | |
17 | use PVE::SafeSyslog; | |
18 | use PVE::INotify; | |
19 | use PVE::RPCEnvironment; | |
20 | use PVE::REST; | |
21 | ||
22 | use URI; | |
23 | use HTTP::Status qw(:constants); | |
24 | use HTTP::Headers; | |
25 | use HTTP::Response; | |
26 | ||
27 | use CGI; # fixme: remove this! | |
28 | # DOS attack prevention | |
29 | # fixme: remove CGI.pm | |
30 | $CGI::DISABLE_UPLOADS = 1; # no uploads | |
31 | $CGI::POST_MAX = 1024 * 10; # max 10K posts | |
32 | ||
57f93db1 DM |
33 | use Data::Dumper; # fixme: remove |
34 | ||
35 | my $known_methods = { | |
36 | GET => 1, | |
37 | POST => 1, | |
38 | PUT => 1, | |
39 | DELETE => 1, | |
40 | }; | |
41 | ||
42 | sub log_request { | |
43 | my ($self, $reqstate) = @_; | |
44 | ||
57f93db1 DM |
45 | my $loginfo = $reqstate->{log}; |
46 | ||
47 | # like apache2 common log format | |
48 | # LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" | |
49 | ||
50 | my $peerip = $reqstate->{peer_host} || '-'; | |
51 | my $userid = $loginfo->{userid} || '-'; | |
52 | my $content_length = defined($loginfo->{content_length}) ? $loginfo->{content_length} : '-'; | |
53 | my $code = $loginfo->{code} || 500; | |
54 | my $requestline = $loginfo->{requestline} || '-'; | |
55 | my $timestr = strftime("%d/%b/%Y:%H:%M:%S %z", localtime()); | |
56 | ||
57 | my $msg = "$peerip - $userid [$timestr] \"$requestline\" $code $content_length\n"; | |
58 | ||
c2e9823c | 59 | $self->write_log($msg); |
57f93db1 DM |
60 | } |
61 | ||
62 | sub log_aborted_request { | |
63 | my ($self, $reqstate, $error) = @_; | |
64 | ||
65 | my $r = $reqstate->{request}; | |
66 | return if !$r; # no active request | |
67 | ||
68 | if ($error) { | |
69 | syslog("err", "problem with client $reqstate->{peer_host}; $error"); | |
70 | } | |
f91072d5 | 71 | |
57f93db1 DM |
72 | $self->log_request($reqstate); |
73 | } | |
74 | ||
75 | sub client_do_disconnect { | |
76 | my ($self, $reqstate) = @_; | |
77 | ||
78 | my $hdl = delete $reqstate->{hdl}; | |
79 | ||
80 | if (!$hdl) { | |
81 | syslog('err', "detected empty handle"); | |
82 | return; | |
83 | } | |
84 | ||
85 | #print "close connection $hdl\n"; | |
86 | ||
87 | shutdown($hdl->{fh}, 1); | |
88 | # clear all handlers | |
f91072d5 | 89 | $hdl->on_drain(undef); |
57f93db1 DM |
90 | $hdl->on_read(undef); |
91 | $hdl->on_eof(undef); | |
92 | $self->{conn_count}--; | |
93 | ||
f91072d5 | 94 | print "$$: CLOSE FH" . $hdl->{fh}->fileno() . " CONN$self->{conn_count}\n" if $self->{debug}; |
57f93db1 DM |
95 | } |
96 | ||
97 | sub finish_response { | |
98 | my ($self, $reqstate) = @_; | |
99 | ||
100 | my $hdl = $reqstate->{hdl}; | |
101 | ||
102 | delete $reqstate->{log}; | |
103 | delete $reqstate->{request}; | |
104 | delete $reqstate->{proto}; | |
105 | ||
106 | if (!$self->{end_loop} && $reqstate->{keep_alive} > 0) { | |
107 | # print "KEEPALIVE $reqstate->{keep_alive}\n"; | |
f91072d5 | 108 | $hdl->on_read(sub { |
57f93db1 DM |
109 | eval { $self->push_request_header($reqstate); }; |
110 | warn $@ if $@; | |
111 | }); | |
112 | } else { | |
113 | $hdl->on_drain (sub { | |
f91072d5 DM |
114 | eval { |
115 | $self->client_do_disconnect($reqstate); | |
116 | }; | |
57f93db1 DM |
117 | warn $@ if $@; |
118 | }); | |
119 | } | |
120 | } | |
121 | ||
122 | sub response { | |
123 | my ($self, $reqstate, $resp, $mtime, $nocomp) = @_; | |
124 | ||
125 | #print "$$: send response: " . Dumper($resp); | |
126 | ||
127 | my $code = $resp->code; | |
128 | my $msg = $resp->message || HTTP::Status::status_message($code); | |
129 | ($msg) = $msg =~m/^(.*)$/m; | |
130 | my $content = $resp->content; | |
131 | ||
132 | if ($code =~ /^(1\d\d|[23]04)$/) { | |
133 | # make sure content we have no content | |
134 | $content = ""; | |
135 | } | |
136 | ||
137 | $reqstate->{keep_alive} = 0 if ($code >= 300) || $self->{end_loop}; | |
138 | ||
139 | $reqstate->{log}->{code} = $code; | |
140 | ||
141 | my $res = "HTTP/1.0 $code $msg\015\012"; | |
f91072d5 | 142 | |
57f93db1 DM |
143 | my $ctime = time(); |
144 | my $date = HTTP::Date::time2str($ctime); | |
145 | $resp->header('Date' => $date); | |
146 | if ($mtime) { | |
147 | $resp->header('Last-Modified' => HTTP::Date::time2str($mtime)); | |
148 | } else { | |
149 | $resp->header('Expires' => $date); | |
150 | $resp->header('Cache-Control' => "max-age=0"); | |
151 | $resp->header("Pragma", "no-cache"); | |
152 | } | |
153 | ||
154 | $resp->header('Server' => "pve-api-daemon/3.0"); | |
155 | ||
156 | my $content_length; | |
f91072d5 | 157 | if ($content) { |
57f93db1 DM |
158 | |
159 | $content_length = length($content); | |
160 | ||
161 | if (!$nocomp && ($content_length > 1024)) { | |
162 | my $comp = Compress::Zlib::memGzip($content); | |
163 | $resp->header('Content-Encoding', 'gzip'); | |
164 | $content = $comp; | |
165 | $content_length = length($content); | |
166 | } | |
167 | $resp->header("Content-Length" => $content_length); | |
168 | $reqstate->{log}->{content_length} = $content_length; | |
f91072d5 | 169 | |
57f93db1 DM |
170 | } else { |
171 | $resp->remove_header("Content-Length"); | |
172 | } | |
f91072d5 | 173 | |
57f93db1 DM |
174 | if ($reqstate->{keep_alive} > 0) { |
175 | $resp->push_header('Connection' => 'Keep-Alive'); | |
176 | } else { | |
177 | $resp->header('Connection' => 'close'); | |
178 | } | |
179 | ||
180 | $res .= $resp->headers_as_string("\015\012"); | |
f91072d5 | 181 | #print "SEND(without content) $res\n" if $self->{debug}; |
57f93db1 DM |
182 | |
183 | $res .= "\015\012"; | |
184 | $res .= $content; | |
185 | ||
186 | $self->log_request($reqstate, $reqstate->{request}); | |
187 | ||
188 | $reqstate->{hdl}->push_write($res); | |
189 | $self->finish_response($reqstate); | |
190 | } | |
191 | ||
192 | sub error { | |
193 | my ($self, $reqstate, $code, $msg, $hdr, $content) = @_; | |
194 | ||
195 | eval { | |
f91072d5 | 196 | my $resp = HTTP::Response->new($code, $msg, $hdr, $content); |
57f93db1 DM |
197 | $self->response($reqstate, $resp); |
198 | }; | |
199 | warn $@ if $@; | |
200 | } | |
201 | ||
202 | sub send_file_start { | |
203 | my ($self, $reqstate, $filename) = @_; | |
204 | ||
205 | eval { | |
206 | # print "SEND FILE $filename\n"; | |
f91072d5 | 207 | # Note: aio_load() this is not really async unless we use IO::AIO! |
57f93db1 DM |
208 | eval { |
209 | ||
210 | my $fh = IO::File->new($filename, '<') || | |
211 | die "$!\n"; | |
212 | my $stat = File::stat::stat($fh) || | |
213 | die "$!\n"; | |
f91072d5 | 214 | |
57f93db1 DM |
215 | my $data; |
216 | my $len = sysread($fh, $data, $stat->size); | |
217 | die "got short file\n" if !defined($len) || $len != $stat->size; | |
218 | ||
219 | my $ct; | |
220 | if ($filename =~ m/\.css$/) { | |
221 | $ct = 'text/css'; | |
f91072d5 | 222 | } elsif ($filename =~ m/\.js$/) { |
57f93db1 | 223 | $ct = 'application/javascript'; |
f91072d5 | 224 | } elsif ($filename =~ m/\.png$/) { |
57f93db1 | 225 | $ct = 'image/png'; |
f91072d5 | 226 | } elsif ($filename =~ m/\.gif$/) { |
57f93db1 | 227 | $ct = 'image/gif'; |
f91072d5 | 228 | } elsif ($filename =~ m/\.jar$/) { |
57f93db1 DM |
229 | $ct = 'application/java-archive'; |
230 | } else { | |
231 | die "unable to detect content type"; | |
232 | } | |
233 | ||
234 | my $header = HTTP::Headers->new(Content_Type => $ct); | |
f91072d5 | 235 | my $resp = HTTP::Response->new(200, "OK", $header, $data); |
57f93db1 DM |
236 | $self->response($reqstate, $resp, $stat->mtime); |
237 | }; | |
238 | if (my $err = $@) { | |
239 | $self->error($reqstate, 501, $err); | |
240 | } | |
241 | }; | |
f91072d5 | 242 | |
57f93db1 DM |
243 | warn $@ if $@; |
244 | } | |
245 | ||
246 | sub proxy_request { | |
247 | my ($self, $reqstate, $r, $clientip, $host, $method, $abs_uri, $ticket, $token, $params) = @_; | |
248 | ||
249 | eval { | |
250 | my $target; | |
251 | if ($host eq 'localhost') { | |
f91072d5 | 252 | $target = "http://$host:85$abs_uri"; |
57f93db1 | 253 | } else { |
f91072d5 | 254 | $target = "https://$host:8006$abs_uri"; |
57f93db1 DM |
255 | } |
256 | ||
257 | my $headers = { | |
258 | PVEDisableProxy => 'true', | |
259 | PVEClientIP => $clientip, | |
260 | }; | |
261 | ||
262 | my $cookie_name = 'PVEAuthCookie'; | |
263 | ||
264 | $headers->{'cookie'} = PVE::REST::create_auth_cookie($ticket) if $ticket; | |
265 | $headers->{'CSRFPreventionToken'} = $token if $token; | |
266 | ||
267 | my $content; | |
268 | ||
269 | if ($method eq 'POST' || $method eq 'PUT') { | |
270 | $headers->{'Content-Type'} = 'application/x-www-form-urlencoded'; | |
271 | # We use a temporary URI object to format | |
272 | # the application/x-www-form-urlencoded content. | |
273 | my $url = URI->new('http:'); | |
274 | $url->query_form(%$params); | |
275 | $content = $url->query; | |
276 | if (defined($content)) { | |
277 | $headers->{'Content-Length'} = length($content); | |
278 | } | |
279 | } | |
280 | ||
57f93db1 | 281 | my $w; $w = http_request( |
f91072d5 DM |
282 | $method => $target, |
283 | headers => $headers, | |
284 | timeout => 30, | |
353fef24 | 285 | recurse => 0, |
f91072d5 | 286 | body => $content, |
353fef24 | 287 | tls_ctx => $self->{tls_ctx}, |
57f93db1 DM |
288 | sub { |
289 | my ($body, $hdr) = @_; | |
290 | ||
291 | undef $w; | |
f91072d5 | 292 | |
57f93db1 DM |
293 | eval { |
294 | my $code = delete $hdr->{Status}; | |
295 | my $msg = delete $hdr->{Reason}; | |
296 | delete $hdr->{URL}; | |
297 | delete $hdr->{HTTPVersion}; | |
298 | my $header = HTTP::Headers->new(%$hdr); | |
299 | my $resp = HTTP::Response->new($code, $msg, $header, $body); | |
300 | $self->response($reqstate, $resp, undef, 1); | |
301 | }; | |
302 | warn $@ if $@; | |
303 | }); | |
304 | }; | |
305 | warn $@ if $@; | |
306 | } | |
307 | ||
308 | my $extract_params = sub { | |
309 | my ($r, $method) = @_; | |
310 | ||
311 | # NOTE: HTTP::Request::Params return undef instead of '' | |
312 | #my $parser = HTTP::Request::Params->new({req => $r}); | |
313 | #my $params = $parser->params; | |
314 | ||
315 | my $post_params = {}; | |
316 | ||
317 | if ($method eq 'PUT' || $method eq 'POST') { | |
318 | $post_params = CGI->new($r->content())->Vars; | |
319 | } | |
320 | ||
321 | my $query_params = CGI->new($r->url->query)->Vars; | |
322 | my $params = $post_params || {}; | |
323 | ||
324 | foreach my $k (keys %{$query_params}) { | |
325 | $params->{$k} = $query_params->{$k}; | |
326 | } | |
327 | ||
328 | return PVE::Tools::decode_utf8_parameters($params); | |
329 | }; | |
330 | ||
331 | sub handle_api2_request { | |
332 | my ($self, $reqstate) = @_; | |
333 | ||
334 | eval { | |
335 | my $r = $reqstate->{request}; | |
336 | my $method = $r->method(); | |
337 | my $path = $r->uri->path(); | |
338 | ||
339 | my ($rel_uri, $format) = PVE::REST::split_abs_uri($path); | |
340 | if (!$format) { | |
341 | $self->error($reqstate, HTTP_NOT_IMPLEMENTED, "no such uri"); | |
342 | return; | |
343 | } | |
344 | ||
345 | my $rpcenv = $self->{rpcenv}; | |
346 | my $headers = $r->headers; | |
347 | ||
348 | my $token = $headers->header('CSRFPreventionToken'); | |
349 | ||
350 | my $cookie = $headers->header('Cookie'); | |
351 | ||
352 | my $ticket = PVE::REST::extract_auth_cookie($cookie); | |
353 | ||
354 | my $params = &$extract_params($r, $method); | |
355 | ||
356 | my $clientip = $headers->header('PVEClientIP'); | |
357 | ||
f91072d5 | 358 | $rpcenv->init_request(params => $params); |
57f93db1 DM |
359 | |
360 | my $res = PVE::REST::rest_handler($rpcenv, $clientip, $method, $path, $rel_uri, $ticket, $token); | |
361 | ||
f91072d5 | 362 | # todo: eval { $userid = $rpcenv->get_user(); }; |
57f93db1 DM |
363 | my $userid = $rpcenv->{user}; # this is faster |
364 | $rpcenv->set_user(undef); # clear after request | |
365 | ||
366 | $reqstate->{log}->{userid} = $userid; | |
f91072d5 | 367 | |
57f93db1 DM |
368 | if ($res->{proxy}) { |
369 | ||
370 | if ($self->{trusted_env}) { | |
371 | $self->error($reqstate, HTTP_INTERNAL_SERVER_ERROR, "proxy not allowed"); | |
372 | return; | |
f91072d5 | 373 | } |
57f93db1 | 374 | |
f91072d5 | 375 | $self->proxy_request($reqstate, $r, $clientip, $res->{proxy}, $method, |
57f93db1 DM |
376 | $r->uri, $ticket, $token, $res->{proxy_params}); |
377 | return; | |
378 | ||
379 | } | |
380 | ||
381 | PVE::REST::prepare_response_data($format, $res); | |
382 | my ($raw, $ct) = PVE::REST::format_response_data($format, $res, $path); | |
383 | ||
384 | my $resp = HTTP::Response->new($res->{status}, $res->{message}); | |
385 | $resp->header("Content-Type" => $ct); | |
386 | $resp->content($raw); | |
387 | $self->response($reqstate, $resp); | |
f91072d5 | 388 | |
57f93db1 DM |
389 | return; |
390 | }; | |
391 | warn $@ if $@; | |
392 | } | |
393 | ||
394 | sub handle_request { | |
395 | my ($self, $reqstate) = @_; | |
396 | ||
397 | #print "REQUEST" . Dumper($reqstate->{request}); | |
398 | ||
399 | eval { | |
400 | my $r = $reqstate->{request}; | |
401 | my $method = $r->method(); | |
402 | my $path = $r->uri->path(); | |
403 | ||
404 | # print "REQUEST $path\n"; | |
405 | ||
406 | if (!$known_methods->{$method}) { | |
407 | my $resp = HTTP::Response->new(HTTP_NOT_IMPLEMENTED, "method '$method' not available"); | |
408 | $self->response($reqstate, $resp); | |
409 | return; | |
410 | } | |
411 | ||
f91072d5 | 412 | if ($path =~ m!/api2!) { |
57f93db1 DM |
413 | $self->handle_api2_request($reqstate); |
414 | return; | |
415 | } | |
416 | ||
417 | if ($self->{pages} && ($method eq 'GET') && (my $handler = $self->{pages}->{$path})) { | |
418 | if (ref($handler) eq 'CODE') { | |
419 | my ($resp, $userid) = &$handler($self, $reqstate->{request}); | |
420 | $self->response($reqstate, $resp); | |
421 | } elsif (ref($handler) eq 'HASH') { | |
422 | if (my $filename = $handler->{file}) { | |
423 | my $fh = IO::File->new($filename) || | |
424 | die "unable to open file '$filename' - $!\n"; | |
425 | send_file_start($self, $reqstate, $filename); | |
426 | } else { | |
427 | die "internal error - no handler"; | |
428 | } | |
429 | } else { | |
430 | die "internal error - no handler"; | |
431 | } | |
432 | return; | |
f91072d5 | 433 | } |
57f93db1 DM |
434 | |
435 | if ($self->{dirs} && ($method eq 'GET')) { | |
436 | # we only allow simple names | |
437 | if ($path =~ m!^(/\S+/)([a-zA-Z0-9\-\_\.]+)$!) { | |
438 | my ($subdir, $file) = ($1, $2); | |
439 | if (my $dir = $self->{dirs}->{$subdir}) { | |
440 | my $filename = "$dir$file"; | |
441 | my $fh = IO::File->new($filename) || | |
442 | die "unable to open file '$filename' - $!\n"; | |
443 | send_file_start($self, $reqstate, $filename); | |
444 | return; | |
445 | } | |
446 | } | |
447 | } | |
448 | ||
449 | die "no such file '$path'"; | |
450 | }; | |
451 | if (my $err = $@) { | |
452 | $self->error($reqstate, 501, $err); | |
453 | } | |
454 | } | |
455 | ||
456 | sub unshift_read_header { | |
457 | my ($self, $reqstate, $state) = @_; | |
458 | ||
459 | $state = {} if !$state; | |
460 | ||
461 | $reqstate->{hdl}->unshift_read(line => sub { | |
462 | my ($hdl, $line) = @_; | |
463 | ||
464 | eval { | |
465 | #print "$$: got header: $line\n"; | |
466 | ||
467 | my $r = $reqstate->{request}; | |
468 | if ($line eq '') { | |
469 | ||
470 | $r->push_header($state->{key}, $state->{val}) | |
471 | if $state->{key}; | |
472 | ||
473 | my $conn = $r->header('Connection'); | |
474 | ||
475 | if ($conn) { | |
476 | $reqstate->{keep_alive} = 0 if $conn =~ m/close/oi; | |
477 | } else { | |
478 | if ($reqstate->{proto}->{ver} < 1001) { | |
479 | $reqstate->{keep_alive} = 0; | |
480 | } | |
481 | } | |
482 | ||
483 | # how much content to read? | |
484 | my $te = $r->header('Transfer-Encoding'); | |
485 | my $len = $r->header('Content-Length'); | |
486 | my $pveclientip = $r->header('PVEClientIP'); | |
487 | ||
f91072d5 | 488 | # fixme: how can we make PVEClientIP header trusted? |
57f93db1 DM |
489 | if ($self->{trusted_env} && $pveclientip) { |
490 | $reqstate->{peer_host} = $pveclientip; | |
491 | } else { | |
492 | $r->header('PVEClientIP', $reqstate->{peer_host}); | |
493 | } | |
494 | ||
495 | if ($te && lc($te) eq 'chunked') { | |
496 | # Handle chunked transfer encoding | |
497 | $self->error($reqstate, 501, "chunked transfer encoding not supported"); | |
498 | } elsif ($te) { | |
499 | $self->error($reqstate, 501, "Unknown transfer encoding '$te'"); | |
500 | } elsif (defined($len)) { | |
501 | $reqstate->{hdl}->unshift_read (chunk => $len, sub { | |
502 | my ($hdl, $data) = @_; | |
503 | $r->content($data); | |
504 | $self->handle_request($reqstate); | |
505 | }); | |
506 | } else { | |
507 | $self->handle_request($reqstate); | |
508 | } | |
509 | } elsif ($line =~ /^([^:\s]+)\s*:\s*(.*)/) { | |
510 | $r->push_header($state->{key}, $state->{val}) if $state->{key}; | |
511 | ($state->{key}, $state->{val}) = ($1, $2); | |
512 | $self->unshift_read_header($reqstate, $state); | |
513 | } elsif ($line =~ /^\s+(.*)/) { | |
514 | $state->{val} .= " $1"; | |
515 | $self->unshift_read_header($reqstate, $state); | |
516 | } else { | |
517 | $self->error($reqstate, 506, "unable to parse request header"); | |
518 | } | |
519 | }; | |
520 | warn $@ if $@; | |
521 | }); | |
522 | }; | |
523 | ||
524 | sub push_request_header { | |
525 | my ($self, $reqstate) = @_; | |
526 | ||
527 | eval { | |
528 | $reqstate->{hdl}->push_read(line => sub { | |
529 | my ($hdl, $line) = @_; | |
530 | ||
531 | eval { | |
532 | #print "got request header: $line\n"; | |
f91072d5 | 533 | |
57f93db1 DM |
534 | $reqstate->{keep_alive}--; |
535 | ||
536 | if ($line =~ /(\S+)\040(\S+)\040HTTP\/(\d+)\.(\d+)/o) { | |
537 | my ($method, $uri, $maj, $min) = ($1, $2, $3, $4); | |
538 | ||
539 | if ($maj != 1) { | |
540 | $self->error($reqstate, 506, "http protocol version $maj.$min not supported"); | |
541 | return; | |
542 | } | |
543 | ||
544 | $self->{request_count}++; # only count valid request headers | |
545 | if ($self->{request_count} >= $self->{max_requests}) { | |
f91072d5 | 546 | $self->{end_loop} = 1; |
57f93db1 DM |
547 | } |
548 | $reqstate->{log} = { requestline => $line }; | |
549 | $reqstate->{proto}->{maj} = $maj; | |
550 | $reqstate->{proto}->{min} = $min; | |
551 | $reqstate->{proto}->{ver} = $maj*1000+$min; | |
552 | $reqstate->{request} = HTTP::Request->new($method, $uri); | |
553 | ||
554 | $self->unshift_read_header($reqstate); | |
555 | } elsif ($line eq '') { | |
556 | # ignore empty lines before requests (browser bugs?) | |
557 | $self->push_request_header($reqstate); | |
558 | } else { | |
559 | $self->error($reqstate, 400, 'bad request'); | |
560 | } | |
561 | }; | |
562 | warn $@ if $@; | |
563 | }); | |
564 | }; | |
565 | warn $@ if $@; | |
566 | } | |
567 | ||
568 | sub accept { | |
569 | my ($self) = @_; | |
570 | ||
571 | my $clientfh; | |
572 | ||
573 | return if $self->{end_loop}; | |
574 | ||
575 | # we need to m make sure that only one process calls accept | |
576 | while (!flock($self->{lockfh}, Fcntl::LOCK_EX())) { | |
577 | next if $! == EINTR; | |
578 | die "could not get lock on file '$self->{lockfile}' - $!\n"; | |
579 | } | |
580 | ||
581 | my $again = 0; | |
582 | my $errmsg; | |
583 | eval { | |
584 | while (!$self->{end_loop} && | |
585 | !defined($clientfh = $self->{socket}->accept()) && | |
586 | ($! == EINTR)) {}; | |
587 | ||
588 | if ($self->{end_loop}) { | |
589 | $again = 0; | |
590 | } else { | |
591 | $again = ($! == EAGAIN || $! == WSAEWOULDBLOCK); | |
592 | if (!defined($clientfh)) { | |
593 | $errmsg = "failed to accept connection: $!\n"; | |
594 | } | |
595 | } | |
596 | }; | |
597 | warn $@ if $@; | |
598 | ||
599 | flock($self->{lockfh}, Fcntl::LOCK_UN()); | |
600 | ||
601 | if (!defined($clientfh)) { | |
602 | return if $again; | |
603 | die $errmsg if $errmsg; | |
604 | } | |
605 | ||
f91072d5 | 606 | fh_nonblocking $clientfh, 1; |
57f93db1 DM |
607 | |
608 | $self->{conn_count}++; | |
609 | ||
57f93db1 DM |
610 | return $clientfh; |
611 | } | |
612 | ||
613 | sub wait_end_loop { | |
614 | my ($self) = @_; | |
615 | ||
616 | $self->{end_loop} = 1; | |
617 | ||
618 | undef $self->{socket_watch}; | |
f91072d5 | 619 | |
57f93db1 DM |
620 | if ($self->{conn_count} <= 0) { |
621 | $self->{end_cond}->send(1); | |
622 | return; | |
623 | } | |
624 | ||
625 | # else we need to wait until all open connections gets closed | |
626 | my $w; $w = AnyEvent->timer (after => 1, interval => 1, cb => sub { | |
627 | eval { | |
f91072d5 | 628 | # todo: test for active connections instead (we can abort idle connections) |
57f93db1 DM |
629 | if ($self->{conn_count} <= 0) { |
630 | undef $w; | |
631 | $self->{end_cond}->send(1); | |
632 | } | |
633 | }; | |
634 | warn $@ if $@; | |
635 | }); | |
636 | } | |
f91072d5 | 637 | |
57f93db1 DM |
638 | sub accept_connections { |
639 | my ($self) = @_; | |
640 | ||
641 | eval { | |
642 | ||
643 | while (my $clientfh = $self->accept()) { | |
644 | ||
645 | my $reqstate = { keep_alive => $self->{keep_alive} }; | |
646 | ||
647 | if (my $sin = getpeername($clientfh)) { | |
648 | my ($pport, $phost) = Socket::unpack_sockaddr_in($sin); | |
649 | ($reqstate->{peer_port}, $reqstate->{peer_host}) = ($pport, Socket::inet_ntoa($phost)); | |
650 | } | |
651 | ||
652 | $reqstate->{hdl} = AnyEvent::Handle->new( | |
653 | fh => $clientfh, | |
654 | rbuf_max => 32768, # fixme: set smaller max read buffer ? | |
655 | timeout => $self->{timeout}, | |
656 | linger => 0, # avoid problems with ssh - really needed ? | |
657 | on_eof => sub { | |
658 | my ($hdl) = @_; | |
659 | eval { | |
660 | $self->log_aborted_request($reqstate); | |
661 | $self->client_do_disconnect($reqstate); | |
662 | }; | |
663 | if (my $err = $@) { syslog('err', $err); } | |
664 | }, | |
f91072d5 | 665 | on_error => sub { |
57f93db1 DM |
666 | my ($hdl, $fatal, $message) = @_; |
667 | eval { | |
668 | $self->log_aborted_request($reqstate, $message); | |
669 | $self->client_do_disconnect($reqstate); | |
670 | }; | |
671 | if (my $err = $@) { syslog('err', "$err"); } | |
672 | }, | |
673 | ($self->{tls_ctx} ? (tls => "accept", tls_ctx => $self->{tls_ctx}) : ())); | |
674 | ||
f91072d5 | 675 | print "$$: ACCEPT FH" . $clientfh->fileno() . " CONN$self->{conn_count}\n" if $self->{debug}; |
57f93db1 DM |
676 | |
677 | $self->push_request_header($reqstate); | |
678 | } | |
679 | }; | |
680 | ||
681 | if (my $err = $@) { | |
682 | syslog('err', $err); | |
683 | $self->{end_loop} = 1; | |
684 | } | |
685 | ||
686 | $self->wait_end_loop() if $self->{end_loop}; | |
687 | } | |
688 | ||
c2e9823c DM |
689 | # Note: We can't open log file in non-blocking mode and use AnyEvent::Handle, |
690 | # because we write from multiple processes, and that would arbitrarily mix output | |
f91072d5 | 691 | # of all processes. |
57f93db1 DM |
692 | sub open_access_log { |
693 | my ($self, $filename) = @_; | |
694 | ||
695 | my $old_mask = umask(0137);; | |
696 | my $logfh = IO::File->new($filename, ">>") || | |
697 | die "unable to open log file '$filename' - $!\n"; | |
698 | umask($old_mask); | |
699 | ||
c2e9823c DM |
700 | $logfh->autoflush(1); |
701 | ||
702 | $self->{logfh} = $logfh; | |
703 | } | |
704 | ||
705 | sub write_log { | |
706 | my ($self, $data) = @_; | |
707 | ||
708 | return if !defined($self->{logfh}) || !$data; | |
709 | ||
710 | my $res = $self->{logfh}->print($data); | |
711 | ||
712 | if (!$res) { | |
713 | delete $self->{logfh}; | |
714 | syslog('err', "error writing access log"); | |
715 | $self->{end_loop} = 1; # terminate asap | |
716 | } | |
57f93db1 DM |
717 | } |
718 | ||
353fef24 DM |
719 | sub atfork_handler { |
720 | my ($self) = @_; | |
721 | ||
722 | eval { | |
723 | # something else do to ? | |
724 | close($self->{socket}); | |
725 | }; | |
726 | warn $@ if $@; | |
727 | } | |
728 | ||
57f93db1 DM |
729 | sub new { |
730 | my ($this, %args) = @_; | |
731 | ||
732 | my $class = ref($this) || $this; | |
733 | ||
f91072d5 | 734 | foreach my $req (qw(socket lockfh lockfile)) { |
57f93db1 DM |
735 | die "misssing required argument '$req'" if !defined($args{$req}); |
736 | } | |
737 | ||
738 | my $self = bless { %args }, $class; | |
739 | ||
f91072d5 DM |
740 | # init inotify |
741 | PVE::INotify::inotify_init(); | |
742 | ||
f91072d5 | 743 | $self->{rpcenv} = PVE::RPCEnvironment->init( |
353fef24 | 744 | $self->{trusted_env} ? 'priv' : 'pub', atfork => sub { $self-> atfork_handler() }); |
f91072d5 | 745 | |
57f93db1 DM |
746 | fh_nonblocking($self->{socket}, 1); |
747 | ||
748 | $self->{end_loop} = 0; | |
749 | $self->{conn_count} = 0; | |
750 | $self->{request_count} = 0; | |
751 | $self->{timeout} = 5 if !$self->{timeout}; | |
752 | $self->{keep_alive} = 0 if !defined($self->{keep_alive}); | |
753 | $self->{max_conn} = 800 if !$self->{max_conn}; | |
754 | $self->{max_requests} = 8000 if !$self->{max_requests}; | |
755 | ||
756 | $self->{end_cond} = AnyEvent->condvar; | |
757 | ||
758 | if ($self->{ssl}) { | |
f91072d5 | 759 | $self->{tls_ctx} = AnyEvent::TLS->new(%{$self->{ssl}}); |
57f93db1 DM |
760 | } |
761 | ||
57f93db1 | 762 | $self->open_access_log($self->{logfile}) if $self->{logfile}; |
f91072d5 | 763 | |
57f93db1 DM |
764 | $self->{socket_watch} = AnyEvent->io(fh => $self->{socket}, poll => 'r', cb => sub { |
765 | eval { | |
766 | if ($self->{conn_count} >= $self->{max_conn}) { | |
767 | my $w; $w = AnyEvent->timer (after => 1, interval => 1, cb => sub { | |
768 | if ($self->{conn_count} < $self->{max_conn}) { | |
769 | undef $w; | |
770 | $self->accept_connections(); | |
771 | } | |
772 | }); | |
773 | } else { | |
774 | $self->accept_connections(); | |
f91072d5 | 775 | } |
57f93db1 DM |
776 | }; |
777 | warn $@ if $@; | |
778 | }); | |
779 | ||
780 | $self->{term_watch} = AnyEvent->signal(signal => "TERM", cb => sub { | |
781 | undef $self->{term_watch}; | |
782 | $self->wait_end_loop(); | |
783 | }); | |
784 | ||
f91072d5 | 785 | $self->{quit_watch} = AnyEvent->signal(signal => "QUIT", cb => sub { |
57f93db1 DM |
786 | undef $self->{quit_watch}; |
787 | $self->wait_end_loop(); | |
788 | }); | |
789 | ||
f91072d5 DM |
790 | $self->{inotify_poll} = AnyEvent->timer(after => 5, interval => 5, cb => sub { |
791 | PVE::INotify::poll(); # read inotify events | |
792 | }); | |
793 | ||
57f93db1 DM |
794 | return $self; |
795 | } | |
796 | ||
797 | sub run { | |
798 | my ($self) = @_; | |
799 | ||
800 | $self->{end_cond}->recv; | |
801 | } | |
802 | ||
803 | 1; |