implement file upload

And remove CGI.pm dependency (because we want nonblocking upload).
This commit is contained in:
Dietmar Maurer 2013-04-12 12:51:28 +02:00
parent bc8f141b9a
commit 519ea5b07f
3 changed files with 375 additions and 143 deletions

View File

@ -2,10 +2,13 @@ package PVE::HTTPServer;
use strict;
use warnings;
use Time::HiRes qw(usleep ualarm gettimeofday tv_interval);
use Socket qw(IPPROTO_TCP TCP_NODELAY SOMAXCONN);
use POSIX qw(strftime EINTR EAGAIN);
use Fcntl;
use IO::File;
use File::stat qw();
use Digest::MD5;
use AnyEvent::Strict;
use AnyEvent::Util qw(guard fh_nonblocking WSAEWOULDBLOCK WSAEINPROGRESS);
use AnyEvent::Handle;
@ -24,11 +27,8 @@ use HTTP::Status qw(:constants);
use HTTP::Headers;
use HTTP::Response;
use CGI; # fixme: remove this!
# DOS attack prevention
# fixme: remove CGI.pm
$CGI::DISABLE_UPLOADS = 1; # no uploads
$CGI::POST_MAX = 1024 * 10; # max 10K posts
# fixme
# POST_MAX = 1024 * 10; # max 10K posts
use Data::Dumper; # fixme: remove
@ -39,6 +39,17 @@ my $known_methods = {
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 {
my ($self, $reqstate) = @_;
@ -75,6 +86,11 @@ sub log_aborted_request {
sub client_do_disconnect {
my ($self, $reqstate) = @_;
if ($reqstate->{tmpfilename}) {
unlink $reqstate->{tmpfilename};
delete $reqstate->{tmpfilename};
}
my $hdl = delete $reqstate->{hdl};
if (!$hdl) {
@ -103,8 +119,13 @@ sub finish_response {
delete $reqstate->{request};
delete $reqstate->{proto};
if ($reqstate->{tmpfilename}) {
unlink $reqstate->{tmpfilename};
delete $reqstate->{tmpfilename};
}
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 {
eval { $self->push_request_header($reqstate); };
warn $@ if $@;
@ -244,17 +265,17 @@ sub send_file_start {
}
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 {
my $target;
my $keep_alive = 1;
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 = 0;
} else {
$target = "https://$host:8006$abs_uri";
$target = "https://$host:8006$uri";
}
my $headers = {
@ -271,8 +292,7 @@ sub proxy_request {
if ($method eq 'POST' || $method eq 'PUT') {
$headers->{'Content-Type'} = 'application/x-www-form-urlencoded';
# We use a temporary URI object to format
# the application/x-www-form-urlencoded content.
# use URI object to format application/x-www-form-urlencoded content.
my $url = URI->new('http:');
$url->query_form(%$params);
$content = $url->query;
@ -309,21 +329,40 @@ sub proxy_request {
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 ($r, $method) = @_;
# NOTE: HTTP::Request::Params return undef instead of ''
#my $parser = HTTP::Request::Params->new({req => $r});
#my $params = $parser->params;
my $post_params = {};
my $params = {};
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 $params = $post_params || {};
my $query_params = decode_urlencoded($r->url->query());
foreach my $k (keys %{$query_params}) {
$params->{$k} = $query_params->{$k};
@ -333,42 +372,41 @@ my $extract_params = sub {
};
sub handle_api2_request {
my ($self, $reqstate) = @_;
my ($self, $reqstate, $auth, $upload_state) = @_;
eval {
my $r = $reqstate->{request};
my $method = $r->method();
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) {
$self->error($reqstate, HTTP_NOT_IMPLEMENTED, "no such uri");
return;
}
print Dumper($upload_state) if $upload_state;
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
$reqstate->{log}->{userid} = $userid;
if ($res->{proxy}) {
if ($self->{trusted_env}) {
@ -376,8 +414,11 @@ sub handle_api2_request {
return;
}
$self->proxy_request($reqstate, $r, $clientip, $res->{proxy}, $method,
$r->uri, $ticket, $token, $res->{proxy_params});
$res->{proxy_params}->{tmpfilename} = $reqstate->{tmpfilename} if $upload_state;
# fixme: cleanup parameter list
$self->proxy_request($reqstate, $clientip, $res->{proxy}, $method,
$r->uri, $auth->{ticket}, $auth->{token}, $res->{proxy_params});
return;
}
@ -389,38 +430,29 @@ sub handle_api2_request {
$resp->header("Content-Type" => $ct);
$resp->content($raw);
$self->response($reqstate, $resp);
return;
};
warn $@ if $@;
if (my $err = $@) {
$self->error($reqstate, 501, $err);
}
}
sub handle_request {
my ($self, $reqstate) = @_;
#print "REQUEST" . Dumper($reqstate->{request});
my ($self, $reqstate, $auth) = @_;
eval {
my $r = $reqstate->{request};
my $method = $r->method();
my $path = $r->uri->path();
# print "REQUEST $path\n";
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);
if ($path =~ m!$baseuri!) {
$self->handle_api2_request($reqstate, $auth);
return;
}
if ($self->{pages} && ($method eq 'GET') && (my $handler = $self->{pages}->{$path})) {
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);
} elsif (ref($handler) eq 'HASH') {
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 {
my ($self, $reqstate, $state) = @_;
@ -471,9 +654,18 @@ sub unshift_read_header {
my $r = $reqstate->{request};
if ($line eq '') {
my $path = $r->uri->path();
my $method = $r->method();
$r->push_header($state->{key}, $state->{val})
if $state->{key};
if (!$known_methods->{$method}) {
my $resp = HTTP::Response->new(HTTP_NOT_IMPLEMENTED, "method '$method' not available");
$self->response($reqstate, $resp);
return;
}
my $conn = $r->header('Connection');
if ($conn) {
@ -484,9 +676,16 @@ sub unshift_read_header {
}
}
# how much content to read?
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');
# fixme: how can we make PVEClientIP header trusted?
@ -496,19 +695,90 @@ sub unshift_read_header {
$r->header('PVEClientIP', $reqstate->{peer_host});
}
if ($te && lc($te) eq 'chunked') {
# Handle chunked transfer encoding
$self->error($reqstate, 501, "chunked transfer encoding not supported");
} elsif ($te) {
$self->error($reqstate, 501, "Unknown transfer encoding '$te'");
} elsif (defined($len)) {
$reqstate->{hdl}->unshift_read (chunk => $len, sub {
my ($hdl, $data) = @_;
$r->content($data);
$self->handle_request($reqstate);
});
my $len = $r->header('Content-Length');
# header processing complete - authenticate now
my $auth = {};
if ($path =~ m!$baseuri!) {
my $token = $r->header('CSRFPreventionToken');
my $cookie = $r->header('Cookie');
my $ticket = PVE::REST::extract_auth_cookie($cookie);
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 {
$self->handle_request($reqstate);
$self->handle_request($reqstate, $auth);
}
} elsif ($line =~ /^([^:\s]+)\s*:\s*(.*)/) {
$r->push_header($state->{key}, $state->{val}) if $state->{key};
@ -655,7 +925,7 @@ sub accept_connections {
$reqstate->{hdl} = AnyEvent::Handle->new(
fh => $clientfh,
rbuf_max => 32768, # fixme: set smaller max read buffer ?
rbuf_max => 64*1024,
timeout => $self->{timeout},
linger => 0, # avoid problems with ssh - really needed ?
on_eof => sub {

View File

@ -2,6 +2,7 @@ package PVE::REST;
use warnings;
use strict;
use English;
use PVE::Cluster;
use PVE::SafeSyslog;
use PVE::Tools;
@ -19,22 +20,8 @@ use URI::Escape;
use Data::Dumper; # fixme: remove
# my $MaxRequestsPerChild = 200;
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 {
my ($cookie) = @_;
@ -184,15 +171,14 @@ my $exc_to_res = sub {
return $resp;
};
sub rest_handler {
my ($rpcenv, $clientip, $method, $abs_uri, $rel_uri, $ticket, $token) = @_;
sub auth_handler {
my ($rpcenv, $clientip, $method, $rel_uri, $ticket, $token) = @_;
# set environment variables
$rpcenv->set_user(undef);
$rpcenv->set_language('C'); # fixme:
$rpcenv->set_client_ip($clientip);
my $euid = $>;
my $require_auth = 1;
# explicitly allow some calls without auth
@ -207,58 +193,49 @@ sub rest_handler {
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$|) {
my ($node, $storeid) = ($1, $2);
# we disable CSRF checks if $isUpload is set,
# to improve security we check user upload permission here
my $perm = { check => ['perm', "/storage/$storeid", ['Datastore.AllocateTemplate']] };
$rpcenv->check_api2_permissions($perm, $username, {});
$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);
if ($method eq 'POST' && $rel_uri =~ m|^/nodes/([^/]+)/storage/([^/]+)/upload$|) {
my ($node, $storeid) = ($1, $2);
# we disable CSRF checks if $isUpload is set,
# to improve security we check user upload permission here
my $perm = { check => ['perm', "/storage/$storeid", ['Datastore.AllocateTemplate']] };
$rpcenv->check_api2_permissions($perm, $username, {});
$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');
}
# 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 ($handler, $info) = PVE::API2->find_handler($method, $rel_uri, $uri_param);
if (!$handler || !$info) {
return {
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}) {
if (defined($uri_param->{$p})) {
return {
@ -270,7 +247,7 @@ sub rest_handler {
}
# 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 = $@) {
return &$exc_to_res($err, HTTP_FORBIDDEN);
}
@ -283,7 +260,7 @@ sub rest_handler {
die "proxy parameter '$pn' does not exists" if !$node;
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);
}
};
@ -295,11 +272,7 @@ sub rest_handler {
}
}
if ($info->{protected} && ($euid != 0)) {
if ($isUpload) {
my $uinfo = $rpcenv->get_upload_info('filename');
$params->{tmpfilename} = $uinfo->{tmpfilename};
}
if ($info->{protected} && ($EUID != 0)) {
return { proxy => 'localhost' , proxy_params => $params }
}
@ -325,13 +298,4 @@ sub rest_handler {
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;

View File

@ -165,7 +165,7 @@ exit (0);
# so we must be very careful here
sub get_index {
my ($server, $r, $params) = @_;
my ($server, $r, $args) = @_;
my $lang = 'en';
my $username;
@ -183,8 +183,6 @@ sub get_index {
}
}
my $args = $r->url->query_form_hash();
my $workspace = defined($args->{console}) ?
"PVE.ConsoleWorkspace" : "PVE.StdWorkspace";