]> git.proxmox.com Git - pve-http-server.git/blobdiff - PVE/APIServer/AnyEvent.pm
move read_proxy_conf from PVE::API2Tools to new Utils module
[pve-http-server.git] / PVE / APIServer / AnyEvent.pm
old mode 100755 (executable)
new mode 100644 (file)
index 0cf8cbc..b2330a5
@@ -1,5 +1,12 @@
 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);
@@ -22,6 +29,7 @@ use AnyEvent::IO;
 use AnyEvent::HTTP;
 use Fcntl ();
 use Compress::Zlib;
+use Encode;
 use PVE::SafeSyslog;
 use PVE::INotify;
 use PVE::Tools;
@@ -39,7 +47,7 @@ use Data::Dumper;
 
 my $limit_max_headers = 30;
 my $limit_max_header_size = 8*1024;
-my $limit_max_post = 16*1024;
+my $limit_max_post = 64*1024;
 
 my $known_methods = {
     GET => 1,
@@ -73,7 +81,7 @@ sub log_request {
     my $content_length = defined($loginfo->{content_length}) ? $loginfo->{content_length} : '-';
     my $code =  $loginfo->{code} || 500;
     my $requestline = $loginfo->{requestline} || '-';
-    my $timestr = strftime("%d/%b/%Y:%H:%M:%S %z", localtime());
+    my $timestr = strftime("%d/%m/%Y:%H:%M:%S %z", localtime());
 
     my $msg = "$peerip - $userid [$timestr] \"$requestline\" $code $content_length\n";
 
@@ -146,10 +154,11 @@ sub client_do_disconnect {
 sub finish_response {
     my ($self, $reqstate) = @_;
 
-    my $hdl = $reqstate->{hdl};
-
     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 {
@@ -175,6 +184,7 @@ sub response {
     $reqstate->{hdl}->timeout_reset();
     $reqstate->{hdl}->timeout($self->{timeout});
 
+    $nocomp = 1 if !$self->{compression};
     $nocomp = 1 if !$reqstate->{accept_gzip};
 
     my $code = $resp->code;
@@ -265,15 +275,21 @@ 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 {
@@ -330,7 +346,7 @@ sub websocket_proxy {
        my $remhost;
        my $remport;
 
-       my $max_payload_size = 65536;
+       my $max_payload_size = 128*1024;
 
        my $binary;
        if ($wsproto eq 'binary') {
@@ -359,8 +375,8 @@ sub websocket_proxy {
 
            $reqstate->{proxyhdl} = AnyEvent::Handle->new(
                fh => $fh,
-               rbuf_max => 64*1024,
-               wbuf_max => 64*10*1024,
+               rbuf_max => $max_payload_size,
+               wbuf_max => $max_payload_size*5,
                timeout => 5,
                on_eof => sub {
                    my ($hdl) = @_;
@@ -383,7 +399,7 @@ sub websocket_proxy {
                my ($hdl) = @_;
 
                my $len = length($hdl->{rbuf});
-               my $data = substr($hdl->{rbuf}, 0, $len, '');
+               my $data = substr($hdl->{rbuf}, 0, $len > $max_payload_size ? $max_payload_size : $len, '');
 
                my $string;
                my $payload;
@@ -414,70 +430,71 @@ sub websocket_proxy {
            my $hdlreader = sub {
                my ($hdl) = @_;
 
-               my $len = length($hdl->{rbuf});
-               return if $len < 2;
+               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;
+                   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;
+                   die "received fragmented websocket frame\n" if !$fin;
 
-               my $rsv = $hdr & 0b01110000;
-               die "received websocket frame with RSV flags\n" if $rsv;
+                   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 $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 $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;
-               }
+                   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);
+                   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);
+                   return if $len < ($offset + 4 + $payload_len);
 
-               my $data = substr($hdl->{rbuf}, 0, $len, ''); # now consume data
+                   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)));
+                   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;
+                   $offset += 4;
 
-               my $payload = substr($data, $offset, $payload_len);
+                   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));
-               }
+                   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;
+                   $payload = decode_base64($payload) if !$binary;
 
-               if ($opcode == 1 || $opcode == 2) {
-                   $reqstate->{proxyhdl}->push_write($payload) if $reqstate->{proxyhdl};
-               } elsif ($opcode == 8) {
-                   print "websocket received close\n" if $self->{debug};
+                   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_write($payload);
-                       $reqstate->{proxyhdl}->push_shutdown();
+                               $reqstate->{proxyhdl}->push_shutdown();
+                   }
+                       $hdl->push_shutdown();
+                   } else {
+                       die "received unhandled websocket opcode $opcode\n";
                    }
-                   $hdl->push_shutdown();
-               } else {
-                   die "received unhandled websocket opcode $opcode\n";
                }
            };
 
@@ -536,7 +553,7 @@ sub proxy_request {
 
        $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};
+       $headers->{'Accept-Encoding'} = 'gzip' if ($reqstate->{accept_gzip} && $self->{compression});
 
        my $content;
 
@@ -609,6 +626,7 @@ sub proxy_request {
 }
 
 # return arrays as \0 separated strings (like CGI.pm)
+# assume data is UTF8 encoded
 sub decode_urlencoded {
     my ($data) = @_;
 
@@ -623,6 +641,8 @@ sub decode_urlencoded {
        $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 {
@@ -647,7 +667,7 @@ sub extract_params {
        $params->{$k} = $query_params->{$k};
     }
 
-    return PVE::Tools::decode_utf8_parameters($params);
+    return $params;
 }
 
 sub handle_api2_request {
@@ -661,7 +681,7 @@ sub handle_api2_request {
        my $formatter = PVE::APIServer::Formatter::get_formatter($format, $method, $rel_uri);
 
        if (!defined($formatter)) {
-           $self->error($reqstate, HTTP_NOT_IMPLEMENTED, "no such uri $rel_uri, $format");
+           $self->error($reqstate, HTTP_NOT_IMPLEMENTED, "no formatter for uri $rel_uri, $format");
            return;
        }
 
@@ -679,7 +699,10 @@ sub handle_api2_request {
 
        my $clientip = $reqstate->{peer_host};
 
-       my $res = $self->rest_handler($clientip, $method, $rel_uri, $auth, $params);
+       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()
 
@@ -733,6 +756,13 @@ sub handle_api2_request {
            $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});
 
@@ -838,7 +868,8 @@ sub handle_spice_proxy_request {
 
                # todo: use stop_read/start_read if write buffer grows to much
 
-               my $res = "$proto 200 OK\015\012"; # hope this is the right answer?
+               # 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
@@ -860,7 +891,10 @@ sub handle_spice_proxy_request {
                    my ($hdl, $line) = @_;
 
                    if ($line =~ m!^$proto 200 OK$!) {
-                       &$startproxy();
+                       # 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);
@@ -900,6 +934,8 @@ sub handle_request {
            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}) {
@@ -929,7 +965,7 @@ sub handle_request {
            }
        }
 
-       die "no such file '$path'";
+       die "no such file '$path'\n";
     };
     if (my $err = $@) {
        $self->error($reqstate, 501, $err);
@@ -1183,9 +1219,18 @@ sub unshift_read_header {
                                                    $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 (my $formatter = PVE::APIServer::Formatter::get_login_formatter($format)) {
+
+                       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;
@@ -1196,7 +1241,7 @@ sub unshift_read_header {
                                $resp->header("Content-Type" => $ct);
                                $resp->content($raw);
                            }
-                           $self->response($reqstate, $resp, undef, $nocomp, 3);
+                           $self->response($reqstate, $resp, undef, $nocomp, $delay);
                        } else {
                            my $resp = HTTP::Response->new(HTTP_UNAUTHORIZED, $err);
                            $self->response($reqstate, $resp, undef, 0, $delay);
@@ -1567,6 +1612,7 @@ sub new {
     $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} = {};
@@ -1600,16 +1646,31 @@ sub new {
     $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}});
-       # TODO : openssl >= 1.0.2 supports SSL_CTX_set_ecdh_auto to select a curve depending on
-       # server and client availability from SSL_CTX_set1_curves.
-       # that way other curves like 25519 can be used.
-       # openssl 1.0.1 can only support 1 curve at a time.
-       my $curve = Net::SSLeay::OBJ_txt2nid('prime256v1');
-       my $ecdh = Net::SSLeay::EC_KEY_new_by_curve_name($curve);
-       Net::SSLeay::CTX_set_options($self->{tls_ctx}->{ctx}, &Net::SSLeay::OP_NO_COMPRESSION | &Net::SSLeay::OP_SINGLE_ECDH_USE | &Net::SSLeay::OP_SINGLE_DH_USE);
-       Net::SSLeay::CTX_set_tmp_ecdh($self->{tls_ctx}->{ctx}, $ecdh);
-       Net::SSLeay::EC_KEY_free($ecdh);
+       Net::SSLeay::CTX_set_options($self->{tls_ctx}->{ctx}, $tls_ctx_flags);
     }
 
     if ($self->{spiceproxy}) {
@@ -1703,7 +1764,7 @@ sub auth_handler {
 }
 
 sub rest_handler {
-    my ($self, $clientip, $method, $rel_uri, $auth, $params) = @_;
+    my ($self, $clientip, $method, $rel_uri, $auth, $params, $format) = @_;
 
     # please do not raise exceptions here (always return a result).
 
@@ -1729,6 +1790,9 @@ sub rest_handler {
 
     # 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 {