#!/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__; Simple Demo Server

Simple Demo Server ($nodename)

You can browse the API here. Please sign in with usrename demo and passwort demo.

Server console is here: Console __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();