+sub assemble_spice_ticket {
+ my ($username, $vmid, $node) = @_;
+
+ my $rsa_priv = get_privkey();
+
+ my $timestamp = sprintf("%08x", time());
+
+ my $randomstr = "PVESPICE:$timestamp:$vmid:$node:" . rand(10);
+
+ # this should be uses as one-time password
+ # max length is 60 chars (spice limit)
+ # we pass this to qemu set_pasword and limit lifetime there
+ # keep this secret
+ my $ticket = Digest::SHA::sha1_hex($rsa_priv->sign($randomstr));
+
+ # Note: spice proxy connects with HTTP, so $proxyticket is exposed to public
+ # we use a signature/timestamp to make sure nobody can fake such ticket
+ # an attacker can use this $proxyticket, but he will fail because $ticket is
+ # private.
+ # The proxy need to be able to extract/verify the ticket
+ # Note: data needs to be lower case only, because virt-viewer needs that
+ # Note: RSA signature are too long (>=256 charaters) and makes problems with remote-viewer
+
+ my $secret = &$get_csrfr_secret();
+ my $plain = "pvespiceproxy:$timestamp:$vmid:" . lc($node);
+
+ # produces 40 characters
+ my $sig = unpack("H*", Digest::SHA::sha1($plain, &$get_csrfr_secret()));
+
+ #my $sig = unpack("H*", $rsa_priv->sign($plain)); # this produce too long strings (512)
+
+ my $proxyticket = $plain . "::" . $sig;
+
+ return ($ticket, $proxyticket);
+}
+
+sub verify_spice_connect_url {
+ my ($connect_str) = @_;
+
+ # Note: we pass the spice ticket as 'host', so the
+ # spice viewer connects with "$ticket:$port"
+
+ return undef if !$connect_str;
+
+ if ($connect_str =~m/^pvespiceproxy:([a-z0-9]{8}):(\d+):(\S+)::([a-z0-9]{40}):(\d+)$/) {
+ my ($timestamp, $vmid, $node, $hexsig, $port) = ($1, $2, $3, $4, $5, $6);
+ my $ttime = hex($timestamp);
+ my $age = time() - $ttime;
+
+ # use very limited lifetime - is this enough?
+ return undef if !(($age > -20) && ($age < 40));
+
+ my $plain = "pvespiceproxy:$timestamp:$vmid:$node";
+ my $sig = unpack("H*", Digest::SHA::sha1($plain, &$get_csrfr_secret()));
+
+ if ($sig eq $hexsig) {
+ return ($vmid, $node, $port);
+ }
+ }
+
+ return undef;
+}
+
+sub read_x509_subject_spice {
+ my ($filename) = @_;
+
+ # read x509 subject
+ my $bio = Net::SSLeay::BIO_new_file($filename, 'r');
+ my $x509 = Net::SSLeay::PEM_read_bio_X509($bio);
+ Net::SSLeay::BIO_free($bio);
+ my $nameobj = Net::SSLeay::X509_get_subject_name($x509);
+ my $subject = Net::SSLeay::X509_NAME_oneline($nameobj);
+ Net::SSLeay::X509_free($x509);
+
+ # remote-viewer wants comma as seperator (not '/')
+ $subject =~ s!^/!!;
+ $subject =~ s!/(\w+=)!,$1!g;
+
+ return $subject;
+}
+
+# helper to generate SPICE remote-viewer configuration
+sub remote_viewer_config {
+ my ($authuser, $vmid, $node, $proxy, $title, $port) = @_;
+
+ if (!$proxy) {
+ my $host = `hostname -f` || PVE::INotify::nodename();
+ chomp $host;
+ $proxy = $host;
+ }
+
+ my ($ticket, $proxyticket) = assemble_spice_ticket($authuser, $vmid, $node);
+
+ my $filename = "/etc/pve/local/pve-ssl.pem";
+ my $subject = read_x509_subject_spice($filename);
+
+ my $cacert = PVE::Tools::file_get_contents("/etc/pve/pve-root-ca.pem", 8192);
+ $cacert =~ s/\n/\\n/g;
+
+ $proxy = "[$proxy]" if Net::IP::ip_is_ipv6($proxy);
+ my $config = {
+ 'secure-attention' => "Ctrl+Alt+Ins",
+ 'toggle-fullscreen' => "Shift+F11",
+ 'release-cursor' => "Ctrl+Alt+R",
+ type => 'spice',
+ title => $title,
+ host => $proxyticket, # this break tls hostname verification, so we need to use 'host-subject'
+ proxy => "http://$proxy:3128",
+ 'tls-port' => $port,
+ 'host-subject' => $subject,
+ ca => $cacert,
+ password => $ticket,
+ 'delete-this-file' => 1,
+ };
+
+ return ($ticket, $proxyticket, $config);
+}
+