mirror of
https://git.proxmox.com/git/pve-manager
synced 2025-08-14 12:44:56 +00:00
implement file upload
And remove CGI.pm dependency (because we want nonblocking upload).
This commit is contained in:
parent
5a68b2b2f0
commit
d06a1c62c3
@ -2,10 +2,13 @@ package PVE::HTTPServer;
|
|||||||
|
|
||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings;
|
||||||
|
use Time::HiRes qw(usleep ualarm gettimeofday tv_interval);
|
||||||
use Socket qw(IPPROTO_TCP TCP_NODELAY SOMAXCONN);
|
use Socket qw(IPPROTO_TCP TCP_NODELAY SOMAXCONN);
|
||||||
use POSIX qw(strftime EINTR EAGAIN);
|
use POSIX qw(strftime EINTR EAGAIN);
|
||||||
use Fcntl;
|
use Fcntl;
|
||||||
|
use IO::File;
|
||||||
use File::stat qw();
|
use File::stat qw();
|
||||||
|
use Digest::MD5;
|
||||||
use AnyEvent::Strict;
|
use AnyEvent::Strict;
|
||||||
use AnyEvent::Util qw(guard fh_nonblocking WSAEWOULDBLOCK WSAEINPROGRESS);
|
use AnyEvent::Util qw(guard fh_nonblocking WSAEWOULDBLOCK WSAEINPROGRESS);
|
||||||
use AnyEvent::Handle;
|
use AnyEvent::Handle;
|
||||||
@ -24,11 +27,8 @@ use HTTP::Status qw(:constants);
|
|||||||
use HTTP::Headers;
|
use HTTP::Headers;
|
||||||
use HTTP::Response;
|
use HTTP::Response;
|
||||||
|
|
||||||
use CGI; # fixme: remove this!
|
# fixme
|
||||||
# DOS attack prevention
|
# POST_MAX = 1024 * 10; # max 10K posts
|
||||||
# fixme: remove CGI.pm
|
|
||||||
$CGI::DISABLE_UPLOADS = 1; # no uploads
|
|
||||||
$CGI::POST_MAX = 1024 * 10; # max 10K posts
|
|
||||||
|
|
||||||
use Data::Dumper; # fixme: remove
|
use Data::Dumper; # fixme: remove
|
||||||
|
|
||||||
@ -39,6 +39,17 @@ my $known_methods = {
|
|||||||
DELETE => 1,
|
DELETE => 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
my $baseuri = "/api2";
|
||||||
|
|
||||||
|
sub split_abs_uri {
|
||||||
|
my ($abs_uri) = @_;
|
||||||
|
|
||||||
|
my ($format, $rel_uri) = $abs_uri =~ m/^\Q$baseuri\E\/+(html|text|json|extjs|png|htmljs)(\/.*)?$/;
|
||||||
|
$rel_uri = '/' if !$rel_uri;
|
||||||
|
|
||||||
|
return wantarray ? ($rel_uri, $format) : $rel_uri;
|
||||||
|
}
|
||||||
|
|
||||||
sub log_request {
|
sub log_request {
|
||||||
my ($self, $reqstate) = @_;
|
my ($self, $reqstate) = @_;
|
||||||
|
|
||||||
@ -75,6 +86,11 @@ sub log_aborted_request {
|
|||||||
sub client_do_disconnect {
|
sub client_do_disconnect {
|
||||||
my ($self, $reqstate) = @_;
|
my ($self, $reqstate) = @_;
|
||||||
|
|
||||||
|
if ($reqstate->{tmpfilename}) {
|
||||||
|
unlink $reqstate->{tmpfilename};
|
||||||
|
delete $reqstate->{tmpfilename};
|
||||||
|
}
|
||||||
|
|
||||||
my $hdl = delete $reqstate->{hdl};
|
my $hdl = delete $reqstate->{hdl};
|
||||||
|
|
||||||
if (!$hdl) {
|
if (!$hdl) {
|
||||||
@ -103,8 +119,13 @@ sub finish_response {
|
|||||||
delete $reqstate->{request};
|
delete $reqstate->{request};
|
||||||
delete $reqstate->{proto};
|
delete $reqstate->{proto};
|
||||||
|
|
||||||
|
if ($reqstate->{tmpfilename}) {
|
||||||
|
unlink $reqstate->{tmpfilename};
|
||||||
|
delete $reqstate->{tmpfilename};
|
||||||
|
}
|
||||||
|
|
||||||
if (!$self->{end_loop} && $reqstate->{keep_alive} > 0) {
|
if (!$self->{end_loop} && $reqstate->{keep_alive} > 0) {
|
||||||
# print "KEEPALIVE $reqstate->{keep_alive}\n";
|
# print "KEEPALIVE $reqstate->{keep_alive}\n" if $self->{debug};
|
||||||
$hdl->on_read(sub {
|
$hdl->on_read(sub {
|
||||||
eval { $self->push_request_header($reqstate); };
|
eval { $self->push_request_header($reqstate); };
|
||||||
warn $@ if $@;
|
warn $@ if $@;
|
||||||
@ -244,17 +265,17 @@ sub send_file_start {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sub proxy_request {
|
sub proxy_request {
|
||||||
my ($self, $reqstate, $r, $clientip, $host, $method, $abs_uri, $ticket, $token, $params) = @_;
|
my ($self, $reqstate, $clientip, $host, $method, $uri, $ticket, $token, $params) = @_;
|
||||||
|
|
||||||
eval {
|
eval {
|
||||||
my $target;
|
my $target;
|
||||||
my $keep_alive = 1;
|
my $keep_alive = 1;
|
||||||
if ($host eq 'localhost') {
|
if ($host eq 'localhost') {
|
||||||
$target = "http://$host:85$abs_uri";
|
$target = "http://$host:85$uri";
|
||||||
# keep alive for localhost is not worth (connection setup is about 0.2ms)
|
# keep alive for localhost is not worth (connection setup is about 0.2ms)
|
||||||
$keep_alive = 0;
|
$keep_alive = 0;
|
||||||
} else {
|
} else {
|
||||||
$target = "https://$host:8006$abs_uri";
|
$target = "https://$host:8006$uri";
|
||||||
}
|
}
|
||||||
|
|
||||||
my $headers = {
|
my $headers = {
|
||||||
@ -271,8 +292,7 @@ sub proxy_request {
|
|||||||
|
|
||||||
if ($method eq 'POST' || $method eq 'PUT') {
|
if ($method eq 'POST' || $method eq 'PUT') {
|
||||||
$headers->{'Content-Type'} = 'application/x-www-form-urlencoded';
|
$headers->{'Content-Type'} = 'application/x-www-form-urlencoded';
|
||||||
# We use a temporary URI object to format
|
# use URI object to format application/x-www-form-urlencoded content.
|
||||||
# the application/x-www-form-urlencoded content.
|
|
||||||
my $url = URI->new('http:');
|
my $url = URI->new('http:');
|
||||||
$url->query_form(%$params);
|
$url->query_form(%$params);
|
||||||
$content = $url->query;
|
$content = $url->query;
|
||||||
@ -309,21 +329,40 @@ sub proxy_request {
|
|||||||
warn $@ if $@;
|
warn $@ if $@;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# return arrays as \0 separated strings (like CGI.pm)
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (defined(my $old = $res->{$k})) {
|
||||||
|
$res->{$k} = "$old\0$v";
|
||||||
|
} else {
|
||||||
|
$res->{$k} = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
|
||||||
my $extract_params = sub {
|
my $extract_params = sub {
|
||||||
my ($r, $method) = @_;
|
my ($r, $method) = @_;
|
||||||
|
|
||||||
# NOTE: HTTP::Request::Params return undef instead of ''
|
my $params = {};
|
||||||
#my $parser = HTTP::Request::Params->new({req => $r});
|
|
||||||
#my $params = $parser->params;
|
|
||||||
|
|
||||||
my $post_params = {};
|
|
||||||
|
|
||||||
if ($method eq 'PUT' || $method eq 'POST') {
|
if ($method eq 'PUT' || $method eq 'POST') {
|
||||||
$post_params = CGI->new($r->content())->Vars;
|
$params = decode_urlencoded($r->content);
|
||||||
}
|
}
|
||||||
|
|
||||||
my $query_params = CGI->new($r->url->query)->Vars;
|
my $query_params = decode_urlencoded($r->url->query());
|
||||||
my $params = $post_params || {};
|
|
||||||
|
|
||||||
foreach my $k (keys %{$query_params}) {
|
foreach my $k (keys %{$query_params}) {
|
||||||
$params->{$k} = $query_params->{$k};
|
$params->{$k} = $query_params->{$k};
|
||||||
@ -333,42 +372,41 @@ my $extract_params = sub {
|
|||||||
};
|
};
|
||||||
|
|
||||||
sub handle_api2_request {
|
sub handle_api2_request {
|
||||||
my ($self, $reqstate) = @_;
|
my ($self, $reqstate, $auth, $upload_state) = @_;
|
||||||
|
|
||||||
eval {
|
eval {
|
||||||
my $r = $reqstate->{request};
|
my $r = $reqstate->{request};
|
||||||
my $method = $r->method();
|
my $method = $r->method();
|
||||||
my $path = $r->uri->path();
|
my $path = $r->uri->path();
|
||||||
|
|
||||||
my ($rel_uri, $format) = PVE::REST::split_abs_uri($path);
|
my ($rel_uri, $format) = split_abs_uri($path);
|
||||||
if (!$format) {
|
if (!$format) {
|
||||||
$self->error($reqstate, HTTP_NOT_IMPLEMENTED, "no such uri");
|
$self->error($reqstate, HTTP_NOT_IMPLEMENTED, "no such uri");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print Dumper($upload_state) if $upload_state;
|
||||||
|
|
||||||
my $rpcenv = $self->{rpcenv};
|
my $rpcenv = $self->{rpcenv};
|
||||||
my $headers = $r->headers;
|
|
||||||
|
|
||||||
my $token = $headers->header('CSRFPreventionToken');
|
my $params;
|
||||||
|
|
||||||
my $cookie = $headers->header('Cookie');
|
if ($upload_state) {
|
||||||
|
$params = $upload_state->{params};
|
||||||
|
} else {
|
||||||
|
$params = &$extract_params($r, $method); # fixme
|
||||||
|
}
|
||||||
|
|
||||||
my $ticket = PVE::REST::extract_auth_cookie($cookie);
|
delete $params->{_dc}; # remove disable cache parameter
|
||||||
|
|
||||||
my $params = &$extract_params($r, $method);
|
my $clientip = $reqstate->{peer_host};
|
||||||
|
|
||||||
my $clientip = $headers->header('PVEClientIP');
|
$rpcenv->init_request();
|
||||||
|
|
||||||
$rpcenv->init_request(params => $params);
|
my $res = PVE::REST::rest_handler($rpcenv, $clientip, $method, $rel_uri, $auth, $params);
|
||||||
|
|
||||||
my $res = PVE::REST::rest_handler($rpcenv, $clientip, $method, $path, $rel_uri, $ticket, $token);
|
|
||||||
|
|
||||||
# todo: eval { $userid = $rpcenv->get_user(); };
|
|
||||||
my $userid = $rpcenv->{user}; # this is faster
|
|
||||||
$rpcenv->set_user(undef); # clear after request
|
$rpcenv->set_user(undef); # clear after request
|
||||||
|
|
||||||
$reqstate->{log}->{userid} = $userid;
|
|
||||||
|
|
||||||
if ($res->{proxy}) {
|
if ($res->{proxy}) {
|
||||||
|
|
||||||
if ($self->{trusted_env}) {
|
if ($self->{trusted_env}) {
|
||||||
@ -376,8 +414,11 @@ sub handle_api2_request {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$self->proxy_request($reqstate, $r, $clientip, $res->{proxy}, $method,
|
$res->{proxy_params}->{tmpfilename} = $reqstate->{tmpfilename} if $upload_state;
|
||||||
$r->uri, $ticket, $token, $res->{proxy_params});
|
|
||||||
|
# fixme: cleanup parameter list
|
||||||
|
$self->proxy_request($reqstate, $clientip, $res->{proxy}, $method,
|
||||||
|
$r->uri, $auth->{ticket}, $auth->{token}, $res->{proxy_params});
|
||||||
return;
|
return;
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -389,38 +430,29 @@ sub handle_api2_request {
|
|||||||
$resp->header("Content-Type" => $ct);
|
$resp->header("Content-Type" => $ct);
|
||||||
$resp->content($raw);
|
$resp->content($raw);
|
||||||
$self->response($reqstate, $resp);
|
$self->response($reqstate, $resp);
|
||||||
|
|
||||||
return;
|
|
||||||
};
|
};
|
||||||
warn $@ if $@;
|
if (my $err = $@) {
|
||||||
|
$self->error($reqstate, 501, $err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sub handle_request {
|
sub handle_request {
|
||||||
my ($self, $reqstate) = @_;
|
my ($self, $reqstate, $auth) = @_;
|
||||||
|
|
||||||
#print "REQUEST" . Dumper($reqstate->{request});
|
|
||||||
|
|
||||||
eval {
|
eval {
|
||||||
my $r = $reqstate->{request};
|
my $r = $reqstate->{request};
|
||||||
my $method = $r->method();
|
my $method = $r->method();
|
||||||
my $path = $r->uri->path();
|
my $path = $r->uri->path();
|
||||||
|
|
||||||
# print "REQUEST $path\n";
|
if ($path =~ m!$baseuri!) {
|
||||||
|
$self->handle_api2_request($reqstate, $auth);
|
||||||
if (!$known_methods->{$method}) {
|
|
||||||
my $resp = HTTP::Response->new(HTTP_NOT_IMPLEMENTED, "method '$method' not available");
|
|
||||||
$self->response($reqstate, $resp);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($path =~ m!/api2!) {
|
|
||||||
$self->handle_api2_request($reqstate);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($self->{pages} && ($method eq 'GET') && (my $handler = $self->{pages}->{$path})) {
|
if ($self->{pages} && ($method eq 'GET') && (my $handler = $self->{pages}->{$path})) {
|
||||||
if (ref($handler) eq 'CODE') {
|
if (ref($handler) eq 'CODE') {
|
||||||
my ($resp, $userid) = &$handler($self, $reqstate->{request});
|
my $params = decode_urlencoded($r->url->query());
|
||||||
|
my ($resp, $userid) = &$handler($self, $reqstate->{request}, $params);
|
||||||
$self->response($reqstate, $resp);
|
$self->response($reqstate, $resp);
|
||||||
} elsif (ref($handler) eq 'HASH') {
|
} elsif (ref($handler) eq 'HASH') {
|
||||||
if (my $filename = $handler->{file}) {
|
if (my $filename = $handler->{file}) {
|
||||||
@ -457,6 +489,157 @@ sub handle_request {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub file_upload_multipart {
|
||||||
|
my ($self, $reqstate, $auth, $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 im multipart - abort upload");
|
||||||
|
$rstate->{phase} = -1;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
$rstate->{fieldname} = $name;
|
||||||
|
|
||||||
|
if (!$ct) {
|
||||||
|
# found form data for field $name
|
||||||
|
$rstate->{phase} = 2;
|
||||||
|
} elsif ($ct && $ct eq 'application/octet-stream' && $name eq 'filename' && $filename) {
|
||||||
|
# found file upload data
|
||||||
|
$rstate->{phase} = 1;
|
||||||
|
$rstate->{filename} = $filename;
|
||||||
|
} else {
|
||||||
|
syslog('err', "wrong content type '$ct' im multipart - abort upload");
|
||||||
|
$rstate->{phase} = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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);
|
||||||
|
if ($len < 1024) { # fixme: max data size
|
||||||
|
$rstate->{params}->{$rstate->{fieldname}} = $rest;
|
||||||
|
$rstate->{phase} = $eof ? 100 : 0;
|
||||||
|
} else {
|
||||||
|
syslog('err', "for 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}) {
|
||||||
|
$self->error($reqstate, 501, "upload failed"); # fixme: better msg
|
||||||
|
} 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)", $rstate->{size}, $elapsed, $rate);
|
||||||
|
$self->handle_api2_request($reqstate, $auth, $rstate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (my $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+?)\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 {
|
sub unshift_read_header {
|
||||||
my ($self, $reqstate, $state) = @_;
|
my ($self, $reqstate, $state) = @_;
|
||||||
|
|
||||||
@ -471,9 +654,18 @@ sub unshift_read_header {
|
|||||||
my $r = $reqstate->{request};
|
my $r = $reqstate->{request};
|
||||||
if ($line eq '') {
|
if ($line eq '') {
|
||||||
|
|
||||||
|
my $path = $r->uri->path();
|
||||||
|
my $method = $r->method();
|
||||||
|
|
||||||
$r->push_header($state->{key}, $state->{val})
|
$r->push_header($state->{key}, $state->{val})
|
||||||
if $state->{key};
|
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 $conn = $r->header('Connection');
|
||||||
|
|
||||||
if ($conn) {
|
if ($conn) {
|
||||||
@ -484,9 +676,16 @@ sub unshift_read_header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# how much content to read?
|
|
||||||
my $te = $r->header('Transfer-Encoding');
|
my $te = $r->header('Transfer-Encoding');
|
||||||
my $len = $r->header('Content-Length');
|
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 $pveclientip = $r->header('PVEClientIP');
|
||||||
|
|
||||||
# fixme: how can we make PVEClientIP header trusted?
|
# fixme: how can we make PVEClientIP header trusted?
|
||||||
@ -496,19 +695,90 @@ sub unshift_read_header {
|
|||||||
$r->header('PVEClientIP', $reqstate->{peer_host});
|
$r->header('PVEClientIP', $reqstate->{peer_host});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($te && lc($te) eq 'chunked') {
|
my $len = $r->header('Content-Length');
|
||||||
# Handle chunked transfer encoding
|
|
||||||
$self->error($reqstate, 501, "chunked transfer encoding not supported");
|
# header processing complete - authenticate now
|
||||||
} elsif ($te) {
|
|
||||||
$self->error($reqstate, 501, "Unknown transfer encoding '$te'");
|
my $auth = {};
|
||||||
} elsif (defined($len)) {
|
if ($path =~ m!$baseuri!) {
|
||||||
$reqstate->{hdl}->unshift_read (chunk => $len, sub {
|
my $token = $r->header('CSRFPreventionToken');
|
||||||
my ($hdl, $data) = @_;
|
my $cookie = $r->header('Cookie');
|
||||||
$r->content($data);
|
my $ticket = PVE::REST::extract_auth_cookie($cookie);
|
||||||
$self->handle_request($reqstate);
|
|
||||||
});
|
my ($rel_uri, $format) = split_abs_uri($path);
|
||||||
|
if (!$format) {
|
||||||
|
$self->error($reqstate, HTTP_NOT_IMPLEMENTED, "no such uri");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
eval {
|
||||||
|
$auth = PVE::REST::auth_handler($self->{rpcenv}, $reqstate->{peer_host}, $method,
|
||||||
|
$rel_uri, $ticket, $token);
|
||||||
|
};
|
||||||
|
if (my $err = $@) {
|
||||||
|
$self->error($reqstate, HTTP_UNAUTHORIZED, $err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$reqstate->{log}->{userid} = $auth->{userid};
|
||||||
|
|
||||||
|
if (defined($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 ($auth->{isUpload} && !$self->{trusted_env}) {
|
||||||
|
die "upload 'Content-Type '$ctype' not implemented\n"
|
||||||
|
if !($boundary && ($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,
|
||||||
|
starttime => [gettimeofday],
|
||||||
|
outfh => $outfh,
|
||||||
|
};
|
||||||
|
$reqstate->{tmpfilename} = $tmpfilename;
|
||||||
|
$reqstate->{hdl}->on_read(sub { $self->file_upload_multipart($reqstate, $auth, $state); });
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$self->error($reqstate, 506, "upload 'Content-Type '$ctype' not implemented");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$self->handle_request($reqstate);
|
$self->handle_request($reqstate, $auth);
|
||||||
}
|
}
|
||||||
} elsif ($line =~ /^([^:\s]+)\s*:\s*(.*)/) {
|
} elsif ($line =~ /^([^:\s]+)\s*:\s*(.*)/) {
|
||||||
$r->push_header($state->{key}, $state->{val}) if $state->{key};
|
$r->push_header($state->{key}, $state->{val}) if $state->{key};
|
||||||
@ -655,7 +925,7 @@ sub accept_connections {
|
|||||||
|
|
||||||
$reqstate->{hdl} = AnyEvent::Handle->new(
|
$reqstate->{hdl} = AnyEvent::Handle->new(
|
||||||
fh => $clientfh,
|
fh => $clientfh,
|
||||||
rbuf_max => 32768, # fixme: set smaller max read buffer ?
|
rbuf_max => 64*1024,
|
||||||
timeout => $self->{timeout},
|
timeout => $self->{timeout},
|
||||||
linger => 0, # avoid problems with ssh - really needed ?
|
linger => 0, # avoid problems with ssh - really needed ?
|
||||||
on_eof => sub {
|
on_eof => sub {
|
||||||
|
108
PVE/REST.pm
108
PVE/REST.pm
@ -2,6 +2,7 @@ package PVE::REST;
|
|||||||
|
|
||||||
use warnings;
|
use warnings;
|
||||||
use strict;
|
use strict;
|
||||||
|
use English;
|
||||||
use PVE::Cluster;
|
use PVE::Cluster;
|
||||||
use PVE::SafeSyslog;
|
use PVE::SafeSyslog;
|
||||||
use PVE::Tools;
|
use PVE::Tools;
|
||||||
@ -19,22 +20,8 @@ use URI::Escape;
|
|||||||
|
|
||||||
use Data::Dumper; # fixme: remove
|
use Data::Dumper; # fixme: remove
|
||||||
|
|
||||||
# my $MaxRequestsPerChild = 200;
|
|
||||||
|
|
||||||
my $cookie_name = 'PVEAuthCookie';
|
my $cookie_name = 'PVEAuthCookie';
|
||||||
|
|
||||||
my $baseuri = "/api2";
|
|
||||||
|
|
||||||
my $debug_enabled;
|
|
||||||
sub enable_debug {
|
|
||||||
$debug_enabled = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
sub debug_msg {
|
|
||||||
return if !$debug_enabled;
|
|
||||||
syslog('info', @_);
|
|
||||||
}
|
|
||||||
|
|
||||||
sub extract_auth_cookie {
|
sub extract_auth_cookie {
|
||||||
my ($cookie) = @_;
|
my ($cookie) = @_;
|
||||||
|
|
||||||
@ -184,15 +171,14 @@ my $exc_to_res = sub {
|
|||||||
return $resp;
|
return $resp;
|
||||||
};
|
};
|
||||||
|
|
||||||
sub rest_handler {
|
sub auth_handler {
|
||||||
my ($rpcenv, $clientip, $method, $abs_uri, $rel_uri, $ticket, $token) = @_;
|
my ($rpcenv, $clientip, $method, $rel_uri, $ticket, $token) = @_;
|
||||||
|
|
||||||
# set environment variables
|
# set environment variables
|
||||||
|
$rpcenv->set_user(undef);
|
||||||
$rpcenv->set_language('C'); # fixme:
|
$rpcenv->set_language('C'); # fixme:
|
||||||
$rpcenv->set_client_ip($clientip);
|
$rpcenv->set_client_ip($clientip);
|
||||||
|
|
||||||
my $euid = $>;
|
|
||||||
|
|
||||||
my $require_auth = 1;
|
my $require_auth = 1;
|
||||||
|
|
||||||
# explicitly allow some calls without auth
|
# explicitly allow some calls without auth
|
||||||
@ -207,58 +193,49 @@ sub rest_handler {
|
|||||||
|
|
||||||
if ($require_auth) {
|
if ($require_auth) {
|
||||||
|
|
||||||
eval {
|
die "No ticket\n" if !$ticket;
|
||||||
die "No ticket\n" if !$ticket;
|
|
||||||
|
|
||||||
($username, $age) = PVE::AccessControl::verify_ticket($ticket);
|
($username, $age) = PVE::AccessControl::verify_ticket($ticket);
|
||||||
|
|
||||||
$rpcenv->set_user($username);
|
$rpcenv->set_user($username);
|
||||||
|
|
||||||
if ($method eq 'POST' && $rel_uri =~ m|^/nodes/([^/]+)/storage/([^/]+)/upload$|) {
|
if ($method eq 'POST' && $rel_uri =~ m|^/nodes/([^/]+)/storage/([^/]+)/upload$|) {
|
||||||
my ($node, $storeid) = ($1, $2);
|
my ($node, $storeid) = ($1, $2);
|
||||||
# we disable CSRF checks if $isUpload is set,
|
# we disable CSRF checks if $isUpload is set,
|
||||||
# to improve security we check user upload permission here
|
# to improve security we check user upload permission here
|
||||||
my $perm = { check => ['perm', "/storage/$storeid", ['Datastore.AllocateTemplate']] };
|
my $perm = { check => ['perm', "/storage/$storeid", ['Datastore.AllocateTemplate']] };
|
||||||
$rpcenv->check_api2_permissions($perm, $username, {});
|
$rpcenv->check_api2_permissions($perm, $username, {});
|
||||||
$isUpload = 1;
|
$isUpload = 1;
|
||||||
}
|
|
||||||
|
|
||||||
# we skip CSRF check for file upload, because it is
|
|
||||||
# difficult to pass CSRF HTTP headers with native html forms,
|
|
||||||
# and it should not be necessary at all.
|
|
||||||
PVE::AccessControl::verify_csrf_prevention_token($username, $token)
|
|
||||||
if !$isUpload && ($euid != 0) && ($method ne 'GET');
|
|
||||||
};
|
|
||||||
if (my $err = $@) {
|
|
||||||
return &$exc_to_res($err, HTTP_UNAUTHORIZED);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# we skip CSRF check for file upload, because it is
|
||||||
|
# difficult to pass CSRF HTTP headers with native html forms,
|
||||||
|
# and it should not be necessary at all.
|
||||||
|
PVE::AccessControl::verify_csrf_prevention_token($username, $token)
|
||||||
|
if !$isUpload && ($EUID != 0) && ($method ne 'GET');
|
||||||
}
|
}
|
||||||
|
|
||||||
# we are authenticated now
|
return {
|
||||||
|
ticket => $ticket,
|
||||||
|
token => $token,
|
||||||
|
userid => $username,
|
||||||
|
age => $age,
|
||||||
|
isUpload => $isUpload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
sub rest_handler {
|
||||||
|
my ($rpcenv, $clientip, $method, $rel_uri, $auth, $params) = @_;
|
||||||
|
|
||||||
my $uri_param = {};
|
my $uri_param = {};
|
||||||
my ($handler, $info) = PVE::API2->find_handler($method, $rel_uri, $uri_param);
|
my ($handler, $info) = PVE::API2->find_handler($method, $rel_uri, $uri_param);
|
||||||
if (!$handler || !$info) {
|
if (!$handler || !$info) {
|
||||||
return {
|
return {
|
||||||
status => HTTP_NOT_IMPLEMENTED,
|
status => HTTP_NOT_IMPLEMENTED,
|
||||||
message => "Method '$method $abs_uri' not implemented",
|
message => "Method '$method $rel_uri' not implemented",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
# Note: we need to delay CGI parameter parsing until
|
|
||||||
# we are authenticated (avoid DOS (file upload) attacs)
|
|
||||||
|
|
||||||
my $params;
|
|
||||||
eval { $params = $rpcenv->parse_params($isUpload); };
|
|
||||||
if (my $err = $@) {
|
|
||||||
return {
|
|
||||||
status => HTTP_BAD_REQUEST,
|
|
||||||
message => "parameter parser failed: $err",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
delete $params->{_dc}; # remove disable cache parameter
|
|
||||||
|
|
||||||
foreach my $p (keys %{$params}) {
|
foreach my $p (keys %{$params}) {
|
||||||
if (defined($uri_param->{$p})) {
|
if (defined($uri_param->{$p})) {
|
||||||
return {
|
return {
|
||||||
@ -270,7 +247,7 @@ sub rest_handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# check access permissions
|
# check access permissions
|
||||||
eval { $rpcenv->check_api2_permissions($info->{permissions}, $username, $uri_param); };
|
eval { $rpcenv->check_api2_permissions($info->{permissions}, $auth->{userid}, $uri_param); };
|
||||||
if (my $err = $@) {
|
if (my $err = $@) {
|
||||||
return &$exc_to_res($err, HTTP_FORBIDDEN);
|
return &$exc_to_res($err, HTTP_FORBIDDEN);
|
||||||
}
|
}
|
||||||
@ -283,7 +260,7 @@ sub rest_handler {
|
|||||||
die "proxy parameter '$pn' does not exists" if !$node;
|
die "proxy parameter '$pn' does not exists" if !$node;
|
||||||
|
|
||||||
if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
|
if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
|
||||||
die "unable to proxy file uploads" if $isUpload;
|
die "unable to proxy file uploads" if $auth->{isUpload};
|
||||||
$remip = PVE::Cluster::remote_node_ip($node);
|
$remip = PVE::Cluster::remote_node_ip($node);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -295,11 +272,7 @@ sub rest_handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($info->{protected} && ($euid != 0)) {
|
if ($info->{protected} && ($EUID != 0)) {
|
||||||
if ($isUpload) {
|
|
||||||
my $uinfo = $rpcenv->get_upload_info('filename');
|
|
||||||
$params->{tmpfilename} = $uinfo->{tmpfilename};
|
|
||||||
}
|
|
||||||
return { proxy => 'localhost' , proxy_params => $params }
|
return { proxy => 'localhost' , proxy_params => $params }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -325,13 +298,4 @@ sub rest_handler {
|
|||||||
return $resp;
|
return $resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub split_abs_uri {
|
|
||||||
my ($abs_uri) = @_;
|
|
||||||
|
|
||||||
my ($format, $rel_uri) = $abs_uri =~ m/^\Q$baseuri\E\/+(html|text|json|extjs|png|htmljs)(\/.*)?$/;
|
|
||||||
$rel_uri = '/' if !$rel_uri;
|
|
||||||
|
|
||||||
return wantarray ? ($rel_uri, $format) : $rel_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
@ -165,7 +165,7 @@ exit (0);
|
|||||||
# so we must be very careful here
|
# so we must be very careful here
|
||||||
|
|
||||||
sub get_index {
|
sub get_index {
|
||||||
my ($server, $r, $params) = @_;
|
my ($server, $r, $args) = @_;
|
||||||
|
|
||||||
my $lang = 'en';
|
my $lang = 'en';
|
||||||
my $username;
|
my $username;
|
||||||
@ -183,8 +183,6 @@ sub get_index {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
my $args = $r->url->query_form_hash();
|
|
||||||
|
|
||||||
my $workspace = defined($args->{console}) ?
|
my $workspace = defined($args->{console}) ?
|
||||||
"PVE.ConsoleWorkspace" : "PVE.StdWorkspace";
|
"PVE.ConsoleWorkspace" : "PVE.StdWorkspace";
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user