diff --git a/PVE/API2.pm b/PVE/API2.pm index 6544ab62..31851250 100644 --- a/PVE/API2.pm +++ b/PVE/API2.pm @@ -4,7 +4,12 @@ use strict; use warnings; use PVE::pvecfg; +use PVE::REST; use PVE::RESTHandler; +use HTTP::Status; +use JSON; +use HTML::Entities; +use PVE::JSONSchema; use base qw(PVE::RESTHandler); @@ -108,4 +113,187 @@ __PACKAGE__->register_method ({ return $res; }}); + +# register result formaters + +my $prepare_response_data = sub { + my ($format, $res) = @_; + + my $success = 1; + my $new = { + data => $res->{data}, + }; + if (scalar(keys %{$res->{errors}})) { + $success = 0; + $new->{errors} = $res->{errors}; + } + + if ($format eq 'extjs' || $format eq 'htmljs') { + # HACK: extjs wants 'success' property instead of useful HTTP status codes + if (HTTP::Status::is_error($res->{status})) { + $success = 0; + $new->{message} = $res->{message} || status_message($res->{status}); + $new->{status} = $res->{status} || 200; + $res->{message} = undef; + $res->{status} = 200; + } + $new->{success} = $success; + } + + if ($success && $res->{total}) { + $new->{total} = $res->{total}; + } + + if ($success && $res->{changes}) { + $new->{changes} = $res->{changes}; + } + + $res->{data} = $new; +}; + +PVE::REST::register_formater('json', sub { + my ($res, $data, $param, $path, $auth) = @_; + + my $nocomp = 0; + + my $ct = 'application/json;charset=UTF-8'; + + &$prepare_response_data('json', $res); + + my $raw = to_json($res->{data}, {utf8 => 1, allow_nonref => 1}); + + return ($raw, $ct, $nocomp); +}); + + +PVE::REST::register_formater('extjs', sub { + my ($res, $data, $param, $path, $auth) = @_; + + my $nocomp = 0; + + my $ct = 'application/json;charset=UTF-8'; + + &$prepare_response_data('extjs', $res); + + my $raw = to_json($res->{data}, {utf8 => 1, allow_nonref => 1}); + + return ($raw, $ct, $nocomp); +}); + +PVE::REST::register_formater('htmljs', sub { + my ($res, $data, $param, $path, $auth) = @_; + + my $nocomp = 0; + + # we use this for extjs file upload forms + + my $ct = 'text/html;charset=UTF-8'; + + &$prepare_response_data('htmljs', $res); + + my $raw = encode_entities(to_json($res->{data}, {allow_nonref => 1})); + + return ($raw, $ct, $nocomp); +}); + + +PVE::REST::register_formater('spiceconfig', sub { + my ($res, $data, $param, $path, $auth) = @_; + + my $nocomp = 0; + + my $ct = 'application/x-virt-viewer;charset=UTF-8'; + + &$prepare_response_data('spiceconfig', $res); + + $data = $res->{data}; + + my $raw; + + if ($data && ref($data) && ref($data->{data})) { + $raw = "[virt-viewer]\n"; + while (my ($key, $value) = each %{$data->{data}}) { + $raw .= "$key=$value\n" if defined($value); + } + } + + return ($raw, $ct, $nocomp); +}); + +PVE::REST::register_formater('png', sub { + my ($res, $data, $param, $path, $auth) = @_; + + my $nocomp = 1; + + my $ct = 'image/png'; + + &$prepare_response_data('png', $res); + + $data = $res->{data}; + + # fixme: better to revove that whole png thing ? + + my $filename; + my $raw = ''; + + if ($data && ref($data) && ref($data->{data}) && + $data->{data}->{filename} && defined($data->{data}->{image})) { + $filename = $data->{data}->{filename}; + $raw = $data->{data}->{image}; + } + + return ($raw, $ct, $nocomp); +}); + +PVE::REST::register_formater('html', sub { + my ($res, $data, $param, $path, $auth) = @_; + + my $nocomp = 0; + + my $ct = 'text/html;charset=UTF-8'; + + &$prepare_response_data('html', $res); + + $data = $res->{data}; + + my $info = $res->{info}; + + my $raw = ""; + if (!HTTP::Status::is_success($res->{status})) { + my $msg = $res->{message} || ''; + $raw .= "

ERROR $res->{status} $msg

"; + } + my $lnk = PVE::JSONSchema::method_get_child_link($info); + + if ($lnk && $data && $data->{data} && HTTP::Status::is_success($res->{status})) { + + my $href = $lnk->{href}; + if ($href =~ m/^\{(\S+)\}$/) { + my $prop = $1; + $path =~ s/\/+$//; # remove trailing slash + foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @{$data->{data}}) { + next if !ref($elem); + + if (defined(my $value = $elem->{$prop})) { + if ($value ne '') { + if (scalar(keys %$elem) > 1) { + my $tv = to_json($elem, {allow_nonref => 1, canonical => 1}); + $raw .= "$value
$tv

