3 # This demo requires some other packages: novnc-pve and
4 # pve-manager (for PVE::NoVncIndex)
7 # First, we need some helpers to create authentication Tickets
17 use Crypt
::OpenSSL
::RSA
;
19 my $min_ticket_lifetime = -60*5; # allow 5 minutes time drift
20 my $max_ticket_lifetime = 60*60*2; # 2 hours
22 my $rsa = Crypt
::OpenSSL
::RSA-
>generate_key(2048);
27 return PVE
::Ticket
::assemble_rsa_ticket
($rsa, 'DEMO', $username);
31 my ($ticket, $noerr) = @_;
33 return PVE
::Ticket
::verify_rsa_ticket
(
34 $rsa, 'DEMO', $ticket, undef,
35 $min_ticket_lifetime, $max_ticket_lifetime, $noerr);
39 # - they do not contain the username in plain text
40 # - they are restricted to a specific resource path (example: '/vms/100')
41 sub assemble_vnc_ticket
{
42 my ($username, $path) = @_;
44 my $secret_data = "$username:$path";
46 return PVE
::Ticket
::assemble_rsa_ticket
(
47 $rsa, 'DEMOVNC', undef, $secret_data);
50 sub verify_vnc_ticket
{
51 my ($ticket, $username, $path, $noerr) = @_;
53 my $secret_data = "$username:$path";
55 return PVE
::Ticket
::verify_rsa_ticket
(
56 $rsa, 'DEMOVNC', $ticket, $secret_data, -20, 40, $noerr);
59 # We stack several PVE::RESTHandler classes to create
60 # the API for the novnc-pve console.
68 use PVE
::JSONSchema
qw(get_standard_option);
69 use PVE
::RESTEnvironment
;
72 use base
qw(PVE::RESTHandler);
74 __PACKAGE__-
>register_method ({
78 permissions
=> { user
=> 'all' },
79 description
=> "Node index.",
81 additionalProperties
=> 0,
83 node
=> get_standard_option
('pve-node'),
92 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
98 { name
=> 'vncshell' },
104 __PACKAGE__-
>register_method ({
108 description
=> "Creates a VNC Shell proxy.",
110 additionalProperties
=> 0,
112 node
=> get_standard_option
('pve-node'),
116 description
=> "use websocket instead of standard vnc.",
122 additionalProperties
=> 0,
124 user
=> { type
=> 'string' },
125 ticket
=> { type
=> 'string' },
126 port
=> { type
=> 'integer' },
127 upid
=> { type
=> 'string' },
133 my $node = $param->{node
};
135 # we only implement the websocket based VNC here
136 my $websocket = $param->{websocket
} // 1;
137 die "standard VNC not implemented" if !$websocket;
139 my $authpath = "/nodes/$node";
141 my $restenv = PVE
::RESTEnvironment-
>get();
142 my $user = $restenv->get_user();
144 my $ticket = Ticket
::assemble_vnc_ticket
($user, $authpath);
146 my $family = PVE
::Tools
::get_host_address_family
($node);
147 my $port = PVE
::Tools
::next_vnc_port
($family);
149 my $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
150 '-timeout', 10, '-notls', '-listen', 'localhost',
151 '-c', '/usr/bin/top'];
156 syslog
('info', "starting vnc proxy $upid\n");
158 my $cmdstr = join (' ', @$cmd);
159 syslog
('info', "launch command: $cmdstr");
162 foreach my $k (keys %ENV) {
163 next if $k eq 'PATH' || $k eq 'TERM' || $k eq 'USER' || $k eq 'HOME';
168 $ENV{PVE_VNC_TICKET
} = $ticket; # pass ticket to vncterm
170 PVE
::Tools
::run_command
($cmd, errmsg
=> "vncterm failed");
179 my $upid = $restenv->fork_worker('vncshell', "", $user, $realcmd);
181 PVE
::Tools
::wait_for_vnc_port
($port);
191 __PACKAGE__-
>register_method({
192 name
=> 'vncwebsocket',
193 path
=> 'vncwebsocket',
195 description
=> "Opens a weksocket for VNC traffic.",
197 additionalProperties
=> 0,
199 node
=> get_standard_option
('pve-node'),
201 description
=> "Ticket from previous call to vncproxy.",
206 description
=> "Port number returned by previous vncproxy call.",
216 port
=> { type
=> 'string' },
222 my $authpath = "/nodes/$param->{node}";
224 my $restenv = PVE
::RESTEnvironment-
>get();
225 my $user = $restenv->get_user();
227 Ticket
::verify_vnc_ticket
($param->{vncticket
}, $user, $authpath);
229 my $port = $param->{port
};
231 return { port
=> $port };
240 use PVE
::RESTHandler
;
241 use PVE
::JSONSchema
qw(get_standard_option);
243 use base
qw(PVE::RESTHandler);
245 __PACKAGE__-
>register_method ({
246 subclass
=> "NodeInfoAPI",
250 __PACKAGE__-
>register_method ({
254 permissions
=> { user
=> 'all' },
255 description
=> "Cluster node index.",
257 additionalProperties
=> 0,
266 links
=> [ { rel
=> 'child', href
=> "{node}" } ],
284 use PVE
::RESTHandler
;
287 use base
qw(PVE::RESTHandler);
289 __PACKAGE__-
>register_method ({
290 subclass
=> "NodeAPI",
294 __PACKAGE__-
>register_method ({
298 permissions
=> { user
=> 'all' },
299 description
=> "Directory index.",
301 additionalProperties
=> 0,
309 subdir
=> { type
=> 'string' },
312 links
=> [ { rel
=> 'child', href
=> "{subdir}" } ],
315 my ($resp, $param) = @_;
317 my $res = [ { subdir
=> 'nodes' } ];
323 # This is the REST/HTTPS Server
328 use HTTP
::Status
qw(:constants);
331 use PVE
::APIServer
::AnyEvent
;
332 use PVE
::Exception
qw(raise_param_exc);
333 use PVE
::RESTEnvironment
;
335 use base
('PVE::APIServer::AnyEvent');
338 my ($this, %args) = @_;
340 my $class = ref($this) || $this;
342 my $self = $class->SUPER::new
(%args);
344 PVE
::RESTEnvironment-
>init('pub');
350 my ($self, $method, $rel_uri, $ticket, $token, $peer_host) = @_;
352 my $restenv = PVE
::RESTEnvironment
::get
();
353 $restenv->set_user(undef);
355 # explicitly allow some calls without authentication
356 if ($rel_uri eq '/access/ticket' &&
357 ($method eq 'POST' || $method eq 'GET')) {
358 return; # allow call to create ticket
361 my $userid = Ticket
::verify_ticket
($ticket);
362 $restenv->set_user($userid);
371 my ($self, $clientip, $method, $rel_uri, $auth, $params) = @_;
374 status
=> HTTP_NOT_IMPLEMENTED
,
375 message
=> "Method '$method $rel_uri' not implemented",
378 if ($rel_uri eq '/access/ticket') {
379 if ($method eq 'POST') {
380 if ($params->{username
} && $params->{username
} eq 'demo' &&
381 $params->{password
} && $params->{password
} eq 'demo') {
385 ticket
=> Ticket
::create_ticket
($params->{username
}),
390 } elsif ($method eq 'GET') {
391 # this is allowed to display the login form
392 return { status
=> HTTP_OK
, data
=> {} };
398 my ($handler, $info);
402 ($handler, $info) = YourAPI-
>find_handler($method, $rel_uri, $uri_param);
403 return if !$handler || !$info;
405 foreach my $p (keys %{$params}) {
406 if (defined($uri_param->{$p})) {
407 raise_param_exc
({$p => "duplicate parameter (already defined in URI)"});
409 $uri_param->{$p} = $params->{$p};
413 data
=> $handler->handle($info, $uri_param),
414 info
=> $info, # useful to format output
419 $resp = { info
=> $info };
420 if (ref($err) eq "PVE::Exception") {
421 $resp->{status
} = $err->{code
} || HTTP_INTERNAL_SERVER_ERROR
;
422 $resp->{errors
} = $err->{errors
} if $err->{errors
};
423 $resp->{message
} = $err->{msg
};
425 $resp->{status
} = HTTP_INTERNAL_SERVER_ERROR
;
426 $resp->{message
} = $err;
434 # The main package creates the socket and runs the server
440 use Socket
qw(IPPROTO_TCP TCP_NODELAY SOMAXCONN);
446 use PVE
::Tools
qw(run_command);
448 use PVE
::APIServer
::Formatter
::Standard
;
449 use PVE
::APIServer
::Formatter
::HTML
;
452 my $nodename = PVE
::INotify
::nodename
();
455 my $cert_file = "simple-demo.pem";
457 if (! -f
$cert_file) {
458 print "generating demo server certificate\n";
459 my $cmd = ['openssl', 'req', '-batch', '-x509', '-newkey', 'rsa:4096',
460 '-nodes', '-keyout', $cert_file, '-out', $cert_file,
461 '-subj', "/CN=$nodename/",
466 my $socket = IO
::Socket
::IP-
>new(
467 LocalAddr
=> $nodename,
471 GetAddrInfoFlags
=> 0,
473 die "unable to create socket - $@\n";
475 # we often observe delays when using Nagle algorithm,
476 # so we disable that to maximize performance
477 setsockopt($socket, IPPROTO_TCP
, TCP_NODELAY
, 1);
479 my $accept_lock_fn = "simple-demo.lck";
480 my $lockfh = IO
::File-
>new(">>${accept_lock_fn}") ||
481 die "unable to open lock file '${accept_lock_fn}' - $!\n";
484 PVE
::APIServer
::AnyEvent
::add_dirs
(
485 $dirs, '/novnc/' => '/usr/share/novnc-pve/');
487 my $server = DemoServer-
>new(
490 lockfile
=> $accept_lock_fn,
492 title
=> 'Simple Demo API',
493 cookie_name
=> 'DEMO',
495 tls_ctx
=> { verify
=> 0, cert_file
=> $cert_file },
498 '/' => sub { get_index
($nodename, @_) },
502 # NOTE: Requests to non-API pages are not authenticated
503 # so you must be very careful here
505 my $root_page = <<__EOD__;
509 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
510 <meta http-equiv="X-UA-Compatible" content="IE=edge">
511 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
512 <title>Simple Demo Server</title>
515 <h1>Simple Demo Server ($nodename)</h1>
517 <p>You can browse the API <a href='/api2/html' >here</a>. Please sign
518 in with usrename <b>demo</b> and passwort <b>demo</b>.</p>
520 <p>Server console is here: <a href="?console=shell&novnc=1&node=$nodename">Console</a>
527 my ($nodename, $server, $r, $args) = @_;
531 my ($ticket, $userid);
532 if (my $cookie = $r->header('Cookie')) {
533 #$ticket = PVE::APIServer::Formatter::extract_auth_cookie($cookie, $server->{cookie_name});
534 # $userid = Ticket::verify_ticket($ticket, 1);
537 my $page = $root_page;
539 if (defined($args->{console
}) && $args->{novnc
}) {
540 $page = PVE
::NoVncIndex
::get_index
('en', $userid, $token,
541 $args->{console
}, $nodename);
544 my $headers = HTTP
::Headers-
>new(Content_Type
=> "text/html; charset=utf-8");
545 my $resp = HTTP
::Response-
>new(200, "OK", $headers, $page);
550 print "demo server listens at: https://$nodename:$port/\n";