mirror of
https://git.proxmox.com/git/pve-http-server
synced 2025-05-02 20:00:18 +00:00
add generic formatter framework
This commit is contained in:
parent
d08808bc8a
commit
63307bebd9
6
Makefile
6
Makefile
@ -22,6 +22,12 @@ deb ${DEB}:
|
||||
install:
|
||||
install -d -m 755 ${PERL5DIR}/PVE/APIServer
|
||||
install -m 0644 PVE/APIServer/AnyEvent.pm ${PERL5DIR}/PVE/APIServer
|
||||
install -m 0644 PVE/APIServer/Formatter.pm ${PERL5DIR}/PVE/APIServer
|
||||
install -d -m 755 ${PERL5DIR}/PVE/APIServer/Formatter
|
||||
install -m 0644 PVE/APIServer/Formatter/Standard.pm ${PERL5DIR}/PVE/APIServer/Formatter
|
||||
install -m 0644 PVE/APIServer/Formatter/Bootstrap.pm ${PERL5DIR}/PVE/APIServer/Formatter
|
||||
install -m 0644 PVE/APIServer/Formatter/HTML.pm ${PERL5DIR}/PVE/APIServer/Formatter
|
||||
|
||||
|
||||
.PHONY: upload
|
||||
upload: ${DEB}
|
||||
|
@ -24,6 +24,7 @@ use Compress::Zlib;
|
||||
use PVE::SafeSyslog;
|
||||
use PVE::INotify;
|
||||
use PVE::Tools;
|
||||
use PVE::APIServer::Formatter;
|
||||
|
||||
use Net::IP;
|
||||
use URI;
|
||||
@ -39,7 +40,6 @@ my $limit_max_headers = 30;
|
||||
my $limit_max_header_size = 8*1024;
|
||||
my $limit_max_post = 16*1024;
|
||||
|
||||
|
||||
my $known_methods = {
|
||||
GET => 1,
|
||||
POST => 1,
|
||||
@ -56,57 +56,6 @@ my $split_abs_uri = sub {
|
||||
return wantarray ? ($rel_uri, $format) : $rel_uri;
|
||||
};
|
||||
|
||||
# generic formatter support
|
||||
|
||||
my $formatter_hash = {};
|
||||
|
||||
sub register_formatter {
|
||||
my ($format, $func) = @_;
|
||||
|
||||
die "formatter '$format' already defined" if $formatter_hash->{$format};
|
||||
|
||||
$formatter_hash->{$format} = {
|
||||
func => $func,
|
||||
};
|
||||
}
|
||||
|
||||
sub get_formatter {
|
||||
my ($format) = @_;
|
||||
|
||||
return undef if !$format;
|
||||
|
||||
my $info = $formatter_hash->{$format};
|
||||
return undef if !$info;
|
||||
|
||||
return $info->{func};
|
||||
}
|
||||
|
||||
my $login_formatter_hash = {};
|
||||
|
||||
sub register_login_formatter {
|
||||
my ($format, $func) = @_;
|
||||
|
||||
die "login formatter '$format' already defined" if $login_formatter_hash->{$format};
|
||||
|
||||
$login_formatter_hash->{$format} = {
|
||||
func => $func,
|
||||
};
|
||||
}
|
||||
|
||||
sub get_login_formatter {
|
||||
my ($format) = @_;
|
||||
|
||||
return undef if !$format;
|
||||
|
||||
my $info = $login_formatter_hash->{$format};
|
||||
return undef if !$info;
|
||||
|
||||
return $info->{func};
|
||||
}
|
||||
|
||||
|
||||
# server implementation
|
||||
|
||||
sub log_request {
|
||||
my ($self, $reqstate) = @_;
|
||||
|
||||
@ -143,28 +92,6 @@ sub log_aborted_request {
|
||||
$self->log_request($reqstate);
|
||||
}
|
||||
|
||||
sub extract_auth_cookie {
|
||||
my ($cookie, $cookie_name) = @_;
|
||||
|
||||
return undef if !$cookie;
|
||||
|
||||
my $ticket = ($cookie =~ /(?:^|\s)\Q$cookie_name\E=([^;]*)/)[0];
|
||||
|
||||
if ($ticket && $ticket =~ m/^PVE%3A/) {
|
||||
$ticket = uri_unescape($ticket);
|
||||
}
|
||||
|
||||
return $ticket;
|
||||
}
|
||||
|
||||
sub create_auth_cookie {
|
||||
my ($ticket, $cookie_name) = @_;
|
||||
|
||||
my $encticket = uri_escape($ticket);
|
||||
|
||||
return "${cookie_name}=$encticket; path=/; secure;";
|
||||
}
|
||||
|
||||
sub cleanup_reqstate {
|
||||
my ($reqstate) = @_;
|
||||
|
||||
@ -606,7 +533,7 @@ sub proxy_request {
|
||||
PVEClientIP => $clientip,
|
||||
};
|
||||
|
||||
$headers->{'cookie'} = create_auth_cookie($ticket, $self->{cookie_name}) if $ticket;
|
||||
$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};
|
||||
|
||||
@ -730,7 +657,7 @@ sub handle_api2_request {
|
||||
|
||||
my ($rel_uri, $format) = &$split_abs_uri($path, $self->{base_uri});
|
||||
|
||||
my $formatter = get_formatter($format);
|
||||
my $formatter = PVE::APIServer::Formatter::get_formatter($format);
|
||||
|
||||
if (!defined($formatter)) {
|
||||
$self->error($reqstate, HTTP_NOT_IMPLEMENTED, "no such uri $rel_uri, $format");
|
||||
@ -1256,7 +1183,7 @@ sub unshift_read_header {
|
||||
} elsif ($path =~ m/^\Q$base_uri\E/) {
|
||||
my $token = $r->header('CSRFPreventionToken');
|
||||
my $cookie = $r->header('Cookie');
|
||||
my $ticket = extract_auth_cookie($cookie, $self->{cookie_name});
|
||||
my $ticket = PVE::APIServer::Formatter::extract_auth_cookie($cookie, $self->{cookie_name});
|
||||
|
||||
my ($rel_uri, $format) = &$split_abs_uri($path, $self->{base_uri});
|
||||
if (!$format) {
|
||||
@ -1276,7 +1203,7 @@ sub unshift_read_header {
|
||||
if (my $err = $@) {
|
||||
# always delay unauthorized calls by 3 seconds
|
||||
my $delay = 3;
|
||||
if (my $formatter = get_login_formatter($format)) {
|
||||
if (my $formatter = PVE::APIServer::Formatter::get_login_formatter($format)) {
|
||||
my ($raw, $ct, $nocomp) = &$formatter($path, $auth);
|
||||
my $resp;
|
||||
if (ref($raw) && (ref($raw) eq 'HTTP::Response')) {
|
||||
|
79
PVE/APIServer/Formatter.pm
Normal file
79
PVE/APIServer/Formatter.pm
Normal file
@ -0,0 +1,79 @@
|
||||
package PVE::APIServer::Formatter;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
# generic formatter support
|
||||
# PVE::APIServer::Formatter::* classes should register themselves here
|
||||
|
||||
my $formatter_hash = {};
|
||||
|
||||
sub register_formatter {
|
||||
my ($format, $func) = @_;
|
||||
|
||||
die "formatter '$format' already defined" if $formatter_hash->{$format};
|
||||
|
||||
$formatter_hash->{$format} = {
|
||||
func => $func,
|
||||
};
|
||||
}
|
||||
|
||||
sub get_formatter {
|
||||
my ($format) = @_;
|
||||
|
||||
return undef if !$format;
|
||||
|
||||
my $info = $formatter_hash->{$format};
|
||||
return undef if !$info;
|
||||
|
||||
return $info->{func};
|
||||
}
|
||||
|
||||
my $login_formatter_hash = {};
|
||||
|
||||
sub register_login_formatter {
|
||||
my ($format, $func) = @_;
|
||||
|
||||
die "login formatter '$format' already defined" if $login_formatter_hash->{$format};
|
||||
|
||||
$login_formatter_hash->{$format} = {
|
||||
func => $func,
|
||||
};
|
||||
}
|
||||
|
||||
sub get_login_formatter {
|
||||
my ($format) = @_;
|
||||
|
||||
return undef if !$format;
|
||||
|
||||
my $info = $login_formatter_hash->{$format};
|
||||
return undef if !$info;
|
||||
|
||||
return $info->{func};
|
||||
}
|
||||
|
||||
# some helper functions
|
||||
|
||||
sub extract_auth_cookie {
|
||||
my ($cookie, $cookie_name) = @_;
|
||||
|
||||
return undef if !$cookie;
|
||||
|
||||
my $ticket = ($cookie =~ /(?:^|\s)\Q$cookie_name\E=([^;]*)/)[0];
|
||||
|
||||
if ($ticket && $ticket =~ m/^PVE%3A/) {
|
||||
$ticket = uri_unescape($ticket);
|
||||
}
|
||||
|
||||
return $ticket;
|
||||
}
|
||||
|
||||
sub create_auth_cookie {
|
||||
my ($ticket, $cookie_name) = @_;
|
||||
|
||||
my $encticket = uri_escape($ticket);
|
||||
|
||||
return "${cookie_name}=$encticket; path=/; secure;";
|
||||
}
|
||||
|
||||
1;
|
238
PVE/APIServer/Formatter/Bootstrap.pm
Normal file
238
PVE/APIServer/Formatter/Bootstrap.pm
Normal file
@ -0,0 +1,238 @@
|
||||
package PVE::APIServer::Formatter::Bootstrap;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use URI::Escape;
|
||||
use HTML::Entities;
|
||||
use JSON;
|
||||
|
||||
use PVE::AccessControl; # to generate CSRF token
|
||||
|
||||
# Helpers to generate simple html pages using Bootstrap markup.
|
||||
|
||||
my $jssrc = <<_EOJS;
|
||||
PVE = {
|
||||
delete_auth_cookie: function() {
|
||||
document.cookie = "PVEAuthCookie=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/; secure;";
|
||||
},
|
||||
open_vm_console: function(node, vmid) {
|
||||
console.log("open vm " + vmid + " on node " + node);
|
||||
|
||||
var downloadWithName = function(uri, name) {
|
||||
var link = jQuery('#pve_console_anchor');
|
||||
link.attr("href", uri);
|
||||
|
||||
// Note: we need to tell android the correct file name extension
|
||||
// but we do not set 'download' tag for other environments, because
|
||||
// It can have strange side effects (additional user prompt on firefox)
|
||||
var andriod = navigator.userAgent.match(/Android/i) ? true : false;
|
||||
if (andriod) {
|
||||
link.attr("download", name);
|
||||
}
|
||||
|
||||
if (document.createEvent) {
|
||||
var evt = document.createEvent("MouseEvents");
|
||||
evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
|
||||
link.get(0).dispatchEvent(evt);
|
||||
} else {
|
||||
link.get(0).fireEvent('onclick');
|
||||
}
|
||||
};
|
||||
|
||||
jQuery.ajax("/api2/json/console", {
|
||||
data: { vmid: vmid, node: node },
|
||||
headers: { CSRFPreventionToken: PVE.CSRFPreventionToken },
|
||||
dataType: 'json',
|
||||
type: 'POST',
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
// fixme: howto view JS errors ?
|
||||
console.log("ERROR " + textStatus + ": " + errorThrown);
|
||||
},
|
||||
success: function(data) {
|
||||
var raw = "[virt-viewer]\\n";
|
||||
jQuery.each(data.data, function(k, v) {
|
||||
raw += k + "=" + v + "\\n";
|
||||
});
|
||||
var url = 'data:application/x-virt-viewer;charset=UTF-8,' +
|
||||
encodeURIComponent(raw);
|
||||
|
||||
downloadWithName(url, "pve-spice.vv");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
_EOJS
|
||||
|
||||
sub new {
|
||||
my ($class, $res, $url) = @_;
|
||||
|
||||
my $self = bless {
|
||||
url => $url,
|
||||
js => '',
|
||||
};
|
||||
|
||||
if (my $username = $res->{auth}->{userid}) {
|
||||
$self->{csrftoken} = PVE::AccessControl::assemble_csrf_prevention_token($username);
|
||||
}
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub body {
|
||||
my ($self, $html) = @_;
|
||||
|
||||
my $jssetup = '';
|
||||
|
||||
if ($self->{csrftoken}) {
|
||||
$jssetup .= "PVE.CSRFPreventionToken = '$self->{csrftoken}';\n";
|
||||
}
|
||||
|
||||
return <<_EOD;
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Proxmox VE API</title>
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<link href="/pve2/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<script type="text/javascript">
|
||||
$jssrc
|
||||
$jssetup
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
padding-top: 70px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
||||
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
|
||||
<!-- Include all compiled plugins (below), or include individual files as needed -->
|
||||
<script src="/pve2/js/bootstrap.min.js"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<a class="hidden" id="pve_console_anchor"></a>
|
||||
$html
|
||||
<script type="text/javascript">
|
||||
$self->{js}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
_EOD
|
||||
}
|
||||
|
||||
my $comp_id_counter = 0;
|
||||
|
||||
sub el {
|
||||
my ($self, %param) = @_;
|
||||
|
||||
$param{tag} = 'div' if !$param{tag};
|
||||
|
||||
my $id;
|
||||
|
||||
my $html = "<$param{tag}";
|
||||
|
||||
if (wantarray) {
|
||||
$comp_id_counter++;
|
||||
$id = "pveid$comp_id_counter";
|
||||
$html .= " id=$id";
|
||||
}
|
||||
|
||||
my $skip = {
|
||||
tag => 1,
|
||||
cn => 1,
|
||||
html => 1,
|
||||
text => 1,
|
||||
};
|
||||
|
||||
my $boolattr = {
|
||||
required => 1,
|
||||
autofocus => 1,
|
||||
};
|
||||
|
||||
my $noescape = {
|
||||
placeholder => 1,
|
||||
};
|
||||
|
||||
foreach my $attr (keys %param) {
|
||||
next if $skip->{$attr};
|
||||
my $v = $noescape->{$attr} ? $param{$attr} : uri_escape_utf8($param{$attr},"[^\/\ A-Za-z0-9\-\._~]");
|
||||
next if !defined($v);
|
||||
if ($boolattr->{$attr}) {
|
||||
$html .= " $attr" if $v;
|
||||
} else {
|
||||
$html .= " $attr=\"$v\"";
|
||||
}
|
||||
}
|
||||
|
||||
$html .= ">";
|
||||
|
||||
|
||||
if (my $cn = $param{cn}) {
|
||||
if(ref($cn) eq 'ARRAY'){
|
||||
foreach my $rec (@$cn) {
|
||||
$html .= $self->el(%$rec);
|
||||
}
|
||||
} else {
|
||||
$html .= $self->el(%$cn);
|
||||
}
|
||||
} elsif ($param{html}) {
|
||||
$html .= $param{html};
|
||||
} elsif ($param{text}) {
|
||||
$html .= encode_entities($param{text});
|
||||
}
|
||||
|
||||
$html .= "</$param{tag}>";
|
||||
|
||||
return wantarray ? ($html, $id) : $html;
|
||||
}
|
||||
|
||||
sub alert {
|
||||
my ($self, %param) = @_;
|
||||
|
||||
return $self->el(class => "alert alert-danger", %param);
|
||||
}
|
||||
|
||||
sub add_js {
|
||||
my ($self, $js) = @_;
|
||||
|
||||
$self->{js} .= $js . "\n";
|
||||
}
|
||||
|
||||
my $format_event_callback = sub {
|
||||
my ($info) = @_;
|
||||
|
||||
my $pstr = encode_json($info->{param});
|
||||
return "function(e){$info->{fn}.apply(e, $pstr);}";
|
||||
};
|
||||
|
||||
sub button {
|
||||
my ($self, %param) = @_;
|
||||
|
||||
$param{tag} = 'button';
|
||||
$param{class} = "btn btn-default btn-xs";
|
||||
|
||||
if (my $click = delete $param{click}) {
|
||||
my ($html, $id) = $self->el(%param);
|
||||
my $cb = &$format_event_callback($click);
|
||||
$self->add_js("jQuery('#$id').on('click', $cb);");
|
||||
return $html;
|
||||
} else {
|
||||
return $self->el(%param);
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
288
PVE/APIServer/Formatter/HTML.pm
Normal file
288
PVE/APIServer/Formatter/HTML.pm
Normal file
@ -0,0 +1,288 @@
|
||||
package PVE::APIServer::Formatter::HTML;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use PVE::APIServer::Formatter;
|
||||
use HTTP::Status;
|
||||
use JSON;
|
||||
use HTML::Entities;
|
||||
use PVE::JSONSchema;
|
||||
use PVE::APIServer::Formatter::Bootstrap;
|
||||
use PVE::APIServer::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::APIServer::Formatter::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::APIServer::Formatter::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};
|
||||
|
||||
$html .= $doc->el(tag => 'h3', text => 'Description');
|
||||
$html .= $doc->el(tag => 'p', text => $info->{description});
|
||||
|
||||
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})) {
|
||||
my $tv = to_json($elem, {pretty => 1, allow_nonref => 1, canonical => 1});
|
||||
|
||||
push @$items, {
|
||||
tag => 'a',
|
||||
class => 'list-group-item',
|
||||
href => "$path/$value",
|
||||
cn => [
|
||||
{
|
||||
tag => 'h4',
|
||||
class => 'list-group-item-heading',
|
||||
text => $value,
|
||||
},
|
||||
{
|
||||
tag => 'pre',
|
||||
class => 'list-group-item',
|
||||
text => $tv,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
$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::RESTHandler->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::RESTHandler->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::APIServer::Formatter::create_auth_cookie(
|
||||
$data->{ticket}, $auth->{cookie_name});
|
||||
|
||||
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::APIServer::Formatter::Bootstrap->new($res, $path);
|
||||
|
||||
my $html = &$login_form($doc);
|
||||
|
||||
my $raw = render_page($doc, $html);
|
||||
return ($raw, $portal_ct);
|
||||
});
|
||||
|
||||
1;
|
141
PVE/APIServer/Formatter/Standard.pm
Normal file
141
PVE/APIServer/Formatter/Standard.pm
Normal file
@ -0,0 +1,141 @@
|
||||
package PVE::APIServer::Formatter::Standard;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use PVE::APIServer::Formatter;
|
||||
use HTTP::Status;
|
||||
use JSON;
|
||||
use HTML::Entities;
|
||||
use PVE::JSONSchema;
|
||||
|
||||
# register result formatters
|
||||
|
||||
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 (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::APIServer::Formatter::register_formatter('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::APIServer::Formatter::register_formatter('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::APIServer::Formatter::register_formatter('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::APIServer::Formatter::register_formatter('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::APIServer::Formatter::register_formatter('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);
|
||||
});
|
2
debian/control
vendored
2
debian/control
vendored
@ -8,6 +8,6 @@ Homepage: http://www.proxmox.com
|
||||
|
||||
Package: libpve-http-server-perl
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, libnet-ip-perl, libio-socket-ssl-perl, libjson-perl, libcrypt-ssleay-perl, libhttp-date-perl, libhttp-message-perl, liburi-perl, libanyevent-perl, libanyevent-http-perl, libpve-common-perl
|
||||
Depends: ${misc:Depends}, libnet-ip-perl, libio-socket-ssl-perl, libjson-perl, libcrypt-ssleay-perl, libhttp-date-perl, libhttp-message-perl, liburi-perl, libhtml-parser-perl, libanyevent-perl, libanyevent-http-perl, libpve-common-perl
|
||||
Description: Proxmox Asynchrounous HTTP Server Implementation
|
||||
This is used to implement the PVE REST API.
|
Loading…
Reference in New Issue
Block a user