"; + } else { + $raw .= "$value
"; + } + } + } + } + } + } else { + $raw .= "
";
+	$raw .= encode_entities(to_json($data, {allow_nonref => 1, pretty => 1}));
+	$raw .= "
"; + } + $raw .= ""; + + return ($raw, $ct, $nocomp); +}); + 1; diff --git a/PVE/HTTPServer.pm b/PVE/HTTPServer.pm index 3ecfb01e..6682eb37 100755 --- a/PVE/HTTPServer.pm +++ b/PVE/HTTPServer.pm @@ -49,7 +49,7 @@ 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|spiceconfig)(\/.*)?$/; + my ($format, $rel_uri) = $abs_uri =~ m/^\Q$baseuri\E\/+([a-z][a-z0-9]+)(\/.*)?$/; $rel_uri = '/' if !$rel_uri; return wantarray ? ($rel_uri, $format) : $rel_uri; @@ -445,8 +445,11 @@ sub handle_api2_request { my $path = $r->uri->path(); my ($rel_uri, $format) = split_abs_uri($path); - if (!$format) { - $self->error($reqstate, HTTP_NOT_IMPLEMENTED, "no such uri"); + + my $formater = PVE::REST::get_formater($format); + + if (!defined($formater)) { + $self->error($reqstate, HTTP_NOT_IMPLEMENTED, "no such uri $rel_uri, $format"); return; } @@ -501,9 +504,14 @@ sub handle_api2_request { $delay = 0 if $delay < 0; } - PVE::REST::prepare_response_data($format, $res); - my ($raw, $ct, $nocomp) = PVE::REST::format_response_data($format, $res, $path); - + if ($res->{info} && $res->{info}->{formater}) { + if (defined(my $func = $res->{info}->{formater}->{$format})) { + $formater = $func; + } + } + + my ($raw, $ct, $nocomp) = &$formater($res, $res->{data}, $path, $auth); + my $resp; if (ref($raw) && (ref($raw) eq 'HTTP::Response')) { $resp = $raw; @@ -954,7 +962,23 @@ sub unshift_read_header { $rel_uri, $ticket, $token); }; if (my $err = $@) { - $self->error($reqstate, HTTP_UNAUTHORIZED, $err); + # always delay unauthorized calls by 3 seconds + my $delay = 3; + if (my $formater = PVE::REST::get_login_formater($format)) { + my ($raw, $ct, $nocomp) = &$formater($path, $auth); + my $resp; + if (ref($raw) && (ref($raw) eq 'HTTP::Response')) { + $resp = $raw; + } else { + $resp = HTTP::Response->new(HTTP_UNAUTHORIZED, "Login Required"); + $resp->header("Content-Type" => $ct); + $resp->content($raw); + } + $self->response($reqstate, $resp, undef, $nocomp, 3); + } else { + my $resp = HTTP::Response->new(HTTP_UNAUTHORIZED, $err); + $self->response($reqstate, $resp, undef, 0, $delay); + } return; } } diff --git a/PVE/REST.pm b/PVE/REST.pm index 7c3dd6f4..5a0792bb 100644 --- a/PVE/REST.pm +++ b/PVE/REST.pm @@ -52,125 +52,6 @@ sub create_auth_cookie { return "${cookie_name}=$encticket; path=/; secure;"; } -sub format_response_data { - my($format, $res, $uri) = @_; - - my $data = $res->{data}; - my $info = $res->{info}; - - my ($ct, $raw, $nocomp); - - if ($format eq 'json') { - $ct = 'application/json;charset=UTF-8'; - $raw = to_json($data, {utf8 => 1, allow_nonref => 1}); - } elsif ($format eq 'html') { - $ct = 'text/html;charset=UTF-8'; - $raw = ""; - if (!is_success($res->{status})) { - my $msg = $res->{message} || ''; - $raw .= "

ERROR $res->{status} $msg

"; - } - my $lnk = PVE::JSONSchema::method_get_child_link($info); - if ($lnk && $data && $data->{data} && is_success($res->{status})) { - - my $href = $lnk->{href}; - if ($href =~ m/^\{(\S+)\}$/) { - my $prop = $1; - $uri =~ s/\/+$//; # remove trailing slash - foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @{$data->{data}}) { - next if !ref($elem); - - if (defined(my $value = $elem->{$prop})) { - if ($value ne '') { - if (scalar(keys %$elem) > 1) { - my $tv = to_json($elem, {allow_nonref => 1, canonical => 1}); - $raw .= "$value
$tv

