From 038808dd925c384bf91f49a07fb855d2dca2f89a Mon Sep 17 00:00:00 2001 From: Dietmar Maurer Date: Sat, 21 Jan 2017 16:08:36 +0100 Subject: [PATCH] add a more complex demo --- examples/console-demo.pl | 552 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 552 insertions(+) create mode 100755 examples/console-demo.pl diff --git a/examples/console-demo.pl b/examples/console-demo.pl new file mode 100755 index 0000000..b47d2f3 --- /dev/null +++ b/examples/console-demo.pl @@ -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__; + + + + + + + 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();