]> git.proxmox.com Git - pve-http-server.git/blame - examples/console-demo.pl
whitespace fixup
[pve-http-server.git] / examples / console-demo.pl
CommitLineData
038808dd
DM
1#!/usr/bin/perl
2
3# This demo requires some other packages: novnc-pve and
4# pve-manager (for PVE::NoVncIndex)
5
6
7# First, we need some helpers to create authentication Tickets
8
9package Ticket;
10
11use strict;
12use warnings;
13use Net::SSLeay;
14
15use PVE::Ticket;
16
17use Crypt::OpenSSL::RSA;
18
19my $min_ticket_lifetime = -60*5; # allow 5 minutes time drift
20my $max_ticket_lifetime = 60*60*2; # 2 hours
21
22my $rsa = Crypt::OpenSSL::RSA->generate_key(2048);
23
24sub create_ticket {
25 my ($username) = @_;
26
27 return PVE::Ticket::assemble_rsa_ticket($rsa, 'DEMO', $username);
28}
29
30sub verify_ticket {
31 my ($ticket, $noerr) = @_;
32
33 return PVE::Ticket::verify_rsa_ticket(
34 $rsa, 'DEMO', $ticket, undef,
35 $min_ticket_lifetime, $max_ticket_lifetime, $noerr);
36}
37
38# VNC tickets
39# - they do not contain the username in plain text
40# - they are restricted to a specific resource path (example: '/vms/100')
41sub assemble_vnc_ticket {
42 my ($username, $path) = @_;
43
44 my $secret_data = "$username:$path";
45
46 return PVE::Ticket::assemble_rsa_ticket(
47 $rsa, 'DEMOVNC', undef, $secret_data);
48}
49
50sub verify_vnc_ticket {
51 my ($ticket, $username, $path, $noerr) = @_;
52
53 my $secret_data = "$username:$path";
54
55 return PVE::Ticket::verify_rsa_ticket(
56 $rsa, 'DEMOVNC', $ticket, $secret_data, -20, 40, $noerr);
57}
58
59# We stack several PVE::RESTHandler classes to create
60# the API for the novnc-pve console.
61
62package NodeInfoAPI;
63
64use strict;
65use warnings;
66
67use PVE::RESTHandler;
68use PVE::JSONSchema qw(get_standard_option);
69use PVE::RESTEnvironment;
70use PVE::SafeSyslog;
71
72use base qw(PVE::RESTHandler);
73
74__PACKAGE__->register_method ({
75 name => 'index',
76 path => '',
77 method => 'GET',
78 permissions => { user => 'all' },
79 description => "Node index.",
80 parameters => {
81 additionalProperties => 0,
82 properties => {
83 node => get_standard_option('pve-node'),
84 },
85 },
86 returns => {
87 type => 'array',
88 items => {
89 type => "object",
90 properties => {},
91 },
92 links => [ { rel => 'child', href => "{name}" } ],
93 },
94 code => sub {
95 my ($param) = @_;
96
97 my $result = [
98 { name => 'vncshell' },
99 ];
100
101 return $result;
102 }});
103
104__PACKAGE__->register_method ({
105 name => 'vncshell',
106 path => 'vncshell',
107 method => 'POST',
108 description => "Creates a VNC Shell proxy.",
109 parameters => {
110 additionalProperties => 0,
111 properties => {
112 node => get_standard_option('pve-node'),
113 websocket => {
114 optional => 1,
115 type => 'boolean',
116 description => "use websocket instead of standard vnc.",
117 default => 1,
118 },
119 },
120 },
121 returns => {
122 additionalProperties => 0,
123 properties => {
124 user => { type => 'string' },
125 ticket => { type => 'string' },
126 port => { type => 'integer' },
127 upid => { type => 'string' },
128 },
129 },
130 code => sub {
131 my ($param) = @_;
132
133 my $node = $param->{node};
134
135 # we only implement the websocket based VNC here
136 my $websocket = $param->{websocket} // 1;
137 die "standard VNC not implemented" if !$websocket;
138
139 my $authpath = "/nodes/$node";
140
141 my $restenv = PVE::RESTEnvironment->get();
142 my $user = $restenv->get_user();
143
144 my $ticket = Ticket::assemble_vnc_ticket($user, $authpath);
145
146 my $family = PVE::Tools::get_host_address_family($node);
147 my $port = PVE::Tools::next_vnc_port($family);
148
149 my $cmd = ['/usr/bin/vncterm', '-rfbport', $port,
150 '-timeout', 10, '-notls', '-listen', 'localhost',
151 '-c', '/usr/bin/top'];
152
153 my $realcmd = sub {
154 my $upid = shift;
155
156 syslog ('info', "starting vnc proxy $upid\n");
157
158 my $cmdstr = join (' ', @$cmd);
159 syslog ('info', "launch command: $cmdstr");
160
161 eval {
162 foreach my $k (keys %ENV) {
163 next if $k eq 'PATH' || $k eq 'TERM' || $k eq 'USER' || $k eq 'HOME';
164 delete $ENV{$k};
165 }
166 $ENV{PWD} = '/';
167
168 $ENV{PVE_VNC_TICKET} = $ticket; # pass ticket to vncterm
169
170 PVE::Tools::run_command($cmd, errmsg => "vncterm failed");
171 };
172 if (my $err = $@) {
173 syslog('err', $err);
174 }
175
176 return;
177 };
178
179 my $upid = $restenv->fork_worker('vncshell', "", $user, $realcmd);
180
181 PVE::Tools::wait_for_vnc_port($port);
182
183 return {
184 user => $user,
185 ticket => $ticket,
186 port => $port,
187 upid => $upid,
188 };
189 }});
190
191__PACKAGE__->register_method({
192 name => 'vncwebsocket',
193 path => 'vncwebsocket',
194 method => 'GET',
195 description => "Opens a weksocket for VNC traffic.",
196 parameters => {
197 additionalProperties => 0,
198 properties => {
199 node => get_standard_option('pve-node'),
200 vncticket => {
201 description => "Ticket from previous call to vncproxy.",
202 type => 'string',
203 maxLength => 512,
204 },
205 port => {
206 description => "Port number returned by previous vncproxy call.",
207 type => 'integer',
208 minimum => 5900,
209 maximum => 5999,
210 },
211 },
212 },
213 returns => {
214 type => "object",
215 properties => {
216 port => { type => 'string' },
217 },
218 },
219 code => sub {
220 my ($param) = @_;
221
222 my $authpath = "/nodes/$param->{node}";
223
224 my $restenv = PVE::RESTEnvironment->get();
225 my $user = $restenv->get_user();
226
227 Ticket::verify_vnc_ticket($param->{vncticket}, $user, $authpath);
228
229 my $port = $param->{port};
230
231 return { port => $port };
232 }});
233
234
235package NodeAPI;
236
237use strict;
238use warnings;
239
240use PVE::RESTHandler;
241use PVE::JSONSchema qw(get_standard_option);
242
243use base qw(PVE::RESTHandler);
244
245__PACKAGE__->register_method ({
246 subclass => "NodeInfoAPI",
247 path => '{node}',
248});
249
250__PACKAGE__->register_method ({
251 name => 'index',
252 path => '',
253 method => 'GET',
254 permissions => { user => 'all' },
255 description => "Cluster node index.",
256 parameters => {
257 additionalProperties => 0,
258 properties => {},
259 },
260 returns => {
261 type => 'array',
262 items => {
263 type => "object",
264 properties => {},
265 },
266 links => [ { rel => 'child', href => "{node}" } ],
267 },
268 code => sub {
269 my ($param) = @_;
270
271 my $res = [
272 { node => 'elsa' },
273 ];
274
275 return $res;
276 }});
277
278
279package YourAPI;
280
281use strict;
282use warnings;
283
284use PVE::RESTHandler;
285use PVE::JSONSchema;
286
287use base qw(PVE::RESTHandler);
288
289__PACKAGE__->register_method ({
290 subclass => "NodeAPI",
291 path => 'nodes',
292});
293
294__PACKAGE__->register_method ({
295 name => 'index',
296 path => '',
297 method => 'GET',
298 permissions => { user => 'all' },
299 description => "Directory index.",
300 parameters => {
301 additionalProperties => 0,
302 properties => {},
303 },
304 returns => {
305 type => 'array',
306 items => {
307 type => "object",
308 properties => {
309 subdir => { type => 'string' },
310 },
311 },
312 links => [ { rel => 'child', href => "{subdir}" } ],
313 },
314 code => sub {
315 my ($resp, $param) = @_;
316
317 my $res = [ { subdir => 'nodes' } ];
318
319 return $res;
320 }});
321
322
323# This is the REST/HTTPS Server
324package DemoServer;
325
326use strict;
327use warnings;
328use HTTP::Status qw(:constants);
329use URI::Escape;
330
331use PVE::APIServer::AnyEvent;
332use PVE::Exception qw(raise_param_exc);
333use PVE::RESTEnvironment;
334
335use base('PVE::APIServer::AnyEvent');
336
337sub new {
338 my ($this, %args) = @_;
339
340 my $class = ref($this) || $this;
341
342 my $self = $class->SUPER::new(%args);
343
344 PVE::RESTEnvironment->init('pub');
345
346 return $self;
347}
348
349sub auth_handler {
350 my ($self, $method, $rel_uri, $ticket, $token, $peer_host) = @_;
351
352 my $restenv = PVE::RESTEnvironment::get();
353 $restenv->set_user(undef);
354
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
359 }
360
361 my $userid = Ticket::verify_ticket($ticket);
362 $restenv->set_user($userid);
363
364 return {
365 ticket => $ticket,
366 userid => $userid,
367 };
368}
369
370sub rest_handler {
371 my ($self, $clientip, $method, $rel_uri, $auth, $params) = @_;
372
373 my $resp = {
374 status => HTTP_NOT_IMPLEMENTED,
375 message => "Method '$method $rel_uri' not implemented",
376 };
377
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') {
382 return {
383 status => HTTP_OK,
384 data => {
385 ticket => Ticket::create_ticket($params->{username}),
386 },
387 };
388 }
389 return $resp;
390 } elsif ($method eq 'GET') {
391 # this is allowed to display the login form
392 return { status => HTTP_OK, data => {} };
393 } else {
394 return $resp;
395 }
396 }
397
398 my ($handler, $info);
399
400 eval {
401 my $uri_param = {};
402 ($handler, $info) = YourAPI->find_handler($method, $rel_uri, $uri_param);
403 return if !$handler || !$info;
404
405 foreach my $p (keys %{$params}) {
406 if (defined($uri_param->{$p})) {
407 raise_param_exc({$p => "duplicate parameter (already defined in URI)"});
408 }
409 $uri_param->{$p} = $params->{$p};
410 }
411
412 $resp = {
413 data => $handler->handle($info, $uri_param),
414 info => $info, # useful to format output
415 status => HTTP_OK,
416 };
417 };
418 if (my $err = $@) {
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};
424 } else {
425 $resp->{status} = HTTP_INTERNAL_SERVER_ERROR;
426 $resp->{message} = $err;
427 }
428 }
429
430 return $resp;
431}
432
433
434# The main package creates the socket and runs the server
435package main;
436
437use strict;
438use warnings;
439
440use Socket qw(IPPROTO_TCP TCP_NODELAY SOMAXCONN);
441use IO::Socket::IP;
442use HTTP::Headers;
443use HTTP::Response;
444use Data::Dumper;
445
446use PVE::Tools qw(run_command);
447use PVE::INotify;
448use PVE::APIServer::Formatter::Standard;
449use PVE::APIServer::Formatter::HTML;
450use PVE::NoVncIndex;
451
452my $nodename = PVE::INotify::nodename();
453my $port = 9999;
454
455my $cert_file = "simple-demo.pem";
456
457if (! -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/",
462 '-days', '3650'];
463 run_command($cmd);
464}
465
466my $socket = IO::Socket::IP->new(
467 LocalAddr => $nodename,
468 LocalPort => $port,
469 Listen => SOMAXCONN,
470 Proto => 'tcp',
471 GetAddrInfoFlags => 0,
472 ReuseAddr => 1) ||
473 die "unable to create socket - $@\n";
474
475# we often observe delays when using Nagle algorithm,
476# so we disable that to maximize performance
477setsockopt($socket, IPPROTO_TCP, TCP_NODELAY, 1);
478
479my $accept_lock_fn = "simple-demo.lck";
480my $lockfh = IO::File->new(">>${accept_lock_fn}") ||
481 die "unable to open lock file '${accept_lock_fn}' - $!\n";
482
483my $dirs = {};
484PVE::APIServer::AnyEvent::add_dirs(
485 $dirs, '/novnc/' => '/usr/share/novnc-pve/');
486
487my $server = DemoServer->new(
488 debug => 1,
489 socket => $socket,
490 lockfile => $accept_lock_fn,
491 lockfh => $lockfh,
492 title => 'Simple Demo API',
493 cookie_name => 'DEMO',
494 logfh => \*STDOUT,
495 tls_ctx => { verify => 0, cert_file => $cert_file },
496 dirs => $dirs,
497 pages => {
498 '/' => sub { get_index($nodename, @_) },
499 },
500);
501
502# NOTE: Requests to non-API pages are not authenticated
503# so you must be very careful here
504
505my $root_page = <<__EOD__;
506<!DOCTYPE html>
507<html>
508 <head>
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>
513 </head>
514 <body>
515 <h1>Simple Demo Server ($nodename)</h1>
516
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>
519
520 <p>Server console is here: <a href="?console=shell&novnc=1&node=$nodename">Console</a>
521
522 </body>
523</html>
524__EOD__
525
526sub get_index {
527 my ($nodename, $server, $r, $args) = @_;
528
529 my $token = '';
530
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);
535 }
536
537 my $page = $root_page;
538
539 if (defined($args->{console}) && $args->{novnc}) {
540 $page = PVE::NoVncIndex::get_index('en', $userid, $token,
541 $args->{console}, $nodename);
542 }
543
544 my $headers = HTTP::Headers->new(Content_Type => "text/html; charset=utf-8");
545 my $resp = HTTP::Response->new(200, "OK", $headers, $page);
546
547 return $resp;
548}
549
550print "demo server listens at: https://$nodename:$port/\n";
551
552$server->run();