]> git.proxmox.com Git - pve-http-server.git/commitdiff
add a more complex demo
authorDietmar Maurer <dietmar@proxmox.com>
Sat, 21 Jan 2017 15:08:36 +0000 (16:08 +0100)
committerDietmar Maurer <dietmar@proxmox.com>
Sat, 21 Jan 2017 15:08:36 +0000 (16:08 +0100)
examples/console-demo.pl [new file with mode: 0755]

diff --git a/examples/console-demo.pl b/examples/console-demo.pl
new file mode 100755 (executable)
index 0000000..b47d2f3
--- /dev/null
@@ -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__;
+<!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();