]> git.proxmox.com Git - pve-http-server.git/blobdiff - PVE/APIServer/AnyEvent.pm
access control: correctly match v4-mapped-v6 addresses
[pve-http-server.git] / PVE / APIServer / AnyEvent.pm
old mode 100755 (executable)
new mode 100644 (file)
index d818e61..f0e2e68
@@ -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);
@@ -8,6 +15,7 @@ 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;
@@ -21,10 +29,12 @@ 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 PVE::APIServer::Utils;
 
 use Net::IP;
 use URI;
@@ -35,10 +45,11 @@ use HTTP::Headers;
 use HTTP::Request;
 use HTTP::Response;
 use Data::Dumper;
+use JSON;
 
-my $limit_max_headers = 30;
+my $limit_max_headers = 64;
 my $limit_max_header_size = 8*1024;
-my $limit_max_post = 16*1024;
+my $limit_max_post = 64*1024;
 
 my $known_methods = {
     GET => 1,
@@ -56,6 +67,16 @@ my $split_abs_uri = sub {
     return wantarray ? ($rel_uri, $format) : $rel_uri;
 };
 
+sub dprint {
+    my ($self, $message) = @_;
+
+    return if !$self->{debug};
+
+    my ($pkg, $pkgfile, $line, $sub) = caller(1);
+    $sub =~ s/^(?:.+::)+//;
+    print "worker[$$]: $pkg +$line: $sub: $message\n";
+}
+
 sub log_request {
     my ($self, $reqstate) = @_;
 
@@ -72,7 +93,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";
 
@@ -133,22 +154,25 @@ sub client_do_disconnect {
        return;
     }
 
-    print "close connection $hdl\n" if $self->{debug};
+    $self->dprint("close connection $hdl");
 
     &$shutdown_hdl($hdl);
 
+    warn "connection count <= 0!\n" if $self->{conn_count} <= 0;
+
     $self->{conn_count}--;
 
-    print "$$: CLOSE FH" .  $hdl->{fh}->fileno() . " CONN$self->{conn_count}\n" if $self->{debug};
+    $self->dprint("CLOSE FH" .  $hdl->{fh}->fileno() . " CONN$self->{conn_count}");
 }
 
 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 {
@@ -165,8 +189,93 @@ sub finish_response {
     }
 }
 
+sub response_stream {
+    my ($self, $reqstate, $stream_fh) = @_;
+
+    # disable timeout, we don't know how big the data is
+    $reqstate->{hdl}->timeout(0);
+
+    my $buf_size = 4*1024*1024;
+
+    my $on_read;
+    $on_read = sub {
+       my ($hdl) = @_;
+       my $reqhdl = $reqstate->{hdl};
+       return if !$reqhdl;
+
+       my $wbuf_len = length($reqhdl->{wbuf});
+       my $rbuf_len = length($hdl->{rbuf});
+       # TODO: Take into account $reqhdl->{wbuf_max} ? Right now
+       # that's unbounded, so just assume $buf_size
+       my $to_read = $buf_size - $wbuf_len;
+       $to_read = $rbuf_len if $rbuf_len < $to_read;
+       if ($to_read > 0) {
+           my $data = substr($hdl->{rbuf}, 0, $to_read, '');
+           $reqhdl->push_write($data);
+           $rbuf_len -= $to_read;
+       } elsif ($hdl->{_eof}) {
+           # workaround: AnyEvent gives us a fake EPIPE if we don't consume
+           # any data when called at EOF, so unregister ourselves - data is
+           # flushed by on_eof anyway
+           # see: https://sources.debian.org/src/libanyevent-perl/7.170-2/lib/AnyEvent/Handle.pm/#L1329
+           $hdl->on_read();
+           return;
+       }
+
+       # apply backpressure so we don't accept any more data into
+       # buffer if the client isn't downloading fast enough
+       # note: read_size can double upon read, and we also need to
+       # account for one more read after start_read, so *4
+       if ($rbuf_len + $hdl->{read_size}*4 > $buf_size) {
+           # stop reading until write buffer is empty
+           $hdl->on_read();
+           my $prev_on_drain = $reqhdl->{on_drain};
+           $reqhdl->on_drain(sub {
+               my ($wrhdl) = @_;
+               # on_drain called because write buffer is empty, continue reading
+               $hdl->on_read($on_read);
+               if ($prev_on_drain) {
+                   $wrhdl->on_drain($prev_on_drain);
+                   $prev_on_drain->($wrhdl);
+               }
+           });
+       }
+    };
+
+    $reqstate->{proxyhdl} = AnyEvent::Handle->new(
+       fh => $stream_fh,
+       rbuf_max => $buf_size,
+       timeout => 0,
+       on_read => $on_read,
+       on_eof => sub {
+           my ($hdl) = @_;
+           eval {
+               if (my $reqhdl = $reqstate->{hdl}) {
+                   $self->log_aborted_request($reqstate);
+                   # write out any remaining data
+                   $reqhdl->push_write($hdl->{rbuf}) if length($hdl->{rbuf}) > 0;
+                   $hdl->{rbuf} = "";
+                   $reqhdl->push_shutdown();
+                   $self->finish_response($reqstate);
+               }
+           };
+           if (my $err = $@) { syslog('err', "$err"); }
+           $on_read = undef;
+       },
+       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"); }
+           $on_read = undef;
+       },
+    );
+}
+
 sub response {
-    my ($self, $reqstate, $resp, $mtime, $nocomp, $delay) = @_;
+    my ($self, $reqstate, $resp, $mtime, $nocomp, $delay, $stream_fh) = @_;
 
     #print "$$: send response: " . Dumper($resp);
 
@@ -174,6 +283,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;
@@ -207,7 +317,7 @@ sub response {
     $resp->header('Server' => "pve-api-daemon/3.0");
 
     my $content_length;
-    if ($content) {
+    if ($content && !$stream_fh) {
 
        $content_length = length($content);
 
@@ -234,11 +344,16 @@ sub response {
     #print "SEND(without content) $res\n" if $self->{debug};
 
     $res .= "\015\012";
-    $res .= $content if $content;
+    $res .= $content if $content && !$stream_fh;
 
     $self->log_request($reqstate, $reqstate->{request});
-    
-    if ($delay && $delay > 0) {
+
+    if ($stream_fh) {
+       # write headers and preamble...
+       $reqstate->{hdl}->push_write($res);
+       # ...then stream data via an AnyEvent::Handle
+       $self->response_stream($reqstate, $stream_fh);
+    } elsif ($delay && $delay > 0) {
        my $w; $w = AnyEvent->timer(after => $delay, cb => sub {
            undef $w; # delete reference
            $reqstate->{hdl}->push_write($res);
@@ -264,19 +379,25 @@ 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) = @_;
+    my ($self, $reqstate, $download) = @_;
 
     eval {
        # print "SEND FILE $filename\n";
@@ -285,11 +406,60 @@ sub send_file_start {
 
            my $r = $reqstate->{request};
 
-           my $fh = IO::File->new($filename, '<') ||
-               die "$!\n";
+           my $fh;
+           my $nocomp;
+           my $mime;
+
+           if (ref($download) eq 'HASH') {
+               $mime = $download->{'content-type'};
+
+               if ($download->{path} && $download->{stream} &&
+                   $reqstate->{request}->header('PVEDisableProxy'))
+               {
+                   # avoid double stream from a file, let the proxy handle it
+                   die "internal error: file proxy streaming only available for pvedaemon\n"
+                       if !$self->{trusted_env};
+                   my $header = HTTP::Headers->new(
+                       pvestreamfile => $download->{path},
+                       Content_Type => $mime,
+                   );
+                   # we need some data so Content-Length gets set correctly and
+                   # the proxy doesn't wait for more data - place a canary
+                   my $resp = HTTP::Response->new(200, "OK", $header, "error canary");
+                   $self->response($reqstate, $resp);
+                   return;
+               }
+
+               if (!($fh = $download->{fh})) {
+                   my $path = $download->{path};
+                   die "internal error: {download} returned but neither fh not path given\n"
+                       if !$path;
+                   sysopen($fh, "$path", O_NONBLOCK | O_RDONLY)
+                       or die "open stream path '$path' for reading failed: $!\n";
+               }
+
+               if ($download->{stream}) {
+                   my $header = HTTP::Headers->new(Content_Type => $mime);
+                   my $resp = HTTP::Response->new(200, "OK", $header);
+                   $self->response($reqstate, $resp, undef, 1, 0, $fh);
+                   return;
+               }
+           } else {
+               my $filename = $download;
+               $fh = IO::File->new($filename, '<') ||
+                   die "unable to open file '$filename' - $!\n";
+
+               my ($ext) = $filename =~ m/\.([^.]*)$/;
+               my $ext_info = $file_extension_info->{$ext};
+
+               die "unable to detect content type" if !$ext_info;
+               $mime = $ext_info->{ct};
+               $nocomp = $ext_info->{nocomp};
+           }
+
            my $stat = File::stat::stat($fh) ||
                die "$!\n";
-           
+
            my $mtime = $stat->mtime;
 
            if (my $ifmod = $r->header('if-modified-since')) {
@@ -305,14 +475,9 @@ sub send_file_start {
            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 $header = HTTP::Headers->new(Content_Type => $mime);
            my $resp = HTTP::Response->new(200, "OK", $header, $data);
-           $self->response($reqstate, $resp, $mtime, $ext_info->{nocomp});
+           $self->response($reqstate, $resp, $mtime, $nocomp);
        };
        if (my $err = $@) {
            $self->error($reqstate, 501, $err);
@@ -329,7 +494,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') {
@@ -350,16 +515,43 @@ sub websocket_proxy {
            die "websocket_proxy: missing port or socket\n";
        }
 
+       my $encode = sub {
+           my ($data, $opcode) = @_;
+
+           my $string;
+           my $payload;
+           if ($binary) {
+               $string = $opcode ? $opcode : "\x82"; # binary frame
+               $payload = $$data;
+           } else {
+               $string = $opcode ? $opcode : "\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;
+           return $string;
+       };
+
        tcp_connect $remhost, $remport, sub {
-           my ($fh) = @_ 
+           my ($fh) = @_
                or die "connect to '$remhost:$remport' failed: $!";
-       
-           print "$$: CONNECTed to '$remhost:$remport'\n" if $self->{debug};
+
+           $self->dprint("CONNECTed to '$remhost:$remport'");
 
            $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) = @_;
@@ -380,32 +572,11 @@ sub websocket_proxy {
 
            my $proxyhdlreader = sub {
                my ($hdl) = @_;
-               
-               my $len = length($hdl->{rbuf});
-               my $data = substr($hdl->{rbuf}, 0, $len, '');
-
-               my $string;
-               my $payload;
 
-               if ($binary) {
-                   $string = "\x82"; # binary frame
-                   $payload = $data;
-               } else {
-                   $string = "\x81"; # text frame
-                   $payload = encode_base64($data, '');
-               }
+               my $len = length($hdl->{rbuf});
+               my $data = substr($hdl->{rbuf}, 0, $len > $max_payload_size ? $max_payload_size : $len, '');
 
-               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;
+               my $string = $encode->(\$data);
 
                $reqstate->{hdl}->push_write($string) if $reqstate->{hdl};
            };
@@ -413,70 +584,76 @@ 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 @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 $data = substr($hdl->{rbuf}, 0, $offset + 4 + $payload_len, ''); # now consume data
 
-               $offset += 4;
+                   my $mask = substr($data, $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));
-               }
+                   # NULL-mask might be used over TLS, skip to increase performance
+                   if ($mask ne pack('N', 0)) {
+                       # repeat 4 byte mask to payload length + up to 4 byte
+                       $mask = $mask x (int($payload_len / 4) + 1);
+                       # truncate mask to payload length
+                       substr($mask, $payload_len) = "";
+                       # (un-)apply mask
+                       $payload ^= $mask;
+                   }
 
-               $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 ($reqstate->{proxyhdl}) {
-                       $reqstate->{proxyhdl}->push_write($payload);
-                       $reqstate->{proxyhdl}->push_shutdown();
+                   if ($opcode == 1 || $opcode == 2) {
+                       $reqstate->{proxyhdl}->push_write($payload) if $reqstate->{proxyhdl};
+                   } elsif ($opcode == 8) {
+                       my $statuscode = unpack ("n", $payload);
+                       $self->dprint("websocket received close. status code: '$statuscode'");
+                       if ($reqstate->{proxyhdl}) {
+                           $reqstate->{proxyhdl}->push_shutdown();
+                       }
+                       $hdl->push_shutdown();
+                   } elsif ($opcode == 9) {
+                       # ping received, schedule pong
+                       $reqstate->{hdl}->push_write($encode->(\$payload, "\x8A")) if $reqstate->{hdl};
+                   } elsif ($opcode == 0xA) {
+                       # pong received, continue
+                   } else {
+                       die "received unhandled websocket opcode $opcode\n";
                    }
-                   $hdl->push_shutdown();
-               } else {
-                   die "received unhandled websocket opcode $opcode\n";
                }
            };
 
@@ -495,7 +672,7 @@ sub websocket_proxy {
                "Sec-WebSocket-Protocol: $wsproto\015\012" .
                "\015\012";
 
-           print $res if $self->{debug};
+           $self->dprint($res);
 
            $reqstate->{hdl}->push_write($res);
 
@@ -513,7 +690,7 @@ sub websocket_proxy {
 }
 
 sub proxy_request {
-    my ($self, $reqstate, $clientip, $host, $node, $method, $uri, $ticket, $token, $params) = @_;
+    my ($self, $reqstate, $clientip, $host, $node, $method, $uri, $auth, $params) = @_;
 
     eval {
        my $target;
@@ -533,9 +710,17 @@ sub proxy_request {
            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};
+       $headers->{'cookie'} = PVE::APIServer::Formatter::create_auth_cookie($auth->{ticket}, $self->{cookie_name})
+           if $auth->{ticket};
+       $headers->{'Authorization'} = PVE::APIServer::Formatter::create_auth_header($auth->{api_token}, $self->{apitoken_name})
+           if $auth->{api_token};
+       $headers->{'CSRFPreventionToken'} = $auth->{token}
+           if $auth->{token};
+       $headers->{'Accept-Encoding'} = 'gzip' if ($reqstate->{accept_gzip} && $self->{compression});
+
+       if (defined(my $host = $reqstate->{request}->header('Host'))) {
+           $headers->{Host} = $host;
+       }
 
        my $content;
 
@@ -590,6 +775,7 @@ sub proxy_request {
                eval {
                    my $code = delete $hdr->{Status};
                    my $msg = delete $hdr->{Reason};
+                   my $stream = delete $hdr->{pvestreamfile};
                    delete $hdr->{URL};
                    delete $hdr->{HTTPVersion};
                    my $header = HTTP::Headers->new(%$hdr);
@@ -597,9 +783,16 @@ sub proxy_request {
                        $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);
+                   if ($stream) {
+                       sysopen(my $fh, "$stream", O_NONBLOCK | O_RDONLY)
+                           or die "open stream path '$stream' for forwarding failed: $!\n";
+                       my $resp = HTTP::Response->new($code, $msg, $header, undef);
+                       $self->response($reqstate, $resp, undef, 1, 0, $fh);
+                   } else {
+                       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 $@;
            });
@@ -608,6 +801,7 @@ sub proxy_request {
 }
 
 # return arrays as \0 separated strings (like CGI.pm)
+# assume data is UTF8 encoded
 sub decode_urlencoded {
     my ($data) = @_;
 
@@ -619,14 +813,19 @@ sub decode_urlencoded {
        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;
 
-       if (defined(my $old = $res->{$k})) {
-           $res->{$k} = "$old\0$v";
-       } else {
-           $res->{$k} = $v;
-       }       
+       if (defined($v)) {
+           $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})) {
+               $v = "$old\0$v";
+           }
+       }
+
+       $res->{$k} = $v;
     }
     return $res;
 }
@@ -637,7 +836,15 @@ sub extract_params {
     my $params = {};
 
     if ($method eq 'PUT' || $method eq 'POST') {
-       $params = decode_urlencoded($r->content);
+       my $ct;
+       if (my $ctype = $r->header('Content-Type')) {
+           $ct = parse_content_type($ctype);
+       }
+       if (defined($ct) && $ct eq 'application/json')  {
+           $params = decode_json($r->content);
+       } else {
+           $params = decode_urlencoded($r->content);
+       }
     }
 
     my $query_params = decode_urlencoded($r->url->query());
@@ -646,7 +853,7 @@ sub extract_params {
        $params->{$k} = $query_params->{$k};
     }
 
-    return PVE::Tools::decode_utf8_parameters($params);
+    return $params;
 }
 
 sub handle_api2_request {
@@ -657,17 +864,15 @@ sub handle_api2_request {
 
        my ($rel_uri, $format) = &$split_abs_uri($path, $self->{base_uri});
 
-       my $formatter = PVE::APIServer::Formatter::get_formatter($format);
+       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) {
@@ -680,14 +885,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;
 
@@ -706,7 +910,7 @@ sub handle_api2_request {
            $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});
+                                $r->uri, $auth, $res->{proxy_params});
            return;
 
        } elsif ($upgrade && ($method eq 'GET') && ($path =~ m|websocket$|)) {
@@ -738,13 +942,16 @@ sub handle_api2_request {
            $delay = 0 if $delay < 0;
        }
 
-       if ($res->{info} && $res->{info}->{formatter}) {
-           if (defined(my $func = $res->{info}->{formatter}->{$format})) {
-               $formatter = $func;
-           }
+       my $download = $res->{download};
+       $download //= $res->{data}->{download}
+            if defined($res->{data}) && ref($res->{data}) eq 'HASH';
+       if (defined($download)) {
+           send_file_start($self, $reqstate, $download);
+           return;
        }
 
-       my ($raw, $ct, $nocomp) = &$formatter($res, $res->{data}, $params, $path, $auth);
+       my ($raw, $ct, $nocomp) = $formatter->($res, $res->{data}, $params, $path,
+                                              $auth, $self->{formatter_config});
 
        my $resp;
        if (ref($raw) && (ref($raw) eq 'HTTP::Response')) {
@@ -766,10 +973,10 @@ sub handle_spice_proxy_request {
 
     eval {
 
-        die "Port $spiceport is not allowed" if ($spiceport < 61000 || $spiceport > 61099);
-
-       my $rpcenv = $self->{rpcenv};
-       $rpcenv->init_request();
+       my ($minport, $maxport) = PVE::Tools::spice_port_range();
+       if ($spiceport < $minport || $spiceport > $maxport) {
+           die "SPICE Port $spiceport is not in allowed range ($minport, $maxport)\n";
+       }
 
        my $clientip = $reqstate->{peer_host};
        my $r = $reqstate->{request};
@@ -778,9 +985,9 @@ sub handle_spice_proxy_request {
 
         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};
+           $self->dprint("REMOTE CONNECT $vmid, $remip, $connect_str");
         } else {
-           print "$$: CONNECT $vmid, $node, $spiceport\n" if $self->{debug};
+           $self->dprint("CONNECT $vmid, $node, $spiceport");
        }
 
        if ($remip && $r->header('PVEDisableProxy')) {
@@ -795,10 +1002,10 @@ sub handle_spice_proxy_request {
        my $remport = $remip ? 3128 : $spiceport;
 
        tcp_connect $remhost, $remport, sub {
-           my ($fh) = @_ 
+           my ($fh) = @_
                or die "connect to '$remhost:$remport' failed: $!";
 
-           print "$$: CONNECTed to '$remhost:$remport'\n" if $self->{debug};
+           $self->dprint("CONNECTed to '$remhost:$remport'");
            $reqstate->{proxyhdl} = AnyEvent::Handle->new(
                fh => $fh,
                rbuf_max => 64*1024,
@@ -851,7 +1058,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
@@ -871,9 +1079,12 @@ sub handle_spice_proxy_request {
                $reqstate->{proxyhdl}->push_write($header);
                $reqstate->{proxyhdl}->push_read(line => sub {
                    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);
@@ -899,7 +1110,7 @@ sub handle_request {
 
     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);
@@ -913,6 +1124,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}) {
@@ -942,7 +1155,7 @@ sub handle_request {
            }
        }
 
-       die "no such file '$path'";
+       die "no such file '$path'\n";
     };
     if (my $err = $@) {
        $self->error($reqstate, 501, $err);
@@ -995,14 +1208,14 @@ sub file_upload_multipart {
                }
            } else {
                my $len = length($hdl->{rbuf});
-               substr($hdl->{rbuf}, 0, $len - $rstate->{maxheader}, '') 
+               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 - $!" 
+               die "write to temporary file failed - $!"
                    if syswrite($rstate->{outfh}, $rest) != $len;
                $rstate->{ctx}->add($rest);
                $rstate->{params}->{filename} = $rstate->{filename};
@@ -1014,7 +1227,7 @@ sub file_upload_multipart {
                my $wlen = $len - $rstate->{boundlen};
                if ($wlen > 0) {
                    my $data = substr($hdl->{rbuf}, 0, $wlen, '');
-                   die "write to temporary file failed - $!" 
+                   die "write to temporary file failed - $!"
                        if syswrite($rstate->{outfh}, $data) != $wlen;
                    $rstate->{bytes} += $wlen;
                    $rstate->{ctx}->add($data);
@@ -1034,7 +1247,7 @@ sub file_upload_multipart {
                    $rstate->{phase} = -1; # skip
                }
            }
-       } else { # skip 
+       } else { # skip
            my $len = length($hdl->{rbuf});
            substr($hdl->{rbuf}, 0, $len, ''); # empty rbuf
        }
@@ -1042,15 +1255,15 @@ sub file_upload_multipart {
        $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 
+           $rstate->{done} = 1; # make sure we dont get called twice
            if ($rstate->{phase} < 0 || !$rstate->{md5sum}) {
-               die "upload failed\n"; 
+               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})", 
+               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);
            }
@@ -1066,13 +1279,13 @@ 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;
 }
 
@@ -1092,7 +1305,7 @@ sub parse_content_disposition {
            $filename =~ s/^"(.*)"$/$1/;
        }
     }
+
     return  wantarray ? ($disp, $name, $filename) : $disp;
 }
 
@@ -1100,7 +1313,7 @@ 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() . $$);
 }
@@ -1116,7 +1329,7 @@ sub unshift_read_header {
        eval {
            # print "$$: got header: $line\n" if $self->{debug};
 
-           die "to many http header lines\n" if ++$state->{count} >= $limit_max_headers;
+           die "too many http header lines (> $limit_max_headers)\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};
@@ -1168,11 +1381,16 @@ sub unshift_read_header {
 
                my $len = $r->header('Content-Length');
 
+               my $host_header = $r->header('Host');
+               if (my $rpcenv = $self->{rpcenv}) {
+                   $rpcenv->set_request_host($host_header);
+               }
+
                # header processing complete - authenticate now
 
                my $auth = {};
                if ($self->{spiceproxy}) {
-                   my $connect_str = $r->header('Host');
+                   my $connect_str = $host_header;
                    my ($vmid, $node, $port) = $self->verify_spice_connect_url($connect_str);
                    if (!(defined($vmid) && $node && $port)) {
                        $self->error($reqstate, HTTP_UNAUTHORIZED, "invalid ticket");
@@ -1183,7 +1401,19 @@ sub unshift_read_header {
                } 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 $auth_header = $r->header('Authorization');
+
+                   # prefer actual cookie
+                   my $ticket = PVE::APIServer::Formatter::extract_auth_value($cookie, $self->{cookie_name});
+
+                   # fallback to cookie in 'Authorization' header
+                   $ticket = PVE::APIServer::Formatter::extract_auth_value($auth_header, $self->{cookie_name})
+                       if !$ticket;
+
+                   # finally, fallback to API token if no ticket has been provided so far
+                   my $api_token;
+                   $api_token = PVE::APIServer::Formatter::extract_auth_value($auth_header, $self->{apitoken_name})
+                       if !$ticket;
 
                    my ($rel_uri, $format) = &$split_abs_uri($path, $self->{base_uri});
                    if (!$format) {
@@ -1191,20 +1421,25 @@ 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, $api_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);
+
+                       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;
@@ -1213,7 +1448,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);
@@ -1232,17 +1467,18 @@ sub unshift_read_header {
                    }
 
                    my $ctype = $r->header('Content-Type');
-                   my ($ct, $boundary) = parse_content_type($ctype) if $ctype;
+                   my ($ct, $boundary);
+                   ($ct, $boundary)= parse_content_type($ctype) if $ctype;
 
                    if ($auth->{isUpload} && !$self->{trusted_env}) {
-                       die "upload 'Content-Type '$ctype' not implemented\n" 
+                       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};
+                       $self->dprint("start upload $path $ct $boundary");
 
                        my $tmpfilename = get_upload_filename();
                        my $outfh = IO::File->new($tmpfilename, O_RDWR|O_CREAT|O_EXCL, 0600) ||
@@ -1275,7 +1511,7 @@ sub unshift_read_header {
                        return;
                    }
 
-                   if (!$ct || $ct eq 'application/x-www-form-urlencoded') {
+                   if (!$ct || $ct eq 'application/x-www-form-urlencoded' || $ct eq 'application/json') {
                        $reqstate->{hdl}->unshift_read(chunk => $len, sub {
                            my ($hdl, $data) = @_;
                            $r->content($data);
@@ -1388,8 +1624,6 @@ sub accept {
 
     fh_nonblocking $clientfh, 1;
 
-    $self->{conn_count}++;
-
     return $clientfh;
 }
 
@@ -1428,9 +1662,15 @@ sub wait_end_loop {
 
 sub check_host_access {
     my ($self, $clientip) = @_;
-    
+
+    $clientip = PVE::APIServer::Utils::normalize_v4_in_v6($clientip);
     my $cip = Net::IP->new($clientip);
 
+    if (!$cip) {
+       $self->dprint("client IP not parsable: $@");
+       return 0;
+    }
+
     my $match_allow = 0;
     my $match_deny = 0;
 
@@ -1438,6 +1678,7 @@ sub check_host_access {
        foreach my $t (@{$self->{allow_from}}) {
            if ($t->overlaps($cip)) {
                $match_allow = 1;
+               $self->dprint("client IP allowed: ". $t->prefix());
                last;
            }
        }
@@ -1446,6 +1687,7 @@ sub check_host_access {
     if ($self->{deny_from}) {
        foreach my $t (@{$self->{deny_from}}) {
            if ($t->overlaps($cip)) {
+               $self->dprint("client IP denied: ". $t->prefix());
                $match_deny = 1;
                last;
            }
@@ -1463,29 +1705,40 @@ sub check_host_access {
 sub accept_connections {
     my ($self) = @_;
 
+    my ($clientfh, $handle_creation);
     eval {
 
-       while (my $clientfh = $self->accept()) {
+       while ($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}) {
+           if ($self->{conn_count} + 1 >= $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));
+           } else {
+               $self->dprint("getpeername failed: $!");
+               close($clientfh);
+               next;
            }
 
            if (!$self->{trusted_env} && !$self->check_host_access($reqstate->{peer_host})) {
-               print "$$: ABORT request from $reqstate->{peer_host} - access denied\n" if $self->{debug};
+               $self->dprint("ABORT request from $reqstate->{peer_host} - access denied");
                $reqstate->{log}->{code} = 403;
                $self->log_request($reqstate);
+               close($clientfh);
                next;
            }
 
+           # Increment conn_count before creating new handle, since creation
+           # triggers callbacks, which can potentialy decrement (e.g.
+           # on_error) conn_count before AnyEvent::Handle->new() returns.
+           $handle_creation = 1;
+           $self->{conn_count}++;
            $reqstate->{hdl} = AnyEvent::Handle->new(
                fh => $clientfh,
                rbuf_max => 64*1024,
@@ -1508,8 +1761,9 @@ sub accept_connections {
                    if (my $err = $@) { syslog('err', "$err"); }
                },
                ($self->{tls_ctx} ? (tls => "accept", tls_ctx => $self->{tls_ctx}) : ()));
+           $handle_creation = 0;
 
-           print "$$: ACCEPT FH" .  $clientfh->fileno() . " CONN$self->{conn_count}\n" if $self->{debug};
+           $self->dprint("ACCEPT FH" .  $clientfh->fileno() . " CONN$self->{conn_count}");
 
            $self->push_request_header($reqstate);
        }
@@ -1517,6 +1771,15 @@ sub accept_connections {
 
     if (my $err = $@) {
        syslog('err', $err);
+       $self->dprint("connection accept error: $err");
+       close($clientfh);
+       if ($handle_creation) {
+           if ($self->{conn_count} <= 0) {
+               warn "connection count <= 0 not decrementing!\n";
+           } else {
+               $self->{conn_count}--;
+           }
+       }
        $self->{end_loop} = 1;
     }
 
@@ -1574,14 +1837,33 @@ 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});
     }
 
     my $self = bless { %args }, $class;
 
     $self->{cookie_name} //= 'PVEAuthCookie';
+    $self->{apitoken_name} //= 'PVEAPIToken';
     $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(apitoken_name 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 $jsbase = '/usr/share/javascript';
+    add_dirs($self->{dirs}, '/js/' => "$jsbase/");
+    # libjs-bootstrap uses symlinks for this, which we do not want to allow..
+    my $glyphicons = '/usr/share/fonts/truetype/glyphicons/';
+    add_dirs($self->{dirs}, '/js/bootstrap/fonts/' => "$glyphicons");
 
     # init inotify
     PVE::INotify::inotify_init();
@@ -1601,16 +1883,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}) {
@@ -1654,6 +1951,24 @@ sub new {
     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 {
@@ -1664,8 +1979,15 @@ sub verify_spice_connect_url {
     #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) = @_;
+    my ($self, $method, $rel_uri, $ticket, $token, $api_token, $peer_host) = @_;
 
     die "implement me";
 
@@ -1675,18 +1997,40 @@ sub auth_handler {
     #    userid => $username,
     #    age => $age,
     #    isUpload => $isUpload,
-    #    cookie_name => $self->{cookie_name},
+    #    api_token => $api_token,
     #};
 }
 
-
 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 {