Merge branch 'master' into array_only_share_not_showing_properly_on_shares_list
@@ -44,7 +44,7 @@ $(function() {
|
||||
<tbody id="ups_summary"><tr class="ups"><td colspan="7"> </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">
|
||||
|
||||
@@ -116,6 +116,20 @@ function loadlist(init) {
|
||||
clearTimeout(timers.docker);
|
||||
var data = d.split(/\0/);
|
||||
$('#docker_list').html(data[0]);
|
||||
$('#docker_list .TS_tooltip').tooltipster({
|
||||
animation: 'fade',
|
||||
delay: 200,
|
||||
trigger: 'custom',
|
||||
triggerOpen: {
|
||||
mouseenter: true,
|
||||
click: true
|
||||
},
|
||||
triggerClose: {
|
||||
mouseleave: true,
|
||||
click: true
|
||||
},
|
||||
contentAsHTML: true
|
||||
});
|
||||
$('head').append('<script>'+data[1]+'<\/script>');
|
||||
<?if (_var($display,'resize')):?>
|
||||
resize();
|
||||
@@ -183,4 +197,3 @@ window.onunload = function(){
|
||||
dockerload.stop();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
After Width: | Height: | Size: 300 KiB |
@@ -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,163 @@ $authoring = $authoringMode ? 'advanced' : 'noshow';
|
||||
$disableEdit = $authoringMode ? 'false' : 'true';
|
||||
$showAdditionalInfo = '';
|
||||
$bgcolor = strstr('white,azure',$display['theme']) ? '#f2f2f2' : '#1c1c1c';
|
||||
|
||||
# Search for existing TAILSCALE_ entries in the Docker template
|
||||
$TS_existing_vars = false;
|
||||
if (isset($xml["Config"]) && is_array($xml["Config"])) {
|
||||
foreach ($xml["Config"] as $config) {
|
||||
if (isset($config["Target"]) && strpos($config["Target"], "TAILSCALE_") === 0) {
|
||||
$TS_existing_vars = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Look for Exit Nodes if Tailscale plugin is installed
|
||||
$ts_exit_nodes = [];
|
||||
$ts_en_check = false;
|
||||
if (file_exists('/usr/local/sbin/tailscale') && exec('pgrep --ns $$ -f "/usr/local/sbin/tailscaled"')) {
|
||||
exec('tailscale exit-node list', $ts_exit_node_list, $retval);
|
||||
if ($retval === 0) {
|
||||
foreach ($ts_exit_node_list as $line) {
|
||||
if (!empty(trim($line))) {
|
||||
if (preg_match('/^(\d+\.\d+\.\d+\.\d+)\s+(.+)$/', trim($line), $matches)) {
|
||||
$parts = preg_split('/\s+/', $matches[2]);
|
||||
$ts_exit_nodes[] = [
|
||||
'ip' => $matches[1],
|
||||
'hostname' => $parts[0],
|
||||
'country' => $parts[1],
|
||||
'city' => $parts[2],
|
||||
'status' => $parts[3]
|
||||
];
|
||||
$ts_en_check = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Try to detect port from WebUI and set webui_url
|
||||
$TSwebuiport = '';
|
||||
$webui_url = '';
|
||||
if (empty($xml['TailscalePort'])) {
|
||||
if (!empty($xml['WebUI'])) {
|
||||
$webui_url = parse_url($xml['WebUI']);
|
||||
preg_match('/:(\d+)\]/', $webui_url['host'], $matches);
|
||||
$TSwebuiport = $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
$TS_raw = [];
|
||||
$TS_container_raw = [];
|
||||
$TS_HostNameWarning = "";
|
||||
$TS_HTTPSDisabledWarning = "";
|
||||
$TS_ExitNodeNeedsApproval = false;
|
||||
$TS_MachinesLink = "https://login.tailscale.com/admin/machines/";
|
||||
$TS_DirectMachineLink = $TS_MachinesLink;
|
||||
$TS_HostNameActual = "";
|
||||
$TS_not_approved = "";
|
||||
$TS_https_enabled = false;
|
||||
// Get Tailscale information and create arrays/variables
|
||||
!empty($xml) && exec("docker exec -i " . escapeshellarg($xml['Name']) . " /bin/sh -c \"tailscale status --peers=false --json\"", $TS_raw);
|
||||
$TS_no_peers = json_decode(implode('', $TS_raw),true);
|
||||
$TS_container = json_decode(implode('', $TS_raw),true);
|
||||
$TS_container = $TS_container['Self']??'';
|
||||
if (!empty($TS_no_peers) && !empty($TS_container)) {
|
||||
// define the direct link to this machine on the Tailscale website
|
||||
if (!empty($TS_container['TailscaleIPs']) && !empty($TS_container['TailscaleIPs'][0])) {
|
||||
$TS_DirectMachineLink = $TS_MachinesLink.$TS_container['TailscaleIPs'][0];
|
||||
}
|
||||
// warn if MagicDNS or HTTPS is disabled
|
||||
if (isset($TS_no_peers['Self']['Capabilities']) && is_array($TS_no_peers['Self']['Capabilities'])) {
|
||||
$TS_https_enabled = in_array("https", $TS_no_peers['Self']['Capabilities'], true) ? true : false;
|
||||
}
|
||||
if (empty($TS_no_peers['CurrentTailnet']['MagicDNSEnabled']) || !$TS_no_peers['CurrentTailnet']['MagicDNSEnabled'] || $TS_https_enabled !== true) {
|
||||
$TS_HTTPSDisabledWarning = "<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 +630,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 +701,9 @@ function editConfigPopup(num,disabled) {
|
||||
}
|
||||
|
||||
Opts.Number = num;
|
||||
if (Opts.Type == "Device") {
|
||||
Opts.Target = Opts.Value;
|
||||
}
|
||||
newConf = makeConfig(Opts);
|
||||
if (config.hasClass("config_"+Opts.Display)) {
|
||||
config.html(newConf);
|
||||
@@ -666,6 +879,18 @@ $(function() {
|
||||
});
|
||||
});
|
||||
</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 +931,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 +1083,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 +1107,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" 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 +1519,230 @@ 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();
|
||||
} 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 +1838,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 +1870,4 @@ if (window.location.href.indexOf("/Apps/") > 0 && <? if (is_file($xmlTemplate))
|
||||
}
|
||||
</script>
|
||||
<?END:?>
|
||||
|
||||
|
||||
@@ -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)?:'/???');
|
||||
|
||||
@@ -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'>❌<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 . " ➔ 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'>✅</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 ➔ " . $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'>✅</span></div>";
|
||||
} else {
|
||||
if (!empty($TSstats["ExitNodeStatus"])) {
|
||||
$TS_exit_node_status = ($TSstats["ExitNodeStatus"]["Online"]) ? "✅" : "❌";
|
||||
$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'>❌</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'>❌ 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='display: inline-block;' title='" . 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;' title='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='display: inline-block;' title='Container not runnig'><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);
|
||||
?>
|
||||
|
||||
|
||||
@@ -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->TailscaleStateDir = xml_encode($post['TSstatedir']);
|
||||
$xml->TailscaleRoutes = xml_encode($post['TSroutes']);;
|
||||
$xml->TailscaleAcceptRoutes = xml_encode($post['TSacceptroutes']);;
|
||||
if (isset($post['TStroubleshooting']) && strtolower($post['TStroubleshooting']) === 'on') {
|
||||
$xml->TailscaleTroubleshooting = 'true';
|
||||
}
|
||||
}
|
||||
$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 = [];
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;}
|
||||
|
||||
@@ -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;}
|
||||
|
||||
@@ -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;}
|
||||
|
||||
@@ -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;}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ;
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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']}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"] ;
|
||||
}
|
||||
|
||||
|
||||
@@ -80,8 +80,8 @@ while (true) {
|
||||
$echodata .= my_scale($vmdata['mem']*1024,$unit)."$unit / ".my_scale($vmdata['curmem']*1024,$unit)."$unit";
|
||||
if ($vmdata['curmem'] === $vmdata['maxmem']) $echodata .= " </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>";
|
||||
$echodata .= _("Read").": ".my_scale($vmdata['rdrate']/$timer,$unit)."$unit/s<br>"._("Write").": ".my_scale($vmdata['wrrate']/$timer,$unit)."$unit/s</td><td>";
|
||||
$echodata .= _("RX").": ".my_scale($vmdata['rxrate']/$timer,$unit)."$unit/s<br>"._("TX").": ".my_scale($vmdata['txrate']/$timer,$unit)."$unit/s</td></tr>";
|
||||
}
|
||||
$echo = $echodata ;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
@@ -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 |
|
After Width: | Height: | Size: 303 KiB |
@@ -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": "Ακύρωση"
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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": "キャンセル"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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": "Выход"
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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。"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 + "]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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/...');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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/Squidly271/community.applications/master/plugins/community.applications.plg">
|
||||
|
||||
|
||||
: <input type="button" value="_(Install)_" onclick="installPlugin(this.form.file.value)">
|
||||
|
||||
@@ -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)_:
|
||||
|
||||
@@ -3,7 +3,7 @@ Title="Credits"
|
||||
Icon="icon-credits"
|
||||
Tag="trophy"
|
||||
---
|
||||
**Unraid webGUI** Copyright © 2005-2023, [Lime Technology, Inc.](http://lime-technology.com)
|
||||
**Unraid webGUI** Copyright © 2005-2023, [Lime Technology, Inc.](https://unraid.net/)
|
||||
|
||||
**Dynamix** Copyright © 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 © 2018-2020, [Magnus Engø.](http://www.magnusengo.net/) Used with permission.
|
||||
|
||||
**Unraid**® is a registered trademark of [Lime Technology, Inc.](http://lime-technology.com)
|
||||
**Unraid**® 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 © 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 © 2020-2023, <a href="https://unraid.net/">Lime Technology, Inc.</a><br><ul>'.implode('',$credits).'</ul>';
|
||||
?>
|
||||
<br><input type="button" value="_(Done)_" onclick="done()">
|
||||
|
||||
@@ -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';}));
|
||||
@@ -1539,7 +1540,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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)_ (%):
|
||||
|
||||
: <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)_:
|
||||
|
||||
|
||||
: <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>
|
||||
|
||||
@@ -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'));?>
|
||||
|
||||
@@ -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)'");
|
||||
|
||||
@@ -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")):?>
|
||||
|
||||
|
||||
: **_(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:?>
|
||||
|
||||
|
||||
@@ -117,51 +117,122 @@ if ($cert2Present) {
|
||||
}
|
||||
}
|
||||
|
||||
// Tailscale LE cert
|
||||
$cert3File = "/boot/config/ssl/certs/ts_bundle.pem";
|
||||
$cert3Present = file_exists("$cert3File");
|
||||
if ($cert3Present) {
|
||||
$cert3Subject = exec("/usr/bin/openssl x509 -in $cert3File -noout -subject -nameopt multiline 2>/dev/null|sed -n 's/ *commonName *= //p'");
|
||||
$cert3Issuer = exec("/usr/bin/openssl x509 -in $cert3File -noout -text | sed -n -e 's/^.*Issuer: //p'");
|
||||
$cert3Expires = exec("/usr/bin/openssl x509 -in $cert3File -noout -text | sed -n -e 's/^.*Not After : //p'");
|
||||
}
|
||||
|
||||
// Note: this disables FQDN6 urls since they are not supported by myunraid.net DNS currently
|
||||
if (!empty($nginx['NGINX_LANFQDN6'])) unset($nginx['NGINX_LANFQDN6']);
|
||||
|
||||
$http_port = _var($var,'PORT','80') != '80' ? ":{$var['PORT']}" : '';
|
||||
$https_port = _var($var,'PORTSSL','443') != '443' ? ":{$var['PORTSSL']}" : '';
|
||||
$http_ip_url = "http://"._var($nginx,'NGINX_LANIP')."{$http_port}/";
|
||||
$https_ip_url = "https://"._var($nginx,'NGINX_LANIP')."{$https_port}/";
|
||||
$http_ip6_url = "http://["._var($nginx,'NGINX_LANIP6')."]{$http_port}/";
|
||||
$https_ip6_url = "https://["._var($nginx,'NGINX_LANIP6')."]{$https_port}/";
|
||||
$http_mdns_url = "http://"._var($nginx,'NGINX_LANMDNS')."{$http_port}/";
|
||||
$https_mdns_url = "https://"._var($nginx,'NGINX_LANMDNS')."{$https_port}/";
|
||||
$https_fqdn_url = "https://"._var($nginx,'NGINX_LANFQDN')."{$https_port}/";
|
||||
$https_fqdn6_url = "https://"._var($nginx,'NGINX_LANFQDN6')."{$https_port}/";
|
||||
$http_ip_url = 'http://'._var($nginx,'NGINX_LANIP').$http_port.'/';
|
||||
$https_ip_url = 'https://'._var($nginx,'NGINX_LANIP').$https_port.'/';
|
||||
// bare IPv6 addresses need to be surrounded in brackets
|
||||
$http_ip6_url = 'http://'.'['._var($nginx,'NGINX_LANIP6').']'.$http_port.'/';
|
||||
$https_ip6_url = 'https://'.'['._var($nginx,'NGINX_LANIP6').']'.$https_port.'/';
|
||||
$http_mdns_url = 'http://'._var($nginx,'NGINX_LANMDNS').$http_port.'/';
|
||||
$https_mdns_url = 'https://'._var($nginx,'NGINX_LANMDNS').$https_port.'/';
|
||||
$https_fqdn_url = 'https://'._var($nginx,'NGINX_LANFQDN').$https_port.'/';
|
||||
$https_fqdn6_url = 'https://'._var($nginx,'NGINX_LANFQDN6').$https_port.'/';
|
||||
|
||||
$urls = [];
|
||||
// push an array of four values into the $urls array:
|
||||
// 0 - the url
|
||||
// 1 - the url it redirects to, or null
|
||||
// 2 - the certificate file used, or null
|
||||
// 3 - self-signed certificate, or false
|
||||
// push an array of five values into the $urls array:
|
||||
// 0 - type of url ['LAN','WAN','WG','TAILSCALE']
|
||||
// 1 - the url
|
||||
// 3 - the url it redirects to, or null
|
||||
// 4 - the certificate file used, or null
|
||||
// 5 - self-signed certificate, or false
|
||||
|
||||
// define LAN access urls and redirects that change based on USE_SSL setting
|
||||
switch(_var($var,'USE_SSL','no')) {
|
||||
case 'no':
|
||||
if (!empty($nginx['NGINX_LANIP'])) $urls[] = [$http_ip_url, null, null, false];
|
||||
if (!empty($nginx['NGINX_LANIP6'])) $urls[] = [$http_ip6_url, null, null, false];
|
||||
if (!empty($nginx['NGINX_LANMDNS'])) $urls[] = [$http_mdns_url, null, null, false];
|
||||
if (!empty($nginx['NGINX_LANFQDN'])) $urls[] = [$https_fqdn_url, null, "certificate_bundle.pem", false];
|
||||
if (!empty($nginx['NGINX_LANFQDN6'])) $urls[] = [$https_fqdn6_url, null, "certificate_bundle.pem", false];
|
||||
if (!empty($nginx['NGINX_LANIP'])) $urls[] = ['LAN', $http_ip_url, null, null, false];
|
||||
if (!empty($nginx['NGINX_LANIP6'])) $urls[] = ['LAN', $http_ip6_url, null, null, false];
|
||||
if (!empty($nginx['NGINX_LANMDNS'])) $urls[] = ['LAN', $http_mdns_url, null, null, false];
|
||||
break;
|
||||
case 'yes':
|
||||
if (!empty($nginx['NGINX_LANIP'])) $urls[] = [$http_ip_url, $https_ip_url, null, false];
|
||||
if (!empty($nginx['NGINX_LANIP'])) $urls[] = [$https_ip_url, null, "{$var['NAME']}_unraid_bundle.pem", $cert1SelfSigned];
|
||||
if (!empty($nginx['NGINX_LANIP6'])) $urls[] = [$http_ip6_url, $https_ip6_url, null, false];
|
||||
if (!empty($nginx['NGINX_LANIP6'])) $urls[] = [$https_ip6_url, null, "{$var['NAME']}_unraid_bundle.pem", $cert1SelfSigned];
|
||||
if (!empty($nginx['NGINX_LANMDNS'])) $urls[] = [$http_mdns_url, $https_mdns_url, null, false];
|
||||
if (!empty($nginx['NGINX_LANMDNS'])) $urls[] = [$https_mdns_url, null, "{$var['NAME']}_unraid_bundle.pem", $cert1SelfSigned];
|
||||
if (!empty($nginx['NGINX_LANFQDN'])) $urls[] = [$https_fqdn_url, null, "certificate_bundle.pem", false];
|
||||
if (!empty($nginx['NGINX_LANFQDN6'])) $urls[] = [$https_fqdn6_url, null, "certificate_bundle.pem", false];
|
||||
if (!empty($nginx['NGINX_LANIP'])) $urls[] = ['LAN', $http_ip_url, $https_ip_url, null, false];
|
||||
if (!empty($nginx['NGINX_LANIP'])) $urls[] = ['LAN', $https_ip_url, null, "{$var['NAME']}_unraid_bundle.pem", $cert1SelfSigned];
|
||||
if (!empty($nginx['NGINX_LANIP6'])) $urls[] = ['LAN', $http_ip6_url, $https_ip6_url, null, false];
|
||||
if (!empty($nginx['NGINX_LANIP6'])) $urls[] = ['LAN', $https_ip6_url, null, "{$var['NAME']}_unraid_bundle.pem", $cert1SelfSigned];
|
||||
if (!empty($nginx['NGINX_LANMDNS'])) $urls[] = ['LAN', $http_mdns_url, $https_mdns_url, null, false];
|
||||
if (!empty($nginx['NGINX_LANMDNS'])) $urls[] = ['LAN', $https_mdns_url, null, "{$var['NAME']}_unraid_bundle.pem", $cert1SelfSigned];
|
||||
break;
|
||||
case 'auto': // aka strict
|
||||
if (!empty($nginx['NGINX_LANIP'])) $urls[] = [$http_ip_url, $https_fqdn_url, null, false];
|
||||
if (!empty($nginx['NGINX_LANIP6'])) $urls[] = [$http_ip6_url, $https_fqdn6_url, null, false];
|
||||
if (!empty($nginx['NGINX_LANMDNS'])) $urls[] = [$http_mdns_url, $https_fqdn_url, null, false];
|
||||
if (!empty($nginx['NGINX_LANFQDN'])) $urls[] = [$https_fqdn_url, null, "certificate_bundle.pem", false];
|
||||
if (!empty($nginx['NGINX_LANFQDN6'])) $urls[] = [$https_fqdn6_url, null, "certificate_bundle.pem", false];
|
||||
if (!empty($nginx['NGINX_LANIP']) && !empty($nginx['NGINX_LANFQDN'])) $urls[] = ['LAN', $http_ip_url, $https_fqdn_url, null, false];
|
||||
if (!empty($nginx['NGINX_LANIP6']) && !empty($nginx['NGINX_LANFQDN6'])) $urls[] = ['LAN', $http_ip6_url, $https_fqdn6_url, null, false];
|
||||
if (!empty($nginx['NGINX_LANMDNS']) && !empty($nginx['NGINX_LANFQDN'])) $urls[] = ['LAN', $http_mdns_url, $https_fqdn_url, null, false];
|
||||
break;
|
||||
}
|
||||
|
||||
// define FQDN urls for each interface
|
||||
// when multiple FQDN urls are available for a given interface, make sure they are sorted
|
||||
asort($nginx);
|
||||
foreach ($nginx as $key => $host) {
|
||||
if (!$host) continue;
|
||||
// Only process keys that include 'FQDN'
|
||||
if (strpos($key, 'FQDN') === false) continue;
|
||||
// Extract the interface from the key, e.g., 'NGINX_LANFQDN' -> 'LAN', 'NGINX_WANFQDN' -> 'WAN', NGINX_WG0FQDN -> WG, NGINX_TAILSCALE1FQDN -> TAILSCALE
|
||||
// Note: this specifically excludes FQDN6 urls since they are not supported by myunraid.net DNS currently
|
||||
if (preg_match('/^NGINX_([A-Z]+)(\d*)FQDN$/', $key, $matches)) {
|
||||
$interface = $matches[1]; // Interface type (LAN, WAN, WG, TAILSCALE, etc.)
|
||||
// ignore the WAN interface because we don't have access to the WANPORT here
|
||||
if ($interface == "WAN") continue;
|
||||
$pem = null;
|
||||
if (str_ends_with($host, '.myunraid.net')) $pem = 'certificate_bundle.pem';
|
||||
elseif (str_ends_with($host, '.ts.net')) $pem = 'ts_bundle.pem';
|
||||
$url = 'https://'.$host.$https_port."/";
|
||||
$urls[] = [$interface, $url, null, $pem, false];
|
||||
}
|
||||
}
|
||||
|
||||
// determine whether there are urls for a given interface
|
||||
function has_urls($interface) {
|
||||
global $urls;
|
||||
foreach($urls as $url) {
|
||||
if ($url[0] == $interface) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// show all urls for a given interface
|
||||
function show_urls($interface) {
|
||||
global $urls;
|
||||
// 0 - type of url ['LAN','WAN','WG','TAILSCALE']
|
||||
// 1 - the url
|
||||
// 3 - the url it redirects to, or null
|
||||
// 4 - the certificate file used, or null
|
||||
// 5 - self-signed certificate, or false
|
||||
$output = "";
|
||||
$linestart = "<dt> </dt><dd>";
|
||||
$lineend = "</dd>\n";
|
||||
$first = true;
|
||||
foreach($urls as $url) {
|
||||
if ($url[0] == $interface) {
|
||||
$msg = "<a class='localURL' href='$url[1]'>$url[1]</a>";
|
||||
if ($url[2]) $msg .= " "._("redirects to")." <a class='localURL' href='$url[2]'>$url[2]</a>";
|
||||
if ($url[3]) $msg .= " "._("uses")." ".$url[3];
|
||||
if ($url[4]) $msg .= "<span class='warning'> <i class='fa fa-warning fa-fw'></i> "._("is a self-signed certificate, ignore the browser's warning and proceed to the GUI")."</span>";
|
||||
// 2nd+ urls need leading $linestart
|
||||
$output .= ($first ? "" : $linestart).$msg.$lineend;
|
||||
$first = false;
|
||||
}
|
||||
}
|
||||
if ($first) {
|
||||
$output = "none";
|
||||
} else {
|
||||
// strip final trailing $lineend as it will be added by markdown
|
||||
$output = substr($output, 0, strlen($lineend)*-1);
|
||||
}
|
||||
echo $output;
|
||||
}
|
||||
|
||||
$cert_time_format = $display['date'].($display['date']!='%c' ? ', '.str_replace(['%M','%R'],['%M:%S','%R:%S'],$display['time']):'');
|
||||
$provisionlabel = $isWildcardCert ? _('Renew') : _('Provision');
|
||||
$disabled_provision = $keyfile===false || ($isWildcardCert && $retval_expired===0) || !$addr ? 'disabled' : '';
|
||||
@@ -334,30 +405,40 @@ _(Local TLD)_:
|
||||
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
_(Local access URLs)_:
|
||||
: <?
|
||||
// url[0] = url
|
||||
// url[1] = redirect url or null
|
||||
// url[2] = certificate used or null
|
||||
// url[3] = is certificate self-signed T/F
|
||||
$n = 0;
|
||||
foreach($urls as $url) {
|
||||
$msg = "";
|
||||
if ($url[1]) $msg .= " "._("redirects to")." <a href='$url[1]'>$url[1]</a>";
|
||||
if ($url[2]) $msg .= " "._("uses")." ".$url[2];
|
||||
if ($url[3]) $msg .= "<span class='warning'> <i class='fa fa-warning fa-fw'></i> "._("is a self-signed certificate, ignore the browser's warning and proceed to the GUI")."</span>";
|
||||
echo ($n ? "<dt> </dt><dd>" : ""),"<a href='$url[0]'>$url[0]</a>$msg",($n++ ? "</dd>" : "");
|
||||
}?>
|
||||
: <? show_urls('LAN'); ?>
|
||||
|
||||
:mgmt_local_access_urls_help:
|
||||
|
||||
<?if (has_urls('WG')): ?>
|
||||
|
||||
_(WireGuard URLs)_:
|
||||
: <? show_urls('WG'); ?>
|
||||
|
||||
:mgmt_wg_access_urls_help:
|
||||
|
||||
<?endif;?>
|
||||
|
||||
<?if (has_urls('TAILSCALE')): ?>
|
||||
|
||||
_(Tailscale URLs)_:
|
||||
: <? show_urls('TAILSCALE'); ?>
|
||||
|
||||
:mgmt_tailscale_access_urls_help:
|
||||
|
||||
<?endif;?>
|
||||
|
||||
<?if ($cert1Present):?>
|
||||
<hr>
|
||||
|
||||
_(Self-signed or user-provided certificate)_:
|
||||
: <?=$cert1File?>
|
||||
|
||||
<?if ($cert1URLvalid && _var($var,'USE_SSL')=='yes'):?>
|
||||
_(Certificate URL)_:
|
||||
: <?="<a href='https://$cert1URL$https_port'>$cert1URL</a>"?>
|
||||
: <?="<a class='localURL' href='https://$cert1URL$https_port'>$cert1URL</a>"?>
|
||||
|
||||
<?elseif ($cert1URLvalid):?>
|
||||
_(Certificate URL)_:
|
||||
@@ -386,11 +467,14 @@ _(Self-signed certificate file)_:
|
||||
<input type="hidden" name="server_name" value="<?=strtok(_var($_SERVER,'HTTP_HOST'),":")?>">
|
||||
<input type="hidden" name="server_addr" value="<?=_var($_SERVER,'SERVER_ADDR')?>">
|
||||
<?if ($cert2Present):?>
|
||||
|
||||
<hr>
|
||||
|
||||
_(Unraid Let's Encrypt certificate)_:
|
||||
: <?=$cert2File?>
|
||||
|
||||
_(Certificate URL)_:
|
||||
: <?="<a href='https://$subject2URL$https_port'>$cert2Subject</a>"?>
|
||||
: <?="<a class='localURL' href='https://$subject2URL$https_port'>$cert2Subject</a>"?>
|
||||
|
||||
_(Certificate issuer)_:
|
||||
: <?=$cert2Issuer?>
|
||||
@@ -415,6 +499,31 @@ _(CA-signed certificate file)_:
|
||||
|
||||
: <button type="submit" name="changePorts" value="Provision" <?=$disabled_provision?>><?=$provisionlabel?></button><button type="submit" name="changePorts" value="Delete" <?=$disabled_delete?> >_(Delete)_</button><?=$disabled_provision_msg?>
|
||||
|
||||
|
||||
<?if ($cert3Present):?>
|
||||
|
||||
<hr>
|
||||
|
||||
_(Tailscale Let's Encrypt certificate)_:
|
||||
: <?=$cert3File?>
|
||||
|
||||
_(Certificate URL)_:
|
||||
: <?="<a class='localURL' href='https://$cert3Subject$https_port'>$cert3Subject</a>"?>
|
||||
|
||||
_(Certificate issuer)_:
|
||||
: <?=$cert3Issuer?>
|
||||
|
||||
_(Certificate expiration)_:
|
||||
: <?=_(my_date($cert_time_format, strtotime($cert3Expires)),0)?>
|
||||
|
||||
<?endif;?>
|
||||
|
||||
:mgmt_certificate_expiration_help:
|
||||
|
||||
</form>
|
||||
|
||||
<?if (has_urls('WG')): ?>
|
||||
|
||||
<small>"WireGuard" and the "WireGuard" logo are registered trademarks of Jason A. Donenfeld</small>
|
||||
|
||||
<?endif;?>
|
||||
|
||||
@@ -67,15 +67,12 @@ $proxy_3_url = $url_array['full_url'];
|
||||
_(Select Proxy)_:
|
||||
: <select name="proxy_active" style="width:20%;" size="1">
|
||||
<?=mk_option($cfg['proxy_active'], "0", "_(None)_");?>
|
||||
|
||||
<?if (($cfg['proxy_url_1']) && ($cfg['proxy_name_1'])):?>
|
||||
<?=mk_option($cfg['proxy_active'], "1", $cfg['proxy_name_1'], "disabled");?>
|
||||
<?endif;?>
|
||||
|
||||
<?if (($cfg['proxy_url_2']) && ($cfg['proxy_name_2'])):?>
|
||||
<?=mk_option($cfg['proxy_active'], "2", $cfg['proxy_name_2'], "disabled");?>
|
||||
<?endif;?>
|
||||
|
||||
<?if (($cfg['proxy_url_3']) && ($cfg['proxy_name_3'])):?>
|
||||
<?=mk_option($cfg['proxy_active'], "3", $cfg['proxy_name_3'], "disabled");?>
|
||||
<?endif;?>
|
||||
|
||||
@@ -77,7 +77,7 @@ _(WSD options [experimental])_:
|
||||
<script>
|
||||
function checkWSDSettings() {
|
||||
form=document.SMBEnable;
|
||||
if (form.USE_WSD.value=="yes") {
|
||||
if (form.USE_WSD.value=="yes" && <?=($var['fsState']=="Started")?> {
|
||||
form.WSD2_OPT.disabled=false;
|
||||
} else {
|
||||
form.WSD2_OPT.disabled=true;
|
||||
|
||||
@@ -100,11 +100,11 @@ _(Case-sensitive names)_:
|
||||
|
||||
<?endif;?>
|
||||
_(Security)_:
|
||||
: <select name="shareSecurity">
|
||||
: <select name="shareSecurity" onchange="checkPublicSelection(this);">
|
||||
<?=mk_option($sec[$name]['security'], "public", _('Public'))?>
|
||||
<?=mk_option($sec[$name]['security'], "secure", _('Secure'))?>
|
||||
<?=mk_option($sec[$name]['security'], "private", _('Private'))?>
|
||||
</select>
|
||||
</select><span id="warningMessage" style="display:none; color: red;">_(Warning)_: _(Windows may require a valid User to be defined even for Public shares)_. _(See Help)_.</span>
|
||||
|
||||
:smb_security_modes_help:
|
||||
|
||||
@@ -315,4 +315,25 @@ function writeUserSMB(data,n,i) {
|
||||
writeUserSMB(data,0,i);
|
||||
}
|
||||
}
|
||||
|
||||
function checkPublicSelection(select) {
|
||||
/* Get reference to the warning message span */
|
||||
let warningMessage = document.getElementById("warningMessage");
|
||||
|
||||
/* Check if 'Public' is selected */
|
||||
if (select.value === "public") {
|
||||
/* Display warning for 'Public' option */
|
||||
warningMessage.style.display = "inline";
|
||||
} else {
|
||||
/* Hide warning for other options */
|
||||
warningMessage.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
/* Call checkPublicSelection with the initial selection on page load */
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
let smbSecuritySelect = document.querySelector('[name="shareSecurity"]');
|
||||
checkPublicSelection(smbSecuritySelect);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -56,6 +56,11 @@ if ((! $share['cachePool']) && ($share['cachePool2'])) {
|
||||
$share['cachePool2'] = "";
|
||||
}
|
||||
|
||||
/* If useCache is "no" with an array, this is invalid and useCache has to be 'only'. */
|
||||
if ((! $poolsOnly) && ($share['useCache'] == "no")) {
|
||||
$share['useCache'] = 'only';
|
||||
}
|
||||
|
||||
/* Check for non existent pool device. */
|
||||
if ($share['cachePool'] && !in_array($share['cachePool'], $pools)) {
|
||||
$poolDefined = false;
|
||||
@@ -541,14 +546,10 @@ _(Mover action)_:
|
||||
|
||||
: <input type="submit" name="cmdEditShare" value="_(Add Share)_" onclick="this.value='Add Share'"><input type="button" value="_(Done)_" onclick="done()">
|
||||
<?else:?>
|
||||
<div markdown="1" class="empty">
|
||||
_(Delete)_<input type="checkbox" name="confirmDelete" onchange="chkDelete(this.form, document.getElementById('cmdEditShare'));">
|
||||
<div markdown="1">
|
||||
<label id="deleteLabel" title="">_(Delete)_</label><input type="checkbox" name="confirmDelete" onchange="chkDelete(this.form, document.getElementById('cmdEditShare'));" title="" disabled>
|
||||
: <input type="submit" id="cmdEditShare" name="cmdEditShare" value="_(Apply)_" onclick="if (this.value=='_(Delete)_') this.value='Delete'; else this.value='Apply'; return handleDeleteClick(this)" disabled><input type="button" value="_(Done)_" onclick="done()">
|
||||
</div>
|
||||
<div markdown="1" class="full">
|
||||
|
||||
: <input type="submit" name="cmdEditShare" value="_(Apply)_" onclick="this.value='Apply'" disabled><input type="button" value="_(Done)_" onclick="done()">
|
||||
</div>
|
||||
<?endif;?>
|
||||
</form>
|
||||
|
||||
@@ -644,6 +645,7 @@ function updateScreen(cache, slow) {
|
||||
secondaryDropdown.options[i].disabled = true;
|
||||
}
|
||||
secondaryDropdown.selectedIndex = 0;
|
||||
checkRequiredSecondary = false;
|
||||
|
||||
if (poolsOnly) {
|
||||
$('#moverDirection2').hide();
|
||||
@@ -1297,13 +1299,24 @@ function handleDeleteClick(button) {
|
||||
|
||||
$(function() {
|
||||
<?if ($name):?>
|
||||
<?
|
||||
$tooltip_enabled = _('Share is empty and is safe to delete');
|
||||
$tooltip_disabled = _('Share must be empty to be deleted');
|
||||
?>
|
||||
|
||||
$.post('/webGui/include/ShareList.php', { scan: "<?=$name?>" }, function(e) {
|
||||
if (e == 1) {
|
||||
$('.empty').show();
|
||||
$('.full').hide();
|
||||
/* Enable delete checkbox and update tooltip. */
|
||||
$('input[name="confirmDelete"]').prop('disabled', false).attr('title', '<?= $tooltip_enabled ?>');
|
||||
$('#deleteLabel').attr('title', '<?= $tooltip_enabled ?>');
|
||||
} else {
|
||||
$('.full1').hide();
|
||||
$('.full2').show();
|
||||
/* Disable delete checkbox and update tooltip. */
|
||||
$('input[name="confirmDelete"]').prop('disabled', true).attr('title', '<?= $tooltip_disabled ?>');
|
||||
$('#deleteLabel').attr('title', '<?= $tooltip_disabled ?>');
|
||||
}
|
||||
});
|
||||
<?endif;?>
|
||||
|
||||
@@ -21,8 +21,11 @@ require_once "$docroot/plugins/dynamix.docker.manager/include/DockerClient.php";
|
||||
require_once "$docroot/plugins/dynamix.vm.manager/include/libvirt_helpers.php";
|
||||
|
||||
if (isset($_POST['ntp'])) {
|
||||
$ntp = exec("ntpq -pn|awk '{if (NR>3 && $2!=\".INIT.\") c++} END {print c}'");
|
||||
die($ntp ? sprintf(_('Clock synchronized with %s NTP server'.($ntp==1?'':'s')),$ntp) : _('Clock is unsynchronized with no NTP servers'));
|
||||
if (exec("pgrep -cf /usr/sbin/ntpd")) {
|
||||
$ntp = exec("ntpq -pn|awk '$1~/^\*/{print $9;exit}'");
|
||||
die($ntp ? sprintf(_('Clock is synchronized using NTP, time offset: %s ms'),abs($ntp)) : _('Clock is unsynchronized with no NTP servers'));
|
||||
}
|
||||
die(_('Clock is unsynchronized, free-running clock'));
|
||||
}
|
||||
|
||||
if ($_POST['docker']) {
|
||||
@@ -48,12 +51,13 @@ if ($_POST['docker']) {
|
||||
$template = $info['template'];
|
||||
$shell = $info['shell'];
|
||||
$webGui = html_entity_decode($info['url']);
|
||||
$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));
|
||||
$shape = $running ? ($paused ? 'pause' : 'play') : 'square';
|
||||
$status = $running ? ($paused ? 'paused' : 'started') : 'stopped';
|
||||
$color = $status=='started' ? 'green-text' : ($status=='paused' ? 'orange-text' : 'red-text');
|
||||
@@ -96,7 +100,8 @@ if ($_POST['vms']) {
|
||||
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=' . $_SERVER['HTTP_HOST'] ;
|
||||
if ($vmrcprotocol == "vnc") $vmrcscale = "&resize=scale"; else $vmrcscale = "";
|
||||
$vmrcurl = autov('/plugins/dynamix.vm.manager/'.$vmrcprotocol.'.html',true).$vmrcscale.'&autoconnect=true&host=' . $_SERVER['HTTP_HOST'] ;
|
||||
if ($vmrcprotocol == "spice") $vmrcurl .= '&vmname='. urlencode($vm) . '&port=/wsproxy/'.$vmrcport.'/' ; else $vmrcurl .= '&port=&path=/wsproxy/' . $wsport . '/';
|
||||
} elseif ($vmrcport == -1 || $autoport) {
|
||||
$vmrcprotocol = $lv->domain_get_vmrc_protocol($res) ;
|
||||
@@ -162,12 +167,10 @@ if ($_POST['vms']) {
|
||||
|
||||
echo "\0";
|
||||
echo "<tr title='' class='useupdated'><td>";
|
||||
if ($vmusage == "Y") {
|
||||
foreach ($vmusagehtml as $vmhtml) {
|
||||
echo $vmhtml;
|
||||
}
|
||||
if (!count($vmusagehtml)) echo "<span id='no_usagevms'><br> "._('No running virtual machines')."<br></span>";
|
||||
if ($running < 1 && count($vmusagehtml)) echo "<span id='no_usagevms'><br>". _('No running virtual machines')."<br></span>";
|
||||
if ($vmusage=='Y') {
|
||||
foreach ($vmusagehtml as $vmhtml) echo $vmhtml;
|
||||
if (!count($vmusagehtml)) echo "<span id='no_usagevms'><br> "._('No running virtual machines')."<br></span>";
|
||||
if ($running<1 && count($vmusagehtml)) echo "<span id='no_usagevms'><br>". _('No running virtual machines')."<br></span>";
|
||||
echo "</td></tr>";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1111,6 +1111,97 @@ $(function() {
|
||||
}
|
||||
$('form').append($('<input>').attr({type:'hidden', name:'csrf_token', value:csrf_token}));
|
||||
});
|
||||
|
||||
var gui_pages_available = [];
|
||||
<?
|
||||
$gui_pages = glob("/usr/local/emhttp/plugins/*/*.page");
|
||||
array_walk($gui_pages,function($value,$key){ ?>
|
||||
gui_pages_available.push('<?=basename($value,".page")?>'); <?
|
||||
});
|
||||
?>
|
||||
|
||||
function isValidURL(url) {
|
||||
try {
|
||||
var ret = new URL(url);
|
||||
return ret;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$('body').on("click","a,.ca_href", function(e) {
|
||||
if ($(this).hasClass("ca_href") ) {
|
||||
var ca_href = true;
|
||||
var href=$(this).attr("data-href");
|
||||
var target=$(this).attr("data-target");
|
||||
} else {
|
||||
var ca_href = false;
|
||||
var href = $(this).attr("href");
|
||||
var target = $(this).attr("target");
|
||||
}
|
||||
if ( href ) {
|
||||
href = href.trim();
|
||||
if ( href.match('https?://[^\.]*.(my)?unraid.net/') || href.indexOf("https://unraid.net/") == 0 || href == "https://unraid.net" || href.indexOf("http://lime-technology.com") == 0) {
|
||||
if ( ca_href ) {
|
||||
window.open(href,target);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (href !== "#" && href.indexOf("javascript") !== 0) {
|
||||
var dom = isValidURL(href);
|
||||
if ( dom == false ) {
|
||||
if ( href.indexOf("/") == 0 ) { // all internal links start with "/"
|
||||
return;
|
||||
}
|
||||
var baseURLpage = href.split("/");
|
||||
if ( gui_pages_available.includes(baseURLpage[0]) ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ( $(this).hasClass("localURL") ) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var domainsAllowed = JSON.parse($.cookie("allowedDomains"));
|
||||
} catch(e) {
|
||||
var domainsAllowed = new Object();
|
||||
}
|
||||
$.cookie("allowedDomains",JSON.stringify(domainsAllowed),{expires:3650}); // rewrite cookie to further extend expiration by 400 days
|
||||
|
||||
if ( domainsAllowed[dom.hostname] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
swal({
|
||||
title: "<?=_('External Link')?>",
|
||||
text: "<span title='"+href+"'><?=_('Clicking OK will take you to a 3rd party website not associated with Lime Technology')?><br><br><b>"+href+"<br><br><input id='Link_Always_Allow' type='checkbox'></input><?=_('Always Allow')?> "+dom.hostname+"</span>",
|
||||
html: true,
|
||||
type: 'warning',
|
||||
showCancelButton: true,
|
||||
showConfirmButton: true,
|
||||
cancelButtonText: "<?=_('Cancel')?>",
|
||||
confirmButtonText: "<?=_('OK')?>"
|
||||
},function(isConfirm) {
|
||||
if (isConfirm) {
|
||||
if ( $("#Link_Always_Allow").is(":checked") ) {
|
||||
domainsAllowed[dom.hostname] = true;
|
||||
$.cookie("allowedDomains",JSON.stringify(domainsAllowed),{expires:3650});
|
||||
}
|
||||
var popupOpen = window.open(href,target);
|
||||
if ( !popupOpen || popupOpen.closed || typeof popupOpen == "undefined" ) {
|
||||
var popupWarning = addBannerWarning("<?=_('Popup Blocked.');?>");
|
||||
setTimeout(function() {
|
||||
removeBannerWarning(popupWarning);}
|
||||
,10000);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -40,9 +40,9 @@ default:
|
||||
$file = "/var/lib/$dir/check.status.$id";
|
||||
if (file_exists($file)) {
|
||||
switch ($cmd) {
|
||||
case 'btrfs-check': $pgrep = "pgrep -f '/sbin/btrfs check .*$dev'"; break;
|
||||
case 'rfs-check': $pgrep = "pgrep -f '/sbin/reiserfsck $dev'"; break;
|
||||
case 'xfs-check': $pgrep = "pgrep -f '/sbin/xfs_repair.*$dev'"; break;
|
||||
case 'btrfs-check': $pgrep = 'pgrep --ns $$ -f '."'/sbin/btrfs check .*$dev'"; break;
|
||||
case 'rfs-check': $pgrep = 'pgrep --ns $$ -f '."'/sbin/reiserfsck $dev'"; break;
|
||||
case 'xfs-check': $pgrep = 'pgrep --ns $$ -f '."'/sbin/xfs_repair.*$dev'"; break;
|
||||
}
|
||||
echo file_get_contents($file);
|
||||
if (!exec($pgrep)) echo "\0";
|
||||
|
||||
@@ -263,7 +263,7 @@ function urlencode_path($path) {
|
||||
return str_replace("%2F", "/", urlencode($path));
|
||||
}
|
||||
function pgrep($process_name, $escape_arg=true) {
|
||||
$pid = exec("pgrep ".($escape_arg?escapeshellarg($process_name):$process_name), $output, $retval);
|
||||
$pid = exec('pgrep --ns $$ '.($escape_arg?escapeshellarg($process_name):$process_name), $output, $retval);
|
||||
return $retval==0 ? $pid : false;
|
||||
}
|
||||
function is_block($path) {
|
||||
@@ -345,27 +345,46 @@ function my_mkdir($dirname,$permissions = 0777,$recursive = false,$own = "nobody
|
||||
return($rtncode);
|
||||
}
|
||||
function my_rmdir($dirname) {
|
||||
if (!is_dir($dirname)) return(false);
|
||||
if (!is_dir("$dirname")) {
|
||||
$return = [
|
||||
'rtncode' => "false",
|
||||
'type' => "NoDir",
|
||||
];
|
||||
return($return);
|
||||
}
|
||||
if (strpos($dirname,'/mnt/user/')===0) {
|
||||
$realdisk = trim(shell_exec("getfattr --absolute-names --only-values -n system.LOCATION ".escapeshellarg($dirname)." 2>/dev/null"));
|
||||
if (!empty($realdisk)) {
|
||||
$dirname = str_replace('/mnt/user/', "/mnt/$realdisk/", $dirname);
|
||||
$dirname = str_replace('/mnt/user/', "/mnt/$realdisk/", "$dirname");
|
||||
}
|
||||
}
|
||||
$fstype = trim(shell_exec(" stat -f -c '%T' $dirname"));
|
||||
$fstype = trim(shell_exec(" stat -f -c '%T' ".escapeshellarg($dirname)));
|
||||
$rtncode = false;
|
||||
switch ($fstype) {
|
||||
case "zfs":
|
||||
$zfsoutput = array();
|
||||
$zfsdataset = trim(shell_exec("zfs list -H -o name \"$dirname\"")) ;
|
||||
exec("zfs destroy \"$zfsdataset\"",$zfsoutput,$rtncode);
|
||||
$zfsdataset = trim(shell_exec("zfs list -H -o name ".escapeshellarg($dirname))) ;
|
||||
$cmdstr = "zfs destroy \"$zfsdataset\" 2>&1 ";
|
||||
$error = exec($cmdstr,$zfsoutput,$rtncode);
|
||||
$return = [
|
||||
'rtncode' => $rtncode,
|
||||
'output' => $zfsoutput,
|
||||
'dataset' => $zfsdataset,
|
||||
'type' => $fstype,
|
||||
'cmd' => $cmdstr,
|
||||
'error' => $error,
|
||||
];
|
||||
break;
|
||||
case "btrfs":
|
||||
default:
|
||||
$rtncode = rmdir($dirname);
|
||||
$return = [
|
||||
'rtncode' => $rtncode,
|
||||
'type' => $fstype,
|
||||
];
|
||||
break;
|
||||
}
|
||||
return($rtncode);
|
||||
return($return);
|
||||
}
|
||||
function get_realvolume($path) {
|
||||
if (strpos($path,"/mnt/user/",0) === 0)
|
||||
|
||||
@@ -44,10 +44,10 @@ switch ($_GET['tag']) {
|
||||
case 'ttyd':
|
||||
// check if ttyd already running
|
||||
$sock = "/var/run/ttyd.sock";
|
||||
exec("pgrep -f '$sock'", $ttyd_pid, $retval);
|
||||
exec('pgrep --ns $$ -f '."'$sock'", $ttyd_pid, $retval);
|
||||
if ($retval == 0) {
|
||||
// check if there are any child processes, ie, curently open tty windows
|
||||
exec("pgrep -P ".$ttyd_pid[0], $output, $retval);
|
||||
exec('pgrep --ns $$ -P '.$ttyd_pid[0], $output, $retval);
|
||||
// no child processes, restart ttyd to pick up possible font size change
|
||||
if ($retval != 0) exec("kill ".$ttyd_pid[0]);
|
||||
}
|
||||
|
||||