mirror of
https://git.proxmox.com/git/pve-http-server
synced 2025-05-02 16:20:13 +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:
|
||||||
install -d -m 755 ${PERL5DIR}/PVE/APIServer
|
install -d -m 755 ${PERL5DIR}/PVE/APIServer
|
||||||
install -m 0644 PVE/APIServer/AnyEvent.pm ${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
|
.PHONY: upload
|
||||||
upload: ${DEB}
|
upload: ${DEB}
|
||||||
|
@ -24,6 +24,7 @@ use Compress::Zlib;
|
|||||||
use PVE::SafeSyslog;
|
use PVE::SafeSyslog;
|
||||||
use PVE::INotify;
|
use PVE::INotify;
|
||||||
use PVE::Tools;
|
use PVE::Tools;
|
||||||
|
use PVE::APIServer::Formatter;
|
||||||
|
|
||||||
use Net::IP;
|
use Net::IP;
|
||||||
use URI;
|
use URI;
|
||||||
@ -39,7 +40,6 @@ my $limit_max_headers = 30;
|
|||||||
my $limit_max_header_size = 8*1024;
|
my $limit_max_header_size = 8*1024;
|
||||||
my $limit_max_post = 16*1024;
|
my $limit_max_post = 16*1024;
|
||||||
|
|
||||||
|
|
||||||
my $known_methods = {
|
my $known_methods = {
|
||||||
GET => 1,
|
GET => 1,
|
||||||
POST => 1,
|
POST => 1,
|
||||||
@ -56,57 +56,6 @@ my $split_abs_uri = sub {
|
|||||||
return wantarray ? ($rel_uri, $format) : $rel_uri;
|
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 {
|
sub log_request {
|
||||||
my ($self, $reqstate) = @_;
|
my ($self, $reqstate) = @_;
|
||||||
|
|
||||||
@ -143,28 +92,6 @@ sub log_aborted_request {
|
|||||||
$self->log_request($reqstate);
|
$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 {
|
sub cleanup_reqstate {
|
||||||
my ($reqstate) = @_;
|
my ($reqstate) = @_;
|
||||||
|
|
||||||
@ -606,7 +533,7 @@ sub proxy_request {
|
|||||||
PVEClientIP => $clientip,
|
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->{'CSRFPreventionToken'} = $token if $token;
|
||||||
$headers->{'Accept-Encoding'} = 'gzip' if $reqstate->{accept_gzip};
|
$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 ($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)) {
|
if (!defined($formatter)) {
|
||||||
$self->error($reqstate, HTTP_NOT_IMPLEMENTED, "no such uri $rel_uri, $format");
|
$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/) {
|
} elsif ($path =~ m/^\Q$base_uri\E/) {
|
||||||
my $token = $r->header('CSRFPreventionToken');
|
my $token = $r->header('CSRFPreventionToken');
|
||||||
my $cookie = $r->header('Cookie');
|
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});
|
my ($rel_uri, $format) = &$split_abs_uri($path, $self->{base_uri});
|
||||||
if (!$format) {
|
if (!$format) {
|
||||||
@ -1276,7 +1203,7 @@ sub unshift_read_header {
|
|||||||
if (my $err = $@) {
|
if (my $err = $@) {
|
||||||
# always delay unauthorized calls by 3 seconds
|
# always delay unauthorized calls by 3 seconds
|
||||||
my $delay = 3;
|
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 ($raw, $ct, $nocomp) = &$formatter($path, $auth);
|
||||||
my $resp;
|
my $resp;
|
||||||
if (ref($raw) && (ref($raw) eq 'HTTP::Response')) {
|
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
|
Package: libpve-http-server-perl
|
||||||
Architecture: all
|
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
|
Description: Proxmox Asynchrounous HTTP Server Implementation
|
||||||
This is used to implement the PVE REST API.
|
This is used to implement the PVE REST API.
|
Loading…
Reference in New Issue
Block a user