[SOLVED] noVNC over API: PVEAuthCookie (PVE Ticket) and Tunnel Auth (VNC Ticket) - How? :-)

linux

Member
Dec 14, 2020
96
36
23
Australia
Hi there,

Just trying to get to the bottom of this after 2 days working on it. UPDATE: We were able to work through the niggles.

Still I am getting 401 No Ticket despite there being a VNC Ticket from vncproxy passed into vncwebsocket by noVNC (via path), and a cookie set with PVE Ticket I think too. UPDATE: No Ticket is to do with Access Ticket for PVE itself. This needed fine-tuning to work through.

PHP:
function pvewhmcs_noVNC($params) {
    $serverip = $params["serverip"];
    $serverusername = 'vnc';
    $serverpassword = Capsule::table('mod_pvewhmcs')->where('id', '1')->value('vnc_secret');
    $proxmox=new PVE2_API($serverip, $serverusername, "pve", $serverpassword);
    if ($proxmox->login()) {
        # Get first node name.
        $nodes = $proxmox->get_node_list();
        $first_node = $nodes[0];
        unset($nodes);
        $guest=Capsule::table('mod_pvewhmcs_vms')->where('id','=',$params['serviceid'])->get()[0] ;
        $vm_vncproxy=$proxmox->post('/nodes/'.$first_node.'/'.$guest->vtype.'/'.$params['serviceid'] .'/vncproxy', array( 'websocket' => '1' )) ;

        // Get both tickets prepared
        $pveticket = $proxmox->getTicket();
        $vncticket = $vm_vncproxy['ticket'];
        // $path should only contain the actual path without any query parameters
        $path = 'api2/json/nodes/' . $first_node . '/' . $guest->vtype . '/' . $params['serviceid'] . '/vncwebsocket?port=' . $vm_vncproxy['port'] . '&vncticket=' . urlencode($vncticket);

        $url = '/modules/servers/pvewhmcs/novnc_router.php?host=' . $serverip . '&pveticket=' . urlencode($pveticket) . '&path=' . urlencode($path) . '&vncticket=' . urlencode($vncticket);
        $vncreply='<center><strong>Console (noVNC) prepared for usage. <a href="'.$url.'" target="_blanK">Click here</a> to open the noVNC window.</strong></center>' ;

        return $vncreply;

    } else {
        $vncreply='Failed to prepare noVNC. Please contact Technical Support.';
        return $vncreply;
    }
}

Then there is novnc_router.php which adds the cookie locally for the ACL-restricted 'vnc' user on PVE, then routes to noVNC HTML file for the connection. Passed in to the file is the PVE Ticket, VNC Ticket, Server IP and Request Path.

We had to change our approach to be same-domain-only due to anti-CSRF (cookies can extend to subdomains only), update DNS to let that reflect, and then work through encoding the VNC Ticket properly to make it survive to pve-api-daemon.

PHP:
<?php
// FILE: novnc_router.php
// TASK: Take WHMCS request, add browser cookie, then redirect to noVNC
if (isset($_GET['pveticket']) && isset($_GET['host']) && isset($_GET['path']) && isset($_GET['vncticket'])) {
    $pveticket = $_GET['pveticket'];
    $vncticket = $_GET['vncticket'];
    $host = $_GET['host'];
    $path = $_GET['path'];

    // Get the requesting hostname/domain from request
    $whmcsdomain = parse_url($_SERVER['HTTP_HOST']);
    $domainonly = preg_replace("/^(.*?)\.(.*)$/","$2",$whmcsdomain['path']);
    setrawcookie('PVEAuthCookie', $pveticket, 0, '/', $domainonly);

    // Create the final noVNC URL with the re-encoded vncticket
    $hostname = gethostbyaddr($host);
    $redirect_url = '/modules/servers/pvewhmcs/novnc/vnc.html?autoconnect=true&encrypt=true&host=' . $hostname . '&port=8006&password=' . urlencode($vncticket) . '&path=' . urlencode($path);

    header('Location: ' . $redirect_url);
    exit;
} else {
    echo 'Error: Missing required info to route your request. Please try again.';
}
?>

