package PVE::APIServer::AnyEvent; # Note 1: interactions with Crypt::OpenSSL::RSA # # Some handlers (auth_handler) use Crypt::OpenSSL::RSA, which seems to # set the openssl error variable. We need to clear that here, else # AnyEvent::TLS aborts the connection. # Net::SSLeay::ERR_clear_error(); use strict; use warnings; use Time::HiRes qw(usleep ualarm gettimeofday tv_interval); use Socket qw(IPPROTO_TCP TCP_NODELAY SOMAXCONN); use POSIX qw(strftime EINTR EAGAIN); use Fcntl; use IO::File; use File::stat qw(); use File::Find; use MIME::Base64; use Digest::MD5; use Digest::SHA; # use AnyEvent::Strict; # only use this for debugging use AnyEvent::Util qw(guard fh_nonblocking WSAEWOULDBLOCK WSAEINPROGRESS); use AnyEvent::Socket; use AnyEvent::Handle; use Net::SSLeay; use AnyEvent::TLS; use AnyEvent::IO; use AnyEvent::HTTP; use Fcntl (); use Compress::Zlib; use Encode; use PVE::SafeSyslog; use PVE::INotify; use PVE::Tools; use PVE::APIServer::Formatter; use Net::IP; use URI; use URI::Escape; use HTTP::Status qw(:constants); use HTTP::Date; use HTTP::Headers; use HTTP::Request; use HTTP::Response; use Data::Dumper; my $limit_max_headers = 30; my $limit_max_header_size = 8*1024; my $limit_max_post = 64*1024; my $known_methods = { GET => 1, POST => 1, PUT => 1, DELETE => 1, }; my $split_abs_uri = sub { my ($abs_uri, $base_uri) = @_; my ($format, $rel_uri) = $abs_uri =~ m/^\Q$base_uri\E\/+([a-z][a-z0-9]+)(\/.*)?$/; $rel_uri = '/' if !$rel_uri; return wantarray ? ($rel_uri, $format) : $rel_uri; }; sub log_request { my ($self, $reqstate) = @_; my $loginfo = $reqstate->{log}; # like apache2 common log format # LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" return if $loginfo->{written}; # avoid duplicate logs $loginfo->{written} = 1; my $peerip = $reqstate->{peer_host} || '-'; my $userid = $loginfo->{userid} || '-'; my $content_length = defined($loginfo->{content_length}) ? $loginfo->{content_length} : '-'; my $code = $loginfo->{code} || 500; my $requestline = $loginfo->{requestline} || '-'; my $timestr = strftime("%d/%m/%Y:%H:%M:%S %z", localtime()); my $msg = "$peerip - $userid [$timestr] \"$requestline\" $code $content_length\n"; $self->write_log($msg); } sub log_aborted_request { my ($self, $reqstate, $error) = @_; my $r = $reqstate->{request}; return if !$r; # no active request if ($error) { syslog("err", "problem with client $reqstate->{peer_host}; $error"); } $self->log_request($reqstate); } sub cleanup_reqstate { my ($reqstate) = @_; delete $reqstate->{log}; delete $reqstate->{request}; delete $reqstate->{proto}; delete $reqstate->{accept_gzip}; delete $reqstate->{starttime}; if ($reqstate->{tmpfilename}) { unlink $reqstate->{tmpfilename}; delete $reqstate->{tmpfilename}; } } sub client_do_disconnect { my ($self, $reqstate) = @_; cleanup_reqstate($reqstate); my $shutdown_hdl = sub { my $hdl = shift; shutdown($hdl->{fh}, 1); # clear all handlers $hdl->on_drain(undef); $hdl->on_read(undef); $hdl->on_eof(undef); }; if (my $proxyhdl = delete $reqstate->{proxyhdl}) { &$shutdown_hdl($proxyhdl); } my $hdl = delete $reqstate->{hdl}; if (!$hdl) { syslog('err', "detected empty handle"); return; } print "close connection $hdl\n" if $self->{debug}; &$shutdown_hdl($hdl); $self->{conn_count}--; print "$$: CLOSE FH" . $hdl->{fh}->fileno() . " CONN$self->{conn_count}\n" if $self->{debug}; } sub finish_response { my ($self, $reqstate) = @_; cleanup_reqstate($reqstate); my $hdl = $reqstate->{hdl}; return if !$hdl; # already disconnected if (!$self->{end_loop} && $reqstate->{keep_alive} > 0) { # print "KEEPALIVE $reqstate->{keep_alive}\n" if $self->{debug}; $hdl->on_read(sub { eval { $self->push_request_header($reqstate); }; warn $@ if $@; }); } else { $hdl->on_drain (sub { eval { $self->client_do_disconnect($reqstate); }; warn $@ if $@; }); } } sub response { my ($self, $reqstate, $resp, $mtime, $nocomp, $delay) = @_; #print "$$: send response: " . Dumper($resp); # activate timeout $reqstate->{hdl}->timeout_reset(); $reqstate->{hdl}->timeout($self->{timeout}); $nocomp = 1 if !$self->{compression}; $nocomp = 1 if !$reqstate->{accept_gzip}; my $code = $resp->code; my $msg = $resp->message || HTTP::Status::status_message($code); ($msg) = $msg =~m/^(.*)$/m; my $content = $resp->content; if ($code =~ /^(1\d\d|[23]04)$/) { # make sure content we have no content $content = ""; } $reqstate->{keep_alive} = 0 if ($code >= 400) || $self->{end_loop}; $reqstate->{log}->{code} = $code; my $proto = $reqstate->{proto} ? $reqstate->{proto}->{str} : 'HTTP/1.0'; my $res = "$proto $code $msg\015\012"; my $ctime = time(); my $date = HTTP::Date::time2str($ctime); $resp->header('Date' => $date); if ($mtime) { $resp->header('Last-Modified' => HTTP::Date::time2str($mtime)); } else { $resp->header('Expires' => $date); $resp->header('Cache-Control' => "max-age=0"); $resp->header("Pragma", "no-cache"); } $resp->header('Server' => "pve-api-daemon/3.0"); my $content_length; if ($content) { $content_length = length($content); if (!$nocomp && ($content_length > 1024)) { my $comp = Compress::Zlib::memGzip($content); $resp->header('Content-Encoding', 'gzip'); $content = $comp; $content_length = length($content); } $resp->header("Content-Length" => $content_length); $reqstate->{log}->{content_length} = $content_length; } else { $resp->remove_header("Content-Length"); } if ($reqstate->{keep_alive} > 0) { $resp->push_header('Connection' => 'Keep-Alive'); } else { $resp->header('Connection' => 'close'); } $res .= $resp->headers_as_string("\015\012"); #print "SEND(without content) $res\n" if $self->{debug}; $res .= "\015\012"; $res .= $content if $content; $self->log_request($reqstate, $reqstate->{request}); if ($delay && $delay > 0) { my $w; $w = AnyEvent->timer(after => $delay, cb => sub { undef $w; # delete reference $reqstate->{hdl}->push_write($res); $self->finish_response($reqstate); }); } else { $reqstate->{hdl}->push_write($res); $self->finish_response($reqstate); } } sub error { my ($self, $reqstate, $code, $msg, $hdr, $content) = @_; eval { my $resp = HTTP::Response->new($code, $msg, $hdr, $content); $self->response($reqstate, $resp); }; warn $@ if $@; } my $file_extension_info = { css => { ct => 'text/css' }, html => { ct => 'text/html' }, js => { ct => 'application/javascript' }, json => { ct => 'application/json' }, map => { ct => 'application/json' }, png => { ct => 'image/png' , nocomp => 1 }, ico => { ct => 'image/x-icon', nocomp => 1}, gif => { ct => 'image/gif', nocomp => 1}, svg => { ct => 'image/svg+xml' }, jar => { ct => 'application/java-archive', nocomp => 1}, woff => { ct => 'application/font-woff', nocomp => 1}, woff2 => { ct => 'application/font-woff2', nocomp => 1}, ttf => { ct => 'application/font-snft', nocomp => 1}, pdf => { ct => 'application/pdf', nocomp => 1}, epub => { ct => 'application/epub+zip', nocomp => 1}, mp3 => { ct => 'audio/mpeg', nocomp => 1}, oga => { ct => 'audio/ogg', nocomp => 1}, tgz => { ct => 'application/x-compressed-tar', nocomp => 1}, }; sub send_file_start { my ($self, $reqstate, $filename) = @_; eval { # print "SEND FILE $filename\n"; # Note: aio_load() this is not really async unless we use IO::AIO! eval { my $r = $reqstate->{request}; my $fh = IO::File->new($filename, '<') || die "$!\n"; my $stat = File::stat::stat($fh) || die "$!\n"; my $mtime = $stat->mtime; if (my $ifmod = $r->header('if-modified-since')) { my $iftime = HTTP::Date::str2time($ifmod); if ($mtime <= $iftime) { my $resp = HTTP::Response->new(304, "NOT MODIFIED"); $self->response($reqstate, $resp, $mtime); return; } } my $data; my $len = sysread($fh, $data, $stat->size); die "got short file\n" if !defined($len) || $len != $stat->size; my ($ext) = $filename =~ m/\.([^.]*)$/; my $ext_info = $file_extension_info->{$ext}; die "unable to detect content type" if !$ext_info; my $header = HTTP::Headers->new(Content_Type => $ext_info->{ct}); my $resp = HTTP::Response->new(200, "OK", $header, $data); $self->response($reqstate, $resp, $mtime, $ext_info->{nocomp}); }; if (my $err = $@) { $self->error($reqstate, 501, $err); } }; warn $@ if $@; } sub websocket_proxy { my ($self, $reqstate, $wsaccept, $wsproto, $param) = @_; eval { my $remhost; my $remport; my $max_payload_size = 128*1024; my $binary; if ($wsproto eq 'binary') { $binary = 1; } elsif ($wsproto eq 'base64') { $binary = 0; } else { die "websocket_proxy: unsupported protocol '$wsproto'\n"; } if ($param->{port}) { $remhost = 'localhost'; $remport = $param->{port}; } elsif ($param->{socket}) { $remhost = 'unix/'; $remport = $param->{socket}; } else { die "websocket_proxy: missing port or socket\n"; } tcp_connect $remhost, $remport, sub { my ($fh) = @_ or die "connect to '$remhost:$remport' failed: $!"; print "$$: CONNECTed to '$remhost:$remport'\n" if $self->{debug}; $reqstate->{proxyhdl} = AnyEvent::Handle->new( fh => $fh, rbuf_max => $max_payload_size, wbuf_max => $max_payload_size*5, timeout => 5, on_eof => sub { my ($hdl) = @_; eval { $self->log_aborted_request($reqstate); $self->client_do_disconnect($reqstate); }; if (my $err = $@) { syslog('err', $err); } }, on_error => sub { my ($hdl, $fatal, $message) = @_; eval { $self->log_aborted_request($reqstate, $message); $self->client_do_disconnect($reqstate); }; if (my $err = $@) { syslog('err', "$err"); } }); my $proxyhdlreader = sub { my ($hdl) = @_; my $len = length($hdl->{rbuf}); my $data = substr($hdl->{rbuf}, 0, $len > $max_payload_size ? $max_payload_size : $len, ''); my $string; my $payload; if ($binary) { $string = "\x82"; # binary frame $payload = $data; } else { $string = "\x81"; # text frame $payload = encode_base64($data, ''); } my $payload_len = length($payload); if ($payload_len <= 125) { $string .= pack 'C', $payload_len; } elsif ($payload_len <= 0xffff) { $string .= pack 'C', 126; $string .= pack 'n', $payload_len; } else { $string .= pack 'C', 127; $string .= pack 'Q>', $payload_len; } $string .= $payload; $reqstate->{hdl}->push_write($string) if $reqstate->{hdl}; }; my $hdlreader = sub { my ($hdl) = @_; while (my $len = length($hdl->{rbuf})) { return if $len < 2; my $hdr = unpack('C', substr($hdl->{rbuf}, 0, 1)); my $opcode = $hdr & 0b00001111; my $fin = $hdr & 0b10000000; die "received fragmented websocket frame\n" if !$fin; my $rsv = $hdr & 0b01110000; die "received websocket frame with RSV flags\n" if $rsv; my $payload_len = unpack 'C', substr($hdl->{rbuf}, 1, 1); my $masked = $payload_len & 0b10000000; die "received unmasked websocket frame from client\n" if !$masked; my $offset = 2; $payload_len = $payload_len & 0b01111111; if ($payload_len == 126) { return if $len < 4; $payload_len = unpack('n', substr($hdl->{rbuf}, $offset, 2)); $offset += 2; } elsif ($payload_len == 127) { return if $len < 10; $payload_len = unpack('Q>', substr($hdl->{rbuf}, $offset, 8)); $offset += 8; } die "received too large websocket frame (len = $payload_len)\n" if ($payload_len > $max_payload_size) || ($payload_len < 0); return if $len < ($offset + 4 + $payload_len); my $data = substr($hdl->{rbuf}, 0, $offset + 4 + $payload_len, ''); # now consume data my @mask = (unpack('C', substr($data, $offset+0, 1)), unpack('C', substr($data, $offset+1, 1)), unpack('C', substr($data, $offset+2, 1)), unpack('C', substr($data, $offset+3, 1))); $offset += 4; my $payload = substr($data, $offset, $payload_len); for (my $i = 0; $i < $payload_len; $i++) { my $d = unpack('C', substr($payload, $i, 1)); my $n = $d ^ $mask[$i % 4]; substr($payload, $i, 1, pack('C', $n)); } $payload = decode_base64($payload) if !$binary; if ($opcode == 1 || $opcode == 2) { $reqstate->{proxyhdl}->push_write($payload) if $reqstate->{proxyhdl}; } elsif ($opcode == 8) { my $statuscode = unpack ("n", $payload); print "websocket received close. status code: '$statuscode'\n" if $self->{debug}; if ($reqstate->{proxyhdl}) { $reqstate->{proxyhdl}->push_shutdown(); } $hdl->push_shutdown(); } else { die "received unhandled websocket opcode $opcode\n"; } } }; my $proto = $reqstate->{proto} ? $reqstate->{proto}->{str} : 'HTTP/1.1'; $reqstate->{proxyhdl}->timeout(0); $reqstate->{proxyhdl}->on_read($proxyhdlreader); $reqstate->{hdl}->on_read($hdlreader); # todo: use stop_read/start_read if write buffer grows to much my $res = "$proto 101 Switching Protocols\015\012" . "Upgrade: websocket\015\012" . "Connection: upgrade\015\012" . "Sec-WebSocket-Accept: $wsaccept\015\012" . "Sec-WebSocket-Protocol: $wsproto\015\012" . "\015\012"; print $res if $self->{debug}; $reqstate->{hdl}->push_write($res); # log early $reqstate->{log}->{code} = 101; $self->log_request($reqstate); }; }; if (my $err = $@) { warn $err; $self->log_aborted_request($reqstate, $err); $self->client_do_disconnect($reqstate); } } sub proxy_request { my ($self, $reqstate, $clientip, $host, $node, $method, $uri, $ticket, $token, $params) = @_; eval { my $target; my $keep_alive = 1; if ($host eq 'localhost') { $target = "http://$host:85$uri"; # keep alive for localhost is not worth (connection setup is about 0.2ms) $keep_alive = 0; } elsif (Net::IP::ip_is_ipv6($host)) { $target = "https://[$host]:8006$uri"; } else { $target = "https://$host:8006$uri"; } my $headers = { PVEDisableProxy => 'true', PVEClientIP => $clientip, }; $headers->{'cookie'} = PVE::APIServer::Formatter::create_auth_cookie($ticket, $self->{cookie_name}) if $ticket; $headers->{'CSRFPreventionToken'} = $token if $token; $headers->{'Accept-Encoding'} = 'gzip' if ($reqstate->{accept_gzip} && $self->{compression}); my $content; if ($method eq 'POST' || $method eq 'PUT') { $headers->{'Content-Type'} = 'application/x-www-form-urlencoded'; # use URI object to format application/x-www-form-urlencoded content. my $url = URI->new('http:'); $url->query_form(%$params); $content = $url->query; if (defined($content)) { $headers->{'Content-Length'} = length($content); } } my $tls = { # TLS 1.x only, with certificate pinning method => 'any', sslv2 => 0, sslv3 => 0, verify => 1, # be compatible with openssl 1.1, fix for debian bug #923615 # remove once libanyeven-perl with this fix transitions to buster dh => 'schmorp2048', verify_cb => sub { my (undef, undef, undef, $depth, undef, undef, $cert) = @_; # we don't care about intermediate or root certificates return 1 if $depth != 0; # check server certificate against cache of pinned FPs return $self->check_cert_fingerprint($cert); }, }; # load and cache cert fingerprint if first time we proxy to this node $self->initialize_cert_cache($node); my $w; $w = http_request( $method => $target, headers => $headers, timeout => 30, recurse => 0, proxy => undef, # avoid use of $ENV{HTTP_PROXY} keepalive => $keep_alive, body => $content, tls_ctx => AnyEvent::TLS->new(%{$tls}), sub { my ($body, $hdr) = @_; undef $w; if (!$reqstate->{hdl}) { warn "proxy detected vanished client connection\n"; return; } eval { my $code = delete $hdr->{Status}; my $msg = delete $hdr->{Reason}; delete $hdr->{URL}; delete $hdr->{HTTPVersion}; my $header = HTTP::Headers->new(%$hdr); if (my $location = $header->header('Location')) { $location =~ s|^http://localhost:85||; $header->header(Location => $location); } my $resp = HTTP::Response->new($code, $msg, $header, $body); # Note: disable compression, because body is already compressed $self->response($reqstate, $resp, undef, 1); }; warn $@ if $@; }); }; warn $@ if $@; } # return arrays as \0 separated strings (like CGI.pm) # assume data is UTF8 encoded sub decode_urlencoded { my ($data) = @_; my $res = {}; return $res if !$data; foreach my $kv (split(/[\&\;]/, $data)) { my ($k, $v) = split(/=/, $kv); $k =~s/\+/ /g; $k =~ s/%([0-9a-fA-F][0-9a-fA-F])/chr(hex($1))/eg; $v =~s/\+/ /g; $v =~ s/%([0-9a-fA-F][0-9a-fA-F])/chr(hex($1))/eg; $v = Encode::decode('utf8', $v); if (defined(my $old = $res->{$k})) { $res->{$k} = "$old\0$v"; } else { $res->{$k} = $v; } } return $res; } sub extract_params { my ($r, $method) = @_; my $params = {}; if ($method eq 'PUT' || $method eq 'POST') { $params = decode_urlencoded($r->content); } my $query_params = decode_urlencoded($r->url->query()); foreach my $k (keys %{$query_params}) { $params->{$k} = $query_params->{$k}; } return $params; } sub handle_api2_request { my ($self, $reqstate, $auth, $method, $path, $upload_state) = @_; eval { my $r = $reqstate->{request}; my ($rel_uri, $format) = &$split_abs_uri($path, $self->{base_uri}); my $formatter = PVE::APIServer::Formatter::get_formatter($format, $method, $rel_uri); if (!defined($formatter)) { $self->error($reqstate, HTTP_NOT_IMPLEMENTED, "no formatter for uri $rel_uri, $format"); return; } #print Dumper($upload_state) if $upload_state; my $params; if ($upload_state) { $params = $upload_state->{params}; } else { $params = extract_params($r, $method); } delete $params->{_dc}; # remove disable cache parameter my $clientip = $reqstate->{peer_host}; my $res = $self->rest_handler($clientip, $method, $rel_uri, $auth, $params, $format); # HACK: see Note 1 Net::SSLeay::ERR_clear_error(); AnyEvent->now_update(); # in case somebody called sleep() my $upgrade = $r->header('upgrade'); $upgrade = lc($upgrade) if $upgrade; if (my $host = $res->{proxy}) { if ($self->{trusted_env}) { $self->error($reqstate, HTTP_INTERNAL_SERVER_ERROR, "proxy not allowed"); return; } if ($host ne 'localhost' && $r->header('PVEDisableProxy')) { $self->error($reqstate, HTTP_INTERNAL_SERVER_ERROR, "proxy loop detected"); return; } $res->{proxy_params}->{tmpfilename} = $reqstate->{tmpfilename} if $upload_state; $self->proxy_request($reqstate, $clientip, $host, $res->{proxynode}, $method, $r->uri, $auth->{ticket}, $auth->{token}, $res->{proxy_params}, $res->{proxynode}); return; } elsif ($upgrade && ($method eq 'GET') && ($path =~ m|websocket$|)) { die "unable to upgrade to protocol '$upgrade'\n" if !$upgrade || ($upgrade ne 'websocket'); my $wsver = $r->header('sec-websocket-version'); die "unsupported websocket-version '$wsver'\n" if !$wsver || ($wsver ne '13'); my $wsproto_str = $r->header('sec-websocket-protocol'); die "missing websocket-protocol header" if !$wsproto_str; my $wsproto; foreach my $p (PVE::Tools::split_list($wsproto_str)) { $wsproto = $p if !$wsproto && $p eq 'base64'; $wsproto = $p if $p eq 'binary'; } die "unsupported websocket-protocol protocol '$wsproto_str'\n" if !$wsproto; my $wskey = $r->header('sec-websocket-key'); die "missing websocket-key\n" if !$wskey; # Note: Digest::SHA::sha1_base64 has wrong padding my $wsaccept = Digest::SHA::sha1_base64("${wskey}258EAFA5-E914-47DA-95CA-C5AB0DC85B11") . "="; if ($res->{status} == HTTP_OK) { $self->websocket_proxy($reqstate, $wsaccept, $wsproto, $res->{data}); return; } } my $delay = 0; if ($res->{status} == HTTP_UNAUTHORIZED) { # always delay unauthorized calls by 3 seconds $delay = 3 - tv_interval($reqstate->{starttime}); $delay = 0 if $delay < 0; } if (defined(my $filename = $res->{download})) { my $fh = IO::File->new($filename) || die "unable to open file '$filename' - $!\n"; send_file_start($self, $reqstate, $filename); return; } my ($raw, $ct, $nocomp) = $formatter->($res, $res->{data}, $params, $path, $auth, $self->{formatter_config}); my $resp; if (ref($raw) && (ref($raw) eq 'HTTP::Response')) { $resp = $raw; } else { $resp = HTTP::Response->new($res->{status}, $res->{message}); $resp->header("Content-Type" => $ct); $resp->content($raw); } $self->response($reqstate, $resp, undef, $nocomp, $delay); }; if (my $err = $@) { $self->error($reqstate, 501, $err); } } sub handle_spice_proxy_request { my ($self, $reqstate, $connect_str, $vmid, $node, $spiceport) = @_; eval { die "Port $spiceport is not allowed" if ($spiceport < 61000 || $spiceport > 61099); my $clientip = $reqstate->{peer_host}; my $r = $reqstate->{request}; my $remip; if ($node ne 'localhost' && PVE::INotify::nodename() !~ m/^$node$/i) { $remip = $self->remote_node_ip($node); print "REMOTE CONNECT $vmid, $remip, $connect_str\n" if $self->{debug}; } else { print "$$: CONNECT $vmid, $node, $spiceport\n" if $self->{debug}; } if ($remip && $r->header('PVEDisableProxy')) { $self->error($reqstate, HTTP_INTERNAL_SERVER_ERROR, "proxy loop detected"); return; } $reqstate->{hdl}->timeout(0); $reqstate->{hdl}->wbuf_max(64*10*1024); my $remhost = $remip ? $remip : "localhost"; my $remport = $remip ? 3128 : $spiceport; tcp_connect $remhost, $remport, sub { my ($fh) = @_ or die "connect to '$remhost:$remport' failed: $!"; print "$$: CONNECTed to '$remhost:$remport'\n" if $self->{debug}; $reqstate->{proxyhdl} = AnyEvent::Handle->new( fh => $fh, rbuf_max => 64*1024, wbuf_max => 64*10*1024, timeout => 5, on_eof => sub { my ($hdl) = @_; eval { $self->log_aborted_request($reqstate); $self->client_do_disconnect($reqstate); }; if (my $err = $@) { syslog('err', $err); } }, on_error => sub { my ($hdl, $fatal, $message) = @_; eval { $self->log_aborted_request($reqstate, $message); $self->client_do_disconnect($reqstate); }; if (my $err = $@) { syslog('err', "$err"); } }); my $proxyhdlreader = sub { my ($hdl) = @_; my $len = length($hdl->{rbuf}); my $data = substr($hdl->{rbuf}, 0, $len, ''); #print "READ1 $len\n"; $reqstate->{hdl}->push_write($data) if $reqstate->{hdl}; }; my $hdlreader = sub { my ($hdl) = @_; my $len = length($hdl->{rbuf}); my $data = substr($hdl->{rbuf}, 0, $len, ''); #print "READ0 $len\n"; $reqstate->{proxyhdl}->push_write($data) if $reqstate->{proxyhdl}; }; my $proto = $reqstate->{proto} ? $reqstate->{proto}->{str} : 'HTTP/1.0'; my $startproxy = sub { $reqstate->{proxyhdl}->timeout(0); $reqstate->{proxyhdl}->on_read($proxyhdlreader); $reqstate->{hdl}->on_read($hdlreader); # todo: use stop_read/start_read if write buffer grows to much # a response must be followed by an empty line my $res = "$proto 200 OK\015\012\015\012"; $reqstate->{hdl}->push_write($res); # log early $reqstate->{log}->{code} = 200; $self->log_request($reqstate); }; if ($remip) { my $header = "CONNECT ${connect_str} $proto\015\012" . "Host: ${connect_str}\015\012" . "Proxy-Connection: keep-alive\015\012" . "User-Agent: spiceproxy\015\012" . "PVEDisableProxy: true\015\012" . "PVEClientIP: $clientip\015\012" . "\015\012"; $reqstate->{proxyhdl}->push_write($header); $reqstate->{proxyhdl}->push_read(line => sub { my ($hdl, $line) = @_; if ($line =~ m!^$proto 200 OK$!) { # read the empty line after the 200 OK $reqstate->{proxyhdl}->unshift_read(line => sub{ &$startproxy(); }); } else { $reqstate->{hdl}->push_write($line); $self->client_do_disconnect($reqstate); } }); } else { &$startproxy(); } }; }; if (my $err = $@) { warn $err; $self->log_aborted_request($reqstate, $err); $self->client_do_disconnect($reqstate); } } sub handle_request { my ($self, $reqstate, $auth, $method, $path) = @_; my $base_uri = $self->{base_uri}; eval { my $r = $reqstate->{request}; # disable timeout on handle (we already have all data we need) # we re-enable timeout in response() $reqstate->{hdl}->timeout(0); if ($path =~ m/^\Q$base_uri\E/) { $self->handle_api2_request($reqstate, $auth, $method, $path); return; } if ($self->{pages} && ($method eq 'GET') && (my $handler = $self->{pages}->{$path})) { if (ref($handler) eq 'CODE') { my $params = decode_urlencoded($r->url->query()); my ($resp, $userid) = &$handler($self, $reqstate->{request}, $params); # HACK: see Note 1 Net::SSLeay::ERR_clear_error(); $self->response($reqstate, $resp); } elsif (ref($handler) eq 'HASH') { if (my $filename = $handler->{file}) { my $fh = IO::File->new($filename) || die "unable to open file '$filename' - $!\n"; send_file_start($self, $reqstate, $filename); } else { die "internal error - no handler"; } } else { die "internal error - no handler"; } return; } if ($self->{dirs} && ($method eq 'GET')) { # we only allow simple names if ($path =~ m!^(/\S+/)([a-zA-Z0-9\-\_\.]+)$!) { my ($subdir, $file) = ($1, $2); if (my $dir = $self->{dirs}->{$subdir}) { my $filename = "$dir$file"; my $fh = IO::File->new($filename) || die "unable to open file '$filename' - $!\n"; send_file_start($self, $reqstate, $filename); return; } } } die "no such file '$path'\n"; }; if (my $err = $@) { $self->error($reqstate, 501, $err); } } sub file_upload_multipart { my ($self, $reqstate, $auth, $method, $path, $rstate) = @_; eval { my $boundary = $rstate->{boundary}; my $hdl = $reqstate->{hdl}; my $startlen = length($hdl->{rbuf}); if ($rstate->{phase} == 0) { # skip everything until start if ($hdl->{rbuf} =~ s/^.*?--\Q$boundary\E \015?\012 ((?:[^\015]+\015\012)* ) \015?\012//xs) { my $header = $1; my ($ct, $disp, $name, $filename); foreach my $line (split(/\015?\012/, $header)) { # assume we have single line headers if ($line =~ m/^Content-Type\s*:\s*(.*)/i) { $ct = parse_content_type($1); } elsif ($line =~ m/^Content-Disposition\s*:\s*(.*)/i) { ($disp, $name, $filename) = parse_content_disposition($1); } } if (!($disp && $disp eq 'form-data' && $name)) { syslog('err', "wrong content disposition in multipart - abort upload"); $rstate->{phase} = -1; } else { $rstate->{fieldname} = $name; if ($filename) { if ($name eq 'filename') { # found file upload data $rstate->{phase} = 1; $rstate->{filename} = $filename; } else { syslog('err', "wrong field name for file upload - abort upload"); $rstate->{phase} = -1; } } else { # found form data for field $name $rstate->{phase} = 2; } } } else { my $len = length($hdl->{rbuf}); substr($hdl->{rbuf}, 0, $len - $rstate->{maxheader}, '') if $len > $rstate->{maxheader}; # skip garbage } } elsif ($rstate->{phase} == 1) { # inside file - dump until end marker if ($hdl->{rbuf} =~ s/^(.*?)\015?\012(--\Q$boundary\E(--)? \015?\012(.*))$/$2/xs) { my ($rest, $eof) = ($1, $3); my $len = length($rest); die "write to temporary file failed - $!" if syswrite($rstate->{outfh}, $rest) != $len; $rstate->{ctx}->add($rest); $rstate->{params}->{filename} = $rstate->{filename}; $rstate->{md5sum} = $rstate->{ctx}->hexdigest; $rstate->{bytes} += $len; $rstate->{phase} = $eof ? 100 : 0; } else { my $len = length($hdl->{rbuf}); my $wlen = $len - $rstate->{boundlen}; if ($wlen > 0) { my $data = substr($hdl->{rbuf}, 0, $wlen, ''); die "write to temporary file failed - $!" if syswrite($rstate->{outfh}, $data) != $wlen; $rstate->{bytes} += $wlen; $rstate->{ctx}->add($data); } } } elsif ($rstate->{phase} == 2) { # inside normal field if ($hdl->{rbuf} =~ s/^(.*?)\015?\012(--\Q$boundary\E(--)? \015?\012(.*))$/$2/xs) { my ($rest, $eof) = ($1, $3); my $len = length($rest); $rstate->{post_size} += $len; if ($rstate->{post_size} < $limit_max_post) { $rstate->{params}->{$rstate->{fieldname}} = $rest; $rstate->{phase} = $eof ? 100 : 0; } else { syslog('err', "form data to large - abort upload"); $rstate->{phase} = -1; # skip } } } else { # skip my $len = length($hdl->{rbuf}); substr($hdl->{rbuf}, 0, $len, ''); # empty rbuf } $rstate->{read} += ($startlen - length($hdl->{rbuf})); if (!$rstate->{done} && ($rstate->{read} + length($hdl->{rbuf})) >= $rstate->{size}) { $rstate->{done} = 1; # make sure we dont get called twice if ($rstate->{phase} < 0 || !$rstate->{md5sum}) { die "upload failed\n"; } else { my $elapsed = tv_interval($rstate->{starttime}); my $rate = int($rstate->{bytes}/($elapsed*1024*1024)); syslog('info', "multipart upload complete " . "(size: %d time: %ds rate: %.2fMiB/s md5sum: $rstate->{md5sum})", $rstate->{bytes}, $elapsed, $rate); $self->handle_api2_request($reqstate, $auth, $method, $path, $rstate); } } }; if (my $err = $@) { syslog('err', $err); $self->error($reqstate, 501, $err); } } sub parse_content_type { my ($ctype) = @_; my ($ct, @params) = split(/\s*[;,]\s*/o, $ctype); foreach my $v (@params) { if ($v =~ m/^\s*boundary\s*=\s*(\S+?)\s*$/o) { return wantarray ? ($ct, $1) : $ct; } } return wantarray ? ($ct) : $ct; } sub parse_content_disposition { my ($line) = @_; my ($disp, @params) = split(/\s*[;,]\s*/o, $line); my $name; my $filename; foreach my $v (@params) { if ($v =~ m/^\s*name\s*=\s*(\S+?)\s*$/o) { $name = $1; $name =~ s/^"(.*)"$/$1/; } elsif ($v =~ m/^\s*filename\s*=\s*(.+?)\s*$/o) { $filename = $1; $filename =~ s/^"(.*)"$/$1/; } } return wantarray ? ($disp, $name, $filename) : $disp; } my $tmpfile_seq_no = 0; sub get_upload_filename { # choose unpredictable tmpfile name $tmpfile_seq_no++; return "/var/tmp/pveupload-" . Digest::MD5::md5_hex($tmpfile_seq_no . time() . $$); } sub unshift_read_header { my ($self, $reqstate, $state) = @_; $state = { size => 0, count => 0 } if !$state; $reqstate->{hdl}->unshift_read(line => sub { my ($hdl, $line) = @_; eval { # print "$$: got header: $line\n" if $self->{debug}; die "to many http header lines\n" if ++$state->{count} >= $limit_max_headers; die "http header too large\n" if ($state->{size} += length($line)) >= $limit_max_header_size; my $r = $reqstate->{request}; if ($line eq '') { my $path = uri_unescape($r->uri->path()); my $method = $r->method(); $r->push_header($state->{key}, $state->{val}) if $state->{key}; if (!$known_methods->{$method}) { my $resp = HTTP::Response->new(HTTP_NOT_IMPLEMENTED, "method '$method' not available"); $self->response($reqstate, $resp); return; } my $conn = $r->header('Connection'); my $accept_enc = $r->header('Accept-Encoding'); $reqstate->{accept_gzip} = ($accept_enc && $accept_enc =~ m/gzip/) ? 1 : 0; if ($conn) { $reqstate->{keep_alive} = 0 if $conn =~ m/close/oi; } else { if ($reqstate->{proto}->{ver} < 1001) { $reqstate->{keep_alive} = 0; } } my $te = $r->header('Transfer-Encoding'); if ($te && lc($te) eq 'chunked') { # Handle chunked transfer encoding $self->error($reqstate, 501, "chunked transfer encoding not supported"); return; } elsif ($te) { $self->error($reqstate, 501, "Unknown transfer encoding '$te'"); return; } my $pveclientip = $r->header('PVEClientIP'); my $base_uri = $self->{base_uri}; # fixme: how can we make PVEClientIP header trusted? if ($self->{trusted_env} && $pveclientip) { $reqstate->{peer_host} = $pveclientip; } else { $r->header('PVEClientIP', $reqstate->{peer_host}); } my $len = $r->header('Content-Length'); # header processing complete - authenticate now my $auth = {}; if ($self->{spiceproxy}) { my $connect_str = $r->header('Host'); my ($vmid, $node, $port) = $self->verify_spice_connect_url($connect_str); if (!(defined($vmid) && $node && $port)) { $self->error($reqstate, HTTP_UNAUTHORIZED, "invalid ticket"); return; } $self->handle_spice_proxy_request($reqstate, $connect_str, $vmid, $node, $port); return; } elsif ($path =~ m/^\Q$base_uri\E/) { my $token = $r->header('CSRFPreventionToken'); my $cookie = $r->header('Cookie'); my $ticket = PVE::APIServer::Formatter::extract_auth_cookie($cookie, $self->{cookie_name}); my ($rel_uri, $format) = &$split_abs_uri($path, $self->{base_uri}); if (!$format) { $self->error($reqstate, HTTP_NOT_IMPLEMENTED, "no such uri"); return; } eval { $auth = $self->auth_handler($method, $rel_uri, $ticket, $token, $reqstate->{peer_host}); }; if (my $err = $@) { # HACK: see Note 1 Net::SSLeay::ERR_clear_error(); # always delay unauthorized calls by 3 seconds my $delay = 3; if (ref($err) eq "PVE::Exception") { $err->{code} ||= HTTP_INTERNAL_SERVER_ERROR, my $resp = HTTP::Response->new($err->{code}, $err->{msg}); $self->response($reqstate, $resp, undef, 0, $delay); } elsif (my $formatter = PVE::APIServer::Formatter::get_login_formatter($format)) { my ($raw, $ct, $nocomp) = $formatter->($path, $auth, $self->{formatter_config}); my $resp; if (ref($raw) && (ref($raw) eq 'HTTP::Response')) { $resp = $raw; } else { $resp = HTTP::Response->new(HTTP_UNAUTHORIZED, "Login Required"); $resp->header("Content-Type" => $ct); $resp->content($raw); } $self->response($reqstate, $resp, undef, $nocomp, $delay); } else { my $resp = HTTP::Response->new(HTTP_UNAUTHORIZED, $err); $self->response($reqstate, $resp, undef, 0, $delay); } return; } } $reqstate->{log}->{userid} = $auth->{userid}; if ($len) { if (!($method eq 'PUT' || $method eq 'POST')) { $self->error($reqstate, 501, "Unexpected content for method '$method'"); return; } my $ctype = $r->header('Content-Type'); my ($ct, $boundary) = parse_content_type($ctype) if $ctype; if ($auth->{isUpload} && !$self->{trusted_env}) { die "upload 'Content-Type '$ctype' not implemented\n" if !($boundary && $ct && ($ct eq 'multipart/form-data')); die "upload without content length header not supported" if !$len; die "upload without content length header not supported" if !$len; print "start upload $path $ct $boundary\n" if $self->{debug}; my $tmpfilename = get_upload_filename(); my $outfh = IO::File->new($tmpfilename, O_RDWR|O_CREAT|O_EXCL, 0600) || die "unable to create temporary upload file '$tmpfilename'"; $reqstate->{keep_alive} = 0; my $boundlen = length($boundary) + 8; # \015?\012--$boundary--\015?\012 my $state = { size => $len, boundary => $boundary, ctx => Digest::MD5->new, boundlen => $boundlen, maxheader => 2048 + $boundlen, # should be large enough params => decode_urlencoded($r->url->query()), phase => 0, read => 0, post_size => 0, starttime => [gettimeofday], outfh => $outfh, }; $reqstate->{tmpfilename} = $tmpfilename; $reqstate->{hdl}->on_read(sub { $self->file_upload_multipart($reqstate, $auth, $method, $path, $state); }); return; } if ($len > $limit_max_post) { $self->error($reqstate, 501, "for data too large"); return; } if (!$ct || $ct eq 'application/x-www-form-urlencoded') { $reqstate->{hdl}->unshift_read(chunk => $len, sub { my ($hdl, $data) = @_; $r->content($data); $self->handle_request($reqstate, $auth, $method, $path); }); } else { $self->error($reqstate, 506, "upload 'Content-Type '$ctype' not implemented"); } } else { $self->handle_request($reqstate, $auth, $method, $path); } } elsif ($line =~ /^([^:\s]+)\s*:\s*(.*)/) { $r->push_header($state->{key}, $state->{val}) if $state->{key}; ($state->{key}, $state->{val}) = ($1, $2); $self->unshift_read_header($reqstate, $state); } elsif ($line =~ /^\s+(.*)/) { $state->{val} .= " $1"; $self->unshift_read_header($reqstate, $state); } else { $self->error($reqstate, 506, "unable to parse request header"); } }; warn $@ if $@; }); }; sub push_request_header { my ($self, $reqstate) = @_; eval { $reqstate->{hdl}->push_read(line => sub { my ($hdl, $line) = @_; eval { # print "got request header: $line\n" if $self->{debug}; $reqstate->{keep_alive}--; if ($line =~ /(\S+)\040(\S+)\040HTTP\/(\d+)\.(\d+)/o) { my ($method, $url, $maj, $min) = ($1, $2, $3, $4); if ($maj != 1) { $self->error($reqstate, 506, "http protocol version $maj.$min not supported"); return; } $self->{request_count}++; # only count valid request headers if ($self->{request_count} >= $self->{max_requests}) { $self->{end_loop} = 1; } $reqstate->{log} = { requestline => $line }; $reqstate->{proto}->{str} = "HTTP/$maj.$min"; $reqstate->{proto}->{maj} = $maj; $reqstate->{proto}->{min} = $min; $reqstate->{proto}->{ver} = $maj*1000+$min; $reqstate->{request} = HTTP::Request->new($method, $url); $reqstate->{starttime} = [gettimeofday], $self->unshift_read_header($reqstate); } elsif ($line eq '') { # ignore empty lines before requests (browser bugs?) $self->push_request_header($reqstate); } else { $self->error($reqstate, 400, 'bad request'); } }; warn $@ if $@; }); }; warn $@ if $@; } sub accept { my ($self) = @_; my $clientfh; return if $self->{end_loop}; # we need to m make sure that only one process calls accept while (!flock($self->{lockfh}, Fcntl::LOCK_EX())) { next if $! == EINTR; die "could not get lock on file '$self->{lockfile}' - $!\n"; } my $again = 0; my $errmsg; eval { while (!$self->{end_loop} && !defined($clientfh = $self->{socket}->accept()) && ($! == EINTR)) {}; if ($self->{end_loop}) { $again = 0; } else { $again = ($! == EAGAIN || $! == WSAEWOULDBLOCK); if (!defined($clientfh)) { $errmsg = "failed to accept connection: $!\n"; } } }; warn $@ if $@; flock($self->{lockfh}, Fcntl::LOCK_UN()); if (!defined($clientfh)) { return if $again; die $errmsg if $errmsg; } fh_nonblocking $clientfh, 1; $self->{conn_count}++; return $clientfh; } sub wait_end_loop { my ($self) = @_; $self->{end_loop} = 1; undef $self->{socket_watch}; $0 = "$0 (shutdown)" if $0 !~ m/\(shutdown\)$/; if ($self->{conn_count} <= 0) { $self->{end_cond}->send(1); return; } # fork and exit, so that parent starts a new worker if (fork()) { exit(0); } # else we need to wait until all open connections gets closed my $w; $w = AnyEvent->timer (after => 1, interval => 1, cb => sub { eval { # todo: test for active connections instead (we can abort idle connections) if ($self->{conn_count} <= 0) { undef $w; $self->{end_cond}->send(1); } }; warn $@ if $@; }); } sub check_host_access { my ($self, $clientip) = @_; my $cip = Net::IP->new($clientip); my $match_allow = 0; my $match_deny = 0; if ($self->{allow_from}) { foreach my $t (@{$self->{allow_from}}) { if ($t->overlaps($cip)) { $match_allow = 1; last; } } } if ($self->{deny_from}) { foreach my $t (@{$self->{deny_from}}) { if ($t->overlaps($cip)) { $match_deny = 1; last; } } } if ($match_allow == $match_deny) { # match both allow and deny, or no match return $self->{policy} && $self->{policy} eq 'allow' ? 1 : 0; } return $match_allow; } sub accept_connections { my ($self) = @_; eval { while (my $clientfh = $self->accept()) { my $reqstate = { keep_alive => $self->{keep_alive} }; # stop keep-alive when there are many open connections if ($self->{conn_count} >= $self->{max_conn_soft_limit}) { $reqstate->{keep_alive} = 0; } if (my $sin = getpeername($clientfh)) { my ($pfamily, $pport, $phost) = PVE::Tools::unpack_sockaddr_in46($sin); ($reqstate->{peer_port}, $reqstate->{peer_host}) = ($pport, Socket::inet_ntop($pfamily, $phost)); } if (!$self->{trusted_env} && !$self->check_host_access($reqstate->{peer_host})) { print "$$: ABORT request from $reqstate->{peer_host} - access denied\n" if $self->{debug}; $reqstate->{log}->{code} = 403; $self->log_request($reqstate); next; } $reqstate->{hdl} = AnyEvent::Handle->new( fh => $clientfh, rbuf_max => 64*1024, timeout => $self->{timeout}, linger => 0, # avoid problems with ssh - really needed ? on_eof => sub { my ($hdl) = @_; eval { $self->log_aborted_request($reqstate); $self->client_do_disconnect($reqstate); }; if (my $err = $@) { syslog('err', $err); } }, on_error => sub { my ($hdl, $fatal, $message) = @_; eval { $self->log_aborted_request($reqstate, $message); $self->client_do_disconnect($reqstate); }; if (my $err = $@) { syslog('err', "$err"); } }, ($self->{tls_ctx} ? (tls => "accept", tls_ctx => $self->{tls_ctx}) : ())); print "$$: ACCEPT FH" . $clientfh->fileno() . " CONN$self->{conn_count}\n" if $self->{debug}; $self->push_request_header($reqstate); } }; if (my $err = $@) { syslog('err', $err); $self->{end_loop} = 1; } $self->wait_end_loop() if $self->{end_loop}; } # Note: We can't open log file in non-blocking mode and use AnyEvent::Handle, # because we write from multiple processes, and that would arbitrarily mix output # of all processes. sub open_access_log { my ($self, $filename) = @_; my $old_mask = umask(0137);; my $logfh = IO::File->new($filename, ">>") || die "unable to open log file '$filename' - $!\n"; umask($old_mask); $logfh->autoflush(1); $self->{logfh} = $logfh; } sub write_log { my ($self, $data) = @_; return if !defined($self->{logfh}) || !$data; my $res = $self->{logfh}->print($data); if (!$res) { delete $self->{logfh}; syslog('err', "error writing access log"); $self->{end_loop} = 1; # terminate asap } } sub atfork_handler { my ($self) = @_; eval { # something else do to ? close($self->{socket}); }; warn $@ if $@; } sub run { my ($self) = @_; $self->{end_cond}->recv; } sub new { my ($this, %args) = @_; my $class = ref($this) || $this; foreach my $req (qw(socket lockfh lockfile)) { die "misssing required argument '$req'" if !defined($args{$req}); } my $self = bless { %args }, $class; $self->{cookie_name} //= 'PVEAuthCookie'; $self->{base_uri} //= "/api2"; $self->{dirs} //= {}; $self->{title} //= 'API Inspector'; $self->{compression} //= 1; # formatter_config: we pass some configuration values to the Formatter $self->{formatter_config} = {}; foreach my $p (qw(cookie_name base_uri title)) { $self->{formatter_config}->{$p} = $self->{$p}; } $self->{formatter_config}->{csrfgen_func} = $self->can('generate_csrf_prevention_token'); # add default dirs which includes jquery and bootstrap my $base = '/usr/share/libpve-http-server-perl'; add_dirs($self->{dirs}, '/css/' => "$base/css/"); add_dirs($self->{dirs}, '/js/' => "$base/js/"); add_dirs($self->{dirs}, '/fonts/' => "$base/fonts/"); # init inotify PVE::INotify::inotify_init(); fh_nonblocking($self->{socket}, 1); $self->{end_loop} = 0; $self->{conn_count} = 0; $self->{request_count} = 0; $self->{timeout} = 5 if !$self->{timeout}; $self->{keep_alive} = 0 if !defined($self->{keep_alive}); $self->{max_conn} = 800 if !$self->{max_conn}; $self->{max_requests} = 8000 if !$self->{max_requests}; $self->{policy} = 'allow' if !$self->{policy}; $self->{end_cond} = AnyEvent->condvar; if ($self->{ssl}) { my $ssl_defaults = { # Note: older versions are considered insecure, for example # search for "Poodle"-Attack method => 'any', sslv2 => 0, sslv3 => 0, cipher_list => 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256', honor_cipher_order => 1, }; foreach my $k (keys %$ssl_defaults) { $self->{ssl}->{$k} //= $ssl_defaults->{$k}; } if (!defined($self->{ssl}->{dh_file})) { $self->{ssl}->{dh} = 'skip2048'; } my $tls_ctx_flags = &Net::SSLeay::OP_NO_COMPRESSION | &Net::SSLeay::OP_SINGLE_ECDH_USE | &Net::SSLeay::OP_SINGLE_DH_USE; if ( delete $self->{ssl}->{honor_cipher_order} ) { $tls_ctx_flags |= &Net::SSLeay::OP_CIPHER_SERVER_PREFERENCE; } $self->{tls_ctx} = AnyEvent::TLS->new(%{$self->{ssl}}); Net::SSLeay::CTX_set_options($self->{tls_ctx}->{ctx}, $tls_ctx_flags); } if ($self->{spiceproxy}) { $known_methods = { CONNECT => 1 }; } $self->open_access_log($self->{logfile}) if $self->{logfile}; $self->{max_conn_soft_limit} = $self->{max_conn} > 100 ? $self->{max_conn} - 20 : $self->{max_conn}; $self->{socket_watch} = AnyEvent->io(fh => $self->{socket}, poll => 'r', cb => sub { eval { if ($self->{conn_count} >= $self->{max_conn}) { my $w; $w = AnyEvent->timer (after => 1, interval => 1, cb => sub { if ($self->{conn_count} < $self->{max_conn}) { undef $w; $self->accept_connections(); } }); } else { $self->accept_connections(); } }; warn $@ if $@; }); $self->{term_watch} = AnyEvent->signal(signal => "TERM", cb => sub { undef $self->{term_watch}; $self->wait_end_loop(); }); $self->{quit_watch} = AnyEvent->signal(signal => "QUIT", cb => sub { undef $self->{quit_watch}; $self->wait_end_loop(); }); $self->{inotify_poll} = AnyEvent->timer(after => 5, interval => 5, cb => sub { PVE::INotify::poll(); # read inotify events }); return $self; } # static helper to add directory including all subdirs # This can be used to setup $self->{dirs} sub add_dirs { my ($result_hash, $alias, $subdir) = @_; $result_hash->{$alias} = $subdir; my $wanted = sub { my $dir = $File::Find::dir; if ($dir =~m!^$subdir(.*)$!) { my $name = "$alias$1/"; $result_hash->{$name} = "$dir/"; } }; find({wanted => $wanted, follow => 0, no_chdir => 1}, $subdir); } # abstract functions - subclass should overwrite/implement them sub verify_spice_connect_url { my ($self, $connect_str) = @_; die "implement me"; #return ($vmid, $node, $port); } # formatters can call this when the generate a new page sub generate_csrf_prevention_token { my ($username) = @_; return undef; # do nothing by default } sub auth_handler { my ($self, $method, $rel_uri, $ticket, $token, $peer_host) = @_; die "implement me"; # return { # ticket => $ticket, # token => $token, # userid => $username, # age => $age, # isUpload => $isUpload, #}; } sub rest_handler { my ($self, $clientip, $method, $rel_uri, $auth, $params, $format) = @_; # please do not raise exceptions here (always return a result). return { status => HTTP_NOT_IMPLEMENTED, message => "Method '$method $rel_uri' not implemented", }; # this should return the following properties, which # are then passed to the Formatter # status: HTTP status code # message: Error message # errors: more detailed error hash (per parameter) # info: reference to JSON schema definition - useful to format output # data: result data # total: additional info passed to output # changes: additional info passed to output # if you want to proxy the request to another node return this # { proxy => $remip, proxynode => $node, proxy_params => $params }; # to pass the request to the local priviledged daemon use: # { proxy => 'localhost' , proxy_params => $params }; # to download aspecific file use: # { download => "/path/to/file" }; } sub check_cert_fingerprint { my ($self, $cert) = @_; die "implement me"; } sub initialize_cert_cache { my ($self, $node) = @_; die "implement me"; } sub remote_node_ip { my ($self, $node) = @_; die "implement me"; # return $remip; } 1;