diff --git a/PVE/API2.pm b/PVE/API2.pm index ed68cc98..7da769b4 100644 --- a/PVE/API2.pm +++ b/PVE/API2.pm @@ -6,7 +6,6 @@ use warnings; use PVE::pvecfg; use PVE::RESTHandler; use PVE::JSONSchema; -use PVE::API2::Formatter::Standard; use base qw(PVE::RESTHandler); diff --git a/PVE/API2/Formatter/Bootstrap.pm b/PVE/API2/Formatter/Bootstrap.pm index 75baf98a..2d656b8c 100644 --- a/PVE/API2/Formatter/Bootstrap.pm +++ b/PVE/API2/Formatter/Bootstrap.pm @@ -94,16 +94,22 @@ sub body { - Proxmox VE Portal at '$hostname' + Proxmox VE API - + + + - + diff --git a/PVE/API2/Formatter/HTML.pm b/PVE/API2/Formatter/HTML.pm new file mode 100644 index 00000000..10c95c75 --- /dev/null +++ b/PVE/API2/Formatter/HTML.pm @@ -0,0 +1,277 @@ +package PVE::API2::Formatter::HTML; + +use strict; +use warnings; + +use PVE::REST; +use PVE::HTTPServer; +use HTTP::Status; +use JSON; +use HTML::Entities; +use PVE::JSONSchema; +use PVE::API2::Formatter::Bootstrap; +use PVE::API2::Formatter::Standard; + +my $portal_format = 'html'; +my $portal_ct = 'text/html;charset=UTF-8'; + +my $baseurl = "/api2/$portal_format"; +my $login_url = "$baseurl/access/ticket"; + +sub render_page { + my ($doc, $html) = @_; + + my $items = []; + + push @$items, { + tag => 'li', + cn => { + tag => 'a', + href => $login_url, + onClick => "PVE.delete_auth_cookie();", + text => "Logout", + }}; + + + my $title = "Proxmox VE"; + + my $nav = $doc->el( + class => "navbar navbar-inverse navbar-fixed-top", + role => "navigation", cn => { + class => "container", cn => [ + { + class => "navbar-header", cn => [ + { + tag => 'button', + type => 'button', + class => "navbar-toggle", + 'data-toggle' => "collapse", + 'data-target' => ".navbar-collapse", + cn => [ + { tag => 'span', class => 'sr-only', text => "Toggle navigation" }, + { tag => 'span', class => 'icon-bar' }, + { tag => 'span', class => 'icon-bar' }, + { tag => 'span', class => 'icon-bar' }, + ], + }, + { + tag => 'a', + class => "navbar-brand", + href => $baseurl, + text => $title, + }, + ], + }, + { + class => "collapse navbar-collapse", + cn => { + tag => 'ul', + class => "nav navbar-nav", + cn => $items, + }, + }, + ], + }); + + $items = []; + my @pcomp = split('/', $doc->{url}); + shift @pcomp; # empty + shift @pcomp; # api2 + shift @pcomp; # $format + + my $href = $baseurl; + push @$items, { tag => 'li', cn => { + tag => 'a', + href => $href, + text => 'Home'}}; + + foreach my $comp (@pcomp) { + $href .= "/$comp"; + push @$items, { tag => 'li', cn => { + tag => 'a', + href => $href, + text => $comp}}; + } + + my $breadcrumbs = $doc->el(tag => 'ol', class => 'breadcrumb container', cn => $items); + + return $doc->body($nav . $breadcrumbs . $html); +} + +my $login_form = sub { + my ($doc, $param, $errmsg) = @_; + + $param = {} if !$param; + + my $username = $param->{username} || ''; + my $password = $param->{password} || ''; + + my $items = [ + { + tag => 'label', + text => "Please sign in", + }, + { + tag => 'input', + type => 'text', + class => 'form-control', + name => 'username', + value => $username, + placeholder => "Enter user name", + required => 1, + autofocus => 1, + }, + { + tag => 'input', + type => 'password', + class => 'form-control', + name => 'password', + value => $password, + placeholder => 'Password', + required => 1, + }, + ]; + + my $html = ''; + + $html .= $doc->alert(text => $errmsg) if ($errmsg); + + $html .= $doc->el( + class => 'container', + cn => { + tag => 'form', + role => 'form', + method => 'POST', + action => $login_url, + cn => [ + { + class => 'form-group', + cn => $items, + }, + { + tag => 'button', + type => 'submit', + class => 'btn btn-lg btn-primary btn-block', + text => "Sign in", + }, + ], + }); + + return $html; +}; + +PVE::HTTPServer::register_login_formatter($portal_format, sub { + my ($path, $auth) = @_; + + my $headers = HTTP::Headers->new(Location => $login_url); + return HTTP::Response->new(301, "Moved", $headers); +}); + +PVE::HTTPServer::register_formatter($portal_format, sub { + my ($res, $data, $param, $path, $auth) = @_; + + # fixme: clumsy! + PVE::API2::Formatter::Standard::prepare_response_data($portal_format, $res); + $data = $res->{data}; + + my $html = ''; + my $doc = PVE::API2::Formatter::Bootstrap->new($res, $path); + + if (!HTTP::Status::is_success($res->{status})) { + $html .= $doc->alert(text => "Error $res->{status}: $res->{message}"); + } + + my $info = $res->{info}; + 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 $items = []; + + 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 '') { + my $text = $value; + if (scalar(keys %$elem) > 1) { + my $tv = to_json($elem, {allow_nonref => 1, canonical => 1}); + $text = "$value $tv"; + } + push @$items, { + tag => 'a', + class => 'list-group-item', + href => "$path/$value", + text => $text, + } + } + } + } + + $html .= $doc->el(class => 'list-group', cn => $items); + + } else { + + my $json = to_json($data, {allow_nonref => 1, pretty => 1}); + $html .= $doc->el(tag => 'pre', text => $json); + } + + } else { + + my $json = to_json($data, {allow_nonref => 1, pretty => 1}); + $html .= $doc->el(tag => 'pre', text => $json); + } + + $html = $doc->el(class => 'container', html => $html); + + my $raw = render_page($doc, $html); + return ($raw, $portal_ct); +}); + +PVE::API2->register_page_formatter( + 'format' => $portal_format, + method => 'GET', + path => "/access/ticket", + code => sub { + my ($res, $data, $param, $path, $auth) = @_; + + my $doc = PVE::API2::Formatter::Bootstrap->new($res, $path); + + my $html = &$login_form($doc); + + my $raw = render_page($doc, $html); + return ($raw, $portal_ct); + }); + +PVE::API2->register_page_formatter( + 'format' => $portal_format, + method => 'POST', + path => "/access/ticket", + code => sub { + my ($res, $data, $param, $path, $auth) = @_; + + if (HTTP::Status::is_success($res->{status})) { + my $cookie = PVE::REST::create_auth_cookie($data->{ticket}); + my $headers = HTTP::Headers->new(Location => $baseurl, + 'Set-Cookie' => $cookie); + return HTTP::Response->new(301, "Moved", $headers); + } + + # Note: HTTP server redirects to 'GET /access/ticket', so below + # output is not really visible. + + my $doc = PVE::API2::Formatter::Bootstrap->new($res, $path); + + my $html = &$login_form($doc); + + my $raw = render_page($doc, $html); + return ($raw, $portal_ct); + }); + +1; diff --git a/PVE/API2/Formatter/Makefile b/PVE/API2/Formatter/Makefile index 418a34bc..cc4a9230 100644 --- a/PVE/API2/Formatter/Makefile +++ b/PVE/API2/Formatter/Makefile @@ -1,6 +1,8 @@ include ../../../defines.mk PERLSOURCE = \ + Bootstrap.pm \ + HTML.pm \ Standard.pm all: diff --git a/PVE/API2/Formatter/Standard.pm b/PVE/API2/Formatter/Standard.pm index ee5e28a2..f1cec1a0 100644 --- a/PVE/API2/Formatter/Standard.pm +++ b/PVE/API2/Formatter/Standard.pm @@ -11,7 +11,7 @@ use PVE::JSONSchema; # register result formatters -my $prepare_response_data = sub { +sub prepare_response_data { my ($format, $res) = @_; my $success = 1; @@ -44,7 +44,7 @@ my $prepare_response_data = sub { } $res->{data} = $new; -}; +} PVE::HTTPServer::register_formatter('json', sub { my ($res, $data, $param, $path, $auth) = @_; @@ -53,7 +53,7 @@ PVE::HTTPServer::register_formatter('json', sub { my $ct = 'application/json;charset=UTF-8'; - &$prepare_response_data('json', $res); + prepare_response_data('json', $res); my $raw = to_json($res->{data}, {utf8 => 1, allow_nonref => 1}); @@ -68,7 +68,7 @@ PVE::HTTPServer::register_formatter('extjs', sub { my $ct = 'application/json;charset=UTF-8'; - &$prepare_response_data('extjs', $res); + prepare_response_data('extjs', $res); my $raw = to_json($res->{data}, {utf8 => 1, allow_nonref => 1}); @@ -84,7 +84,7 @@ PVE::HTTPServer::register_formatter('htmljs', sub { my $ct = 'text/html;charset=UTF-8'; - &$prepare_response_data('htmljs', $res); + prepare_response_data('htmljs', $res); my $raw = encode_entities(to_json($res->{data}, {allow_nonref => 1})); @@ -99,7 +99,7 @@ PVE::HTTPServer::register_formatter('spiceconfig', sub { my $ct = 'application/x-virt-viewer;charset=UTF-8'; - &$prepare_response_data('spiceconfig', $res); + prepare_response_data('spiceconfig', $res); $data = $res->{data}; @@ -122,7 +122,7 @@ PVE::HTTPServer::register_formatter('png', sub { my $ct = 'image/png'; - &$prepare_response_data('png', $res); + prepare_response_data('png', $res); $data = $res->{data}; @@ -140,53 +140,3 @@ PVE::HTTPServer::register_formatter('png', sub { return ($raw, $ct, $nocomp); }); -PVE::HTTPServer::register_formatter('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); -}); diff --git a/PVE/HTTPServer.pm b/PVE/HTTPServer.pm index 2dfe22df..2de3579d 100755 --- a/PVE/HTTPServer.pm +++ b/PVE/HTTPServer.pm @@ -434,6 +434,10 @@ sub proxy_request { delete $hdr->{URL}; delete $hdr->{HTTPVersion}; my $header = HTTP::Headers->new(%$hdr); + if (my $location = $header->header('Location')) { + $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); @@ -560,7 +564,7 @@ sub handle_api2_request { } } - my ($raw, $ct, $nocomp) = &$formatter($res, $res->{data}, $path, $auth); + my ($raw, $ct, $nocomp) = &$formatter($res, $res->{data}, $params, $path, $auth); my $resp; if (ref($raw) && (ref($raw) eq 'HTTP::Response')) { diff --git a/bin/pvedaemon b/bin/pvedaemon index c6e93f98..276292c9 100755 --- a/bin/pvedaemon +++ b/bin/pvedaemon @@ -12,6 +12,8 @@ use Socket; use PVE::SafeSyslog; use PVE::APIDaemon; use PVE::API2; +use PVE::API2::Formatter::Standard; +use PVE::API2::Formatter::HTML; my $pidfile = "/var/run/pvedaemon.pid"; my $lockfile = "/var/lock/pvedaemon.lck"; diff --git a/bin/pveproxy b/bin/pveproxy index 15225315..aec049b3 100755 --- a/bin/pveproxy +++ b/bin/pveproxy @@ -20,6 +20,8 @@ use URI::QueryParam; use File::Find; use Data::Dumper; use PVE::API2; +use PVE::API2::Formatter::Standard; +use PVE::API2::Formatter::HTML; my $pidfile = "/var/run/pveproxy/pveproxy.pid"; my $lockfile = "/var/lock/pveproxy.lck";