"; - } else { - $raw .= "$value
"; - } - } - } - } - } - } else { - $raw .= "
";
-	    $raw .= encode_entities(to_json($data, {allow_nonref => 1, pretty => 1}));
-	    $raw .= "
"; - } - $raw .= ""; - - } elsif ($format eq 'png') { - $ct = 'image/png'; - $nocomp = 1; - # fixme: better to revove that whole png thing ? - - my $filename; - $raw = ''; - - if ($data && ref($data) && ref($data->{data}) && - $data->{data}->{filename} && defined($data->{data}->{image})) { - $filename = $data->{data}->{filename}; - $raw = $data->{data}->{image}; - } - - } elsif ($format eq 'extjs') { - $ct = 'application/json;charset=UTF-8'; - $raw = to_json($data, {utf8 => 1, allow_nonref => 1}); - } elsif ($format eq 'htmljs') { - # we use this for extjs file upload forms - $ct = 'text/html;charset=UTF-8'; - $raw = encode_entities(to_json($data, {allow_nonref => 1})); - } elsif ($format eq 'spiceconfig') { - $ct = 'application/x-virt-viewer;charset=UTF-8'; - if ($data && ref($data) && ref($data->{data})) { - $raw = "[virt-viewer]\n"; - while (my ($key, $value) = each %{$data->{data}}) { - $raw .= "$key=$value\n" if defined($value); - } - } - } else { - $ct = 'text/plain;charset=UTF-8'; - $raw = to_json($data, {utf8 => 1, allow_nonref => 1, pretty => 1}); - } - - return wantarray ? ($raw, $ct, $nocomp) : $raw; -} - -sub prepare_response_data { - my ($format, $res) = @_; - - my $success = 1; - my $new = { - data => $res->{data}, - }; - if (scalar(keys %{$res->{errors}})) { - $success = 0; - $new->{errors} = $res->{errors}; - } - - if ($format eq 'extjs' || $format eq 'htmljs') { - # HACK: extjs wants 'success' property instead of useful HTTP status codes - if (is_error($res->{status})) { - $success = 0; - $new->{message} = $res->{message} || status_message($res->{status}); - $new->{status} = $res->{status} || 200; - $res->{message} = undef; - $res->{status} = 200; - } - $new->{success} = $success; - } - - if ($success && $res->{total}) { - $new->{total} = $res->{total}; - } - - if ($success && $res->{changes}) { - $new->{changes} = $res->{changes}; - } - - $res->{data} = $new; -} - my $exc_to_res = sub { my ($info, $err, $status) = @_; @@ -201,7 +82,7 @@ sub auth_handler { # explicitly allow some calls without auth if (($rel_uri eq '/access/domains' && $method eq 'GET') || - ($rel_uri eq '/access/ticket' && $method eq 'POST')) { + ($rel_uri eq '/access/ticket' && ($method eq 'GET' || $method eq 'POST'))) { $require_auth = 0; } @@ -318,4 +199,83 @@ sub rest_handler { return $resp; } +# generic formater support + +my $formater_hash = {}; + +sub register_formater { + my ($format, $func) = @_; + + die "formater '$format' already defined" if $formater_hash->{$format}; + + $formater_hash->{$format} = { + func => $func, + }; +} + +sub get_formater { + my ($format) = @_; + + return undef if !$format; + + my $info = $formater_hash->{$format}; + return undef if !$info; + + return $info->{func}; +} + +my $login_formater_hash = {}; + +sub register_login_formater { + my ($format, $func) = @_; + + die "login formater '$format' already defined" if $login_formater_hash->{$format}; + + $login_formater_hash->{$format} = { + func => $func, + }; +} + +sub get_login_formater { + my ($format) = @_; + + return undef if !$format; + + my $info = $login_formater_hash->{$format}; + return undef if !$info; + + return $info->{func}; +} + +sub register_page_formater { + my (%config) = @_; + + my $base_handler_class = $config{base_handler_class} || + die "missing base_handler_class"; + + my $format = $config{format} || + die "missing format"; + + die "format '$format' is not registered" + if !$formater_hash->{$format}; + + my $path = $config{path} || + die "missing path"; + + my $method = $config{method} || + die "missing method"; + + my $code = $config{code} || + die "missing formater code"; + + my $uri_param = {}; + my ($handler, $info) = $base_handler_class->find_handler($method, $path, $uri_param); + die "unabe to find handler for '$method: $path'" if !($handler && $info); + + die "duplicate formater for '$method: $path'" + if $info->{formater} && $info->{formater}->{$format}; + + $info->{formater}->{$format} = $code; +} + 1;