Unverified Commit 5fdd73d1 authored by Francesc Guasch's avatar Francesc Guasch Committed by GitHub
Browse files

Feature #249 tls (#972)

* test(TLS): check display info with TLS fields

issue #249

* feature(TLS): fetch stored vv file

issue #249

* wip(TLS): create and store vv file with TLS

Following the guidelines by @fv3rdugo on https://ravada.readthedocs.io/en/latest/docs/spice_tls.html

issue #249

* wip(test): check tls display file

issue #249

* feature(frontend): show the TLS display file

issue #249

* refactor(frontend): both TLS and old display file

issue #249

* refactor(test): wait longer for node to start

* refactor(test): check tls file

issue #249

* refactor(frontend): set tls_file if there is tls port

It failed for some tests with no TLS VMM

issue #249

* wip(frontend): properly show the tls vv file

issue #249

* feature(backend): secure connection support with TLS

With assistance from @fv3rdugo

issue #249

* feature(frontend): show secure vv file if available

issue #249

* fix(install): added dependencies for nodes support
parent f9bb9683
......@@ -4,7 +4,7 @@ Architecture: all
Section: utils
Priority: optional
Maintainer: Francesc Guasch <frankie@telecos.upc.edu>
Depends: perl (>=5.18),libmojolicious-perl,mysql-common,libauthen-passphrase-perl, libdatetime-perl, libdbd-mysql-perl,libdbi-perl,libdbix-connector-perl,libipc-run3-perl,libnet-ldap-perl,libproc-pid-file-perl,libvirt-bin,libvirt-daemon-system,libsys-virt-perl,libxml-libxml-perl,libconfig-yaml-perl,libmoose-perl,libjson-xs-perl,qemu-utils,perlmagick,libmoosex-types-netaddr-ip-perl,libio-interface-perl,libiptables-chainmgr-perl,libnet-dns-perl,wget,liblocale-maketext-lexicon-perl,libmojolicious-plugin-i18n-perl,libdbd-sqlite3-perl, debconf (>= 0.2.26), adduser, libdigest-sha-perl, qemu-kvm, net-tools, libparallel-forkmanager-perl, libfile-rsync-perl
Depends: perl (>=5.18),libmojolicious-perl,mysql-common,libauthen-passphrase-perl, libdatetime-perl, libdbd-mysql-perl,libdbi-perl,libdbix-connector-perl,libipc-run3-perl,libnet-ldap-perl,libproc-pid-file-perl,libvirt-bin,libvirt-daemon-system,libsys-virt-perl,libxml-libxml-perl,libconfig-yaml-perl,libmoose-perl,libjson-xs-perl,qemu-utils,perlmagick,libmoosex-types-netaddr-ip-perl,libio-interface-perl,libiptables-chainmgr-perl,libnet-dns-perl,wget,liblocale-maketext-lexicon-perl,libmojolicious-plugin-i18n-perl,libdbd-sqlite3-perl, debconf (>= 0.2.26), adduser, libdigest-sha-perl, qemu-kvm, net-tools, libparallel-forkmanager-perl, libfile-rsync-perl, libdate-calc-perl, libnet-ssh2-perl
Description: Remote Virtual Desktops Manager
Ravada is a software that allows the user to connect to a
remote virtual desktop.
......@@ -1168,6 +1168,7 @@ sub _upgrade_tables {
$self->_upgrade_table('domains','status','varchar(32) DEFAULT "shutdown"');
$self->_upgrade_table('domains','display','varchar(128) DEFAULT NULL');
$self->_upgrade_table('domains','display_file','text DEFAULT NULL');
$self->_upgrade_table('domains','info','varchar(255) DEFAULT NULL');
$self->_upgrade_table('domains','internal_id','varchar(64) DEFAULT NULL');
$self->_upgrade_table('domains','id_vm','int default null');
......@@ -2759,6 +2760,7 @@ sub _cmd_refresh_machine($self, $request) {
my $id_domain = $request->args('id_domain');
my $domain = Ravada::Domain->open($id_domain);
$domain->get_info();
$domain->info(Ravada::Utils::user_daemon);
}
......
......@@ -40,7 +40,7 @@ _init_connector();
requires 'name';
requires 'remove';
requires 'display';
requires 'display_info';
requires 'is_active';
requires 'is_hibernated';
......@@ -120,12 +120,6 @@ has '_vm' => (
,required => 0
);
has 'tls' => (
is => 'rw'
,isa => 'Int'
,default => 0
);
has 'description' => (
is => 'rw'
,isa => 'Str'
......@@ -142,7 +136,8 @@ has 'description' => (
# Method Modifiers
#
around 'display' => \&_around_display;
around 'display_info' => \&_around_display_info;
around 'display_file_tls' => \&_around_display_file_tls;
around 'add_volume' => \&_around_add_volume;
......@@ -662,10 +657,12 @@ sub _allowed {
}
sub _around_display($orig,$self,$user) {
sub _around_display_info($orig,$self,$user ) {
$self->_allowed($user);
my $display = $self->$orig($user);
$self->_data(display => $display) if !$self->readonly;
if (!$self->readonly) {
$self->_data(display => encode_json($display));
}
return $display;
}
......@@ -992,21 +989,39 @@ sub display_file($self,$user) {
return $self->_display_file_spice($user);
}
sub _around_display_file_tls($orig, $self, $user) {
my $display_file = $self->$orig($user);
if (!$self->readonly) {
$self->_data(display_file => $display_file);
}
return $display_file;
}
sub display_file_tls($self, $user) {
return $self->_display_file_spice($user,1);
}
sub display($self, $user) {
my $display_info = $self->display_info($user);
return $display_info->{display};
}
# taken from isard-vdi thanks to @tuxinthejungle Alberto Larraz
sub _display_file_spice($self,$user) {
sub _display_file_spice($self,$user, $tls = 0) {
# my ($ip,$port) = $self->display($user) =~ m{spice://(\d+\.\d+\.\d+\.\d+):(\d+)};
my ($ip,$port) = $self->display($user) =~ m{spice://(\d+\.\d+\.\d+\.\d+):(\d+)};
my $display = $self->display_info($user);
die "I can't find ip port in ".$self->display if !$ip ||!$port;
die "I can't find ip port in ".$self->display if !$display->{address} || !$display->{port};
my $ret =
"[virt-viewer]\n"
."type=spice\n"
."host=$ip\n";
if ($self->tls) {
$ret .= "tls-port=%s\n";
."host=".$display->{address}."\n";
if ($tls) {
$ret .= "tls-port=".$display->{tls_port}."\n";
} else {
$ret .= "port=$port\n";
$ret .= "port=".$display->{port}."\n";
}
$ret .="password=%s\n" if $self->spice_password();
......@@ -1018,16 +1033,16 @@ sub _display_file_spice($self,$user) {
."enable-usb-autoshare=1\n"
."delete-this-file=1\n";
$ret .=";" if !$self->tls;
$ret .= "tls-ciphers=DEFAULT\n"
.";host-subject=O=".$ip.",CN=?\n";
if ( $tls ) {
$ret .= "tls-ciphers=DEFAULT\n"
."host-subject=".$self->_vm->tls_host_subject."\n"
.="ca=".$self->_vm->tls_ca."\n"
}
$ret .=";" if !$self->tls;
$ret .="ca=CA\n"
."release-cursor=shift+f11\n"
$ret .="release-cursor=shift+f11\n"
."toggle-fullscreen=shift+f12\n"
."secure-attention=ctrl+alt+end\n";
$ret .=";" if !$self->tls;
$ret .=";" if !$tls;
$ret .="secure-channels=main;inputs;cursor;playback;record;display;usbredir;smartcard\n";
return $ret;
......@@ -1054,6 +1069,7 @@ sub info($self, $user) {
};
eval {
$info->{display_url} = $self->display($user) if $self->is_active;
$self->display_file($user) if $self->is_active && !$self->_data('display_file');
};
die $@ if $@ && $@ !~ /not allowed/i;
if (!$info->{description} && $self->id_base) {
......@@ -1061,10 +1077,11 @@ sub info($self, $user) {
$info->{description} = $base->description;
}
if ($self->is_active) {
my $display = $self->display($user);
my ($local_ip, $local_port) = $display =~ m{\w+://(.*):(\d+)};
$info->{display_ip} = $local_ip;
$info->{display_port} = $local_port;
my $display = $self->display_info($user);
$self->display_file($user) if !$self->_data('display_file');
$self->display_file_tls($user)
if $display->{tls_port} && !$self->_data('display_file');
$info->{display} = $display;
}
$info->{hardware} = $self->get_controllers();
......@@ -1696,6 +1713,7 @@ sub _post_shutdown {
$info = {} if !$info;
delete $info->{ip};
$self->_data(info => encode_json($info));
$self->_data(display_file => '');
# only if not volatile
my $request;
$request = $arg{request} if exists $arg{request};
......@@ -1926,7 +1944,11 @@ sub _post_start {
$self->get_info();
# get the display so it is stored for front access
$self->display($arg{user}) if $self->is_active;
if ($self->is_active) {
$self->display($arg{user});
$self->display_file($arg{user});
$self->info($arg{user});
}
Ravada::Request->enforce_limits(at => time + 60);
$self->post_resume_aux;
}
......@@ -1961,6 +1983,7 @@ sub _add_iptable {
return if !$self->is_active;
my $display = $self->display($user);
$self->display_file($user) if !$self->_data('display_file');
my ($local_port) = $display =~ m{\w+://.*:(\d+)};
$self->_remove_iptables( port => $local_port );
......@@ -2105,6 +2128,7 @@ sub open_iptables {
}
$self->_add_iptable(%args);
$self->info($user);
}
sub _log_iptable {
......
......@@ -13,7 +13,7 @@ use Carp qw(cluck confess croak);
use Data::Dumper;
use File::Copy;
use File::Path qw(make_path);
use Hash::Util qw(lock_keys);
use Hash::Util qw(lock_keys lock_hash);
use IPC::Run3 qw(run3);
use Moose;
use Sys::Virt::Stream;
......@@ -48,6 +48,7 @@ has readonly => (
##################################################
#
our $TIMEOUT_SHUTDOWN = 60;
our $OUT;
our %SET_DRIVER_SUB = (
......@@ -538,13 +539,13 @@ sub post_resume_aux($self) {
}
}
=head2 display
=head2 display_info
Returns the display URI
Returns the display information as a hashref. The display URI is in the display entry
=cut
sub display($self, $user) {
sub display_info($self, $user) {
my $xml = XML::LibXML->load_xml(string => $self->xml_description);
my ($graph) = $xml->findnodes('/domain/devices/graphics')
......@@ -552,6 +553,7 @@ sub display($self, $user) {
my ($type) = $graph->getAttribute('type');
my ($port) = $graph->getAttribute('port');
my ($tls_port) = $graph->getAttribute('tlsPort');
my ($address) = $graph->getAttribute('listen');
$address = $self->_vm->nat_ip if $self->_vm->nat_ip;
......@@ -560,7 +562,17 @@ sub display($self, $user) {
die "Unable to get port for domain ".$self->name." ".$graph->toString
if !$port;
return "$type://$address:$port";
my $display = $type."://$address:$port";
my %display = (
type => $type
,port => $port
,address => $address
,display => $display
,tls_port => $tls_port
);
lock_hash(%display);
return \%display;
}
=head2 is_active
......
......@@ -63,11 +63,12 @@ sub name {
return $self->domain;
};
sub display {
sub display_info {
my $self = shift;
my $ip = ($self->_vm->nat_ip or $self->_vm->ip());
return "void://$ip:5990/";
my $display="void://$ip:5990/";
return { display => $display , type => 'void', address => $ip, port => 5990 };
}
sub is_active {
......
......@@ -73,7 +73,16 @@ sub disk_device { confess "TODO" }
sub disk_size { confess "TODO" }
sub display($self, $user) {
return $self->_data('display');
my $info = $self->display_info($user);
return $info->{display};
}
sub display_info($self, $user) {
return decode_json($self->_data('display'));
}
sub display_file_tls($self, $user) {
return $self->_data('display_file');
}
sub force_shutdown { confess "TODO" }
......
......@@ -62,6 +62,9 @@ requires 'import_domain';
requires 'is_alive';
requires 'free_memory';
requires '_fetch_dir_cert';
############################################################
has 'host' => (
......@@ -86,6 +89,27 @@ has 'readonly' => (
,default => 0
);
has 'tls_host_subject' => (
isa => 'Str'
, is => 'ro'
, builder => '_fetch_tls_host_subject'
, lazy => 1
);
has 'tls_ca' => (
isa => 'Str'
, is => 'ro'
, builder => '_fetch_tls_ca'
, lazy => 1
);
has dir_cert => (
isa => 'Str'
,is => 'ro'
,lazy => 1
,builder => '_fetch_dir_cert'
);
has 'store' => (
isa => 'Bool'
, is => 'rw'
......@@ -1187,6 +1211,29 @@ sub shutdown_domains($self) {
$sth->finish;
}
sub _fetch_tls_host_subject($self) {
my @cmd= qw(/usr/bin/openssl x509 -noout -text -in );
push @cmd, ( $self->dir_cert."/server-cert.pem" );
my ($out, $err) = $self->run_command(@cmd);
die $err if $err;
for my $line (split /\n/,$out) {
chomp $line;
next if $line !~ /^\s+Subject:\s+(.*)/;
my $subject = $1;
$subject =~ s/ = /=/g;
$subject =~ s/, /,/g;
return $subject;
}
}
sub _fetch_tls_ca($self) {
my ($out, $err) = $self->run_command("/bin/cat", $self->dir_cert."/ca-cert.pem");
return join('\n', (split /\n/,$out) );
}
sub _store_mac_address($self, $force=0 ) {
return if !$force && $self->_data('mac');
die "Error: I can't find arp" if !$ARP;
......
......@@ -62,6 +62,8 @@ has type => (
our $DIR_XML = "etc/xml";
$DIR_XML = "/var/lib/ravada/xml/" if $0 =~ m{^/usr/sbin};
our $FILE_CONFIG_QEMU = "/etc/libvirt/qemu.conf";
our $XML = XML::LibXML->new();
#-----------
......@@ -2118,4 +2120,17 @@ sub _free_memory_available($self) {
return $free_mem;
}
sub _fetch_dir_cert($self) {
open my $in,'<',$FILE_CONFIG_QEMU or die "$! $FILE_CONFIG_QEMU";
while(my $line = <$in>) {
chomp $line;
$line =~ s/#.*//;
next if !length($line);
next if $line !~ /^\s*spice_tls_x509_cert_dir\s*=\s*"(.*)"\s*/;
return $1 if $1;
}
close $in;
}
1;
......@@ -291,6 +291,9 @@ sub free_memory {
return $memory;
}
sub _fetch_dir_cert {
confess "TODO";
}
#########################################################################3
1;
......@@ -665,6 +665,26 @@ get '/machine/display/(:id).vv' => sub {
return $c->render(data => $domain->display_file($USER), format => 'vv');
};
get '/machine/display-tls/(:id)-tls.vv' => sub {
my $c = shift;
my $id = $c->stash('id');
my $domain = $RAVADA->search_domain_by_id($id);
return $c->render(text => "unknown machine id=$id") if !$id;
return access_denied($c)
if $USER->id ne $domain->id_owner
&& !$USER->is_admin;
$c->res->headers->content_type('application/x-virt-viewer');
$c->res->headers->content_disposition(
"inline;filename=".$domain->id."-tls.vv");
return $c->render(data => $domain->display_file_tls($USER), format => 'vv');
};
# Users ##########################################################3
##add user
......
use warnings;
use strict;
use Carp qw(confess);
use Data::Dumper;
use Test::More;
use lib 't/lib';
use Test::Ravada;
init();
my $FILE_CONFIG_QEMU = "/etc/libvirt/qemu.conf";
#################################################################
sub _check_libvirt_tls {
my %search = map { $_ => 0 }
('spice_tls = 1',
'spice_tls_x509_cert_dir = '
);
open my $in,'<',$FILE_CONFIG_QEMU or die "$! $FILE_CONFIG_QEMU";
while(my $line = <$in>) {
chomp $line;
$line =~ s/#.*//;
next if !length($line);
for my $pattern (keys %search) {
delete $search{$pattern} if $line =~ /^$pattern/
}
last if !keys %search;
}
return if !keys %search;
return "Missing in $FILE_CONFIG_QEMU: ".join(" , ",keys %search)
."\n".'https://ravada.readthedocs.io/en/latest/docs/spice_tls.html';
}
sub test_tls {
my $vm_name = shift;
my $domain = create_domain($vm_name);
my $vm = $domain->_vm;
like($vm->tls_host_subject,qr'.') or return;
$domain->start(user_admin);
my $display;
eval {
$display = $domain->display(user_admin);
};
is($@,'') or return;
my $display_file = $domain->display_file_tls(user_admin);
my @lines = split /\n/,$display_file;
ok(grep(/^ca=-+BEGIN/, @lines),"Expecting ca on ".Dumper(\@lines));
ok(grep(/^tls-port=.+/, @lines),"Expecting tls-port on ".Dumper(\@lines));
ok(grep(/^tls-ciphers=.+/, @lines),"Expecting tls-ciphers on ".Dumper(\@lines));
ok(grep(/^host-subject=.+/, @lines),"Expecting host-subject on ".Dumper(\@lines));
=pod
open my $out,'>',"/var/tmp/".$domain->name.".xml" or die $!;
print $out join("\n", @lines)."\n";
close $out;
exit;
=cut
my $domain_f = Ravada::Front::Domain->open($domain->id);
my $file_f = $domain_f->display_file_tls(user_admin);
is($file_f, $display_file);
$domain->remove(user_admin);
}
#################################################################
clean();
my $vm_name = 'KVM';
my $vm = rvd_back->search_vm($vm_name);
SKIP: {
my $msg = "SKIPPED: No virtual managers found";
if ($vm && $vm_name =~ /kvm/i && $>) {
$msg = "SKIPPED: Test must run as root";
$vm = undef;
}
if ($vm) {
$msg = _check_libvirt_tls();
$vm = undef if $msg;
}
diag($msg) if !$vm;
skip($msg,10) if !$vm;
test_tls($vm_name);
}
clean();
done_testing();
......@@ -934,7 +934,7 @@ sub start_node($node) {
$domain->start(user => user_admin, remote_ip => '127.0.0.1') if !$domain->is_active;
for ( 1 .. 30 ) {
for ( 1 .. 60 ) {
last if $node->ping ;
sleep 1;
diag("Waiting for ping node ".$node->name." ".$node->ip." $_") if !($_ % 10);
......@@ -942,7 +942,7 @@ sub start_node($node) {
is($node->ping('debug'),1,"[".$node->type."] Expecting ping node ".$node->name) or exit;
for ( 1 .. 20 ) {
for ( 1 .. 60 ) {
my $is_active;
eval { $is_active = $node->_do_is_active };
last if $is_active;
......
......@@ -45,8 +45,8 @@
<a ng-click="copy_password(); redirect()"
href="{{domain.display_url}}">{{domain.display_url}}</a></li>
</li>
<li><b>Display IP :</b> 192.168.1.106</li>
<li><b>Display Port :</b> 5900</li>
<li><b>Display IP :</b> {{domain.display.address}} </li>
<li><b>Display Port :</b> {{domain.display.port}} </li>
</ul>
<h3>Machine Information</h3>
<ul>
......@@ -59,14 +59,27 @@
</ul>
</div>
<div ng-show="domain.is_active">
<div>
<a type="button" class="btn btn-success"
ng-show="!domain.display.tls_port"
ng-click="view_clicked=true; copy_password(); redirect();"
href="/machine/display/{{domain.id}}.vv">
<b><%=l 'view'%></b></a>
<i><%=l 'Press SHIFT + F12 to exit' %></i>
<br/>
<a type="button" class="btn btn-success" ng-view="domain,display.tls_port"
ng-show="domain.display.tls_port"
ng-click="view_clicked_tls=true; copy_password(); redirect();"
href="/machine/display-tls/{{domain.id}}-tls.vv">
<b><%=l 'view'%></b></a>
</div>
<div>
<span ng-show="view_clicked"><%=l 'If you can not see the machine screen in a few seconds check for a file called' %> <b>{{domain.id}}.vv</b> <%=l 'in your downloads folder.' %>
</span>
<span ng-show="view_clicked_tls"><%=l 'If you can not see the machine screen in a few seconds check for a file called' %> <b>{{domain.id}}-tls.vv</b> <%=l 'in your downloads folder.' %>
</span>
<br/>
<i><%=l 'Press SHIFT + F12 to exit the virtual machine' %></i>
</div>
</div>
</div>
<div>
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment