Merge branch 'master' into fix_nfs_ui_so_settings_can_be_made_with_nfs_enabled

This commit is contained in:
dlandon
2025-01-08 18:50:13 -06:00
committed by GitHub
166 changed files with 7324 additions and 2264 deletions

1
.gitignore vendored
View File

@@ -58,7 +58,6 @@ games/
info/
lib64/
man/
share/
# Auto-generated when emhttpd/webGUI start
emhttp/languages/en_US/helptext.dot

View File

@@ -256,7 +256,7 @@ Unraid OS uses these default options when creating a multiple-device pool:
`-dconvert=raid1 -mconvert=raid1`
For more complete documentation, please refer to the btrfs-balance [Manpage](https://btrfs.wiki.kernel.org/index.php/Manpage/btrfs-balance)
For more complete documentation, please refer to the btrfs-balance [Manpage](https://man7.org/linux/man-pages/man8/btrfs-balance.8.html)
*Note: raid5 and raid6 are generally still considered **experimental** by the Linux community*
:end
@@ -269,7 +269,14 @@ For more complete documentation, please refer to the btrfs-balance [Manpage](htt
**Scrub** runs the *btrfs scrub* program which will read all data and metadata blocks from all
devices and verify checksums.
If *Repair corrupted blocks* is checked, *btrfs scrub* will repair corrupted blocks if theres a correct copy available.
*btrfs scrub* will repair corrupted blocks if there is a correct copy available.
:end
:info_zfs_scrub_help:
**Scrub** runs the *zfs scrub* program which will read all data and metadata blocks from all
devices and verify checksums.
Click the **Upgrade Pool** button to upgrade the ZFS pool to enable the latest ZFS features.
:end
:info_scrub_cancel_help:
@@ -282,6 +289,10 @@ If *Repair corrupted blocks* is checked, *btrfs scrub* will repair corrupted blo
The *Options* field is initialized with *--readonly* which specifies check-only. If repair is needed, you should run
a second Check pass, setting the *Options* to *--repair*; this will permit *btrfs check* to fix the file system.
WARNING: **Do not use** *--repair* unless you are advised to do so by a developer or an experienced user,
and then only after having accepted that no fsck successfully repair all types of filesystem corruption.
E.g. some other software or hardware bugs can fatally damage a volume.
After starting a Check, you should Refresh to monitor progress and status. Depending on
how large the file system is, and what errors might be present, the operation can take **a long time** to finish (hours).
Not much info is printed in the window, but you can verify the operation is running by observing the read/write counters
@@ -695,6 +706,12 @@ Summary of security modes:
**Secure** All users including guests have read access, you select which of your users have write access.
**Private** No guest access at all, you select which of your users have read/write, read-only access or no access.
Windows Server Signing:
If you are unable to browse SMB shares with Windows 11 version 24H2 or newer, you need to make some changes to accomodate a new feature called Server Signing. Server Signing is enabled in Unraid and you need to make changes to access Public shares.
You can <u><a href='https://techcommunity.microsoft.com/t5/storage-at-microsoft/accessing-a-third-party-nas-with-smb-in-windows-11-24h2-may-fail/ba-p/4154300', target='_blank'>disable it in Windows</a></u>, or to work with Unraid with Server Signing enabled, the easiest way is to create a user (with a password set) in Unraid with the same name as the Windows account you are using, Windows should then ask you for the credentials.
If you are using a Microsoft account, it may be better to just create a user in Unraid with a simple username and set a password, then in Windows go to Control Panel -> Credential Manager -> Windows credentials -> Add a Windows Credential and add the correct Unraid server name and credentials.
:end
:smb_secure_access_help:
@@ -727,6 +744,15 @@ Select the desired share name and press **Read** to copy the NFS security settin
Select the desired destinations and press **Write** to copy the NFS security settings to the selected shares.
:end
:nfs_security_rules_help:
Put the rule for each IP address on a separate line and terminate the Rule with a new line.
You cannot enter a Rule in the format IP1,IP2(...). Unraid does not format the exports file in that format.
The default rules for every NFS Rule are - async and no_subtree_check.
**Note:** The Public Rule used is '*(rw,sec=sys,insecure,anongid=100,anonuid=99,no_root_squash)'.
:end
:user_add_username_help:
Usernames may be up to 40 characters long and must start with a **lower case letter** or an underscore,
followed by **lower case letters**, digits, underscores, or dashes. They can end with a dollar sign.
@@ -1283,6 +1309,14 @@ The Local Access URLs shown above are based on your current settings.
To adjust URLs or redirects, see the help text for "Use SSL/TLS".
:end
:mgmt_wg_access_urls_help:
These URLs will only work when connected via the appropriate WireGuard tunnel as configured on ***Settings > VPN Manager***
:end
:mgmt_tailscale_access_urls_help:
These URLs will only work when connected to the appropriate Tailscale Tailnet.
:end
:mgmt_certificate_expiration_help:
**Provision** may be used to install a *free* myunraid.net SSL Certificate from
[Let's Encrypt](https://letsencrypt.org/).
@@ -1445,6 +1479,20 @@ your client. Be aware that setting a value of -1 will cause the memory footprin
files/directories you access via NFS this may or may not lead to out-of-memory conditions.
:end
:nfs_server_max_protocol_help:
Select the max NFS Protocol version you want your Unraid server to support.
:end
:nfs_client_max_protocol_help:
Select the max NFS Protocol version you want to use to mount your remote shares.
:end
:nfs_threads_help:
Set the number of threads for NFS. For light NFS loads, 8 is sufficient. For heaver NFS loads, a higher number of threads might be appropriate.
If there aren't enough threads, NFS will probably crash.
:end
:shares_enable_disk_help:
If set to No, disk shares are unconditionally not exported.
@@ -1502,6 +1550,10 @@ This will increase write performance but might possibly decrease read performanc
*Auto* selects No.
:end
:shares_fuse_file_descriptors_io_help:
Set the number of fuse file descriptors. With a lot of file activity on the array, you may need to increase this value.
:end
:syslog_local_server_help:
Let the server act as a central syslog server and collect syslog messages from other systems.
The server can listen on UDP, TCP or both with a selectable port number.
@@ -1520,7 +1572,6 @@ Select the identifier for the remote system (used in the logfile name).
* "IP Address" uses the IP address (IPv4 or IPv6) of the sending system.
* "Hostname (from syslog message)" uses the hostname included in each syslog message.
* "Hostname (from DNS reverse lookup)" performs a DNS reverse lookup for the sending IP and uses the result.
:end
:syslog_local_rotation_help:
@@ -2295,6 +2346,141 @@ Generally speaking, it is recommended to leave this setting to its default value
IMPORTANT NOTE: If adjusting port mappings, do not modify the settings for the Container port as only the Host port can be adjusted.
:end
:docker_container_network_help:
This allows your container to utilize the network configuration of another container. Select the appropriate container from the list.<br>This setup can be particularly beneficial if you wish to route your container's traffic through a VPN.
:end
:docker_tailscale_help:
Enable Tailscale to add this container as a machine on your Tailnet.
:end
:docker_tailscale_hostname_help:
Provide the hostname for this container. It does not need to match the container name, but it must be unique on your Tailnet. Note that an HTTPS certificate will be generated for this hostname, which means it will be placed in a public ledger, so use a name that you don't mind being public.
For more information see <a href="https://tailscale.com/kb/1153/enabling-https" target="_blank">enabling https</a>.
:end
:docker_tailscale_be_exitnode_help:
Enable this if other machines on your Tailnet should route their Internet traffic through this container, this is most useful for containers that connect to commercial VPN services.
Be sure to authorize this Exit Node in your <a href="https://login.tailscale.com/admin/machines" target="_blank">Tailscale Machines Admin Panel</a>.
For more details, see the Tailscale documentation on <a href="https://tailscale.com/kb/1103/exit-nodes" target="_blank">Exit Nodes</a>.
:end
:docker_tailscale_exitnode_ip_help:
Optionally route this container's outgoing Internet traffic through an Exit Node on your Tailnet. Choose the Exit Node or input its Tailscale IP address.
For more details, see <a href="https://tailscale.com/kb/1103/exit-nodes" target="_blank">Exit Nodes</a>.
:end
:docker_tailscale_lanaccess_help:
Only applies when this container is using an Exit Node. Enable this to allow the container to access the local network.
<b>WARNING:</b>&nbsp;Even with this feature enabled, systems on your LAN may not be able to access the container unless they have Tailscale installed.
:end
:docker_tailscale_userspace_networking_help:
When enabled, this container will operate in a restricted environment. Tailscale DNS will not work, and the container will not be able to initiate connections to other Tailscale machines. However, other machines on your Tailnet will still be able to communicate with this container.
When disabled, this container will have full access to your Tailnet. Tailscale DNS will work, and the container can fully communicate with other machines on the Tailnet.
However, systems on your LAN may not be able to access the container unless they have Tailscale installed.
:end
:docker_tailscale_ssh_help:
Tailscale SSH is similar to the Docker "Console" option in the Unraid webgui, except you connect with an SSH client and authenticate via Tailscale.
For more details, see the <a href="https://tailscale.com/kb/1193/tailscale-ssh" target="_blank">Tailscale SSH</a> documentation.
:end
:docker_tailscale_serve_mode_help:
Enabling <b>Serve</b> will automatically reverse proxy the primary web service from this container and make it available on your Tailnet using https with a valid certificate!
Note that when accessing the <b>Tailscale WebUI</b> url, no additional authentication layer is added beyond restricting it to your Tailnet - the container is still responsible for managing usernames/passwords that are allowed to access it. Depending on your configuration, direct access to the container may still be possible as well.
For more details, see the <a href="https://tailscale.com/kb/1312/serve" target="_blank">Tailscale Serve</a> documentation.
If the documentation recommends additional settings for a more complex use case, enable "Tailscale Show Advanced Settings". Support for these advanced settings is not available beyond confirming the commands are passed to Tailscale correctly.
<b>Funnel</b> is similar to <b>Serve</b>, except that the web service is made available on the open Internet. Use with care as the service will likely be attacked. As with <b>Serve</b>, the container itself is responsible for handling any authentication.
We recommend reading the <a href="https://tailscale.com/kb/1223/funnel" target="_blank">Tailscale Funnel</a> documentation before enabling this feature.
<b>Note:</b>&nbsp;Enabling <b>Serve</b> or <b>Funnel</b> publishes the Tailscale hostname to a public ledger.
For more details, see the Tailscale Documentation: <a href="https://tailscale.com/kb/1153/enabling-https" target="_blank">Enabling HTTPS</a>.
:end
:docker_tailscale_serve_port_help:
This field should specify the port for the primary web service this container offers. Note: it should specify the port in the container, not a port that was remapped on the host.
The system attempted to determine the correct port automatically. If it used the wrong value then there is likely an issue with the "Web UI" field for this container, visible by switching from "Basic View" to "Advanced View" in the upper right corner of this page.
In most cases this port is all you will need to specify in order to Serve the website in this container, although additional options are available below for more complex containers.
This value is passed to the `<serve_port>` portion of this command which starts serve or funnel:<br>
`tailscale [serve|funnel] --bg --<protocol><protocol_port><path> http://localhost:<serve_port><local_path>`<br>
For more details see the <a href="https://tailscale.com/kb/1242/tailscale-serve" target="_blank">Tailscale Serve Command Line</a> documentation.
:end
:docker_tailscale_show_advanced_help:
Here there be dragons!
:end
:docker_tailscale_serve_local_path_help:
When not specified, this value defaults to an empty string. It is passed to the `<local_path>` portion of this command which starts serve or funnel:<br>
`tailscale [serve|funnel] --bg --<protocol><protocol_port><path> http://localhost:<serve_port><local_path>`<br>
For more details see the <a href="https://tailscale.com/kb/1242/tailscale-serve" target="_blank">Tailscale Serve Command Line</a> documentation.
:end
:docker_tailscale_serve_protocol_help:
When not specified, this value defaults to "https". It is passed to the `<protocol>` portion of this command which starts serve or funnel:<br>
`tailscale [serve|funnel] --bg --<protocol>=<protocol_port><path> http://localhost:<serve_port><local_path>`<br>
For more details see the <a href="https://tailscale.com/kb/1242/tailscale-serve" target="_blank">Tailscale Serve Command Line</a> documentation.
:end
:docker_tailscale_serve_protocol_port_help:
When not specified, this value defaults to "=443". It is passed to the `<protocol_port>` portion of this command which starts serve or funnel:<br>
`tailscale [serve|funnel] --bg --<protocol><protocol_port><path> http://localhost:<serve_port><local_path>`<br>
For more details see the <a href="https://tailscale.com/kb/1242/tailscale-serve" target="_blank">Tailscale Serve Command Line</a> documentation.
:end
:docker_tailscale_serve_path_help:
When not specified, this value defaults to an empty string. It is passed to the `<path>` portion of this command which starts serve or funnel:<br>
`tailscale [serve|funnel] --bg --<protocol><protocol_port><path> http://localhost:<serve_port><local_path>`<br>
For more details see the <a href="https://tailscale.com/kb/1242/tailscale-serve" target="_blank">Tailscale Serve Command Line</a> documentation.
:end
:docker_tailscale_serve_webui_help:
If <b>Serve</b> is enabled this will be an https url with a proper domain name that is accessible over your Tailnet, no port needed!
If <b>Funnel</b> is enabled the same url will be available on the Internet.
If they are disabled then the url will be generated from the container's main "Web UI" field, but modified to use the Tailscale IP. If the wrong port is specified here then switch from "Basic View" to "Advanced View" and review the "Web UI" field for this container.
:end
:docker_tailscale_advertise_routes_help:
If desired, specify any routes that should be passed to the **`--advertise-routes=`** parameter when running **`tailscale up`**.
For more details see the <a href="https://tailscale.com/kb/1019/subnets#connect-to-tailscale-as-a-subnet-router" target="_blank">Subnet routers</a> documentation.
:end
:docker_tailscale_accept_routes_help:
When enabled, this will accept your subnet routes from other devices, adding the **`--accept-routes`** parameter when running **`tailscale up`**.
For more details see the <a href="https://tailscale.com/kb/1019/subnets?q=allow+routes#use-your-subnet-routes-from-other-devices" target="_blank">Use your subnet routes from other devices</a> documentation.
:end
:docker_tailscale_daemon_extra_params_help:
Specify any extra parameters to pass when starting **`tailscaled`**.
For more details see the <a href="https://tailscale.com/kb/1278/tailscaled" target="_blank">tailscaled</a> documentation.
:end
:docker_tailscale_extra_param_help:
Specify any extra parameters to pass when running **`tailscale up`**.
For more details see the <a href="https://tailscale.com/kb/1080/cli#up" target="_blank">Tailscale CLI</a> documentation.
:end
:docker_tailscale_statedir_help:
If state directory detection fails on startup, you can specify a persistent directory in the container to override automatic detection, i.e. `/container-path/.tailscale_state`
:end
:docker_tailscale_troubleshooting_packages_help:
Enable this to install `ping`, `nslookup`, `curl`, and `speedtest-cli` into the container to help troubleshoot networking issues. Once the issues are resolved we recommend disabling this to reduce the size of the container.
:end
:docker_privileged_help:
For containers that require the use of host-device access directly or need full exposure to host capabilities, this option will need to be selected.
For more information, see this link: <a href="https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities" target="_blank">https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities</a>

View File

@@ -44,7 +44,7 @@ $(function() {
<tbody id="ups_summary"><tr class="ups"><td colspan="7">&nbsp;</td></tr></tbody>
</table>
<span style="float:right;margin-right:10px"><a href="http://apcupsd.org/manual/manual.html" target="_blank" title="_(APC UPS Daemon user manual)_"><i class="fa fa-file-text-o"></i> <u>_(Online Manual)_</u></a></span>
<span style="float:right;margin-right:10px"><a href="https://linux.die.net/man/8/apcupsd" target="_blank" title="_(APC UPS Daemon user manual)_"><i class="fa fa-file-text-o"></i> <u>_(Online Manual)_</u></a></span>
<form markdown="1" name="apcupsd_settings" method="POST" action="/update.php" target="progressFrame">
<input type="hidden" name="#file" value="<?=$sName?>/<?=$sName?>.cfg">
<input type="hidden" name="#include" value="/plugins/<?=$sName?>/include/update.apcupsd.php">

View File

@@ -1,6 +1,6 @@
<?PHP
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
/* Copyright 2005-2024, Lime Technology
* Copyright 2012-2024, Bergware International.
* Copyright 2015, Dan Landon.
*
* This program is free software; you can redistribute it and/or
@@ -99,7 +99,7 @@ if (file_exists("/var/run/apcupsd.pid")) {
$status[4] = $power>0 ? "<td $green>$power W</td>" : "<td $red>$power W</td>";
}
if ($power && isset($load)) $status[5] = ($load<90 ? "<td $green>" : "<td $red>").round($power*$load/100)." W (".$status[5].")</td>";
if ( ($power??false) && isset($load)) $status[5] = ($load<90 ? "<td $green>" : "<td $red>").round($power*$load/100)." W (".$status[5].")</td>";
elseif (isset($load)) $status[5] = ($load<90 ? "<td $green>" : "<td $red>").$status[5]."</td>";
$status[6] = isset($output) ? ((!$volt || ($minv<$output && $output<$maxv) ? "<td $green>" : "<td $red>").$status[6].(isset($freq) ? " ~ $freq Hz" : "")."</td>") : $status[6];
}

View File

@@ -115,7 +115,23 @@ function loadlist(init) {
$.get('/plugins/dynamix.docker.manager/include/DockerContainers.php',function(d) {
clearTimeout(timers.docker);
var data = d.split(/\0/);
$(".TS_tooltip").tooltipster("destroy");
$('#docker_list').html(data[0]);
$('.TS_tooltip').tooltipster({
animation: 'fade',
delay: 200,
trigger: 'custom',
triggerOpen: {click:true,touchstart:true,mouseenter:true},
triggerClose:{click:true,scroll:false,mouseleave:true},
interactive: true,
viewportAware: true,
contentAsHTML: true,
functionBefore: function(instance,helper) {
var origin = $(helper.origin);
var TScontent = $(origin).attr("data-tstitle");
instance.content(TScontent);
}
});
$('head').append('<script>'+data[1]+'<\/script>');
<?if (_var($display,'resize')):?>
resize();
@@ -183,4 +199,3 @@ window.onunload = function(){
dockerload.stop();
}
</script>

View File

@@ -135,10 +135,8 @@ _(Enable Docker)_:
_(Enable container table readmore-js)_:
: <select id="DOCKER_READMORE" name="DOCKER_READMORE">
<?=mk_option(_var($dockercfg,'DOCKER_READMORE'), 'yes', _('Yes'))?>
<?=mk_option(_var($dockercfg,'DOCKER_READMORE'), 'no', _('No'))?>
</select>
:docker_readmore_help:
@@ -195,8 +193,8 @@ _(Docker directory)_:
<div markdown="1" id="backingfs_type" style="display:none">
_(Docker storage driver)_:
: <select id="DOCKER_BACKINGFS" name="DOCKER_BACKINGFS" onchange="updateBackingFS(this.value)">
<?=mk_option(_var($dockercfg,'DOCKER_BACKINGFS'), 'native', _('native'))?>
<?=mk_option(_var($dockercfg,'DOCKER_BACKINGFS'), 'overlay2', _('overlay2'))?>
<?=mk_option(_var($dockercfg,'DOCKER_BACKINGFS'), 'native', _('native'))?>
</select>
<?if ($var['fsState'] != "Started"):?>
<span id="WARNING_BACKINGFS" style="display:none;"><i class="fa fa-warning icon warning"></i>_(Only modify if this is a new installation since this can lead to unwanted behaviour!)_</span>
@@ -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');

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

View File

@@ -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 '<div style="text-align:center"><button type="button" onclick="done()">'._('Done').'</button></div><br>';
echo '<div style="text-align:center"><button type="button" onclick="openTerminal(\'docker\',\''.addslashes($Name).'\',\'.log\')">'._('View Container Log').'</button> <button type="button" onclick="done()">'._('Done').'</button></div><br>';
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,164 @@ $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;
}
}
}
# 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;
$ts_exit_nodes = [];
$ts_en_check = 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']??'';
# Look for Exit Nodes through Tailscale plugin (if installed) when container is not running
if (empty($TS_container) && 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;
}
}
}
}
}
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 = "<span><b><a href='https://tailscale.com/kb/1153/enabling-https' target='_blank'>Enable HTTPS</a> on your Tailscale account to use Tailscale Serve/Funnel.</b></span>";
}
// 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 = "<span><b>Warning: the actual Tailscale hostname is '".$TS_HostNameActual."'</b></span>";
}
// 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'];
}
}
}
?>
<link type="text/css" rel="stylesheet" href="<?autov("/webGui/styles/jquery.ui.css")?>">
<link type="text/css" rel="stylesheet" href="<?autov("/webGui/styles/jquery.switchbutton.css")?>">
@@ -423,6 +631,9 @@ function addConfigPopup() {
Opts.Buttons += "<button type='button' onclick='removeConfig("+confNum+")'>_(Remove)_</button>";
}
Opts.Number = confNum;
if (Opts.Type == "Device") {
Opts.Target = Opts.Value;
}
newConf = makeConfig(Opts);
$("#configLocation").append(newConf);
reloadTriggers();
@@ -491,6 +702,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 +880,18 @@ $(function() {
});
});
</script>
<?php
if (isset($xml["Config"])) {
foreach ($xml["Config"] as $config) {
if (isset($config["Target"]) && is_array($config) && strpos($config["Target"], "TAILSCALE_") === 0) {
$tailscaleTargetFound = true;
break;
}
}
}
?>
<div id="canvas">
<form markdown="1" method="POST" autocomplete="off" onsubmit="prepareConfig(this)">
<input type="hidden" name="csrf_token" value="<?=$var['csrf_token']?>">
@@ -706,7 +932,7 @@ _(Template)_:
<div markdown="1" class="<?=$showAdditionalInfo?>">
_(Name)_:
: <input type="text" name="contName" pattern="[a-zA-Z0-9][a-zA-Z0-9_.-]+" required>
: <input type="text" name="contName" pattern="[a-zA-Z0-9][a-zA-Z0-9_.\-]+" required>
:docker_client_name_help:
@@ -858,6 +1084,7 @@ _(Network Type)_:
: <select name="contNetwork" onchange="showSubnet(this.value)">
<?=mk_option(1,'bridge',_('Bridge'))?>
<?=mk_option(1,'host',_('Host'))?>
<?=mk_option(1,'container',_('Container'))?>
<?=mk_option(1,'none',_('None'))?>
<?foreach ($custom as $network):?>
<?$name = $network;
@@ -881,6 +1108,286 @@ _(Fixed IP address)_ (_(optional)_):
:docker_fixed_ip_help:
</div>
<div markdown="1" class="netCONT noshow">
_(Container Network)_:
: <select name="netCONT" id="netCONT">
<?php
$container_name = !empty($xml['Name']) ? $xml['Name'] : '';
foreach ($DockerClient->getDockerContainers() as $ct) {
if ($ct['Name'] !== $container_name) {
$list[] = $ct['Name'];
echo mk_option($ct['Name'], $ct['Name'], $ct['Name']);
}
}
?>
</select>
:docker_container_network_help:
</div>
<div markdown="1" class="TSdivider noshow"><hr></div>
<?if ($TS_existing_vars == 'true'):?>
<div markdown="1" class="TSwarning noshow">
<b style="color:red;">_(WARNING)_</b>:
: <b>_(Existing TAILSCALE variables found, please remove any existing modifications in the Template for Tailscale before using this function!)_</b>
</div>
<?endif;?>
<?if (empty($xml['TailscaleEnabled'])):?>
<div markdown="1" class="TSdeploy noshow">
<b>_(First deployment)_</b>:
: <p>_(After deploying the container, open the log and follow the link to register the container to your Tailnet!)_</p>
</div>
<?if (!file_exists('/usr/local/sbin/tailscale')):?>
<div markdown="1" class="TSdeploy noshow">
<b>_(Recommendation)_</b>:
: <p>_(For the best experience with Tailscale, install "Tailscale (Plugin)" from)_ <a href="/Apps?search=Tailscale%20(Plugin)" target='_blank'> Community Applications</a>.</p>
</div>
<?endif;?>
<?endif;?>
<div markdown="1">
_(Use Tailscale)_:
: <input type="checkbox" class="switch-on-off" name="contTailscale" id="contTailscale" <?php if (!empty($xml['TailscaleEnabled']) && $xml['TailscaleEnabled'] == 'true') echo 'checked'; ?> onchange="showTailscale(this)">
:docker_tailscale_help:
</div>
<div markdown="1" class="TSdivider noshow">
<b>_(NOTE)_</b>:
: <i>_(This option will install Tailscale and dependencies into the container.)_</i>
</div>
<?if($TS_ExitNodeNeedsApproval):?>
<div markdown="1" class="TShostname noshow">
<b>Warning:</b>
: Exit Node not yet approved. Navigate to the <a href="<?=$TS_DirectMachineLink?>" target='_blank'>Tailscale website</a> and approve it.
</div>
<?endif;?>
<?if(!empty($TS_expiry_diff)):?>
<div markdown="1" class="TSdivider noshow">
<b>_(Warning)_</b>:
<?if($TS_expiry_diff->invert):?>
: <b>Tailscale Key expired!</b> <a href="<?=$TS_MachinesLink?>" target='_blank'>Renew/Disable key expiry</a> for '<b><?=$TS_HostNameActual?></b>'.
<?else:?>
: Tailscale Key will expire in <b><?=$TS_expiry_diff->days?> days</b>! <a href="<?=$TS_MachinesLink?>" target='_blank'>Disable Key Expiry</a> for '<b><?=$TS_HostNameActual?></b>'.
<?endif;?>
<label>See <a href="https://tailscale.com/kb/1028/key-expiry" target='_blank'>key-expiry</a>.</label>
</div>
<?endif;?>
<?if(!empty($TS_not_approved)):?>
<div markdown="1" class="TSdivider noshow">
<b>_(Warning)_</b>:
: The following route(s) are not approved: <b><?=trim($TS_not_approved)?></b>
</div>
<?endif;?>
<div markdown="1" class="TShostname noshow">
_(Tailscale Hostname)_:
: <input type="text" pattern="[A-Za-z0-9_\-]*" name="TShostname" <?php if (!empty($xml['TailscaleHostname'])) echo 'value="' . $xml['TailscaleHostname'] . '"'; ?> placeholder="_(Hostname for the container)_"> <?=$TS_HostNameWarning?>
:docker_tailscale_hostname_help:
</div>
<div markdown="1" class="TSisexitnode noshow">
_(Be a Tailscale Exit Node)_:
: <select name="TSisexitnode" id="TSisexitnode" onchange="showTailscale(this)">
<?=mk_option(1,'false',_('No'))?>
<?=mk_option(1,'true',_('Yes'))?>
</select>
<span id='TSisexitnode_msg' style='font-style: italic;'></span>
:docker_tailscale_be_exitnode_help:
</div>
<div markdown="1" class="TSexitnodeip noshow">
_(Use a Tailscale Exit Node)_:
<?if($ts_en_check !== true && empty($ts_exit_nodes)):?>
: <input type="text" name="TSexitnodeip" <?php if (!empty($xml['TailscaleExitNodeIP'])) echo 'value="' . $xml['TailscaleExitNodeIP'] . '"'; ?> placeholder="_(IP/Hostname from Exit Node)_" onchange="processExitNodeoptions(this)">
<?else:?>
: <select name="TSexitnodeip" id="TSexitnodeip" onchange="processExitNodeoptions(this)">
<?=mk_option(1,'',_('None'))?>
<?foreach ($ts_exit_nodes as $ts_exit_node):?>
<?=$node_offline = $ts_exit_node['status'] === 'offline' ? ' - OFFLINE' : '';?>
<?=mk_option(1,$ts_exit_node['ip'],$ts_exit_node['ip'] . ' - ' . $ts_exit_node['hostname'] . $node_offline)?>
<?endforeach;?></select>
<?endif;?>
</select>
<span id='TSexitnodeip_msg' style='font-style: italic;'></span>
:docker_tailscale_exitnode_ip_help:
</div>
<div markdown="1" class="TSallowlanaccess noshow">
_(Tailscale Allow LAN Access)_:
: <select name="TSallowlanaccess" id="TSallowlanaccess">
<?=mk_option(1,'false',_('No'))?>
<?=mk_option(1,'true',_('Yes'))?>
</select>
:docker_tailscale_lanaccess_help:
</div>
<div markdown="1" class="TSuserspacenetworking noshow">
_(Tailscale Userspace Networking)_:
: <select name="TSuserspacenetworking" id="TSuserspacenetworking" onchange="setExitNodeoptions()">
<?=mk_option(1,'true',_('Enabled'))?>
<?=mk_option(1,'false',_('Disabled'))?>
</select>
<span id='TSuserspacenetworking_msg' style='font-style: italic;'></span>
:docker_tailscale_userspace_networking_help:
</div>
<div markdown="1" class="TSssh noshow">
_(Enable Tailscale SSH)_:
: <select name="TSssh" id="TSssh">
<?=mk_option(1,'false',_('No'))?>
<?=mk_option(1,'true',_('Yes'))?>
</select>
:docker_tailscale_ssh_help:
</div>
<div markdown="1" class="TSserve noshow">
_(Tailscale Serve)_:
: <select name="TSserve" id="TSserve" onchange="showServe(this.value)">
<?=mk_option(1,'no',_('No'))?>
<?=mk_option(1,'serve',_('Serve'))?>
<?=mk_option(1,'funnel',_('Funnel'))?>
</select>
<?=$TS_HTTPSDisabledWarning?><?php if (!empty($TS_webui_url)) echo '<label for="TSserve"><a href="' . $TS_webui_url . '" target="_blank">' . $TS_webui_url . '</a></label>'; ?>
:docker_tailscale_serve_mode_help:
</div>
<div markdown="1" class="TSserveport noshow">
_(Tailscale Serve Port)_:
: <input type="text" name="TSserveport" value="<?php echo !empty($xml['TailscaleServePort']) ? $xml['TailscaleServePort'] : (!empty($TSwebuiport) ? $TSwebuiport : ''); ?>" placeholder="_(Will be detected automatically if possible)_">
:docker_tailscale_serve_port_help:
</div>
<div markdown="1" class="TSadvanced noshow">
_(Tailscale Show Advanced Settings)_:
: <input type="checkbox" name="TSadvanced" class="switch-on-off" onchange="showTSAdvanced(this.checked)">
:docker_tailscale_show_advanced_help:
</div>
<div markdown="1" class="TSservelocalpath noshow">
_(Tailscale Serve Local Path)_:
: <input type="text" name="TSservelocalpath" <?php if (!empty($xml['TailscaleServeLocalPath'])) echo 'value="' . $xml['TailscaleServeLocalPath'] . '"'; ?> placeholder="_(Leave empty if unsure)_">
:docker_tailscale_serve_local_path_help:
</div>
<div markdown="1" class="TSserveprotocol noshow">
_(Tailscale Serve Protocol)_:
: <input type="text" name="TSserveprotocol" <?php if (!empty($xml['TailscaleServeProtocol'])) echo 'value="' . $xml['TailscaleServeProtocol'] . '"'; ?> placeholder="_(Leave empty if unsure, defaults to https)_">
:docker_tailscale_serve_protocol_help:
</div>
<div markdown="1" class="TSserveprotocolport noshow">
_(Tailscale Serve Protocol Port)_:
: <input type="text" name="TSserveprotocolport" <?php if (!empty($xml['TailscaleServeProtocolPort'])) echo 'value="' . $xml['TailscaleServeProtocolPort'] . '"'; ?> placeholder="_(Leave empty if unsure, defaults to =443)_">
:docker_tailscale_serve_protocol_port_help:
</div>
<div markdown="1" class="TSservepath noshow">
_(Tailscale Serve Path)_:
: <input type="text" name="TSservepath" <?php if (!empty($xml['TailscaleServePath'])) echo 'value="' . $xml['TailscaleServePath'] . '"'; ?> placeholder="_(Leave empty if unsure)_">
:docker_tailscale_serve_path_help:
</div>
<div markdown="1" class="TSwebui noshow">
_(Tailscale WebUI)_:
: <input type="text" name="TSwebui" value="<?php echo !empty($TS_webui_url) ? $TS_webui_url : ''; ?>" placeholder="Will be determined automatically if possible" disabled>
<input type="hidden" name="TSwebui" <?php if (!empty($xml['TailscaleWebUI'])) echo 'value="' . $xml['TailscaleWebUI'] . '"'; ?>>
:docker_tailscale_serve_webui_help:
</div>
<div markdown="1" class="TSroutes noshow">
_(Tailscale Advertise Routes)_:
: <input type="text" pattern="[0-9:., ]*" name="TSroutes" <?php if (!empty($xml['TailscaleRoutes'])) echo 'value="' . $xml['TailscaleRoutes'] . '"'?> placeholder="_(Leave empty if unsure)_">
:docker_tailscale_advertise_routes_help:
</div>
<div markdown="1" class="TSacceptroutes noshow">
_(Tailscale Accept Routes)_:
: <select name="TSacceptroutes" id="TSacceptroutes">
<?=mk_option(1,'false',_('No'))?>
<?=mk_option(1,'true',_('Yes'))?>
</select>
:docker_tailscale_accept_routes_help:
</div>
<div markdown="1" class="TSdaemonparams noshow">
_(Tailscale Daemon Parameters)_:
: <input type="text" name="TSdaemonparams" <?php if (!empty($xml['TailscaleDParams'])) echo 'value="' . $xml['TailscaleDParams'] . '"'; ?> placeholder="_(Leave empty if unsure)_">
:docker_tailscale_daemon_extra_params_help:
</div>
<div markdown="1" class="TSextraparams noshow">
_(Tailscale Extra Parameters)_:
: <input type="text" name="TSextraparams" <?php if (!empty($xml['TailscaleParams'])) echo 'value="' . $xml['TailscaleParams'] . '"'; ?> placeholder="_(Leave empty if unsure)_">
:docker_tailscale_extra_param_help:
</div>
<div markdown="1" class="TSstatedir noshow">
_(Tailscale State Directory)_:
: <input type="text" name="TSstatedir" <?php if (!empty($xml['TailscaleStateDir'])) echo 'value="' . $xml['TailscaleStateDir'] . '"'; ?> placeholder="_(Leave empty if unsure)_">
:docker_tailscale_statedir_help:
</div>
<div markdown="1" class="TStroubleshooting noshow">
_(Tailscale Install Troubleshooting Packages)_:
: <input type="checkbox" class="switch-on-off" name="TStroubleshooting" <?php if (!empty($xml['TailscaleTroubleshooting']) && $xml['TailscaleTroubleshooting'] == 'true') echo 'checked'; ?>>
:docker_tailscale_troubleshooting_packages_help:
</div>
<div markdown="1" class="TSdivider noshow">
<hr>
</div>
_(Console shell command)_:
: <select name="contShell">
<?=mk_option(1,'sh',_('Shell'))?>
@@ -1013,9 +1520,231 @@ function showSubnet(bridge) {
if (bridge.match(/^(bridge|host|none)$/i) !== null) {
$('.myIP').hide();
$('input[name="contMyIP"]').val('');
$('.netCONT').hide();
$('#netCONT').val('');
} else if (bridge.match(/^(container)$/i) !== null) {
$('.netCONT').show();
$('#netCONT').val('<?php echo (isset($xml) && isset($xml['Network'][1])) ? $xml['Network'][1] : ''; ?>');
$('.myIP').hide();
$('input[name="contMyIP"]').val('');
} else {
$('.myIP').show();
$('#myIP').html('Subnet: '+subnet[bridge]);
$('.netCONT').hide();
$('#netCONT').val('');
}
// make sure to re-trigger Tailscale check when network is changed
if ($('#contTailscale').prop('checked')) {
showTailscale(true);
}
}
function processExitNodeoptions(value) {
val = null;
if (value.tagName.toLowerCase() === "input") {
val = value.value.trim();
} else if (value.tagName.toLowerCase() === "select") {
val = value.value;
}
if (val) {
$('.TSallowlanaccess').show();
} else {
$('#TSallowlanaccess').val('false');
$('.TSallowlanaccess').hide();
}
setUserspaceNetworkOptions();
setIsExitNodeoptions();
}
function setUserspaceNetworkOptions() {
optTrueDisabled = false;
optFalseDisabled = false;
optMessage = "";
value = null;
var network = $('select[name="contNetwork"]')[0].value;
var isExitnode = $('#TSisexitnode').val();
if (network == 'host' || isExitnode == 'true') {
// in host mode or if this container is an Exit Node
// then Userspace Networking MUST be enabled ('true')
value = 'true';
optTrueDisabled = false;
optFalseDisabled = true;
optMessage = (isExitnode == 'true') ? "Enabled because this is an Exit Node" : "Enabled due to Docker "+network+" mode";
} else {
if (document.querySelector('input[name="TSexitnodeip"], select[name="TSexitnodeip"]').value) {
// If an Exit Node IP is set, Userspace Networking MUST be disabled ('false')
value = 'false';
optTrueDisabled = true;
optFalseDisabled = false;
optMessage = "Disabled due to use of an Exit Node";
} else {
// Exit Node IP is not set, user can decide whether to enable/disable Userspace Networking
optTrueDisabled = false;
optFalseDisabled = false;
optMessage = "";
}
}
$("#TSuserspacenetworking option[value='true']").prop("disabled", optTrueDisabled);
$("#TSuserspacenetworking option[value='false']").prop("disabled", optFalseDisabled);
if (value != null) $('#TSuserspacenetworking').val(value);
$('#TSuserspacenetworking_msg').text(optMessage);
setExitNodeoptions();
}
function setIsExitNodeoptions() {
optTrueDisabled = false;
optFalseDisabled = false;
optMessage = "";
value = null;
var network = $('select[name="contNetwork"]')[0].value;
if (network == 'host') {
// in host mode then this cannot be an Exit Node
value = 'false';
optTrueDisabled = true;
optFalseDisabled = false;
optMessage = "Disabled due to Docker "+network+" mode";
} else {
if (document.querySelector('input[name="TSexitnodeip"], select[name="TSexitnodeip"]').value) {
// If an Exit Node IP is set, this cannot be an Exit Node
value = 'false';
optTrueDisabled = true;
optFalseDisabled = false;
optMessage = "Disabled due to use of an Exit Node";
} else {
optTrueDisabled = false;
optFalseDisabled = false;
}
}
$("#TSisexitnode option[value='true']").prop("disabled", optTrueDisabled);
$("#TSisexitnode option[value='false']").prop("disabled", optFalseDisabled);
if (value != null) $('#TSisexitnode').val(value);
$('#TSisexitnode_msg').text(optMessage);
}
function setExitNodeoptions() {
optMessage = "";
var $exitNodeInput = $('input[name="TSexitnodeip"]');
var $exitNodeSelect = $('#TSexitnodeip');
// In host mode, TSuserspacenetworking is true
if ($('#TSuserspacenetworking').val() == 'true') {
// if TSuserspacenetworking is true, then TSexitnodeip must be "" and all options are disabled
optMessage = "Disabled because Userspace Networking is Enabled.";
$exitNodeInput.val('').prop('disabled', true); // Disable the input field
$exitNodeSelect.val('').prop('disabled', true).find('option').each(function() {
if ($(this).val() === "") {
$(this).prop('disabled', false); // Enable the option with value=""
} else {
$(this).prop('disabled', true); // Disable all other options
}
});
} else {
// if TSuserspacenetworking is false, then all TSexitnodeip options can be enabled
$exitNodeInput.prop('disabled', false); // Enable the input field
$exitNodeSelect.prop('disabled', false).find('option').each(function() {
$(this).prop('disabled', false); // Enable all options
});
}
$('#TSexitnodeip_msg').text(optMessage);
}
function showTSAdvanced(checked) {
if (!checked) {
<?if (!empty($TSwebuiport)):?>
$('.TSserveport').hide();
<?elseif (empty($contTailscale) || $contTailscale == 'false'):?>
$('.TSserveport').hide();
<?else:?>
$('.TSserveport').show();
<?endif;?>
$('.TSdaemonparams').hide();
$('.TSextraparams').hide();
$('.TSstatedir').hide();
$('.TSservepath').hide();
$('.TSserveprotocol').hide();
$('.TSserveprotocolport').hide();
$('.TSservelocalpath').hide();
$('.TSwebui').hide();
$('.TStroubleshooting').hide();
$('.TSroutes').hide();
$('.TSacceptroutes').hide();
} else {
$('.TSdaemonparams').show();
$('.TSextraparams').show();
$('.TSstatedir').show();
$('.TSserveport').show();
$('.TSservepath').show();
$('.TSserveprotocol').show();
$('.TSserveprotocolport').show();
$('.TSservelocalpath').show();
$('.TSwebui').show();
$('.TStroubleshooting').show();
$('.TSroutes').show();
$('.TSacceptroutes').show();
}
}
function showTailscale(source) {
if (!$.trim($('#TSallowlanaccess').val())) {
$('#TSallowlanaccess').val('false');
}
if (!$.trim($('#TSserve').val())) {
$('#TSserve').val('no');
}
checked = $('#contTailscale').prop('checked');
if (!checked) {
$('.TSdivider').hide();
$('.TSwarning').hide();
$('.TSdeploy').hide();
$('.TSisexitnode').hide();
$('.TShostname').hide();
$('.TSexitnodeip').hide();
$('.TSssh').hide();
$('.TSallowlanaccess').hide();
$('.TSdaemonparams').hide();
$('.TSextraparams').hide();
$('.TSstatedir').hide();
$('.TSserve').hide();
$('.TSuserspacenetworking').hide();
$('.TSservepath').hide();
$('.TSserveprotocol').hide();
$('.TSserveprotocolport').hide();
$('.TSservelocalpath').hide();
$('.TSwebui').hide();
$('.TSserveport').hide();
$('.TSadvanced').hide();
$('.TSroutes').hide();
$('.TSacceptroutes').hide();
$('.TStroubleshooting').hide();
} else {
// reset these vals back to what they were in the XML
$('#TSssh').val('<?php echo (!empty($xml) && !empty($xml['TailscaleSSH'])) ? $xml['TailscaleSSH'] : 'false'; ?>');
$('#TSallowlanaccess').val('<?php echo (!empty($xml) && !empty($xml['TailscaleLANAccess'])) ? $xml['TailscaleLANAccess'] : 'false'; ?>');
$('#TSserve').val('<?php echo (!empty($xml) && !empty($xml['TailscaleServe'])) ? $xml['TailscaleServe'] : 'false'; ?>');
$('#TSexitnodeip').val('<?php echo (!empty($xml) && !empty($xml['TailscaleExitNodeIP'])) ? $xml['TailscaleExitNodeIP'] : ''; ?>');
$('#TSuserspacenetworking').val('<?php echo (!empty($xml) && !empty($xml['TailscaleUserspaceNetworking'])) ? $xml['TailscaleUserspaceNetworking'] : 'false'; ?>');
$('#TSacceptroutes').val('<?php echo (!empty($xml) && !empty($xml['TailscaleAcceptRoutes'])) ? $xml['TailscaleAcceptRoutes'] : 'false'; ?>');
<?if (empty($xml['TailscaleServe']) && !empty($TSwebuiport) && empty($xml['TailscaleServePort'])):?>
$('#TSserve').val('serve');
<?elseif (empty($xml['TailscaleServe']) && empty($TSwebuiport) && empty($xml['TailscaleServePort'])):?>
$('#TSserve').val('no');
<?endif;?>
// don't reset this field if caller was the onchange event for this field
if (source.id != 'TSisexitnode') $('#TSisexitnode').val('<?php echo !empty($xml['TailscaleIsExitNode']) ? $xml['TailscaleIsExitNode'] : 'false'; ?>');
$('.TSisexitnode').show();
$('.TShostname').show();
$('.TSssh').show();
$('.TSexitnodeip').show();
$('.TSallowlanaccess').hide();
$('.TSserve').show();
$('.TSuserspacenetworking').show();
processExitNodeoptions(document.querySelector('input[name="TSexitnodeip"], select[name="TSexitnodeip"]'));
$('.TSdivider').show();
$('.TSwarning').show();
$('.TSdeploy').show();
$('.TSadvanced').show();
}
}
@@ -1111,6 +1840,9 @@ $(function() {
Opts.Buttons += "<button type='button' onclick='removeConfig("+confNum+")'>_(Remove)_</button>";
}
Opts.Number = confNum;
if (Opts.Type == "Device") {
Opts.Target = Opts.Value;
}
newConf = makeConfig(Opts);
if (Opts.Display == 'advanced' || Opts.Display == 'advanced-hide') {
$("#configLocationAdvanced").append(newConf);
@@ -1140,3 +1872,4 @@ if (window.location.href.indexOf("/Apps/") > 0 && <? if (is_file($xmlTemplate))
}
</script>
<?END:?>

View File

@@ -292,6 +292,16 @@ class DockerTemplates {
return $WebUI;
}
private function getTailscaleJson($name) {
$TS_raw = [];
exec("docker exec -i ".$name." /bin/sh -c \"tailscale status --peers=false --json\" 2>/dev/null", $TS_raw);
if (!empty($TS_raw)) {
$TS_raw = implode("\n", $TS_raw);
return json_decode($TS_raw, true);
}
return '';
}
public function getAllInfo($reload=false,$com=true,$communityApplications=false) {
global $driver, $dockerManPaths, $host;
$DockerClient = new DockerClient();
@@ -334,6 +344,43 @@ class DockerTemplates {
if (strpos($ct['NetworkMode'], 'container:') === 0)
$tmp['url'] = '';
}
// Check if webui & ct TSurl is set, if set construct WebUI URL on Docker page
$tmp['TSurl'] = '';
if (!empty($webui) && !empty($ct['TSUrl'])) {
$TS_no_peers = $this->getTailscaleJson($name);
if (!empty($TS_no_peers['CurrentTailnet']) && !empty($TS_no_peers['CurrentTailnet']['MagicDNSEnabled'])) {
$TS_container = $TS_no_peers['Self'];
$TS_DNSName = _var($TS_container,'DNSName','');
$TS_HostNameActual = substr($TS_DNSName, 0, strpos($TS_DNSName, '.'));
// Check if serve or funnel are enabled by checking for [hostname] and replace string with TS_DNSName
if (strpos($ct['TSUrl'], '[hostname]') !== false && isset($TS_DNSName)) {
$tmp['TSurl'] = str_replace("[hostname][magicdns]", rtrim($TS_DNSName, '.'), $ct['TSUrl']);
$tmp['TSurl'] = preg_replace('/\[IP\]/', rtrim($TS_DNSName, '.'), $tmp['TSurl']);
$tmp['TSurl'] = preg_replace('/\[PORT:(\d{1,5})\]/', '443', $tmp['TSurl']);
// Check if serve is disabled, construct url with port, path and query if present and replace [noserve] with url
} elseif (strpos($ct['TSUrl'], '[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($webui) ? parse_url($webui) : '';
$webui_port = (preg_match('/\[PORT:(\d+)\]/', $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);
$tmp['TSurl'] = 'http://' . $ipv4 . $webui_port . $webui_path . $webui_query;
}
// Check if TailscaleWebUI in the xml is custom and display instead
} elseif (strpos($ct['TSUrl'], '[hostname]') === false && strpos($ct['TSUrl'], '[noserve]') === false) {
$tmp['TSurl'] = $ct['TSUrl'];
}
}
}
if ( ($tmp['shell'] ?? false) == false )
$tmp['shell'] = $this->getTemplateValue($image, 'Shell');
}
@@ -929,13 +976,18 @@ class DockerClient {
$c['Created'] = $this->humanTiming($ct['Created']);
$c['NetworkMode'] = $ct['HostConfig']['NetworkMode'];
$c['Manager'] = $info['Config']['Labels']['net.unraid.docker.managed'] ?? false;
if ($c['Manager'] == 'composeman') {
$c['ComposeProject'] = $info['Config']['Labels']['com.docker.compose.project'];
}
[$net, $id] = array_pad(explode(':',$c['NetworkMode']),2,'');
$c['CPUset'] = $info['HostConfig']['CpusetCpus'];
$c['BaseImage'] = $ct['Labels']['BASEIMAGE'] ?? false;
$c['Icon'] = $info['Config']['Labels']['net.unraid.docker.icon'] ?? false;
$c['Url'] = $info['Config']['Labels']['net.unraid.docker.webui'] ?? false;
$c['Shell'] = $info['Config']['Labels']['net.unraid.docker.shell'] ?? false;
$c['Manager'] = $info['Config']['Labels']['net.unraid.docker.managed'] ?? false;
$c['TSUrl'] = $info['Config']['Labels']['net.unraid.docker.tailscale.webui'] ?? false;
$c['TSHostname'] = $info['Config']['Labels']['net.unraid.docker.tailscale.hostname'] ?? false;
$c['Shell'] = $info['Config']['Labels']['net.unraid.docker.shell'] ?? false;
$c['Manager'] = $info['Config']['Labels']['net.unraid.docker.managed'] ?? false;
$c['Ports'] = [];
$c['Networks'] = [];
if ($id) $c['NetworkMode'] = $net.str_replace('/',':',DockerUtil::ctMap($id)?:'/???');

View File

@@ -48,6 +48,55 @@ $null = '0.0.0.0';
$autostart = (array)@file($autostart_file,FILE_IGNORE_NEW_LINES);
$names = array_map('var_split',$autostart);
// Grab Tailscale json from container
function tailscale_stats($name) {
exec("docker exec -i ".$name." /bin/sh -c \"tailscale status --json | jq '{Self: .Self, ExitNodeStatus: .ExitNodeStatus, Version: .Version}'\" 2>/dev/null", $TS_stats);
if (!empty($TS_stats)) {
$TS_stats = implode("\n", $TS_stats);
return json_decode($TS_stats, true);
}
return '';
}
// Download Tailscal JSON and return Array, refresh file if older than 24 hours
function tailscale_json_dl($file, $url) {
$dl_status = 0;
if (!is_dir('/tmp/tailscale')) {
mkdir('/tmp/tailscale', 0777, true);
}
if (!file_exists($file)) {
exec("wget -T 3 -q -O " . $file . " " . $url, $output, $dl_status);
} else {
$fileage = time() - filemtime($file);
if ($fileage > 86400) {
unlink($file);
exec("wget -T 3 -q -O " . $file . " " . $url, $output, $dl_status);
}
}
if ($dl_status === 0) {
return json_decode(@file_get_contents($file), true);
} elseif ($dl_status === 0 && is_file($file)) {
return json_decode(@file_get_contents($file), true);
} else {
unlink($file);
return '';
}
}
// Grab Tailscale DERP map JSON
$TS_derp_url = 'https://login.tailscale.com/derpmap/default';
$TS_derp_file = '/tmp/tailscale/tailscale-derpmap.json';
$TS_derp_list = tailscale_json_dl($TS_derp_file, $TS_derp_url);
// Grab Tailscale version JSON
$TS_version_url = 'https://pkgs.tailscale.com/stable/?mode=json';
$TS_version_file = '/tmp/tailscale/tailscale-latest-version.json';
// Extract tarbal version string
$TS_latest_version = tailscale_json_dl($TS_version_file, $TS_version_url);
if (!empty($TS_latest_version)) {
$TS_latest_version = $TS_latest_version["TarballsVersion"];
}
function my_lang_time($text) {
[$number, $text] = my_explode(' ',$text,2);
return sprintf(_("%s $text"),$number);
@@ -69,21 +118,24 @@ foreach ($containers as $ct) {
$running = $info['running'] ? 1 : 0;
$paused = $info['paused'] ? 1 : 0;
$is_autostart = $info['autostart'] ? 'true':'false';
$updateStatus = substr($ct['NetworkMode'],-4)==':???' ? 2 : ($info['updated']=='true' ? 0 : ($info['updated']=='false' ? 1 : 3));
$composestack = isset($ct['ComposeProject']) ? $ct['ComposeProject'] : '';
$updateStatus = substr($ct['NetworkMode'], -4) == ':???' ? 2 : ($info['updated'] == 'true' ? 0 : ($info['updated'] == 'false' ? 1 : 3));
$template = $info['template']??'';
$shell = $info['shell']??'';
$webGui = html_entity_decode($info['url']??'');
$TShostname = isset($ct['TSHostname']) ? $ct['TSHostname'] : '';
$TSwebGui = html_entity_decode($info['TSurl']??'');
$support = html_entity_decode($info['Support']??'');
$project = html_entity_decode($info['Project']??'');
$registry = html_entity_decode($info['registry']??'');
$donateLink = html_entity_decode($info['DonateLink']??'');
$readme = html_entity_decode($info['ReadMe']??'');
$menu = sprintf("onclick=\"addDockerContainerContext('%s','%s','%s',%s,%s,%s,%s,'%s','%s','%s','%s','%s','%s', '%s','%s')\"", addslashes($name), addslashes($ct['ImageId']), addslashes($template), $running, $paused, $updateStatus, $is_autostart, addslashes($webGui), $shell, $id, addslashes($support), addslashes($project),addslashes($registry),addslashes($donateLink),addslashes($readme));
$menu = sprintf("onclick=\"addDockerContainerContext('%s','%s','%s',%s,%s,%s,%s,'%s','%s','%s','%s','%s','%s','%s', '%s','%s')\"", addslashes($name), addslashes($ct['ImageId']), addslashes($template), $running, $paused, $updateStatus, $is_autostart, addslashes($webGui), addslashes($TSwebGui), $shell, $id, addslashes($support), addslashes($project),addslashes($registry),addslashes($donateLink),addslashes($readme));
$docker[] = "docker.push({name:'$name',id:'$id',state:$running,pause:$paused,update:$updateStatus});";
$shape = $running ? ($paused ? 'pause' : 'play') : 'square';
$status = $running ? ($paused ? 'paused' : 'started') : 'stopped';
$color = $status=='started' ? 'green-text' : ($status=='paused' ? 'orange-text' : 'red-text');
$update = $updateStatus==1 ? 'blue-text' : '';
$update = $updateStatus==1 && !empty($compose) ? 'blue-text' : '';
$icon = $info['icon'] ?: '/plugins/dynamix.docker.manager/images/question.png';
$image = substr($icon,-4)=='.png' ? "<img src='$icon?".filemtime("$docroot{$info['icon']}")."' class='img' onerror=this.src='/plugins/dynamix.docker.manager/images/question.png';>" : (substr($icon,0,5)=='icon-' ? "<i class='$icon img'></i>" : "<i class='fa fa-$icon img'></i>");
$wait = var_split($autostart[array_search($name,$names)]??'',1);
@@ -119,12 +171,12 @@ foreach ($containers as $ct) {
$paths[] = sprintf('%s<i class="fa fa-%s" style="margin:0 6px"></i>%s', htmlspecialchars($container_path), $access_mode=='ro'?'long-arrow-left':'arrows-h', htmlspecialchars($host_path));
}
echo "<tr class='sortable'><td class='ct-name' style='width:220px;padding:8px'><i class='fa fa-arrows-v mover orange-text'></i>";
if ($template) {
if ($template && empty($composestack)) {
$appname = "<a class='exec' onclick=\"editContainer('".addslashes(htmlspecialchars($name))."','".addslashes(htmlspecialchars($template))."')\">".htmlspecialchars($name)."</a>";
} else {
$appname = htmlspecialchars($name);
}
echo "<span class='outer'><span id='$id' $menu class='hand'>$image</span><span class='inner'><span class='appname $update'>$appname</span><br><i id='load-$id' class='fa fa-$shape $status $color'></i><span class='state'>"._($status)."</span></span></span>";
echo "<span class='outer'><span id='$id' $menu class='hand'>$image</span><span class='inner'><span class='appname $update'>$appname</span><br><i id='load-$id' class='fa fa-$shape $status $color'></i><span class='state'>"._($status).(!empty($composestack) ? '<br/>Compose Stack: ' . $composestack : '')."</span></span></span>";
echo "<div class='advanced' style='margin-top:8px'>"._('Container ID').": $id<br>";
if ($ct['BaseImage']) echo "<i class='fa fa-cubes' style='margin-right:5px'></i>".htmlspecialchars($ct['BaseImage'])."<br>";
echo _('By').": ";
@@ -137,37 +189,154 @@ foreach ($containers as $ct) {
}
echo "</div></td><td class='updatecolumn'>";
switch ($updateStatus) {
case 0:
echo "<span class='green-text' style='white-space:nowrap;'><i class='fa fa-check fa-fw'></i> "._('up-to-date')."</span>";
if ($ct['Manager'] == "dockerman")
echo "<div class='advanced'><a class='exec' onclick=\"updateContainer('".addslashes(htmlspecialchars($name))."');\"><span style='white-space:nowrap;'><i class='fa fa-cloud-download fa-fw'></i> "._('force update')."</span></a></div>";
break;
case 0:
if ($ct['Manager'] == "dockerman") {
echo "<span class='green-text' style='white-space:nowrap;'><i class='fa fa-check fa-fw'></i> "._('up-to-date')."</span>";
echo "<div class='advanced'><a class='exec' onclick=\"updateContainer('".addslashes(htmlspecialchars($name))."');\"><span style='white-space:nowrap;'><i class='fa fa-cloud-download fa-fw'></i> "._('force update')."</span></a></div>";
} elseif (!empty($composestack)) {
echo "<div><span><i class='fa fa-docker fa-fw'/></i> "._("Compose")."</span></div>";
echo "<span tyle='white-space:nowrap;'><i class='fa fa-check fa-fw'></i> "._('up-to-date')."</span>";
} else {
echo "<div><span><i class='fa fa-docker fa-fw'/></i> "._("3rd Party")."</span></div>";
echo "<span tyle='white-space:nowrap;'><i class='fa fa-check fa-fw'></i> "._('up-to-date')."</span>";
}
break;
case 1:
echo "<div class='advanced'><span class='orange-text' style='white-space:nowrap;'><i class='fa fa-flash fa-fw'></i> "._('update ready')."</span></div>";
if ($ct['Manager'] == "dockerman")
if ($ct['Manager'] == "dockerman") {
echo "<a class='exec' onclick=\"updateContainer('".addslashes(htmlspecialchars($name))."');\"><span style='white-space:nowrap;'><i class='fa fa-cloud-download fa-fw'></i> "._('apply update')."</span></a>";
else
} elseif (!empty($composestack)) {
echo "<div><span><i class='fa fa-docker fa-fw'/></i> Compose</span></a></div>";
echo "<span style='white-space:nowrap;'><i class='fa fa-cloud-download fa-fw'></i> "._('update available')."</span>";
} else {
echo "<div><span><i class='fa fa-docker fa-fw'/></i> 3rd Party</span></div>";
echo "<span style='white-space:nowrap;'><i class='fa fa-cloud-download fa-fw'></i> "._('update available')."</span>";
}
break;
case 2:
echo "<div class='advanced'><span class='orange-text' style='white-space:nowrap;'><i class='fa fa-flash fa-fw'></i> "._('rebuild ready')."</span></div>";
echo "<a class='exec'><span style='white-space:nowrap;'><i class='fa fa-recycle fa-fw'></i> "._('rebuilding')."</span></a>";
break;
default:
echo "<span class='orange-text' style='white-space:nowrap;'><i class='fa fa-unlink'></i> "._('not available')."</span>";
if ($ct['Manager'] == "dockerman")
if ($ct['Manager'] == "dockerman") {
echo "<span class='orange-text' style='white-space:nowrap;'><i class='fa fa-unlink'></i> "._('not available')."</span>";
echo "<div class='advanced'><a class='exec' onclick=\"updateContainer('".addslashes(htmlspecialchars($name))."');\"><span style='white-space:nowrap;'><i class='fa fa-cloud-download fa-fw'></i> "._('force update')."</span></a></div>";
} elseif (!empty($composestack)) {
echo "<div><span><i class='fa fa-docker fa-fw'/></i> "._("Compose")."</span></div>";
echo "<span style='white-space:nowrap;'><i class='fa fa-unlink'></i> "._('not available')."</span>";
} else {
echo "<div><span><i class='fa fa-docker fa-fw'/></i> "._("3rd Party")."</span></div>";
echo "<span style='white-space:nowrap;'><i class='fa fa-unlink'></i> "._('not available')."</span>";
}
break;
}
// Check if Tailscale for container is enabled by checking if TShostname is set
$TS_status = '';
if (!empty($TShostname)) {
if ($running) {
// Get stats from container and check if they are not empty
$TSstats = tailscale_stats($name);
if (!empty($TSstats)) {
// Construct TSinfo from TSstats
$TSinfo = '';
if (!$TSstats["Self"]["Online"]) {
$TSinfo .= "<div class='ui-tailscale-row'><span class='ui-tailscale-label'>"._("Online:")."</span><span class='ui-tailscale-value'>&#10060;<br/>"._("Please check the logs!")."</span></div>";
} else {
$TS_version = explode('-', $TSstats["Version"])[0];
if (!empty($TS_version)) {
if (!empty($TS_latest_version)) {
if ($TS_version !== $TS_latest_version) {
$TSinfo .= "<div class='ui-tailscale-row'><span class='ui-tailscale-label'>"._("Tailscale:")."</span><span class='ui-tailscale-value'>v" . $TS_version . " &#10132; v" . $TS_latest_version . " "._("available!")."</span></div>";
} else {
$TSinfo .= "<div class='ui-tailscale-row'><span class='ui-tailscale-label'>"._("Tailscale:")."</span><span class='ui-tailscale-value'>v" . $TS_version . "</span></div>";
}
} else {
$TSinfo .= "<div class='ui-tailscale-row'><span class='ui-tailscale-label'>".("Tailscale:")."</span><span class='ui-tailscale-value'>v" . $TS_version . "</span></div>";
}
}
$TSinfo .= "<div class='ui-tailscale-row'><span class='ui-tailscale-label'>"._("Online:")."</span><span class='ui-tailscale-value'>&#9989;</span></div>";
$TS_DNSName = $TSstats["Self"]["DNSName"];
$TS_HostNameActual = substr($TS_DNSName, 0, strpos($TS_DNSName, '.'));
if (strcasecmp($TS_HostNameActual, $TShostname) !== 0 && !empty($TS_DNSName)) {
$TSinfo .= "<div class='ui-tailscale-row'><span class='ui-tailscale-label'>"._("Hostname:")."</span><span class='ui-tailscale-value'>"._("Real Hostname")." &#10132; " . $TS_HostNameActual . "</span></div>";
} else {
$TSinfo .= "<div class='ui-tailscale-row'><span class='ui-tailscale-label'>"._("Hostname:")."</span><span class='ui-tailscale-value'>" . $TShostname . "</span></div>";
}
// Map region relay code to cleartext region if TS_derp_list is available
if (!empty($TS_derp_list)) {
foreach ($TS_derp_list['Regions'] as $region) {
if ($region['RegionCode'] === $TSstats["Self"]["Relay"]) {
$TSregion = $region['RegionName'];
break;
}
}
if (!empty($TSregion)) {
$TSinfo .= "<div class='ui-tailscale-row'><span class='ui-tailscale-label'>"._("DERP Relay:")."</span><span class='ui-tailscale-value'>" . $TSregion . "</span></div>";
} else {
$TSinfo .= "<div class='ui-tailscale-row'><span class='ui-tailscale-label'>"._("DERP Relay:")."</span><span class='ui-tailscale-value'>" . $TSstats["Self"]["Relay"] . "</span></div>";
}
} else {
$TSinfo .= "<div class='ui-tailscale-row'><span class='ui-tailscale-label'>"._("DERP Relay").":</span><span class='ui-tailscale-value'>" . $TSstats["Self"]["Relay"] . "</span></div>";
}
if (!empty($TSstats["Self"]["TailscaleIPs"])) {
$TSinfo .= "<div class='ui-tailscale-row'><span class='ui-tailscale-label'>"._("Addresses:")."</span><span class='ui-tailscale-value'>" . implode("<br/>", $TSstats["Self"]["TailscaleIPs"]) . "</span></div>";
}
if (!empty($TSstats["Self"]["PrimaryRoutes"])) {
$TSinfo .= "<div class='ui-tailscale-row'><span class='ui-tailscale-label'>"._("Routes:")."</span><span class='ui-tailscale-value'>" . implode("<br/>", $TSstats["Self"]["PrimaryRoutes"]) . "</span></div>";
}
if ($TSstats["Self"]["ExitNodeOption"]) {
$TSinfo .= "<div class='ui-tailscale-row'><span class='ui-tailscale-label'>"._("Is Exit Node:")."</span><span class='ui-tailscale-value'>&#9989;</span></div>";
} else {
if (!empty($TSstats["ExitNodeStatus"])) {
$TS_exit_node_status = ($TSstats["ExitNodeStatus"]["Online"]) ? "&#9989;" : "&#10060;";
$TSinfo .= "<div class='ui-tailscale-row'><span class='ui-tailscale-label'>"._("Exit Node:")."</span><span class='ui-tailscale-value'>" . strstr($TSstats["ExitNodeStatus"]["TailscaleIPs"][0], '/', true) . " | Status: " . $TS_exit_node_status ."</span></div>";
} else {
$TSinfo .= "<div class='ui-tailscale-row'><span class='ui-tailscale-label'>"._("Is Exit Node:")."</span><span class='ui-tailscale-value'>&#10060;</span></div>";
}
}
if (!empty($TSwebGui)) {
$TSinfo .= "<div class='ui-tailscale-row'><span class='ui-tailscale-label'>"._("URL:")."</span><span class='ui-tailscale-value'>" . $TSwebGui . "</span></div>";
}
if (!empty($TSstats["Self"]["KeyExpiry"])) {
$TS_expiry = new DateTime($TSstats["Self"]["KeyExpiry"]);
$current_Date = new DateTime();
$TS_expiry_formatted = $TS_expiry->format('Y-m-d');
$TS_expiry_diff = $current_Date->diff($TS_expiry);
if ($TS_expiry_diff->invert) {
$TSinfo .= "<div class='ui-tailscale-row'><span class='ui-tailscale-label'>"._("Key Expiry:")."</span><span class='ui-tailscale-value'>&#10060; "._("Expired! Renew/Disable key expiry!")."</span></div>";
} else {
$TSinfo .= "<div class='ui-tailscale-row'><span class='ui-tailscale-label'>"._("Key Expiry:")."</span><span class='ui-tailscale-value'>" . $TS_expiry_formatted . " (" . $TS_expiry_diff->days . " days)</span></div>";
}
}
}
// Display TSinfo if data was fetched correctly
$TS_status = "<br/><div class='TS_tooltip' style='cursor:pointer; display: inline-block;' data-tstitle='" . htmlspecialchars($TSinfo) . "'><img src='/plugins/dynamix.docker.manager/images/tailscale.png' style='height: 1.23em;'> Tailscale</div>";
} else {
// Display message to refresh page if Tailscale in the container wasn't maybe ready to get the data
$TS_status = "<br/><div class='TS_tooltip' style='display: inline-block;' data-tstitle='"._("Error gathering Tailscale information from container.")."<br/>"._("Please check the logs and refresh the page.")."'><img src='/plugins/dynamix.docker.manager/images/tailscale.png' style='height: 1.23em;'> Tailscale</div>";
}
} else {
// Display message that container isn't running
$TS_status = "<br/><div class='TS_tooltip' style='cursor:pointer;display: inline-block;' data-tstitle='"._("Container not running")."'><img src='/plugins/dynamix.docker.manager/images/tailscale.png' style='height: 1.23em;'> Tailscale</div>";
}
}
echo "<div class='advanced'><i class='fa fa-info-circle fa-fw'></i> ".compress(_($version),12,0)."</div></td>";
echo "<td style='white-space:nowrap'><span class='docker_readmore'> ".implode('<br>',$networks)."</span></td>";
echo "<td style='white-space:nowrap'><span class='docker_readmore'> ".implode('<br>',$networks).$TS_status."</span></td>";
echo "<td style='white-space:nowrap'><span class='docker_readmore'> ".implode('<br>',$network_ips)."</span></td>";
echo "<td style='white-space:nowrap'><span class='docker_readmore'>".implode('<br>',$ports_internal)."</span></td>";
echo "<td style='white-space:nowrap'><span class='docker_readmore'>".implode('<br>',$ports_external)."</span></td>";
echo "<td style='word-break:break-all'><span class='docker_readmore'>".implode('<br>',$paths)."</span></td>";
echo "<td class='advanced'><span class='cpu-$id'>0%</span><div class='usage-disk mm'><span id='cpu-$id' style='width:0'></span><span></span></div>";
echo "<br><span class='mem-$id'>0 / 0</span></td>";
echo "<td><input type='checkbox' id='$id-auto' class='autostart' container='".htmlspecialchars($name)."'".($info['autostart'] ? ' checked':'').">";
if (empty($composestack)) {
if ($ct['Manager'] == "dockerman") {
echo "<td><input type='checkbox' id='$id-auto' class='autostart' container='".htmlspecialchars($name)."'".($info['autostart'] ? ' checked':'').">";
} else {
echo "<td><i class='fa fa-docker fa-fw'/></i> 3rd Party";
}
} else {
echo "<td><i class='fa fa-docker'/></i> Compose";
}
echo "<span id='$id-wait' style='float:right;display:none'>"._('wait')."<input class='wait' container='".htmlspecialchars($name)."' type='number' value='$wait' placeholder='0' title=\""._('seconds')."\"></span></td>";
echo "<td><div style='white-space:nowrap'>".htmlspecialchars(str_replace('Up',_('Uptime').':',my_lang_log($ct['Status'])))."<div style='margin-top:4px'>"._('Created').": ".htmlspecialchars(my_lang_time($ct['Created']))."</div></div></td></tr>";
}
@@ -183,4 +352,3 @@ foreach ($images as $image) {
}
echo "\0".implode($docker)."\0".(pgrep('rc.docker')!==false ? 1:0);
?>

View File

@@ -32,33 +32,65 @@ function xml_decode($string) {
return strval(html_entity_decode($string, ENT_XML1, 'UTF-8'));
}
function generateTSwebui($url, $serve, $webUI) {
if (!isset($webUI)) {
return '';
}
$webui_url = isset($webUI) ? parse_url($webUI) : '';
$webui_port = (preg_match('/\[PORT:(\d+)\]/', $webUI, $matches)) ? ':' . $matches[1] : '';
$webui_path = $webui_url['path'] ?? '';
$webui_query = isset($webui_url['query']) ? '?' . $webui_url['query'] : '';
if (!empty($url)) {
if (strpos($url, '[hostname]') !== false || strpos($url, '[noserve]') !== false) {
if ($serve === 'serve' || $serve === 'funnel') {
return 'https://[hostname][magicdns]' . $webui_path . $webui_query;
} elseif ($serve === 'no') {
return 'http://[noserve]' . $webui_port . $webui_path . $webui_query;
}
}
return $url;
} else {
if (!empty($webUI)) {
if ($serve === 'serve' || $serve === 'funnel') {
return 'https://[hostname][magicdns]' . $webui_path . $webui_query;
} elseif ($serve === 'no') {
return 'http://[noserve]' . $webui_port . $webui_path . $webui_query;
}
}
return '';
}
}
function postToXML($post, $setOwnership=false) {
$dom = new domDocument;
$dom->appendChild($dom->createElement("Container"));
$xml = simplexml_import_dom($dom);
$xml['version'] = 2;
$xml->Name = xml_encode(preg_replace('/\s+/', '', $post['contName']));
$xml->Repository = xml_encode(trim($post['contRepository']));
$xml->Registry = xml_encode(trim($post['contRegistry']));
$xml->Network = xml_encode($post['contNetwork']);
$xml->MyIP = xml_encode($post['contMyIP']);
$xml->Shell = xml_encode($post['contShell']);
$xml->Privileged = strtolower($post['contPrivileged']??'')=='on' ? 'true' : 'false';
$xml->Support = xml_encode($post['contSupport']);
$xml->Project = xml_encode($post['contProject']);
$xml->Overview = xml_encode($post['contOverview']);
$xml->Category = xml_encode($post['contCategory']);
$xml->WebUI = xml_encode(trim($post['contWebUI']));
$xml->TemplateURL = xml_encode($post['contTemplateURL']);
$xml->Icon = xml_encode(trim($post['contIcon']));
$xml->ExtraParams = xml_encode($post['contExtraParams']);
$xml->PostArgs = xml_encode($post['contPostArgs']);
$xml->CPUset = xml_encode($post['contCPUset']);
$xml->DateInstalled = xml_encode(time());
$xml->DonateText = xml_encode($post['contDonateText']);
$xml->DonateLink = xml_encode($post['contDonateLink']);
$xml->Requires = xml_encode($post['contRequires']);
$xml['version'] = 2;
$xml->Name = xml_encode(preg_replace('/\s+/', '', $post['contName']));
$xml->Repository = xml_encode(trim($post['contRepository']));
$xml->Registry = xml_encode(trim($post['contRegistry']));
if (isset($post['netCONT']) && !empty(trim($post['netCONT']))) {
$xml->Network = xml_encode($post['contNetwork'].':'.$post['netCONT']);
} else {
$xml->Network = xml_encode($post['contNetwork']);
}
$xml->MyIP = xml_encode($post['contMyIP']);
$xml->Shell = xml_encode($post['contShell']);
$xml->Privileged = strtolower($post['contPrivileged']??'')=='on' ? 'true' : 'false';
$xml->Support = xml_encode($post['contSupport']);
$xml->Project = xml_encode($post['contProject']);
$xml->Overview = xml_encode($post['contOverview']);
$xml->Category = xml_encode($post['contCategory']);
$xml->WebUI = xml_encode(trim($post['contWebUI']));
$xml->TemplateURL = xml_encode($post['contTemplateURL']);
$xml->Icon = xml_encode(trim($post['contIcon']));
$xml->ExtraParams = xml_encode($post['contExtraParams']);
$xml->PostArgs = xml_encode($post['contPostArgs']);
$xml->CPUset = xml_encode($post['contCPUset']);
$xml->DateInstalled = xml_encode(time());
$xml->DonateText = xml_encode($post['contDonateText']);
$xml->DonateLink = xml_encode($post['contDonateLink']);
$xml->Requires = xml_encode($post['contRequires']);
$size = is_array($post['confName']??null) ? count($post['confName']) : 0;
for ($i = 0; $i < $size; $i++) {
$Type = $post['confType'][$i];
@@ -73,6 +105,32 @@ function postToXML($post, $setOwnership=false) {
$config['Required'] = xml_encode($post['confRequired'][$i]);
$config['Mask'] = xml_encode($post['confMask'][$i]);
}
if (isset($post['contTailscale']) && strtolower($post['contTailscale']) == 'on') {
$xml->TailscaleEnabled = 'true';
$xml->TailscaleIsExitNode = xml_encode($post['TSisexitnode']);
$xml->TailscaleHostname = xml_encode($post['TShostname']);
$xml->TailscaleExitNodeIP = isset($post['TSexitnodeip']) ? xml_encode($post['TSexitnodeip']) : '';
$xml->TailscaleSSH = xml_encode($post['TSssh']);
$xml->TailscaleUserspaceNetworking = xml_encode($post['TSuserspacenetworking']);
$xml->TailscaleLANAccess = xml_encode($post['TSallowlanaccess']);
$xml->TailscaleServe = xml_encode($post['TSserve']);
$xml->TailscaleWebUI = xml_encode(generateTSwebui($post['TSwebui'], $post['TSserve'], $post['contWebUI']));
if (isset($post['TSserve']) && strtolower($post['TSserve']) !== 'no') {
$xml->TailscaleServePort = xml_encode($post['TSserveport']);
$xml->TailscaleServeLocalPath = xml_encode($post['TSservelocalpath']);
$xml->TailscaleServeProtocol = xml_encode($post['TSserveprotocol']);
$xml->TailscaleServeProtocolPort = xml_encode($post['TSserveprotocolport']);
$xml->TailscaleServePath = xml_encode($post['TSservepath']);
}
$xml->TailscaleDParams = xml_encode($post['TSdaemonparams']);
$xml->TailscaleParams = xml_encode($post['TSextraparams']);
$xml->TailscaleRoutes = xml_encode($post['TSroutes']);;
$xml->TailscaleAcceptRoutes = xml_encode($post['TSacceptroutes']);;
if (isset($post['TStroubleshooting']) && strtolower($post['TStroubleshooting']) === 'on') {
$xml->TailscaleTroubleshooting = 'true';
}
}
$xml->TailscaleStateDir = xml_encode($post['TSstatedir']);
$dom = new DOMDocument('1.0');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
@@ -82,29 +140,49 @@ function postToXML($post, $setOwnership=false) {
function xmlToVar($xml) {
global $subnet;
$xml = is_file($xml) ? simplexml_load_file($xml) : simplexml_load_string($xml);
$out = [];
$out['Name'] = preg_replace('/\s+/', '', xml_decode($xml->Name));
$out['Repository'] = xml_decode($xml->Repository);
$out['Registry'] = xml_decode($xml->Registry);
$out['Network'] = xml_decode($xml->Network);
$out['MyIP'] = xml_decode($xml->MyIP ?? '');
$out['Shell'] = xml_decode($xml->Shell ?? 'sh');
$out['Privileged'] = xml_decode($xml->Privileged);
$out['Support'] = xml_decode($xml->Support);
$out['Project'] = xml_decode($xml->Project);
$out['Overview'] = stripslashes(xml_decode($xml->Overview));
$out['Category'] = xml_decode($xml->Category);
$out['WebUI'] = xml_decode($xml->WebUI);
$out['TemplateURL'] = xml_decode($xml->TemplateURL);
$out['Icon'] = xml_decode($xml->Icon);
$out['ExtraParams'] = xml_decode($xml->ExtraParams);
$out['PostArgs'] = xml_decode($xml->PostArgs);
$out['CPUset'] = xml_decode($xml->CPUset);
$out['DonateText'] = xml_decode($xml->DonateText);
$out['DonateLink'] = xml_decode($xml->DonateLink);
$out['Requires'] = xml_decode($xml->Requires);
$out['Config'] = [];
$xml = is_file($xml) ? simplexml_load_file($xml) : simplexml_load_string($xml);
$out = [];
$out['Name'] = preg_replace('/\s+/', '', xml_decode($xml->Name));
$out['Repository'] = xml_decode($xml->Repository);
$out['Registry'] = xml_decode($xml->Registry);
$out['Network'] = xml_decode($xml->Network);
$out['MyIP'] = xml_decode($xml->MyIP ?? '');
$out['Shell'] = xml_decode($xml->Shell ?? 'sh');
$out['Privileged'] = xml_decode($xml->Privileged);
$out['Support'] = xml_decode($xml->Support);
$out['Project'] = xml_decode($xml->Project);
$out['Overview'] = stripslashes(xml_decode($xml->Overview));
$out['Category'] = xml_decode($xml->Category);
$out['WebUI'] = xml_decode($xml->WebUI);
$out['TemplateURL'] = xml_decode($xml->TemplateURL);
$out['Icon'] = xml_decode($xml->Icon);
$out['ExtraParams'] = xml_decode($xml->ExtraParams);
$out['PostArgs'] = xml_decode($xml->PostArgs);
$out['CPUset'] = xml_decode($xml->CPUset);
$out['DonateText'] = xml_decode($xml->DonateText);
$out['DonateLink'] = xml_decode($xml->DonateLink);
$out['Requires'] = xml_decode($xml->Requires);
$out['TailscaleEnabled'] = xml_decode($xml->TailscaleEnabled ?? '');
$out['TailscaleIsExitNode'] = xml_decode($xml->TailscaleIsExitNode ?? '');
$out['TailscaleHostname'] = xml_decode($xml->TailscaleHostname ?? '');
$out['TailscaleExitNodeIP'] = xml_decode($xml->TailscaleExitNodeIP ?? '');
$out['TailscaleSSH'] = xml_decode($xml->TailscaleSSH ?? '');
$out['TailscaleLANAccess'] = xml_decode($xml->TailscaleLANAccess ?? '');
$out['TailscaleUserspaceNetworking'] = xml_decode($xml->TailscaleUserspaceNetworking ?? '');
$out['TailscaleServe'] = xml_decode($xml->TailscaleServe ?? '');
$out['TailscaleServePort'] = xml_decode($xml->TailscaleServePort ?? '');
$out['TailscaleServeLocalPath'] = xml_decode($xml->TailscaleServeLocalPath ?? '');
$out['TailscaleServeProtocol'] = xml_decode($xml->TailscaleServeProtocol ?? '');
$out['TailscaleServeProtocolPort'] = xml_decode($xml->TailscaleServeProtocolPort ?? '');
$out['TailscaleServePath'] = xml_decode($xml->TailscaleServePath ?? '');
$out['TailscaleWebUI'] = xml_decode($xml->TailscaleWebUI ?? '');
$out['TailscaleRoutes'] = xml_decode($xml->TailscaleRoutes ?? '');
$out['TailscaleAcceptRoutes'] = xml_decode($xml->TailscaleAcceptRoutes ?? '');
$out['TailscaleDParams'] = xml_decode($xml->TailscaleDParams ?? '');
$out['TailscaleParams'] = xml_decode($xml->TailscaleParams ?? '');
$out['TailscaleStateDir'] = xml_decode($xml->TailscaleStateDir ?? '');
$out['TailscaleTroubleshooting'] = xml_decode($xml->TailscaleTroubleshooting ?? '');
$out['Config'] = [];
if (isset($xml->Config)) {
foreach ($xml->Config as $config) {
$c = [];
@@ -132,7 +210,11 @@ function xmlToVar($xml) {
$out['Network'] = xml_decode($xml->Networking->Mode);
}
// check if network exists
if (!key_exists($out['Network'],$subnet)) $out['Network'] = 'none';
if (preg_match('/^container:(.*)/', $out['Network'])) {
$out['Network'] = $out['Network'];
} elseif (!key_exists($out['Network'],$subnet)) {
$out['Network'] = 'none';
}
// V1 compatibility
if ($xml['version'] != '2') {
if (isset($xml->Description)) {
@@ -241,7 +323,11 @@ function xmlToCommand($xml, $create_paths=false) {
$xml = xmlToVar($xml);
$cmdName = strlen($xml['Name']) ? '--name='.escapeshellarg($xml['Name']) : '';
$cmdPrivileged = strtolower($xml['Privileged'])=='true' ? '--privileged=true' : '';
$cmdNetwork = preg_match('/\-\-net(work)?=/',$xml['ExtraParams']) ? "" : '--net='.escapeshellarg(strtolower($xml['Network']));
if (preg_match('/^container:(.*)/', $xml['Network'])) {
$cmdNetwork = preg_match('/\-\-net(work)?=/',$xml['ExtraParams']) ? "" : '--net='.escapeshellarg($xml['Network']);
} else {
$cmdNetwork = preg_match('/\-\-net(work)?=/',$xml['ExtraParams']) ? "" : '--net='.escapeshellarg(strtolower($xml['Network']));
}
$cmdMyIP = '';
foreach (explode(' ',str_replace(',',' ',$xml['MyIP'])) as $myIP) if ($myIP) $cmdMyIP .= (strpos($myIP,':')?'--ip6=':'--ip=').escapeshellarg($myIP).' ';
$cmdCPUset = strlen($xml['CPUset']) ? '--cpuset-cpus='.escapeshellarg($xml['CPUset']) : '';
@@ -254,7 +340,7 @@ function xmlToCommand($xml, $create_paths=false) {
$Variables[] = 'TZ="'.$var['timeZone'].'"';
// Add HOST_OS variable
$Variables[] = 'HOST_OS="Unraid"';
// Add HOST_HOSTNAME variable
// Add HOST_HOSTNAME variable
$Variables[] = 'HOST_HOSTNAME="'.$var['NAME'].'"';
// Add HOST_CONTAINERNAME variable
$Variables[] = 'HOST_CONTAINERNAME="'.$xml['Name'].'"';
@@ -263,6 +349,70 @@ function xmlToCommand($xml, $create_paths=false) {
if (strlen($xml['WebUI'])) $Labels[] = 'net.unraid.docker.webui='.escapeshellarg($xml['WebUI']);
if (strlen($xml['Icon'])) $Labels[] = 'net.unraid.docker.icon='.escapeshellarg($xml['Icon']);
// Initialize Tailscale variables
$TS_entrypoint = '';
$TS_hook = '';
$TS_hostname = '';
$TS_hostname_label = '';
$TS_ssh = '';
$TS_tundev = '';
$TS_cap = '';
$TS_exitnode = '';
$TS_exitnode_ip = '';
$TS_lan_access = '';
$TS_userspace_networking = '';
$TS_daemon_params = '';
$TS_extra_params = '';
$TS_state_dir = '';
$TS_serve_funnel = '';
$TS_serve_port = '';
$TS_serve_local_path = '';
$TS_serve_protocol = '';
$TS_serve_protocol_port = '';
$TS_serve_path = '';
$TS_web_ui = '';
$TS_troubleshooting = '';
$TS_routes = '';
$TS_accept_routes ='';
$TS_postargs = '';
// Get all information from xml and create variables for cmd
if ($xml['TailscaleEnabled'] == 'true') {
$TS_entrypoint = '--entrypoint=\'/opt/unraid/tailscale\'';
$TS_hook = '-v \'/usr/local/share/docker/tailscale_container_hook\':\'/opt/unraid/tailscale\'';
$TS_hostname = !empty($xml['TailscaleHostname']) ? '-e TAILSCALE_HOSTNAME=' . escapeshellarg($xml['TailscaleHostname']) : '';
$TS_hostname_label = !empty($xml['TailscaleHostname']) ? '-l net.unraid.docker.tailscale.hostname=' . escapeshellarg($xml['TailscaleHostname']) : '';
$TS_ssh = !empty($xml['TailscaleSSH']) ? '-e TAILSCALE_USE_SSH=' . escapeshellarg($xml['TailscaleSSH']) : '';
$TS_daemon_params = !empty($xml['TailscaleDParams']) ? '-e TAILSCALED_PARAMS=' . escapeshellarg($xml['TailscaleDParams']) : '';
$TS_extra_params = !empty($xml['TailscaleParams']) ? '-e TAILSCALE_PARAMS=' . escapeshellarg($xml['TailscaleParams']) : '';
$TS_state_dir = !empty($xml['TailscaleStateDir']) ? '-e TAILSCALE_STATE_DIR=' . escapeshellarg($xml['TailscaleStateDir']) : '';
$TS_userspace_networking = !empty($xml['TailscaleUserspaceNetworking']) ? '-e TAILSCALE_USERSPACE_NETWORKING=' . escapeshellarg($xml['TailscaleUserspaceNetworking']) : '';
// Only add tun, cap and specific vairables to containers which are defined as Exit Nodes and Userspace Networking disabled
if (_var($xml,'TailscaleIsExitNode') == 'true') {
$TS_tundev = preg_match('/--d(evice)?[= ](\'?\/dev\/net\/tun\'?)/', $xml['ExtraParams']) ? "" : "--device='/dev/net/tun'";
$TS_cap = preg_match('/--cap\-add=NET_ADMIN/', $xml['ExtraParams']) ? "" : "--cap-add=NET_ADMIN";
$TS_exitnode = '-e TAILSCALE_EXIT_NODE=true';
} elseif (_var($xml,'TailscaleUserspaceNetworking') == 'false') {
$TS_tundev = preg_match('/--d(evice)?[= ](\'?\/dev\/net\/tun\'?)/', $xml['ExtraParams']) ? "" : "--device='/dev/net/tun'";
$TS_cap = preg_match('/--cap\-add=NET_ADMIN/', $xml['ExtraParams']) ? "" : "--cap-add=NET_ADMIN";
$TS_lan_access = '-e TAILSCALE_ALLOW_LAN_ACCESS=' . escapeshellarg($xml['TailscaleLANAccess']);
$TS_exitnode_ip = !empty($xml['TailscaleExitNodeIP']) ? '-e TAILSCALE_EXIT_NODE_IP=' . escapeshellarg($xml['TailscaleExitNodeIP']) : '';
}
$TS_serve_funnel = ($xml['TailscaleServe'] == 'funnel') ? '-e TAILSCALE_FUNNEL=true' : '';
$TS_serve_port = !empty($xml['TailscaleServePort']) ? '-e TAILSCALE_SERVE_PORT=' . escapeshellarg($xml['TailscaleServePort']) : '';
$TS_serve_local_path = !empty($xml['TailscaleServeLocalPath']) ? '-e TAILSCALE_SERVE_LOCALPATH=' . escapeshellarg($xml['TailscaleServeLocalPath']) : '';
$TS_serve_protocol = !empty($xml['TailscaleServeProtocol']) ? '-e TAILSCALE_SERVE_PROTOCOL=' . escapeshellarg($xml['TailscaleServeProtocol']) : '';
$TS_serve_protocol_port = !empty($xml['TailscaleServeProtocolPort']) ? '-e TAILSCALE_SERVE_PROTOCOL_PORT=' . escapeshellarg($xml['TailscaleServeProtocolPort']) : '';
$TS_serve_path = !empty($xml['TailscaleServePath']) ? '-e TAILSCALE_SERVE_PATH=' . escapeshellarg($xml['TailscaleServePath']) : '';
$TS_web_ui = !empty($xml['TailscaleWebUI']) ? '-l net.unraid.docker.tailscale.webui=' . escapeshellarg($xml['TailscaleWebUI']) : '';
$TS_troubleshooting = !empty($xml['TailscaleTroubleshooting']) ? '-e TAILSCALE_TROUBLESHOOTING=' . escapeshellarg($xml['TailscaleTroubleshooting']) : '';
$TS_routes = !empty($xml['TailscaleRoutes']) ? '-e TAILSCALE_ADVERTISE_ROUTES=' . escapeshellarg($xml['TailscaleRoutes']) : '';
$TS_accept_routes = !empty($xml['TailscaleAcceptRoutes']) && $xml['TailscaleAcceptRoutes'] === 'true' ? '-e TAILSCALE_ACCEPT_ROUTES=true' : '';
if (!empty($xml['PostArgs'])) {
$TS_postargs = '-e ORG_POSTARGS=' . escapeshellarg($xml['PostArgs']);
$xml['PostArgs'] = '';
}
}
foreach ($xml['Config'] as $key => $config) {
$confType = strtolower(strval($config['Type']));
$hostConfig = strlen($config['Value']) ? $config['Value'] : $config['Default'];
@@ -320,8 +470,8 @@ function xmlToCommand($xml, $create_paths=false) {
$pid_limit = "";
}
$cmd = sprintf($docroot.'/plugins/dynamix.docker.manager/scripts/docker create %s %s %s %s %s %s %s %s %s %s %s %s %s %s',
$cmdName, $cmdNetwork, $cmdMyIP, $cmdCPUset, $pid_limit, $cmdPrivileged, implode(' -e ', $Variables), implode(' -l ', $Labels), implode(' -p ', $Ports), implode(' -v ', $Volumes), implode(' --device=', $Devices), $xml['ExtraParams'], escapeshellarg($xml['Repository']), $xml['PostArgs']);
$cmd = sprintf($docroot.'/plugins/dynamix.docker.manager/scripts/docker create %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s',
$cmdName, $TS_entrypoint, $cmdNetwork, $cmdMyIP, $cmdCPUset, $pid_limit, $cmdPrivileged, implode(' -e ', $Variables), $TS_hostname, $TS_exitnode, $TS_exitnode_ip, $TS_lan_access, $TS_routes, $TS_accept_routes, $TS_ssh, $TS_userspace_networking, $TS_serve_funnel, $TS_serve_port, $TS_serve_local_path, $TS_serve_protocol, $TS_serve_protocol_port, $TS_serve_path, $TS_daemon_params, $TS_extra_params, $TS_state_dir, $TS_troubleshooting, $TS_postargs, implode(' -l ', $Labels), $TS_web_ui, $TS_hostname_label, implode(' -p ', $Ports), implode(' -v ', $Volumes), $TS_hook, $TS_cap, $TS_tundev, implode(' --device=', $Devices), $xml['ExtraParams'], escapeshellarg($xml['Repository']), $xml['PostArgs']);
return [preg_replace('/\s\s+/', ' ', $cmd), $xml['Name'], $xml['Repository']];
}
function stopContainer($name, $t=false, $echo=true) {
@@ -486,7 +636,7 @@ function execCommand($command, $echo=true) {
function getXmlVal($xml, $element, $attr=null, $pos=0) {
$xml = (is_file($xml)) ? simplexml_load_file($xml) : simplexml_load_string($xml);
$element = $xml->xpath("//$element")[$pos];
$element = $xml->xpath("//$element")[$pos] ?? null;
return isset($element) ? (isset($element[$attr]) ? strval($element[$attr]) : strval($element)) : "";
}
@@ -508,7 +658,7 @@ function setXmlVal(&$xml, $value, $el, $attr=null, $pos=0) {
function getAllocations() {
global $DockerClient, $host;
$ports = [];
foreach ($DockerClient->getDockerContainers() as $ct) {
$list = $port = [];

View File

@@ -1,10 +1,11 @@
var eventURL = '/plugins/dynamix.docker.manager/include/Events.php';
function addDockerContainerContext(container, image, template, started, paused, update, autostart, webui, shell, id, Support, Project, Registry, donateLink, ReadMe) {
function addDockerContainerContext(container, image, template, started, paused, update, autostart, webui, tswebui, shell, id, Support, Project, Registry, donateLink, ReadMe) {
var opts = [];
context.settings({right:false,above:false});
if (started && !paused) {
if (webui !== '' && webui != '#') opts.push({text:_('WebUI'), icon:'fa-globe', href:webui, target:'_blank'});
if (webui !== '' && webui != '#') opts.push({text:_('WebUI'), icon:'fa-globe', action:function(e){e.preventDefault();window.open(webui,'_blank');}});
if (tswebui !== '' && tswebui != '#') opts.push({text:_('Tailscale WebUI'), icon:'fa-globe', action:function(e){e.preventDefault();window.open(tswebui,'_blank');}});
opts.push({text:_('Console'), icon:'fa-terminal', action:function(e){e.preventDefault(); openTerminal('docker',container,shell);}});
opts.push({divider:true});
}
@@ -48,6 +49,7 @@ function addDockerContainerContext(container, image, template, started, paused,
opts.push({divider:true});
opts.push({text:_('Donate'),icon:'fa-external-link', href:donateLink,target:'_blank'});
}
context.destroy('#'+id);
context.attach('#'+id, opts);
}
function addDockerImageContext(image, imageTag) {

View File

@@ -24,7 +24,7 @@ $_SERVER['REQUEST_URI'] = "scripts";
$login_locale = _var($display,'locale');
require_once "$docroot/webGui/include/Translations.php";
exec("pgrep docker", $pid);
exec('pgrep --ns $$ docker', $pid);
if (count($pid) == 1) exit(0);
$DockerClient = new DockerClient();

View File

@@ -170,6 +170,7 @@ foreach (explode('*',rawurldecode($argv[1])) as $value) {
$xml = file_get_contents($tmpl);
[$cmd, $Name, $Repository] = xmlToCommand($tmpl);
$Registry = getXmlVal($xml, "Registry");
$TS_Enabled = getXmlVal($xml, "TailscaleEnabled");
$oldImageID = $DockerClient->getImageID($Repository);
// pull image
if (!pullImage_nchan($Name, $Repository)) continue;
@@ -182,14 +183,25 @@ foreach (explode('*',rawurldecode($argv[1])) as $value) {
// attempt graceful stop of container first
stopContainer_nchan($Name);
}
if ( ($argv[2]??null) == "ca_docker_run_override" )
if ( ($argv[2]??null) == "ca_docker_run_override" )
$startContainer = true;
if ( $startContainer )
$cmd = str_replace('/docker create ', '/docker run -d ', $cmd);
// force kill container if still running after 10 seconds
if (empty($_GET['communityApplications'])) removeContainer_nchan($Name);
// 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_nchan($cmd);
if ($startContainer) addRoute($Name); // add route for remote WireGuard access
$DockerClient->flushCaches();

View File

@@ -12,3 +12,6 @@
.ui-dropdownchecklist-indent{padding-left:7px}
.ui-dropdownchecklist-text{color:#1c1c1c;font-size:1.3rem}
.ui-dropdownchecklist .ui-widget-content .ui-state-default{background:#f2f2f2;border:0px}
.ui-tailscale-row {display: flex;justify-content: space-between;min-width: 300px;width: 100%;flex-wrap: nowrap;overflow-x: auto;}
.ui-tailscale-label {flex: 1;}
.ui-tailscale-value {flex: 3;}

View File

@@ -12,3 +12,6 @@
.ui-dropdownchecklist-indent{padding-left:7px}
.ui-dropdownchecklist-text{color:#f2f2f2;font-size:1.3rem}
.ui-dropdownchecklist .ui-widget-content .ui-state-default{background:#1c1c1c;border:0px}
.ui-tailscale-row {display: flex;justify-content: space-between;min-width: 300px;width: 100%;flex-wrap: nowrap;overflow-x: auto;}
.ui-tailscale-label {flex: 1;}
.ui-tailscale-value {flex: 3;}

View File

@@ -12,3 +12,6 @@
.ui-dropdownchecklist-indent{padding-left:7px}
.ui-dropdownchecklist-text{color:#f2f2f2;font-size:1.3rem}
.ui-dropdownchecklist .ui-widget-content .ui-state-default{background:#1c1c1c;border:0px}
.ui-tailscale-row {display: flex;justify-content: space-between;min-width: 300px;width: 100%;flex-wrap: nowrap;overflow-x: auto;}
.ui-tailscale-label {flex: 1;}
.ui-tailscale-value {flex: 3;}

View File

@@ -12,3 +12,6 @@
.ui-dropdownchecklist-indent{padding-left:7px}
.ui-dropdownchecklist-text{color:#1c1c1c;font-size:1.3rem}
.ui-dropdownchecklist .ui-widget-content .ui-state-default{background:#f2f2f2;border:0px}
.ui-tailscale-row {display: flex;justify-content: space-between;min-width: 300px;width: 100%;flex-wrap: nowrap;overflow-x: auto;}
.ui-tailscale-label {flex: 1;}
.ui-tailscale-value {flex: 3;}

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{
".nuxt/nuxt-custom-elements/entries/unraid-components.client.mjs": {
"file": "_nuxt/unraid-components.client-CQOXNcK4.js",
"file": "_nuxt/unraid-components.client-DaXtPGYC.js",
"name": "unraid-components.client",
"src": ".nuxt/nuxt-custom-elements/entries/unraid-components.client.mjs",
"isEntry": true,
@@ -8,5 +8,5 @@
"_nuxt/unraid-components-p_3YF86n.css"
]
},
"ts": 1723595088
"ts": 1736299175
}

View File

@@ -19,7 +19,7 @@ Code="e944"
?>
<?
// Remove stale /tmp/plugin/*.plg entries (check that script 'plugin' is not running to avoid clashes)
if (!exec("pgrep -f $docroot/plugins/dynamix.plugin.manager/scripts/plugin")) {
if (!exec('pgrep --ns $$ -f '."$docroot/plugins/dynamix.plugin.manager/scripts/plugin")) {
foreach (glob("/tmp/plugins/*.{plg,txt}", GLOB_NOSORT+GLOB_BRACE) as $entry) if (!file_exists("/var/log/plugins/".basename($entry))) @unlink($entry);
}
$check = $notify['version'] ? 0 : 1;

View File

@@ -209,6 +209,8 @@ function VMClone(uuid, name){
box.find('#target').val(name + "_clone");
document.getElementById("Free").checked = true;
document.getElementById("Overwrite").checked = true;
overwrite = box.find("#Overwrite");
overwrite.attr("checked:true");
box.dialog({
title: "_(VM Clone)_",
height: 'auto',
@@ -511,9 +513,9 @@ $(function() {
<table id='snapshot' class='snapshot'>
<tr><td>_(VM Name)_:</td><td><label id="VMName"></label></td></tr>
<tr><td>_(Snapshot Name)_:</td><td><input type="text" id="targetsnap" autocomplete="off" spellcheck="false" value="--generate" onclick="this.select()">_(Check free space)_: <input type="checkbox" id="targetsnapfspc" checked></td></tr>
<tr><td>_(Description )_:</td><td><input type="text" id="targetsnapdesc" autocomplete="off" spellcheck="false" value="" onclick="this.select()"></td></tr>
<tr id="memoryline"><td>_(Memory dump )_:</td><td><input type="checkbox" id="targetsnapmem" checked></td></tr>
<tr id="fstypeline"><td>_(FS Native Snapshot )_:</td><td><label id="fstype"></label><input type="checkbox" id="targetsnapfstype">_(Unchecked will use QEMU External Snapshot)_</td></tr>
<tr><td>_(Description)_:</td><td><input type="text" id="targetsnapdesc" autocomplete="off" spellcheck="false" value="" onclick="this.select()"></td></tr>
<tr id="memoryline"><td>_(Memory dump)_:</td><td><input type="checkbox" id="targetsnapmem" checked></td></tr>
<tr id="fstypeline"><td>_(FS Native Snapshot)_:</td><td><label id="fstype"></label><input type="checkbox" id="targetsnapfstype">_(Unchecked will use QEMU External Snapshot)_</td></tr>
</table>
</div>
@@ -537,7 +539,7 @@ _(VM Name)_: <label id="VMName"></label><br>
_(Snapshot Name)_: <input type="text" id="targetsnap" hidden><label id="targetsnapl"></label><br><br>
<table id='block' class='snapshot'>
<tr><td>_(Base Image)_:</td><td><select class="targetblockbase"></select></td></tr>
<tr name="toprow" class="toprow" ><td>_(Top Image )_:</td><td><select class="targetblocktop" name="targetblocktop" id="targetblocktop"></select></td><td></tr>
<tr name="toprow" class="toprow" ><td>_(Top Image)_:</td><td><select class="targetblocktop" name="targetblocktop" id="targetblocktop"></select></td><td></tr>
<tr name="targetpivotrow" class="targetpivotrow" ><td>_(Pivot)_:</td><td><input type="checkbox" id="targetpivot" checked></td></tr>
<tr name="targetdeleterow" class="targetdeleterow" ><td>_(Delete)_:</td><td><input type="checkbox" id="targetdelete" checked></td></tr>
</table>
@@ -549,9 +551,9 @@ _(Snapshot Name)_: <input type="text" id="targetsnap" hidden><label id="targetsn
<table class='snapshot'>
<tr><td>_(VM Being Cloned)_:</td><td><span id="VMBeingCloned"></span></td></tr>
<tr><td>_(New VM)_:</td><td><input type="text" id="target" autocomplete="off" spellcheck="false" value="" onclick="this.select()" ></td></tr>
<tr><td>_(Overwrite)_:</td><td><input type="checkbox" id="Overwrite" value="" ></td></tr>
<tr><td>_(Overwrite)_:</td><td><input type="checkbox" id="Overwrite" value="" checked></td></tr>
<tr hidden><td>_(Start Cloned VM)_:</td><td><input type="checkbox" id="Start" value="" ></td></tr>
<tr hidden><td>_(Edit VM after clone)_:</td><td><input type="checkbox" id="Edit" value="" ></td></tr>
<tr><td>_(Check free space)_:</td><td><input type="checkbox" id="Free" value="" ></td></tr>
<tr><td>_(Check free space)_:</td><td><input type="checkbox" id="Free" value="" checked></td></tr>
</table>
</div>

View File

@@ -1,5 +1,5 @@
Menu="VMs:0"
Title="VM Usage Statisics"
Title="VM Usage Statistics"
Nchan="vm_usage:stop"
Cond="exec(\"grep -o '^USAGE=.Y' /boot/config/domain.cfg 2>/dev/null\") && is_file('/var/run/libvirt/libvirtd.pid')"
---

View File

@@ -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 ;
@@ -112,7 +113,7 @@ foreach ($vms as $vm) {
unset($dom);
if (!isset($domain_cfg["CONSOLE"])) $vmrcconsole = "web" ; else $vmrcconsole = $domain_cfg["CONSOLE"] ;
if (!isset($domain_cfg["RDPOPT"])) $vmrcconsole .= ";no" ; else $vmrcconsole .= ";".$domain_cfg["RDPOPT"] ;
$menu = sprintf("onclick=\"addVMContext('%s','%s','%s','%s','%s','%s','%s','%s','%s','%s', '%s')\"", addslashes($vm),addslashes($uuid),addslashes($template),$state,addslashes($vmrcurl),strtoupper($vmrcprotocol),addslashes($log),addslashes($fstype), $vmrcconsole,false,addslashes(str_replace('"',"'",$WebUI)));
$menu = sprintf("onclick=\"addVMContext('%s','%s','%s','%s','%s','%s','%s','%s','%s','%s', '%s')\"", htmlentities($vm, ENT_QUOTES),addslashes($uuid),addslashes($template),$state,addslashes($vmrcurl),strtoupper($vmrcprotocol),htmlentities($log,ENT_QUOTES),addslashes($fstype), $vmrcconsole,false,addslashes(str_replace('"',"'",$WebUI)));
$kvm[] = "kvm.push({id:'$uuid',state:'$state'});";
switch ($state) {
case 'running':

View File

@@ -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':
@@ -149,7 +150,9 @@ case 'domain-start-consoleRV':
$vvarray = array() ;
$vvarray[] = "[virt-viewer]\n";
$vvarray[] = "type=$protocol\n";
$vvarray[] = "host="._var($_SERVER,'HTTP_HOST')."\n" ;
$vvarrayhost = _var($_SERVER,'HTTP_HOST');
if (strpos($vvarrayhost,":")) $vvarrayhost = parse_url($vvarrayhost,PHP_URL_HOST);
$vvarray[] = "host=$vvarrayhost\n" ;
$vvarray[] = "port=$port\n" ;
$vvarray[] = "delete-this-file=1\n" ;
if (!is_dir("/mnt/user/system/remoteviewer")) mkdir("/mnt/user/system/remoteviewer") ;
@@ -182,7 +185,9 @@ case 'domain-consoleRV':
$vvarray = array() ;
$vvarray[] = "[virt-viewer]\n";
$vvarray[] = "type=$protocol\n";
$vvarray[] = "host="._var($_SERVER,'HTTP_HOST')."\n" ;
$vvarrayhost = _var($_SERVER,'HTTP_HOST');
if (strpos($vvarrayhost,":")) $vvarrayhost = parse_url($vvarrayhost,PHP_URL_HOST);
$vvarray[] = "host=$vvarrayhost\n" ;
$vvarray[] = "port=$port\n" ;
$vvarray[] = "delete-this-file=1\n" ;
if (!is_dir("/mnt/user/system/remoteviewer")) mkdir("/mnt/user/system/remoteviewer") ;

View File

@@ -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']}");
}
}

View File

@@ -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"] ;
}

View File

@@ -239,7 +239,7 @@ function addVMContext(name, uuid, template, state, vmrcurl, vmrcprotocol, log, f
}});
}
}
if (usage) context.attach('#vmusage-'+uuid, opts); else context.attach('#vm-'+uuid, opts);
if (usage) { context.destroy('#vmusage-'+uuid); context.attach('#vmusage-'+uuid, opts); } else { context.destroy('#vm-'+uuid); context.attach('#vm-'+uuid, opts); }
}
function addVMSnapContext(name, uuid, template, state, snapshotname, method){
var opts = [];
@@ -273,6 +273,7 @@ function addVMSnapContext(name, uuid, template, state, snapshotname, method){
$('#vm-'+uuid).find('i').removeClass('fa-play fa-square fa-pause').addClass('fa-refresh fa-spin');
selectsnapshot(uuid, name, snapshotname, "remove",true) ;
}});
context.destroy('#vmsnap-'+uuid);
context.attach('#vmsnap-'+uuid, opts);
}
function startAll() {

View File

@@ -77,11 +77,11 @@ while (true) {
$echodata .= "<tr><td>$vm</td>" ;
$echodata .= "<td class='advanced'><span class='cpug-".$vm."'>".$vmdata['cpuguest']."%</span><div class='usage-disk mm'><span id='cpug-".$vm."' style='width:".$vmdata['cpuguest']."%;'> </span><span></span></div></td>";
$echodata .= "<td class='advanced'><span class='cpuh-".$vm."'>".$vmdata['cpuhost']."%</span><div class='usage-disk mm'><span id='cpuh-".$vm."' style='width:".$vmdata['cpuhost']."%;'> </span><span></span></div></td><td>";
$echodata .= my_scale($vmdata['mem']*1024,$unit)."$unit / ".my_scale($vmdata['curmem']*1024,$unit)."$unit";
$echodata .= my_scale($vmdata['mem']*1024,$unit,null,null,1024)."$unit / ".my_scale($vmdata['curmem']*1024,$unit,null,null,1024)."$unit";
if ($vmdata['curmem'] === $vmdata['maxmem']) $echodata .= " </td><td>";
else $echodata .= " / " .my_scale($vmdata['maxmem']*1024,$unit)."$unit </td><td>";
$echodata .= _("Read").": ".my_scale($vmdata['rdrate'],$unit)."$unit/s<br>"._("Write").": ".my_scale($vmdata['wrrate'],$unit)."$unit/s</td><td>";
$echodata .= _("RX").": ".my_scale($vmdata['rxrate'],$unit)."$unit/s<br>"._("TX").": ".my_scale($vmdata['txrate'],$unit)."$unit/s</td></tr>";
else $echodata .= " / " .my_scale($vmdata['maxmem']*1024,$unit,null,null,1024)."$unit </td><td>";
$echodata .= _("Read").": ".my_scale($vmdata['rdrate']/$timer,$unit,null,null,1024)."$unit/s<br>"._("Write").": ".my_scale($vmdata['wrrate']/$timer,$unit,null,null,1024)."$unit/s</td><td>";
$echodata .= _("RX").": ".my_scale($vmdata['rxrate']/$timer,$unit,null,null,1024)."$unit/s<br>"._("TX").": ".my_scale($vmdata['txrate']/$timer,$unit,null,null,1024)."$unit/s</td></tr>";
}
$echo = $echodata ;
}

View File

@@ -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));

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,183 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="48"
height="48"
viewBox="0 0 48 48.000001"
id="svg2"
version="1.1"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="novnc-ios-icon.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="11.313708"
inkscape:cx="27.356195"
inkscape:cy="17.810253"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:object-nodes="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:window-width="2560"
inkscape:window-height="1371"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
<inkscape:grid
type="xygrid"
id="grid4169" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1004.3621)">
<rect
style="opacity:1;fill:#494949;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4167"
width="48"
height="48"
x="0"
y="1004.3621"
inkscape:label="background" />
<path
style="opacity:1;fill:#313131;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 0,1004.3621 v 48 h 20 c 15.512,0 28,-16.948 28,-38 v -10 z"
id="rect4173"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccc"
inkscape:label="darker_grey_plate" />
<g
id="g4300"
style="display:inline;fill:#000000;fill-opacity:1;stroke:none"
transform="translate(0.5,0.5)"
inkscape:label="shadows">
<g
id="g4302"
style="fill:#000000;fill-opacity:1;stroke:none"
inkscape:label="no">
<path
sodipodi:nodetypes="scsccsssscccs"
d="m 11.986926,1016.3621 c 0.554325,0 1.025987,0.2121 1.414987,0.6362 0.398725,0.4138 0.600909,0.9155 0.598087,1.5052 v 6.8586 h -2 v -6.8914 c 0,-0.072 -0.03404,-0.1086 -0.102113,-0.1086 H 7.1021125 C 7.0340375,1018.3621 7,1018.3983 7,1018.4707 v 6.8914 H 5 v -9 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4304"
inkscape:connector-curvature="0"
inkscape:label="n" />
<path
sodipodi:nodetypes="sscsscsscsscssssssssss"
d="m 17.013073,1016.3621 h 4.973854 c 0.554325,0 1.025987,0.2121 1.414986,0.6362 0.398725,0.4138 0.598087,0.9155 0.598087,1.5052 v 4.7172 c 0,0.5897 -0.199362,1.0966 -0.598087,1.5207 -0.388999,0.4138 -0.860661,0.6207 -1.414986,0.6207 h -4.973854 c -0.554325,0 -1.030849,-0.2069 -1.429574,-0.6207 C 15.1945,1024.3173 15,1023.8104 15,1023.2207 v -4.7172 c 0,-0.5897 0.1945,-1.0914 0.583499,-1.5052 0.398725,-0.4241 0.875249,-0.6362 1.429574,-0.6362 z m 4.884815,2 h -4.795776 c -0.06808,0 -0.102112,0.036 -0.102112,0.1086 v 4.7828 c 0,0.072 0.03404,0.1086 0.102112,0.1086 h 4.795776 c 0.06807,0 0.102112,-0.036 0.102112,-0.1086 v -4.7828 c 0,-0.072 -0.03404,-0.1086 -0.102112,-0.1086 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4306"
inkscape:connector-curvature="0"
inkscape:label="o" />
</g>
<g
id="g4308"
style="fill:#000000;fill-opacity:1;stroke:none"
inkscape:label="VNC">
<path
sodipodi:nodetypes="cccccccc"
d="m 12,1036.9177 4.768114,-8.5556 H 19 l -6,11 h -2 l -6,-11 h 2.2318854 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4310"
inkscape:connector-curvature="0"
inkscape:label="V" />
<path
sodipodi:nodetypes="ccccccccccc"
d="m 29,1036.3621 v -8 h 2 v 11 h -2 l -7,-8 v 8 h -2 v -11 h 2 z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4312"
inkscape:connector-curvature="0"
inkscape:label="N" />
<path
sodipodi:nodetypes="cssssccscsscscc"
d="m 43,1030.3621 h -8.897887 c -0.06808,0 -0.102113,0.036 -0.102113,0.1069 v 6.7862 c 0,0.071 0.03404,0.1069 0.102113,0.1069 H 43 v 2 h -8.972339 c -0.56405,0 -1.045437,-0.2037 -1.444162,-0.6111 C 32.1945,1038.3334 32,1037.8292 32,1037.2385 v -6.7528 c 0,-0.5907 0.1945,-1.0898 0.583499,-1.4972 0.398725,-0.4176 0.880112,-0.6264 1.444162,-0.6264 H 43 Z"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path4314"
inkscape:connector-curvature="0"
inkscape:label="C" />
</g>
</g>
<g
id="g4291"
style="stroke:none"
inkscape:label="noVNC">
<g
id="g4282"
style="stroke:none"
inkscape:label="no">
<path
inkscape:connector-curvature="0"
id="path4143"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 11.986926,1016.3621 c 0.554325,0 1.025987,0.2121 1.414987,0.6362 0.398725,0.4138 0.600909,0.9155 0.598087,1.5052 l 0,6.8586 -2,0 0,-6.8914 c 0,-0.072 -0.03404,-0.1086 -0.102113,-0.1086 l -4.7957745,0 C 7.0340375,1018.3621 7,1018.3983 7,1018.4707 l 0,6.8914 -2,0 0,-9 z"
sodipodi:nodetypes="scsccsssscccs"
inkscape:label="n" />
<path
inkscape:connector-curvature="0"
id="path4145"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 17.013073,1016.3621 4.973854,0 c 0.554325,0 1.025987,0.2121 1.414986,0.6362 0.398725,0.4138 0.598087,0.9155 0.598087,1.5052 l 0,4.7172 c 0,0.5897 -0.199362,1.0966 -0.598087,1.5207 -0.388999,0.4138 -0.860661,0.6207 -1.414986,0.6207 l -4.973854,0 c -0.554325,0 -1.030849,-0.2069 -1.429574,-0.6207 C 15.1945,1024.3173 15,1023.8104 15,1023.2207 l 0,-4.7172 c 0,-0.5897 0.1945,-1.0914 0.583499,-1.5052 0.398725,-0.4241 0.875249,-0.6362 1.429574,-0.6362 z m 4.884815,2 -4.795776,0 c -0.06808,0 -0.102112,0.036 -0.102112,0.1086 l 0,4.7828 c 0,0.072 0.03404,0.1086 0.102112,0.1086 l 4.795776,0 c 0.06807,0 0.102112,-0.036 0.102112,-0.1086 l 0,-4.7828 c 0,-0.072 -0.03404,-0.1086 -0.102112,-0.1086 z"
sodipodi:nodetypes="sscsscsscsscssssssssss"
inkscape:label="o" />
</g>
<g
id="g4286"
style="stroke:none"
inkscape:label="VNC">
<path
inkscape:connector-curvature="0"
id="path4147"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 12,1036.9177 4.768114,-8.5556 2.231886,0 -6,11 -2,0 -6,-11 2.2318854,0 z"
sodipodi:nodetypes="cccccccc"
inkscape:label="V" />
<path
inkscape:connector-curvature="0"
id="path4149"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 29,1036.3621 0,-8 2,0 0,11 -2,0 -7,-8 0,8 -2,0 0,-11 2,0 z"
sodipodi:nodetypes="ccccccccccc"
inkscape:label="N" />
<path
inkscape:connector-curvature="0"
id="path4151"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 43,1030.3621 -8.897887,0 c -0.06808,0 -0.102113,0.036 -0.102113,0.1069 l 0,6.7862 c 0,0.071 0.03404,0.1069 0.102113,0.1069 l 8.897887,0 0,2 -8.972339,0 c -0.56405,0 -1.045437,-0.2037 -1.444162,-0.6111 C 32.1945,1038.3334 32,1037.8292 32,1037.2385 l 0,-6.7528 c 0,-0.5907 0.1945,-1.0898 0.583499,-1.4972 0.398725,-0.4176 0.880112,-0.6264 1.444162,-0.6264 l 8.972339,0 z"
sodipodi:nodetypes="cssssccscsscscc"
inkscape:label="C" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

View File

@@ -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": "Ακύρωση"
}

View File

@@ -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."
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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": "キャンセル"
}

View File

@@ -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"
}

View File

@@ -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": "Выход"
}

View File

@@ -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",

View File

@@ -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。"
}

View File

@@ -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;
}

View File

@@ -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{

View File

@@ -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, \
<svg width="8" height="6" version="1.1" viewBox="0 0 8 6" \
xmlns="http://www.w3.org/2000/svg"> \
<path d="m6.5 1.5 -2.5 3 -2.5 -3 5 0" stroke-width="3" \
stroke="rgb(31,31,31)" fill="none" \
stroke-linecap="round" stroke-linejoin="round" /> \
</svg>');
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 <select> 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, \
<svg width="8" height="6" version="1.1" viewBox="0 0 8 6" \
xmlns="http://www.w3.org/2000/svg" transform="rotate(180)" > \
<path d="m6.5 1.5 -2.5 3 -2.5 -3 5 0" stroke-width="3" \
stroke="rgb(31,31,31)" fill="none" \
stroke-linecap="round" stroke-linejoin="round" /> \
</svg>'), 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;
}

View File

@@ -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;

View File

@@ -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();
});
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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 <dzimm@widget.com>, 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 <jef@acme.com>. 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<<m)) !== 0) ? 1: 0;
}
for (let i = 0; i < 16; ++i) {
const m = i << 1;
const n = m + 1;
kn[m] = kn[n] = 0;
for (let o = 28; o < 59; o += 28) {
for (let j = o - 28; j < o; ++j) {
const l = j + totrot[i];
pcr[j] = l < o ? pc1m[l] : pc1m[l - 28];
}
}
for (let j = 0; j < 24; ++j) {
if (pcr[PC2[j]] !== 0) {
kn[m] |= 1 << (23 - j);
}
if (pcr[PC2[j + 24]] !== 0) {
kn[n] |= 1 << (23 - j);
}
}
}
// cookey
for (let i = 0, rawi = 0, KnLi = 0; i < 16; ++i) {
const raw0 = kn[rawi++];
const raw1 = kn[rawi++];
this.keys[KnLi] = (raw0 & 0x00fc0000) << 6;
this.keys[KnLi] |= (raw0 & 0x00000fc0) << 10;
this.keys[KnLi] |= (raw1 & 0x00fc0000) >>> 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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<d.length;i++) {
f[i] = d.charCodeAt(i);
}
return f;
}
function X(d) {
let r = Array(d.length >> 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;
}

View File

@@ -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 };
}
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<tileh; y++) {
let shift = 8-bitsPerPixel;
for (let x=0; x<tilew; x++) {
if (shift<0) {
shift=8-bitsPerPixel;
encoded = this._inflator.inflate(1)[0];
}
let indexInPalette = (encoded>>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<tileh-1) {
encoded = this._inflator.inflate(1)[0];
}
}
return data;
}
_decodeRLETile(tileSize) {
const data = this._tileBuffer;
let i = 0;
while (i < tileSize) {
const pixel = this._readPixels(1);
const length = this._readRLELength();
for (let j = 0; j < length; j++) {
data[i * 4] = pixel[0];
data[i * 4 + 1] = pixel[1];
data[i * 4 + 2] = pixel[2];
data[i * 4 + 3] = pixel[3];
i++;
}
}
return data;
}
_decodeRLEPaletteTile(paletteSize, tileSize) {
const data = this._tileBuffer;
// palette
const palette = this._readPixels(paletteSize);
let offset = 0;
while (offset < tileSize) {
let indexInPalette = this._inflator.inflate(1)[0];
let length = 1;
if (indexInPalette >= 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;
}
}

View File

@@ -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) {

View File

@@ -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;
}
}
}

View File

@@ -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 + "]";
}
}

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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

View File

@@ -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];
}

View File

@@ -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',

View File

@@ -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 */

View File

@@ -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;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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/...');
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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");
}

View File

@@ -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 <github@martintribe.org> (https://github.com/kanaka)",
"contributors": [
"Solly Ross <sross@redhat.com> (https://github.com/directxman12)",
"Peter Åstrand <astrand@cendio.se> (https://github.com/astrand)",
"Samuel Mannehed <samuel@cendio.se> (https://github.com/samhed)",
"Pierre Ossman <ossman@cendio.se> (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": [

View File

@@ -1,7 +1,7 @@
#!/usr/bin/php -q
<?PHP
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
/* Copyright 2005-2024, Lime Technology
* Copyright 2012-2024, Bergware International.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
@@ -22,7 +22,7 @@ $_SERVER['REQUEST_URI'] = '';
$login_locale = _var($display,'locale');
require_once "$docroot/webGui/include/Translations.php";
function write(...$messages){
function write(...$messages) {
$com = curl_init();
curl_setopt_array($com,[
CURLOPT_URL => 'http://localhost/pub/vmaction?buffer_length=1',
@@ -35,7 +35,8 @@ function write(...$messages){
curl_exec($com);
}
curl_close($com);
}
}
function execCommand_nchan($command,$idx) {
$waitID = mt_rand();
[$cmd,$args] = explode(' ',$command,2);

View File

@@ -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"]);

View File

@@ -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";
};
?>
<link rel="stylesheet" href="<?autov('/plugins/dynamix.vm.manager/scripts/codemirror/lib/codemirror.css')?>">
@@ -336,10 +349,12 @@
<input type="hidden" name="domain[memoryBacking]" id="domain_memorybacking" value="<?=htmlspecialchars($arrConfig['domain']['memoryBacking'])?>">
<table>
<tr><td></td><td><span hidden id="zfs-name" class="orange-text"><i class="fa fa-warning"></i> _(Name contains invalid characters or does not start with an alphanumberic for a ZFS storage location<br>Only these special characters are valid Underscore (_) Hyphen (-) Colon (:) Period (.))_</span></td></tr>
<tr><td></td><td>
<span <?=$snaprenamehidden?> id="snap-rename" class="orange-text"><i class="fa fa-warning"></i> _(Rename disabled, <?=$snapcount?> snapshot(s) exists.)_</span>
<span hidden id="zfs-name" class="orange-text"><i class="fa fa-warning"></i> _(Name contains invalid characters or does not start with an alphanumberic for a ZFS storage location<br>Only these special characters are valid Underscore (_) Hyphen (-) Colon (:) Period (.))_</span></td></tr>
<tr>
<td>_(Name)_:</td>
<td><input type="text" name="domain[name]" id="domain_name" oninput="checkName(this.value)" class="textTemplate" title="_(Name of virtual machine)_" placeholder="_(e.g.)_ _(My Workstation)_" value="<?=htmlspecialchars($arrConfig['domain']['name'])?>" required /></td>
<td><input <?=$namedisable?> type="text" name="domain[name]" id="domain_name" oninput="checkName(this.value)" class="textTemplate" title="_(Name of virtual machine)_" placeholder="_(e.g.)_ _(My Workstation)_" value="<?=htmlspecialchars($arrConfig['domain']['name'])?>" required /></td>
<td><textarea class="xml" id="xmlname" rows=1 disabled ><?=htmlspecialchars($xml2['name'])."\n".htmlspecialchars($xml2['uuid'])."\n".htmlspecialchars($xml2['metadata'])?></textarea></td>
</tr>
</table>
@@ -2202,6 +2217,7 @@ $(function() {
$disk_file_sections.filter('.advanced').removeClass('advanced').addClass('wasadvanced');
$disk_input.attr('name', $disk_input.attr('name').replace('new', 'image'));
if (info.isfile) $table.find('.disk_driver').val(info.format);
} else {
$disk_file_sections.filter('.wasadvanced').removeClass('wasadvanced').addClass('advanced');
slideDownRows($disk_file_sections.not(isVMAdvancedMode() ? '.basic' : '.advanced'));
@@ -2546,13 +2562,13 @@ $(function() {
$button.val($button.attr('busyvalue'));
swal({
title: _("Template Name")_,
text: _("Enter name:\nIf name already exists it will be replaced.")_,
title: '_(Template Name)_',
text: "_(Enter name:\nIf name already exists it will be replaced.)_",
type: "input",
showCancelButton: true,
closeOnConfirm: false,
//animation: "slide-from-top",
inputPlaceholder: _("Leaving blank will use OS name.")_
inputPlaceholder: "_(Leaving blank will use OS name.)_"
},
function(inputValue){
@@ -2627,13 +2643,13 @@ $(function() {
$button.val($button.attr('busyvalue'));
swal({
title: _("Template Name")_,
text: _("Enter name:\nIf name already exists it will be replaced.")_,
title: "_(Template Name)_",
text: "_(Enter name:\nIf name already exists it will be replaced.)_",
type: "input",
showCancelButton: true,
closeOnConfirm: false,
//animation: "slide-from-top",
inputPlaceholder: _("Leaving blank will use OS name.")_
inputPlaceholder: "_(Leaving blank will use OS name.)_"
},
function(inputValue){

View File

@@ -216,13 +216,13 @@ $(function() {
$form.find('input').prop('disabled', true);
$button.val($button.attr('busyvalue'));
swal({
title: _("Template Name")_,
text: _("Enter name:\nIf name already exists it will be replaced.")_,
title: "_(Template Name)_",
text: "_(Enter name:\nIf name already exists it will be replaced.)_",
type: "input",
showCancelButton: true,
closeOnConfirm: false,
//animation: "slide-from-top",
inputPlaceholder: _("Leaving blank will use OS name.")_
inputPlaceholder: "_(Leaving blank will use OS name.)_"
},
function(inputValue){
postdata=postdata+"&templatename="+inputValue;

View File

@@ -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
-->
<title>noVNC</title>
<base href="novnc/">
<meta charset="utf-8">
<meta charset="utf-8">
<!-- Always force latest IE rendering engine (even in intranet) & Chrome Frame
Remove this if you use the .htaccess -->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
@@ -42,31 +42,37 @@
-->
<!-- Repeated last so that legacy handling will pick this -->
<link rel="icon" sizes="16x16" type="image/png" href="app/images/icons/novnc-16x16.png">
<link rel="icon" type="image/x-icon" href="app/images/icons/novnc.ico">
<!-- Apple iOS Safari settings -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<!-- Home Screen Icons (favourites and bookmarks use the normal icons) -->
<link rel="apple-touch-icon" sizes="60x60" type="image/png" href="app/images/icons/novnc-60x60.png">
<link rel="apple-touch-icon" sizes="76x76" type="image/png" href="app/images/icons/novnc-76x76.png">
<link rel="apple-touch-icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-120x120.png">
<link rel="apple-touch-icon" sizes="152x152" type="image/png" href="app/images/icons/novnc-152x152.png">
<!-- Home Screen Icons (favourites and bookmarks use the normal icons) -->
<!-- @2x -->
<link rel="apple-touch-icon" sizes="40x40" type="image/png" href="app/images/icons/novnc-ios-40.png">
<link rel="apple-touch-icon" sizes="58x58" type="image/png" href="app/images/icons/novnc-ios-58.png">
<link rel="apple-touch-icon" sizes="80x80" type="image/png" href="app/images/icons/novnc-ios-80.png">
<link rel="apple-touch-icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-ios-120.png">
<link rel="apple-touch-icon" sizes="152x152" type="image/png" href="app/images/icons/novnc-ios-152.png">
<link rel="apple-touch-icon" sizes="167x167" type="image/png" href="app/images/icons/novnc-ios-167.png">
<!-- @3x -->
<link rel="apple-touch-icon" sizes="60x60" type="image/png" href="app/images/icons/novnc-ios-60.png">
<link rel="apple-touch-icon" sizes="87x87" type="image/png" href="app/images/icons/novnc-ios-87.png">
<link rel="apple-touch-icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-ios-120.png">
<link rel="apple-touch-icon" sizes="180x180" type="image/png" href="app/images/icons/novnc-ios-180.png">
<!-- Stylesheets -->
<link rel="stylesheet" href="app/styles/base.css">
<link rel="stylesheet" href="app/styles/input.css">
<!-- this is included as a normal file in order to catch script-loading errors as well -->
<script src="app/error-handler.js"></script>
<!-- Images that will later appear via CSS -->
<link rel="preload" as="image" href="app/images/info.svg">
<link rel="preload" as="image" href="app/images/error.svg">
<link rel="preload" as="image" href="app/images/warning.svg">
<!-- begin scripts -->
<!-- promise polyfills promises for IE11 -->
<script src="vendor/promise.js?ts=20200718"></script>
<!-- ES2015/ES6 modules polyfill -->
<script nomodule src="vendor/browser-es-module-loader/dist/browser-es-module-loader.js?ts=20200718"></script>
<!-- actual script modules -->
<script type="module" crossorigin="anonymous" src="app/ui.js?ts=20200718"></script>
<!-- end scripts -->
<script type="module" crossorigin="anonymous" src="app/error-handler.js"></script>
<script type="module" crossorigin="anonymous" src="app/ui.js"></script>
</head>
<body>
@@ -89,6 +95,8 @@
<h1 class="noVNC_logo" translate="no"><span>no</span><br>VNC</h1>
<hr>
<!-- Drag/Pan the viewport -->
<input type="image" alt="Drag" src="app/images/drag.svg"
id="noVNC_view_drag_button" class="noVNC_button noVNC_hidden"
@@ -151,17 +159,17 @@
<div class="noVNC_heading">
<img alt="" src="app/images/clipboard.svg"> Clipboard
</div>
<p class="noVNC_subheading">
Edit clipboard content in the textarea below.
</p>
<textarea id="noVNC_clipboard_text" rows=5></textarea>
<br>
<input id="noVNC_clipboard_clear_button" type="button"
value="Clear" class="noVNC_submit">
</div>
</div>
<!-- Toggle fullscreen -->
<input type="image" alt="Fullscreen" src="app/images/fullscreen.svg"
<input type="image" alt="Full Screen" src="app/images/fullscreen.svg"
id="noVNC_fullscreen_button" class="noVNC_button noVNC_hidden"
title="Fullscreen">
title="Full Screen">
<!-- Settings -->
<input type="image" alt="Settings" src="app/images/settings.svg"
@@ -169,10 +177,10 @@
title="Settings">
<div class="noVNC_vcenter">
<div id="noVNC_settings" class="noVNC_panel">
<div class="noVNC_heading">
<img alt="" src="app/images/settings.svg"> Settings
</div>
<ul>
<li class="noVNC_heading">
<img alt="" src="app/images/settings.svg"> Settings
</li>
<li>
<label><input id="noVNC_setting_shared" type="checkbox"> Shared Mode</label>
</li>
@@ -267,39 +275,69 @@
</div>
</div>
<div id="noVNC_control_bar_hint"></div>
</div> <!-- End of noVNC_control_bar -->
<div id="noVNC_hint_anchor" class="noVNC_vcenter">
<div id="noVNC_control_bar_hint">
</div>
</div>
<!-- Status Dialog -->
<div id="noVNC_status"></div>
<!-- Connect button -->
<div class="noVNC_center">
<div id="noVNC_connect_dlg">
<div class="noVNC_logo" translate="no"><span>no</span>VNC</div>
<div id="noVNC_connect_button"><div>
<img alt="" src="app/images/connect.svg"> Connect
</div></div>
<p class="noVNC_logo" translate="no"><span>no</span>VNC</p>
<div>
<button id="noVNC_connect_button">
<img alt="" src="app/images/connect.svg"> Connect
</button>
</div>
</div>
</div>
<!-- Server Key Verification Dialog -->
<div class="noVNC_center noVNC_connect_layer">
<div id="noVNC_verify_server_dlg" class="noVNC_panel"><form>
<div class="noVNC_heading">
Server identity
</div>
<div>
The server has provided the following identifying information:
</div>
<div id="noVNC_fingerprint_block">
<b>Fingerprint:</b>
<span id="noVNC_fingerprint"></span>
</div>
<div>
Please verify that the information is correct and press
"Approve". Otherwise press "Reject".
</div>
<div>
<input id="noVNC_approve_server_button" type="submit" value="Approve" class="noVNC_submit">
<input id="noVNC_reject_server_button" type="button" value="Reject" class="noVNC_submit">
</div>
</form></div>
</div>
<!-- Password Dialog -->
<div class="noVNC_center noVNC_connect_layer">
<div id="noVNC_credentials_dlg" class="noVNC_panel"><form>
<ul>
<li id="noVNC_username_block">
<label>Username:</label>
<input id="noVNC_username_input">
</li>
<li id="noVNC_password_block">
<label>Password:</label>
<input id="noVNC_password_input" type="password">
</li>
<li>
<input id="noVNC_credentials_button" type="submit" value="Send Credentials" class="noVNC_submit">
</li>
</ul>
<div class="noVNC_heading">
Credentials
</div>
<div id="noVNC_username_block">
<label for="noVNC_username_input">Username:</label>
<input id="noVNC_username_input">
</div>
<div id="noVNC_password_block">
<label for="noVNC_password_input">Password:</label>
<input id="noVNC_password_input" type="password">
</div>
<div>
<input id="noVNC_credentials_button" type="submit" value="Send Credentials" class="noVNC_submit">
</div>
</form></div>
</div>
@@ -318,7 +356,8 @@
html attributes which attempt to disable text suggestions on the
on-screen keyboard. Let's hope Chrome implements the ime-mode
style for example -->
<textarea id="noVNC_keyboardinput" autocapitalize="off" autocomplete="off" spellcheck="false" tabindex="-1"></textarea>
<textarea id="noVNC_keyboardinput" autocapitalize="off"
autocomplete="off" spellcheck="false" tabindex="-1"></textarea>
</div>
<audio id="noVNC_bell">

View File

@@ -23,7 +23,7 @@ function installPlugin(file) {
<div class="notice">_(Click **Install** to download and install the **Community Applications** plugin)_</div>
<form markdown="1" name="ca_install" method="POST" target="progressFrame">
<input type="hidden" name="file" value="https://raw.githubusercontent.com/Squidly271/community.applications/master/plugins/community.applications.plg">
<input type="hidden" name="file" value="https://ca.unraid.net/dl/https://raw.githubusercontent.com/unraid/community.applications/master/plugins/community.applications.plg">
&nbsp;
: <input type="button" value="_(Install)_" onclick="installPlugin(this.form.file.value)">

View File

@@ -193,7 +193,7 @@ _(Name)_:
<?=mk_option("","logs",_("logs - Separate Intent Log (SLOG)"))?>
<?=mk_option("","dedup",_("dedup - Deduplication Tables"))?>
<?=mk_option("","cache",_("cache - L2ARC"))?>
<?=mk_option("","spares",_("spares - Hot Spares"))?>
<?=mk_option("","spares",_("spares - Hot Spares"),'disabled')?>
</select>
_(Slots)_:

View File

@@ -3,7 +3,7 @@ Title="Credits"
Icon="icon-credits"
Tag="trophy"
---
**Unraid webGUI** Copyright &copy; 2005-2023, [Lime Technology, Inc.](http://lime-technology.com)
**Unraid webGUI** Copyright &copy; 2005-2023, [Lime Technology, Inc.](https://unraid.net/)
**Dynamix** Copyright &copy; 2012-2023, Bergware International.
@@ -29,7 +29,7 @@ and may not be used in any other project without written permission from Lime Te
* Settings, Tools and Case icons. Copyright &copy; 2018-2020, [Magnus Engø.](http://www.magnusengo.net/) Used with permission.
**Unraid**&reg; is a registered trademark of [Lime Technology, Inc.](http://lime-technology.com)
**Unraid**&reg; is a registered trademark of [Lime Technology, Inc.](https://unraid.net/)
This file shall be included in all copies or substantial portions of the Software.
@@ -47,6 +47,6 @@ foreach (glob("$plugins/lang-*.xml", GLOB_NOSORT) as $link) {
$author = language('Author', $xml_file);
$credits[] = "<li><p><i>$lang ($local)</i> translation by $author</p></li>";
}
if (count($credits)) echo '<br><b>Language Translations</b> Copyright &copy; 2020-2023, <a href="http://lime-technology.com">Lime Technology, Inc.</a><br><ul>'.implode('',$credits).'</ul>';
if (count($credits)) echo '<br><b>Language Translations</b> Copyright &copy; 2020-2023, <a href="https://unraid.net/">Lime Technology, Inc.</a><br><ul>'.implode('',$credits).'</ul>';
?>
<br><input type="button" value="_(Done)_" onclick="done()">

View File

@@ -2,7 +2,8 @@ Menu="Dashboard"
Nchan="wg_poller,update_1,update_2,update_3,ups_status:stop,vm_dashusage"
---
<?PHP
/* Copyright 2005-2023, Lime Technology * Copyright 2012-2023, Bergware International.
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
@@ -69,7 +70,7 @@ $cache_type = $cache_rate = [];
$parity = _var($var,'mdResync');
$mover = file_exists('/var/run/mover.pid');
$btrfs = exec('pgrep -cf /sbin/btrfs');
$btrfs = exec('pgrep --ns $$ -cf /sbin/btrfs');
$vdisk = exec("grep -Pom1 '^DOCKER_IMAGE_TYPE=\"\\K[^\"]+' /boot/config/docker.cfg 2>/dev/null")!='folder' ? _('Docker vdisk') : _('Docker folder');
$dot = _var($display,'number','.,')[0];
$zfs = count(array_filter(array_column($disks,'fsType'),function($fs){return str_replace('luks:','',$fs??'')=='zfs';}));
@@ -698,8 +699,8 @@ function hideShow() {
<tr><td>_(VM Name)_:</td><td><label id="VMName"></label></td></tr>
<tr><td>_(Snapshot Name)_:</td><td><input type="text" id="targetsnap" autocomplete="off" spellcheck="false" value="--generate" onclick="this.select()">_(Check free space)_:<input type="checkbox" id="targetsnapfspc" checked></td></tr>
<tr><td>_(Description)_:</td><td><input type="text" id="targetsnapdesc" autocomplete="off" spellcheck="false" value="" onclick="this.select()"></td></tr>
<tr id="memoryline"><td>_(Memory dump )_:</td><td><input type="checkbox" id="targetsnapmem" checked></td></tr>
<tr id="fstypeline"><td>_(FS Native Snapshot )_:</td><td><label id="fstype"></label><input type="checkbox" id="targetsnapfstype" >_(Unchecked will use QEMU External Snapshot)_</td></tr>
<tr id="memoryline"><td>_(Memory dump)_:</td><td><input type="checkbox" id="targetsnapmem" checked></td></tr>
<tr id="fstypeline"><td>_(FS Native Snapshot)_:</td><td><label id="fstype"></label><input type="checkbox" id="targetsnapfstype">_(Unchecked will use QEMU External Snapshot)_</td></tr>
</table>
</div>
@@ -1032,6 +1033,7 @@ function smartMenu(table) {
opts.push({text:"_(Acknowledge)_",icon:'fa-check-square-o',action:function(e){e.preventDefault();acknowledge(id,disk);}});
}
$(id).bind('click',function(){update2=false;}).bind('mouseout',function(){setTimeout(function(){update2=true;},15000);});
context.destroy(id);
context.attach(id,opts);
});
}
@@ -1539,7 +1541,7 @@ vmdashusage.on('message', function(msg){
var data = JSON.parse(msg);
for (const [vm, vmdata] of Object.entries(data)) {
for (const [displayitem, value] of Object.entries(vmdata)) {
$('#vmmetrics-'+displayitem + '-' + vm ).html(value);
$('#vmmetrics-'+displayitem + '-' + vm ).html(value);
}
}
});

View File

@@ -59,6 +59,14 @@ _(Use NTP)_:
:use_ntp_help:
_(NTP interval)_:
: <select name="display_ntppoll">
<?=mk_option(_var($display,'ntppoll'), "", _('Default'))?>
<?=mk_option(_var($display,'ntppoll'), "8", _('Slow'))?>
<?=mk_option(_var($display,'ntppoll'), "5", _('Medium'))?>
<?=mk_option(_var($display,'ntppoll'), "3", _('Fast'))?>
</select><span class="ntp orange-text">_(Use DEFAULT setting when public NTP servers are defined)_</span>
_(NTP server)_ 1:
: <input type="text" name="NTP_SERVER1" maxlength="40" class="narrow" value="<?=htmlspecialchars($var['NTP_SERVER1'])?>">
@@ -107,12 +115,14 @@ function presetTime(form) {
function checkDateTimeSettings(form) {
if (form.USE_NTP.value=="yes") {
form.newDateTime.disabled=true;
form.display_ntppoll.disabled=false;
form.NTP_SERVER1.disabled=false;
form.NTP_SERVER2.disabled=false;
form.NTP_SERVER3.disabled=false;
form.NTP_SERVER4.disabled=false;
} else {
form.newDateTime.disabled=false;
form.display_ntppoll.disabled=true;
form.NTP_SERVER1.disabled=true;
form.NTP_SERVER2.disabled=true;
form.NTP_SERVER3.disabled=true;

View File

@@ -17,7 +17,6 @@ Tag="hdd-o"
<?
require_once "$docroot/webGui/include/Preselect.php";
$subpool_name = isSubpool($name) ? isSubpool($name) : '';
$unassigned = array_key_exists($name,$devs);
$disk = $disks[$name] ?? $devs[$name] ?? [];
$dev = _var($disk,'device');
@@ -27,6 +26,22 @@ $days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Satu
$sheets = [];
$i = $n = 0;
function hasSubpools($name) {
global $disks, $subpools;
foreach ($subpools as $subpool) {
$index = "$name~$subpool";
if (isset($disks[$index])) return true;
}
return false;
}
if (!isSubpool($name)) {
$fsTypeImmutable = !(_var($var,'fsState')=='Stopped' && !hasSubpools($name) && (empty(_var($disk,'uuid')) || _var($disk,'slots',1)==1));
$fsProfileImmutable = $fsTypeImmutable;
} else {
$fsTypeImmutable = true;
$fsProfileImmutable = !(_var($var,'fsState')=='Stopped' && empty(_var($disk,'fsGroups','1')));
}
foreach ($disks as $sheet) {
if (_var($sheet,'type')=="Flash" || _var($sheet,'color')=="grey-off" || empty($sheet['name'])) continue;
$sheets[] = $sheet['name'];
@@ -45,7 +60,6 @@ $prev = $i>0 ? $sheets[$i-1] : $sheets[$end];
$next = $i<$end ? $sheets[$i+1] : $sheets[0];
$textErase = isPool($name) ? _('This will ERASE content of ALL devices in the pool') : _('This will ERASE ALL device content');
$textDelete = _('This will unassign all devices from the pool but will NOT modify any device contents');
$fsTypeImmutable = !(_var($var,'fsState')=='Stopped' && (empty(_var($disk,'uuid')) || _var($disk,'slots',1)==1));
function disabled_if($condition) {
if ($condition !== false) echo ' disabled';
@@ -111,6 +125,14 @@ function isPool($name) {
global $pools;
return in_array($name,$pools);
}
/* Check to see if a pool has already been upgraded. */
function is_upgraded_ZFS_pool($pool_name) {
/* See if the pool is aready upgraded. */
$upgrade = trim(shell_exec("/usr/sbin/zpool status ".escapeshellarg($pool_name)." | /usr/bin/grep 'Enable all features using.'") ?? "");
return ($upgrade ? false : true);
}
?>
<link type="text/css" rel="stylesheet" href="<?autov("/webGui/styles/jquery.ui.css")?>">
<link type="text/css" rel="stylesheet" href="<?autov("/plugins/dynamix.docker.manager/styles/style-$theme.css")?>">
@@ -200,7 +222,7 @@ function prepareZFS(form) {
}
<?endif;?>
function selectDiskFsWidth(slots) {
function setDiskFsWidth(slots) {
$('#diskFsWidth').empty();
$('#diskFsWidth').append($('<option>', {value: slots, text:''}));
$('#diskFsWidth').val(slots);
@@ -210,9 +232,15 @@ function selectDiskFsProfileAuto() {
$('#diskFsProfile').empty();
$('#diskFsProfile').append($('<option>', {value: '', text:''}));
$('#diskFsProfile').val('');
selectDiskFsWidth('');
setDiskFsWidth('');
}
function selectDiskFsProfileBTRFS(slots,set_default) {
function selectDiskFsProfileXFS() {
$('#diskFsProfile').empty();
$('#diskFsProfile').append($('<option>', {value: '', text:''}));
$('#diskFsProfile').val('');
setDiskFsWidth(1);
}
function selectDiskFsProfileBTRFS(slots,init) {
$('#diskFsProfile').empty();
$('#diskFsProfile').append($('<option>', {value: 'single', text:_('single')}));
if (slots >= 2) $('#diskFsProfile').append($('<option>', {value: 'raid0', text:_('raid0')}));
@@ -222,15 +250,16 @@ function selectDiskFsProfileBTRFS(slots,set_default) {
if (slots >= 4) $('#diskFsProfile').append($('<option>', {value: 'raid10', text:_('raid10')}));
if (slots >= 3) $('#diskFsProfile').append($('<option>', {value: 'raid5', text:_('raid5')}));
if (slots >= 4) $('#diskFsProfile').append($('<option>', {value: 'raid6', text:_('raid6')}));
if (set_default) {
if (slots == 1) $('#diskFsProfile').val('');
if (slots > 1) $('#diskFsProfile').val('raid1');
} else {
if (init) {
$('#diskFsProfile').val("<?=_var($disk,'fsProfile')?>");
} else {
if (slots == 1) $('#diskFsProfile').val('');
if (slots >= 2) $('#diskFsProfile').val('raid1');
}
selectDiskFsWidth(slots);
setDiskFsWidth(slots);
}
function selectDiskFsWidthZFS(slots) {
function selectDiskFsWidthZFS(slots,init) {
var selected_width = init ? Number("<?=_var($disk,'fsWidth')?>") : 0;
$('#diskFsWidth').empty();
if ($('#diskFsProfile').val() == '') {
var label = (slots == 1) ? "device" : "devices";
@@ -238,16 +267,18 @@ function selectDiskFsWidthZFS(slots) {
value: 1,
text: _(sprintf('%s '+label,slots))
}));
if (selected_width == 0) selected_width = 1;
} else if ($('#diskFsProfile').val() == 'mirror') {
var width;
for (width=2; width<=Math.min(slots,4); width++) {
if ((slots % width) == 0) {
var groups = slots / width;
var label = (groups == 1) ? "group" : "groups";
var label = (groups == 1) ? "vdev" : "vdevs";
$('#diskFsWidth').append($('<option>', {
value: width,
text: _(sprintf('%s '+label+' of %s devices',groups,width)),
}));
if (selected_width == 0) selected_width = width;
}
}
} else {
@@ -258,20 +289,22 @@ function selectDiskFsWidthZFS(slots) {
for (width=min_width; width<=slots; width++) {
if ((slots % width) == 0) {
var groups = slots / width;
var label = (groups == 1) ? "group" : "groups";
var label = (groups == 1) ? "vdev" : "vdevs";
$('#diskFsWidth').append($('<option>', {
value: width,
text: _(sprintf('%s '+label+' of %s devices',groups,width)),
}));
if (selected_width == 0) selected_width = width;
}
}
}
$('#diskFsWidth').val(selected_width);
}
function selectDiskFsProfileZFS(slots,set_default,subpool) {
function selectDiskFsProfileZFS(slots,init,subpool) {
$('#diskFsProfile').empty();
if (slots == 1) $('#diskFsProfile').append($('<option>', {value: '', text: _('single')}));
if (slots >= 2) $('#diskFsProfile').append($('<option>', {value: '', text: _('stripe')}));
if (subpool != 'cache' && subpool != 'spare') {
if (subpool != 'cache' && subpool != 'spares') {
if (slots%2 == 0 || slots%3 == 0 || slots%4 == 0) $('#diskFsProfile').append($('<option>', {value: 'mirror', text: _('mirror')}));
if (subpool == '') {
if (slots >= 3 && subpool == '') $('#diskFsProfile').append($('<option>', {value: 'raidz1', text: _('raidz1')}));
@@ -279,47 +312,33 @@ function selectDiskFsProfileZFS(slots,set_default,subpool) {
if (slots >= 4 && subpool == '') $('#diskFsProfile').append($('<option>', {value: 'raidz3', text: _('raidz3')}));
}
}
if (set_default) {
if (init) {
$('#diskFsProfile').val("<?=_var($disk,'fsProfile')?>");
} else {
if (slots == 1) $('#diskFsProfile').val('');
if (slots == 2) $('#diskFsProfile').val('mirror');
if (slots > 2) $('#diskFsProfile').val('raidz1');
selectDiskFsWidthZFS(slots);
$('#diskFsWidth').val(slots);
} else {
$('#diskFsProfile').val("<?=_var($disk,'fsProfile')?>");
selectDiskFsWidthZFS(slots);
$('#diskFsWidth').val(<?=_var($disk,'fsWidth')?>);
if (slots >= 3) $('#diskFsProfile').val('raidz1');
}
selectDiskFsWidthZFS(slots,init);
$('#diskFsProfile').on('change', function() {
selectDiskFsWidthZFS(slots);
selectDiskFsWidthZFS(slots,false);
});
}
function selectDiskFsProfileXFS() {
$('#diskFsProfile').empty();
$('#diskFsProfile').append($('<option>', {value: '', text:''}));
$('#diskFsProfile').val('');
selectDiskFsWidth(1);
}
/* called upon page load (init==true) and when user changes file system type (init==false) */
function selectDiskFsProfile(init) {
var t = init ? null : 'slow';
/* for array disks, 'slots', 'fsWidth', and 'fsGroups' is not defined so assume value 1 for all three */
<?if ($fsTypeImmutable):?>
var slots = <?=_var($disk,'fsWidth',1) * _var($disk,'fsGroups',1)?>;
<?else:?>
var slots = <?=_var($disk,'slots',1)?>;
<?endif;?>
/* for array disks, 'slots', 'fsWidth', and 'fsGroups' is not defined */
var slots = Number("<?=_var($disk,'fsWidth',1)?>") * Number("<?=_var($disk,'fsGroups',1)?>");
if (slots == 0) slots = <?=_var($disk,'slots',1)?>;
var subpool = "<?=$subpool_name?>";
var subpool = "<?=isSubpool($name) ?: ''?>";
var fsType;
var set_default;
if (subpool == '') {
fsType = init ? "<?=_var($disk,'fsType','')?>" : $('#diskFsType').val();
set_default = fsType != "<?=_var($disk,'fsType','')?>";
} else {
fsType = 'zfs';
set_default = false;
}
if (slots == 1 || fsType == 'auto') {
@@ -327,8 +346,14 @@ function selectDiskFsProfile(init) {
} else {
$('#profile').show(t);
if (fsType.indexOf('zfs') != -1) {
if (subpool != 'cache' && subpool != 'spares') {
$('#diskFsProfile').show();
} else {
$('#diskFsProfile').hide();
}
$('#diskFsWidth').show();
} else {
$('#diskFsProfile').show();
$('#diskFsWidth').hide()
}
}
@@ -336,9 +361,9 @@ function selectDiskFsProfile(init) {
if (fsType == 'auto') {
selectDiskFsProfileAuto();
} else if (fsType.indexOf('btrfs') != -1) {
selectDiskFsProfileBTRFS(slots,set_default);
selectDiskFsProfileBTRFS(slots,init);
} else if (fsType.indexOf('zfs') != -1) {
selectDiskFsProfileZFS(slots,set_default,subpool);
selectDiskFsProfileZFS(slots,init,subpool);
} else if (fsType.indexOf('xfs') != -1) {
selectDiskFsProfileXFS();
}
@@ -603,7 +628,7 @@ function eraseDisk(name) {
text:"<?=$textErase?><p style='font-weight:bold;color:red;margin:8px 0'>_(Existing content is permanently lost)_</p>",
html:true,
type:'input',
inputPlaceholder:"<?=sprintf(_('To confirm your action type: %s'),$name)?>",
inputPlaceholder:"<?=sprintf(_('To confirm type: %s'),$name)?>",
showCancelButton:true,
closeOnConfirm:false,
confirmButtonText:"_(Proceed)_",
@@ -614,7 +639,7 @@ function eraseDisk(name) {
swal.close();
$('#doneButton').prop('disabled',true);
$('#eraseButton').prop('disabled',true);
$('#deleteButton').prop('disabled',true);
$('#removeButton').prop('disabled',true);
$('div.spinner.fixed').show();
$.post("/update.htm",{cmdWipefs:name},function(){
$('div.spinner.fixed').hide();
@@ -625,13 +650,41 @@ function eraseDisk(name) {
}
});
}
function deletePool(name) {
function removePool(name) {
swal({
title:"_(Delete pool)_?",
title:"_(Remove pool)_?",
text:"<?=$textDelete?>",
html:true,
type:'input',
inputPlaceholder:"<?=sprintf(_('To confirm your action type: %s'),$name)?>",
inputPlaceholder:"<?=sprintf(_('To confirm type: %s'),$name)?>",
showCancelButton:true,
closeOnConfirm:false,
confirmButtonText:"_(Proceed)_",
cancelButtonText:"_(Cancel)_"
},
function(confirm){
if (confirm == "<?=$name?>") {
swal.close();
$('#doneButton').prop('disabled',true);
$('#eraseButton').prop('disabled',true);
$('#removeButton').prop('disabled',true);
$('div.spinner.fixed').show();
$.post("/update.htm",{changeSlots:"apply",poolName:name,poolSlots:0},function(){
$('div.spinner.fixed').hide();
refresh();
});
} else {
if (confirm.length) swal({title:"_(Incorrect confirmation)_",text:"_(Please try again)_!",type:'error',html:true,confirmButtonText:"_(Ok)_"});
}
});
}
function upgradeZpool(name) {
swal({
title:"_(Upgrade ZFS Pool)_?",
text:"_(This operation cannot be reversed)_. _(After upgrading the volume may not be mountable in previous versions of Unraid)_.<p style='font-weight:bold;color:red;margin:8px 0'>_(The ZFS volume will be upgraded)_</p>",
html:true,
type:'input',
inputPlaceholder:"<?=sprintf(_('To confirm type: %s'),$name)?>",
showCancelButton:true,
closeOnConfirm:false,
confirmButtonText:"_(Proceed)_",
@@ -644,7 +697,7 @@ function deletePool(name) {
$('#eraseButton').prop('disabled',true);
$('#deleteButton').prop('disabled',true);
$('div.spinner.fixed').show();
$.post("/update.htm",{changeSlots:"apply",poolName:name,poolSlots:0},function(){
$.post("/webGui/include/zfs_upgrade.php",{name:name},function(){
$('div.spinner.fixed').hide();
refresh();
});
@@ -735,9 +788,9 @@ _(File system type)_:
<?if (diskType('Data') || isPool($tag)):?>
<div markdown="1" id="profile">
_(Allocation profile)_:
: <select id="diskFsProfile" name="diskFsProfile.<?=_var($disk,'idx')?>" <?=disabled_if($fsTypeImmutable)?>>
: <select id="diskFsProfile" name="diskFsProfile.<?=_var($disk,'idx')?>" <?=disabled_if($fsProfileImmutable)?>>
</select>
<select id="diskFsWidth" name="diskFsWidth.<?=_var($disk,'idx')?>" <?=disabled_if($fsTypeImmutable)?>>
<select id="diskFsWidth" name="diskFsWidth.<?=_var($disk,'idx')?>" <?=disabled_if($fsProfileImmutable)?>>
</select>
:info_profile_help:
@@ -792,18 +845,17 @@ _(Critical disk utilization threshold)_ (%):
&nbsp;
: <input type="submit" name="changeDisk" value="_(Apply)_" disabled><input type="button" id="doneButton" value="_(Done)_" onclick="done()">
<?$erasable=false?>
<?$removeable=false?>
<?if (diskType('Parity','Data')):?>
<?if (_var($var,'fsState')=="Stopped" && diskStatus('_NEW')): $erasable=true; endif;?>
<?if (_var($var,'fsState')=="Started" && _var($var,'startMode')!="Normal" && diskType('Data')): $erasable=true; endif;?>
<input type="button" id="eraseButton" value="_(Erase)_" onclick="eraseDisk('<?=$name?>')"<?=$erasable?'':' disabled'?>>
<?endif;?>
<?if (isPool($name) && strpos($name,$_tilde_)===false):?>
<?if (isPool($name) && isSubpool($name)===false):?>
<?if (_var($var,'fsState')=="Stopped" || (_var($var,'fsState')=="Started" && _var($var,'startMode')!="Normal")): $erasable=true; endif;?>
<input type="button" id="eraseButton" value="_(Erase Pool)_" onclick="eraseDisk('<?=$name?>')"<?=$erasable?'':' disabled'?>>
<?endif;?>
<?if (isPool($name)):?>
<?$deleteable=_var($var,'fsState')=="Stopped" && !isSubpool($name)?>
<input type="button" id="deleteButton" value="_(Delete Pool)_" onclick="deletePool('<?=$name?>')"<?=$deleteable?'':' disabled'?>>
<?if (_var($var,'fsState')=="Stopped"): $removeable=true; endif;?>
<input type="button" id="removeButton" value="_(Remove Pool)_" onclick="removePool('<?=$name?>')"<?=$removeable?'':' disabled'?>>
<?endif;?>
</form>
@@ -1090,6 +1142,9 @@ _(zfs pool status)_:
&nbsp;
: <input type="submit" id="zfs-button" value="<?=$zfs_cmd=='start' ? _('Scrub') : _('Clear')?>">
<?if (! is_upgraded_ZFS_pool($name)):?>
<input type="button" id="upgradeButton" value="_(Upgrade Pool)_" onclick="upgradeZpool('<?=$name?>')">
<?endif;?>
:info_zfs_scrub_help:
@@ -1401,9 +1456,9 @@ _(SMART attribute notifications)_:
<form markdown="1" method="POST" action="/update.htm" target="progressFrame" onsubmit="return validate(this.poolName.value)">
<input type="hidden" name="poolNameOrig" value="<?=$name?>">
<input type="hidden" name="changeSlots" value="apply">
<p>_(Caution)_: _(Renaming the pool will change the share storage allocations)_. _(After renaming the pool, check that your shares are assigned to the proper primary and secondary storage locations)_.</p>
_(Name)_:
: <input type="text" name="poolName" maxlength="40" value="<?=$name?>">
<p>_(Caution)_: _(Renaming the pool will change the share storage allocations)_. _(After renaming the pool, check that your shares are assigned to the proper primary and secondary storage locations)_.</p>
</form>
</div>

View File

@@ -154,7 +154,7 @@ _(Enable spinup groups)_:
:disk_spinup_groups_help:
_(Default file system)_:
_(Default file system for Array disks)_:
: <select name="defaultFsType">
<?=mk_option($var['defaultFsType'], "xfs", _('xfs'));?>
<?=mk_option($var['defaultFsType'], "zfs", _('zfs'));?>

View File

@@ -60,8 +60,8 @@ foreach ($ports as $ethX) {
}
}
// enable interface only when VMs and Docker are stopped
$service = exec("pgrep libvirt") ? _('VM manager') : '';
$service .= exec("pgrep docker") ? ($service ? ' '._('and').' ' : '')._('Docker service') : '';
$service = exec('pgrep --ns $$ libvirt') ? _('VM manager') : '';
$service .= exec('pgrep --ns $$ docker') ? ($service ? ' '._('and').' ' : '')._('Docker service') : '';
// eth0 port status
$no_eth0 = exec("ip link show eth0|grep -Pom1 '(NO-CARRIER|state DOWN)'");

View File

@@ -3,8 +3,8 @@ Title="Flash Device Settings"
Tag="usb"
---
<?PHP
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
/* Copyright 2005-2024, Lime Technology
* Copyright 2012-2024, Bergware International.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
@@ -56,7 +56,7 @@ _(Flash GUID)_:
<?if (strstr($var['regTy'], "blacklisted")):?>
&nbsp;
: **_(Blacklisted)_** - <a href="http://lime-technology.com/contact" target="_blank">_(Contact Support)_</a>
: **_(Blacklisted)_** - <a href="https://unraid.net/contact" target="_blank">_(Contact Support)_</a>
<?else:?>

Some files were not shown because too many files have changed in this diff Show More