_(Docker storage driver)_:
:
- =mk_option(_var($dockercfg,'DOCKER_BACKINGFS'), 'native', _('native'))?>
=mk_option(_var($dockercfg,'DOCKER_BACKINGFS'), 'overlay2', _('overlay2'))?>
+ =mk_option(_var($dockercfg,'DOCKER_BACKINGFS'), 'native', _('native'))?>
_(Only modify if this is a new installation since this can lead to unwanted behaviour!)_
@@ -886,13 +884,14 @@ function btrfsScrub(path) {
}
});
}
+var originalPath = $("#DOCKER_IMAGE_FILE2").val();
function updateLocation(val) {
var content1 = $("#DOCKER_IMAGE_FILE1");
var content2 = $("#DOCKER_IMAGE_FILE2");
var dropdown = $("#DOCKER_BACKINGFS");
+ var path = originalPath.split('/');
switch (val) {
case 'xfs':
- var path = content2.val().split('/');
path.splice(-1,1);
content1.val((path.join('/') + '/docker-xfs.img'));
$('#vdisk_file').show('slow');
@@ -903,9 +902,8 @@ function updateLocation(val) {
dropdown.val('native');
break;
case 'folder':
- var path = content2.val().split('/');
if (path[path.length-1]=='') path.splice(-2,2); else path.splice(-1,1);
- content2.val(path.join('/'));
+ content2.val(path.join('/') + '/');
$('#vdisk_file').hide('slow');
$('#vdisk_dir').show('slow');
$('#backingfs_type').show('slow');
@@ -913,7 +911,6 @@ function updateLocation(val) {
content2.prop('disabled',false).trigger('change');
break;
default:
- var path = content2.val().split('/');
path.splice(-1,1);
content1.val((path.join('/') + '/docker.img'));
$('#vdisk_file').show('slow');
diff --git a/emhttp/plugins/dynamix.docker.manager/images/tailscale.png b/emhttp/plugins/dynamix.docker.manager/images/tailscale.png
new file mode 100755
index 000000000..fd4a4fced
Binary files /dev/null and b/emhttp/plugins/dynamix.docker.manager/images/tailscale.png differ
diff --git a/emhttp/plugins/dynamix.docker.manager/include/CreateDocker.php b/emhttp/plugins/dynamix.docker.manager/include/CreateDocker.php
index 11e4eac2a..b2fa890d1 100644
--- a/emhttp/plugins/dynamix.docker.manager/include/CreateDocker.php
+++ b/emhttp/plugins/dynamix.docker.manager/include/CreateDocker.php
@@ -141,11 +141,24 @@ if (isset($_POST['contName'])) {
@unlink("$userTmplDir/my-$existing.xml");
}
}
+ // Extract real Entrypoint and Cmd from container for Tailscale
+ if (isset($_POST['contTailscale']) && $_POST['contTailscale'] == 'on') {
+ // Create preliminary base container but don't run it
+ exec("/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/docker create --name '" . escapeshellarg($Name) . "' '" . escapeshellarg($Repository) . "'");
+ // Get Entrypoint and Cmd from docker inspect
+ $containerInfo = $DockerClient->getContainerDetails($Name);
+ $ts_env = isset($containerInfo['Config']['Entrypoint']) ? '-e ORG_ENTRYPOINT="' . implode(' ', $containerInfo['Config']['Entrypoint']) . '" ' : '';
+ $ts_env .= isset($containerInfo['Config']['Cmd']) ? '-e ORG_CMD="' . implode(' ', $containerInfo['Config']['Cmd']) . '" ' : '';
+ // Insert Entrypoint and Cmd to docker command
+ $cmd = str_replace('-l net.unraid.docker.managed=dockerman', $ts_env . '-l net.unraid.docker.managed=dockerman' , $cmd);
+ // Remove preliminary container
+ exec("/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/docker rm '" . escapeshellarg($Name) . "'");
+ }
if ($startContainer) $cmd = str_replace('/docker create ', '/docker run -d ', $cmd);
execCommand($cmd);
if ($startContainer) addRoute($Name); // add route for remote WireGuard access
- echo '
'._('Done').'
';
+ echo '
'._('View Container Log').' '._('Done').'
';
goto END;
}
@@ -169,6 +182,9 @@ if (isset($_GET['updateContainer'])){
$xml = file_get_contents($tmpl);
[$cmd, $Name, $Repository] = xmlToCommand($tmpl);
$Registry = getXmlVal($xml, "Registry");
+ $ExtraParams = getXmlVal($xml, "ExtraParams");
+ $Network = getXmlVal($xml, "Network");
+ $TS_Enabled = getXmlVal($xml, "TailscaleEnabled");
$oldImageID = $DockerClient->getImageID($Repository);
// pull image
if ($echo && !pullImage($Name, $Repository)) continue;
@@ -182,8 +198,39 @@ if (isset($_GET['updateContainer'])){
// attempt graceful stop of container first
stopContainer($Name, false, $echo);
}
+ // check if network from another container is specified in xml (Network & ExtraParams)
+ if (preg_match('/^container:(.*)/', $Network)) {
+ $Net_Container = str_replace("container:", "", $Network);
+ } else {
+ preg_match("/--(net|network)=container:[^\s]+/", $ExtraParams, $NetworkParam);
+ if (!empty($NetworkParam[0])) {
+ $Net_Container = explode(':', $NetworkParam[0])[1];
+ $Net_Container = str_replace(['"', "'"], '', $Net_Container);
+ }
+ }
+ // check if the container still exists from which the network should be used, if it doesn't exist any more recreate container with network none and don't start it
+ if (!empty($Net_Container)) {
+ $Net_Container_ID = $DockerClient->getContainerID($Net_Container);
+ if (empty($Net_Container_ID)) {
+ $cmd = str_replace('/docker run -d ', '/docker create ', $cmd);
+ $cmd = preg_replace("/--(net|network)=(['\"]?)container:[^'\"]+\\2/", "--network=none ", $cmd);
+ }
+ }
// force kill container if still running after time-out
if (empty($_GET['communityApplications'])) removeContainer($Name, $echo);
+ // Extract real Entrypoint and Cmd from container for Tailscale
+ if ($TS_Enabled == 'true') {
+ // Create preliminary base container but don't run it
+ exec("/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/docker create --name '" . escapeshellarg($Name) . "' '" . escapeshellarg($Repository) . "'");
+ // Get Entrypoint and Cmd from docker inspect
+ $containerInfo = $DockerClient->getContainerDetails($Name);
+ $ts_env = isset($containerInfo['Config']['Entrypoint']) ? '-e ORG_ENTRYPOINT="' . implode(' ', $containerInfo['Config']['Entrypoint']) . '" ' : '';
+ $ts_env .= isset($containerInfo['Config']['Cmd']) ? '-e ORG_CMD="' . implode(' ', $containerInfo['Config']['Cmd']) . '" ' : '';
+ // Insert Entrypoint and Cmd to docker command
+ $cmd = str_replace('-l net.unraid.docker.managed=dockerman', $ts_env . '-l net.unraid.docker.managed=dockerman' , $cmd);
+ // Remove preliminary container
+ exec("/usr/local/emhttp/plugins/dynamix.docker.manager/scripts/docker rm '" . escapeshellarg($Name) . "'");
+ }
execCommand($cmd, $echo);
if ($startContainer) addRoute($Name); // add route for remote WireGuard access
$DockerClient->flushCaches();
@@ -213,6 +260,9 @@ if (isset($_GET['xmlTemplate'])) {
if (is_file($xmlTemplate)) {
$xml = xmlToVar($xmlTemplate);
$templateName = $xml['Name'];
+ if (preg_match('/^container:(.*)/', $xml['Network'])) {
+ $xml['Network'] = explode(':', $xml['Network'], 2);
+ }
if ($xmlType == 'default') {
if (!empty($dockercfg['DOCKER_APP_CONFIG_PATH']) && file_exists($dockercfg['DOCKER_APP_CONFIG_PATH'])) {
// override /config
@@ -269,6 +319,163 @@ $authoring = $authoringMode ? 'advanced' : 'noshow';
$disableEdit = $authoringMode ? 'false' : 'true';
$showAdditionalInfo = '';
$bgcolor = strstr('white,azure',$display['theme']) ? '#f2f2f2' : '#1c1c1c';
+
+# Search for existing TAILSCALE_ entries in the Docker template
+$TS_existing_vars = false;
+if (isset($xml["Config"]) && is_array($xml["Config"])) {
+ foreach ($xml["Config"] as $config) {
+ if (isset($config["Target"]) && strpos($config["Target"], "TAILSCALE_") === 0) {
+ $TS_existing_vars = true;
+ break;
+ }
+ }
+}
+
+# Look for Exit Nodes if Tailscale plugin is installed
+$ts_exit_nodes = [];
+$ts_en_check = false;
+if (file_exists('/usr/local/sbin/tailscale') && exec('pgrep --ns $$ -f "/usr/local/sbin/tailscaled"')) {
+ exec('tailscale exit-node list', $ts_exit_node_list, $retval);
+ if ($retval === 0) {
+ foreach ($ts_exit_node_list as $line) {
+ if (!empty(trim($line))) {
+ if (preg_match('/^(\d+\.\d+\.\d+\.\d+)\s+(.+)$/', trim($line), $matches)) {
+ $parts = preg_split('/\s+/', $matches[2]);
+ $ts_exit_nodes[] = [
+ 'ip' => $matches[1],
+ 'hostname' => $parts[0],
+ 'country' => $parts[1],
+ 'city' => $parts[2],
+ 'status' => $parts[3]
+ ];
+ $ts_en_check = true;
+ }
+ }
+ }
+ }
+}
+
+# Try to detect port from WebUI and set webui_url
+$TSwebuiport = '';
+$webui_url = '';
+if (empty($xml['TailscalePort'])) {
+ if (!empty($xml['WebUI'])) {
+ $webui_url = parse_url($xml['WebUI']);
+ preg_match('/:(\d+)\]/', $webui_url['host'], $matches);
+ $TSwebuiport = $matches[1];
+ }
+}
+
+$TS_raw = [];
+$TS_container_raw = [];
+$TS_HostNameWarning = "";
+$TS_HTTPSDisabledWarning = "";
+$TS_ExitNodeNeedsApproval = false;
+$TS_MachinesLink = "https://login.tailscale.com/admin/machines/";
+$TS_DirectMachineLink = $TS_MachinesLink;
+$TS_HostNameActual = "";
+$TS_not_approved = "";
+$TS_https_enabled = false;
+// Get Tailscale information and create arrays/variables
+!empty($xml) && exec("docker exec -i " . escapeshellarg($xml['Name']) . " /bin/sh -c \"tailscale status --peers=false --json\"", $TS_raw);
+$TS_no_peers = json_decode(implode('', $TS_raw),true);
+$TS_container = json_decode(implode('', $TS_raw),true);
+$TS_container = $TS_container['Self']??'';
+if (!empty($TS_no_peers) && !empty($TS_container)) {
+ // define the direct link to this machine on the Tailscale website
+ if (!empty($TS_container['TailscaleIPs']) && !empty($TS_container['TailscaleIPs'][0])) {
+ $TS_DirectMachineLink = $TS_MachinesLink.$TS_container['TailscaleIPs'][0];
+ }
+ // warn if MagicDNS or HTTPS is disabled
+ if (isset($TS_no_peers['Self']['Capabilities']) && is_array($TS_no_peers['Self']['Capabilities'])) {
+ $TS_https_enabled = in_array("https", $TS_no_peers['Self']['Capabilities'], true) ? true : false;
+ }
+ if (empty($TS_no_peers['CurrentTailnet']['MagicDNSEnabled']) || !$TS_no_peers['CurrentTailnet']['MagicDNSEnabled'] || $TS_https_enabled !== true) {
+ $TS_HTTPSDisabledWarning = "
Enable HTTPS on your Tailscale account to use Tailscale Serve/Funnel.";
+ }
+ // In $TS_container, 'HostName' is what the user requested, need to parse 'DNSName' to find the actual HostName in use
+ $TS_DNSName = _var($TS_container,'DNSName','');
+ $TS_HostNameActual = substr($TS_DNSName, 0, strpos($TS_DNSName, '.'));
+ // compare the actual HostName in use to the one in the XML file
+ if (strcasecmp($TS_HostNameActual, _var($xml, 'TailscaleHostname')) !== 0 && !empty($TS_DNSName)) {
+ // they are different, show a warning
+ $TS_HostNameWarning = "
Warning: the actual Tailscale hostname is '".$TS_HostNameActual."' ";
+ }
+ // If this is an Exit Node, show warning if it still needs approval
+ if (_var($xml,'TailscaleIsExitNode') == 'true' && _var($TS_container, 'ExitNodeOption') === false) {
+ $TS_ExitNodeNeedsApproval = true;
+ }
+ //Check for key expiry
+ if(!empty($TS_container['KeyExpiry'])) {
+ $TS_expiry = new DateTime($TS_container['KeyExpiry']);
+ $current_Date = new DateTime();
+ $TS_expiry_diff = $current_Date->diff($TS_expiry);
+ }
+ // Check for non approved routes
+ if(!empty($xml['TailscaleRoutes'])) {
+ $TS_advertise_routes = str_replace(' ', '', $xml['TailscaleRoutes']);
+ if (empty($TS_container['PrimaryRoutes'])) {
+ $TS_container['PrimaryRoutes'] = [];
+ }
+ $routes = explode(',', $TS_advertise_routes);
+ foreach ($routes as $route) {
+ if (!in_array($route, $TS_container['PrimaryRoutes'])) {
+ $TS_not_approved .= " " . $route;
+ }
+ }
+ }
+ // Check for exit nodes if ts_en_check was not already done
+ if (!$ts_en_check) {
+ exec("docker exec -i ".$xml['Name']." /bin/sh -c \"tailscale exit-node list\"", $ts_exit_node_list, $retval);
+ if ($retval === 0) {
+ foreach ($ts_exit_node_list as $line) {
+ if (!empty(trim($line))) {
+ if (preg_match('/^(\d+\.\d+\.\d+\.\d+)\s+(.+)$/', trim($line), $matches)) {
+ $parts = preg_split('/\s+/', $matches[2]);
+ $ts_exit_nodes[] = [
+ 'ip' => $matches[1],
+ 'hostname' => $parts[0],
+ 'country' => $parts[1],
+ 'city' => $parts[2],
+ 'status' => $parts[3]
+ ];
+ }
+ }
+ }
+ }
+ }
+ // Construct WebUI URL on container template page
+ // Check if webui_url, Tailscale WebUI and MagicDNS are not empty and make sure that MagicDNS is enabled
+ if (!empty($webui_url) && !empty($xml['TailscaleWebUI']) && (!empty($TS_no_peers['CurrentTailnet']['MagicDNSEnabled']) || $TS_no_peers['CurrentTailnet']['MagicDNSEnabled'])) {
+ // Check if serve or funnel are enabled by checking for [hostname] and replace string with TS_DNSName
+ if (!empty($xml['TailscaleWebUI']) && strpos($xml['TailscaleWebUI'], '[hostname]') !== false && isset($TS_DNSName)) {
+ $TS_webui_url = str_replace("[hostname][magicdns]", rtrim($TS_DNSName, '.'), $xml['TailscaleWebUI']);
+ $TS_webui_url = preg_replace('/\[IP\]/', rtrim($TS_DNSName, '.'), $TS_webui_url);
+ $TS_webui_url = preg_replace('/\[PORT:(\d{1,5})\]/', '443', $TS_webui_url);
+ // Check if serve is disabled, construct url with port, path and query if present and replace [noserve] with url
+ } elseif (strpos($xml['TailscaleWebUI'], '[noserve]') !== false && isset($TS_container['TailscaleIPs'])) {
+ $ipv4 = '';
+ foreach ($TS_container['TailscaleIPs'] as $ip) {
+ if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
+ $ipv4 = $ip;
+ break;
+ }
+ }
+ if (!empty($ipv4)) {
+ $webui_url = isset($xml['WebUI']) ? parse_url($xml['WebUI']) : '';
+ $webui_port = (preg_match('/\[PORT:(\d+)\]/', $xml['WebUI'], $matches)) ? ':' . $matches[1] : '';
+ $webui_path = $webui_url['path'] ?? '';
+ $webui_query = isset($webui_url['query']) ? '?' . $webui_url['query'] : '';
+ $webui_query = preg_replace('/\[IP\]/', $ipv4, $webui_query);
+ $webui_query = preg_replace('/\[PORT:(\d{1,5})\]/', ltrim($webui_port, ':'), $webui_query);
+ $TS_webui_url = 'http://' . $ipv4 . $webui_port . $webui_path . $webui_query;
+ }
+ // Check if TailscaleWebUI in the xml is custom and display instead
+ } elseif (strpos($xml['TailscaleWebUI'], '[hostname]') === false && strpos($xml['TailscaleWebUI'], '[noserve]') === false) {
+ $TS_webui_url = $xml['TailscaleWebUI'];
+ }
+ }
+}
?>
">
">
@@ -423,6 +630,9 @@ function addConfigPopup() {
Opts.Buttons += "
_(Remove)_ ";
}
Opts.Number = confNum;
+ if (Opts.Type == "Device") {
+ Opts.Target = Opts.Value;
+ }
newConf = makeConfig(Opts);
$("#configLocation").append(newConf);
reloadTriggers();
@@ -491,6 +701,9 @@ function editConfigPopup(num,disabled) {
}
Opts.Number = num;
+ if (Opts.Type == "Device") {
+ Opts.Target = Opts.Value;
+ }
newConf = makeConfig(Opts);
if (config.hasClass("config_"+Opts.Display)) {
config.html(newConf);
@@ -666,6 +879,18 @@ $(function() {
});
});
+
+
+
diff --git a/emhttp/plugins/dynamix.vm.manager/include/VMMachines.php b/emhttp/plugins/dynamix.vm.manager/include/VMMachines.php
index 846cbe874..820a53f79 100644
--- a/emhttp/plugins/dynamix.vm.manager/include/VMMachines.php
+++ b/emhttp/plugins/dynamix.vm.manager/include/VMMachines.php
@@ -81,7 +81,8 @@ foreach ($vms as $vm) {
if ($vmrcport > 0) {
$wsport = $lv->domain_get_ws_port($res);
$vmrcprotocol = $lv->domain_get_vmrc_protocol($res);
- $vmrcurl = autov('/plugins/dynamix.vm.manager/'.$vmrcprotocol.'.html',true).'&autoconnect=true&host='._var($_SERVER,'HTTP_HOST');
+ if ($vmrcprotocol == "vnc") $vmrcscale = "&resize=scale"; else $vmrcscale = "";
+ $vmrcurl = autov('/plugins/dynamix.vm.manager/'.$vmrcprotocol.'.html',true).$vmrcscale.'&autoconnect=true&host='._var($_SERVER,'HTTP_HOST');
if ($vmrcprotocol == "spice") $vmrcurl .= '&vmname='. urlencode($vm) .'&port=/wsproxy/'.$vmrcport.'/'; else $vmrcurl .= '&port=&path=/wsproxy/'.$wsport.'/';
$graphics = strtoupper($vmrcprotocol).":".$vmrcport."\n";
$virtual = true ;
diff --git a/emhttp/plugins/dynamix.vm.manager/include/VMajax.php b/emhttp/plugins/dynamix.vm.manager/include/VMajax.php
index 7e9b18735..26032267f 100644
--- a/emhttp/plugins/dynamix.vm.manager/include/VMajax.php
+++ b/emhttp/plugins/dynamix.vm.manager/include/VMajax.php
@@ -133,7 +133,8 @@ case 'domain-start-console':
$vmrcurl = autov('/plugins/dynamix.vm.manager/'.$protocol.'.html',true).'&autoconnect=true&host='._var($_SERVER,'HTTP_HOST');
if ($protocol == "spice") $vmrcurl .= '&vmname='. urlencode($domName) .'&port=/wsproxy/'.$vmrcport.'/'; else $vmrcurl .= '&port=&path=/wsproxy/'.$wsport.'/';
}
- $arrResponse['vmrcurl'] = $vmrcurl;
+ if ($protocol == "vnc") $vmrcscale = "&resize=scale"; else $vmrcscale = "";
+ $arrResponse['vmrcurl'] = $vmrcurl.$vmrcscale;
break;
case 'domain-start-consoleRV':
diff --git a/emhttp/plugins/dynamix.vm.manager/include/libvirt.php b/emhttp/plugins/dynamix.vm.manager/include/libvirt.php
index 33e224e98..042a17ae0 100644
--- a/emhttp/plugins/dynamix.vm.manager/include/libvirt.php
+++ b/emhttp/plugins/dynamix.vm.manager/include/libvirt.php
@@ -910,9 +910,9 @@
}
if ($gpu['multi'] == "on"){
- $newgpu_bus= 0x10;
+ $newgpu_bus= 0x07;
if (!isset($multibus[$newgpu_bus])) {
- $multibus[$newgpu_bus] = 0x10;
+ $multibus[$newgpu_bus] = 0x07;
} else {
#Get next bus
$newgpu_bus = end($multibus) + 0x01;
@@ -2004,8 +2004,17 @@
if (is_file($cfg)) unlink($cfg);
if (is_file($xml)) unlink($xml);
if (is_dir($dir) && $this->is_dir_empty($dir)) {
- $error = my_rmdir($dir);
- qemu_log("$domain","delete empty $dir $error");
+ $result= my_rmdir($dir);
+ if ($result['type'] == "zfs") {
+ qemu_log("$domain","delete empty zfs $dir {$result['rtncode']}");
+ if (isset($result['dataset'])) qemu_log("$domain","dataset {$result['dataset']} ");
+ if (isset($result['cmd'])) qemu_log("$domain","Command {$result['cmd']} ");
+ if (isset($result['output'])) {
+ $outputlogs = implode(" ",$result['output']);
+ qemu_log("$domain","Output $outputlogs end");
+ }
+ }
+ else qemu_log("$domain","delete empty $dir {$result['rtncode']}");
}
}
diff --git a/emhttp/plugins/dynamix.vm.manager/include/libvirt_helpers.php b/emhttp/plugins/dynamix.vm.manager/include/libvirt_helpers.php
index 59c77cbd2..e2e9526ba 100644
--- a/emhttp/plugins/dynamix.vm.manager/include/libvirt_helpers.php
+++ b/emhttp/plugins/dynamix.vm.manager/include/libvirt_helpers.php
@@ -712,7 +712,7 @@ private static $encoding = 'UTF-8';
$domain_cfgfile = "/boot/config/domain.cfg";
$domain_cfg = parse_ini_file($domain_cfgfile);
- if ($domain_cfg['DEBUG'] != "yes") {
+ if ( ($domain_cfg['DEBUG'] ?? false) != "yes") {
error_reporting(0);
}
@@ -1115,6 +1115,7 @@ private static $encoding = 'UTF-8';
$arrValidVNCModels = [
'cirrus' => 'Cirrus',
'qxl' => 'QXL (best)',
+ 'virtio' => 'Virtio(2d)',
'vmvga' => 'vmvga'
];
@@ -1749,6 +1750,9 @@ private static $encoding = 'UTF-8';
$pi = pathinfo($config["disk"][$diskid]["new"]) ;
$isdir = is_dir($pi['dirname']) ;
if (is_file($config["disk"][$diskid]["new"])) $file_exists = true ;
+ write("addLog\0".htmlspecialchars("Checking from file:".$file_clone[$diskid]["source"]));
+ write("addLog\0".htmlspecialchars("Checking to file:".$config["disk"][$diskid]["new"]));
+ write("addLog\0".htmlspecialchars("File exists value:". ($file_exists ? "True" : "False")));
$file_clone[$diskid]["target"] = $config["disk"][$diskid]["new"] ;
}
diff --git a/emhttp/plugins/dynamix.vm.manager/nchan/vm_usage b/emhttp/plugins/dynamix.vm.manager/nchan/vm_usage
index ce62b8a93..887ec80c3 100755
--- a/emhttp/plugins/dynamix.vm.manager/nchan/vm_usage
+++ b/emhttp/plugins/dynamix.vm.manager/nchan/vm_usage
@@ -80,8 +80,8 @@ while (true) {
$echodata .= my_scale($vmdata['mem']*1024,$unit)."$unit / ".my_scale($vmdata['curmem']*1024,$unit)."$unit";
if ($vmdata['curmem'] === $vmdata['maxmem']) $echodata .= "
";
else $echodata .= " / " .my_scale($vmdata['maxmem']*1024,$unit)."$unit ";
- $echodata .= _("Read").": ".my_scale($vmdata['rdrate'],$unit)."$unit/s "._("Write").": ".my_scale($vmdata['wrrate'],$unit)."$unit/s ";
- $echodata .= _("RX").": ".my_scale($vmdata['rxrate'],$unit)."$unit/s "._("TX").": ".my_scale($vmdata['txrate'],$unit)."$unit/s ";
+ $echodata .= _("Read").": ".my_scale($vmdata['rdrate']/$timer,$unit)."$unit/s
"._("Write").": ".my_scale($vmdata['wrrate']/$timer,$unit)."$unit/s
";
+ $echodata .= _("RX").": ".my_scale($vmdata['rxrate']/$timer,$unit)."$unit/s "._("TX").": ".my_scale($vmdata['txrate']/$timer,$unit)."$unit/s ";
}
$echo = $echodata ;
}
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/error-handler.js b/emhttp/plugins/dynamix.vm.manager/novnc/app/error-handler.js
index 81a6cba8e..67b63720c 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/app/error-handler.js
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/app/error-handler.js
@@ -6,61 +6,74 @@
* See README.md for usage and integration instructions.
*/
-// NB: this should *not* be included as a module until we have
-// native support in the browsers, so that our error handler
-// can catch script-loading errors.
+// Fallback for all uncought errors
+function handleError(event, err) {
+ try {
+ const msg = document.getElementById('noVNC_fallback_errormsg');
-// No ES6 can be used in this file since it's used for the translation
-/* eslint-disable prefer-arrow-callback */
-
-(function _scope() {
- "use strict";
-
- // Fallback for all uncought errors
- function handleError(event, err) {
- try {
- const msg = document.getElementById('noVNC_fallback_errormsg');
-
- // Only show the initial error
- if (msg.hasChildNodes()) {
- return false;
- }
-
- let div = document.createElement("div");
- div.classList.add('noVNC_message');
- div.appendChild(document.createTextNode(event.message));
- msg.appendChild(div);
-
- if (event.filename) {
- div = document.createElement("div");
- div.className = 'noVNC_location';
- let text = event.filename;
- if (event.lineno !== undefined) {
- text += ":" + event.lineno;
- if (event.colno !== undefined) {
- text += ":" + event.colno;
- }
- }
- div.appendChild(document.createTextNode(text));
- msg.appendChild(div);
- }
-
- if (err && err.stack) {
- div = document.createElement("div");
- div.className = 'noVNC_stack';
- div.appendChild(document.createTextNode(err.stack));
- msg.appendChild(div);
- }
-
- document.getElementById('noVNC_fallback_error')
- .classList.add("noVNC_open");
- } catch (exc) {
- document.write("noVNC encountered an error.");
+ // Work around Firefox bug:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1685038
+ if (event.message === "ResizeObserver loop completed with undelivered notifications.") {
+ return false;
}
- // Don't return true since this would prevent the error
- // from being printed to the browser console.
- return false;
+
+ // Only show the initial error
+ if (msg.hasChildNodes()) {
+ return false;
+ }
+
+ let div = document.createElement("div");
+ div.classList.add('noVNC_message');
+ div.appendChild(document.createTextNode(event.message));
+ msg.appendChild(div);
+
+ if (event.filename) {
+ div = document.createElement("div");
+ div.className = 'noVNC_location';
+ let text = event.filename;
+ if (event.lineno !== undefined) {
+ text += ":" + event.lineno;
+ if (event.colno !== undefined) {
+ text += ":" + event.colno;
+ }
+ }
+ div.appendChild(document.createTextNode(text));
+ msg.appendChild(div);
+ }
+
+ if (err && err.stack) {
+ div = document.createElement("div");
+ div.className = 'noVNC_stack';
+ div.appendChild(document.createTextNode(err.stack));
+ msg.appendChild(div);
+ }
+
+ document.getElementById('noVNC_fallback_error')
+ .classList.add("noVNC_open");
+
+ } catch (exc) {
+ document.write("noVNC encountered an error.");
}
- window.addEventListener('error', function onerror(evt) { handleError(evt, evt.error); });
- window.addEventListener('unhandledrejection', function onreject(evt) { handleError(evt.reason, evt.reason); });
-})();
+
+ // Try to disable keyboard interaction, best effort
+ try {
+ // Remove focus from the currently focused element in order to
+ // prevent keyboard interaction from continuing
+ if (document.activeElement) { document.activeElement.blur(); }
+
+ // Don't let any element be focusable when showing the error
+ let keyboardFocusable = 'a[href], button, input, textarea, select, details, [tabindex]';
+ document.querySelectorAll(keyboardFocusable).forEach((elem) => {
+ elem.setAttribute("tabindex", "-1");
+ });
+ } catch (exc) {
+ // Do nothing
+ }
+
+ // Don't return true since this would prevent the error
+ // from being printed to the browser console.
+ return false;
+}
+
+window.addEventListener('error', evt => handleError(evt, evt.error));
+window.addEventListener('unhandledrejection', evt => handleError(evt.reason, evt.reason));
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/Makefile b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/Makefile
index be564b43b..03eaed071 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/Makefile
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/Makefile
@@ -1,42 +1,42 @@
-ICONS := \
- novnc-16x16.png \
- novnc-24x24.png \
- novnc-32x32.png \
- novnc-48x48.png \
- novnc-64x64.png
+BROWSER_SIZES := 16 24 32 48 64
+#ANDROID_SIZES := 72 96 144 192
+# FIXME: The ICO is limited to 8 icons due to a Chrome bug:
+# https://bugs.chromium.org/p/chromium/issues/detail?id=1381393
+ANDROID_SIZES := 96 144 192
+WEB_ICON_SIZES := $(BROWSER_SIZES) $(ANDROID_SIZES)
-ANDROID_LAUNCHER := \
- novnc-48x48.png \
- novnc-72x72.png \
- novnc-96x96.png \
- novnc-144x144.png \
- novnc-192x192.png
+#IOS_1X_SIZES := 20 29 40 76 # No such devices exist anymore
+IOS_2X_SIZES := 40 58 80 120 152 167
+IOS_3X_SIZES := 60 87 120 180
+ALL_IOS_SIZES := $(IOS_1X_SIZES) $(IOS_2X_SIZES) $(IOS_3X_SIZES)
-IPHONE_LAUNCHER := \
- novnc-60x60.png \
- novnc-120x120.png
-
-IPAD_LAUNCHER := \
- novnc-76x76.png \
- novnc-152x152.png
-
-ALL_ICONS := $(ICONS) $(ANDROID_LAUNCHER) $(IPHONE_LAUNCHER) $(IPAD_LAUNCHER)
+ALL_ICONS := \
+ $(ALL_IOS_SIZES:%=novnc-ios-%.png) \
+ novnc.ico
all: $(ALL_ICONS)
-novnc-16x16.png: novnc-icon-sm.svg
- convert -density 90 \
- -background transparent "$<" "$@"
-novnc-24x24.png: novnc-icon-sm.svg
- convert -density 135 \
- -background transparent "$<" "$@"
-novnc-32x32.png: novnc-icon-sm.svg
- convert -density 180 \
- -background transparent "$<" "$@"
+# Our testing shows that the ICO file need to be sorted in largest to
+# smallest to get the apporpriate behviour
+WEB_ICON_SIZES_REVERSE := $(shell echo $(WEB_ICON_SIZES) | tr ' ' '\n' | sort -nr | tr '\n' ' ')
+WEB_BASE_ICONS := $(WEB_ICON_SIZES_REVERSE:%=novnc-%.png)
+.INTERMEDIATE: $(WEB_BASE_ICONS)
+novnc.ico: $(WEB_BASE_ICONS)
+ convert $(WEB_BASE_ICONS) "$@"
+
+# General conversion
novnc-%.png: novnc-icon.svg
- convert -density $$[`echo $* | cut -d x -f 1` * 90 / 48] \
- -background transparent "$<" "$@"
+ convert -depth 8 -background transparent \
+ -size $*x$* "$(lastword $^)" "$@"
+
+# iOS icons use their own SVG
+novnc-ios-%.png: novnc-ios-icon.svg
+ convert -depth 8 -background transparent \
+ -size $*x$* "$(lastword $^)" "$@"
+
+# The smallest sizes are generated using a different SVG
+novnc-16.png novnc-24.png novnc-32.png: novnc-icon-sm.svg
clean:
rm -f *.png
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-120.png b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-120.png
new file mode 100644
index 000000000..8da7bab3d
Binary files /dev/null and b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-120.png differ
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-152.png b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-152.png
new file mode 100644
index 000000000..60b2bcef5
Binary files /dev/null and b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-152.png differ
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-167.png b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-167.png
new file mode 100644
index 000000000..98fade2e2
Binary files /dev/null and b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-167.png differ
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-180.png b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-180.png
new file mode 100644
index 000000000..5d24df70a
Binary files /dev/null and b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-180.png differ
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-40.png b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-40.png
new file mode 100644
index 000000000..cf14894da
Binary files /dev/null and b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-40.png differ
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-58.png b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-58.png
new file mode 100644
index 000000000..f6dfbebd2
Binary files /dev/null and b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-58.png differ
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-60.png b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-60.png
new file mode 100644
index 000000000..8cda29530
Binary files /dev/null and b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-60.png differ
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-80.png b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-80.png
new file mode 100644
index 000000000..6c417c47e
Binary files /dev/null and b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-80.png differ
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-87.png b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-87.png
new file mode 100644
index 000000000..4377d874b
Binary files /dev/null and b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-87.png differ
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-icon.svg b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-icon.svg
new file mode 100644
index 000000000..009452ac6
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc-ios-icon.svg
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc.ico b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc.ico
new file mode 100644
index 000000000..c3bc58e38
Binary files /dev/null and b/emhttp/plugins/dynamix.vm.manager/novnc/app/images/icons/novnc.ico differ
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/el.json b/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/el.json
index f801251c5..4df3e03c4 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/el.json
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/el.json
@@ -1,4 +1,5 @@
{
+ "HTTPS is required for full functionality": "Το HTTPS είναι απαιτούμενο για πλήρη λειτουργικότητα",
"Connecting...": "Συνδέεται...",
"Disconnecting...": "Aποσυνδέεται...",
"Reconnecting...": "Επανασυνδέεται...",
@@ -7,19 +8,15 @@
"Connected (encrypted) to ": "Συνδέθηκε (κρυπτογραφημένα) με το ",
"Connected (unencrypted) to ": "Συνδέθηκε (μη κρυπτογραφημένα) με το ",
"Something went wrong, connection is closed": "Κάτι πήγε στραβά, η σύνδεση διακόπηκε",
+ "Failed to connect to server": "Αποτυχία στη σύνδεση με το διακομιστή",
"Disconnected": "Αποσυνδέθηκε",
"New connection has been rejected with reason: ": "Η νέα σύνδεση απορρίφθηκε διότι: ",
"New connection has been rejected": "Η νέα σύνδεση απορρίφθηκε ",
- "Password is required": "Απαιτείται ο κωδικός πρόσβασης",
+ "Credentials are required": "Απαιτούνται διαπιστευτήρια",
"noVNC encountered an error:": "το noVNC αντιμετώπισε ένα σφάλμα:",
"Hide/Show the control bar": "Απόκρυψη/Εμφάνιση γραμμής ελέγχου",
+ "Drag": "Σύρσιμο",
"Move/Drag Viewport": "Μετακίνηση/Σύρσιμο Θεατού πεδίου",
- "viewport drag": "σύρσιμο θεατού πεδίου",
- "Active Mouse Button": "Ενεργό Πλήκτρο Ποντικιού",
- "No mousebutton": "Χωρίς Πλήκτρο Ποντικιού",
- "Left mousebutton": "Αριστερό Πλήκτρο Ποντικιού",
- "Middle mousebutton": "Μεσαίο Πλήκτρο Ποντικιού",
- "Right mousebutton": "Δεξί Πλήκτρο Ποντικιού",
"Keyboard": "Πληκτρολόγιο",
"Show Keyboard": "Εμφάνιση Πληκτρολογίου",
"Extra keys": "Επιπλέον πλήκτρα",
@@ -28,6 +25,8 @@
"Toggle Ctrl": "Εναλλαγή Ctrl",
"Alt": "Alt",
"Toggle Alt": "Εναλλαγή Alt",
+ "Toggle Windows": "Εναλλαγή Παράθυρων",
+ "Windows": "Παράθυρα",
"Send Tab": "Αποστολή Tab",
"Tab": "Tab",
"Esc": "Esc",
@@ -41,8 +40,7 @@
"Reboot": "Επανεκκίνηση",
"Reset": "Επαναφορά",
"Clipboard": "Πρόχειρο",
- "Clear": "Καθάρισμα",
- "Fullscreen": "Πλήρης Οθόνη",
+ "Edit clipboard content in the textarea below.": "Επεξεργαστείτε το περιεχόμενο του πρόχειρου στην περιοχή κειμένου παρακάτω.",
"Settings": "Ρυθμίσεις",
"Shared Mode": "Κοινόχρηστη Λειτουργία",
"View Only": "Μόνο Θέαση",
@@ -52,6 +50,8 @@
"Local Scaling": "Τοπική Κλιμάκωση",
"Remote Resizing": "Απομακρυσμένη Αλλαγή μεγέθους",
"Advanced": "Για προχωρημένους",
+ "Quality:": "Ποιότητα:",
+ "Compression level:": "Επίπεδο συμπίεσης:",
"Repeater ID:": "Repeater ID:",
"WebSocket": "WebSocket",
"Encrypt": "Κρυπτογράφηση",
@@ -60,10 +60,20 @@
"Path:": "Διαδρομή:",
"Automatic Reconnect": "Αυτόματη επανασύνδεση",
"Reconnect Delay (ms):": "Καθυστέρηση επανασύνδεσης (ms):",
+ "Show Dot when No Cursor": "Εμφάνιση Τελείας όταν δεν υπάρχει Δρομέας",
"Logging:": "Καταγραφή:",
+ "Version:": "Έκδοση:",
"Disconnect": "Αποσύνδεση",
"Connect": "Σύνδεση",
+ "Server identity": "Ταυτότητα Διακομιστή",
+ "The server has provided the following identifying information:": "Ο διακομιστής παρείχε την ακόλουθη πληροφορία ταυτοποίησης:",
+ "Fingerprint:": "Δακτυλικό αποτύπωμα:",
+ "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Παρακαλώ επαληθεύσετε ότι η πληροφορία είναι σωστή και πιέστε \"Αποδοχή\". Αλλιώς πιέστε \"Απόρριψη\".",
+ "Approve": "Αποδοχή",
+ "Reject": "Απόρριψη",
+ "Credentials": "Διαπιστευτήρια",
+ "Username:": "Κωδικός Χρήστη:",
"Password:": "Κωδικός Πρόσβασης:",
- "Cancel": "Ακύρωση",
- "Canvas not supported.": "Δεν υποστηρίζεται το στοιχείο Canvas"
+ "Send Credentials": "Αποστολή Διαπιστευτηρίων",
+ "Cancel": "Ακύρωση"
}
\ No newline at end of file
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/es.json b/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/es.json
index 23f23f497..b9e663a3d 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/es.json
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/es.json
@@ -4,9 +4,9 @@
"Connected (unencrypted) to ": "Conectado (sin encriptación) a",
"Disconnecting...": "Desconectando...",
"Disconnected": "Desconectado",
- "Must set host": "Debes configurar el host",
+ "Must set host": "Se debe configurar el host",
"Reconnecting...": "Reconectando...",
- "Password is required": "Contraseña es obligatoria",
+ "Password is required": "La contraseña es obligatoria",
"Disconnect timeout": "Tiempo de desconexión agotado",
"noVNC encountered an error:": "noVNC ha encontrado un error:",
"Hide/Show the control bar": "Ocultar/Mostrar la barra de control",
@@ -41,6 +41,7 @@
"Clear": "Vaciar",
"Fullscreen": "Pantalla Completa",
"Settings": "Configuraciones",
+ "Encrypt": "Encriptar",
"Shared Mode": "Modo Compartido",
"View Only": "Solo visualización",
"Clip to Window": "Recortar al tamaño de la ventana",
@@ -51,18 +52,17 @@
"Remote Resizing": "Cambio de tamaño remoto",
"Advanced": "Avanzado",
"Local Cursor": "Cursor Local",
- "Repeater ID:": "ID del Repetidor",
+ "Repeater ID:": "ID del Repetidor:",
"WebSocket": "WebSocket",
- "Encrypt": "",
- "Host:": "Host",
- "Port:": "Puesto",
- "Path:": "Ruta",
+ "Host:": "Host:",
+ "Port:": "Puerto:",
+ "Path:": "Ruta:",
"Automatic Reconnect": "Reconexión automática",
- "Reconnect Delay (ms):": "Retraso en la reconexión (ms)",
- "Logging:": "Logging",
+ "Reconnect Delay (ms):": "Retraso en la reconexión (ms):",
+ "Logging:": "Registrando:",
"Disconnect": "Desconectar",
"Connect": "Conectar",
- "Password:": "Contraseña",
+ "Password:": "Contraseña:",
"Cancel": "Cancelar",
- "Canvas not supported.": "Canvas no está soportado"
+ "Canvas not supported.": "Canvas no soportado."
}
\ No newline at end of file
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/fr.json b/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/fr.json
new file mode 100644
index 000000000..c0eeec7d3
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/fr.json
@@ -0,0 +1,72 @@
+{
+ "Connecting...": "En cours de connexion...",
+ "Disconnecting...": "Déconnexion en cours...",
+ "Reconnecting...": "Reconnexion en cours...",
+ "Internal error": "Erreur interne",
+ "Must set host": "Doit définir l'hôte",
+ "Connected (encrypted) to ": "Connecté (chiffré) à ",
+ "Connected (unencrypted) to ": "Connecté (non chiffré) à ",
+ "Something went wrong, connection is closed": "Quelque chose s'est mal passé, la connexion a été fermée",
+ "Failed to connect to server": "Échec de connexion au serveur",
+ "Disconnected": "Déconnecté",
+ "New connection has been rejected with reason: ": "Une nouvelle connexion a été rejetée avec motif : ",
+ "New connection has been rejected": "Une nouvelle connexion a été rejetée",
+ "Credentials are required": "Les identifiants sont requis",
+ "noVNC encountered an error:": "noVNC a rencontré une erreur :",
+ "Hide/Show the control bar": "Masquer/Afficher la barre de contrôle",
+ "Drag": "Faire glisser",
+ "Move/Drag Viewport": "Déplacer/faire glisser le Viewport",
+ "Keyboard": "Clavier",
+ "Show Keyboard": "Afficher le clavier",
+ "Extra keys": "Touches supplémentaires",
+ "Show Extra Keys": "Afficher les touches supplémentaires",
+ "Ctrl": "Ctrl",
+ "Toggle Ctrl": "Basculer Ctrl",
+ "Alt": "Alt",
+ "Toggle Alt": "Basculer Alt",
+ "Toggle Windows": "Basculer Windows",
+ "Windows": "Windows",
+ "Send Tab": "Envoyer l'onglet",
+ "Tab": "l'onglet",
+ "Esc": "Esc",
+ "Send Escape": "Envoyer Escape",
+ "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+ "Send Ctrl-Alt-Del": "Envoyer Ctrl-Alt-Del",
+ "Shutdown/Reboot": "Arrêter/Redémarrer",
+ "Shutdown/Reboot...": "Arrêter/Redémarrer...",
+ "Power": "Alimentation",
+ "Shutdown": "Arrêter",
+ "Reboot": "Redémarrer",
+ "Reset": "Réinitialiser",
+ "Clipboard": "Presse-papiers",
+ "Clear": "Effacer",
+ "Fullscreen": "Plein écran",
+ "Settings": "Paramètres",
+ "Shared Mode": "Mode partagé",
+ "View Only": "Afficher uniquement",
+ "Clip to Window": "Clip à fenêtre",
+ "Scaling Mode:": "Mode mise à l'échelle :",
+ "None": "Aucun",
+ "Local Scaling": "Mise à l'échelle locale",
+ "Remote Resizing": "Redimensionnement à distance",
+ "Advanced": "Avancé",
+ "Quality:": "Qualité :",
+ "Compression level:": "Niveau de compression :",
+ "Repeater ID:": "ID Répéteur :",
+ "WebSocket": "WebSocket",
+ "Encrypt": "Chiffrer",
+ "Host:": "Hôte :",
+ "Port:": "Port :",
+ "Path:": "Chemin :",
+ "Automatic Reconnect": "Reconnecter automatiquemen",
+ "Reconnect Delay (ms):": "Délai de reconnexion (ms) :",
+ "Show Dot when No Cursor": "Afficher le point lorsqu'il n'y a pas de curseur",
+ "Logging:": "Se connecter :",
+ "Version:": "Version :",
+ "Disconnect": "Déconnecter",
+ "Connect": "Connecter",
+ "Username:": "Nom d'utilisateur :",
+ "Password:": "Mot de passe :",
+ "Send Credentials": "Envoyer les identifiants",
+ "Cancel": "Annuler"
+}
\ No newline at end of file
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/it.json b/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/it.json
new file mode 100644
index 000000000..18a7f7447
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/it.json
@@ -0,0 +1,68 @@
+{
+ "Connecting...": "Connessione in corso...",
+ "Disconnecting...": "Disconnessione...",
+ "Reconnecting...": "Riconnessione...",
+ "Internal error": "Errore interno",
+ "Must set host": "Devi impostare l'host",
+ "Connected (encrypted) to ": "Connesso (crittografato) a ",
+ "Connected (unencrypted) to ": "Connesso (non crittografato) a",
+ "Something went wrong, connection is closed": "Qualcosa è andato storto, la connessione è stata chiusa",
+ "Failed to connect to server": "Impossibile connettersi al server",
+ "Disconnected": "Disconnesso",
+ "New connection has been rejected with reason: ": "La nuova connessione è stata rifiutata con motivo: ",
+ "New connection has been rejected": "La nuova connessione è stata rifiutata",
+ "Credentials are required": "Le credenziali sono obbligatorie",
+ "noVNC encountered an error:": "noVNC ha riscontrato un errore:",
+ "Hide/Show the control bar": "Nascondi/Mostra la barra di controllo",
+ "Keyboard": "Tastiera",
+ "Show Keyboard": "Mostra tastiera",
+ "Extra keys": "Tasti Aggiuntivi",
+ "Show Extra Keys": "Mostra Tasti Aggiuntivi",
+ "Ctrl": "Ctrl",
+ "Toggle Ctrl": "Tieni premuto Ctrl",
+ "Alt": "Alt",
+ "Toggle Alt": "Tieni premuto Alt",
+ "Toggle Windows": "Tieni premuto Windows",
+ "Windows": "Windows",
+ "Send Tab": "Invia Tab",
+ "Tab": "Tab",
+ "Esc": "Esc",
+ "Send Escape": "Invia Esc",
+ "Ctrl+Alt+Del": "Ctrl+Alt+Canc",
+ "Send Ctrl-Alt-Del": "Invia Ctrl-Alt-Canc",
+ "Shutdown/Reboot": "Spegnimento/Riavvio",
+ "Shutdown/Reboot...": "Spegnimento/Riavvio...",
+ "Power": "Alimentazione",
+ "Shutdown": "Spegnimento",
+ "Reboot": "Riavvio",
+ "Reset": "Reset",
+ "Clipboard": "Clipboard",
+ "Clear": "Pulisci",
+ "Fullscreen": "Schermo intero",
+ "Settings": "Impostazioni",
+ "Shared Mode": "Modalità condivisa",
+ "View Only": "Sola Visualizzazione",
+ "Scaling Mode:": "Modalità di ridimensionamento:",
+ "None": "Nessuna",
+ "Local Scaling": "Ridimensionamento Locale",
+ "Remote Resizing": "Ridimensionamento Remoto",
+ "Advanced": "Avanzate",
+ "Quality:": "Qualità:",
+ "Compression level:": "Livello Compressione:",
+ "Repeater ID:": "ID Ripetitore:",
+ "WebSocket": "WebSocket",
+ "Encrypt": "Crittografa",
+ "Host:": "Host:",
+ "Port:": "Porta:",
+ "Path:": "Percorso:",
+ "Automatic Reconnect": "Riconnessione Automatica",
+ "Reconnect Delay (ms):": "Ritardo Riconnessione (ms):",
+ "Show Dot when No Cursor": "Mostra Punto quando Nessun Cursore",
+ "Version:": "Versione:",
+ "Disconnect": "Disconnetti",
+ "Connect": "Connetti",
+ "Username:": "Utente:",
+ "Password:": "Password:",
+ "Send Credentials": "Invia Credenziale",
+ "Cancel": "Annulla"
+}
\ No newline at end of file
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/ja.json b/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/ja.json
index e5fe3401f..70fd7a5d1 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/ja.json
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/ja.json
@@ -1,4 +1,5 @@
{
+ "HTTPS is required for full functionality": "すべての機能を使用するにはHTTPS接続が必要です",
"Connecting...": "接続しています...",
"Disconnecting...": "切断しています...",
"Reconnecting...": "再接続しています...",
@@ -6,30 +7,25 @@
"Must set host": "ホストを設定する必要があります",
"Connected (encrypted) to ": "接続しました (暗号化済み): ",
"Connected (unencrypted) to ": "接続しました (暗号化されていません): ",
- "Something went wrong, connection is closed": "何かが問題で、接続が閉じられました",
+ "Something went wrong, connection is closed": "何らかの問題で、接続が閉じられました",
"Failed to connect to server": "サーバーへの接続に失敗しました",
"Disconnected": "切断しました",
"New connection has been rejected with reason: ": "新規接続は次の理由で拒否されました: ",
"New connection has been rejected": "新規接続は拒否されました",
- "Password is required": "パスワードが必要です",
+ "Credentials are required": "資格情報が必要です",
"noVNC encountered an error:": "noVNC でエラーが発生しました:",
"Hide/Show the control bar": "コントロールバーを隠す/表示する",
+ "Drag": "ドラッグ",
"Move/Drag Viewport": "ビューポートを移動/ドラッグ",
- "viewport drag": "ビューポートをドラッグ",
- "Active Mouse Button": "アクティブなマウスボタン",
- "No mousebutton": "マウスボタンなし",
- "Left mousebutton": "左マウスボタン",
- "Middle mousebutton": "中マウスボタン",
- "Right mousebutton": "右マウスボタン",
"Keyboard": "キーボード",
"Show Keyboard": "キーボードを表示",
"Extra keys": "追加キー",
"Show Extra Keys": "追加キーを表示",
"Ctrl": "Ctrl",
- "Toggle Ctrl": "Ctrl キーを切り替え",
+ "Toggle Ctrl": "Ctrl キーをトグル",
"Alt": "Alt",
- "Toggle Alt": "Alt キーを切り替え",
- "Toggle Windows": "Windows キーを切り替え",
+ "Toggle Alt": "Alt キーをトグル",
+ "Toggle Windows": "Windows キーをトグル",
"Windows": "Windows",
"Send Tab": "Tab キーを送信",
"Tab": "Tab",
@@ -44,17 +40,19 @@
"Reboot": "再起動",
"Reset": "リセット",
"Clipboard": "クリップボード",
- "Clear": "クリア",
- "Fullscreen": "全画面表示",
+ "Edit clipboard content in the textarea below.": "以下の入力欄からクリップボードの内容を編集できます。",
+ "Full Screen": "全画面表示",
"Settings": "設定",
"Shared Mode": "共有モード",
- "View Only": "表示のみ",
+ "View Only": "表示専用",
"Clip to Window": "ウィンドウにクリップ",
"Scaling Mode:": "スケーリングモード:",
"None": "なし",
"Local Scaling": "ローカルスケーリング",
"Remote Resizing": "リモートでリサイズ",
"Advanced": "高度",
+ "Quality:": "品質:",
+ "Compression level:": "圧縮レベル:",
"Repeater ID:": "リピーター ID:",
"WebSocket": "WebSocket",
"Encrypt": "暗号化",
@@ -63,11 +61,20 @@
"Path:": "パス:",
"Automatic Reconnect": "自動再接続",
"Reconnect Delay (ms):": "再接続する遅延 (ミリ秒):",
- "Show Dot when No Cursor": "カーソルがないときにドットを表示",
+ "Show Dot when No Cursor": "カーソルがないときにドットを表示する",
"Logging:": "ロギング:",
+ "Version:": "バージョン:",
"Disconnect": "切断",
"Connect": "接続",
+ "Server identity": "サーバーの識別情報",
+ "The server has provided the following identifying information:": "サーバーは以下の識別情報を提供しています:",
+ "Fingerprint:": "フィンガープリント:",
+ "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "この情報が正しい場合は「承認」を、そうでない場合は「拒否」を押してください。",
+ "Approve": "承認",
+ "Reject": "拒否",
+ "Credentials": "資格情報",
+ "Username:": "ユーザー名:",
"Password:": "パスワード:",
- "Send Password": "パスワードを送信",
+ "Send Credentials": "資格情報を送信",
"Cancel": "キャンセル"
}
\ No newline at end of file
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/pt_BR.json b/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/pt_BR.json
new file mode 100644
index 000000000..aa130f764
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/pt_BR.json
@@ -0,0 +1,72 @@
+{
+ "Connecting...": "Conectando...",
+ "Disconnecting...": "Desconectando...",
+ "Reconnecting...": "Reconectando...",
+ "Internal error": "Erro interno",
+ "Must set host": "É necessário definir o host",
+ "Connected (encrypted) to ": "Conectado (com criptografia) a ",
+ "Connected (unencrypted) to ": "Conectado (sem criptografia) a ",
+ "Something went wrong, connection is closed": "Algo deu errado. A conexão foi encerrada.",
+ "Failed to connect to server": "Falha ao conectar-se ao servidor",
+ "Disconnected": "Desconectado",
+ "New connection has been rejected with reason: ": "A nova conexão foi rejeitada pelo motivo: ",
+ "New connection has been rejected": "A nova conexão foi rejeitada",
+ "Credentials are required": "Credenciais são obrigatórias",
+ "noVNC encountered an error:": "O noVNC encontrou um erro:",
+ "Hide/Show the control bar": "Esconder/mostrar a barra de controles",
+ "Drag": "Arrastar",
+ "Move/Drag Viewport": "Mover/arrastar a janela",
+ "Keyboard": "Teclado",
+ "Show Keyboard": "Mostrar teclado",
+ "Extra keys": "Teclas adicionais",
+ "Show Extra Keys": "Mostar teclas adicionais",
+ "Ctrl": "Ctrl",
+ "Toggle Ctrl": "Pressionar/soltar Ctrl",
+ "Alt": "Alt",
+ "Toggle Alt": "Pressionar/soltar Alt",
+ "Toggle Windows": "Pressionar/soltar Windows",
+ "Windows": "Windows",
+ "Send Tab": "Enviar Tab",
+ "Tab": "Tab",
+ "Esc": "Esc",
+ "Send Escape": "Enviar Esc",
+ "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+ "Send Ctrl-Alt-Del": "Enviar Ctrl-Alt-Del",
+ "Shutdown/Reboot": "Desligar/reiniciar",
+ "Shutdown/Reboot...": "Desligar/reiniciar...",
+ "Power": "Ligar",
+ "Shutdown": "Desligar",
+ "Reboot": "Reiniciar",
+ "Reset": "Reiniciar (forçado)",
+ "Clipboard": "Área de transferência",
+ "Clear": "Limpar",
+ "Fullscreen": "Tela cheia",
+ "Settings": "Configurações",
+ "Shared Mode": "Modo compartilhado",
+ "View Only": "Apenas visualizar",
+ "Clip to Window": "Recortar à janela",
+ "Scaling Mode:": "Modo de dimensionamento:",
+ "None": "Nenhum",
+ "Local Scaling": "Local",
+ "Remote Resizing": "Remoto",
+ "Advanced": "Avançado",
+ "Quality:": "Qualidade:",
+ "Compression level:": "Nível de compressão:",
+ "Repeater ID:": "ID do repetidor:",
+ "WebSocket": "WebSocket",
+ "Encrypt": "Criptografar",
+ "Host:": "Host:",
+ "Port:": "Porta:",
+ "Path:": "Caminho:",
+ "Automatic Reconnect": "Reconexão automática",
+ "Reconnect Delay (ms):": "Atraso da reconexão (ms)",
+ "Show Dot when No Cursor": "Mostrar ponto quando não há cursor",
+ "Logging:": "Registros:",
+ "Version:": "Versão:",
+ "Disconnect": "Desconectar",
+ "Connect": "Conectar",
+ "Username:": "Nome de usuário:",
+ "Password:": "Senha:",
+ "Send Credentials": "Enviar credenciais",
+ "Cancel": "Cancelar"
+}
\ No newline at end of file
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/ru.json b/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/ru.json
index 52e57f37f..cab97396e 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/ru.json
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/ru.json
@@ -9,26 +9,21 @@
"Something went wrong, connection is closed": "Что-то пошло не так, подключение разорвано",
"Failed to connect to server": "Ошибка подключения к серверу",
"Disconnected": "Отключено",
- "New connection has been rejected with reason: ": "Подключиться не удалось: ",
- "New connection has been rejected": "Подключиться не удалось",
- "Password is required": "Требуется пароль",
+ "New connection has been rejected with reason: ": "Новое соединение отклонено по причине: ",
+ "New connection has been rejected": "Новое соединение отклонено",
+ "Credentials are required": "Требуются учетные данные",
"noVNC encountered an error:": "Ошибка noVNC: ",
"Hide/Show the control bar": "Скрыть/Показать контрольную панель",
+ "Drag": "Переместить",
"Move/Drag Viewport": "Переместить окно",
- "viewport drag": "Переместить окно",
- "Active Mouse Button": "Активировать кнопки мыши",
- "No mousebutton": "Отключить кнопки мыши",
- "Left mousebutton": "Левая кнопка мыши",
- "Middle mousebutton": "Средняя кнопка мыши",
- "Right mousebutton": "Правая кнопка мыши",
"Keyboard": "Клавиатура",
"Show Keyboard": "Показать клавиатуру",
- "Extra keys": "Доп. кнопки",
- "Show Extra Keys": "Показать дополнительные кнопки",
+ "Extra keys": "Дополнительные Кнопки",
+ "Show Extra Keys": "Показать Дополнительные Кнопки",
"Ctrl": "Ctrl",
- "Toggle Ctrl": "Передать нажатие Ctrl",
+ "Toggle Ctrl": "Переключение нажатия Ctrl",
"Alt": "Alt",
- "Toggle Alt": "Передать нажатие Alt",
+ "Toggle Alt": "Переключение нажатия Alt",
"Toggle Windows": "Переключение вкладок",
"Windows": "Вкладка",
"Send Tab": "Передать нажатие Tab",
@@ -48,13 +43,15 @@
"Fullscreen": "Во весь экран",
"Settings": "Настройки",
"Shared Mode": "Общий режим",
- "View Only": "Просмотр",
+ "View Only": "Только Просмотр",
"Clip to Window": "В окно",
"Scaling Mode:": "Масштаб:",
"None": "Нет",
"Local Scaling": "Локльный масштаб",
- "Remote Resizing": "Удаленный масштаб",
+ "Remote Resizing": "Удаленная перенастройка размера",
"Advanced": "Дополнительно",
+ "Quality:": "Качество",
+ "Compression level:": "Уровень Сжатия",
"Repeater ID:": "Идентификатор ID:",
"WebSocket": "WebSocket",
"Encrypt": "Шифрование",
@@ -65,9 +62,11 @@
"Reconnect Delay (ms):": "Задержка переподключения (мс):",
"Show Dot when No Cursor": "Показать точку вместо курсора",
"Logging:": "Лог:",
+ "Version:": "Версия",
"Disconnect": "Отключение",
"Connect": "Подключение",
+ "Username:": "Имя Пользователя",
"Password:": "Пароль:",
- "Send Password": "Пароль: ",
+ "Send Credentials": "Передача Учетных Данных",
"Cancel": "Выход"
}
\ No newline at end of file
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/sv.json b/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/sv.json
index e46df45b5..80a400bfa 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/sv.json
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/sv.json
@@ -1,9 +1,11 @@
{
+ "Running without HTTPS is not recommended, crashes or other issues are likely.": "Det är ej rekommenderat att köra utan HTTPS, krascher och andra problem är troliga.",
"Connecting...": "Ansluter...",
"Disconnecting...": "Kopplar ner...",
"Reconnecting...": "Återansluter...",
"Internal error": "Internt fel",
"Must set host": "Du måste specifiera en värd",
+ "Failed to connect to server: ": "Misslyckades att ansluta till servern: ",
"Connected (encrypted) to ": "Ansluten (krypterat) till ",
"Connected (unencrypted) to ": "Ansluten (okrypterat) till ",
"Something went wrong, connection is closed": "Något gick fel, anslutningen avslutades",
@@ -39,8 +41,8 @@
"Reboot": "Boota om",
"Reset": "Återställ",
"Clipboard": "Urklipp",
- "Clear": "Rensa",
- "Fullscreen": "Fullskärm",
+ "Edit clipboard content in the textarea below.": "Redigera urklippets innehåll i fältet nedan.",
+ "Full Screen": "Fullskärm",
"Settings": "Inställningar",
"Shared Mode": "Delat Läge",
"View Only": "Endast Visning",
@@ -65,6 +67,13 @@
"Version:": "Version:",
"Disconnect": "Koppla från",
"Connect": "Anslut",
+ "Server identity": "Server-identitet",
+ "The server has provided the following identifying information:": "Servern har gett följande identifierande information:",
+ "Fingerprint:": "Fingeravtryck:",
+ "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Kontrollera att informationen är korrekt och tryck sedan \"Godkänn\". Tryck annars \"Neka\".",
+ "Approve": "Godkänn",
+ "Reject": "Neka",
+ "Credentials": "Användaruppgifter",
"Username:": "Användarnamn:",
"Password:": "Lösenord:",
"Send Credentials": "Skicka Användaruppgifter",
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/zh_CN.json b/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/zh_CN.json
index f0aea9af3..3679eaddd 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/zh_CN.json
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/app/locale/zh_CN.json
@@ -1,69 +1,69 @@
{
"Connecting...": "连接中...",
+ "Connected (encrypted) to ": "已连接(已加密)到",
+ "Connected (unencrypted) to ": "已连接(未加密)到",
"Disconnecting...": "正在断开连接...",
- "Reconnecting...": "重新连接中...",
- "Internal error": "内部错误",
- "Must set host": "请提供主机名",
- "Connected (encrypted) to ": "已连接到(加密)",
- "Connected (unencrypted) to ": "已连接到(未加密)",
- "Something went wrong, connection is closed": "发生错误,连接已关闭",
- "Failed to connect to server": "无法连接到服务器",
"Disconnected": "已断开连接",
- "New connection has been rejected with reason: ": "连接被拒绝,原因:",
- "New connection has been rejected": "连接被拒绝",
+ "Must set host": "必须设置主机",
+ "Reconnecting...": "重新连接中...",
"Password is required": "请提供密码",
+ "Disconnect timeout": "超时断开",
"noVNC encountered an error:": "noVNC 遇到一个错误:",
"Hide/Show the control bar": "显示/隐藏控制栏",
- "Move/Drag Viewport": "拖放显示范围",
- "viewport drag": "显示范围拖放",
- "Active Mouse Button": "启动鼠标按鍵",
- "No mousebutton": "禁用鼠标按鍵",
- "Left mousebutton": "鼠标左鍵",
- "Middle mousebutton": "鼠标中鍵",
- "Right mousebutton": "鼠标右鍵",
+ "Move/Drag Viewport": "移动/拖动窗口",
+ "viewport drag": "窗口拖动",
+ "Active Mouse Button": "启动鼠标按键",
+ "No mousebutton": "禁用鼠标按键",
+ "Left mousebutton": "鼠标左键",
+ "Middle mousebutton": "鼠标中键",
+ "Right mousebutton": "鼠标右键",
"Keyboard": "键盘",
"Show Keyboard": "显示键盘",
"Extra keys": "额外按键",
"Show Extra Keys": "显示额外按键",
"Ctrl": "Ctrl",
"Toggle Ctrl": "切换 Ctrl",
+ "Edit clipboard content in the textarea below.": "在下面的文本区域中编辑剪贴板内容。",
"Alt": "Alt",
"Toggle Alt": "切换 Alt",
"Send Tab": "发送 Tab 键",
"Tab": "Tab",
"Esc": "Esc",
"Send Escape": "发送 Escape 键",
- "Ctrl+Alt+Del": "Ctrl-Alt-Del",
- "Send Ctrl-Alt-Del": "发送 Ctrl-Alt-Del 键",
- "Shutdown/Reboot": "关机/重新启动",
- "Shutdown/Reboot...": "关机/重新启动...",
+ "Ctrl+Alt+Del": "Ctrl+Alt+Del",
+ "Send Ctrl-Alt-Del": "发送 Ctrl+Alt+Del 键",
+ "Shutdown/Reboot": "关机/重启",
+ "Shutdown/Reboot...": "关机/重启...",
"Power": "电源",
"Shutdown": "关机",
- "Reboot": "重新启动",
+ "Reboot": "重启",
"Reset": "重置",
"Clipboard": "剪贴板",
"Clear": "清除",
"Fullscreen": "全屏",
"Settings": "设置",
+ "Encrypt": "加密",
"Shared Mode": "分享模式",
"View Only": "仅查看",
"Clip to Window": "限制/裁切窗口大小",
"Scaling Mode:": "缩放模式:",
"None": "无",
"Local Scaling": "本地缩放",
+ "Local Downscaling": "降低本地尺寸",
"Remote Resizing": "远程调整大小",
"Advanced": "高级",
+ "Local Cursor": "本地光标",
"Repeater ID:": "中继站 ID",
"WebSocket": "WebSocket",
- "Encrypt": "加密",
"Host:": "主机:",
"Port:": "端口:",
"Path:": "路径:",
"Automatic Reconnect": "自动重新连接",
"Reconnect Delay (ms):": "重新连接间隔 (ms):",
"Logging:": "日志级别:",
- "Disconnect": "中断连接",
+ "Disconnect": "断开连接",
"Connect": "连接",
"Password:": "密码:",
- "Cancel": "取消"
+ "Cancel": "取消",
+ "Canvas not supported.": "不支持 Canvas。"
}
\ No newline at end of file
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/localization.js b/emhttp/plugins/dynamix.vm.manager/novnc/app/localization.js
index 100901c9d..7d7e6e6af 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/app/localization.js
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/app/localization.js
@@ -16,13 +16,19 @@ export class Localizer {
this.language = 'en';
// Current dictionary of translations
- this.dictionary = undefined;
+ this._dictionary = undefined;
}
// Configure suitable language based on user preferences
- setup(supportedLanguages) {
+ async setup(supportedLanguages, baseURL) {
this.language = 'en'; // Default: US English
+ this._dictionary = undefined;
+ this._setupLanguage(supportedLanguages);
+ await this._setupDictionary(baseURL);
+ }
+
+ _setupLanguage(supportedLanguages) {
/*
* Navigator.languages only available in Chrome (32+) and FireFox (32+)
* Fall back to navigator.language for other browsers
@@ -40,12 +46,6 @@ export class Localizer {
.replace("_", "-")
.split("-");
- // Built-in default?
- if ((userLang[0] === 'en') &&
- ((userLang[1] === undefined) || (userLang[1] === 'us'))) {
- return;
- }
-
// First pass: perfect match
for (let j = 0; j < supportedLanguages.length; j++) {
const supLang = supportedLanguages[j]
@@ -64,7 +64,12 @@ export class Localizer {
return;
}
- // Second pass: fallback
+ // Second pass: English fallback
+ if (userLang[0] === 'en') {
+ return;
+ }
+
+ // Third pass pass: other fallback
for (let j = 0;j < supportedLanguages.length;j++) {
const supLang = supportedLanguages[j]
.toLowerCase()
@@ -84,10 +89,32 @@ export class Localizer {
}
}
+ async _setupDictionary(baseURL) {
+ if (baseURL) {
+ if (!baseURL.endsWith("/")) {
+ baseURL = baseURL + "/";
+ }
+ } else {
+ baseURL = "";
+ }
+
+ if (this.language === "en") {
+ return;
+ }
+
+ let response = await fetch(baseURL + this.language + ".json");
+ if (!response.ok) {
+ throw Error("" + response.status + " " + response.statusText);
+ }
+
+ this._dictionary = await response.json();
+ }
+
// Retrieve localised text
get(id) {
- if (typeof this.dictionary !== 'undefined' && this.dictionary[id]) {
- return this.dictionary[id];
+ if (typeof this._dictionary !== 'undefined' &&
+ this._dictionary[id]) {
+ return this._dictionary[id];
} else {
return id;
}
@@ -103,13 +130,20 @@ export class Localizer {
return items.indexOf(searchElement) !== -1;
}
+ function translateString(str) {
+ // We assume surrounding whitespace, and whitespace around line
+ // breaks is just for source formatting
+ str = str.split("\n").map(s => s.trim()).join(" ").trim();
+ return self.get(str);
+ }
+
function translateAttribute(elem, attr) {
- const str = self.get(elem.getAttribute(attr));
+ const str = translateString(elem.getAttribute(attr));
elem.setAttribute(attr, str);
}
function translateTextNode(node) {
- const str = self.get(node.data.trim());
+ const str = translateString(node.data);
node.data = str;
}
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/styles/base.css b/emhttp/plugins/dynamix.vm.manager/novnc/app/styles/base.css
index fd78b79c7..f83ad4b93 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/app/styles/base.css
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/app/styles/base.css
@@ -19,10 +19,23 @@
* 10000: Max (used for polyfills)
*/
+/*
+ * State variables (set on :root):
+ *
+ * noVNC_loading: Page is still loading
+ * noVNC_connecting: Connecting to server
+ * noVNC_reconnecting: Re-establishing a connection
+ * noVNC_connected: Connected to server (most common state)
+ * noVNC_disconnecting: Disconnecting from server
+ */
+
+:root {
+ font-family: sans-serif;
+}
+
body {
margin:0;
padding:0;
- font-family: Helvetica;
/*Background image with light grey curve.*/
background-color:#494949;
background-repeat:no-repeat;
@@ -78,144 +91,6 @@ html {
50% { box-shadow: 60px 10px 0 rgba(255, 255, 255, 0); width: 10px; }
}
-/* ----------------------------------------
- * Input Elements
- * ----------------------------------------
- */
-
-input:not([type]),
-input[type=date],
-input[type=datetime-local],
-input[type=email],
-input[type=month],
-input[type=number],
-input[type=password],
-input[type=search],
-input[type=tel],
-input[type=text],
-input[type=time],
-input[type=url],
-input[type=week],
-textarea {
- /* Disable default rendering */
- -webkit-appearance: none;
- -moz-appearance: none;
- background: none;
-
- margin: 2px;
- padding: 2px;
- border: 1px solid rgb(192, 192, 192);
- border-radius: 5px;
- color: black;
- background: linear-gradient(to top, rgb(255, 255, 255) 80%, rgb(240, 240, 240));
-}
-
-input[type=button],
-input[type=color],
-input[type=reset],
-input[type=submit],
-select {
- /* Disable default rendering */
- -webkit-appearance: none;
- -moz-appearance: none;
- background: none;
-
- margin: 2px;
- padding: 2px;
- border: 1px solid rgb(192, 192, 192);
- border-bottom-width: 2px;
- border-radius: 5px;
- color: black;
- background: linear-gradient(to top, rgb(255, 255, 255), rgb(240, 240, 240));
-
- /* This avoids it jumping around when :active */
- vertical-align: middle;
-}
-
-input[type=button],
-input[type=color],
-input[type=reset],
-input[type=submit] {
- padding-left: 20px;
- padding-right: 20px;
-}
-
-option {
- color: black;
- background: white;
-}
-
-input:not([type]):focus,
-input[type=button]:focus,
-input[type=color]:focus,
-input[type=date]:focus,
-input[type=datetime-local]:focus,
-input[type=email]:focus,
-input[type=month]:focus,
-input[type=number]:focus,
-input[type=password]:focus,
-input[type=reset]:focus,
-input[type=search]:focus,
-input[type=submit]:focus,
-input[type=tel]:focus,
-input[type=text]:focus,
-input[type=time]:focus,
-input[type=url]:focus,
-input[type=week]:focus,
-select:focus,
-textarea:focus {
- box-shadow: 0px 0px 3px rgba(74, 144, 217, 0.5);
- border-color: rgb(74, 144, 217);
- outline: none;
-}
-
-input[type=button]::-moz-focus-inner,
-input[type=color]::-moz-focus-inner,
-input[type=reset]::-moz-focus-inner,
-input[type=submit]::-moz-focus-inner {
- border: none;
-}
-
-input:not([type]):disabled,
-input[type=button]:disabled,
-input[type=color]:disabled,
-input[type=date]:disabled,
-input[type=datetime-local]:disabled,
-input[type=email]:disabled,
-input[type=month]:disabled,
-input[type=number]:disabled,
-input[type=password]:disabled,
-input[type=reset]:disabled,
-input[type=search]:disabled,
-input[type=submit]:disabled,
-input[type=tel]:disabled,
-input[type=text]:disabled,
-input[type=time]:disabled,
-input[type=url]:disabled,
-input[type=week]:disabled,
-select:disabled,
-textarea:disabled {
- color: rgb(128, 128, 128);
- background: rgb(240, 240, 240);
-}
-
-input[type=button]:active,
-input[type=color]:active,
-input[type=reset]:active,
-input[type=submit]:active,
-select:active {
- border-bottom-width: 1px;
- margin-top: 3px;
-}
-
-:root:not(.noVNC_touch) input[type=button]:hover:not(:disabled),
-:root:not(.noVNC_touch) input[type=color]:hover:not(:disabled),
-:root:not(.noVNC_touch) input[type=reset]:hover:not(:disabled),
-:root:not(.noVNC_touch) input[type=submit]:hover:not(:disabled),
-:root:not(.noVNC_touch) select:hover:not(:disabled) {
- background: linear-gradient(to top, rgb(255, 255, 255), rgb(250, 250, 250));
-}
-
/* ----------------------------------------
* WebKit centering hacks
* ----------------------------------------
@@ -242,13 +117,15 @@ select:active {
pointer-events: auto;
}
.noVNC_vcenter {
- display: flex;
+ display: flex !important;
flex-direction: column;
justify-content: center;
position: fixed;
top: 0;
left: 0;
height: 100%;
+ margin: 0 !important;
+ padding: 0 !important;
pointer-events: none;
}
.noVNC_vcenter > * {
@@ -272,13 +149,20 @@ select:active {
#noVNC_fallback_error {
z-index: 1000;
visibility: hidden;
+ /* Put a dark background in front of everything but the error,
+ and don't let mouse events pass through */
+ background: rgba(0, 0, 0, 0.8);
+ pointer-events: all;
}
#noVNC_fallback_error.noVNC_open {
visibility: visible;
}
#noVNC_fallback_error > div {
- max-width: 90%;
+ max-width: calc(100vw - 30px - 30px);
+ max-height: calc(100vh - 30px - 30px);
+ overflow: auto;
+
padding: 15px;
transition: 0.5s ease-in-out;
@@ -317,7 +201,6 @@ select:active {
}
#noVNC_fallback_error .noVNC_stack {
- max-height: 50vh;
padding: 10px;
margin: 10px;
font-size: 0.8em;
@@ -361,6 +244,9 @@ select:active {
background-color: rgb(110, 132, 163);
border-radius: 0 10px 10px 0;
+ user-select: none;
+ -webkit-user-select: none;
+ -webkit-touch-callout: none; /* Disable iOS image long-press popup */
}
#noVNC_control_bar.noVNC_open {
box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
@@ -433,38 +319,50 @@ select:active {
.noVNC_right #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
transform: none;
}
+/* Larger touch area for the handle, used when a touch screen is available */
#noVNC_control_bar_handle div {
position: absolute;
right: -35px;
top: 0;
width: 50px;
- height: 50px;
-}
-:root:not(.noVNC_touch) #noVNC_control_bar_handle div {
+ height: 100%;
display: none;
}
+@media (any-pointer: coarse) {
+ #noVNC_control_bar_handle div {
+ display: initial;
+ }
+}
.noVNC_right #noVNC_control_bar_handle div {
left: -35px;
right: auto;
}
-#noVNC_control_bar .noVNC_scroll {
+#noVNC_control_bar > .noVNC_scroll {
max-height: 100vh; /* Chrome is buggy with 100% */
overflow-x: hidden;
overflow-y: auto;
- padding: 0 10px 0 5px;
+ padding: 0 10px;
}
-.noVNC_right #noVNC_control_bar .noVNC_scroll {
- padding: 0 5px 0 10px;
+
+#noVNC_control_bar > .noVNC_scroll > * {
+ display: block;
+ margin: 10px auto;
}
/* Control bar hint */
-#noVNC_control_bar_hint {
+#noVNC_hint_anchor {
position: fixed;
- left: calc(100vw - 50px);
+ right: -50px;
+ left: auto;
+}
+#noVNC_control_bar_anchor.noVNC_right + #noVNC_hint_anchor {
+ left: -50px;
right: auto;
- top: 50%;
- transform: translateY(-50%) scale(0);
+}
+#noVNC_control_bar_hint {
+ position: relative;
+ transform: scale(0);
width: 100px;
height: 50%;
max-height: 600px;
@@ -477,61 +375,65 @@ select:active {
border-radius: 10px;
transition-delay: 0s;
}
-#noVNC_control_bar_anchor.noVNC_right #noVNC_control_bar_hint{
- left: auto;
- right: calc(100vw - 50px);
-}
#noVNC_control_bar_hint.noVNC_active {
visibility: visible;
opacity: 1;
transition-delay: 0.2s;
- transform: translateY(-50%) scale(1);
+ transform: scale(1);
+}
+#noVNC_control_bar_hint.noVNC_notransition {
+ transition: none !important;
}
-/* General button style */
-.noVNC_button {
- display: block;
+/* Control bar buttons */
+#noVNC_control_bar .noVNC_button {
padding: 4px 4px;
- margin: 10px 0;
vertical-align: middle;
border:1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
+ background-color: transparent;
+ background-image: unset; /* we don't want the gradiant from input.css */
}
-.noVNC_button.noVNC_selected {
+#noVNC_control_bar .noVNC_button.noVNC_selected {
border-color: rgba(0, 0, 0, 0.8);
- background: rgba(0, 0, 0, 0.5);
+ background-color: rgba(0, 0, 0, 0.5);
}
-.noVNC_button:disabled {
- opacity: 0.4;
+#noVNC_control_bar .noVNC_button.noVNC_selected:not(:disabled):hover {
+ border-color: rgba(0, 0, 0, 0.4);
+ background-color: rgba(0, 0, 0, 0.2);
}
-.noVNC_button:focus {
- outline: none;
+#noVNC_control_bar .noVNC_button:not(:disabled):hover {
+ background-color: rgba(255, 255, 255, 0.2);
}
-.noVNC_button:active {
+#noVNC_control_bar .noVNC_button:not(:disabled):active {
padding-top: 5px;
padding-bottom: 3px;
}
-/* Android browsers don't properly update hover state if touch events
- * are intercepted, but focus should be safe to display */
-:root:not(.noVNC_touch) .noVNC_button.noVNC_selected:hover,
-.noVNC_button.noVNC_selected:focus {
- border-color: rgba(0, 0, 0, 0.4);
- background: rgba(0, 0, 0, 0.2);
+#noVNC_control_bar .noVNC_button.noVNC_hidden {
+ display: none !important;
}
-:root:not(.noVNC_touch) .noVNC_button:hover,
-.noVNC_button:focus {
- background: rgba(255, 255, 255, 0.2);
-}
-.noVNC_button.noVNC_hidden {
- display: none;
+
+/* Android browsers don't properly update hover state if touch events are
+ * intercepted, like they are when clicking on the remote screen. */
+@media (any-pointer: coarse) {
+ #noVNC_control_bar .noVNC_button:not(:disabled):hover {
+ background-color: transparent;
+ }
+ #noVNC_control_bar .noVNC_button.noVNC_selected:not(:disabled):hover {
+ border-color: rgba(0, 0, 0, 0.8);
+ background-color: rgba(0, 0, 0, 0.5);
+ }
}
+
/* Panels */
.noVNC_panel {
transform: translateX(25px);
transition: 0.5s ease-in-out;
+ box-sizing: border-box; /* so max-width don't have to care about padding */
+ max-width: calc(100vw - 75px - 25px); /* minus left and right margins */
max-height: 100vh; /* Chrome is buggy with 100% */
overflow-x: hidden;
overflow-y: auto;
@@ -563,6 +465,17 @@ select:active {
transform: translateX(-75px);
}
+.noVNC_panel > * {
+ display: block;
+ margin: 10px auto;
+}
+.noVNC_panel > *:first-child {
+ margin-top: 0 !important;
+}
+.noVNC_panel > *:last-child {
+ margin-bottom: 0 !important;
+}
+
.noVNC_panel hr {
border: none;
border-top: 1px solid rgb(192, 192, 192);
@@ -571,6 +484,11 @@ select:active {
.noVNC_panel label {
display: block;
white-space: nowrap;
+ margin: 5px;
+}
+
+.noVNC_panel li {
+ margin: 5px;
}
.noVNC_panel .noVNC_heading {
@@ -581,7 +499,6 @@ select:active {
padding-right: 8px;
color: white;
font-size: 20px;
- margin-bottom: 10px;
white-space: nowrap;
}
.noVNC_panel .noVNC_heading img {
@@ -622,6 +539,12 @@ select:active {
font-size: 13px;
}
+.noVNC_logo + hr {
+ /* Remove all but top border */
+ border: none;
+ border-top: 1px solid rgba(255, 255, 255, 0.2);
+}
+
:root:not(.noVNC_connected) #noVNC_view_drag_button {
display: none;
}
@@ -630,8 +553,15 @@ select:active {
:root:not(.noVNC_connected) #noVNC_mobile_buttons {
display: none;
}
-:root:not(.noVNC_touch) #noVNC_mobile_buttons {
- display: none;
+@media not all and (any-pointer: coarse) {
+ /* FIXME: The button for the virtual keyboard is the only button in this
+ group of "mobile buttons". It is bad to assume that no touch
+ devices have physical keyboards available. Hopefully we can get
+ a media query for this:
+ https://github.com/w3c/csswg-drafts/issues/3871 */
+ :root.noVNC_connected #noVNC_mobile_buttons {
+ display: none;
+ }
}
/* Extra manual keys */
@@ -642,7 +572,7 @@ select:active {
#noVNC_modifiers {
background-color: rgb(92, 92, 92);
border: none;
- padding: 0 10px;
+ padding: 10px;
}
/* Shutdown/Reboot */
@@ -663,13 +593,16 @@ select:active {
:root:not(.noVNC_connected) #noVNC_clipboard_button {
display: none;
}
-#noVNC_clipboard {
- /* Full screen, minus padding and left and right margins */
- max-width: calc(100vw - 2*15px - 75px - 25px);
-}
#noVNC_clipboard_text {
- width: 500px;
+ width: 360px;
+ min-width: 150px;
+ height: 160px;
+ min-height: 70px;
+
+ box-sizing: border-box;
max-width: 100%;
+ /* minus approximate height of title, height of subtitle, and margin */
+ max-height: calc(100vh - 10em - 25px);
}
/* Settings */
@@ -677,7 +610,6 @@ select:active {
}
#noVNC_settings ul {
list-style: none;
- margin: 0px;
padding: 0px;
}
#noVNC_setting_port {
@@ -729,7 +661,7 @@ select:active {
justify-content: center;
align-content: center;
- line-height: 25px;
+ line-height: 1.6;
word-wrap: break-word;
color: #fff;
@@ -803,36 +735,32 @@ select:active {
font-size: calc(25vw - 30px);
}
}
-#noVNC_connect_button {
- cursor: pointer;
+#noVNC_connect_dlg div {
+ padding: 12px;
- padding: 10px;
-
- color: white;
background-color: rgb(110, 132, 163);
border-radius: 12px;
-
text-align: center;
font-size: 20px;
box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
}
-#noVNC_connect_button div {
- margin: 2px;
+#noVNC_connect_button {
+ width: 100%;
padding: 5px 30px;
- border: 1px solid rgb(83, 99, 122);
- border-bottom-width: 2px;
+
+ cursor: pointer;
+
+ border-color: rgb(83, 99, 122);
border-radius: 5px;
+
background: linear-gradient(to top, rgb(110, 132, 163), rgb(99, 119, 147));
+ color: white;
/* This avoids it jumping around when :active */
vertical-align: middle;
}
-#noVNC_connect_button div:active {
- border-bottom-width: 1px;
- margin-top: 3px;
-}
-:root:not(.noVNC_touch) #noVNC_connect_button div:hover {
+#noVNC_connect_button:hover {
background: linear-gradient(to top, rgb(110, 132, 163), rgb(105, 125, 155));
}
@@ -841,6 +769,23 @@ select:active {
height: 1.3em;
}
+/* ----------------------------------------
+ * Server verification Dialog
+ * ----------------------------------------
+ */
+
+#noVNC_verify_server_dlg {
+ position: relative;
+
+ transform: translateY(-50px);
+}
+#noVNC_verify_server_dlg.noVNC_open {
+ transform: translateY(0);
+}
+#noVNC_fingerprint_block {
+ margin: 10px;
+}
+
/* ----------------------------------------
* Password Dialog
* ----------------------------------------
@@ -854,12 +799,8 @@ select:active {
#noVNC_credentials_dlg.noVNC_open {
transform: translateY(0);
}
-#noVNC_credentials_dlg ul {
- list-style: none;
- margin: 0px;
- padding: 0px;
-}
-.noVNC_hidden {
+#noVNC_username_block.noVNC_hidden,
+#noVNC_password_block.noVNC_hidden {
display: none;
}
@@ -871,7 +812,11 @@ select:active {
/* Transition screen */
#noVNC_transition {
- display: none;
+ transition: 0.5s ease-in-out;
+
+ display: flex;
+ opacity: 0;
+ visibility: hidden;
position: fixed;
top: 0;
@@ -892,7 +837,8 @@ select:active {
:root.noVNC_connecting #noVNC_transition,
:root.noVNC_disconnecting #noVNC_transition,
:root.noVNC_reconnecting #noVNC_transition {
- display: flex;
+ opacity: 1;
+ visibility: visible;
}
:root:not(.noVNC_reconnecting) #noVNC_cancel_reconnect_button {
display: none;
@@ -908,6 +854,12 @@ select:active {
background-color: #313131;
border-bottom-right-radius: 800px 600px;
/*border-top-left-radius: 800px 600px;*/
+
+ /* If selection isn't disabled, long-pressing stuff in the sidebar
+ can accidentally select the container or the canvas. This can
+ happen when attempting to move the handle. */
+ user-select: none;
+ -webkit-user-select: none;
}
#noVNC_keyboardinput {
@@ -935,7 +887,7 @@ select:active {
.noVNC_logo {
color:yellow;
font-family: 'Orbitron', 'OrbitronTTF', sans-serif;
- line-height:90%;
+ line-height: 0.9;
text-shadow: 0.1em 0.1em 0 black;
}
.noVNC_logo span{
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/styles/input.css b/emhttp/plugins/dynamix.vm.manager/novnc/app/styles/input.css
new file mode 100644
index 000000000..dc345aabc
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/app/styles/input.css
@@ -0,0 +1,281 @@
+/*
+ * noVNC general input element CSS
+ * Copyright (C) 2022 The noVNC Authors
+ * noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
+ * This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
+ */
+
+/*
+ * Common for all inputs
+ */
+input, input::file-selector-button, button, select, textarea {
+ /* Respect standard font settings */
+ font: inherit;
+
+ /* Disable default rendering */
+ appearance: none;
+ background: none;
+
+ padding: 5px;
+ border: 1px solid rgb(192, 192, 192);
+ border-radius: 5px;
+ color: black;
+ --bg-gradient: linear-gradient(to top, rgb(255, 255, 255) 80%, rgb(240, 240, 240));
+ background-image: var(--bg-gradient);
+}
+
+/*
+ * Buttons
+ */
+input[type=button],
+input[type=color],
+input[type=image],
+input[type=reset],
+input[type=submit],
+input::file-selector-button,
+button,
+select {
+ border-bottom-width: 2px;
+
+ /* This avoids it jumping around when :active */
+ vertical-align: middle;
+ margin-top: 0;
+
+ padding-left: 20px;
+ padding-right: 20px;
+
+ /* Disable Chrome's touch tap highlight */
+ -webkit-tap-highlight-color: transparent;
+}
+
+/*
+ * Select dropdowns
+ */
+select {
+ --select-arrow: url('data:image/svg+xml;utf8, \
+
\
+ \
+ ');
+ background-image: var(--select-arrow), var(--bg-gradient);
+ background-position: calc(100% - 7px), left top;
+ background-repeat: no-repeat;
+ padding-right: calc(2*7px + 8px);
+ padding-left: 7px;
+}
+/* FIXME: :active isn't set when the
is opened in Firefox:
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1805406 */
+select:active {
+ /* Rotated arrow */
+ background-image: url('data:image/svg+xml;utf8, \
+ \
+ \
+ '), var(--bg-gradient);
+}
+option {
+ color: black;
+ background: white;
+}
+
+/*
+ * Checkboxes
+ */
+input[type=checkbox] {
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ background-color: white;
+ background-image: unset;
+ border: 1px solid dimgrey;
+ border-radius: 3px;
+ width: 13px;
+ height: 13px;
+ padding: 0;
+ margin-right: 6px;
+ vertical-align: bottom;
+ transition: 0.2s background-color linear;
+}
+input[type=checkbox]:checked {
+ background-color: rgb(110, 132, 163);
+ border-color: rgb(110, 132, 163);
+}
+input[type=checkbox]:checked::after {
+ content: "";
+ display: block; /* width & height doesn't work on inline elements */
+ width: 3px;
+ height: 7px;
+ border: 1px solid white;
+ border-width: 0 2px 2px 0;
+ transform: rotate(40deg) translateY(-1px);
+}
+
+/*
+ * Radiobuttons
+ */
+input[type=radio] {
+ border-radius: 50%;
+ border: 1px solid dimgrey;
+ width: 12px;
+ height: 12px;
+ padding: 0;
+ margin-right: 6px;
+ transition: 0.2s border linear;
+}
+input[type=radio]:checked {
+ border: 6px solid rgb(110, 132, 163);
+}
+
+/*
+ * Range sliders
+ */
+input[type=range] {
+ border: unset;
+ border-radius: 3px;
+ height: 20px;
+ padding: 0;
+ background: transparent;
+}
+/* -webkit-slider.. & -moz-range.. cant be in selector lists:
+ https://bugs.chromium.org/p/chromium/issues/detail?id=1154623 */
+input[type=range]::-webkit-slider-runnable-track {
+ background-color: rgb(110, 132, 163);
+ height: 6px;
+ border-radius: 3px;
+}
+input[type=range]::-moz-range-track {
+ background-color: rgb(110, 132, 163);
+ height: 6px;
+ border-radius: 3px;
+}
+input[type=range]::-webkit-slider-thumb {
+ appearance: none;
+ width: 18px;
+ height: 20px;
+ border-radius: 5px;
+ background-color: white;
+ border: 1px solid dimgray;
+ margin-top: -7px;
+}
+input[type=range]::-moz-range-thumb {
+ appearance: none;
+ width: 18px;
+ height: 20px;
+ border-radius: 5px;
+ background-color: white;
+ border: 1px solid dimgray;
+ margin-top: -7px;
+}
+
+/*
+ * File choosers
+ */
+input[type=file] {
+ background-image: none;
+ border: none;
+}
+input::file-selector-button {
+ margin-right: 6px;
+}
+
+/*
+ * Hover
+ */
+input[type=button]:hover,
+input[type=color]:hover,
+input[type=image]:hover,
+input[type=reset]:hover,
+input[type=submit]:hover,
+input::file-selector-button:hover,
+button:hover {
+ background-image: linear-gradient(to top, rgb(255, 255, 255), rgb(250, 250, 250));
+}
+select:hover {
+ background-image: var(--select-arrow),
+ linear-gradient(to top, rgb(255, 255, 255), rgb(250, 250, 250));
+ background-position: calc(100% - 7px), left top;
+ background-repeat: no-repeat;
+}
+@media (any-pointer: coarse) {
+ /* We don't want a hover style after touch input */
+ input[type=button]:hover,
+ input[type=color]:hover,
+ input[type=image]:hover,
+ input[type=reset]:hover,
+ input[type=submit]:hover,
+ input::file-selector-button:hover,
+ button:hover {
+ background-image: var(--bg-gradient);
+ }
+ select:hover {
+ background-image: var(--select-arrow), var(--bg-gradient);
+ }
+}
+
+/*
+ * Active (clicked)
+ */
+input[type=button]:active,
+input[type=color]:active,
+input[type=image]:active,
+input[type=reset]:active,
+input[type=submit]:active,
+input::file-selector-button:active,
+button:active,
+select:active {
+ border-bottom-width: 1px;
+ margin-top: 1px;
+}
+
+/*
+ * Focus (tab)
+ */
+input:focus-visible,
+input:focus-visible::file-selector-button,
+button:focus-visible,
+select:focus-visible,
+textarea:focus-visible {
+ outline: 2px solid rgb(74, 144, 217);
+ outline-offset: 1px;
+}
+input[type=file]:focus-visible {
+ outline: none; /* We outline the button instead of the entire element */
+}
+
+/*
+ * Disabled
+ */
+input:disabled,
+input:disabled::file-selector-button,
+button:disabled,
+select:disabled,
+textarea:disabled {
+ opacity: 0.4;
+}
+input[type=button]:disabled,
+input[type=color]:disabled,
+input[type=image]:disabled,
+input[type=reset]:disabled,
+input[type=submit]:disabled,
+input:disabled::file-selector-button,
+button:disabled,
+select:disabled {
+ background-image: var(--bg-gradient);
+ border-bottom-width: 2px;
+ margin-top: 0;
+}
+input[type=file]:disabled {
+ background-image: none;
+}
+select:disabled {
+ background-image: var(--select-arrow), var(--bg-gradient);
+}
+input[type=image]:disabled {
+ /* See Firefox bug:
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1798304 */
+ cursor: default;
+}
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/ui.js b/emhttp/plugins/dynamix.vm.manager/novnc/app/ui.js
index dc0a28c96..f27dfe28e 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/app/ui.js
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/app/ui.js
@@ -8,7 +8,8 @@
import * as Log from '../core/util/logging.js';
import _, { l10n } from './localization.js';
-import { isTouchDevice, isSafari, hasScrollbarGutter, dragThreshold }
+import { isTouchDevice, isMac, isIOS, isAndroid, isChromeOS, isSafari,
+ hasScrollbarGutter, dragThreshold }
from '../core/util/browser.js';
import { setCapture, getPointerEvent } from '../core/util/events.js';
import KeyTable from "../core/input/keysym.js";
@@ -61,7 +62,21 @@ const UI = {
// Translate the DOM
l10n.translateDOM();
- WebUtil.fetchJSON('./package.json')
+ // We rely on modern APIs which might not be available in an
+ // insecure context
+ if (!window.isSecureContext) {
+ // FIXME: This gets hidden when connecting
+ UI.showStatus(_("Running without HTTPS is not recommended, crashes or other issues are likely."), 'error');
+ }
+
+ // Try to fetch version number
+ fetch('./package.json')
+ .then((response) => {
+ if (!response.ok) {
+ throw Error("" + response.status + " " + response.statusText);
+ }
+ return response.json();
+ })
.then((packageInfo) => {
Array.from(document.getElementsByClassName('noVNC_version')).forEach(el => el.innerText = packageInfo.version);
})
@@ -74,7 +89,6 @@ const UI = {
// Adapt the interface for touch screen devices
if (isTouchDevice) {
- document.documentElement.classList.add("noVNC_touch");
// Remove the address bar
setTimeout(() => window.scrollTo(0, 1), 100);
}
@@ -160,7 +174,7 @@ const UI = {
UI.initSetting('port', port);
UI.initSetting('encrypt', (window.location.protocol === "https:"));
UI.initSetting('view_clip', false);
- UI.initSetting('resize', 'scale');
+ UI.initSetting('resize', 'off');
UI.initSetting('quality', 6);
UI.initSetting('compression', 2);
UI.initSetting('shared', true);
@@ -310,6 +324,10 @@ const UI = {
document.getElementById("noVNC_cancel_reconnect_button")
.addEventListener('click', UI.cancelReconnect);
+ document.getElementById("noVNC_approve_server_button")
+ .addEventListener('click', UI.approveServer);
+ document.getElementById("noVNC_reject_server_button")
+ .addEventListener('click', UI.rejectServer);
document.getElementById("noVNC_credentials_button")
.addEventListener('click', UI.setCredentials);
},
@@ -319,8 +337,6 @@ const UI = {
.addEventListener('click', UI.toggleClipboardPanel);
document.getElementById("noVNC_clipboard_text")
.addEventListener('change', UI.clipboardSend);
- document.getElementById("noVNC_clipboard_clear_button")
- .addEventListener('click', UI.clipboardClear);
},
// Add a call to save settings when the element changes,
@@ -439,6 +455,8 @@ const UI = {
// State change closes dialogs as they may not be relevant
// anymore
UI.closeAllPanels();
+ document.getElementById('noVNC_verify_server_dlg')
+ .classList.remove('noVNC_open');
document.getElementById('noVNC_credentials_dlg')
.classList.remove('noVNC_open');
},
@@ -571,10 +589,20 @@ const UI = {
// Consider this a movement of the handle
UI.controlbarDrag = true;
+
+ // The user has "followed" hint, let's hide it until the next drag
+ UI.showControlbarHint(false, false);
},
- showControlbarHint(show) {
+ showControlbarHint(show, animate=true) {
const hint = document.getElementById('noVNC_control_bar_hint');
+
+ if (animate) {
+ hint.classList.remove("noVNC_notransition");
+ } else {
+ hint.classList.add("noVNC_notransition");
+ }
+
if (show) {
hint.classList.add("noVNC_active");
} else {
@@ -755,11 +783,6 @@ const UI = {
}
}
} else {
- /*Weird IE9 error leads to 'null' appearring
- in textboxes instead of ''.*/
- if (value === null) {
- value = "";
- }
ctrl.value = value;
}
},
@@ -953,11 +976,6 @@ const UI = {
Log.Debug("<< UI.clipboardReceive");
},
- clipboardClear() {
- document.getElementById('noVNC_clipboard_text').value = "";
- UI.rfb.clipboardPasteFrom("");
- },
-
clipboardSend() {
const text = document.getElementById('noVNC_clipboard_text').value;
Log.Debug(">> UI.clipboardSend: " + text.substr(0, 40) + "...");
@@ -1023,15 +1041,24 @@ const UI = {
}
url += '/' + path;
- UI.rfb = new RFB(document.getElementById('noVNC_container'), url,
- { shared: UI.getSetting('shared'),
- repeaterID: UI.getSetting('repeaterID'),
- credentials: { password: password },
- wsProtocols: ['binary'] });
+ try {
+ UI.rfb = new RFB(document.getElementById('noVNC_container'), url,
+ { shared: UI.getSetting('shared'),
+ repeaterID: UI.getSetting('repeaterID'),
+ credentials: { password: password } });
+ } catch (exc) {
+ Log.Error("Failed to connect to server: " + exc);
+ UI.updateVisualState('disconnected');
+ UI.showStatus(_("Failed to connect to server: ") + exc, 'error');
+ return;
+ }
+
UI.rfb.addEventListener("connect", UI.connectFinished);
UI.rfb.addEventListener("disconnect", UI.disconnectFinished);
+ UI.rfb.addEventListener("serververification", UI.serverVerify);
UI.rfb.addEventListener("credentialsrequired", UI.credentials);
UI.rfb.addEventListener("securityfailure", UI.securityFailed);
+ UI.rfb.addEventListener("clippingviewport", UI.updateViewDrag);
UI.rfb.addEventListener("capabilities", UI.updatePowerButton);
UI.rfb.addEventListener("clipboard", UI.clipboardReceive);
UI.rfb.addEventListener("bell", UI.bell);
@@ -1118,7 +1145,9 @@ const UI = {
} else {
UI.showStatus(_("Failed to connect to server"), 'error');
}
- } else if (UI.getSetting('reconnect', false) === true && !UI.inhibitReconnect) {
+ }
+ // If reconnecting is allowed process it now
+ if (UI.getSetting('reconnect', false) === true && !UI.inhibitReconnect) {
UI.updateVisualState('reconnecting');
const delay = parseInt(UI.getSetting('reconnect_delay'));
@@ -1152,6 +1181,37 @@ const UI = {
/* ------^-------
* /CONNECTION
* ==============
+ * SERVER VERIFY
+ * ------v------*/
+
+ async serverVerify(e) {
+ const type = e.detail.type;
+ if (type === 'RSA') {
+ const publickey = e.detail.publickey;
+ let fingerprint = await window.crypto.subtle.digest("SHA-1", publickey);
+ // The same fingerprint format as RealVNC
+ fingerprint = Array.from(new Uint8Array(fingerprint).slice(0, 8)).map(
+ x => x.toString(16).padStart(2, '0')).join('-');
+ document.getElementById('noVNC_verify_server_dlg').classList.add('noVNC_open');
+ document.getElementById('noVNC_fingerprint').innerHTML = fingerprint;
+ }
+ },
+
+ approveServer(e) {
+ e.preventDefault();
+ document.getElementById('noVNC_verify_server_dlg').classList.remove('noVNC_open');
+ UI.rfb.approveServer();
+ },
+
+ rejectServer(e) {
+ e.preventDefault();
+ document.getElementById('noVNC_verify_server_dlg').classList.remove('noVNC_open');
+ UI.disconnect();
+ },
+
+/* ------^-------
+ * /SERVER VERIFY
+ * ==============
* PASSWORD
* ------v------*/
@@ -1275,13 +1335,25 @@ const UI = {
const scaling = UI.getSetting('resize') === 'scale';
+ // Some platforms have overlay scrollbars that are difficult
+ // to use in our case, which means we have to force panning
+ // FIXME: Working scrollbars can still be annoying to use with
+ // touch, so we should ideally be able to have both
+ // panning and scrollbars at the same time
+
+ let brokenScrollbars = false;
+
+ if (!hasScrollbarGutter) {
+ if (isIOS() || isAndroid() || isMac() || isChromeOS()) {
+ brokenScrollbars = true;
+ }
+ }
+
if (scaling) {
// Can't be clipping if viewport is scaled to fit
UI.forceSetting('view_clip', false);
UI.rfb.clipViewport = false;
- } else if (!hasScrollbarGutter) {
- // Some platforms have scrollbars that are difficult
- // to use in our case, so we always use our own panning
+ } else if (brokenScrollbars) {
UI.forceSetting('view_clip', true);
UI.rfb.clipViewport = true;
} else {
@@ -1312,7 +1384,8 @@ const UI = {
const viewDragButton = document.getElementById('noVNC_view_drag_button');
- if (!UI.rfb.clipViewport && UI.rfb.dragViewport) {
+ if ((!UI.rfb.clipViewport || !UI.rfb.clippingViewport) &&
+ UI.rfb.dragViewport) {
// We are no longer clipping the viewport. Make sure
// viewport drag isn't active when it can't be used.
UI.rfb.dragViewport = false;
@@ -1329,6 +1402,8 @@ const UI = {
} else {
viewDragButton.classList.add("noVNC_hidden");
}
+
+ viewDragButton.disabled = !UI.rfb.clippingViewport;
},
/* ------^-------
@@ -1695,15 +1770,9 @@ const UI = {
};
// Set up translations
-const LINGUAS = ["cs", "de", "el", "es", "ja", "ko", "nl", "pl", "ru", "sv", "tr", "zh_CN", "zh_TW"];
-l10n.setup(LINGUAS);
-if (l10n.language === "en" || l10n.dictionary !== undefined) {
- UI.prime();
-} else {
- WebUtil.fetchJSON('app/locale/' + l10n.language + '.json')
- .then((translations) => { l10n.dictionary = translations; })
- .catch(err => Log.Error("Failed to load translations: " + err))
- .then(UI.prime);
-}
+const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"];
+l10n.setup(LINGUAS, "app/locale/")
+ .catch(err => Log.Error("Failed to load translations: " + err))
+ .then(UI.prime);
export default UI;
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/app/webutil.js b/emhttp/plugins/dynamix.vm.manager/novnc/app/webutil.js
index a099f9d70..6011442cb 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/app/webutil.js
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/app/webutil.js
@@ -6,24 +6,33 @@
* See README.md for usage and integration instructions.
*/
-import { initLogging as mainInitLogging } from '../core/util/logging.js';
+import * as Log from '../core/util/logging.js';
// init log level reading the logging HTTP param
export function initLogging(level) {
"use strict";
if (typeof level !== "undefined") {
- mainInitLogging(level);
+ Log.initLogging(level);
} else {
const param = document.location.href.match(/logging=([A-Za-z0-9._-]*)/);
- mainInitLogging(param || undefined);
+ Log.initLogging(param || undefined);
}
}
// Read a query string variable
+// A URL with a query parameter can look like this (But will most probably get logged on the http server):
+// https://www.example.com?myqueryparam=myvalue
+//
+// For privacy (Using a hastag #, the parameters will not be sent to the server)
+// the url can be requested in the following way:
+// https://www.example.com#myqueryparam=myvalue&password=secretvalue
+//
+// Even Mixing public and non public parameters will work:
+// https://www.example.com?nonsecretparam=example.com#password=secretvalue
export function getQueryVar(name, defVal) {
"use strict";
const re = new RegExp('.*[?&]' + name + '=([^]*)'),
- match = document.location.href.match(re);
+ match = document.location.href.match(re);
if (typeof defVal === 'undefined') { defVal = null; }
if (match) {
@@ -37,7 +46,7 @@ export function getQueryVar(name, defVal) {
export function getHashVar(name, defVal) {
"use strict";
const re = new RegExp('.*[]' + name + '=([^&]*)'),
- match = document.location.hash.match(re);
+ match = document.location.hash.match(re);
if (typeof defVal === 'undefined') { defVal = null; }
if (match) {
@@ -137,7 +146,7 @@ export function writeSetting(name, value) {
if (window.chrome && window.chrome.storage) {
window.chrome.storage.sync.set(settings);
} else {
- localStorage.setItem(name, value);
+ localStorageSet(name, value);
}
}
@@ -147,7 +156,7 @@ export function readSetting(name, defaultValue) {
if ((name in settings) || (window.chrome && window.chrome.storage)) {
value = settings[name];
} else {
- value = localStorage.getItem(name);
+ value = localStorageGet(name);
settings[name] = value;
}
if (typeof value === "undefined") {
@@ -172,68 +181,70 @@ export function eraseSetting(name) {
if (window.chrome && window.chrome.storage) {
window.chrome.storage.sync.remove(name);
} else {
+ localStorageRemove(name);
+ }
+}
+
+let loggedMsgs = [];
+function logOnce(msg, level = "warn") {
+ if (!loggedMsgs.includes(msg)) {
+ switch (level) {
+ case "error":
+ Log.Error(msg);
+ break;
+ case "warn":
+ Log.Warn(msg);
+ break;
+ case "debug":
+ Log.Debug(msg);
+ break;
+ default:
+ Log.Info(msg);
+ }
+ loggedMsgs.push(msg);
+ }
+}
+
+let cookiesMsg = "Couldn't access noVNC settings, are cookies disabled?";
+
+function localStorageGet(name) {
+ let r;
+ try {
+ r = localStorage.getItem(name);
+ } catch (e) {
+ if (e instanceof DOMException) {
+ logOnce(cookiesMsg);
+ logOnce("'localStorage.getItem(" + name + ")' failed: " + e,
+ "debug");
+ } else {
+ throw e;
+ }
+ }
+ return r;
+}
+function localStorageSet(name, value) {
+ try {
+ localStorage.setItem(name, value);
+ } catch (e) {
+ if (e instanceof DOMException) {
+ logOnce(cookiesMsg);
+ logOnce("'localStorage.setItem(" + name + "," + value +
+ ")' failed: " + e, "debug");
+ } else {
+ throw e;
+ }
+ }
+}
+function localStorageRemove(name) {
+ try {
localStorage.removeItem(name);
+ } catch (e) {
+ if (e instanceof DOMException) {
+ logOnce(cookiesMsg);
+ logOnce("'localStorage.removeItem(" + name + ")' failed: " + e,
+ "debug");
+ } else {
+ throw e;
+ }
}
}
-
-export function injectParamIfMissing(path, param, value) {
- // force pretend that we're dealing with a relative path
- // (assume that we wanted an extra if we pass one in)
- path = "/" + path;
-
- const elem = document.createElement('a');
- elem.href = path;
-
- const paramEq = encodeURIComponent(param) + "=";
- let query;
- if (elem.search) {
- query = elem.search.slice(1).split('&');
- } else {
- query = [];
- }
-
- if (!query.some(v => v.startsWith(paramEq))) {
- query.push(paramEq + encodeURIComponent(value));
- elem.search = "?" + query.join("&");
- }
-
- // some browsers (e.g. IE11) may occasionally omit the leading slash
- // in the elem.pathname string. Handle that case gracefully.
- if (elem.pathname.charAt(0) == "/") {
- return elem.pathname.slice(1) + elem.search + elem.hash;
- }
-
- return elem.pathname + elem.search + elem.hash;
-}
-
-// sadly, we can't use the Fetch API until we decide to drop
-// IE11 support or polyfill promises and fetch in IE11.
-// resolve will receive an object on success, while reject
-// will receive either an event or an error on failure.
-export function fetchJSON(path) {
- return new Promise((resolve, reject) => {
- // NB: IE11 doesn't support JSON as a responseType
- const req = new XMLHttpRequest();
- req.open('GET', path);
-
- req.onload = () => {
- if (req.status === 200) {
- let resObj;
- try {
- resObj = JSON.parse(req.responseText);
- } catch (err) {
- reject(err);
- }
- resolve(resObj);
- } else {
- reject(new Error("XHR got non-200 status while trying to load '" + path + "': " + req.status));
- }
- };
-
- req.onerror = evt => reject(new Error("XHR encountered an error while trying to load '" + path + "': " + evt.message));
-
- req.ontimeout = evt => reject(new Error("XHR timed out while trying to load '" + path + "'"));
-
- req.send();
- });
-}
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/crypto/aes.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/crypto/aes.js
new file mode 100644
index 000000000..e6aaea7c2
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/crypto/aes.js
@@ -0,0 +1,178 @@
+export class AESECBCipher {
+ constructor() {
+ this._key = null;
+ }
+
+ get algorithm() {
+ return { name: "AES-ECB" };
+ }
+
+ static async importKey(key, _algorithm, extractable, keyUsages) {
+ const cipher = new AESECBCipher;
+ await cipher._importKey(key, extractable, keyUsages);
+ return cipher;
+ }
+
+ async _importKey(key, extractable, keyUsages) {
+ this._key = await window.crypto.subtle.importKey(
+ "raw", key, {name: "AES-CBC"}, extractable, keyUsages);
+ }
+
+ async encrypt(_algorithm, plaintext) {
+ const x = new Uint8Array(plaintext);
+ if (x.length % 16 !== 0 || this._key === null) {
+ return null;
+ }
+ const n = x.length / 16;
+ for (let i = 0; i < n; i++) {
+ const y = new Uint8Array(await window.crypto.subtle.encrypt({
+ name: "AES-CBC",
+ iv: new Uint8Array(16),
+ }, this._key, x.slice(i * 16, i * 16 + 16))).slice(0, 16);
+ x.set(y, i * 16);
+ }
+ return x;
+ }
+}
+
+export class AESEAXCipher {
+ constructor() {
+ this._rawKey = null;
+ this._ctrKey = null;
+ this._cbcKey = null;
+ this._zeroBlock = new Uint8Array(16);
+ this._prefixBlock0 = this._zeroBlock;
+ this._prefixBlock1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
+ this._prefixBlock2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]);
+ }
+
+ get algorithm() {
+ return { name: "AES-EAX" };
+ }
+
+ async _encryptBlock(block) {
+ const encrypted = await window.crypto.subtle.encrypt({
+ name: "AES-CBC",
+ iv: this._zeroBlock,
+ }, this._cbcKey, block);
+ return new Uint8Array(encrypted).slice(0, 16);
+ }
+
+ async _initCMAC() {
+ const k1 = await this._encryptBlock(this._zeroBlock);
+ const k2 = new Uint8Array(16);
+ const v = k1[0] >>> 6;
+ for (let i = 0; i < 15; i++) {
+ k2[i] = (k1[i + 1] >> 6) | (k1[i] << 2);
+ k1[i] = (k1[i + 1] >> 7) | (k1[i] << 1);
+ }
+ const lut = [0x0, 0x87, 0x0e, 0x89];
+ k2[14] ^= v >>> 1;
+ k2[15] = (k1[15] << 2) ^ lut[v];
+ k1[15] = (k1[15] << 1) ^ lut[v >> 1];
+ this._k1 = k1;
+ this._k2 = k2;
+ }
+
+ async _encryptCTR(data, counter) {
+ const encrypted = await window.crypto.subtle.encrypt({
+ name: "AES-CTR",
+ counter: counter,
+ length: 128
+ }, this._ctrKey, data);
+ return new Uint8Array(encrypted);
+ }
+
+ async _decryptCTR(data, counter) {
+ const decrypted = await window.crypto.subtle.decrypt({
+ name: "AES-CTR",
+ counter: counter,
+ length: 128
+ }, this._ctrKey, data);
+ return new Uint8Array(decrypted);
+ }
+
+ async _computeCMAC(data, prefixBlock) {
+ if (prefixBlock.length !== 16) {
+ return null;
+ }
+ const n = Math.floor(data.length / 16);
+ const m = Math.ceil(data.length / 16);
+ const r = data.length - n * 16;
+ const cbcData = new Uint8Array((m + 1) * 16);
+ cbcData.set(prefixBlock);
+ cbcData.set(data, 16);
+ if (r === 0) {
+ for (let i = 0; i < 16; i++) {
+ cbcData[n * 16 + i] ^= this._k1[i];
+ }
+ } else {
+ cbcData[(n + 1) * 16 + r] = 0x80;
+ for (let i = 0; i < 16; i++) {
+ cbcData[(n + 1) * 16 + i] ^= this._k2[i];
+ }
+ }
+ let cbcEncrypted = await window.crypto.subtle.encrypt({
+ name: "AES-CBC",
+ iv: this._zeroBlock,
+ }, this._cbcKey, cbcData);
+
+ cbcEncrypted = new Uint8Array(cbcEncrypted);
+ const mac = cbcEncrypted.slice(cbcEncrypted.length - 32, cbcEncrypted.length - 16);
+ return mac;
+ }
+
+ static async importKey(key, _algorithm, _extractable, _keyUsages) {
+ const cipher = new AESEAXCipher;
+ await cipher._importKey(key);
+ return cipher;
+ }
+
+ async _importKey(key) {
+ this._rawKey = key;
+ this._ctrKey = await window.crypto.subtle.importKey(
+ "raw", key, {name: "AES-CTR"}, false, ["encrypt", "decrypt"]);
+ this._cbcKey = await window.crypto.subtle.importKey(
+ "raw", key, {name: "AES-CBC"}, false, ["encrypt"]);
+ await this._initCMAC();
+ }
+
+ async encrypt(algorithm, message) {
+ const ad = algorithm.additionalData;
+ const nonce = algorithm.iv;
+ const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
+ const encrypted = await this._encryptCTR(message, nCMAC);
+ const adCMAC = await this._computeCMAC(ad, this._prefixBlock1);
+ const mac = await this._computeCMAC(encrypted, this._prefixBlock2);
+ for (let i = 0; i < 16; i++) {
+ mac[i] ^= nCMAC[i] ^ adCMAC[i];
+ }
+ const res = new Uint8Array(16 + encrypted.length);
+ res.set(encrypted);
+ res.set(mac, encrypted.length);
+ return res;
+ }
+
+ async decrypt(algorithm, data) {
+ const encrypted = data.slice(0, data.length - 16);
+ const ad = algorithm.additionalData;
+ const nonce = algorithm.iv;
+ const mac = data.slice(data.length - 16);
+ const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
+ const adCMAC = await this._computeCMAC(ad, this._prefixBlock1);
+ const computedMac = await this._computeCMAC(encrypted, this._prefixBlock2);
+ for (let i = 0; i < 16; i++) {
+ computedMac[i] ^= nCMAC[i] ^ adCMAC[i];
+ }
+ if (computedMac.length !== mac.length) {
+ return null;
+ }
+ for (let i = 0; i < mac.length; i++) {
+ if (computedMac[i] !== mac[i]) {
+ return null;
+ }
+ }
+ const res = await this._decryptCTR(encrypted, nCMAC);
+ return res;
+ }
+}
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/crypto/bigint.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/crypto/bigint.js
new file mode 100644
index 000000000..d34432650
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/crypto/bigint.js
@@ -0,0 +1,34 @@
+export function modPow(b, e, m) {
+ let r = 1n;
+ b = b % m;
+ while (e > 0n) {
+ if ((e & 1n) === 1n) {
+ r = (r * b) % m;
+ }
+ e = e >> 1n;
+ b = (b * b) % m;
+ }
+ return r;
+}
+
+export function bigIntToU8Array(bigint, padLength=0) {
+ let hex = bigint.toString(16);
+ if (padLength === 0) {
+ padLength = Math.ceil(hex.length / 2);
+ }
+ hex = hex.padStart(padLength * 2, '0');
+ const length = hex.length / 2;
+ const arr = new Uint8Array(length);
+ for (let i = 0; i < length; i++) {
+ arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
+ }
+ return arr;
+}
+
+export function u8ArrayToBigInt(arr) {
+ let hex = '0x';
+ for (let i = 0; i < arr.length; i++) {
+ hex += arr[i].toString(16).padStart(2, '0');
+ }
+ return BigInt(hex);
+}
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/crypto/crypto.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/crypto/crypto.js
new file mode 100644
index 000000000..cc17da228
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/crypto/crypto.js
@@ -0,0 +1,90 @@
+import { AESECBCipher, AESEAXCipher } from "./aes.js";
+import { DESCBCCipher, DESECBCipher } from "./des.js";
+import { RSACipher } from "./rsa.js";
+import { DHCipher } from "./dh.js";
+import { MD5 } from "./md5.js";
+
+// A single interface for the cryptographic algorithms not supported by SubtleCrypto.
+// Both synchronous and asynchronous implmentations are allowed.
+class LegacyCrypto {
+ constructor() {
+ this._algorithms = {
+ "AES-ECB": AESECBCipher,
+ "AES-EAX": AESEAXCipher,
+ "DES-ECB": DESECBCipher,
+ "DES-CBC": DESCBCCipher,
+ "RSA-PKCS1-v1_5": RSACipher,
+ "DH": DHCipher,
+ "MD5": MD5,
+ };
+ }
+
+ encrypt(algorithm, key, data) {
+ if (key.algorithm.name !== algorithm.name) {
+ throw new Error("algorithm does not match");
+ }
+ if (typeof key.encrypt !== "function") {
+ throw new Error("key does not support encryption");
+ }
+ return key.encrypt(algorithm, data);
+ }
+
+ decrypt(algorithm, key, data) {
+ if (key.algorithm.name !== algorithm.name) {
+ throw new Error("algorithm does not match");
+ }
+ if (typeof key.decrypt !== "function") {
+ throw new Error("key does not support encryption");
+ }
+ return key.decrypt(algorithm, data);
+ }
+
+ importKey(format, keyData, algorithm, extractable, keyUsages) {
+ if (format !== "raw") {
+ throw new Error("key format is not supported");
+ }
+ const alg = this._algorithms[algorithm.name];
+ if (typeof alg === "undefined" || typeof alg.importKey !== "function") {
+ throw new Error("algorithm is not supported");
+ }
+ return alg.importKey(keyData, algorithm, extractable, keyUsages);
+ }
+
+ generateKey(algorithm, extractable, keyUsages) {
+ const alg = this._algorithms[algorithm.name];
+ if (typeof alg === "undefined" || typeof alg.generateKey !== "function") {
+ throw new Error("algorithm is not supported");
+ }
+ return alg.generateKey(algorithm, extractable, keyUsages);
+ }
+
+ exportKey(format, key) {
+ if (format !== "raw") {
+ throw new Error("key format is not supported");
+ }
+ if (typeof key.exportKey !== "function") {
+ throw new Error("key does not support exportKey");
+ }
+ return key.exportKey();
+ }
+
+ digest(algorithm, data) {
+ const alg = this._algorithms[algorithm];
+ if (typeof alg !== "function") {
+ throw new Error("algorithm is not supported");
+ }
+ return alg(data);
+ }
+
+ deriveBits(algorithm, key, length) {
+ if (key.algorithm.name !== algorithm.name) {
+ throw new Error("algorithm does not match");
+ }
+ if (typeof key.deriveBits !== "function") {
+ throw new Error("key does not support deriveBits");
+ }
+ return key.deriveBits(algorithm, length);
+ }
+}
+
+export default new LegacyCrypto;
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/crypto/des.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/crypto/des.js
new file mode 100644
index 000000000..8dab31fb4
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/crypto/des.js
@@ -0,0 +1,330 @@
+/*
+ * Ported from Flashlight VNC ActionScript implementation:
+ * http://www.wizhelp.com/flashlight-vnc/
+ *
+ * Full attribution follows:
+ *
+ * -------------------------------------------------------------------------
+ *
+ * This DES class has been extracted from package Acme.Crypto for use in VNC.
+ * The unnecessary odd parity code has been removed.
+ *
+ * These changes are:
+ * Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+
+ * DesCipher - the DES encryption method
+ *
+ * The meat of this code is by Dave Zimmerman , and is:
+ *
+ * Copyright (c) 1996 Widget Workshop, Inc. All Rights Reserved.
+ *
+ * Permission to use, copy, modify, and distribute this software
+ * and its documentation for NON-COMMERCIAL or COMMERCIAL purposes and
+ * without fee is hereby granted, provided that this copyright notice is kept
+ * intact.
+ *
+ * WIDGET WORKSHOP MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY
+ * OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+ * TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ * PARTICULAR PURPOSE, OR NON-INFRINGEMENT. WIDGET WORKSHOP SHALL NOT BE LIABLE
+ * FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
+ * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES.
+ *
+ * THIS SOFTWARE IS NOT DESIGNED OR INTENDED FOR USE OR RESALE AS ON-LINE
+ * CONTROL EQUIPMENT IN HAZARDOUS ENVIRONMENTS REQUIRING FAIL-SAFE
+ * PERFORMANCE, SUCH AS IN THE OPERATION OF NUCLEAR FACILITIES, AIRCRAFT
+ * NAVIGATION OR COMMUNICATION SYSTEMS, AIR TRAFFIC CONTROL, DIRECT LIFE
+ * SUPPORT MACHINES, OR WEAPONS SYSTEMS, IN WHICH THE FAILURE OF THE
+ * SOFTWARE COULD LEAD DIRECTLY TO DEATH, PERSONAL INJURY, OR SEVERE
+ * PHYSICAL OR ENVIRONMENTAL DAMAGE ("HIGH RISK ACTIVITIES"). WIDGET WORKSHOP
+ * SPECIFICALLY DISCLAIMS ANY EXPRESS OR IMPLIED WARRANTY OF FITNESS FOR
+ * HIGH RISK ACTIVITIES.
+ *
+ *
+ * The rest is:
+ *
+ * Copyright (C) 1996 by Jef Poskanzer . All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * Visit the ACME Labs Java page for up-to-date versions of this and other
+ * fine Java utilities: http://www.acme.com/java/
+ */
+
+/* eslint-disable comma-spacing */
+
+// Tables, permutations, S-boxes, etc.
+const PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3,
+ 25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39,
+ 50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ],
+ totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28];
+
+const z = 0x0;
+let a,b,c,d,e,f;
+a=1<<16; b=1<<24; c=a|b; d=1<<2; e=1<<10; f=d|e;
+const SP1 = [c|e,z|z,a|z,c|f,c|d,a|f,z|d,a|z,z|e,c|e,c|f,z|e,b|f,c|d,b|z,z|d,
+ z|f,b|e,b|e,a|e,a|e,c|z,c|z,b|f,a|d,b|d,b|d,a|d,z|z,z|f,a|f,b|z,
+ a|z,c|f,z|d,c|z,c|e,b|z,b|z,z|e,c|d,a|z,a|e,b|d,z|e,z|d,b|f,a|f,
+ c|f,a|d,c|z,b|f,b|d,z|f,a|f,c|e,z|f,b|e,b|e,z|z,a|d,a|e,z|z,c|d];
+a=1<<20; b=1<<31; c=a|b; d=1<<5; e=1<<15; f=d|e;
+const SP2 = [c|f,b|e,z|e,a|f,a|z,z|d,c|d,b|f,b|d,c|f,c|e,b|z,b|e,a|z,z|d,c|d,
+ a|e,a|d,b|f,z|z,b|z,z|e,a|f,c|z,a|d,b|d,z|z,a|e,z|f,c|e,c|z,z|f,
+ z|z,a|f,c|d,a|z,b|f,c|z,c|e,z|e,c|z,b|e,z|d,c|f,a|f,z|d,z|e,b|z,
+ z|f,c|e,a|z,b|d,a|d,b|f,b|d,a|d,a|e,z|z,b|e,z|f,b|z,c|d,c|f,a|e];
+a=1<<17; b=1<<27; c=a|b; d=1<<3; e=1<<9; f=d|e;
+const SP3 = [z|f,c|e,z|z,c|d,b|e,z|z,a|f,b|e,a|d,b|d,b|d,a|z,c|f,a|d,c|z,z|f,
+ b|z,z|d,c|e,z|e,a|e,c|z,c|d,a|f,b|f,a|e,a|z,b|f,z|d,c|f,z|e,b|z,
+ c|e,b|z,a|d,z|f,a|z,c|e,b|e,z|z,z|e,a|d,c|f,b|e,b|d,z|e,z|z,c|d,
+ b|f,a|z,b|z,c|f,z|d,a|f,a|e,b|d,c|z,b|f,z|f,c|z,a|f,z|d,c|d,a|e];
+a=1<<13; b=1<<23; c=a|b; d=1<<0; e=1<<7; f=d|e;
+const SP4 = [c|d,a|f,a|f,z|e,c|e,b|f,b|d,a|d,z|z,c|z,c|z,c|f,z|f,z|z,b|e,b|d,
+ z|d,a|z,b|z,c|d,z|e,b|z,a|d,a|e,b|f,z|d,a|e,b|e,a|z,c|e,c|f,z|f,
+ b|e,b|d,c|z,c|f,z|f,z|z,z|z,c|z,a|e,b|e,b|f,z|d,c|d,a|f,a|f,z|e,
+ c|f,z|f,z|d,a|z,b|d,a|d,c|e,b|f,a|d,a|e,b|z,c|d,z|e,b|z,a|z,c|e];
+a=1<<25; b=1<<30; c=a|b; d=1<<8; e=1<<19; f=d|e;
+const SP5 = [z|d,a|f,a|e,c|d,z|e,z|d,b|z,a|e,b|f,z|e,a|d,b|f,c|d,c|e,z|f,b|z,
+ a|z,b|e,b|e,z|z,b|d,c|f,c|f,a|d,c|e,b|d,z|z,c|z,a|f,a|z,c|z,z|f,
+ z|e,c|d,z|d,a|z,b|z,a|e,c|d,b|f,a|d,b|z,c|e,a|f,b|f,z|d,a|z,c|e,
+ c|f,z|f,c|z,c|f,a|e,z|z,b|e,c|z,z|f,a|d,b|d,z|e,z|z,b|e,a|f,b|d];
+a=1<<22; b=1<<29; c=a|b; d=1<<4; e=1<<14; f=d|e;
+const SP6 = [b|d,c|z,z|e,c|f,c|z,z|d,c|f,a|z,b|e,a|f,a|z,b|d,a|d,b|e,b|z,z|f,
+ z|z,a|d,b|f,z|e,a|e,b|f,z|d,c|d,c|d,z|z,a|f,c|e,z|f,a|e,c|e,b|z,
+ b|e,z|d,c|d,a|e,c|f,a|z,z|f,b|d,a|z,b|e,b|z,z|f,b|d,c|f,a|e,c|z,
+ a|f,c|e,z|z,c|d,z|d,z|e,c|z,a|f,z|e,a|d,b|f,z|z,c|e,b|z,a|d,b|f];
+a=1<<21; b=1<<26; c=a|b; d=1<<1; e=1<<11; f=d|e;
+const SP7 = [a|z,c|d,b|f,z|z,z|e,b|f,a|f,c|e,c|f,a|z,z|z,b|d,z|d,b|z,c|d,z|f,
+ b|e,a|f,a|d,b|e,b|d,c|z,c|e,a|d,c|z,z|e,z|f,c|f,a|e,z|d,b|z,a|e,
+ b|z,a|e,a|z,b|f,b|f,c|d,c|d,z|d,a|d,b|z,b|e,a|z,c|e,z|f,a|f,c|e,
+ z|f,b|d,c|f,c|z,a|e,z|z,z|d,c|f,z|z,a|f,c|z,z|e,b|d,b|e,z|e,a|d];
+a=1<<18; b=1<<28; c=a|b; d=1<<6; e=1<<12; f=d|e;
+const SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d,
+ c|z,b|d,b|e,z|f,a|e,a|d,c|d,c|e,z|f,z|z,z|z,c|d,b|d,b|e,a|f,a|z,
+ a|f,a|z,c|e,z|e,z|d,c|d,z|e,a|f,b|e,z|d,b|d,c|z,c|d,b|z,a|z,b|f,
+ z|z,c|f,a|d,b|d,c|z,b|e,b|f,z|z,c|f,a|e,a|e,z|f,z|f,a|d,b|z,c|e];
+
+/* eslint-enable comma-spacing */
+
+class DES {
+ constructor(password) {
+ this.keys = [];
+
+ // Set the key.
+ const pc1m = [], pcr = [], kn = [];
+
+ for (let j = 0, l = 56; j < 56; ++j, l -= 8) {
+ l += l < -5 ? 65 : l < -3 ? 31 : l < -1 ? 63 : l === 27 ? 35 : 0; // PC1
+ const m = l & 0x7;
+ pc1m[j] = ((password[l >>> 3] & (1<>> 10;
+ this.keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6;
+ ++KnLi;
+ this.keys[KnLi] = (raw0 & 0x0003f000) << 12;
+ this.keys[KnLi] |= (raw0 & 0x0000003f) << 16;
+ this.keys[KnLi] |= (raw1 & 0x0003f000) >>> 4;
+ this.keys[KnLi] |= (raw1 & 0x0000003f);
+ ++KnLi;
+ }
+ }
+
+ // Encrypt 8 bytes of text
+ enc8(text) {
+ const b = text.slice();
+ let i = 0, l, r, x; // left, right, accumulator
+
+ // Squash 8 bytes to 2 ints
+ l = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++];
+ r = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++];
+
+ x = ((l >>> 4) ^ r) & 0x0f0f0f0f;
+ r ^= x;
+ l ^= (x << 4);
+ x = ((l >>> 16) ^ r) & 0x0000ffff;
+ r ^= x;
+ l ^= (x << 16);
+ x = ((r >>> 2) ^ l) & 0x33333333;
+ l ^= x;
+ r ^= (x << 2);
+ x = ((r >>> 8) ^ l) & 0x00ff00ff;
+ l ^= x;
+ r ^= (x << 8);
+ r = (r << 1) | ((r >>> 31) & 1);
+ x = (l ^ r) & 0xaaaaaaaa;
+ l ^= x;
+ r ^= x;
+ l = (l << 1) | ((l >>> 31) & 1);
+
+ for (let i = 0, keysi = 0; i < 8; ++i) {
+ x = (r << 28) | (r >>> 4);
+ x ^= this.keys[keysi++];
+ let fval = SP7[x & 0x3f];
+ fval |= SP5[(x >>> 8) & 0x3f];
+ fval |= SP3[(x >>> 16) & 0x3f];
+ fval |= SP1[(x >>> 24) & 0x3f];
+ x = r ^ this.keys[keysi++];
+ fval |= SP8[x & 0x3f];
+ fval |= SP6[(x >>> 8) & 0x3f];
+ fval |= SP4[(x >>> 16) & 0x3f];
+ fval |= SP2[(x >>> 24) & 0x3f];
+ l ^= fval;
+ x = (l << 28) | (l >>> 4);
+ x ^= this.keys[keysi++];
+ fval = SP7[x & 0x3f];
+ fval |= SP5[(x >>> 8) & 0x3f];
+ fval |= SP3[(x >>> 16) & 0x3f];
+ fval |= SP1[(x >>> 24) & 0x3f];
+ x = l ^ this.keys[keysi++];
+ fval |= SP8[x & 0x0000003f];
+ fval |= SP6[(x >>> 8) & 0x3f];
+ fval |= SP4[(x >>> 16) & 0x3f];
+ fval |= SP2[(x >>> 24) & 0x3f];
+ r ^= fval;
+ }
+
+ r = (r << 31) | (r >>> 1);
+ x = (l ^ r) & 0xaaaaaaaa;
+ l ^= x;
+ r ^= x;
+ l = (l << 31) | (l >>> 1);
+ x = ((l >>> 8) ^ r) & 0x00ff00ff;
+ r ^= x;
+ l ^= (x << 8);
+ x = ((l >>> 2) ^ r) & 0x33333333;
+ r ^= x;
+ l ^= (x << 2);
+ x = ((r >>> 16) ^ l) & 0x0000ffff;
+ l ^= x;
+ r ^= (x << 16);
+ x = ((r >>> 4) ^ l) & 0x0f0f0f0f;
+ l ^= x;
+ r ^= (x << 4);
+
+ // Spread ints to bytes
+ x = [r, l];
+ for (i = 0; i < 8; i++) {
+ b[i] = (x[i>>>2] >>> (8 * (3 - (i % 4)))) % 256;
+ if (b[i] < 0) { b[i] += 256; } // unsigned
+ }
+ return b;
+ }
+}
+
+export class DESECBCipher {
+ constructor() {
+ this._cipher = null;
+ }
+
+ get algorithm() {
+ return { name: "DES-ECB" };
+ }
+
+ static importKey(key, _algorithm, _extractable, _keyUsages) {
+ const cipher = new DESECBCipher;
+ cipher._importKey(key);
+ return cipher;
+ }
+
+ _importKey(key, _extractable, _keyUsages) {
+ this._cipher = new DES(key);
+ }
+
+ encrypt(_algorithm, plaintext) {
+ const x = new Uint8Array(plaintext);
+ if (x.length % 8 !== 0 || this._cipher === null) {
+ return null;
+ }
+ const n = x.length / 8;
+ for (let i = 0; i < n; i++) {
+ x.set(this._cipher.enc8(x.slice(i * 8, i * 8 + 8)), i * 8);
+ }
+ return x;
+ }
+}
+
+export class DESCBCCipher {
+ constructor() {
+ this._cipher = null;
+ }
+
+ get algorithm() {
+ return { name: "DES-CBC" };
+ }
+
+ static importKey(key, _algorithm, _extractable, _keyUsages) {
+ const cipher = new DESCBCCipher;
+ cipher._importKey(key);
+ return cipher;
+ }
+
+ _importKey(key) {
+ this._cipher = new DES(key);
+ }
+
+ encrypt(algorithm, plaintext) {
+ const x = new Uint8Array(plaintext);
+ let y = new Uint8Array(algorithm.iv);
+ if (x.length % 8 !== 0 || this._cipher === null) {
+ return null;
+ }
+ const n = x.length / 8;
+ for (let i = 0; i < n; i++) {
+ for (let j = 0; j < 8; j++) {
+ y[j] ^= plaintext[i * 8 + j];
+ }
+ y = this._cipher.enc8(y);
+ x.set(y, i * 8);
+ }
+ return x;
+ }
+}
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/crypto/dh.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/crypto/dh.js
new file mode 100644
index 000000000..bd705d9bf
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/crypto/dh.js
@@ -0,0 +1,55 @@
+import { modPow, bigIntToU8Array, u8ArrayToBigInt } from "./bigint.js";
+
+class DHPublicKey {
+ constructor(key) {
+ this._key = key;
+ }
+
+ get algorithm() {
+ return { name: "DH" };
+ }
+
+ exportKey() {
+ return this._key;
+ }
+}
+
+export class DHCipher {
+ constructor() {
+ this._g = null;
+ this._p = null;
+ this._gBigInt = null;
+ this._pBigInt = null;
+ this._privateKey = null;
+ }
+
+ get algorithm() {
+ return { name: "DH" };
+ }
+
+ static generateKey(algorithm, _extractable) {
+ const cipher = new DHCipher;
+ cipher._generateKey(algorithm);
+ return { privateKey: cipher, publicKey: new DHPublicKey(cipher._publicKey) };
+ }
+
+ _generateKey(algorithm) {
+ const g = algorithm.g;
+ const p = algorithm.p;
+ this._keyBytes = p.length;
+ this._gBigInt = u8ArrayToBigInt(g);
+ this._pBigInt = u8ArrayToBigInt(p);
+ this._privateKey = window.crypto.getRandomValues(new Uint8Array(this._keyBytes));
+ this._privateKeyBigInt = u8ArrayToBigInt(this._privateKey);
+ this._publicKey = bigIntToU8Array(modPow(
+ this._gBigInt, this._privateKeyBigInt, this._pBigInt), this._keyBytes);
+ }
+
+ deriveBits(algorithm, length) {
+ const bytes = Math.ceil(length / 8);
+ const pkey = new Uint8Array(algorithm.public);
+ const len = bytes > this._keyBytes ? bytes : this._keyBytes;
+ const secret = modPow(u8ArrayToBigInt(pkey), this._privateKeyBigInt, this._pBigInt);
+ return bigIntToU8Array(secret, len).slice(0, len);
+ }
+}
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/crypto/md5.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/crypto/md5.js
new file mode 100644
index 000000000..fcfefff06
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/crypto/md5.js
@@ -0,0 +1,82 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2021 The noVNC Authors
+ * Licensed under MPL 2.0 (see LICENSE.txt)
+ *
+ * See README.md for usage and integration instructions.
+ */
+
+/*
+ * Performs MD5 hashing on an array of bytes, returns an array of bytes
+ */
+
+export async function MD5(d) {
+ let s = "";
+ for (let i = 0; i < d.length; i++) {
+ s += String.fromCharCode(d[i]);
+ }
+ return M(V(Y(X(s), 8 * s.length)));
+}
+
+function M(d) {
+ let f = new Uint8Array(d.length);
+ for (let i=0;i> 2);
+ for (let m = 0; m < r.length; m++) r[m] = 0;
+ for (let m = 0; m < 8 * d.length; m += 8) r[m >> 5] |= (255 & d.charCodeAt(m / 8)) << m % 32;
+ return r;
+}
+
+function V(d) {
+ let r = "";
+ for (let m = 0; m < 32 * d.length; m += 8) r += String.fromCharCode(d[m >> 5] >>> m % 32 & 255);
+ return r;
+}
+
+function Y(d, g) {
+ d[g >> 5] |= 128 << g % 32, d[14 + (g + 64 >>> 9 << 4)] = g;
+ let m = 1732584193, f = -271733879, r = -1732584194, i = 271733878;
+ for (let n = 0; n < d.length; n += 16) {
+ let h = m,
+ t = f,
+ g = r,
+ e = i;
+ f = ii(f = ii(f = ii(f = ii(f = hh(f = hh(f = hh(f = hh(f = gg(f = gg(f = gg(f = gg(f = ff(f = ff(f = ff(f = ff(f, r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 0], 7, -680876936), f, r, d[n + 1], 12, -389564586), m, f, d[n + 2], 17, 606105819), i, m, d[n + 3], 22, -1044525330), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 4], 7, -176418897), f, r, d[n + 5], 12, 1200080426), m, f, d[n + 6], 17, -1473231341), i, m, d[n + 7], 22, -45705983), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 8], 7, 1770035416), f, r, d[n + 9], 12, -1958414417), m, f, d[n + 10], 17, -42063), i, m, d[n + 11], 22, -1990404162), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 12], 7, 1804603682), f, r, d[n + 13], 12, -40341101), m, f, d[n + 14], 17, -1502002290), i, m, d[n + 15], 22, 1236535329), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 1], 5, -165796510), f, r, d[n + 6], 9, -1069501632), m, f, d[n + 11], 14, 643717713), i, m, d[n + 0], 20, -373897302), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 5], 5, -701558691), f, r, d[n + 10], 9, 38016083), m, f, d[n + 15], 14, -660478335), i, m, d[n + 4], 20, -405537848), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 9], 5, 568446438), f, r, d[n + 14], 9, -1019803690), m, f, d[n + 3], 14, -187363961), i, m, d[n + 8], 20, 1163531501), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 13], 5, -1444681467), f, r, d[n + 2], 9, -51403784), m, f, d[n + 7], 14, 1735328473), i, m, d[n + 12], 20, -1926607734), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 5], 4, -378558), f, r, d[n + 8], 11, -2022574463), m, f, d[n + 11], 16, 1839030562), i, m, d[n + 14], 23, -35309556), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 1], 4, -1530992060), f, r, d[n + 4], 11, 1272893353), m, f, d[n + 7], 16, -155497632), i, m, d[n + 10], 23, -1094730640), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 13], 4, 681279174), f, r, d[n + 0], 11, -358537222), m, f, d[n + 3], 16, -722521979), i, m, d[n + 6], 23, 76029189), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 9], 4, -640364487), f, r, d[n + 12], 11, -421815835), m, f, d[n + 15], 16, 530742520), i, m, d[n + 2], 23, -995338651), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 0], 6, -198630844), f, r, d[n + 7], 10, 1126891415), m, f, d[n + 14], 15, -1416354905), i, m, d[n + 5], 21, -57434055), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 12], 6, 1700485571), f, r, d[n + 3], 10, -1894986606), m, f, d[n + 10], 15, -1051523), i, m, d[n + 1], 21, -2054922799), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 8], 6, 1873313359), f, r, d[n + 15], 10, -30611744), m, f, d[n + 6], 15, -1560198380), i, m, d[n + 13], 21, 1309151649), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 4], 6, -145523070), f, r, d[n + 11], 10, -1120210379), m, f, d[n + 2], 15, 718787259), i, m, d[n + 9], 21, -343485551), m = add(m, h), f = add(f, t), r = add(r, g), i = add(i, e);
+ }
+ return Array(m, f, r, i);
+}
+
+function cmn(d, g, m, f, r, i) {
+ return add(rol(add(add(g, d), add(f, i)), r), m);
+}
+
+function ff(d, g, m, f, r, i, n) {
+ return cmn(g & m | ~g & f, d, g, r, i, n);
+}
+
+function gg(d, g, m, f, r, i, n) {
+ return cmn(g & f | m & ~f, d, g, r, i, n);
+}
+
+function hh(d, g, m, f, r, i, n) {
+ return cmn(g ^ m ^ f, d, g, r, i, n);
+}
+
+function ii(d, g, m, f, r, i, n) {
+ return cmn(m ^ (g | ~f), d, g, r, i, n);
+}
+
+function add(d, g) {
+ let m = (65535 & d) + (65535 & g);
+ return (d >> 16) + (g >> 16) + (m >> 16) << 16 | 65535 & m;
+}
+
+function rol(d, g) {
+ return d << g | d >>> 32 - g;
+}
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/crypto/rsa.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/crypto/rsa.js
new file mode 100644
index 000000000..68e8e869f
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/crypto/rsa.js
@@ -0,0 +1,132 @@
+import Base64 from "../base64.js";
+import { modPow, bigIntToU8Array, u8ArrayToBigInt } from "./bigint.js";
+
+export class RSACipher {
+ constructor() {
+ this._keyLength = 0;
+ this._keyBytes = 0;
+ this._n = null;
+ this._e = null;
+ this._d = null;
+ this._nBigInt = null;
+ this._eBigInt = null;
+ this._dBigInt = null;
+ this._extractable = false;
+ }
+
+ get algorithm() {
+ return { name: "RSA-PKCS1-v1_5" };
+ }
+
+ _base64urlDecode(data) {
+ data = data.replace(/-/g, "+").replace(/_/g, "/");
+ data = data.padEnd(Math.ceil(data.length / 4) * 4, "=");
+ return Base64.decode(data);
+ }
+
+ _padArray(arr, length) {
+ const res = new Uint8Array(length);
+ res.set(arr, length - arr.length);
+ return res;
+ }
+
+ static async generateKey(algorithm, extractable, _keyUsages) {
+ const cipher = new RSACipher;
+ await cipher._generateKey(algorithm, extractable);
+ return { privateKey: cipher };
+ }
+
+ async _generateKey(algorithm, extractable) {
+ this._keyLength = algorithm.modulusLength;
+ this._keyBytes = Math.ceil(this._keyLength / 8);
+ const key = await window.crypto.subtle.generateKey(
+ {
+ name: "RSA-OAEP",
+ modulusLength: algorithm.modulusLength,
+ publicExponent: algorithm.publicExponent,
+ hash: {name: "SHA-256"},
+ },
+ true, ["encrypt", "decrypt"]);
+ const privateKey = await window.crypto.subtle.exportKey("jwk", key.privateKey);
+ this._n = this._padArray(this._base64urlDecode(privateKey.n), this._keyBytes);
+ this._nBigInt = u8ArrayToBigInt(this._n);
+ this._e = this._padArray(this._base64urlDecode(privateKey.e), this._keyBytes);
+ this._eBigInt = u8ArrayToBigInt(this._e);
+ this._d = this._padArray(this._base64urlDecode(privateKey.d), this._keyBytes);
+ this._dBigInt = u8ArrayToBigInt(this._d);
+ this._extractable = extractable;
+ }
+
+ static async importKey(key, _algorithm, extractable, keyUsages) {
+ if (keyUsages.length !== 1 || keyUsages[0] !== "encrypt") {
+ throw new Error("only support importing RSA public key");
+ }
+ const cipher = new RSACipher;
+ await cipher._importKey(key, extractable);
+ return cipher;
+ }
+
+ async _importKey(key, extractable) {
+ const n = key.n;
+ const e = key.e;
+ if (n.length !== e.length) {
+ throw new Error("the sizes of modulus and public exponent do not match");
+ }
+ this._keyBytes = n.length;
+ this._keyLength = this._keyBytes * 8;
+ this._n = new Uint8Array(this._keyBytes);
+ this._e = new Uint8Array(this._keyBytes);
+ this._n.set(n);
+ this._e.set(e);
+ this._nBigInt = u8ArrayToBigInt(this._n);
+ this._eBigInt = u8ArrayToBigInt(this._e);
+ this._extractable = extractable;
+ }
+
+ async encrypt(_algorithm, message) {
+ if (message.length > this._keyBytes - 11) {
+ return null;
+ }
+ const ps = new Uint8Array(this._keyBytes - message.length - 3);
+ window.crypto.getRandomValues(ps);
+ for (let i = 0; i < ps.length; i++) {
+ ps[i] = Math.floor(ps[i] * 254 / 255 + 1);
+ }
+ const em = new Uint8Array(this._keyBytes);
+ em[1] = 0x02;
+ em.set(ps, 2);
+ em.set(message, ps.length + 3);
+ const emBigInt = u8ArrayToBigInt(em);
+ const c = modPow(emBigInt, this._eBigInt, this._nBigInt);
+ return bigIntToU8Array(c, this._keyBytes);
+ }
+
+ async decrypt(_algorithm, message) {
+ if (message.length !== this._keyBytes) {
+ return null;
+ }
+ const msgBigInt = u8ArrayToBigInt(message);
+ const emBigInt = modPow(msgBigInt, this._dBigInt, this._nBigInt);
+ const em = bigIntToU8Array(emBigInt, this._keyBytes);
+ if (em[0] !== 0x00 || em[1] !== 0x02) {
+ return null;
+ }
+ let i = 2;
+ for (; i < em.length; i++) {
+ if (em[i] === 0x00) {
+ break;
+ }
+ }
+ if (i === em.length) {
+ return null;
+ }
+ return em.slice(i + 1, em.length);
+ }
+
+ async exportKey() {
+ if (!this._extractable) {
+ throw new Error("key is not extractable");
+ }
+ return { n: this._n, e: this._e, d: this._d };
+ }
+}
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/decoders/copyrect.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/decoders/copyrect.js
index 0e0536a6a..9e6391a17 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/core/decoders/copyrect.js
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/decoders/copyrect.js
@@ -15,6 +15,11 @@ export default class CopyRectDecoder {
let deltaX = sock.rQshift16();
let deltaY = sock.rQshift16();
+
+ if ((width === 0) || (height === 0)) {
+ return true;
+ }
+
display.copyImage(deltaX, deltaY, x, y, width, height);
return true;
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/decoders/hextile.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/decoders/hextile.js
index 8dbe80922..cc33e0e10 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/core/decoders/hextile.js
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/decoders/hextile.js
@@ -13,6 +13,7 @@ export default class HextileDecoder {
constructor() {
this._tiles = 0;
this._lastsubencoding = 0;
+ this._tileBuffer = new Uint8Array(16 * 16 * 4);
}
decodeRect(x, y, width, height, sock, display, depth) {
@@ -30,10 +31,7 @@ export default class HextileDecoder {
return false;
}
- let rQ = sock.rQ;
- let rQi = sock.rQi;
-
- let subencoding = rQ[rQi]; // Peek
+ let subencoding = sock.rQpeek8();
if (subencoding > 30) { // Raw
throw new Error("Illegal hextile subencoding (subencoding: " +
subencoding + ")");
@@ -64,7 +62,7 @@ export default class HextileDecoder {
return false;
}
- let subrects = rQ[rQi + bytes - 1]; // Peek
+ let subrects = sock.rQpeekBytes(bytes).at(-1);
if (subencoding & 0x10) { // SubrectsColoured
bytes += subrects * (4 + 2);
} else {
@@ -78,7 +76,7 @@ export default class HextileDecoder {
}
// We know the encoding and have a whole tile
- rQi++;
+ sock.rQshift8();
if (subencoding === 0) {
if (this._lastsubencoding & 0x01) {
// Weird: ignore blanks are RAW
@@ -87,51 +85,97 @@ export default class HextileDecoder {
display.fillRect(tx, ty, tw, th, this._background);
}
} else if (subencoding & 0x01) { // Raw
- display.blitImage(tx, ty, tw, th, rQ, rQi);
- rQi += bytes - 1;
+ let pixels = tw * th;
+ let data = sock.rQshiftBytes(pixels * 4, false);
+ // Max sure the image is fully opaque
+ for (let i = 0;i < pixels;i++) {
+ data[i * 4 + 3] = 255;
+ }
+ display.blitImage(tx, ty, tw, th, data, 0);
} else {
if (subencoding & 0x02) { // Background
- this._background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
- rQi += 4;
+ this._background = new Uint8Array(sock.rQshiftBytes(4));
}
if (subencoding & 0x04) { // Foreground
- this._foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
- rQi += 4;
+ this._foreground = new Uint8Array(sock.rQshiftBytes(4));
}
- display.startTile(tx, ty, tw, th, this._background);
+ this._startTile(tx, ty, tw, th, this._background);
if (subencoding & 0x08) { // AnySubrects
- let subrects = rQ[rQi];
- rQi++;
+ let subrects = sock.rQshift8();
for (let s = 0; s < subrects; s++) {
let color;
if (subencoding & 0x10) { // SubrectsColoured
- color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
- rQi += 4;
+ color = sock.rQshiftBytes(4);
} else {
color = this._foreground;
}
- const xy = rQ[rQi];
- rQi++;
+ const xy = sock.rQshift8();
const sx = (xy >> 4);
const sy = (xy & 0x0f);
- const wh = rQ[rQi];
- rQi++;
+ const wh = sock.rQshift8();
const sw = (wh >> 4) + 1;
const sh = (wh & 0x0f) + 1;
- display.subTile(sx, sy, sw, sh, color);
+ this._subTile(sx, sy, sw, sh, color);
}
}
- display.finishTile();
+ this._finishTile(display);
}
- sock.rQi = rQi;
this._lastsubencoding = subencoding;
this._tiles--;
}
return true;
}
+
+ // start updating a tile
+ _startTile(x, y, width, height, color) {
+ this._tileX = x;
+ this._tileY = y;
+ this._tileW = width;
+ this._tileH = height;
+
+ const red = color[0];
+ const green = color[1];
+ const blue = color[2];
+
+ const data = this._tileBuffer;
+ for (let i = 0; i < width * height * 4; i += 4) {
+ data[i] = red;
+ data[i + 1] = green;
+ data[i + 2] = blue;
+ data[i + 3] = 255;
+ }
+ }
+
+ // update sub-rectangle of the current tile
+ _subTile(x, y, w, h, color) {
+ const red = color[0];
+ const green = color[1];
+ const blue = color[2];
+ const xend = x + w;
+ const yend = y + h;
+
+ const data = this._tileBuffer;
+ const width = this._tileW;
+ for (let j = y; j < yend; j++) {
+ for (let i = x; i < xend; i++) {
+ const p = (i + (j * width)) * 4;
+ data[p] = red;
+ data[p + 1] = green;
+ data[p + 2] = blue;
+ data[p + 3] = 255;
+ }
+ }
+ }
+
+ // draw the current tile to the screen
+ _finishTile(display) {
+ display.blitImage(this._tileX, this._tileY,
+ this._tileW, this._tileH,
+ this._tileBuffer, 0);
+ }
}
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/decoders/jpeg.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/decoders/jpeg.js
new file mode 100644
index 000000000..feb2aeb6c
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/decoders/jpeg.js
@@ -0,0 +1,146 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2019 The noVNC Authors
+ * Licensed under MPL 2.0 (see LICENSE.txt)
+ *
+ * See README.md for usage and integration instructions.
+ *
+ */
+
+export default class JPEGDecoder {
+ constructor() {
+ // RealVNC will reuse the quantization tables
+ // and Huffman tables, so we need to cache them.
+ this._cachedQuantTables = [];
+ this._cachedHuffmanTables = [];
+
+ this._segments = [];
+ }
+
+ decodeRect(x, y, width, height, sock, display, depth) {
+ // A rect of JPEG encodings is simply a JPEG file
+ while (true) {
+ let segment = this._readSegment(sock);
+ if (segment === null) {
+ return false;
+ }
+ this._segments.push(segment);
+ // End of image?
+ if (segment[1] === 0xD9) {
+ break;
+ }
+ }
+
+ let huffmanTables = [];
+ let quantTables = [];
+ for (let segment of this._segments) {
+ let type = segment[1];
+ if (type === 0xC4) {
+ // Huffman tables
+ huffmanTables.push(segment);
+ } else if (type === 0xDB) {
+ // Quantization tables
+ quantTables.push(segment);
+ }
+ }
+
+ const sofIndex = this._segments.findIndex(
+ x => x[1] == 0xC0 || x[1] == 0xC2
+ );
+ if (sofIndex == -1) {
+ throw new Error("Illegal JPEG image without SOF");
+ }
+
+ if (quantTables.length === 0) {
+ this._segments.splice(sofIndex+1, 0,
+ ...this._cachedQuantTables);
+ }
+ if (huffmanTables.length === 0) {
+ this._segments.splice(sofIndex+1, 0,
+ ...this._cachedHuffmanTables);
+ }
+
+ let length = 0;
+ for (let segment of this._segments) {
+ length += segment.length;
+ }
+
+ let data = new Uint8Array(length);
+ length = 0;
+ for (let segment of this._segments) {
+ data.set(segment, length);
+ length += segment.length;
+ }
+
+ display.imageRect(x, y, width, height, "image/jpeg", data);
+
+ if (huffmanTables.length !== 0) {
+ this._cachedHuffmanTables = huffmanTables;
+ }
+ if (quantTables.length !== 0) {
+ this._cachedQuantTables = quantTables;
+ }
+
+ this._segments = [];
+
+ return true;
+ }
+
+ _readSegment(sock) {
+ if (sock.rQwait("JPEG", 2)) {
+ return null;
+ }
+
+ let marker = sock.rQshift8();
+ if (marker != 0xFF) {
+ throw new Error("Illegal JPEG marker received (byte: " +
+ marker + ")");
+ }
+ let type = sock.rQshift8();
+ if (type >= 0xD0 && type <= 0xD9 || type == 0x01) {
+ // No length after marker
+ return new Uint8Array([marker, type]);
+ }
+
+ if (sock.rQwait("JPEG", 2, 2)) {
+ return null;
+ }
+
+ let length = sock.rQshift16();
+ if (length < 2) {
+ throw new Error("Illegal JPEG length received (length: " +
+ length + ")");
+ }
+
+ if (sock.rQwait("JPEG", length-2, 4)) {
+ return null;
+ }
+
+ let extra = 0;
+ if (type === 0xDA) {
+ // start of scan
+ extra += 2;
+ while (true) {
+ if (sock.rQwait("JPEG", length-2+extra, 4)) {
+ return null;
+ }
+ let data = sock.rQpeekBytes(length-2+extra, false);
+ if (data.at(-2) === 0xFF && data.at(-1) !== 0x00 &&
+ !(data.at(-1) >= 0xD0 && data.at(-1) <= 0xD7)) {
+ extra -= 2;
+ break;
+ }
+ extra++;
+ }
+ }
+
+ let segment = new Uint8Array(2 + length + extra);
+ segment[0] = marker;
+ segment[1] = type;
+ segment[2] = length >> 8;
+ segment[3] = length;
+ segment.set(sock.rQshiftBytes(length-2+extra, false), 4);
+
+ return segment;
+ }
+}
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/decoders/raw.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/decoders/raw.js
index 4d84d7d10..3c1661425 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/core/decoders/raw.js
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/decoders/raw.js
@@ -13,6 +13,10 @@ export default class RawDecoder {
}
decodeRect(x, y, width, height, sock, display, depth) {
+ if ((width === 0) || (height === 0)) {
+ return true;
+ }
+
if (this._lines === 0) {
this._lines = height;
}
@@ -20,35 +24,34 @@ export default class RawDecoder {
const pixelSize = depth == 8 ? 1 : 4;
const bytesPerLine = width * pixelSize;
- if (sock.rQwait("RAW", bytesPerLine)) {
- return false;
- }
-
- const curY = y + (height - this._lines);
- const currHeight = Math.min(this._lines,
- Math.floor(sock.rQlen / bytesPerLine));
- let data = sock.rQ;
- let index = sock.rQi;
-
- // Convert data if needed
- if (depth == 8) {
- const pixels = width * currHeight;
- const newdata = new Uint8Array(pixels * 4);
- for (let i = 0; i < pixels; i++) {
- newdata[i * 4 + 0] = ((data[index + i] >> 0) & 0x3) * 255 / 3;
- newdata[i * 4 + 1] = ((data[index + i] >> 2) & 0x3) * 255 / 3;
- newdata[i * 4 + 2] = ((data[index + i] >> 4) & 0x3) * 255 / 3;
- newdata[i * 4 + 4] = 0;
+ while (this._lines > 0) {
+ if (sock.rQwait("RAW", bytesPerLine)) {
+ return false;
}
- data = newdata;
- index = 0;
- }
- display.blitImage(x, curY, width, currHeight, data, index);
- sock.rQskipBytes(currHeight * bytesPerLine);
- this._lines -= currHeight;
- if (this._lines > 0) {
- return false;
+ const curY = y + (height - this._lines);
+
+ let data = sock.rQshiftBytes(bytesPerLine, false);
+
+ // Convert data if needed
+ if (depth == 8) {
+ const newdata = new Uint8Array(width * 4);
+ for (let i = 0; i < width; i++) {
+ newdata[i * 4 + 0] = ((data[i] >> 0) & 0x3) * 255 / 3;
+ newdata[i * 4 + 1] = ((data[i] >> 2) & 0x3) * 255 / 3;
+ newdata[i * 4 + 2] = ((data[i] >> 4) & 0x3) * 255 / 3;
+ newdata[i * 4 + 3] = 255;
+ }
+ data = newdata;
+ }
+
+ // Max sure the image is fully opaque
+ for (let i = 0; i < width; i++) {
+ data[i * 4 + 3] = 255;
+ }
+
+ display.blitImage(x, curY, width, 1, data, 0);
+ this._lines--;
}
return true;
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/decoders/tight.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/decoders/tight.js
index b207419e1..8bc977a79 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/core/decoders/tight.js
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/decoders/tight.js
@@ -56,7 +56,7 @@ export default class TightDecoder {
} else if (this._ctl === 0x0A) {
ret = this._pngRect(x, y, width, height,
sock, display, depth);
- } else if ((this._ctl & 0x80) == 0) {
+ } else if ((this._ctl & 0x08) == 0) {
ret = this._basicRect(this._ctl, x, y, width, height,
sock, display, depth);
} else {
@@ -76,12 +76,8 @@ export default class TightDecoder {
return false;
}
- const rQi = sock.rQi;
- const rQ = sock.rQ;
-
- display.fillRect(x, y, width, height,
- [rQ[rQi + 2], rQ[rQi + 1], rQ[rQi]], false);
- sock.rQskipBytes(3);
+ let pixel = sock.rQshiftBytes(3);
+ display.fillRect(x, y, width, height, pixel, false);
return true;
}
@@ -148,6 +144,10 @@ export default class TightDecoder {
const uncompressedSize = width * height * 3;
let data;
+ if (uncompressedSize === 0) {
+ return true;
+ }
+
if (uncompressedSize < 12) {
if (sock.rQwait("TIGHT", uncompressedSize)) {
return false;
@@ -165,7 +165,15 @@ export default class TightDecoder {
this._zlibs[streamId].setInput(null);
}
- display.blitRgbImage(x, y, width, height, data, 0, false);
+ let rgbx = new Uint8Array(width * height * 4);
+ for (let i = 0, j = 0; i < width * height * 4; i += 4, j += 3) {
+ rgbx[i] = data[j];
+ rgbx[i + 1] = data[j + 1];
+ rgbx[i + 2] = data[j + 2];
+ rgbx[i + 3] = 255; // Alpha
+ }
+
+ display.blitImage(x, y, width, height, rgbx, 0, false);
return true;
}
@@ -195,6 +203,10 @@ export default class TightDecoder {
let data;
+ if (uncompressedSize === 0) {
+ return true;
+ }
+
if (uncompressedSize < 12) {
if (sock.rQwait("TIGHT", uncompressedSize)) {
return false;
@@ -237,7 +249,7 @@ export default class TightDecoder {
for (let b = 7; b >= 0; b--) {
dp = (y * width + x * 8 + 7 - b) * 4;
sp = (data[y * w + x] >> b & 1) * 3;
- dest[dp] = palette[sp];
+ dest[dp] = palette[sp];
dest[dp + 1] = palette[sp + 1];
dest[dp + 2] = palette[sp + 2];
dest[dp + 3] = 255;
@@ -247,14 +259,14 @@ export default class TightDecoder {
for (let b = 7; b >= 8 - width % 8; b--) {
dp = (y * width + x * 8 + 7 - b) * 4;
sp = (data[y * w + x] >> b & 1) * 3;
- dest[dp] = palette[sp];
+ dest[dp] = palette[sp];
dest[dp + 1] = palette[sp + 1];
dest[dp + 2] = palette[sp + 2];
dest[dp + 3] = 255;
}
}
- display.blitRgbxImage(x, y, width, height, dest, 0, false);
+ display.blitImage(x, y, width, height, dest, 0, false);
}
_paletteRect(x, y, width, height, data, palette, display) {
@@ -263,17 +275,83 @@ export default class TightDecoder {
const total = width * height * 4;
for (let i = 0, j = 0; i < total; i += 4, j++) {
const sp = data[j] * 3;
- dest[i] = palette[sp];
+ dest[i] = palette[sp];
dest[i + 1] = palette[sp + 1];
dest[i + 2] = palette[sp + 2];
dest[i + 3] = 255;
}
- display.blitRgbxImage(x, y, width, height, dest, 0, false);
+ display.blitImage(x, y, width, height, dest, 0, false);
}
_gradientFilter(streamId, x, y, width, height, sock, display, depth) {
- throw new Error("Gradient filter not implemented");
+ // assume the TPIXEL is 3 bytes long
+ const uncompressedSize = width * height * 3;
+ let data;
+
+ if (uncompressedSize === 0) {
+ return true;
+ }
+
+ if (uncompressedSize < 12) {
+ if (sock.rQwait("TIGHT", uncompressedSize)) {
+ return false;
+ }
+
+ data = sock.rQshiftBytes(uncompressedSize);
+ } else {
+ data = this._readData(sock);
+ if (data === null) {
+ return false;
+ }
+
+ this._zlibs[streamId].setInput(data);
+ data = this._zlibs[streamId].inflate(uncompressedSize);
+ this._zlibs[streamId].setInput(null);
+ }
+
+ let rgbx = new Uint8Array(4 * width * height);
+
+ let rgbxIndex = 0, dataIndex = 0;
+ let left = new Uint8Array(3);
+ for (let x = 0; x < width; x++) {
+ for (let c = 0; c < 3; c++) {
+ const prediction = left[c];
+ const value = data[dataIndex++] + prediction;
+ rgbx[rgbxIndex++] = value;
+ left[c] = value;
+ }
+ rgbx[rgbxIndex++] = 255;
+ }
+
+ let upperIndex = 0;
+ let upper = new Uint8Array(3),
+ upperleft = new Uint8Array(3);
+ for (let y = 1; y < height; y++) {
+ left.fill(0);
+ upperleft.fill(0);
+ for (let x = 0; x < width; x++) {
+ for (let c = 0; c < 3; c++) {
+ upper[c] = rgbx[upperIndex++];
+ let prediction = left[c] + upper[c] - upperleft[c];
+ if (prediction < 0) {
+ prediction = 0;
+ } else if (prediction > 255) {
+ prediction = 255;
+ }
+ const value = data[dataIndex++] + prediction;
+ rgbx[rgbxIndex++] = value;
+ upperleft[c] = upper[c];
+ left[c] = value;
+ }
+ rgbx[rgbxIndex++] = 255;
+ upperIndex++;
+ }
+ }
+
+ display.blitImage(x, y, width, height, rgbx, 0, false);
+
+ return true;
}
_readData(sock) {
@@ -300,7 +378,7 @@ export default class TightDecoder {
return null;
}
- let data = sock.rQshiftBytes(this._len);
+ let data = sock.rQshiftBytes(this._len, false);
this._len = 0;
return data;
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/decoders/zrle.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/decoders/zrle.js
new file mode 100644
index 000000000..49128e798
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/decoders/zrle.js
@@ -0,0 +1,185 @@
+/*
+ * noVNC: HTML5 VNC client
+ * Copyright (C) 2021 The noVNC Authors
+ * Licensed under MPL 2.0 (see LICENSE.txt)
+ *
+ * See README.md for usage and integration instructions.
+ *
+ */
+
+import Inflate from "../inflator.js";
+
+const ZRLE_TILE_WIDTH = 64;
+const ZRLE_TILE_HEIGHT = 64;
+
+export default class ZRLEDecoder {
+ constructor() {
+ this._length = 0;
+ this._inflator = new Inflate();
+
+ this._pixelBuffer = new Uint8Array(ZRLE_TILE_WIDTH * ZRLE_TILE_HEIGHT * 4);
+ this._tileBuffer = new Uint8Array(ZRLE_TILE_WIDTH * ZRLE_TILE_HEIGHT * 4);
+ }
+
+ decodeRect(x, y, width, height, sock, display, depth) {
+ if (this._length === 0) {
+ if (sock.rQwait("ZLib data length", 4)) {
+ return false;
+ }
+ this._length = sock.rQshift32();
+ }
+ if (sock.rQwait("Zlib data", this._length)) {
+ return false;
+ }
+
+ const data = sock.rQshiftBytes(this._length, false);
+
+ this._inflator.setInput(data);
+
+ for (let ty = y; ty < y + height; ty += ZRLE_TILE_HEIGHT) {
+ let th = Math.min(ZRLE_TILE_HEIGHT, y + height - ty);
+
+ for (let tx = x; tx < x + width; tx += ZRLE_TILE_WIDTH) {
+ let tw = Math.min(ZRLE_TILE_WIDTH, x + width - tx);
+
+ const tileSize = tw * th;
+ const subencoding = this._inflator.inflate(1)[0];
+ if (subencoding === 0) {
+ // raw data
+ const data = this._readPixels(tileSize);
+ display.blitImage(tx, ty, tw, th, data, 0, false);
+ } else if (subencoding === 1) {
+ // solid
+ const background = this._readPixels(1);
+ display.fillRect(tx, ty, tw, th, [background[0], background[1], background[2]]);
+ } else if (subencoding >= 2 && subencoding <= 16) {
+ const data = this._decodePaletteTile(subencoding, tileSize, tw, th);
+ display.blitImage(tx, ty, tw, th, data, 0, false);
+ } else if (subencoding === 128) {
+ const data = this._decodeRLETile(tileSize);
+ display.blitImage(tx, ty, tw, th, data, 0, false);
+ } else if (subencoding >= 130 && subencoding <= 255) {
+ const data = this._decodeRLEPaletteTile(subencoding - 128, tileSize);
+ display.blitImage(tx, ty, tw, th, data, 0, false);
+ } else {
+ throw new Error('Unknown subencoding: ' + subencoding);
+ }
+ }
+ }
+ this._length = 0;
+ return true;
+ }
+
+ _getBitsPerPixelInPalette(paletteSize) {
+ if (paletteSize <= 2) {
+ return 1;
+ } else if (paletteSize <= 4) {
+ return 2;
+ } else if (paletteSize <= 16) {
+ return 4;
+ }
+ }
+
+ _readPixels(pixels) {
+ let data = this._pixelBuffer;
+ const buffer = this._inflator.inflate(3*pixels);
+ for (let i = 0, j = 0; i < pixels*4; i += 4, j += 3) {
+ data[i] = buffer[j];
+ data[i + 1] = buffer[j + 1];
+ data[i + 2] = buffer[j + 2];
+ data[i + 3] = 255; // Add the Alpha
+ }
+ return data;
+ }
+
+ _decodePaletteTile(paletteSize, tileSize, tilew, tileh) {
+ const data = this._tileBuffer;
+ const palette = this._readPixels(paletteSize);
+ const bitsPerPixel = this._getBitsPerPixelInPalette(paletteSize);
+ const mask = (1 << bitsPerPixel) - 1;
+
+ let offset = 0;
+ let encoded = this._inflator.inflate(1)[0];
+
+ for (let y=0; y>shift) & mask;
+
+ data[offset] = palette[indexInPalette * 4];
+ data[offset + 1] = palette[indexInPalette * 4 + 1];
+ data[offset + 2] = palette[indexInPalette * 4 + 2];
+ data[offset + 3] = palette[indexInPalette * 4 + 3];
+ offset += 4;
+ shift-=bitsPerPixel;
+ }
+ if (shift<8-bitsPerPixel && y= 128) {
+ indexInPalette -= 128;
+ length = this._readRLELength();
+ }
+ if (indexInPalette > paletteSize) {
+ throw new Error('Too big index in palette: ' + indexInPalette + ', palette size: ' + paletteSize);
+ }
+ if (offset + length > tileSize) {
+ throw new Error('Too big rle length in palette mode: ' + length + ', allowed length is: ' + (tileSize - offset));
+ }
+
+ for (let j = 0; j < length; j++) {
+ data[offset * 4] = palette[indexInPalette * 4];
+ data[offset * 4 + 1] = palette[indexInPalette * 4 + 1];
+ data[offset * 4 + 2] = palette[indexInPalette * 4 + 2];
+ data[offset * 4 + 3] = palette[indexInPalette * 4 + 3];
+ offset++;
+ }
+ }
+ return data;
+ }
+
+ _readRLELength() {
+ let length = 0;
+ let current = 0;
+ do {
+ current = this._inflator.inflate(1)[0];
+ length += current;
+ } while (current === 255);
+ return length + 1;
+ }
+}
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/deflator.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/deflator.js
index fe2a8f703..22f6770b3 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/core/deflator.js
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/deflator.js
@@ -7,7 +7,7 @@
*/
import { deflateInit, deflate } from "../vendor/pako/lib/zlib/deflate.js";
-import { Z_FULL_FLUSH } from "../vendor/pako/lib/zlib/deflate.js";
+import { Z_FULL_FLUSH, Z_DEFAULT_COMPRESSION } from "../vendor/pako/lib/zlib/deflate.js";
import ZStream from "../vendor/pako/lib/zlib/zstream.js";
export default class Deflator {
@@ -15,9 +15,8 @@ export default class Deflator {
this.strm = new ZStream();
this.chunkSize = 1024 * 10 * 10;
this.outputBuffer = new Uint8Array(this.chunkSize);
- this.windowBits = 5;
- deflateInit(this.strm, this.windowBits);
+ deflateInit(this.strm, Z_DEFAULT_COMPRESSION);
}
deflate(inData) {
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/display.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/display.js
index cf1a51aaa..fcd626999 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/core/display.js
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/display.js
@@ -8,7 +8,6 @@
import * as Log from './util/logging.js';
import Base64 from "./base64.js";
-import { supportsImageMetadata } from './util/browser.js';
import { toSigned32bit } from './util/int.js';
export default class Display {
@@ -16,17 +15,13 @@ export default class Display {
this._drawCtx = null;
this._renderQ = []; // queue drawing actions for in-oder rendering
- this._flushing = false;
+ this._flushPromise = null;
// the full frame buffer (logical canvas) size
this._fbWidth = 0;
this._fbHeight = 0;
this._prevDrawStyle = "";
- this._tile = null;
- this._tile16x16 = null;
- this._tileX = 0;
- this._tileY = 0;
Log.Debug(">> Display.constructor");
@@ -60,22 +55,12 @@ export default class Display {
Log.Debug("User Agent: " + navigator.userAgent);
- // Check canvas features
- if (!('createImageData' in this._drawCtx)) {
- throw new Error("Canvas does not support createImageData");
- }
-
- this._tile16x16 = this._drawCtx.createImageData(16, 16);
Log.Debug("<< Display.constructor");
// ===== PROPERTIES =====
this._scale = 1.0;
this._clipViewport = false;
-
- // ===== EVENT HANDLERS =====
-
- this.onflush = () => {}; // A flush request has finished
}
// ===== PROPERTIES =====
@@ -235,6 +220,18 @@ export default class Display {
this.viewportChangePos(0, 0);
}
+ getImageData() {
+ return this._drawCtx.getImageData(0, 0, this.width, this.height);
+ }
+
+ toDataURL(type, encoderOptions) {
+ return this._backbuffer.toDataURL(type, encoderOptions);
+ }
+
+ toBlob(callback, type, quality) {
+ return this._backbuffer.toBlob(callback, type, quality);
+ }
+
// Track what parts of the visible canvas that need updating
_damage(x, y, w, h) {
if (x < this._damageBounds.left) {
@@ -305,9 +302,14 @@ export default class Display {
flush() {
if (this._renderQ.length === 0) {
- this.onflush();
+ return Promise.resolve();
} else {
- this._flushing = true;
+ if (this._flushPromise === null) {
+ this._flushPromise = new Promise((resolve) => {
+ this._flushResolve = resolve;
+ });
+ }
+ return this._flushPromise;
}
}
@@ -378,57 +380,6 @@ export default class Display {
});
}
- // start updating a tile
- startTile(x, y, width, height, color) {
- this._tileX = x;
- this._tileY = y;
- if (width === 16 && height === 16) {
- this._tile = this._tile16x16;
- } else {
- this._tile = this._drawCtx.createImageData(width, height);
- }
-
- const red = color[2];
- const green = color[1];
- const blue = color[0];
-
- const data = this._tile.data;
- for (let i = 0; i < width * height * 4; i += 4) {
- data[i] = red;
- data[i + 1] = green;
- data[i + 2] = blue;
- data[i + 3] = 255;
- }
- }
-
- // update sub-rectangle of the current tile
- subTile(x, y, w, h, color) {
- const red = color[2];
- const green = color[1];
- const blue = color[0];
- const xend = x + w;
- const yend = y + h;
-
- const data = this._tile.data;
- const width = this._tile.width;
- for (let j = y; j < yend; j++) {
- for (let i = x; i < xend; i++) {
- const p = (i + (j * width)) * 4;
- data[p] = red;
- data[p + 1] = green;
- data[p + 2] = blue;
- data[p + 3] = 255;
- }
- }
- }
-
- // draw the current tile to the screen
- finishTile() {
- this._drawCtx.putImageData(this._tile, this._tileX, this._tileY);
- this._damage(this._tileX, this._tileY,
- this._tile.width, this._tile.height);
- }
-
blitImage(x, y, width, height, arr, offset, fromQueue) {
if (this._renderQ.length !== 0 && !fromQueue) {
// NB(directxman12): it's technically more performant here to use preallocated arrays,
@@ -445,47 +396,13 @@ export default class Display {
'height': height,
});
} else {
- this._bgrxImageData(x, y, width, height, arr, offset);
- }
- }
-
- blitRgbImage(x, y, width, height, arr, offset, fromQueue) {
- if (this._renderQ.length !== 0 && !fromQueue) {
- // NB(directxman12): it's technically more performant here to use preallocated arrays,
- // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
- // this probably isn't getting called *nearly* as much
- const newArr = new Uint8Array(width * height * 3);
- newArr.set(new Uint8Array(arr.buffer, 0, newArr.length));
- this._renderQPush({
- 'type': 'blitRgb',
- 'data': newArr,
- 'x': x,
- 'y': y,
- 'width': width,
- 'height': height,
- });
- } else {
- this._rgbImageData(x, y, width, height, arr, offset);
- }
- }
-
- blitRgbxImage(x, y, width, height, arr, offset, fromQueue) {
- if (this._renderQ.length !== 0 && !fromQueue) {
- // NB(directxman12): it's technically more performant here to use preallocated arrays,
- // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
- // this probably isn't getting called *nearly* as much
- const newArr = new Uint8Array(width * height * 4);
- newArr.set(new Uint8Array(arr.buffer, 0, newArr.length));
- this._renderQPush({
- 'type': 'blitRgbx',
- 'data': newArr,
- 'x': x,
- 'y': y,
- 'width': width,
- 'height': height,
- });
- } else {
- this._rgbxImageData(x, y, width, height, arr, offset);
+ // NB(directxman12): arr must be an Type Array view
+ let data = new Uint8ClampedArray(arr.buffer,
+ arr.byteOffset + offset,
+ width * height * 4);
+ let img = new ImageData(data, width, height);
+ this._drawCtx.putImageData(img, x, y);
+ this._damage(x, y, width, height);
}
}
@@ -537,52 +454,13 @@ export default class Display {
}
_setFillColor(color) {
- const newStyle = 'rgb(' + color[2] + ',' + color[1] + ',' + color[0] + ')';
+ const newStyle = 'rgb(' + color[0] + ',' + color[1] + ',' + color[2] + ')';
if (newStyle !== this._prevDrawStyle) {
this._drawCtx.fillStyle = newStyle;
this._prevDrawStyle = newStyle;
}
}
- _rgbImageData(x, y, width, height, arr, offset) {
- const img = this._drawCtx.createImageData(width, height);
- const data = img.data;
- for (let i = 0, j = offset; i < width * height * 4; i += 4, j += 3) {
- data[i] = arr[j];
- data[i + 1] = arr[j + 1];
- data[i + 2] = arr[j + 2];
- data[i + 3] = 255; // Alpha
- }
- this._drawCtx.putImageData(img, x, y);
- this._damage(x, y, img.width, img.height);
- }
-
- _bgrxImageData(x, y, width, height, arr, offset) {
- const img = this._drawCtx.createImageData(width, height);
- const data = img.data;
- for (let i = 0, j = offset; i < width * height * 4; i += 4, j += 4) {
- data[i] = arr[j + 2];
- data[i + 1] = arr[j + 1];
- data[i + 2] = arr[j];
- data[i + 3] = 255; // Alpha
- }
- this._drawCtx.putImageData(img, x, y);
- this._damage(x, y, img.width, img.height);
- }
-
- _rgbxImageData(x, y, width, height, arr, offset) {
- // NB(directxman12): arr must be an Type Array view
- let img;
- if (supportsImageMetadata) {
- img = new ImageData(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4), width, height);
- } else {
- img = this._drawCtx.createImageData(width, height);
- img.data.set(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4));
- }
- this._drawCtx.putImageData(img, x, y);
- this._damage(x, y, img.width, img.height);
- }
-
_renderQPush(action) {
this._renderQ.push(action);
if (this._renderQ.length === 1) {
@@ -616,15 +494,8 @@ export default class Display {
case 'blit':
this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true);
break;
- case 'blitRgb':
- this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0, true);
- break;
- case 'blitRgbx':
- this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0, true);
- break;
case 'img':
- /* IE tends to set "complete" prematurely, so check dimensions */
- if (a.img.complete && (a.img.width !== 0) && (a.img.height !== 0)) {
+ if (a.img.complete) {
if (a.img.width !== a.width || a.img.height !== a.height) {
Log.Error("Decoded image has incorrect dimensions. Got " +
a.img.width + "x" + a.img.height + ". Expected " +
@@ -647,9 +518,11 @@ export default class Display {
}
}
- if (this._renderQ.length === 0 && this._flushing) {
- this._flushing = false;
- this.onflush();
+ if (this._renderQ.length === 0 &&
+ this._flushPromise !== null) {
+ this._flushResolve();
+ this._flushPromise = null;
+ this._flushResolve = null;
}
}
}
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/encodings.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/encodings.js
index 51c099291..1a79989d1 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/core/encodings.js
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/encodings.js
@@ -12,7 +12,9 @@ export const encodings = {
encodingRRE: 2,
encodingHextile: 5,
encodingTight: 7,
+ encodingZRLE: 16,
encodingTightPNG: -260,
+ encodingJPEG: 21,
pseudoEncodingQualityLevel9: -23,
pseudoEncodingQualityLevel0: -32,
@@ -20,6 +22,7 @@ export const encodings = {
pseudoEncodingLastRect: -224,
pseudoEncodingCursor: -239,
pseudoEncodingQEMUExtendedKeyEvent: -258,
+ pseudoEncodingQEMULedEvent: -261,
pseudoEncodingDesktopName: -307,
pseudoEncodingExtendedDesktopSize: -308,
pseudoEncodingXvp: -309,
@@ -38,7 +41,9 @@ export function encodingName(num) {
case encodings.encodingRRE: return "RRE";
case encodings.encodingHextile: return "Hextile";
case encodings.encodingTight: return "Tight";
+ case encodings.encodingZRLE: return "ZRLE";
case encodings.encodingTightPNG: return "TightPNG";
+ case encodings.encodingJPEG: return "JPEG";
default: return "[unknown encoding " + num + "]";
}
}
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/inflator.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/inflator.js
index 4b337607b..f851f2a7d 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/core/inflator.js
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/inflator.js
@@ -14,9 +14,8 @@ export default class Inflate {
this.strm = new ZStream();
this.chunkSize = 1024 * 10 * 10;
this.strm.output = new Uint8Array(this.chunkSize);
- this.windowBits = 5;
- inflateInit(this.strm, this.windowBits);
+ inflateInit(this.strm);
}
setInput(data) {
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/input/domkeytable.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/input/domkeytable.js
index b84ad45de..f79aeadfa 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/core/input/domkeytable.js
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/input/domkeytable.js
@@ -35,7 +35,7 @@ function addNumpad(key, standard, numpad) {
DOMKeyTable[key] = [standard, standard, standard, numpad];
}
-// 2.2. Modifier Keys
+// 3.2. Modifier Keys
addLeftRight("Alt", KeyTable.XK_Alt_L, KeyTable.XK_Alt_R);
addStandard("AltGraph", KeyTable.XK_ISO_Level3_Shift);
@@ -49,25 +49,27 @@ addStandard("ScrollLock", KeyTable.XK_Scroll_Lock);
addLeftRight("Shift", KeyTable.XK_Shift_L, KeyTable.XK_Shift_R);
// - Symbol
// - SymbolLock
+// - Hyper
+// - Super
-// 2.3. Whitespace Keys
+// 3.3. Whitespace Keys
addNumpad("Enter", KeyTable.XK_Return, KeyTable.XK_KP_Enter);
addStandard("Tab", KeyTable.XK_Tab);
addNumpad(" ", KeyTable.XK_space, KeyTable.XK_KP_Space);
-// 2.4. Navigation Keys
+// 3.4. Navigation Keys
addNumpad("ArrowDown", KeyTable.XK_Down, KeyTable.XK_KP_Down);
-addNumpad("ArrowUp", KeyTable.XK_Up, KeyTable.XK_KP_Up);
addNumpad("ArrowLeft", KeyTable.XK_Left, KeyTable.XK_KP_Left);
addNumpad("ArrowRight", KeyTable.XK_Right, KeyTable.XK_KP_Right);
+addNumpad("ArrowUp", KeyTable.XK_Up, KeyTable.XK_KP_Up);
addNumpad("End", KeyTable.XK_End, KeyTable.XK_KP_End);
addNumpad("Home", KeyTable.XK_Home, KeyTable.XK_KP_Home);
addNumpad("PageDown", KeyTable.XK_Next, KeyTable.XK_KP_Next);
addNumpad("PageUp", KeyTable.XK_Prior, KeyTable.XK_KP_Prior);
-// 2.5. Editing Keys
+// 3.5. Editing Keys
addStandard("Backspace", KeyTable.XK_BackSpace);
// Browsers send "Clear" for the numpad 5 without NumLock because
@@ -85,7 +87,7 @@ addStandard("Paste", KeyTable.XF86XK_Paste);
addStandard("Redo", KeyTable.XK_Redo);
addStandard("Undo", KeyTable.XK_Undo);
-// 2.6. UI Keys
+// 3.6. UI Keys
// - Accept
// - Again (could just be XK_Redo)
@@ -103,7 +105,7 @@ addStandard("Select", KeyTable.XK_Select);
addStandard("ZoomIn", KeyTable.XF86XK_ZoomIn);
addStandard("ZoomOut", KeyTable.XF86XK_ZoomOut);
-// 2.7. Device Keys
+// 3.7. Device Keys
addStandard("BrightnessDown", KeyTable.XF86XK_MonBrightnessDown);
addStandard("BrightnessUp", KeyTable.XF86XK_MonBrightnessUp);
@@ -116,10 +118,10 @@ addStandard("Hibernate", KeyTable.XF86XK_Hibernate);
addStandard("Standby", KeyTable.XF86XK_Standby);
addStandard("WakeUp", KeyTable.XF86XK_WakeUp);
-// 2.8. IME and Composition Keys
+// 3.8. IME and Composition Keys
addStandard("AllCandidates", KeyTable.XK_MultipleCandidate);
-addStandard("Alphanumeric", KeyTable.XK_Eisu_Shift); // could also be _Eisu_Toggle
+addStandard("Alphanumeric", KeyTable.XK_Eisu_toggle);
addStandard("CodeInput", KeyTable.XK_Codeinput);
addStandard("Compose", KeyTable.XK_Multi_key);
addStandard("Convert", KeyTable.XK_Henkan);
@@ -137,7 +139,7 @@ addStandard("PreviousCandidate", KeyTable.XK_PreviousCandidate);
addStandard("SingleCandidate", KeyTable.XK_SingleCandidate);
addStandard("HangulMode", KeyTable.XK_Hangul);
addStandard("HanjaMode", KeyTable.XK_Hangul_Hanja);
-addStandard("JunjuaMode", KeyTable.XK_Hangul_Jeonja);
+addStandard("JunjaMode", KeyTable.XK_Hangul_Jeonja);
addStandard("Eisu", KeyTable.XK_Eisu_toggle);
addStandard("Hankaku", KeyTable.XK_Hankaku);
addStandard("Hiragana", KeyTable.XK_Hiragana);
@@ -147,9 +149,9 @@ addStandard("KanjiMode", KeyTable.XK_Kanji);
addStandard("Katakana", KeyTable.XK_Katakana);
addStandard("Romaji", KeyTable.XK_Romaji);
addStandard("Zenkaku", KeyTable.XK_Zenkaku);
-addStandard("ZenkakuHanaku", KeyTable.XK_Zenkaku_Hankaku);
+addStandard("ZenkakuHankaku", KeyTable.XK_Zenkaku_Hankaku);
-// 2.9. General-Purpose Function Keys
+// 3.9. General-Purpose Function Keys
addStandard("F1", KeyTable.XK_F1);
addStandard("F2", KeyTable.XK_F2);
@@ -188,7 +190,7 @@ addStandard("F34", KeyTable.XK_F34);
addStandard("F35", KeyTable.XK_F35);
// - Soft1...
-// 2.10. Multimedia Keys
+// 3.10. Multimedia Keys
// - ChannelDown
// - ChannelUp
@@ -200,6 +202,7 @@ addStandard("MailSend", KeyTable.XF86XK_Send);
addStandard("MediaFastForward", KeyTable.XF86XK_AudioForward);
addStandard("MediaPause", KeyTable.XF86XK_AudioPause);
addStandard("MediaPlay", KeyTable.XF86XK_AudioPlay);
+// - MediaPlayPause
addStandard("MediaRecord", KeyTable.XF86XK_AudioRecord);
addStandard("MediaRewind", KeyTable.XF86XK_AudioRewind);
addStandard("MediaStop", KeyTable.XF86XK_AudioStop);
@@ -211,12 +214,12 @@ addStandard("Print", KeyTable.XK_Print);
addStandard("Save", KeyTable.XF86XK_Save);
addStandard("SpellCheck", KeyTable.XF86XK_Spell);
-// 2.11. Multimedia Numpad Keys
+// 3.11. Multimedia Numpad Keys
// - Key11
// - Key12
-// 2.12. Audio Keys
+// 3.12. Audio Keys
// - AudioBalanceLeft
// - AudioBalanceRight
@@ -236,16 +239,17 @@ addStandard("AudioVolumeMute", KeyTable.XF86XK_AudioMute);
// - MicrophoneVolumeUp
addStandard("MicrophoneVolumeMute", KeyTable.XF86XK_AudioMicMute);
-// 2.13. Speech Keys
+// 3.13. Speech Keys
// - SpeechCorrectionList
// - SpeechInputToggle
-// 2.14. Application Keys
+// 3.14. Application Keys
addStandard("LaunchApplication1", KeyTable.XF86XK_MyComputer);
addStandard("LaunchApplication2", KeyTable.XF86XK_Calculator);
addStandard("LaunchCalendar", KeyTable.XF86XK_Calendar);
+// - LaunchContacts
addStandard("LaunchMail", KeyTable.XF86XK_Mail);
addStandard("LaunchMediaPlayer", KeyTable.XF86XK_AudioMedia);
addStandard("LaunchMusicPlayer", KeyTable.XF86XK_Music);
@@ -256,7 +260,7 @@ addStandard("LaunchWebBrowser", KeyTable.XF86XK_WWW);
addStandard("LaunchWebCam", KeyTable.XF86XK_WebCam);
addStandard("LaunchWordProcessor", KeyTable.XF86XK_Word);
-// 2.15. Browser Keys
+// 3.15. Browser Keys
addStandard("BrowserBack", KeyTable.XF86XK_Back);
addStandard("BrowserFavorites", KeyTable.XF86XK_Favorites);
@@ -266,15 +270,15 @@ addStandard("BrowserRefresh", KeyTable.XF86XK_Refresh);
addStandard("BrowserSearch", KeyTable.XF86XK_Search);
addStandard("BrowserStop", KeyTable.XF86XK_Stop);
-// 2.16. Mobile Phone Keys
+// 3.16. Mobile Phone Keys
// - A whole bunch...
-// 2.17. TV Keys
+// 3.17. TV Keys
// - A whole bunch...
-// 2.18. Media Controller Keys
+// 3.18. Media Controller Keys
// - A whole bunch...
addStandard("Dimmer", KeyTable.XF86XK_BrightnessAdjust);
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/input/keyboard.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/input/keyboard.js
index 9e6af2ac7..68da2312b 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/core/input/keyboard.js
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/input/keyboard.js
@@ -20,16 +20,13 @@ export default class Keyboard {
this._keyDownList = {}; // List of depressed keys
// (even if they are happy)
- this._pendingKey = null; // Key waiting for keypress
this._altGrArmed = false; // Windows AltGr detection
// keep these here so we can refer to them later
this._eventHandlers = {
'keyup': this._handleKeyUp.bind(this),
'keydown': this._handleKeyDown.bind(this),
- 'keypress': this._handleKeyPress.bind(this),
'blur': this._allKeysUp.bind(this),
- 'checkalt': this._checkAlt.bind(this),
};
// ===== EVENT HANDLERS =====
@@ -39,7 +36,7 @@ export default class Keyboard {
// ===== PRIVATE METHODS =====
- _sendKeyEvent(keysym, code, down) {
+ _sendKeyEvent(keysym, code, down, numlock = null, capslock = null) {
if (down) {
this._keyDownList[code] = keysym;
} else {
@@ -51,8 +48,9 @@ export default class Keyboard {
}
Log.Debug("onkeyevent " + (down ? "down" : "up") +
- ", keysym: " + keysym, ", code: " + code);
- this.onkeyevent(keysym, code, down);
+ ", keysym: " + keysym, ", code: " + code +
+ ", numlock: " + numlock + ", capslock: " + capslock);
+ this.onkeyevent(keysym, code, down, numlock, capslock);
}
_getKeyCode(e) {
@@ -62,9 +60,7 @@ export default class Keyboard {
}
// Unstable, but we don't have anything else to go on
- // (don't use it for 'keypress' events thought since
- // WebKit sets it to the same as charCode)
- if (e.keyCode && (e.type !== 'keypress')) {
+ if (e.keyCode) {
// 229 is used for composition events
if (e.keyCode !== 229) {
return 'Platform' + e.keyCode;
@@ -91,6 +87,14 @@ export default class Keyboard {
_handleKeyDown(e) {
const code = this._getKeyCode(e);
let keysym = KeyboardUtil.getKeysym(e);
+ let numlock = e.getModifierState('NumLock');
+ let capslock = e.getModifierState('CapsLock');
+
+ // getModifierState for NumLock is not supported on mac and ios and always returns false.
+ // Set to null to indicate unknown/unsupported instead.
+ if (browser.isMac() || browser.isIOS()) {
+ numlock = null;
+ }
// Windows doesn't have a proper AltGr, but handles it using
// fake Ctrl+Alt. However the remote end might not be Windows,
@@ -112,7 +116,7 @@ export default class Keyboard {
// key to "AltGraph".
keysym = KeyTable.XK_ISO_Level3_Shift;
} else {
- this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
+ this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true, numlock, capslock);
}
}
@@ -123,8 +127,8 @@ export default class Keyboard {
// If it's a virtual keyboard then it should be
// sufficient to just send press and release right
// after each other
- this._sendKeyEvent(keysym, code, true);
- this._sendKeyEvent(keysym, code, false);
+ this._sendKeyEvent(keysym, code, true, numlock, capslock);
+ this._sendKeyEvent(keysym, code, false, numlock, capslock);
}
stopEvent(e);
@@ -158,31 +162,41 @@ export default class Keyboard {
keysym = this._keyDownList[code];
}
+ // macOS doesn't send proper key releases if a key is pressed
+ // while meta is held down
+ if ((browser.isMac() || browser.isIOS()) &&
+ (e.metaKey && code !== 'MetaLeft' && code !== 'MetaRight')) {
+ this._sendKeyEvent(keysym, code, true, numlock, capslock);
+ this._sendKeyEvent(keysym, code, false, numlock, capslock);
+ stopEvent(e);
+ return;
+ }
+
// macOS doesn't send proper key events for modifiers, only
// state change events. That gets extra confusing for CapsLock
// which toggles on each press, but not on release. So pretend
// it was a quick press and release of the button.
if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) {
- this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
- this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
+ this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true, numlock, capslock);
+ this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false, numlock, capslock);
stopEvent(e);
return;
}
- // If this is a legacy browser then we'll need to wait for
- // a keypress event as well
- // (IE and Edge has a broken KeyboardEvent.key, so we can't
- // just check for the presence of that field)
- if (!keysym && (!e.key || browser.isIE() || browser.isEdge())) {
- this._pendingKey = code;
- // However we might not get a keypress event if the key
- // is non-printable, which needs some special fallback
- // handling
- setTimeout(this._handleKeyPressTimeout.bind(this), 10, e);
+ // Windows doesn't send proper key releases for a bunch of
+ // Japanese IM keys so we have to fake the release right away
+ const jpBadKeys = [ KeyTable.XK_Zenkaku_Hankaku,
+ KeyTable.XK_Eisu_toggle,
+ KeyTable.XK_Katakana,
+ KeyTable.XK_Hiragana,
+ KeyTable.XK_Romaji ];
+ if (browser.isWindows() && jpBadKeys.includes(keysym)) {
+ this._sendKeyEvent(keysym, code, true, numlock, capslock);
+ this._sendKeyEvent(keysym, code, false, numlock, capslock);
+ stopEvent(e);
return;
}
- this._pendingKey = null;
stopEvent(e);
// Possible start of AltGr sequence? (see above)
@@ -194,70 +208,7 @@ export default class Keyboard {
return;
}
- this._sendKeyEvent(keysym, code, true);
- }
-
- // Legacy event for browsers without code/key
- _handleKeyPress(e) {
- stopEvent(e);
-
- // Are we expecting a keypress?
- if (this._pendingKey === null) {
- return;
- }
-
- let code = this._getKeyCode(e);
- const keysym = KeyboardUtil.getKeysym(e);
-
- // The key we were waiting for?
- if ((code !== 'Unidentified') && (code != this._pendingKey)) {
- return;
- }
-
- code = this._pendingKey;
- this._pendingKey = null;
-
- if (!keysym) {
- Log.Info('keypress with no keysym:', e);
- return;
- }
-
- this._sendKeyEvent(keysym, code, true);
- }
-
- _handleKeyPressTimeout(e) {
- // Did someone manage to sort out the key already?
- if (this._pendingKey === null) {
- return;
- }
-
- let keysym;
-
- const code = this._pendingKey;
- this._pendingKey = null;
-
- // We have no way of knowing the proper keysym with the
- // information given, but the following are true for most
- // layouts
- if ((e.keyCode >= 0x30) && (e.keyCode <= 0x39)) {
- // Digit
- keysym = e.keyCode;
- } else if ((e.keyCode >= 0x41) && (e.keyCode <= 0x5a)) {
- // Character (A-Z)
- let char = String.fromCharCode(e.keyCode);
- // A feeble attempt at the correct case
- if (e.shiftKey) {
- char = char.toUpperCase();
- } else {
- char = char.toLowerCase();
- }
- keysym = char.charCodeAt();
- } else {
- // Unknown, give up
- keysym = 0;
- }
-
- this._sendKeyEvent(keysym, code, true);
+ this._sendKeyEvent(keysym, code, true, numlock, capslock);
}
_handleKeyUp(e) {
@@ -312,30 +263,6 @@ export default class Keyboard {
Log.Debug("<< Keyboard.allKeysUp");
}
- // Alt workaround for Firefox on Windows, see below
- _checkAlt(e) {
- if (e.skipCheckAlt) {
- return;
- }
- if (e.altKey) {
- return;
- }
-
- const target = this._target;
- const downList = this._keyDownList;
- ['AltLeft', 'AltRight'].forEach((code) => {
- if (!(code in downList)) {
- return;
- }
-
- const event = new KeyboardEvent('keyup',
- { key: downList[code],
- code: code });
- event.skipCheckAlt = true;
- target.dispatchEvent(event);
- });
- }
-
// ===== PUBLIC METHODS =====
grab() {
@@ -343,41 +270,18 @@ export default class Keyboard {
this._target.addEventListener('keydown', this._eventHandlers.keydown);
this._target.addEventListener('keyup', this._eventHandlers.keyup);
- this._target.addEventListener('keypress', this._eventHandlers.keypress);
// Release (key up) if window loses focus
window.addEventListener('blur', this._eventHandlers.blur);
- // Firefox on Windows has broken handling of Alt, so we need to
- // poll as best we can for releases (still doesn't prevent the
- // menu from popping up though as we can't call
- // preventDefault())
- if (browser.isWindows() && browser.isFirefox()) {
- const handler = this._eventHandlers.checkalt;
- ['mousedown', 'mouseup', 'mousemove', 'wheel',
- 'touchstart', 'touchend', 'touchmove',
- 'keydown', 'keyup'].forEach(type =>
- document.addEventListener(type, handler,
- { capture: true,
- passive: true }));
- }
-
//Log.Debug("<< Keyboard.grab");
}
ungrab() {
//Log.Debug(">> Keyboard.ungrab");
- if (browser.isWindows() && browser.isFirefox()) {
- const handler = this._eventHandlers.checkalt;
- ['mousedown', 'mouseup', 'mousemove', 'wheel',
- 'touchstart', 'touchend', 'touchmove',
- 'keydown', 'keyup'].forEach(type => document.removeEventListener(type, handler));
- }
-
this._target.removeEventListener('keydown', this._eventHandlers.keydown);
this._target.removeEventListener('keyup', this._eventHandlers.keyup);
- this._target.removeEventListener('keypress', this._eventHandlers.keypress);
window.removeEventListener('blur', this._eventHandlers.blur);
// Release (key up) all keys that are in a down state
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/input/util.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/input/util.js
index 1b98040be..36b698176 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/core/input/util.js
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/input/util.js
@@ -22,9 +22,8 @@ export function getKeycode(evt) {
}
// The de-facto standard is to use Windows Virtual-Key codes
- // in the 'keyCode' field for non-printable characters. However
- // Webkit sets it to the same as charCode in 'keypress' events.
- if ((evt.type !== 'keypress') && (evt.keyCode in vkeys)) {
+ // in the 'keyCode' field for non-printable characters
+ if (evt.keyCode in vkeys) {
let code = vkeys[evt.keyCode];
// macOS has messed up this code for some reason
@@ -68,27 +67,7 @@ export function getKeycode(evt) {
// Get 'KeyboardEvent.key', handling legacy browsers
export function getKey(evt) {
// Are we getting a proper key value?
- if (evt.key !== undefined) {
- // IE and Edge use some ancient version of the spec
- // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8860571/
- switch (evt.key) {
- case 'Spacebar': return ' ';
- case 'Esc': return 'Escape';
- case 'Scroll': return 'ScrollLock';
- case 'Win': return 'Meta';
- case 'Apps': return 'ContextMenu';
- case 'Up': return 'ArrowUp';
- case 'Left': return 'ArrowLeft';
- case 'Right': return 'ArrowRight';
- case 'Down': return 'ArrowDown';
- case 'Del': return 'Delete';
- case 'Divide': return '/';
- case 'Multiply': return '*';
- case 'Subtract': return '-';
- case 'Add': return '+';
- case 'Decimal': return evt.char;
- }
-
+ if ((evt.key !== undefined) && (evt.key !== 'Unidentified')) {
// Mozilla isn't fully in sync with the spec yet
switch (evt.key) {
case 'OS': return 'Meta';
@@ -110,18 +89,7 @@ export function getKey(evt) {
return 'Delete';
}
- // IE and Edge need special handling, but for everyone else we
- // can trust the value provided
- if (!browser.isIE() && !browser.isEdge()) {
- return evt.key;
- }
-
- // IE and Edge have broken handling of AltGraph so we can only
- // trust them for non-printable characters (and unfortunately
- // they also specify 'Unidentified' for some problem keys)
- if ((evt.key.length !== 1) && (evt.key !== 'Unidentified')) {
- return evt.key;
- }
+ return evt.key;
}
// Try to deduce it based on the physical key
@@ -189,6 +157,21 @@ export function getKeysym(evt) {
}
}
+ // Windows sends alternating symbols for some keys when using a
+ // Japanese layout. We have no way of synchronising with the IM
+ // running on the remote system, so we send some combined keysym
+ // instead and hope for the best.
+ if (browser.isWindows()) {
+ switch (key) {
+ case 'Zenkaku':
+ case 'Hankaku':
+ return KeyTable.XK_Zenkaku_Hankaku;
+ case 'Romaji':
+ case 'KanaMode':
+ return KeyTable.XK_Romaji;
+ }
+ }
+
return DOMKeyTable[key][location];
}
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/input/vkeys.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/input/vkeys.js
index f84109b25..dacc35809 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/core/input/vkeys.js
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/input/vkeys.js
@@ -13,7 +13,6 @@ export default {
0x08: 'Backspace',
0x09: 'Tab',
0x0a: 'NumpadClear',
- 0x0c: 'Numpad5', // IE11 sends evt.keyCode: 12 when numlock is off
0x0d: 'Enter',
0x10: 'ShiftLeft',
0x11: 'ControlLeft',
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/input/xtscancodes.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/input/xtscancodes.js
index 514809c6f..8ab9c17fd 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/core/input/xtscancodes.js
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/input/xtscancodes.js
@@ -1,8 +1,8 @@
/*
- * This file is auto-generated from keymaps.csv on 2017-05-31 16:20
- * Database checksum sha256(92fd165507f2a3b8c5b3fa56e425d45788dbcb98cf067a307527d91ce22cab94)
+ * This file is auto-generated from keymaps.csv
+ * Database checksum sha256(76d68c10e97d37fe2ea459e210125ae41796253fb217e900bf2983ade13a7920)
* To re-generate, run:
- * keymap-gen --lang=js code-map keymaps.csv html atset1
+ * keymap-gen code-map --lang=js keymaps.csv html atset1
*/
export default {
"Again": 0xe005, /* html:Again (Again) -> linux:129 (KEY_AGAIN) -> atset1:57349 */
@@ -111,6 +111,8 @@ export default {
"KeyX": 0x2d, /* html:KeyX (KeyX) -> linux:45 (KEY_X) -> atset1:45 */
"KeyY": 0x15, /* html:KeyY (KeyY) -> linux:21 (KEY_Y) -> atset1:21 */
"KeyZ": 0x2c, /* html:KeyZ (KeyZ) -> linux:44 (KEY_Z) -> atset1:44 */
+ "Lang1": 0x72, /* html:Lang1 (Lang1) -> linux:122 (KEY_HANGEUL) -> atset1:114 */
+ "Lang2": 0x71, /* html:Lang2 (Lang2) -> linux:123 (KEY_HANJA) -> atset1:113 */
"Lang3": 0x78, /* html:Lang3 (Lang3) -> linux:90 (KEY_KATAKANA) -> atset1:120 */
"Lang4": 0x77, /* html:Lang4 (Lang4) -> linux:91 (KEY_HIRAGANA) -> atset1:119 */
"Lang5": 0x76, /* html:Lang5 (Lang5) -> linux:85 (KEY_ZENKAKUHANKAKU) -> atset1:118 */
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/ra2.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/ra2.js
new file mode 100644
index 000000000..d330b848d
--- /dev/null
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/ra2.js
@@ -0,0 +1,312 @@
+import { encodeUTF8 } from './util/strings.js';
+import EventTargetMixin from './util/eventtarget.js';
+import legacyCrypto from './crypto/crypto.js';
+
+class RA2Cipher {
+ constructor() {
+ this._cipher = null;
+ this._counter = new Uint8Array(16);
+ }
+
+ async setKey(key) {
+ this._cipher = await legacyCrypto.importKey(
+ "raw", key, { name: "AES-EAX" }, false, ["encrypt, decrypt"]);
+ }
+
+ async makeMessage(message) {
+ const ad = new Uint8Array([(message.length & 0xff00) >>> 8, message.length & 0xff]);
+ const encrypted = await legacyCrypto.encrypt({
+ name: "AES-EAX",
+ iv: this._counter,
+ additionalData: ad,
+ }, this._cipher, message);
+ for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
+ const res = new Uint8Array(message.length + 2 + 16);
+ res.set(ad);
+ res.set(encrypted, 2);
+ return res;
+ }
+
+ async receiveMessage(length, encrypted) {
+ const ad = new Uint8Array([(length & 0xff00) >>> 8, length & 0xff]);
+ const res = await legacyCrypto.decrypt({
+ name: "AES-EAX",
+ iv: this._counter,
+ additionalData: ad,
+ }, this._cipher, encrypted);
+ for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
+ return res;
+ }
+}
+
+export default class RSAAESAuthenticationState extends EventTargetMixin {
+ constructor(sock, getCredentials) {
+ super();
+ this._hasStarted = false;
+ this._checkSock = null;
+ this._checkCredentials = null;
+ this._approveServerResolve = null;
+ this._sockReject = null;
+ this._credentialsReject = null;
+ this._approveServerReject = null;
+ this._sock = sock;
+ this._getCredentials = getCredentials;
+ }
+
+ _waitSockAsync(len) {
+ return new Promise((resolve, reject) => {
+ const hasData = () => !this._sock.rQwait('RA2', len);
+ if (hasData()) {
+ resolve();
+ } else {
+ this._checkSock = () => {
+ if (hasData()) {
+ resolve();
+ this._checkSock = null;
+ this._sockReject = null;
+ }
+ };
+ this._sockReject = reject;
+ }
+ });
+ }
+
+ _waitApproveKeyAsync() {
+ return new Promise((resolve, reject) => {
+ this._approveServerResolve = resolve;
+ this._approveServerReject = reject;
+ });
+ }
+
+ _waitCredentialsAsync(subtype) {
+ const hasCredentials = () => {
+ if (subtype === 1 && this._getCredentials().username !== undefined &&
+ this._getCredentials().password !== undefined) {
+ return true;
+ } else if (subtype === 2 && this._getCredentials().password !== undefined) {
+ return true;
+ }
+ return false;
+ };
+ return new Promise((resolve, reject) => {
+ if (hasCredentials()) {
+ resolve();
+ } else {
+ this._checkCredentials = () => {
+ if (hasCredentials()) {
+ resolve();
+ this._checkCredentials = null;
+ this._credentialsReject = null;
+ }
+ };
+ this._credentialsReject = reject;
+ }
+ });
+ }
+
+ checkInternalEvents() {
+ if (this._checkSock !== null) {
+ this._checkSock();
+ }
+ if (this._checkCredentials !== null) {
+ this._checkCredentials();
+ }
+ }
+
+ approveServer() {
+ if (this._approveServerResolve !== null) {
+ this._approveServerResolve();
+ this._approveServerResolve = null;
+ }
+ }
+
+ disconnect() {
+ if (this._sockReject !== null) {
+ this._sockReject(new Error("disconnect normally"));
+ this._sockReject = null;
+ }
+ if (this._credentialsReject !== null) {
+ this._credentialsReject(new Error("disconnect normally"));
+ this._credentialsReject = null;
+ }
+ if (this._approveServerReject !== null) {
+ this._approveServerReject(new Error("disconnect normally"));
+ this._approveServerReject = null;
+ }
+ }
+
+ async negotiateRA2neAuthAsync() {
+ this._hasStarted = true;
+ // 1: Receive server public key
+ await this._waitSockAsync(4);
+ const serverKeyLengthBuffer = this._sock.rQpeekBytes(4);
+ const serverKeyLength = this._sock.rQshift32();
+ if (serverKeyLength < 1024) {
+ throw new Error("RA2: server public key is too short: " + serverKeyLength);
+ } else if (serverKeyLength > 8192) {
+ throw new Error("RA2: server public key is too long: " + serverKeyLength);
+ }
+ const serverKeyBytes = Math.ceil(serverKeyLength / 8);
+ await this._waitSockAsync(serverKeyBytes * 2);
+ const serverN = this._sock.rQshiftBytes(serverKeyBytes);
+ const serverE = this._sock.rQshiftBytes(serverKeyBytes);
+ const serverRSACipher = await legacyCrypto.importKey(
+ "raw", { n: serverN, e: serverE }, { name: "RSA-PKCS1-v1_5" }, false, ["encrypt"]);
+ const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2);
+ serverPublickey.set(serverKeyLengthBuffer);
+ serverPublickey.set(serverN, 4);
+ serverPublickey.set(serverE, 4 + serverKeyBytes);
+
+ // verify server public key
+ let approveKey = this._waitApproveKeyAsync();
+ this.dispatchEvent(new CustomEvent("serververification", {
+ detail: { type: "RSA", publickey: serverPublickey }
+ }));
+ await approveKey;
+
+ // 2: Send client public key
+ const clientKeyLength = 2048;
+ const clientKeyBytes = Math.ceil(clientKeyLength / 8);
+ const clientRSACipher = (await legacyCrypto.generateKey({
+ name: "RSA-PKCS1-v1_5",
+ modulusLength: clientKeyLength,
+ publicExponent: new Uint8Array([1, 0, 1]),
+ }, true, ["encrypt"])).privateKey;
+ const clientExportedRSAKey = await legacyCrypto.exportKey("raw", clientRSACipher);
+ const clientN = clientExportedRSAKey.n;
+ const clientE = clientExportedRSAKey.e;
+ const clientPublicKey = new Uint8Array(4 + clientKeyBytes * 2);
+ clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24;
+ clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16;
+ clientPublicKey[2] = (clientKeyLength & 0xff00) >>> 8;
+ clientPublicKey[3] = clientKeyLength & 0xff;
+ clientPublicKey.set(clientN, 4);
+ clientPublicKey.set(clientE, 4 + clientKeyBytes);
+ this._sock.sQpushBytes(clientPublicKey);
+ this._sock.flush();
+
+ // 3: Send client random
+ const clientRandom = new Uint8Array(16);
+ window.crypto.getRandomValues(clientRandom);
+ const clientEncryptedRandom = await legacyCrypto.encrypt(
+ { name: "RSA-PKCS1-v1_5" }, serverRSACipher, clientRandom);
+ const clientRandomMessage = new Uint8Array(2 + serverKeyBytes);
+ clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8;
+ clientRandomMessage[1] = serverKeyBytes & 0xff;
+ clientRandomMessage.set(clientEncryptedRandom, 2);
+ this._sock.sQpushBytes(clientRandomMessage);
+ this._sock.flush();
+
+ // 4: Receive server random
+ await this._waitSockAsync(2);
+ if (this._sock.rQshift16() !== clientKeyBytes) {
+ throw new Error("RA2: wrong encrypted message length");
+ }
+ const serverEncryptedRandom = this._sock.rQshiftBytes(clientKeyBytes);
+ const serverRandom = await legacyCrypto.decrypt(
+ { name: "RSA-PKCS1-v1_5" }, clientRSACipher, serverEncryptedRandom);
+ if (serverRandom === null || serverRandom.length !== 16) {
+ throw new Error("RA2: corrupted server encrypted random");
+ }
+
+ // 5: Compute session keys and set ciphers
+ let clientSessionKey = new Uint8Array(32);
+ let serverSessionKey = new Uint8Array(32);
+ clientSessionKey.set(serverRandom);
+ clientSessionKey.set(clientRandom, 16);
+ serverSessionKey.set(clientRandom);
+ serverSessionKey.set(serverRandom, 16);
+ clientSessionKey = await window.crypto.subtle.digest("SHA-1", clientSessionKey);
+ clientSessionKey = new Uint8Array(clientSessionKey).slice(0, 16);
+ serverSessionKey = await window.crypto.subtle.digest("SHA-1", serverSessionKey);
+ serverSessionKey = new Uint8Array(serverSessionKey).slice(0, 16);
+ const clientCipher = new RA2Cipher();
+ await clientCipher.setKey(clientSessionKey);
+ const serverCipher = new RA2Cipher();
+ await serverCipher.setKey(serverSessionKey);
+
+ // 6: Compute and exchange hashes
+ let serverHash = new Uint8Array(8 + serverKeyBytes * 2 + clientKeyBytes * 2);
+ let clientHash = new Uint8Array(8 + serverKeyBytes * 2 + clientKeyBytes * 2);
+ serverHash.set(serverPublickey);
+ serverHash.set(clientPublicKey, 4 + serverKeyBytes * 2);
+ clientHash.set(clientPublicKey);
+ clientHash.set(serverPublickey, 4 + clientKeyBytes * 2);
+ serverHash = await window.crypto.subtle.digest("SHA-1", serverHash);
+ clientHash = await window.crypto.subtle.digest("SHA-1", clientHash);
+ serverHash = new Uint8Array(serverHash);
+ clientHash = new Uint8Array(clientHash);
+ this._sock.sQpushBytes(await clientCipher.makeMessage(clientHash));
+ this._sock.flush();
+ await this._waitSockAsync(2 + 20 + 16);
+ if (this._sock.rQshift16() !== 20) {
+ throw new Error("RA2: wrong server hash");
+ }
+ const serverHashReceived = await serverCipher.receiveMessage(
+ 20, this._sock.rQshiftBytes(20 + 16));
+ if (serverHashReceived === null) {
+ throw new Error("RA2: failed to authenticate the message");
+ }
+ for (let i = 0; i < 20; i++) {
+ if (serverHashReceived[i] !== serverHash[i]) {
+ throw new Error("RA2: wrong server hash");
+ }
+ }
+
+ // 7: Receive subtype
+ await this._waitSockAsync(2 + 1 + 16);
+ if (this._sock.rQshift16() !== 1) {
+ throw new Error("RA2: wrong subtype");
+ }
+ let subtype = (await serverCipher.receiveMessage(
+ 1, this._sock.rQshiftBytes(1 + 16)));
+ if (subtype === null) {
+ throw new Error("RA2: failed to authenticate the message");
+ }
+ subtype = subtype[0];
+ let waitCredentials = this._waitCredentialsAsync(subtype);
+ if (subtype === 1) {
+ if (this._getCredentials().username === undefined ||
+ this._getCredentials().password === undefined) {
+ this.dispatchEvent(new CustomEvent(
+ "credentialsrequired",
+ { detail: { types: ["username", "password"] } }));
+ }
+ } else if (subtype === 2) {
+ if (this._getCredentials().password === undefined) {
+ this.dispatchEvent(new CustomEvent(
+ "credentialsrequired",
+ { detail: { types: ["password"] } }));
+ }
+ } else {
+ throw new Error("RA2: wrong subtype");
+ }
+ await waitCredentials;
+ let username;
+ if (subtype === 1) {
+ username = encodeUTF8(this._getCredentials().username).slice(0, 255);
+ } else {
+ username = "";
+ }
+ const password = encodeUTF8(this._getCredentials().password).slice(0, 255);
+ const credentials = new Uint8Array(username.length + password.length + 2);
+ credentials[0] = username.length;
+ credentials[username.length + 1] = password.length;
+ for (let i = 0; i < username.length; i++) {
+ credentials[i + 1] = username.charCodeAt(i);
+ }
+ for (let i = 0; i < password.length; i++) {
+ credentials[username.length + 2 + i] = password.charCodeAt(i);
+ }
+ this._sock.sQpushBytes(await clientCipher.makeMessage(credentials));
+ this._sock.flush();
+ }
+
+ get hasStarted() {
+ return this._hasStarted;
+ }
+
+ set hasStarted(s) {
+ this._hasStarted = s;
+ }
+}
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/rfb.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/rfb.js
index f35d503f1..f2deb0e7b 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/core/rfb.js
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/rfb.js
@@ -21,11 +21,11 @@ import Keyboard from "./input/keyboard.js";
import GestureHandler from "./input/gesturehandler.js";
import Cursor from "./util/cursor.js";
import Websock from "./websock.js";
-import DES from "./des.js";
import KeyTable from "./input/keysym.js";
import XtScancode from "./input/xtscancodes.js";
import { encodings } from "./encodings.js";
-import "./util/polyfill.js";
+import RSAAESAuthenticationState from "./ra2.js";
+import legacyCrypto from "./crypto/crypto.js";
import RawDecoder from "./decoders/raw.js";
import CopyRectDecoder from "./decoders/copyrect.js";
@@ -33,6 +33,8 @@ import RREDecoder from "./decoders/rre.js";
import HextileDecoder from "./decoders/hextile.js";
import TightDecoder from "./decoders/tight.js";
import TightPNGDecoder from "./decoders/tightpng.js";
+import ZRLEDecoder from "./decoders/zrle.js";
+import JPEGDecoder from "./decoders/jpeg.js";
// How many seconds to wait for a disconnect to finish
const DISCONNECT_TIMEOUT = 3;
@@ -51,6 +53,22 @@ const GESTURE_SCRLSENS = 50;
const DOUBLE_TAP_TIMEOUT = 1000;
const DOUBLE_TAP_THRESHOLD = 50;
+// Security types
+const securityTypeNone = 1;
+const securityTypeVNCAuth = 2;
+const securityTypeRA2ne = 6;
+const securityTypeTight = 16;
+const securityTypeVeNCrypt = 19;
+const securityTypeXVP = 22;
+const securityTypeARD = 30;
+const securityTypeMSLogonII = 113;
+
+// Special Tight security types
+const securityTypeUnixLogon = 129;
+
+// VeNCrypt security types
+const securityTypePlain = 256;
+
// Extended clipboard pseudo-encoding formats
const extendedClipboardFormatText = 1;
/*eslint-disable no-unused-vars */
@@ -67,20 +85,31 @@ const extendedClipboardActionPeek = 1 << 26;
const extendedClipboardActionNotify = 1 << 27;
const extendedClipboardActionProvide = 1 << 28;
-
export default class RFB extends EventTargetMixin {
- constructor(target, url, options) {
+ constructor(target, urlOrChannel, options) {
if (!target) {
throw new Error("Must specify target");
}
- if (!url) {
- throw new Error("Must specify URL");
+ if (!urlOrChannel) {
+ throw new Error("Must specify URL, WebSocket or RTCDataChannel");
+ }
+
+ // We rely on modern APIs which might not be available in an
+ // insecure context
+ if (!window.isSecureContext) {
+ Log.Error("noVNC requires a secure context (TLS). Expect crashes!");
}
super();
this._target = target;
- this._url = url;
+
+ if (typeof urlOrChannel === "string") {
+ this._url = urlOrChannel;
+ } else {
+ this._url = null;
+ this._rawChannel = urlOrChannel;
+ }
// Connection details
options = options || {};
@@ -94,6 +123,7 @@ export default class RFB extends EventTargetMixin {
this._rfbInitState = '';
this._rfbAuthScheme = -1;
this._rfbCleanDisconnect = true;
+ this._rfbRSAAESAuthenticationState = null;
// Server capabilities
this._rfbVersion = 0;
@@ -130,6 +160,7 @@ export default class RFB extends EventTargetMixin {
this._flushing = false; // Display flushing state
this._keyboard = null; // Keyboard input handler object
this._gestures = null; // Gesture input handler object
+ this._resizeObserver = null; // Resize observer object
// Timers
this._disconnTimer = null; // disconnection timer
@@ -167,10 +198,12 @@ export default class RFB extends EventTargetMixin {
// Bound event handlers
this._eventHandlers = {
focusCanvas: this._focusCanvas.bind(this),
- windowResize: this._windowResize.bind(this),
+ handleResize: this._handleResize.bind(this),
handleMouse: this._handleMouse.bind(this),
handleWheel: this._handleWheel.bind(this),
handleGesture: this._handleGesture.bind(this),
+ handleRSAAESCredentialsRequired: this._handleRSAAESCredentialsRequired.bind(this),
+ handleRSAAESServerVerification: this._handleRSAAESServerVerification.bind(this),
};
// main setup
@@ -187,8 +220,6 @@ export default class RFB extends EventTargetMixin {
this._canvas.style.margin = 'auto';
// Some browsers add an outline on focus
this._canvas.style.outline = 'none';
- // IE miscalculates width without this :(
- this._canvas.style.flexShrink = '0';
this._canvas.width = 0;
this._canvas.height = 0;
this._canvas.tabIndex = -1;
@@ -215,6 +246,8 @@ export default class RFB extends EventTargetMixin {
this._decoders[encodings.encodingHextile] = new HextileDecoder();
this._decoders[encodings.encodingTight] = new TightDecoder();
this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder();
+ this._decoders[encodings.encodingZRLE] = new ZRLEDecoder();
+ this._decoders[encodings.encodingJPEG] = new JPEGDecoder();
// NB: nothing that needs explicit teardown should be done
// before this point, since this can throw an exception
@@ -224,66 +257,26 @@ export default class RFB extends EventTargetMixin {
Log.Error("Display exception: " + exc);
throw exc;
}
- this._display.onflush = this._onFlush.bind(this);
this._keyboard = new Keyboard(this._canvas);
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
+ this._remoteCapsLock = null; // Null indicates unknown or irrelevant
+ this._remoteNumLock = null;
this._gestures = new GestureHandler();
this._sock = new Websock();
- this._sock.on('message', () => {
- this._handleMessage();
- });
- this._sock.on('open', () => {
- if ((this._rfbConnectionState === 'connecting') &&
- (this._rfbInitState === '')) {
- this._rfbInitState = 'ProtocolVersion';
- Log.Debug("Starting VNC handshake");
- } else {
- this._fail("Unexpected server connection while " +
- this._rfbConnectionState);
- }
- });
- this._sock.on('close', (e) => {
- Log.Debug("WebSocket on-close event");
- let msg = "";
- if (e.code) {
- msg = "(code: " + e.code;
- if (e.reason) {
- msg += ", reason: " + e.reason;
- }
- msg += ")";
- }
- switch (this._rfbConnectionState) {
- case 'connecting':
- this._fail("Connection closed " + msg);
- break;
- case 'connected':
- // Handle disconnects that were initiated server-side
- this._updateConnectionState('disconnecting');
- this._updateConnectionState('disconnected');
- break;
- case 'disconnecting':
- // Normal disconnection path
- this._updateConnectionState('disconnected');
- break;
- case 'disconnected':
- this._fail("Unexpected server disconnect " +
- "when already disconnected " + msg);
- break;
- default:
- this._fail("Unexpected server disconnect before connecting " +
- msg);
- break;
- }
- this._sock.off('close');
- });
- this._sock.on('error', e => Log.Warn("WebSocket on-error event"));
+ this._sock.on('open', this._socketOpen.bind(this));
+ this._sock.on('close', this._socketClose.bind(this));
+ this._sock.on('message', this._handleMessage.bind(this));
+ this._sock.on('error', this._socketError.bind(this));
- // Slight delay of the actual connection so that the caller has
- // time to set up callbacks
- setTimeout(this._updateConnectionState.bind(this, 'connecting'));
+ this._expectedClientWidth = null;
+ this._expectedClientHeight = null;
+ this._resizeObserver = new ResizeObserver(this._eventHandlers.handleResize);
+
+ // All prepared, kick off the connection
+ this._updateConnectionState('connecting');
Log.Debug("<< RFB.constructor");
@@ -294,6 +287,7 @@ export default class RFB extends EventTargetMixin {
this._viewOnly = false;
this._clipViewport = false;
+ this._clippingViewport = false;
this._scaleViewport = false;
this._resizeSession = false;
@@ -325,6 +319,16 @@ export default class RFB extends EventTargetMixin {
get capabilities() { return this._capabilities; }
+ get clippingViewport() { return this._clippingViewport; }
+ _setClippingViewport(on) {
+ if (on === this._clippingViewport) {
+ return;
+ }
+ this._clippingViewport = on;
+ this.dispatchEvent(new CustomEvent("clippingviewport",
+ { detail: this._clippingViewport }));
+ }
+
get touchButton() { return 0; }
set touchButton(button) { Log.Warn("Using old API!"); }
@@ -412,11 +416,20 @@ export default class RFB extends EventTargetMixin {
this._sock.off('error');
this._sock.off('message');
this._sock.off('open');
+ if (this._rfbRSAAESAuthenticationState !== null) {
+ this._rfbRSAAESAuthenticationState.disconnect();
+ }
+ }
+
+ approveServer() {
+ if (this._rfbRSAAESAuthenticationState !== null) {
+ this._rfbRSAAESAuthenticationState.approveServer();
+ }
}
sendCredentials(creds) {
this._rfbCredentials = creds;
- setTimeout(this._initMsg.bind(this), 0);
+ this._resumeAuthentication();
}
sendCtrlAltDel() {
@@ -472,8 +485,8 @@ export default class RFB extends EventTargetMixin {
}
}
- focus() {
- this._canvas.focus();
+ focus(options) {
+ this._canvas.focus(options);
}
blur() {
@@ -489,31 +502,66 @@ export default class RFB extends EventTargetMixin {
this._clipboardText = text;
RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);
} else {
- let data = new Uint8Array(text.length);
- for (let i = 0; i < text.length; i++) {
- // FIXME: text can have values outside of Latin1/Uint8
- data[i] = text.charCodeAt(i);
+ let length, i;
+ let data;
+
+ length = 0;
+ // eslint-disable-next-line no-unused-vars
+ for (let codePoint of text) {
+ length++;
+ }
+
+ data = new Uint8Array(length);
+
+ i = 0;
+ for (let codePoint of text) {
+ let code = codePoint.codePointAt(0);
+
+ /* Only ISO 8859-1 is supported */
+ if (code > 0xff) {
+ code = 0x3f; // '?'
+ }
+
+ data[i++] = code;
}
RFB.messages.clientCutText(this._sock, data);
}
}
+ getImageData() {
+ return this._display.getImageData();
+ }
+
+ toDataURL(type, encoderOptions) {
+ return this._display.toDataURL(type, encoderOptions);
+ }
+
+ toBlob(callback, type, quality) {
+ return this._display.toBlob(callback, type, quality);
+ }
+
// ===== PRIVATE METHODS =====
_connect() {
Log.Debug(">> RFB.connect");
- Log.Info("connecting to " + this._url);
-
- try {
- // WebSocket.onopen transitions to the RFB init states
+ if (this._url) {
+ Log.Info(`connecting to ${this._url}`);
this._sock.open(this._url, this._wsProtocols);
- } catch (e) {
- if (e.name === 'SyntaxError') {
- this._fail("Invalid host or port (" + e + ")");
- } else {
- this._fail("Error when opening socket (" + e + ")");
+ } else {
+ Log.Info(`attaching ${this._rawChannel} to Websock`);
+ this._sock.attach(this._rawChannel);
+
+ if (this._sock.readyState === 'closed') {
+ throw Error("Cannot use already closed WebSocket/RTCDataChannel");
+ }
+
+ if (this._sock.readyState === 'open') {
+ // FIXME: _socketOpen() can in theory call _fail(), which
+ // isn't allowed this early, but I'm not sure that can
+ // happen without a bug messing up our state variables
+ this._socketOpen();
}
}
@@ -525,9 +573,8 @@ export default class RFB extends EventTargetMixin {
this._cursor.attach(this._canvas);
this._refreshCursor();
- // Monitor size changes of the screen
- // FIXME: Use ResizeObserver, or hidden overflow
- window.addEventListener('resize', this._eventHandlers.windowResize);
+ // Monitor size changes of the screen element
+ this._resizeObserver.observe(this._screen);
// Always grab focus on some kind of click event
this._canvas.addEventListener("mousedown", this._eventHandlers.focusCanvas);
@@ -568,7 +615,7 @@ export default class RFB extends EventTargetMixin {
this._canvas.removeEventListener('contextmenu', this._eventHandlers.handleMouse);
this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas);
this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas);
- window.removeEventListener('resize', this._eventHandlers.windowResize);
+ this._resizeObserver.disconnect();
this._keyboard.ungrab();
this._gestures.detach();
this._sock.close();
@@ -587,12 +634,64 @@ export default class RFB extends EventTargetMixin {
Log.Debug("<< RFB.disconnect");
}
+ _socketOpen() {
+ if ((this._rfbConnectionState === 'connecting') &&
+ (this._rfbInitState === '')) {
+ this._rfbInitState = 'ProtocolVersion';
+ Log.Debug("Starting VNC handshake");
+ } else {
+ this._fail("Unexpected server connection while " +
+ this._rfbConnectionState);
+ }
+ }
+
+ _socketClose(e) {
+ Log.Debug("WebSocket on-close event");
+ let msg = "";
+ if (e.code) {
+ msg = "(code: " + e.code;
+ if (e.reason) {
+ msg += ", reason: " + e.reason;
+ }
+ msg += ")";
+ }
+ switch (this._rfbConnectionState) {
+ case 'connecting':
+ this._fail("Connection closed " + msg);
+ break;
+ case 'connected':
+ // Handle disconnects that were initiated server-side
+ this._updateConnectionState('disconnecting');
+ this._updateConnectionState('disconnected');
+ break;
+ case 'disconnecting':
+ // Normal disconnection path
+ this._updateConnectionState('disconnected');
+ break;
+ case 'disconnected':
+ this._fail("Unexpected server disconnect " +
+ "when already disconnected " + msg);
+ break;
+ default:
+ this._fail("Unexpected server disconnect before connecting " +
+ msg);
+ break;
+ }
+ this._sock.off('close');
+ // Delete reference to raw channel to allow cleanup.
+ this._rawChannel = null;
+ }
+
+ _socketError(e) {
+ Log.Warn("WebSocket on-error event");
+ }
+
_focusCanvas(event) {
if (!this.focusOnClick) {
return;
}
- this.focus();
+ this.focus({ preventScroll: true });
}
_setDesktopName(name) {
@@ -602,7 +701,26 @@ export default class RFB extends EventTargetMixin {
{ detail: { name: this._fbName } }));
}
- _windowResize(event) {
+ _saveExpectedClientSize() {
+ this._expectedClientWidth = this._screen.clientWidth;
+ this._expectedClientHeight = this._screen.clientHeight;
+ }
+
+ _currentClientSize() {
+ return [this._screen.clientWidth, this._screen.clientHeight];
+ }
+
+ _clientHasExpectedSize() {
+ const [currentWidth, currentHeight] = this._currentClientSize();
+ return currentWidth == this._expectedClientWidth &&
+ currentHeight == this._expectedClientHeight;
+ }
+
+ _handleResize() {
+ // Don't change anything if the client size is already as expected
+ if (this._clientHasExpectedSize()) {
+ return;
+ }
// If the window resized then our screen element might have
// as well. Update the viewport dimensions.
window.requestAnimationFrame(() => {
@@ -642,6 +760,16 @@ export default class RFB extends EventTargetMixin {
const size = this._screenSize();
this._display.viewportChangeSize(size.w, size.h);
this._fixScrollbars();
+ this._setClippingViewport(size.w < this._display.width ||
+ size.h < this._display.height);
+ } else {
+ this._setClippingViewport(false);
+ }
+
+ // When changing clipping we might show or hide scrollbars.
+ // This causes the expected client dimensions to change.
+ if (curClip !== newClip) {
+ this._saveExpectedClientSize();
}
}
@@ -667,6 +795,7 @@ export default class RFB extends EventTargetMixin {
}
const size = this._screenSize();
+
RFB.messages.setDesktopSize(this._sock,
Math.floor(size.w), Math.floor(size.h),
this._screenID, this._screenFlags);
@@ -682,12 +811,13 @@ export default class RFB extends EventTargetMixin {
}
_fixScrollbars() {
- // This is a hack because Chrome screws up the calculation
- // for when scrollbars are needed. So to fix it we temporarily
- // toggle them off and on.
+ // This is a hack because Safari on macOS screws up the calculation
+ // for when scrollbars are needed. We get scrollbars when making the
+ // browser smaller, despite remote resize being enabled. So to fix it
+ // we temporarily toggle them off and on.
const orig = this._screen.style.overflow;
this._screen.style.overflow = 'hidden';
- // Force Chrome to recalculate the layout by asking for
+ // Force Safari to recalculate the layout by asking for
// an element's dimensions
this._screen.getBoundingClientRect();
this._screen.style.overflow = orig;
@@ -830,7 +960,7 @@ export default class RFB extends EventTargetMixin {
}
_handleMessage() {
- if (this._sock.rQlen === 0) {
+ if (this._sock.rQwait("message", 1)) {
Log.Warn("handleMessage called on an empty receive queue");
return;
}
@@ -847,18 +977,53 @@ export default class RFB extends EventTargetMixin {
if (!this._normalMsg()) {
break;
}
- if (this._sock.rQlen === 0) {
+ if (this._sock.rQwait("message", 1)) {
+ break;
+ }
+ }
+ break;
+ case 'connecting':
+ while (this._rfbConnectionState === 'connecting') {
+ if (!this._initMsg()) {
break;
}
}
break;
default:
- this._initMsg();
+ Log.Error("Got data while in an invalid state");
break;
}
}
- _handleKeyEvent(keysym, code, down) {
+ _handleKeyEvent(keysym, code, down, numlock, capslock) {
+ // If remote state of capslock is known, and it doesn't match the local led state of
+ // the keyboard, we send a capslock keypress first to bring it into sync.
+ // If we just pressed CapsLock, or we toggled it remotely due to it being out of sync
+ // we clear the remote state so that we don't send duplicate or spurious fixes,
+ // since it may take some time to receive the new remote CapsLock state.
+ if (code == 'CapsLock' && down) {
+ this._remoteCapsLock = null;
+ }
+ if (this._remoteCapsLock !== null && capslock !== null && this._remoteCapsLock !== capslock && down) {
+ Log.Debug("Fixing remote caps lock");
+
+ this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', true);
+ this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', false);
+ // We clear the remote capsLock state when we do this to prevent issues with doing this twice
+ // before we receive an update of the the remote state.
+ this._remoteCapsLock = null;
+ }
+
+ // Logic for numlock is exactly the same.
+ if (code == 'NumLock' && down) {
+ this._remoteNumLock = null;
+ }
+ if (this._remoteNumLock !== null && numlock !== null && this._remoteNumLock !== numlock && down) {
+ Log.Debug("Fixing remote num lock");
+ this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', true);
+ this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', false);
+ this._remoteNumLock = null;
+ }
this.sendKey(keysym, code, down);
}
@@ -1225,13 +1390,13 @@ export default class RFB extends EventTargetMixin {
break;
case "003.003":
case "003.006": // UltraVNC
- case "003.889": // Apple Remote Desktop
this._rfbVersion = 3.3;
break;
case "003.007":
this._rfbVersion = 3.7;
break;
case "003.008":
+ case "003.889": // Apple Remote Desktop
case "004.000": // Intel AMT KVM
case "004.001": // RealVNC 4.6
case "005.000": // RealVNC 5.3
@@ -1246,7 +1411,8 @@ export default class RFB extends EventTargetMixin {
while (repeaterID.length < 250) {
repeaterID += "\0";
}
- this._sock.sendString(repeaterID);
+ this._sock.sQpushString(repeaterID);
+ this._sock.flush();
return true;
}
@@ -1256,24 +1422,30 @@ export default class RFB extends EventTargetMixin {
const cversion = "00" + parseInt(this._rfbVersion, 10) +
".00" + ((this._rfbVersion * 10) % 10);
- this._sock.sendString("RFB " + cversion + "\n");
+ this._sock.sQpushString("RFB " + cversion + "\n");
+ this._sock.flush();
Log.Debug('Sent ProtocolVersion: ' + cversion);
this._rfbInitState = 'Security';
}
- _negotiateSecurity() {
- // Polyfill since IE and PhantomJS doesn't have
- // TypedArray.includes()
- function includes(item, array) {
- for (let i = 0; i < array.length; i++) {
- if (array[i] === item) {
- return true;
- }
- }
- return false;
- }
+ _isSupportedSecurityType(type) {
+ const clientTypes = [
+ securityTypeNone,
+ securityTypeVNCAuth,
+ securityTypeRA2ne,
+ securityTypeTight,
+ securityTypeVeNCrypt,
+ securityTypeXVP,
+ securityTypeARD,
+ securityTypeMSLogonII,
+ securityTypePlain,
+ ];
+ return clientTypes.includes(type);
+ }
+
+ _negotiateSecurity() {
if (this._rfbVersion >= 3.7) {
// Server sends supported list, client decides
const numTypes = this._sock.rQshift8();
@@ -1283,28 +1455,28 @@ export default class RFB extends EventTargetMixin {
this._rfbInitState = "SecurityReason";
this._securityContext = "no security types";
this._securityStatus = 1;
- return this._initMsg();
+ return true;
}
const types = this._sock.rQshiftBytes(numTypes);
Log.Debug("Server security types: " + types);
- // Look for each auth in preferred order
- if (includes(1, types)) {
- this._rfbAuthScheme = 1; // None
- } else if (includes(22, types)) {
- this._rfbAuthScheme = 22; // XVP
- } else if (includes(16, types)) {
- this._rfbAuthScheme = 16; // Tight
- } else if (includes(2, types)) {
- this._rfbAuthScheme = 2; // VNC Auth
- } else if (includes(19, types)) {
- this._rfbAuthScheme = 19; // VeNCrypt Auth
- } else {
+ // Look for a matching security type in the order that the
+ // server prefers
+ this._rfbAuthScheme = -1;
+ for (let type of types) {
+ if (this._isSupportedSecurityType(type)) {
+ this._rfbAuthScheme = type;
+ break;
+ }
+ }
+
+ if (this._rfbAuthScheme === -1) {
return this._fail("Unsupported security types (types: " + types + ")");
}
- this._sock.send([this._rfbAuthScheme]);
+ this._sock.sQpush8(this._rfbAuthScheme);
+ this._sock.flush();
} else {
// Server decides
if (this._sock.rQwait("security scheme", 4)) { return false; }
@@ -1314,14 +1486,14 @@ export default class RFB extends EventTargetMixin {
this._rfbInitState = "SecurityReason";
this._securityContext = "authentication scheme";
this._securityStatus = 1;
- return this._initMsg();
+ return true;
}
}
this._rfbInitState = 'Authentication';
Log.Debug('Authenticating using scheme: ' + this._rfbAuthScheme);
- return this._initMsg(); // jump to authentication
+ return true;
}
_handleSecurityReason() {
@@ -1366,12 +1538,15 @@ export default class RFB extends EventTargetMixin {
return false;
}
- const xvpAuthStr = String.fromCharCode(this._rfbCredentials.username.length) +
- String.fromCharCode(this._rfbCredentials.target.length) +
- this._rfbCredentials.username +
- this._rfbCredentials.target;
- this._sock.sendString(xvpAuthStr);
- this._rfbAuthScheme = 2;
+ this._sock.sQpush8(this._rfbCredentials.username.length);
+ this._sock.sQpush8(this._rfbCredentials.target.length);
+ this._sock.sQpushString(this._rfbCredentials.username);
+ this._sock.sQpushString(this._rfbCredentials.target);
+
+ this._sock.flush();
+
+ this._rfbAuthScheme = securityTypeVNCAuth;
+
return this._negotiateAuthentication();
}
@@ -1389,7 +1564,9 @@ export default class RFB extends EventTargetMixin {
return this._fail("Unsupported VeNCrypt version " + major + "." + minor);
}
- this._sock.send([0, 2]);
+ this._sock.sQpush8(0);
+ this._sock.sQpush8(2);
+ this._sock.flush();
this._rfbVeNCryptState = 1;
}
@@ -1429,40 +1606,55 @@ export default class RFB extends EventTargetMixin {
subtypes.push(this._sock.rQshift32());
}
- // 256 = Plain subtype
- if (subtypes.indexOf(256) != -1) {
- // 0x100 = 256
- this._sock.send([0, 0, 1, 0]);
- this._rfbVeNCryptState = 4;
- } else {
- return this._fail("VeNCrypt Plain subtype not offered by server");
- }
- }
+ // Look for a matching security type in the order that the
+ // server prefers
+ this._rfbAuthScheme = -1;
+ for (let type of subtypes) {
+ // Avoid getting in to a loop
+ if (type === securityTypeVeNCrypt) {
+ continue;
+ }
- // negotiated Plain subtype, server waits for password
- if (this._rfbVeNCryptState == 4) {
- if (!this._rfbCredentials.username ||
- !this._rfbCredentials.password) {
- this.dispatchEvent(new CustomEvent(
- "credentialsrequired",
- { detail: { types: ["username", "password"] } }));
- return false;
+ if (this._isSupportedSecurityType(type)) {
+ this._rfbAuthScheme = type;
+ break;
+ }
}
- const user = encodeUTF8(this._rfbCredentials.username);
- const pass = encodeUTF8(this._rfbCredentials.password);
+ if (this._rfbAuthScheme === -1) {
+ return this._fail("Unsupported security types (types: " + subtypes + ")");
+ }
- // XXX we assume lengths are <= 255 (should not be an issue in the real world)
- this._sock.send([0, 0, 0, user.length]);
- this._sock.send([0, 0, 0, pass.length]);
- this._sock.sendString(user);
- this._sock.sendString(pass);
+ this._sock.sQpush32(this._rfbAuthScheme);
+ this._sock.flush();
- this._rfbInitState = "SecurityResult";
+ this._rfbVeNCryptState = 4;
return true;
}
}
+ _negotiatePlainAuth() {
+ if (this._rfbCredentials.username === undefined ||
+ this._rfbCredentials.password === undefined) {
+ this.dispatchEvent(new CustomEvent(
+ "credentialsrequired",
+ { detail: { types: ["username", "password"] } }));
+ return false;
+ }
+
+ const user = encodeUTF8(this._rfbCredentials.username);
+ const pass = encodeUTF8(this._rfbCredentials.password);
+
+ this._sock.sQpush32(user.length);
+ this._sock.sQpush32(pass.length);
+ this._sock.sQpushString(user);
+ this._sock.sQpushString(pass);
+ this._sock.flush();
+
+ this._rfbInitState = "SecurityResult";
+ return true;
+ }
+
_negotiateStdVNCAuth() {
if (this._sock.rQwait("auth challenge", 16)) { return false; }
@@ -1476,11 +1668,82 @@ export default class RFB extends EventTargetMixin {
// TODO(directxman12): make genDES not require an Array
const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16));
const response = RFB.genDES(this._rfbCredentials.password, challenge);
- this._sock.send(response);
+ this._sock.sQpushBytes(response);
+ this._sock.flush();
this._rfbInitState = "SecurityResult";
return true;
}
+ _negotiateARDAuth() {
+
+ if (this._rfbCredentials.username === undefined ||
+ this._rfbCredentials.password === undefined) {
+ this.dispatchEvent(new CustomEvent(
+ "credentialsrequired",
+ { detail: { types: ["username", "password"] } }));
+ return false;
+ }
+
+ if (this._rfbCredentials.ardPublicKey != undefined &&
+ this._rfbCredentials.ardCredentials != undefined) {
+ // if the async web crypto is done return the results
+ this._sock.sQpushBytes(this._rfbCredentials.ardCredentials);
+ this._sock.sQpushBytes(this._rfbCredentials.ardPublicKey);
+ this._sock.flush();
+ this._rfbCredentials.ardCredentials = null;
+ this._rfbCredentials.ardPublicKey = null;
+ this._rfbInitState = "SecurityResult";
+ return true;
+ }
+
+ if (this._sock.rQwait("read ard", 4)) { return false; }
+
+ let generator = this._sock.rQshiftBytes(2); // DH base generator value
+
+ let keyLength = this._sock.rQshift16();
+
+ if (this._sock.rQwait("read ard keylength", keyLength*2, 4)) { return false; }
+
+ // read the server values
+ let prime = this._sock.rQshiftBytes(keyLength); // predetermined prime modulus
+ let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key
+
+ let clientKey = legacyCrypto.generateKey(
+ { name: "DH", g: generator, p: prime }, false, ["deriveBits"]);
+ this._negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey);
+
+ return false;
+ }
+
+ async _negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey) {
+ const clientPublicKey = legacyCrypto.exportKey("raw", clientKey.publicKey);
+ const sharedKey = legacyCrypto.deriveBits(
+ { name: "DH", public: serverPublicKey }, clientKey.privateKey, keyLength * 8);
+
+ const username = encodeUTF8(this._rfbCredentials.username).substring(0, 63);
+ const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
+
+ const credentials = window.crypto.getRandomValues(new Uint8Array(128));
+ for (let i = 0; i < username.length; i++) {
+ credentials[i] = username.charCodeAt(i);
+ }
+ credentials[username.length] = 0;
+ for (let i = 0; i < password.length; i++) {
+ credentials[64 + i] = password.charCodeAt(i);
+ }
+ credentials[64 + password.length] = 0;
+
+ const key = await legacyCrypto.digest("MD5", sharedKey);
+ const cipher = await legacyCrypto.importKey(
+ "raw", key, { name: "AES-ECB" }, false, ["encrypt"]);
+ const encrypted = await legacyCrypto.encrypt({ name: "AES-ECB" }, cipher, credentials);
+
+ this._rfbCredentials.ardCredentials = encrypted;
+ this._rfbCredentials.ardPublicKey = clientPublicKey;
+
+ this._resumeAuthentication();
+ }
+
_negotiateTightUnixAuth() {
if (this._rfbCredentials.username === undefined ||
this._rfbCredentials.password === undefined) {
@@ -1490,10 +1753,12 @@ export default class RFB extends EventTargetMixin {
return false;
}
- this._sock.send([0, 0, 0, this._rfbCredentials.username.length]);
- this._sock.send([0, 0, 0, this._rfbCredentials.password.length]);
- this._sock.sendString(this._rfbCredentials.username);
- this._sock.sendString(this._rfbCredentials.password);
+ this._sock.sQpush32(this._rfbCredentials.username.length);
+ this._sock.sQpush32(this._rfbCredentials.password.length);
+ this._sock.sQpushString(this._rfbCredentials.username);
+ this._sock.sQpushString(this._rfbCredentials.password);
+ this._sock.flush();
+
this._rfbInitState = "SecurityResult";
return true;
}
@@ -1531,7 +1796,8 @@ export default class RFB extends EventTargetMixin {
"vendor or signature");
}
Log.Debug("Selected tunnel type: " + clientSupportedTunnelTypes[0]);
- this._sock.send([0, 0, 0, 0]); // use NOTUNNEL
+ this._sock.sQpush32(0); // use NOTUNNEL
+ this._sock.flush();
return false; // wait until we receive the sub auth count to continue
} else {
return this._fail("Server wanted tunnels, but doesn't support " +
@@ -1581,19 +1847,20 @@ export default class RFB extends EventTargetMixin {
for (let authType in clientSupportedTypes) {
if (serverSupportedTypes.indexOf(authType) != -1) {
- this._sock.send([0, 0, 0, clientSupportedTypes[authType]]);
+ this._sock.sQpush32(clientSupportedTypes[authType]);
+ this._sock.flush();
Log.Debug("Selected authentication type: " + authType);
switch (authType) {
case 'STDVNOAUTH__': // no auth
this._rfbInitState = 'SecurityResult';
return true;
- case 'STDVVNCAUTH_': // VNC auth
- this._rfbAuthScheme = 2;
- return this._initMsg();
- case 'TGHTULGNAUTH': // UNIX auth
- this._rfbAuthScheme = 129;
- return this._initMsg();
+ case 'STDVVNCAUTH_':
+ this._rfbAuthScheme = securityTypeVNCAuth;
+ return true;
+ case 'TGHTULGNAUTH':
+ this._rfbAuthScheme = securityTypeUnixLogon;
+ return true;
default:
return this._fail("Unsupported tiny auth scheme " +
"(scheme: " + authType + ")");
@@ -1604,31 +1871,124 @@ export default class RFB extends EventTargetMixin {
return this._fail("No supported sub-auth types!");
}
+ _handleRSAAESCredentialsRequired(event) {
+ this.dispatchEvent(event);
+ }
+
+ _handleRSAAESServerVerification(event) {
+ this.dispatchEvent(event);
+ }
+
+ _negotiateRA2neAuth() {
+ if (this._rfbRSAAESAuthenticationState === null) {
+ this._rfbRSAAESAuthenticationState = new RSAAESAuthenticationState(this._sock, () => this._rfbCredentials);
+ this._rfbRSAAESAuthenticationState.addEventListener(
+ "serververification", this._eventHandlers.handleRSAAESServerVerification);
+ this._rfbRSAAESAuthenticationState.addEventListener(
+ "credentialsrequired", this._eventHandlers.handleRSAAESCredentialsRequired);
+ }
+ this._rfbRSAAESAuthenticationState.checkInternalEvents();
+ if (!this._rfbRSAAESAuthenticationState.hasStarted) {
+ this._rfbRSAAESAuthenticationState.negotiateRA2neAuthAsync()
+ .catch((e) => {
+ if (e.message !== "disconnect normally") {
+ this._fail(e.message);
+ }
+ })
+ .then(() => {
+ this._rfbInitState = "SecurityResult";
+ return true;
+ }).finally(() => {
+ this._rfbRSAAESAuthenticationState.removeEventListener(
+ "serververification", this._eventHandlers.handleRSAAESServerVerification);
+ this._rfbRSAAESAuthenticationState.removeEventListener(
+ "credentialsrequired", this._eventHandlers.handleRSAAESCredentialsRequired);
+ this._rfbRSAAESAuthenticationState = null;
+ });
+ }
+ return false;
+ }
+
+ _negotiateMSLogonIIAuth() {
+ if (this._sock.rQwait("mslogonii dh param", 24)) { return false; }
+
+ if (this._rfbCredentials.username === undefined ||
+ this._rfbCredentials.password === undefined) {
+ this.dispatchEvent(new CustomEvent(
+ "credentialsrequired",
+ { detail: { types: ["username", "password"] } }));
+ return false;
+ }
+
+ const g = this._sock.rQshiftBytes(8);
+ const p = this._sock.rQshiftBytes(8);
+ const A = this._sock.rQshiftBytes(8);
+ const dhKey = legacyCrypto.generateKey({ name: "DH", g: g, p: p }, true, ["deriveBits"]);
+ const B = legacyCrypto.exportKey("raw", dhKey.publicKey);
+ const secret = legacyCrypto.deriveBits({ name: "DH", public: A }, dhKey.privateKey, 64);
+
+ const key = legacyCrypto.importKey("raw", secret, { name: "DES-CBC" }, false, ["encrypt"]);
+ const username = encodeUTF8(this._rfbCredentials.username).substring(0, 255);
+ const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
+ let usernameBytes = new Uint8Array(256);
+ let passwordBytes = new Uint8Array(64);
+ window.crypto.getRandomValues(usernameBytes);
+ window.crypto.getRandomValues(passwordBytes);
+ for (let i = 0; i < username.length; i++) {
+ usernameBytes[i] = username.charCodeAt(i);
+ }
+ usernameBytes[username.length] = 0;
+ for (let i = 0; i < password.length; i++) {
+ passwordBytes[i] = password.charCodeAt(i);
+ }
+ passwordBytes[password.length] = 0;
+ usernameBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, usernameBytes);
+ passwordBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, passwordBytes);
+ this._sock.sQpushBytes(B);
+ this._sock.sQpushBytes(usernameBytes);
+ this._sock.sQpushBytes(passwordBytes);
+ this._sock.flush();
+ this._rfbInitState = "SecurityResult";
+ return true;
+ }
+
_negotiateAuthentication() {
switch (this._rfbAuthScheme) {
- case 1: // no auth
+ case securityTypeNone:
if (this._rfbVersion >= 3.8) {
this._rfbInitState = 'SecurityResult';
- return true;
+ } else {
+ this._rfbInitState = 'ClientInitialisation';
}
- this._rfbInitState = 'ClientInitialisation';
- return this._initMsg();
+ return true;
- case 22: // XVP auth
+ case securityTypeXVP:
return this._negotiateXvpAuth();
- case 2: // VNC authentication
+ case securityTypeARD:
+ return this._negotiateARDAuth();
+
+ case securityTypeVNCAuth:
return this._negotiateStdVNCAuth();
- case 16: // TightVNC Security Type
+ case securityTypeTight:
return this._negotiateTightAuth();
- case 19: // VeNCrypt Security Type
+ case securityTypeVeNCrypt:
return this._negotiateVeNCryptAuth();
- case 129: // TightVNC UNIX Security Type
+ case securityTypePlain:
+ return this._negotiatePlainAuth();
+
+ case securityTypeUnixLogon:
return this._negotiateTightUnixAuth();
+ case securityTypeRA2ne:
+ return this._negotiateRA2neAuth();
+
+ case securityTypeMSLogonII:
+ return this._negotiateMSLogonIIAuth();
+
default:
return this._fail("Unsupported auth scheme (scheme: " +
this._rfbAuthScheme + ")");
@@ -1643,13 +2003,13 @@ export default class RFB extends EventTargetMixin {
if (status === 0) { // OK
this._rfbInitState = 'ClientInitialisation';
Log.Debug('Authentication OK');
- return this._initMsg();
+ return true;
} else {
if (this._rfbVersion >= 3.8) {
this._rfbInitState = "SecurityReason";
this._securityContext = "security result";
this._securityStatus = status;
- return this._initMsg();
+ return true;
} else {
this.dispatchEvent(new CustomEvent(
"securityfailure",
@@ -1757,6 +2117,8 @@ export default class RFB extends EventTargetMixin {
if (this._fbDepth == 24) {
encs.push(encodings.encodingTight);
encs.push(encodings.encodingTightPNG);
+ encs.push(encodings.encodingZRLE);
+ encs.push(encodings.encodingJPEG);
encs.push(encodings.encodingHextile);
encs.push(encodings.encodingRRE);
}
@@ -1769,6 +2131,7 @@ export default class RFB extends EventTargetMixin {
encs.push(encodings.pseudoEncodingDesktopSize);
encs.push(encodings.pseudoEncodingLastRect);
encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
+ encs.push(encodings.pseudoEncodingQEMULedEvent);
encs.push(encodings.pseudoEncodingExtendedDesktopSize);
encs.push(encodings.pseudoEncodingXvp);
encs.push(encodings.pseudoEncodingFence);
@@ -1810,7 +2173,8 @@ export default class RFB extends EventTargetMixin {
return this._handleSecurityReason();
case 'ClientInitialisation':
- this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation
+ this._sock.sQpush8(this._shared ? 1 : 0); // ClientInitialisation
+ this._sock.flush();
this._rfbInitState = 'ServerInitialisation';
return true;
@@ -1823,6 +2187,14 @@ export default class RFB extends EventTargetMixin {
}
}
+ // Resume authentication handshake after it was paused for some
+ // reason, e.g. waiting for a password from the user
+ _resumeAuthentication() {
+ // We use setTimeout() so it's run in its own context, just like
+ // it originally did via the WebSocket's event handler
+ setTimeout(this._initMsg.bind(this), 0);
+ }
+
_handleSetColourMapMsg() {
Log.Debug("SetColorMapEntries");
@@ -1984,7 +2356,7 @@ export default class RFB extends EventTargetMixin {
textData = textData.slice(0, -1);
}
- textData = textData.replace("\r\n", "\n");
+ textData = textData.replaceAll("\r\n", "\n");
this.dispatchEvent(new CustomEvent(
"clipboard",
@@ -2115,19 +2487,11 @@ export default class RFB extends EventTargetMixin {
default:
this._fail("Unexpected server message (type " + msgType + ")");
- Log.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30));
+ Log.Debug("sock.rQpeekBytes(30): " + this._sock.rQpeekBytes(30));
return true;
}
}
- _onFlush() {
- this._flushing = false;
- // Resume processing
- if (this._sock.rQlen > 0) {
- this._handleMessage();
- }
- }
-
_framebufferUpdate() {
if (this._FBU.rects === 0) {
if (this._sock.rQwait("FBU header", 3, 1)) { return false; }
@@ -2138,7 +2502,14 @@ export default class RFB extends EventTargetMixin {
// to avoid building up an excessive queue
if (this._display.pending()) {
this._flushing = true;
- this._display.flush();
+ this._display.flush()
+ .then(() => {
+ this._flushing = false;
+ // Resume processing
+ if (!this._sock.rQwait("message", 1)) {
+ this._handleMessage();
+ }
+ });
return false;
}
}
@@ -2148,13 +2519,13 @@ export default class RFB extends EventTargetMixin {
if (this._sock.rQwait("rect header", 12)) { return false; }
/* New FramebufferUpdate */
- const hdr = this._sock.rQshiftBytes(12);
- this._FBU.x = (hdr[0] << 8) + hdr[1];
- this._FBU.y = (hdr[2] << 8) + hdr[3];
- this._FBU.width = (hdr[4] << 8) + hdr[5];
- this._FBU.height = (hdr[6] << 8) + hdr[7];
- this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) +
- (hdr[10] << 8) + hdr[11], 10);
+ this._FBU.x = this._sock.rQshift16();
+ this._FBU.y = this._sock.rQshift16();
+ this._FBU.width = this._sock.rQshift16();
+ this._FBU.height = this._sock.rQshift16();
+ this._FBU.encoding = this._sock.rQshift32();
+ /* Encodings are signed */
+ this._FBU.encoding >>= 0;
}
if (!this._handleRect()) {
@@ -2183,15 +2554,7 @@ export default class RFB extends EventTargetMixin {
return this._handleCursor();
case encodings.pseudoEncodingQEMUExtendedKeyEvent:
- // Old Safari doesn't support creating keyboard events
- try {
- const keyboardEvent = document.createEvent("keyboardEvent");
- if (keyboardEvent.code !== undefined) {
- this._qemuExtKeyEventSupported = true;
- }
- } catch (err) {
- // Do nothing
- }
+ this._qemuExtKeyEventSupported = true;
return true;
case encodings.pseudoEncodingDesktopName:
@@ -2204,6 +2567,9 @@ export default class RFB extends EventTargetMixin {
case encodings.pseudoEncodingExtendedDesktopSize:
return this._handleExtendedDesktopSize();
+ case encodings.pseudoEncodingQEMULedEvent:
+ return this._handleLedEvent();
+
default:
return this._handleDataRect();
}
@@ -2381,6 +2747,21 @@ export default class RFB extends EventTargetMixin {
return true;
}
+ _handleLedEvent() {
+ if (this._sock.rQwait("LED Status", 1)) {
+ return false;
+ }
+
+ let data = this._sock.rQshift8();
+ // ScrollLock state can be retrieved with data & 1. This is currently not needed.
+ let numLock = data & 2 ? true : false;
+ let capsLock = data & 4 ? true : false;
+ this._remoteCapsLock = capsLock;
+ this._remoteNumLock = numLock;
+
+ return true;
+ }
+
_handleExtendedDesktopSize() {
if (this._sock.rQwait("ExtendedDesktopSize", 4)) {
return false;
@@ -2396,26 +2777,18 @@ export default class RFB extends EventTargetMixin {
const firstUpdate = !this._supportsSetDesktopSize;
this._supportsSetDesktopSize = true;
- // Normally we only apply the current resize mode after a
- // window resize event. However there is no such trigger on the
- // initial connect. And we don't know if the server supports
- // resizing until we've gotten here.
- if (firstUpdate) {
- this._requestRemoteResize();
- }
-
this._sock.rQskipBytes(1); // number-of-screens
this._sock.rQskipBytes(3); // padding
for (let i = 0; i < numberOfScreens; i += 1) {
// Save the id and flags of the first screen
if (i === 0) {
- this._screenID = this._sock.rQshiftBytes(4); // id
- this._sock.rQskipBytes(2); // x-position
- this._sock.rQskipBytes(2); // y-position
- this._sock.rQskipBytes(2); // width
- this._sock.rQskipBytes(2); // height
- this._screenFlags = this._sock.rQshiftBytes(4); // flags
+ this._screenID = this._sock.rQshift32(); // id
+ this._sock.rQskipBytes(2); // x-position
+ this._sock.rQskipBytes(2); // y-position
+ this._sock.rQskipBytes(2); // width
+ this._sock.rQskipBytes(2); // height
+ this._screenFlags = this._sock.rQshift32(); // flags
} else {
this._sock.rQskipBytes(16);
}
@@ -2453,6 +2826,14 @@ export default class RFB extends EventTargetMixin {
this._resize(this._FBU.width, this._FBU.height);
}
+ // Normally we only apply the current resize mode after a
+ // window resize event. However there is no such trigger on the
+ // initial connect. And we don't know if the server supports
+ // resizing until we've gotten here.
+ if (firstUpdate) {
+ this._requestRemoteResize();
+ }
+
return true;
}
@@ -2493,6 +2874,9 @@ export default class RFB extends EventTargetMixin {
this._updateScale();
this._updateContinuousUpdates();
+
+ // Keep this size until browser client size changes
+ this._saveExpectedClientSize();
}
_xvpOp(ver, op) {
@@ -2545,28 +2929,22 @@ export default class RFB extends EventTargetMixin {
static genDES(password, challenge) {
const passwordChars = password.split('').map(c => c.charCodeAt(0));
- return (new DES(passwordChars)).encrypt(challenge);
+ const key = legacyCrypto.importKey(
+ "raw", passwordChars, { name: "DES-ECB" }, false, ["encrypt"]);
+ return legacyCrypto.encrypt({ name: "DES-ECB" }, key, challenge);
}
}
// Class Methods
RFB.messages = {
keyEvent(sock, keysym, down) {
- const buff = sock._sQ;
- const offset = sock._sQlen;
+ sock.sQpush8(4); // msg-type
+ sock.sQpush8(down);
- buff[offset] = 4; // msg-type
- buff[offset + 1] = down;
+ sock.sQpush16(0);
- buff[offset + 2] = 0;
- buff[offset + 3] = 0;
+ sock.sQpush32(keysym);
- buff[offset + 4] = (keysym >> 24);
- buff[offset + 5] = (keysym >> 16);
- buff[offset + 6] = (keysym >> 8);
- buff[offset + 7] = keysym;
-
- sock._sQlen += 8;
sock.flush();
},
@@ -2580,46 +2958,28 @@ RFB.messages = {
return xtScanCode;
}
- const buff = sock._sQ;
- const offset = sock._sQlen;
+ sock.sQpush8(255); // msg-type
+ sock.sQpush8(0); // sub msg-type
- buff[offset] = 255; // msg-type
- buff[offset + 1] = 0; // sub msg-type
+ sock.sQpush16(down);
- buff[offset + 2] = (down >> 8);
- buff[offset + 3] = down;
-
- buff[offset + 4] = (keysym >> 24);
- buff[offset + 5] = (keysym >> 16);
- buff[offset + 6] = (keysym >> 8);
- buff[offset + 7] = keysym;
+ sock.sQpush32(keysym);
const RFBkeycode = getRFBkeycode(keycode);
- buff[offset + 8] = (RFBkeycode >> 24);
- buff[offset + 9] = (RFBkeycode >> 16);
- buff[offset + 10] = (RFBkeycode >> 8);
- buff[offset + 11] = RFBkeycode;
+ sock.sQpush32(RFBkeycode);
- sock._sQlen += 12;
sock.flush();
},
pointerEvent(sock, x, y, mask) {
- const buff = sock._sQ;
- const offset = sock._sQlen;
+ sock.sQpush8(5); // msg-type
- buff[offset] = 5; // msg-type
+ sock.sQpush8(mask);
- buff[offset + 1] = mask;
+ sock.sQpush16(x);
+ sock.sQpush16(y);
- buff[offset + 2] = x >> 8;
- buff[offset + 3] = x;
-
- buff[offset + 4] = y >> 8;
- buff[offset + 5] = y;
-
- sock._sQlen += 6;
sock.flush();
},
@@ -2719,14 +3079,11 @@ RFB.messages = {
},
clientCutText(sock, data, extended = false) {
- const buff = sock._sQ;
- const offset = sock._sQlen;
+ sock.sQpush8(6); // msg-type
- buff[offset] = 6; // msg-type
-
- buff[offset + 1] = 0; // padding
- buff[offset + 2] = 0; // padding
- buff[offset + 3] = 0; // padding
+ sock.sQpush8(0); // padding
+ sock.sQpush8(0); // padding
+ sock.sQpush8(0); // padding
let length;
if (extended) {
@@ -2735,121 +3092,63 @@ RFB.messages = {
length = data.length;
}
- buff[offset + 4] = length >> 24;
- buff[offset + 5] = length >> 16;
- buff[offset + 6] = length >> 8;
- buff[offset + 7] = length;
-
- sock._sQlen += 8;
-
- // We have to keep track of from where in the data we begin creating the
- // buffer for the flush in the next iteration.
- let dataOffset = 0;
-
- let remaining = data.length;
- while (remaining > 0) {
-
- let flushSize = Math.min(remaining, (sock._sQbufferSize - sock._sQlen));
- for (let i = 0; i < flushSize; i++) {
- buff[sock._sQlen + i] = data[dataOffset + i];
- }
-
- sock._sQlen += flushSize;
- sock.flush();
-
- remaining -= flushSize;
- dataOffset += flushSize;
- }
-
+ sock.sQpush32(length);
+ sock.sQpushBytes(data);
+ sock.flush();
},
setDesktopSize(sock, width, height, id, flags) {
- const buff = sock._sQ;
- const offset = sock._sQlen;
+ sock.sQpush8(251); // msg-type
- buff[offset] = 251; // msg-type
- buff[offset + 1] = 0; // padding
- buff[offset + 2] = width >> 8; // width
- buff[offset + 3] = width;
- buff[offset + 4] = height >> 8; // height
- buff[offset + 5] = height;
+ sock.sQpush8(0); // padding
- buff[offset + 6] = 1; // number-of-screens
- buff[offset + 7] = 0; // padding
+ sock.sQpush16(width);
+ sock.sQpush16(height);
+
+ sock.sQpush8(1); // number-of-screens
+
+ sock.sQpush8(0); // padding
// screen array
- buff[offset + 8] = id >> 24; // id
- buff[offset + 9] = id >> 16;
- buff[offset + 10] = id >> 8;
- buff[offset + 11] = id;
- buff[offset + 12] = 0; // x-position
- buff[offset + 13] = 0;
- buff[offset + 14] = 0; // y-position
- buff[offset + 15] = 0;
- buff[offset + 16] = width >> 8; // width
- buff[offset + 17] = width;
- buff[offset + 18] = height >> 8; // height
- buff[offset + 19] = height;
- buff[offset + 20] = flags >> 24; // flags
- buff[offset + 21] = flags >> 16;
- buff[offset + 22] = flags >> 8;
- buff[offset + 23] = flags;
+ sock.sQpush32(id);
+ sock.sQpush16(0); // x-position
+ sock.sQpush16(0); // y-position
+ sock.sQpush16(width);
+ sock.sQpush16(height);
+ sock.sQpush32(flags);
- sock._sQlen += 24;
sock.flush();
},
clientFence(sock, flags, payload) {
- const buff = sock._sQ;
- const offset = sock._sQlen;
+ sock.sQpush8(248); // msg-type
- buff[offset] = 248; // msg-type
+ sock.sQpush8(0); // padding
+ sock.sQpush8(0); // padding
+ sock.sQpush8(0); // padding
- buff[offset + 1] = 0; // padding
- buff[offset + 2] = 0; // padding
- buff[offset + 3] = 0; // padding
+ sock.sQpush32(flags);
- buff[offset + 4] = flags >> 24; // flags
- buff[offset + 5] = flags >> 16;
- buff[offset + 6] = flags >> 8;
- buff[offset + 7] = flags;
+ sock.sQpush8(payload.length);
+ sock.sQpushString(payload);
- const n = payload.length;
-
- buff[offset + 8] = n; // length
-
- for (let i = 0; i < n; i++) {
- buff[offset + 9 + i] = payload.charCodeAt(i);
- }
-
- sock._sQlen += 9 + n;
sock.flush();
},
enableContinuousUpdates(sock, enable, x, y, width, height) {
- const buff = sock._sQ;
- const offset = sock._sQlen;
+ sock.sQpush8(150); // msg-type
- buff[offset] = 150; // msg-type
- buff[offset + 1] = enable; // enable-flag
+ sock.sQpush8(enable);
- buff[offset + 2] = x >> 8; // x
- buff[offset + 3] = x;
- buff[offset + 4] = y >> 8; // y
- buff[offset + 5] = y;
- buff[offset + 6] = width >> 8; // width
- buff[offset + 7] = width;
- buff[offset + 8] = height >> 8; // height
- buff[offset + 9] = height;
+ sock.sQpush16(x);
+ sock.sQpush16(y);
+ sock.sQpush16(width);
+ sock.sQpush16(height);
- sock._sQlen += 10;
sock.flush();
},
pixelFormat(sock, depth, trueColor) {
- const buff = sock._sQ;
- const offset = sock._sQlen;
-
let bpp;
if (depth > 16) {
@@ -2862,100 +3161,69 @@ RFB.messages = {
const bits = Math.floor(depth/3);
- buff[offset] = 0; // msg-type
+ sock.sQpush8(0); // msg-type
- buff[offset + 1] = 0; // padding
- buff[offset + 2] = 0; // padding
- buff[offset + 3] = 0; // padding
+ sock.sQpush8(0); // padding
+ sock.sQpush8(0); // padding
+ sock.sQpush8(0); // padding
- buff[offset + 4] = bpp; // bits-per-pixel
- buff[offset + 5] = depth; // depth
- buff[offset + 6] = 0; // little-endian
- buff[offset + 7] = trueColor ? 1 : 0; // true-color
+ sock.sQpush8(bpp);
+ sock.sQpush8(depth);
+ sock.sQpush8(0); // little-endian
+ sock.sQpush8(trueColor ? 1 : 0);
- buff[offset + 8] = 0; // red-max
- buff[offset + 9] = (1 << bits) - 1; // red-max
+ sock.sQpush16((1 << bits) - 1); // red-max
+ sock.sQpush16((1 << bits) - 1); // green-max
+ sock.sQpush16((1 << bits) - 1); // blue-max
- buff[offset + 10] = 0; // green-max
- buff[offset + 11] = (1 << bits) - 1; // green-max
+ sock.sQpush8(bits * 0); // red-shift
+ sock.sQpush8(bits * 1); // green-shift
+ sock.sQpush8(bits * 2); // blue-shift
- buff[offset + 12] = 0; // blue-max
- buff[offset + 13] = (1 << bits) - 1; // blue-max
+ sock.sQpush8(0); // padding
+ sock.sQpush8(0); // padding
+ sock.sQpush8(0); // padding
- buff[offset + 14] = bits * 2; // red-shift
- buff[offset + 15] = bits * 1; // green-shift
- buff[offset + 16] = bits * 0; // blue-shift
-
- buff[offset + 17] = 0; // padding
- buff[offset + 18] = 0; // padding
- buff[offset + 19] = 0; // padding
-
- sock._sQlen += 20;
sock.flush();
},
clientEncodings(sock, encodings) {
- const buff = sock._sQ;
- const offset = sock._sQlen;
+ sock.sQpush8(2); // msg-type
- buff[offset] = 2; // msg-type
- buff[offset + 1] = 0; // padding
+ sock.sQpush8(0); // padding
- buff[offset + 2] = encodings.length >> 8;
- buff[offset + 3] = encodings.length;
-
- let j = offset + 4;
+ sock.sQpush16(encodings.length);
for (let i = 0; i < encodings.length; i++) {
- const enc = encodings[i];
- buff[j] = enc >> 24;
- buff[j + 1] = enc >> 16;
- buff[j + 2] = enc >> 8;
- buff[j + 3] = enc;
-
- j += 4;
+ sock.sQpush32(encodings[i]);
}
- sock._sQlen += j - offset;
sock.flush();
},
fbUpdateRequest(sock, incremental, x, y, w, h) {
- const buff = sock._sQ;
- const offset = sock._sQlen;
-
if (typeof(x) === "undefined") { x = 0; }
if (typeof(y) === "undefined") { y = 0; }
- buff[offset] = 3; // msg-type
- buff[offset + 1] = incremental ? 1 : 0;
+ sock.sQpush8(3); // msg-type
- buff[offset + 2] = (x >> 8) & 0xFF;
- buff[offset + 3] = x & 0xFF;
+ sock.sQpush8(incremental ? 1 : 0);
- buff[offset + 4] = (y >> 8) & 0xFF;
- buff[offset + 5] = y & 0xFF;
+ sock.sQpush16(x);
+ sock.sQpush16(y);
+ sock.sQpush16(w);
+ sock.sQpush16(h);
- buff[offset + 6] = (w >> 8) & 0xFF;
- buff[offset + 7] = w & 0xFF;
-
- buff[offset + 8] = (h >> 8) & 0xFF;
- buff[offset + 9] = h & 0xFF;
-
- sock._sQlen += 10;
sock.flush();
},
xvpOp(sock, ver, op) {
- const buff = sock._sQ;
- const offset = sock._sQlen;
+ sock.sQpush8(250); // msg-type
- buff[offset] = 250; // msg-type
- buff[offset + 1] = 0; // padding
+ sock.sQpush8(0); // padding
- buff[offset + 2] = ver;
- buff[offset + 3] = op;
+ sock.sQpush8(ver);
+ sock.sQpush8(op);
- sock._sQlen += 4;
sock.flush();
}
};
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/util/browser.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/util/browser.js
index 155480142..bbc9f5c1e 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/core/util/browser.js
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/util/browser.js
@@ -45,15 +45,6 @@ try {
export const supportsCursorURIs = _supportsCursorURIs;
-let _supportsImageMetadata = false;
-try {
- new ImageData(new Uint8ClampedArray(4), 1, 1);
- _supportsImageMetadata = true;
-} catch (ex) {
- // ignore failure
-}
-export const supportsImageMetadata = _supportsImageMetadata;
-
let _hasScrollbarGutter = true;
try {
// Create invisible container
@@ -86,35 +77,76 @@ export const hasScrollbarGutter = _hasScrollbarGutter;
* It's better to use feature detection than platform detection.
*/
+/* OS */
+
export function isMac() {
- return navigator && !!(/mac/i).exec(navigator.platform);
+ return !!(/mac/i).exec(navigator.platform);
}
export function isWindows() {
- return navigator && !!(/win/i).exec(navigator.platform);
+ return !!(/win/i).exec(navigator.platform);
}
export function isIOS() {
- return navigator &&
- (!!(/ipad/i).exec(navigator.platform) ||
+ return (!!(/ipad/i).exec(navigator.platform) ||
!!(/iphone/i).exec(navigator.platform) ||
!!(/ipod/i).exec(navigator.platform));
}
+export function isAndroid() {
+ /* Android sets navigator.platform to Linux :/ */
+ return !!navigator.userAgent.match('Android ');
+}
+
+export function isChromeOS() {
+ /* ChromeOS sets navigator.platform to Linux :/ */
+ return !!navigator.userAgent.match(' CrOS ');
+}
+
+/* Browser */
+
export function isSafari() {
- return navigator && (navigator.userAgent.indexOf('Safari') !== -1 &&
- navigator.userAgent.indexOf('Chrome') === -1);
-}
-
-export function isIE() {
- return navigator && !!(/trident/i).exec(navigator.userAgent);
-}
-
-export function isEdge() {
- return navigator && !!(/edge/i).exec(navigator.userAgent);
+ return !!navigator.userAgent.match('Safari/...') &&
+ !navigator.userAgent.match('Chrome/...') &&
+ !navigator.userAgent.match('Chromium/...') &&
+ !navigator.userAgent.match('Epiphany/...');
}
export function isFirefox() {
- return navigator && !!(/firefox/i).exec(navigator.userAgent);
+ return !!navigator.userAgent.match('Firefox/...') &&
+ !navigator.userAgent.match('Seamonkey/...');
}
+export function isChrome() {
+ return !!navigator.userAgent.match('Chrome/...') &&
+ !navigator.userAgent.match('Chromium/...') &&
+ !navigator.userAgent.match('Edg/...') &&
+ !navigator.userAgent.match('OPR/...');
+}
+
+export function isChromium() {
+ return !!navigator.userAgent.match('Chromium/...');
+}
+
+export function isOpera() {
+ return !!navigator.userAgent.match('OPR/...');
+}
+
+export function isEdge() {
+ return !!navigator.userAgent.match('Edg/...');
+}
+
+/* Engine */
+
+export function isGecko() {
+ return !!navigator.userAgent.match('Gecko/...');
+}
+
+export function isWebKit() {
+ return !!navigator.userAgent.match('AppleWebKit/...') &&
+ !navigator.userAgent.match('Chrome/...');
+}
+
+export function isBlink() {
+ return !!navigator.userAgent.match('Chrome/...');
+}
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/util/cursor.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/util/cursor.js
index 4db1dab23..20e75f1b2 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/core/util/cursor.js
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/util/cursor.js
@@ -18,6 +18,10 @@ export default class Cursor {
this._canvas.style.position = 'fixed';
this._canvas.style.zIndex = '65535';
this._canvas.style.pointerEvents = 'none';
+ // Safari on iOS can select the cursor image
+ // https://bugs.webkit.org/show_bug.cgi?id=249223
+ this._canvas.style.userSelect = 'none';
+ this._canvas.style.WebkitUserSelect = 'none';
// Can't use "display" because of Firefox bug #1445997
this._canvas.style.visibility = 'hidden';
}
@@ -43,9 +47,6 @@ export default class Cursor {
if (useFallback) {
document.body.appendChild(this._canvas);
- // FIXME: These don't fire properly except for mouse
- /// movement in IE. We want to also capture element
- // movement, size changes, visibility, etc.
const options = { capture: true, passive: true };
this._target.addEventListener('mouseover', this._eventHandlers.mouseover, options);
this._target.addEventListener('mouseleave', this._eventHandlers.mouseleave, options);
@@ -68,7 +69,9 @@ export default class Cursor {
this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options);
this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options);
- document.body.removeChild(this._canvas);
+ if (document.contains(this._canvas)) {
+ document.body.removeChild(this._canvas);
+ }
}
this._target = null;
@@ -90,14 +93,7 @@ export default class Cursor {
this._canvas.width = w;
this._canvas.height = h;
- let img;
- try {
- // IE doesn't support this
- img = new ImageData(new Uint8ClampedArray(rgba), w, h);
- } catch (ex) {
- img = ctx.createImageData(w, h);
- img.data.set(new Uint8ClampedArray(rgba));
- }
+ let img = new ImageData(new Uint8ClampedArray(rgba), w, h);
ctx.clearRect(0, 0, w, h);
ctx.putImageData(img, 0, 0);
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/util/events.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/util/events.js
index 39eefd459..eb09fe1e2 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/core/util/events.js
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/util/events.js
@@ -65,10 +65,6 @@ export function setCapture(target) {
target.setCapture();
document.captureElement = target;
-
- // IE releases capture on 'click' events which might not trigger
- target.addEventListener('mouseup', releaseCapture);
-
} else {
// Release any existing capture in case this method is
// called multiple times without coordination
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/core/websock.js b/emhttp/plugins/dynamix.vm.manager/novnc/core/websock.js
index 3156aed6f..21327c31a 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/core/websock.js
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/core/websock.js
@@ -1,10 +1,10 @@
/*
- * Websock: high-performance binary WebSockets
+ * Websock: high-performance buffering wrapper
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
- * Websock is similar to the standard WebSocket object but with extra
- * buffer handling.
+ * Websock is similar to the standard WebSocket / RTCDataChannel object
+ * but with extra buffer handling.
*
* Websock has built-in receive queue buffering; the message event
* does not contain actual data but is simply a notification that
@@ -17,14 +17,39 @@ import * as Log from './util/logging.js';
// this has performance issues in some versions Chromium, and
// doesn't gain a tremendous amount of performance increase in Firefox
// at the moment. It may be valuable to turn it on in the future.
-// Also copyWithin() for TypedArrays is not supported in IE 11 or
-// Safari 13 (at the moment we want to support Safari 11).
-const ENABLE_COPYWITHIN = false;
const MAX_RQ_GROW_SIZE = 40 * 1024 * 1024; // 40 MiB
+// Constants pulled from RTCDataChannelState enum
+// https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/readyState#RTCDataChannelState_enum
+const DataChannel = {
+ CONNECTING: "connecting",
+ OPEN: "open",
+ CLOSING: "closing",
+ CLOSED: "closed"
+};
+
+const ReadyStates = {
+ CONNECTING: [WebSocket.CONNECTING, DataChannel.CONNECTING],
+ OPEN: [WebSocket.OPEN, DataChannel.OPEN],
+ CLOSING: [WebSocket.CLOSING, DataChannel.CLOSING],
+ CLOSED: [WebSocket.CLOSED, DataChannel.CLOSED],
+};
+
+// Properties a raw channel must have, WebSocket and RTCDataChannel are two examples
+const rawChannelProps = [
+ "send",
+ "close",
+ "binaryType",
+ "onerror",
+ "onmessage",
+ "onopen",
+ "protocol",
+ "readyState",
+];
+
export default class Websock {
constructor() {
- this._websocket = null; // WebSocket object
+ this._websocket = null; // WebSocket or RTCDataChannel object
this._rQi = 0; // Receive queue index
this._rQlen = 0; // Next write position in the receive queue
@@ -46,27 +71,30 @@ export default class Websock {
}
// Getters and Setters
- get sQ() {
- return this._sQ;
- }
- get rQ() {
- return this._rQ;
- }
+ get readyState() {
+ let subState;
- get rQi() {
- return this._rQi;
- }
+ if (this._websocket === null) {
+ return "unused";
+ }
- set rQi(val) {
- this._rQi = val;
+ subState = this._websocket.readyState;
+
+ if (ReadyStates.CONNECTING.includes(subState)) {
+ return "connecting";
+ } else if (ReadyStates.OPEN.includes(subState)) {
+ return "open";
+ } else if (ReadyStates.CLOSING.includes(subState)) {
+ return "closing";
+ } else if (ReadyStates.CLOSED.includes(subState)) {
+ return "closed";
+ }
+
+ return "unknown";
}
// Receive Queue
- get rQlen() {
- return this._rQlen - this._rQi;
- }
-
rQpeek8() {
return this._rQ[this._rQi];
}
@@ -93,42 +121,47 @@ export default class Websock {
for (let byte = bytes - 1; byte >= 0; byte--) {
res += this._rQ[this._rQi++] << (byte * 8);
}
- return res;
+ return res >>> 0;
}
rQshiftStr(len) {
- if (typeof(len) === 'undefined') { len = this.rQlen; }
let str = "";
// Handle large arrays in steps to avoid long strings on the stack
for (let i = 0; i < len; i += 4096) {
- let part = this.rQshiftBytes(Math.min(4096, len - i));
+ let part = this.rQshiftBytes(Math.min(4096, len - i), false);
str += String.fromCharCode.apply(null, part);
}
return str;
}
- rQshiftBytes(len) {
- if (typeof(len) === 'undefined') { len = this.rQlen; }
+ rQshiftBytes(len, copy=true) {
this._rQi += len;
- return new Uint8Array(this._rQ.buffer, this._rQi - len, len);
+ if (copy) {
+ return this._rQ.slice(this._rQi - len, this._rQi);
+ } else {
+ return this._rQ.subarray(this._rQi - len, this._rQi);
+ }
}
rQshiftTo(target, len) {
- if (len === undefined) { len = this.rQlen; }
// TODO: make this just use set with views when using a ArrayBuffer to store the rQ
target.set(new Uint8Array(this._rQ.buffer, this._rQi, len));
this._rQi += len;
}
- rQslice(start, end = this.rQlen) {
- return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start);
+ rQpeekBytes(len, copy=true) {
+ if (copy) {
+ return this._rQ.slice(this._rQi, this._rQi + len);
+ } else {
+ return this._rQ.subarray(this._rQi, this._rQi + len);
+ }
}
// Check to see if we must wait for 'num' bytes (default to FBU.bytes)
// to be available in the receive queue. Return true if we need to
// wait (and possibly print a debug message), otherwise false.
rQwait(msg, num, goback) {
- if (this.rQlen < num) {
+ if (this._rQlen - this._rQi < num) {
if (goback) {
if (this._rQi < goback) {
throw new Error("rQwait cannot backup " + goback + " bytes");
@@ -142,21 +175,56 @@ export default class Websock {
// Send Queue
+ sQpush8(num) {
+ this._sQensureSpace(1);
+ this._sQ[this._sQlen++] = num;
+ }
+
+ sQpush16(num) {
+ this._sQensureSpace(2);
+ this._sQ[this._sQlen++] = (num >> 8) & 0xff;
+ this._sQ[this._sQlen++] = (num >> 0) & 0xff;
+ }
+
+ sQpush32(num) {
+ this._sQensureSpace(4);
+ this._sQ[this._sQlen++] = (num >> 24) & 0xff;
+ this._sQ[this._sQlen++] = (num >> 16) & 0xff;
+ this._sQ[this._sQlen++] = (num >> 8) & 0xff;
+ this._sQ[this._sQlen++] = (num >> 0) & 0xff;
+ }
+
+ sQpushString(str) {
+ let bytes = str.split('').map(chr => chr.charCodeAt(0));
+ this.sQpushBytes(new Uint8Array(bytes));
+ }
+
+ sQpushBytes(bytes) {
+ for (let offset = 0;offset < bytes.length;) {
+ this._sQensureSpace(1);
+
+ let chunkSize = this._sQbufferSize - this._sQlen;
+ if (chunkSize > bytes.length - offset) {
+ chunkSize = bytes.length - offset;
+ }
+
+ this._sQ.set(bytes.subarray(offset, chunkSize), this._sQlen);
+ this._sQlen += chunkSize;
+ offset += chunkSize;
+ }
+ }
+
flush() {
- if (this._sQlen > 0 && this._websocket.readyState === WebSocket.OPEN) {
- this._websocket.send(this._encodeMessage());
+ if (this._sQlen > 0 && this.readyState === 'open') {
+ this._websocket.send(new Uint8Array(this._sQ.buffer, 0, this._sQlen));
this._sQlen = 0;
}
}
- send(arr) {
- this._sQ.set(arr, this._sQlen);
- this._sQlen += arr.length;
- this.flush();
- }
-
- sendString(str) {
- this.send(str.split('').map(chr => chr.charCodeAt(0)));
+ _sQensureSpace(bytes) {
+ if (this._sQbufferSize - this._sQlen < bytes) {
+ this.flush();
+ }
}
// Event Handlers
@@ -180,12 +248,25 @@ export default class Websock {
}
open(uri, protocols) {
+ this.attach(new WebSocket(uri, protocols));
+ }
+
+ attach(rawChannel) {
this.init();
- this._websocket = new WebSocket(uri, protocols);
- this._websocket.binaryType = 'arraybuffer';
+ // Must get object and class methods to be compatible with the tests.
+ const channelProps = [...Object.keys(rawChannel), ...Object.getOwnPropertyNames(Object.getPrototypeOf(rawChannel))];
+ for (let i = 0; i < rawChannelProps.length; i++) {
+ const prop = rawChannelProps[i];
+ if (channelProps.indexOf(prop) < 0) {
+ throw new Error('Raw channel missing property: ' + prop);
+ }
+ }
+ this._websocket = rawChannel;
+ this._websocket.binaryType = "arraybuffer";
this._websocket.onmessage = this._recvMessage.bind(this);
+
this._websocket.onopen = () => {
Log.Debug('>> WebSock.onopen');
if (this._websocket.protocol) {
@@ -195,11 +276,13 @@ export default class Websock {
this._eventHandlers.open();
Log.Debug("<< WebSock.onopen");
};
+
this._websocket.onclose = (e) => {
Log.Debug(">> WebSock.onclose");
this._eventHandlers.close(e);
Log.Debug("<< WebSock.onclose");
};
+
this._websocket.onerror = (e) => {
Log.Debug(">> WebSock.onerror: " + e);
this._eventHandlers.error(e);
@@ -209,8 +292,8 @@ export default class Websock {
close() {
if (this._websocket) {
- if ((this._websocket.readyState === WebSocket.OPEN) ||
- (this._websocket.readyState === WebSocket.CONNECTING)) {
+ if (this.readyState === 'connecting' ||
+ this.readyState === 'open') {
Log.Info("Closing WebSocket connection");
this._websocket.close();
}
@@ -220,17 +303,12 @@ export default class Websock {
}
// private methods
- _encodeMessage() {
- // Put in a binary arraybuffer
- // according to the spec, you can send ArrayBufferViews with the send method
- return new Uint8Array(this._sQ.buffer, 0, this._sQlen);
- }
// We want to move all the unread data to the start of the queue,
// e.g. compacting.
// The function also expands the receive que if needed, and for
// performance reasons we combine these two actions to avoid
- // unneccessary copying.
+ // unnecessary copying.
_expandCompactRQ(minFit) {
// if we're using less than 1/8th of the buffer even with the incoming bytes, compact in place
// instead of resizing
@@ -246,7 +324,7 @@ export default class Websock {
// we don't want to grow unboundedly
if (this._rQbufferSize > MAX_RQ_GROW_SIZE) {
this._rQbufferSize = MAX_RQ_GROW_SIZE;
- if (this._rQbufferSize - this.rQlen < minFit) {
+ if (this._rQbufferSize - (this._rQlen - this._rQi) < minFit) {
throw new Error("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit");
}
}
@@ -256,11 +334,7 @@ export default class Websock {
this._rQ = new Uint8Array(this._rQbufferSize);
this._rQ.set(new Uint8Array(oldRQbuffer, this._rQi, this._rQlen - this._rQi));
} else {
- if (ENABLE_COPYWITHIN) {
- this._rQ.copyWithin(0, this._rQi, this._rQlen);
- } else {
- this._rQ.set(new Uint8Array(this._rQ.buffer, this._rQi, this._rQlen - this._rQi));
- }
+ this._rQ.copyWithin(0, this._rQi, this._rQlen);
}
this._rQlen = this._rQlen - this._rQi;
@@ -268,25 +342,22 @@ export default class Websock {
}
// push arraybuffer values onto the end of the receive que
- _DecodeMessage(data) {
- const u8 = new Uint8Array(data);
+ _recvMessage(e) {
+ if (this._rQlen == this._rQi) {
+ // All data has now been processed, this means we
+ // can reset the receive queue.
+ this._rQlen = 0;
+ this._rQi = 0;
+ }
+ const u8 = new Uint8Array(e.data);
if (u8.length > this._rQbufferSize - this._rQlen) {
this._expandCompactRQ(u8.length);
}
this._rQ.set(u8, this._rQlen);
this._rQlen += u8.length;
- }
- _recvMessage(e) {
- this._DecodeMessage(e.data);
- if (this.rQlen > 0) {
+ if (this._rQlen - this._rQi > 0) {
this._eventHandlers.message();
- if (this._rQlen == this._rQi) {
- // All data has now been processed, this means we
- // can reset the receive queue.
- this._rQlen = 0;
- this._rQi = 0;
- }
} else {
Log.Debug("Ignoring empty message");
}
diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/package.json b/emhttp/plugins/dynamix.vm.manager/novnc/package.json
index 8fc04e50a..9fa8c312d 100644
--- a/emhttp/plugins/dynamix.vm.manager/novnc/package.json
+++ b/emhttp/plugins/dynamix.vm.manager/novnc/package.json
@@ -1,6 +1,6 @@
{
"name": "@novnc/novnc",
- "version": "1.2.0",
+ "version": "1.5.0",
"description": "An HTML5 VNC client",
"browser": "lib/rfb",
"directories": {
@@ -14,14 +14,12 @@
"VERSION",
"docs/API.md",
"docs/LIBRARY.md",
- "docs/LICENSE*",
- "core",
- "vendor/pako"
+ "docs/LICENSE*"
],
"scripts": {
"lint": "eslint app core po/po2js po/xgettext-html tests utils",
"test": "karma start karma.conf.js",
- "prepublish": "node ./utils/use_require.js --as commonjs --clean"
+ "prepublish": "node ./utils/convert.js --clean"
},
"repository": {
"type": "git",
@@ -29,8 +27,6 @@
},
"author": "Joel Martin (https://github.com/kanaka)",
"contributors": [
- "Solly Ross (https://github.com/directxman12)",
- "Peter Åstrand (https://github.com/astrand)",
"Samuel Mannehed (https://github.com/samhed)",
"Pierre Ossman (https://github.com/CendioOssman)"
],
@@ -40,42 +36,31 @@
},
"homepage": "https://github.com/novnc/noVNC",
"devDependencies": {
- "@babel/core": "*",
- "@babel/plugin-syntax-dynamic-import": "*",
- "@babel/plugin-transform-modules-amd": "*",
- "@babel/plugin-transform-modules-commonjs": "*",
- "@babel/plugin-transform-modules-systemjs": "*",
- "@babel/plugin-transform-modules-umd": "*",
- "@babel/preset-env": "*",
- "@babel/cli": "*",
- "babel-plugin-import-redirect": "*",
- "browserify": "*",
- "babelify": "*",
- "core-js": "*",
- "chai": "*",
- "commander": "*",
- "es-module-loader": "*",
- "eslint": "*",
- "fs-extra": "*",
- "jsdom": "*",
- "karma": "*",
- "karma-mocha": "*",
- "karma-chrome-launcher": "*",
- "@chiragrupani/karma-chromium-edge-launcher": "*",
- "karma-firefox-launcher": "*",
- "karma-ie-launcher": "*",
- "karma-mocha-reporter": "*",
- "karma-safari-launcher": "*",
- "karma-script-launcher": "*",
- "karma-sinon-chai": "*",
- "mocha": "*",
- "node-getopt": "*",
- "po2json": "*",
- "requirejs": "*",
- "rollup": "*",
- "rollup-plugin-node-resolve": "*",
- "sinon": "*",
- "sinon-chai": "*"
+ "@babel/core": "latest",
+ "@babel/preset-env": "latest",
+ "babel-plugin-import-redirect": "latest",
+ "browserify": "latest",
+ "chai": "latest",
+ "commander": "latest",
+ "eslint": "latest",
+ "fs-extra": "latest",
+ "globals": "latest",
+ "jsdom": "latest",
+ "karma": "latest",
+ "karma-mocha": "latest",
+ "karma-chrome-launcher": "latest",
+ "@chiragrupani/karma-chromium-edge-launcher": "latest",
+ "karma-firefox-launcher": "latest",
+ "karma-ie-launcher": "latest",
+ "karma-mocha-reporter": "latest",
+ "karma-safari-launcher": "latest",
+ "karma-script-launcher": "latest",
+ "karma-sinon-chai": "latest",
+ "mocha": "latest",
+ "node-getopt": "latest",
+ "po2json": "latest",
+ "sinon": "latest",
+ "sinon-chai": "latest"
},
"dependencies": {},
"keywords": [
diff --git a/emhttp/plugins/dynamix.vm.manager/scripts/virtiofsd.php b/emhttp/plugins/dynamix.vm.manager/scripts/virtiofsd.php
index 9d4945ab6..b19348cf8 100755
--- a/emhttp/plugins/dynamix.vm.manager/scripts/virtiofsd.php
+++ b/emhttp/plugins/dynamix.vm.manager/scripts/virtiofsd.php
@@ -27,7 +27,9 @@
}
# Check if options file exists. Each option should be on a new line.
if (is_file($file)) $options = explode("\n",file_get_contents($file)) ; else $options = ['--syslog','--inode-file-handles=mandatory','--announce-submounts'];
- $options[] = "--fd=".$argoptions['fd'];
+ if (isset($argoptions['fd'])) {
+ $options[] = "--fd=".$argoptions['fd'];
+}
if (isset($argoptions['o'])) {
$virtiofsoptions = explode(',',$argoptions["o"]);
diff --git a/emhttp/plugins/dynamix.vm.manager/templates/Custom.form.php b/emhttp/plugins/dynamix.vm.manager/templates/Custom.form.php
index 58452f19e..616cbd878 100644
--- a/emhttp/plugins/dynamix.vm.manager/templates/Custom.form.php
+++ b/emhttp/plugins/dynamix.vm.manager/templates/Custom.form.php
@@ -154,7 +154,8 @@
$protocol = $lv->domain_get_vmrc_protocol($dom);
$reply = ['success' => true];
if ($vmrcport > 0) {
- $reply['vmrcurl'] = autov('/plugins/dynamix.vm.manager/'.$protocol.'.html',true).'&autoconnect=true&host=' . $_SERVER['HTTP_HOST'] ;
+ if ($protocol == "vnc") $vmrcscale = "&resize=scale"; else $vmrcscale = "";
+ $reply['vmrcurl'] = autov('/plugins/dynamix.vm.manager/'.$protocol.'.html',true).'&autoconnect=true'.$vmrcscale.'&host=' . $_SERVER['HTTP_HOST'] ;
if ($protocol == "spice") $reply['vmrcurl'] .= '&port=/wsproxy/'.$vmrcport.'/'; else $reply['vmrcurl'] .= '&port=&path=/wsproxy/' . $wsport . '/';
}
} else {
@@ -317,6 +318,18 @@
}
if ($usertemplate == 1) unset($arrConfig['domain']['uuid']);
$xml2 = build_xml_templates($strXML);
+ #disable rename if snapshots exist
+ $snapshots = getvmsnapshots($arrConfig['domain']['name']) ;
+ if ($snapshots != null && count($snapshots) && !$boolNew)
+ {
+ $snaprenamehidden = "";
+ $namedisable = "disabled";
+ $snapcount = count($snapshots);
+ } else {
+ $snaprenamehidden = "hidden";
+ $namedisable = "";
+ $snapcount = "0";
+ };
?>
@@ -336,10 +349,12 @@
diff --git a/emhttp/plugins/dynamix.vm.manager/vnc.html b/emhttp/plugins/dynamix.vm.manager/vnc.html
index 6e9ea184e..edfe49b40 100644
--- a/emhttp/plugins/dynamix.vm.manager/vnc.html
+++ b/emhttp/plugins/dynamix.vm.manager/vnc.html
@@ -12,13 +12,13 @@
http://example.com/?host=HOST&port=PORT&encrypt=1
or the fragment:
http://example.com/#host=HOST&port=PORT&encrypt=1
+
+ 1.5
-->
noVNC
-
-
-
+
@@ -42,31 +42,37 @@
-->
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
-
-
-
-
-
-
-
-
+
+
@@ -89,6 +95,8 @@
no VNC
+
+
Clipboard
+
+ Edit clipboard content in the textarea below.
+