noVNC then correctly has the Web Socket config with 1.2.3.4, port 8006, and path:

api2/json/nodes/syd-pvetest/qemu/103/vncwebsocket?port=5900&vncticket=PVEVNC%3A648F1097%3A%3ALF3XL%2FdXR%2FDhfXJMCPSduSCYkKEQn6m4lTkdnfQe9bSvCBQUFxaehjdyhhCd0EavucnmcTRZndnQPaLKSlWzSaTpb4yEhL%2B8rvuw%2Fec%2BNLPMh7JOin7vSiQJR2nJ%2BGtO%2BYPMXV9aD4Ib0vzRwmcrjx21u387nnQTX%2Bof0Ap8L0u3xN1XCcabKIMRDwvMMt9Xd5hX7dwg%2BHVufzMarCr2surgkJk7pRDIXB5VzHDBd6%2FQDoD7t29uhsbboY94vVwNA0n4cn2wF0gqqnKE01eUQOm0OdGPE%2BvPpjSxIRkNzSnNeI2PoJ1vLZX%2BOluNUC9KGW2J27LszC78KEKzrvZcqQ%3D%3D

Firefox can’t establish a connection to the server at wss://1.2.3.4:8006/api2/json/nodes/syd-pvetest/qemu/103/vncwebsocket?port=5900&vncticket=PVEVNC%3A648F10... due to 401 No Ticket. The WHMCS and Proxmox are on different "domains" (Proxmox is only on an IP) but cookie error was same without adding 5th param to setrawcookie().

Update 1: We had to change how the cookie was being set for this to be worked through. And of course, anti-CSRF means same-domain.

Update 2: We then had a PVEVNC Ticket Invalid style response. This turned out to be due to a need to twice-encode the VNC Ticket.

Update 3: We had to change the noVNC query strings to include autoconnect=true and encrypted=true, & pass the password in.

Ref: (dual-encode) https://forum.proxmox.com/threads/4...ermission-denied-invalid-pvevnc-ticket.34144/
Ref: (moving to "vnc" user from "root" re: security) https://github.com/ZzAntares/ProxmoxVE/issues/17#issuecomment-688937674
Ref: (Invalid VNC Ticket - re: encoding) https://forum.proxmox.com/threads/401permission-denied-invalid-pvevnc-ticket.110961/
Ref: (cookie needed, class can't do token) https://forum.proxmox.com/threads/working-with-api-getting-401-no-ticket.75108/

Many people online seem to just revert to using iframe of the Proxmox VE GUI, and I see why. Finally we got it working. :-)

Cheers,
Linux
 
Last edited:
finally,i use nginx as proxy and JS set the same orign cookie
it's working now!
Code:
server {
    listen 80 default_server;
    rewrite ^(.*) https://$host$1 permanent;
}
 
