mirror of
https://git.proxmox.com/git/pve-http-server
synced 2025-05-03 10:33:34 +00:00
553 lines
12 KiB
Perl
Executable File
553 lines
12 KiB
Perl
Executable File
#!/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();
|