5
0
mirror of git://git.proxmox.com/git/pve-http-server.git synced 2025-01-21 18:03:52 +03:00

add a more complex demo

This commit is contained in:
Dietmar Maurer 2017-01-21 16:08:36 +01:00
parent fc75d78694
commit 038808dd92

552
examples/console-demo.pl Executable file
View File

@ -0,0 +1,552 @@
#!/usr/bin/perl
# This demo requires some other packages: novnc-pve and
# pve-manager (for PVE::NoVncIndex)
# First, we need some helpers to create authentication Tickets
package Ticket;
use strict;
use warnings;
use Net::SSLeay;
use PVE::Ticket;
use Crypt::OpenSSL::RSA;
my $min_ticket_lifetime = -60*5; # allow 5 minutes time drift
my $max_ticket_lifetime = 60*60*2; # 2 hours
my $rsa = Crypt::OpenSSL::RSA->generate_key(2048);
sub create_ticket {
my ($username) = @_;
return PVE::Ticket::assemble_rsa_ticket($rsa, 'DEMO', $username);
}
sub verify_ticket {
my ($ticket, $noerr) = @_;
return PVE::Ticket::verify_rsa_ticket(
$rsa, 'DEMO', $ticket, undef,
$min_ticket_lifetime, $max_ticket_lifetime, $noerr);
}
# VNC tickets
# - they do not contain the username in plain text
# - they are restricted to a specific resource path (example: '/vms/100')
sub assemble_vnc_ticket {
my ($username, $path) = @_;
my $secret_data = "$username:$path";
return PVE::Ticket::assemble_rsa_ticket(
$rsa, 'DEMOVNC', undef, $secret_data);
}
sub verify_vnc_ticket {
my ($ticket, $username, $path, $noerr) = @_;
my $secret_data = "$username:$path";
return PVE::Ticket::verify_rsa_ticket(
$rsa, 'DEMOVNC', $ticket, $secret_data, -20, 40, $noerr);
}
# We stack several PVE::RESTHandler classes to create
# the API for the novnc-pve console.
package NodeInfoAPI;
use strict;
use warnings;
use PVE::RESTHandler;
use PVE::JSONSchema qw(get_standard_option);
use PVE::RESTEnvironment;
use PVE::SafeSyslog;
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
name => 'index',
path => '',
method => 'GET',
permissions => { user => 'all' },
description => "Node index.",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
links => [ { rel => 'child', href => "{name}" } ],
},
code => sub {
my ($param) = @_;
my $result = [
{ name => 'vncshell' },
];
return $result;
}});
__PACKAGE__->register_method ({
name => 'vncshell',
path => 'vncshell',
method => 'POST',
description => "Creates a VNC Shell proxy.",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
websocket => {
optional => 1,
type => 'boolean',
description => "use websocket instead of standard vnc.",
default => 1,
},
},
},
returns => {
additionalProperties => 0,
properties => {
user => { type => 'string' },
ticket => { type => 'string' },
port => { type => 'integer' },
upid => { type => 'string' },
},
},
code => sub {
my ($param) = @_;
my $node = $param->{node};
# we only implement the websocket based VNC here
my $websocket = $param->{websocket} // 1;
die "standard VNC not implemented" if !$websocket;
my $authpath = "/nodes/$node";
my $restenv = PVE::RESTEnvironment->get();
my $user = $restenv->get_user();
my $ticket = Ticket::assemble_vnc_ticket($user, $authpath);
my $family = PVE::Tools::get_host_address_family($node);
my $port = PVE::Tools::next_vnc_port($family);
my $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
'-timeout', 10, '-notls', '-listen', 'localhost',
'-c', '/usr/bin/top'];
my $realcmd = sub {
my $upid = shift;
syslog ('info', "starting vnc proxy $upid\n");
my $cmdstr = join (' ', @$cmd);
syslog ('info', "launch command: $cmdstr");
eval {
foreach my $k (keys %ENV) {
next if $k eq 'PATH' || $k eq 'TERM' || $k eq 'USER' || $k eq 'HOME';
delete $ENV{$k};
}
$ENV{PWD} = '/';
$ENV{PVE_VNC_TICKET} = $ticket; # pass ticket to vncterm
PVE::Tools::run_command($cmd, errmsg => "vncterm failed");
};
if (my $err = $@) {
syslog('err', $err);
}
return;
};
my $upid = $restenv->fork_worker('vncshell', "", $user, $realcmd);
PVE::Tools::wait_for_vnc_port($port);
return {
user => $user,
ticket => $ticket,
port => $port,
upid => $upid,
};
}});
__PACKAGE__->register_method({
name => 'vncwebsocket',
path => 'vncwebsocket',
method => 'GET',
description => "Opens a weksocket for VNC traffic.",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
vncticket => {
description => "Ticket from previous call to vncproxy.",
type => 'string',
maxLength => 512,
},
port => {
description => "Port number returned by previous vncproxy call.",
type => 'integer',
minimum => 5900,
maximum => 5999,
},
},
},
returns => {
type => "object",
properties => {
port => { type => 'string' },
},
},
code => sub {
my ($param) = @_;
my $authpath = "/nodes/$param->{node}";
my $restenv = PVE::RESTEnvironment->get();
my $user = $restenv->get_user();
Ticket::verify_vnc_ticket($param->{vncticket}, $user, $authpath);
my $port = $param->{port};
return { port => $port };
}});
package NodeAPI;
use strict;
use warnings;
use PVE::RESTHandler;
use PVE::JSONSchema qw(get_standard_option);
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
subclass => "NodeInfoAPI",
path => '{node}',
});
__PACKAGE__->register_method ({
name => 'index',
path => '',
method => 'GET',
permissions => { user => 'all' },
description => "Cluster node index.",
parameters => {
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
links => [ { rel => 'child', href => "{node}" } ],
},
code => sub {
my ($param) = @_;
my $res = [
{ node => 'elsa' },
];
return $res;
}});
package YourAPI;
use strict;
use warnings;
use PVE::RESTHandler;
use PVE::JSONSchema;
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
subclass => "NodeAPI",
path => 'nodes',
});
__PACKAGE__->register_method ({
name => 'index',
path => '',
method => 'GET',
permissions => { user => 'all' },
description => "Directory index.",
parameters => {
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
subdir => { type => 'string' },
},
},
links => [ { rel => 'child', href => "{subdir}" } ],
},
code => sub {
my ($resp, $param) = @_;
my $res = [ { subdir => 'nodes' } ];
return $res;
}});
# This is the REST/HTTPS Server
package DemoServer;
use strict;
use warnings;
use HTTP::Status qw(:constants);
use URI::Escape;
use PVE::APIServer::AnyEvent;
use PVE::Exception qw(raise_param_exc);
use PVE::RESTEnvironment;
use base('PVE::APIServer::AnyEvent');
sub new {
my ($this, %args) = @_;
my $class = ref($this) || $this;
my $self = $class->SUPER::new(%args);
PVE::RESTEnvironment->init('pub');
return $self;
}
sub auth_handler {
my ($self, $method, $rel_uri, $ticket, $token, $peer_host) = @_;
my $restenv = PVE::RESTEnvironment::get();
$restenv->set_user(undef);
# explicitly allow some calls without authentication
if ($rel_uri eq '/access/ticket' &&
($method eq 'POST' || $method eq 'GET')) {
return; # allow call to create ticket
}
my $userid = Ticket::verify_ticket($ticket);
$restenv->set_user($userid);
return {
ticket => $ticket,
userid => $userid,
};
}
sub rest_handler {
my ($self, $clientip, $method, $rel_uri, $auth, $params) = @_;
my $resp = {
status => HTTP_NOT_IMPLEMENTED,
message => "Method '$method $rel_uri' not implemented",
};
if ($rel_uri eq '/access/ticket') {
if ($method eq 'POST') {
if ($params->{username} && $params->{username} eq 'demo' &&
$params->{password} && $params->{password} eq 'demo') {
return {
status => HTTP_OK,
data => {
ticket => Ticket::create_ticket($params->{username}),
},
};
}
return $resp;
} elsif ($method eq 'GET') {
# this is allowed to display the login form
return { status => HTTP_OK, data => {} };
} else {
return $resp;
}
}
my ($handler, $info);
eval {
my $uri_param = {};
($handler, $info) = YourAPI->find_handler($method, $rel_uri, $uri_param);
return if !$handler || !$info;
foreach my $p (keys %{$params}) {
if (defined($uri_param->{$p})) {
raise_param_exc({$p => "duplicate parameter (already defined in URI)"});
}
$uri_param->{$p} = $params->{$p};
}
$resp = {
data => $handler->handle($info, $uri_param),
info => $info, # useful to format output
status => HTTP_OK,
};
};
if (my $err = $@) {
$resp = { info => $info };
if (ref($err) eq "PVE::Exception") {
$resp->{status} = $err->{code} || HTTP_INTERNAL_SERVER_ERROR;
$resp->{errors} = $err->{errors} if $err->{errors};
$resp->{message} = $err->{msg};
} else {
$resp->{status} = HTTP_INTERNAL_SERVER_ERROR;
$resp->{message} = $err;
}
}
return $resp;
}
# The main package creates the socket and runs the server
package main;
use strict;
use warnings;
use Socket qw(IPPROTO_TCP TCP_NODELAY SOMAXCONN);
use IO::Socket::IP;
use HTTP::Headers;
use HTTP::Response;
use Data::Dumper;
use PVE::Tools qw(run_command);
use PVE::INotify;
use PVE::APIServer::Formatter::Standard;
use PVE::APIServer::Formatter::HTML;
use PVE::NoVncIndex;
my $nodename = PVE::INotify::nodename();
my $port = 9999;
my $cert_file = "simple-demo.pem";
if (! -f $cert_file) {
print "generating demo server certificate\n";
my $cmd = ['openssl', 'req', '-batch', '-x509', '-newkey', 'rsa:4096',
'-nodes', '-keyout', $cert_file, '-out', $cert_file,
'-subj', "/CN=$nodename/",
'-days', '3650'];
run_command($cmd);
}
my $socket = IO::Socket::IP->new(
LocalAddr => $nodename,
LocalPort => $port,
Listen => SOMAXCONN,
Proto => 'tcp',
GetAddrInfoFlags => 0,
ReuseAddr => 1) ||
die "unable to create socket - $@\n";
# we often observe delays when using Nagle algorithm,
# so we disable that to maximize performance
setsockopt($socket, IPPROTO_TCP, TCP_NODELAY, 1);
my $accept_lock_fn = "simple-demo.lck";
my $lockfh = IO::File->new(">>${accept_lock_fn}") ||
die "unable to open lock file '${accept_lock_fn}' - $!\n";
my $dirs = {};
PVE::APIServer::AnyEvent::add_dirs(
$dirs, '/novnc/' => '/usr/share/novnc-pve/');
my $server = DemoServer->new(
debug => 1,
socket => $socket,
lockfile => $accept_lock_fn,
lockfh => $lockfh,
title => 'Simple Demo API',
cookie_name => 'DEMO',
logfh => \*STDOUT,
tls_ctx => { verify => 0, cert_file => $cert_file },
dirs => $dirs,
pages => {
'/' => sub { get_index($nodename, @_) },
},
);
# NOTE: Requests to non-API pages are not authenticated
# so you must be very careful here
my $root_page = <<__EOD__;
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Simple Demo Server</title>
</head>
<body>
<h1>Simple Demo Server ($nodename)</h1>
<p>You can browse the API <a href='/api2/html' >here</a>. Please sign
in with usrename <b>demo</b> and passwort <b>demo</b>.</p>
<p>Server console is here: <a href="?console=shell&novnc=1&node=$nodename">Console</a>
</body>
</html>
__EOD__
sub get_index {
my ($nodename, $server, $r, $args) = @_;
my $token = '';
my ($ticket, $userid);
if (my $cookie = $r->header('Cookie')) {
#$ticket = PVE::APIServer::Formatter::extract_auth_cookie($cookie, $server->{cookie_name});
# $userid = Ticket::verify_ticket($ticket, 1);
}
my $page = $root_page;
if (defined($args->{console}) && $args->{novnc}) {
$page = PVE::NoVncIndex::get_index('en', $userid, $token,
$args->{console}, $nodename);
}
my $headers = HTTP::Headers->new(Content_Type => "text/html; charset=utf-8");
my $resp = HTTP::Response->new(200, "OK", $headers, $page);
return $resp;
}
print "demo server listens at: https://$nodename:$port/\n";
$server->run();