server {
    listen 443 ssl;
    server_name _;
    #ssl on;
    ssl_certificate /etc/Nginx1.15.11/conf/proxmox/pve-ssl.pem;
    ssl_certificate_key /etc/Nginx1.15.11/conf/proxmox/pve-ssl.key;
    proxy_redirect off;
        location ~ /pve {
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        add_header 'Access-Control-Allow-Origin' '*' always;
        add_header 'Access-Control-Allow-Methods' '*' always;
        add_header 'Access-Control-Allow-Headers' '*' always;
        add_header 'Access-Control-Allow-Credentials' true always;
        #my php webapp
        proxy_pass http://127.0.0.1:8989;
        proxy_buffering off;
        client_max_body_size 0;
        proxy_connect_timeout  3600s;
        proxy_read_timeout  3600s;
        proxy_send_timeout  3600s;
        send_timeout  3600s;
   }
    location /api2/json {
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        add_header 'Access-Control-Allow-Origin' '*' always;
        add_header 'Access-Control-Allow-Methods' '*' always;
        add_header 'Access-Control-Allow-Headers' '*' always;
        add_header 'Access-Control-Allow-Credentials' true always;
        #my pve server
        proxy_pass https://192.168.99.230:8006;
        proxy_buffering off;
        client_max_body_size 0;
        proxy_connect_timeout  3600s;
        proxy_read_timeout  3600s;
        proxy_send_timeout  3600s;
        send_timeout  3600s;
   }
 
Last edited:
finally,i use nginx as proxy and JS set the same orign cookie
it's working now!
Code:
server {
    listen 80 default_server;
    rewrite ^(.*) https://$host$1 permanent;
}
 
server {
    listen 443 ssl;
    server_name _;
    #ssl on;
    ssl_certificate /etc/Nginx1.15.11/conf/proxmox/pve-ssl.pem;
    ssl_certificate_key /etc/Nginx1.15.11/conf/proxmox/pve-ssl.key;
    proxy_redirect off;
        location ~ /pve {
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        add_header 'Access-Control-Allow-Origin' '*' always;
        add_header 'Access-Control-Allow-Methods' '*' always;
        add_header 'Access-Control-Allow-Headers' '*' always;
        add_header 'Access-Control-Allow-Credentials' true always;
        #my php webapp
        proxy_pass http://127.0.0.1:8989;
        proxy_buffering off;
        client_max_body_size 0;
        proxy_connect_timeout  3600s;
        proxy_read_timeout  3600s;
        proxy_send_timeout  3600s;
        send_timeout  3600s;
   }
    location /api2/json {
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        add_header 'Access-Control-Allow-Origin' '*' always;
        add_header 'Access-Control-Allow-Methods' '*' always;
        add_header 'Access-Control-Allow-Headers' '*' always;
        add_header 'Access-Control-Allow-Credentials' true always;
        #my pve server
        proxy_pass https://192.168.99.230:8006;
        proxy_buffering off;
        client_max_body_size 0;
        proxy_connect_timeout  3600s;
        proxy_read_timeout  3600s;
        proxy_send_timeout  3600s;
        send_timeout  3600s;
   }
Can you explain how you did it? The entire flow?
 
PHP:
use Corsinvest\ProxmoxVE\Api\PveClient;
public function createCookie()
    {

        $client = new PveClient("192.168.99.230", 8006);
        if ($client->login('root', 'rootroot', 'pam')) {
            //
            $csr = $client->ticketCSRFPreventionToken;
            $ticket = $client->ticketPVEAuthCookie;
            $res['data'] = array('username' => 'root@pam', 'CSRFPreventionToken' => $csr, 'ticket' => $ticket);
            return json($res);
        }
//another file
    <?php

/*
 * SPDX-FileCopyrightText: Copyright Corsinvest Srl
 * SPDX-License-Identifier: GPL-3.0-only
 */

namespace Corsinvest\ProxmoxVE\Api;

/**
 * Class ClientBase
 * @package Corsinvest\ProxmoxVE\Api
 *
 * Proxmox VE Client Base
 */
class PveClientBase
{

    /**
     * @ignore
     */
    //private->public
    public $ticketCSRFPreventionToken;

    /**
     * @ignore
     */
    //private->public
    public $ticketPVEAuthCookie;
    }
if you use get createCookie function can get a json array,
JavaScript:
var xhr = new XMLHttpRequest();
        xhr.open('GET', '/admin/pve.lxc/createCookie', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        xhr.withCredentials=true
xhr.onload = function() {
          if (xhr.status === 200) {
            
            var res = JSON.parse(xhr.responseText);
            console.log(res);
            PVE.UserName = res.data.username;
            PVE.CSRFPreventionToken = res.data.CSRFPreventionToken
            
            setCookie('PVEAuthCookie',res.data.ticket,8600)
            createTerminal();
          } else {
          
            console.error('faild' + xhr.status);
          }
        };
        var params = [];
        xhr.send(params);
nginx proxy the pve server make sure in the same area https://127.0.0.1 ,same cookies
 

About

The Proxmox community has been around for many years and offers help and support for Proxmox VE, Proxmox Backup Server, and Proxmox Mail Gateway.
We think our community is one of the best thanks to people like you!

Get your subscription!

The Proxmox team works very hard to make sure you are running the best software and getting stable updates and security enhancements, as well as quick enterprise support. Tens of thousands of happy customers have a Proxmox subscription. Get yours easily in our online shop.

Buy now!