]> git.proxmox.com Git - pve-http-server.git/blobdiff - PVE/APIServer/AnyEvent.pm
allow API calls to download file contents.
[pve-http-server.git] / PVE / APIServer / AnyEvent.pm
index 101c5dccdae46cbeedfc42a37d701dfc823c9f76..ef0417ce63991911b5464185981b92bda3aca8da 100755 (executable)
@@ -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 {
@@ -265,15 +274,19 @@ my $file_extension_info = {
     css   => { ct => 'text/css' },
     html  => { ct => 'text/html' },
     js    => { ct => 'application/javascript' },
+    json  => { 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},
 };
 
 sub send_file_start {
@@ -609,6 +622,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 +637,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 +663,7 @@ sub extract_params {
        $params->{$k} = $query_params->{$k};
     }
 
-    return PVE::Tools::decode_utf8_parameters($params);
+    return $params;
 }
 
 sub handle_api2_request {
@@ -661,14 +677,12 @@ 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;
        }
 
        #print Dumper($upload_state) if $upload_state;
 
-       my $rpcenv = $self->{rpcenv};
-
        my $params;
 
        if ($upload_state) {
@@ -681,14 +695,13 @@ sub handle_api2_request {
 
        my $clientip = $reqstate->{peer_host};
 
-       $rpcenv->init_request();
+       my $res = $self->rest_handler($clientip, $method, $rel_uri, $auth, $params, $format);
 
-       my $res = $self->rest_handler($clientip, $method, $rel_uri, $auth, $params);
+       # HACK: see Note 1
+       Net::SSLeay::ERR_clear_error();
 
        AnyEvent->now_update(); # in case somebody called sleep()
 
-       $rpcenv->set_user(undef); # clear after request
-
        my $upgrade = $r->header('upgrade');
        $upgrade = lc($upgrade) if $upgrade;
 
@@ -739,8 +752,15 @@ sub handle_api2_request {
            $delay = 0 if $delay < 0;
        }
 
-       my $csrfgen_func = $self->can('generate_csrf_prevention_token');
-       my ($raw, $ct, $nocomp) = &$formatter($res, $res->{data}, $params, $path, $auth, $csrfgen_func);
+       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')) {
@@ -764,9 +784,6 @@ sub handle_spice_proxy_request {
 
         die "Port $spiceport is not allowed" if ($spiceport < 61000 || $spiceport > 61099);
 
-       my $rpcenv = $self->{rpcenv};
-       $rpcenv->init_request();
-
        my $clientip = $reqstate->{peer_host};
        my $r = $reqstate->{request};
 
@@ -909,6 +926,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}) {
@@ -938,7 +957,7 @@ sub handle_request {
            }
        }
 
-       die "no such file '$path'";
+       die "no such file '$path'\n";
     };
     if (my $err = $@) {
        $self->error($reqstate, 501, $err);
@@ -1187,20 +1206,18 @@ sub unshift_read_header {
                        return;
                    }
 
-                   my $rpcenv = $self->{rpcenv};
-                   # set environment variables
-                   $rpcenv->set_user(undef);
-                   $rpcenv->set_language('C');
-                   $rpcenv->set_client_ip($reqstate->{peer_host});
-
                    eval {
-                       $auth = $self->auth_handler($method, $rel_uri, $ticket, $token);
+                       $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 (my $formatter = PVE::APIServer::Formatter::get_login_formatter($format)) {
-                           my ($raw, $ct, $nocomp) = &$formatter($path, $auth);
+                           my ($raw, $ct, $nocomp) =
+                               $formatter->($path, $auth, $self->{formatter_config});
                            my $resp;
                            if (ref($raw) && (ref($raw) eq 'HTTP::Response')) {
                                $resp = $raw;
@@ -1209,7 +1226,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);
@@ -1570,7 +1587,7 @@ sub new {
 
     my $class = ref($this) || $this;
 
-    foreach my $req (qw(base_handler_class socket lockfh lockfile)) {
+    foreach my $req (qw(socket lockfh lockfile)) {
        die "misssing required argument '$req'" if !defined($args{$req});
     }
 
@@ -1579,6 +1596,21 @@ sub new {
     $self->{cookie_name} //= 'PVEAuthCookie';
     $self->{base_uri} //= "/api2";
     $self->{dirs} //= {};
+    $self->{title} //= 'API Inspector';
+
+    # 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();
@@ -1599,15 +1631,7 @@ sub new {
 
     if ($self->{ssl}) {
        $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);
     }
 
     if ($self->{spiceproxy}) {
@@ -1687,7 +1711,7 @@ sub generate_csrf_prevention_token {
 }
 
 sub auth_handler {
-    my ($self, $method, $rel_uri, $ticket, $token) = @_;
+    my ($self, $method, $rel_uri, $ticket, $token, $peer_host) = @_;
 
     die "implement me";
 
@@ -1697,17 +1721,39 @@ sub auth_handler {
     #    userid => $username,
     #    age => $age,
     #    isUpload => $isUpload,
-    #    cookie_name => $self->{cookie_name},
     #};
 }
 
 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).
 
     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 {