diff --git a/agent/src/main/java/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java b/agent/src/main/java/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java index d150ea728c73..9974db0d230b 100644 --- a/agent/src/main/java/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java +++ b/agent/src/main/java/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java @@ -18,6 +18,7 @@ import java.io.BufferedReader; import java.io.BufferedWriter; +import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; @@ -32,7 +33,10 @@ import javax.naming.ConfigurationException; +import org.apache.cloudstack.framework.security.keystore.KeystoreManager.Certificates; import org.apache.cloudstack.managed.context.ManagedContextRunnable; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import com.cloud.agent.Agent.ExitStatus; @@ -53,6 +57,7 @@ import com.cloud.agent.api.proxy.ConsoleProxyLoadAnswer; import com.cloud.agent.api.proxy.StartConsoleProxyAgentHttpHandlerCommand; import com.cloud.agent.api.proxy.WatchConsoleProxyLoadCommand; +import com.cloud.consoleproxy.api.NoVncKeyIvPair; import com.cloud.exception.AgentControlChannelException; import com.cloud.host.Host; import com.cloud.host.Host.Type; @@ -112,11 +117,79 @@ public Answer executeRequest(final Command cmd) { } private Answer execute(StartConsoleProxyAgentHttpHandlerCommand cmd) { + launchNoVNC(cmd.getCertificates(), cmd.getNoVncKeyIvPair()); s_logger.info("Invoke launchConsoleProxy() in responding to StartConsoleProxyAgentHttpHandlerCommand"); launchConsoleProxy(cmd.getKeystoreBits(), cmd.getKeystorePassword(), cmd.getEncryptorPassword()); return new Answer(cmd); } + private void launchNoVNC(Certificates certificates, NoVncKeyIvPair keyIvPair) { + if (certificates != null) { + s_logger.info("Saving certificate data for use in CPVM"); + saveCertDataForNoVNC(certificates); + } + if (keyIvPair != null) { + s_logger.info("Saving noVNC key/iv pair for use in CPVM"); + saveKeyIvPairForNoVNC(keyIvPair); + } + startNoVNCScript(certificates); + } + + private void startNoVNCScript(Certificates certificates) { + Script command = new Script("/usr/local/cloud/systemvm/noVNC_daemons.sh", s_logger); + if (certificates != null) { + command = new Script("/usr/local/cloud/systemvm/noVNC_daemons_ssl.sh", s_logger); + } + s_logger.info("Starting noVNC command line: " + command.toString()); + String result = command.execute(); + if (result != null) { + s_logger.warn("Error while starting the noVNC daemons websockify and socat. err=" + result); + } else { + s_logger.info("noVNC daemons websockify and socat are started."); + } + } + + private void saveCertDataForNoVNC(Certificates certificates) { + if (certificates == null) { + s_logger.debug("No certificates"); + return; + } + final String customCertificate = certificates.getPrivCert(); + final String customKey = certificates.getPrivKey(); + if (StringUtils.isEmpty(customCertificate) || StringUtils.isEmpty(customKey) ) { + s_logger.debug("No certificate data"); + return; + } else if (s_logger.isDebugEnabled()) { + s_logger.debug("Saving " + customCertificate.length() + " + " + customKey.length() + " bytes of certificate data."); + } + // considder making these configurable or settable through the command class + final String customCertificateLocation = "/usr/local/cloud/systemvm/certs/customssl.crt"; + final String customKeyLocation = "/usr/local/cloud/systemvm/certs/customssl.key"; + try (FileWriter certificateStream = new FileWriter(customCertificateLocation); + FileWriter keyStream = new FileWriter(customKeyLocation); + BufferedWriter certificateBufferer = new BufferedWriter(certificateStream); + BufferedWriter keyBufferer = new BufferedWriter(keyStream); + ) { + certificateBufferer.write(customCertificate); + keyBufferer.write(customKey); + } catch (IOException e) { + s_logger.error("saving custom certificates failed due to ", e); + } + + } + + private void saveKeyIvPairForNoVNC(NoVncKeyIvPair keyIvPair) { + try { + File file = new File("/root/noVNC/utils/websockify/websockify/websocketproxy.py"); + String websockify = FileUtils.readFileToString(file); + websockify = websockify.replaceFirst("self.novnc_key =(.*)", "self.novnc_key = '" + keyIvPair.getKey() + "'"); + websockify = websockify.replaceFirst("self.novnc_iv =(.*)", "self.novnc_iv = '" + keyIvPair.getIv() + "'"); + FileUtils.writeStringToFile(file, websockify); + } catch (IOException e) { + s_logger.error("Unable to save noVNC key/iv pair due to ", e); + } + } + private void disableRpFilter() { try (FileWriter fstream = new FileWriter("/proc/sys/net/ipv4/conf/eth2/rp_filter"); BufferedWriter out = new BufferedWriter(fstream);) diff --git a/api/pom.xml b/api/pom.xml index fdee3d8879af..c51e55f9ee04 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -66,6 +66,11 @@ cloud-framework-direct-download ${project.version} + + org.projectlombok + lombok + ${cs.lombok.version} + diff --git a/api/src/main/java/com/cloud/consoleproxy/api/NoVncKeyIvPair.java b/api/src/main/java/com/cloud/consoleproxy/api/NoVncKeyIvPair.java new file mode 100644 index 000000000000..179bc6f3b7fb --- /dev/null +++ b/api/src/main/java/com/cloud/consoleproxy/api/NoVncKeyIvPair.java @@ -0,0 +1,29 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.consoleproxy.api; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class NoVncKeyIvPair { + String key; + String iv; +} diff --git a/client/src/main/webapp/WEB-INF/web.xml b/client/src/main/webapp/WEB-INF/web.xml index 03f7d15db62e..e72a51b44541 100644 --- a/client/src/main/webapp/WEB-INF/web.xml +++ b/client/src/main/webapp/WEB-INF/web.xml @@ -51,6 +51,12 @@ 6 + + noVncConsoleServlet + com.cloud.servlet.NoVncConsoleProxyServlet + 7 + + apiServlet /api/* @@ -61,6 +67,11 @@ /console + + noVncConsoleServlet + /novncconsole + + java.lang.Exception /error.html diff --git a/core/src/main/java/com/cloud/agent/api/proxy/StartConsoleProxyAgentHttpHandlerCommand.java b/core/src/main/java/com/cloud/agent/api/proxy/StartConsoleProxyAgentHttpHandlerCommand.java index 223cf8adae28..069c406f7dff 100644 --- a/core/src/main/java/com/cloud/agent/api/proxy/StartConsoleProxyAgentHttpHandlerCommand.java +++ b/core/src/main/java/com/cloud/agent/api/proxy/StartConsoleProxyAgentHttpHandlerCommand.java @@ -19,9 +19,12 @@ package com.cloud.agent.api.proxy; +import org.apache.cloudstack.framework.security.keystore.KeystoreManager.Certificates; + import com.cloud.agent.api.Command; import com.cloud.agent.api.LogLevel; import com.cloud.agent.api.LogLevel.Log4jLevel; +import com.cloud.consoleproxy.api.NoVncKeyIvPair; public class StartConsoleProxyAgentHttpHandlerCommand extends Command { @LogLevel(Log4jLevel.Off) @@ -30,6 +33,10 @@ public class StartConsoleProxyAgentHttpHandlerCommand extends Command { private String keystorePassword; @LogLevel(Log4jLevel.Off) private String encryptorPassword; + @LogLevel(Log4jLevel.Off) + private Certificates certificates; + @LogLevel(Log4jLevel.Off) + private NoVncKeyIvPair keyIvPair; public StartConsoleProxyAgentHttpHandlerCommand() { super(); @@ -68,4 +75,20 @@ public String getEncryptorPassword() { public void setEncryptorPassword(String encryptorPassword) { this.encryptorPassword = encryptorPassword; } + + public void setCertificates(Certificates certificates) { + this.certificates = certificates; + } + + public Certificates getCertificates() { + return certificates; + } + + public void setNoVncKeyIvPair(NoVncKeyIvPair keyIvPair) { + this.keyIvPair = keyIvPair; + } + + public NoVncKeyIvPair getNoVncKeyIvPair() { + return keyIvPair; + } } diff --git a/pom.xml b/pom.xml index 28cd1c4f98d7..86c225b47b6b 100644 --- a/pom.xml +++ b/pom.xml @@ -65,6 +65,7 @@ 2.22.2 3.0.5 3.2.0 + 1.18.12 3.12.0 3.0.0 2.5.3 @@ -705,6 +706,11 @@ javax.inject 1 + + org.projectlombok + lombok + ${cs.lombok.version} + diff --git a/server/src/main/java/com/cloud/consoleproxy/AgentHookBase.java b/server/src/main/java/com/cloud/consoleproxy/AgentHookBase.java index c3911826feb6..27b00d44468a 100644 --- a/server/src/main/java/com/cloud/consoleproxy/AgentHookBase.java +++ b/server/src/main/java/com/cloud/consoleproxy/AgentHookBase.java @@ -30,6 +30,7 @@ import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.security.keys.KeysManager; import org.apache.cloudstack.framework.security.keystore.KeystoreManager; +import org.apache.cloudstack.framework.security.keystore.KeystoreManager.Certificates; import com.cloud.agent.AgentManager; import com.cloud.agent.api.AgentControlAnswer; @@ -43,6 +44,7 @@ import com.cloud.agent.api.StartupProxyCommand; import com.cloud.agent.api.proxy.StartConsoleProxyAgentHttpHandlerCommand; import com.cloud.configuration.Config; +import com.cloud.consoleproxy.api.NoVncKeyIvPair; import com.cloud.exception.AgentUnavailableException; import com.cloud.exception.OperationTimedoutException; import com.cloud.host.Host; @@ -69,6 +71,7 @@ public abstract class AgentHookBase implements AgentHook { AgentManager _agentMgr; KeystoreManager _ksMgr; KeysManager _keysMgr; + ConsoleProxyManager _consoleMgr; public AgentHookBase(VMInstanceDao instanceDao, HostDao hostDao, ConfigurationDao cfgDao, KeystoreManager ksMgr, AgentManager agentMgr, KeysManager keysMgr) { _instanceDao = instanceDao; @@ -198,16 +201,29 @@ public void startAgentHttpHandlerInVM(StartupProxyCommand startupCmd) { String storePassword = Base64.encodeBase64String(randomBytes); byte[] ksBits = null; + Certificates certificates = null; + boolean sslEnabled = false; + String sslEnabledStr = _configDao.getValue("consoleproxy.sslEnabled"); + if (sslEnabledStr != null && sslEnabledStr.equalsIgnoreCase("true")) { + sslEnabled = true; + } String consoleProxyUrlDomain = _configDao.getValue(Config.ConsoleProxyUrlDomain.key()); - if (consoleProxyUrlDomain == null || consoleProxyUrlDomain.isEmpty()) { + if (!sslEnabled || consoleProxyUrlDomain == null || consoleProxyUrlDomain.isEmpty()) { s_logger.debug("SSL is disabled for console proxy based on global config, skip loading certificates"); } else { ksBits = _ksMgr.getKeystoreBits(ConsoleProxyManager.CERTIFICATE_NAME, ConsoleProxyManager.CERTIFICATE_NAME, storePassword); + if (sslEnabled) { + certificates = _ksMgr.getCertificates(ConsoleProxyManager.CERTIFICATE_NAME); + } //ks manager raises exception if ksBits are null, hence no need to explicltly handle the condition } cmd = new StartConsoleProxyAgentHttpHandlerCommand(ksBits, storePassword); cmd.setEncryptorPassword(getEncryptorPassword()); + s_logger.debug("Adding data for noVNC based console proxy"); + cmd.setCertificates(certificates); + NoVncKeyIvPair keyIvPair = new NoVncKeyIvPair(ConsoleProxyManager.NoVncEncryptionKey.value(), ConsoleProxyManager.NoVncEncryptionIV.value()); + cmd.setNoVncKeyIvPair(keyIvPair); HostVO consoleProxyHost = findConsoleProxyHost(startupCmd); diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java index 8496301aa430..aa0a461f6d9e 100644 --- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java @@ -16,6 +16,8 @@ // under the License. package com.cloud.consoleproxy; +import org.apache.cloudstack.framework.config.ConfigKey; + import com.cloud.utils.component.Manager; import com.cloud.vm.ConsoleProxyVO; @@ -34,6 +36,14 @@ public interface ConsoleProxyManager extends Manager, ConsoleProxyService { public static final String ALERT_SUBJECT = "proxy-alert"; public static final String CERTIFICATE_NAME = "CPVMCertificate"; + public static final int DEFAULT_NOVNC_PORT = 8080; + public static final ConfigKey NoVncConsoleDefault = new ConfigKey("Advanced", Boolean.class, "novnc.console.default", "true", + "If true, noVNC console will be default console for virtual machines", true); + public static final ConfigKey NoVncEncryptionKey = new ConfigKey("Hidden", String.class, "novnc.encryption.key", null, + "The key to be used to encryption in novnc console. The ConsoleProxy VM needs to be restarted if this changes.", true, ConfigKey.Scope.Global); + public static final ConfigKey NoVncEncryptionIV = new ConfigKey("Hidden", String.class, "novnc.encryption.iv", null, + "The iv to be used to encryption in novnc console. The ConsoleProxy VM needs to be restarted if this changes.", true, ConfigKey.Scope.Global); + public void setManagementState(ConsoleProxyManagementState state); public ConsoleProxyManagementState getManagementState(); diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java index a299937650c7..cf5cd16fdcf2 100644 --- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java @@ -17,6 +17,8 @@ package com.cloud.consoleproxy; import java.nio.charset.Charset; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; @@ -32,6 +34,8 @@ import org.apache.cloudstack.agent.lb.IndirectAgentLB; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.framework.security.keys.KeysManager; import org.apache.cloudstack.framework.security.keystore.KeystoreDao; @@ -41,6 +45,7 @@ import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; +import org.apache.commons.codec.binary.Base64; import org.apache.commons.collections.CollectionUtils; import org.apache.log4j.Logger; @@ -153,7 +158,7 @@ // Starting, HA, Migrating, Running state are all counted as "Open" for available capacity calculation // because sooner or later, it will be driven into Running state // -public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxyManager, VirtualMachineGuru, SystemVmLoadScanHandler, ResourceStateAdapter { +public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxyManager, VirtualMachineGuru, SystemVmLoadScanHandler, ResourceStateAdapter, Configurable { private static final Logger s_logger = Logger.getLogger(ConsoleProxyManagerImpl.class); private static final int DEFAULT_CAPACITY_SCAN_INTERVAL = 30000; // 30 seconds @@ -370,6 +375,18 @@ protected HostVO findConsoleProxyHost(StartupProxyCommand startupCmd) { @Override public ConsoleProxyInfo assignProxy(final long dataCenterId, final long vmId) { + // Get latest configurations + _sslEnabled = false; + String sslEnabledStr = _configDao.getValue("consoleproxy.sslEnabled"); + if (sslEnabledStr != null && sslEnabledStr.equalsIgnoreCase("true")) { + _sslEnabled = true; + } + _consoleProxyUrlDomain = _configDao.getValue(Config.ConsoleProxyUrlDomain.key()); + if(_sslEnabled && (_consoleProxyUrlDomain == null || _consoleProxyUrlDomain.isEmpty())) { + s_logger.warn("Empty console proxy domain, explicitly disabling SSL"); + _sslEnabled = false; + } + ConsoleProxyVO proxy = doAssignProxy(dataCenterId, vmId); if (proxy == null) { return null; @@ -1330,6 +1347,10 @@ public boolean configure(String name, Map params) throws Configu } } + // Initialize NoVncEncryptionKey and NoVncEncryptionIV + _configDao.getValueAndInitIfNotExist(NoVncEncryptionKey.key(), NoVncEncryptionKey.category(), getBase64EncodedRandomKey(128), NoVncEncryptionKey.description()); + _configDao.getValueAndInitIfNotExist(NoVncEncryptionIV.key(), NoVncEncryptionIV.category(), getBase64EncodedRandomKey(128), NoVncEncryptionIV.description()); + _loadScanner = new SystemVmLoadScanner(this); _loadScanner.initScan(STARTUP_DELAY, _capacityScanInterval); _resourceMgr.registerResourceStateAdapter(this.getClass().getSimpleName(), this); @@ -1348,6 +1369,19 @@ public boolean configure(String name, Map params) throws Configu protected ConsoleProxyManagerImpl() { } + private static String getBase64EncodedRandomKey(int nBits) { + SecureRandom random; + try { + random = SecureRandom.getInstance("SHA1PRNG"); + byte[] keyBytes = new byte[nBits / 8]; + random.nextBytes(keyBytes); + return Base64.encodeBase64URLSafeString(keyBytes); + } catch (NoSuchAlgorithmException e) { + s_logger.error("Unhandled exception: ", e); + } + return null; + } + @Override public boolean finalizeVirtualMachineProfile(VirtualMachineProfile profile, DeployDestination dest, ReservationContext context) { @@ -1735,4 +1769,14 @@ public void setConsoleProxyAllocators(List consoleProxyAl _consoleProxyAllocators = consoleProxyAllocators; } + @Override + public String getConfigComponentName() { + return ConsoleProxyManager.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[] { NoVncConsoleDefault, NoVncEncryptionKey, NoVncEncryptionIV }; + } + } diff --git a/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java b/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java index ae9b5c548e56..ce832d77d26a 100644 --- a/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java +++ b/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java @@ -41,6 +41,7 @@ import org.springframework.stereotype.Component; import org.springframework.web.context.support.SpringBeanAutowiringSupport; +import com.cloud.consoleproxy.ConsoleProxyManager; import com.cloud.exception.PermissionDeniedException; import com.cloud.host.HostVO; import com.cloud.hypervisor.Hypervisor; @@ -116,6 +117,11 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) { return; } + if (ConsoleProxyManager.NoVncConsoleDefault.value()) { + req.getRequestDispatcher("/novncconsole").forward(req, resp); + return; + } + if (_keysMgr.getHashKey() == null) { s_logger.debug("Console/thumbnail access denied. Ticket service is not ready yet"); sendResponse(resp, "Service is not ready"); diff --git a/server/src/main/java/com/cloud/servlet/NoVncConsoleProxyClientParam.java b/server/src/main/java/com/cloud/servlet/NoVncConsoleProxyClientParam.java new file mode 100644 index 000000000000..af42aa92ea0b --- /dev/null +++ b/server/src/main/java/com/cloud/servlet/NoVncConsoleProxyClientParam.java @@ -0,0 +1,172 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.servlet; + +// To maintain independency of console proxy project, we duplicate this class from console proxy project +public class NoVncConsoleProxyClientParam { + private String clientHostAddress; + private int clientHostPort; + private String clientHostPassword; + private String clientIp; + private String clientTag; + private String ticket; + private String locale; + private String clientTunnelUrl; + private String clientTunnelSession; + + private String hypervHost; + + private String ajaxSessionId; + private String username; + private String password; + + private String proxy; + private String vmName; + + public NoVncConsoleProxyClientParam() { + clientHostPort = 0; + } + + public String getClientHostAddress() { + return clientHostAddress; + } + + public void setClientHostAddress(String clientHostAddress) { + this.clientHostAddress = clientHostAddress; + } + + public int getClientHostPort() { + return clientHostPort; + } + + public void setClientHostPort(int clientHostPort) { + this.clientHostPort = clientHostPort; + } + + public String getClientHostPassword() { + return clientHostPassword; + } + + public void setClientHostPassword(String clientHostPassword) { + this.clientHostPassword = clientHostPassword; + } + + public String getClientIp() { + return clientIp; + } + + public void setClientIp(String clientIp) { + this.clientIp = clientIp; + } + + public String getClientTag() { + return clientTag; + } + + public void setClientTag(String clientTag) { + this.clientTag = clientTag; + } + + public String getTicket() { + return ticket; + } + + public void setTicket(String ticket) { + this.ticket = ticket; + } + + public String getClientTunnelUrl() { + return clientTunnelUrl; + } + + public void setClientTunnelUrl(String clientTunnelUrl) { + this.clientTunnelUrl = clientTunnelUrl; + } + + public String getClientTunnelSession() { + return clientTunnelSession; + } + + public void setClientTunnelSession(String clientTunnelSession) { + this.clientTunnelSession = clientTunnelSession; + } + + public String getAjaxSessionId() { + return ajaxSessionId; + } + + public void setAjaxSessionId(String ajaxSessionId) { + this.ajaxSessionId = ajaxSessionId; + } + + public String getLocale() { + return locale; + } + + public void setLocale(String locale) { + this.locale = locale; + } + + public String getClientMapKey() { + if (clientTag != null && !clientTag.isEmpty()) + return clientTag; + + return clientHostAddress + ":" + clientHostPort; + } + + public void setHypervHost(String host) { + hypervHost = host; + } + + public String getHypervHost() { + return hypervHost; + } + + public void setUsername(String username) { + this.username = username; + + } + + public String getUsername() { + return username; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getPassword() { + return password; + } + + public void setProxy(String proxy) { + this.proxy = proxy; + } + + public String getProxy() { + return proxy; + } + + public void setVmName(String vmName) { + this.vmName = vmName; + } + + public String getVmName() { + return vmName; + } + +} diff --git a/server/src/main/java/com/cloud/servlet/NoVncConsoleProxyEncryptor.java b/server/src/main/java/com/cloud/servlet/NoVncConsoleProxyEncryptor.java new file mode 100644 index 000000000000..ce7f5e310e67 --- /dev/null +++ b/server/src/main/java/com/cloud/servlet/NoVncConsoleProxyEncryptor.java @@ -0,0 +1,212 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.servlet; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.apache.commons.codec.binary.Hex; +import org.apache.log4j.Logger; + +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.bouncycastle.crypto.engines.RijndaelEngine; +import org.bouncycastle.crypto.modes.CBCBlockCipher; +import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.bouncycastle.crypto.paddings.ZeroBytePadding; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; + +import com.cloud.consoleproxy.ConsoleProxyManager; + +public class NoVncConsoleProxyEncryptor { + private static final Logger s_logger = Logger.getLogger(NoVncConsoleProxyEncryptor.class); + + private static String key; + private static String iv; + + public NoVncConsoleProxyEncryptor(String key, String iv) { + this.key = key; + this.iv = iv; + } + + protected String encrypt(NoVncConsoleProxyClientParam param) { + if (param == null) + return null; + + try { + String host = param.getClientHostAddress(); + int port = param.getClientHostPort(); + String password = param.getClientHostPassword(); + String clientIp = param.getClientIp(); + String proxy = param.getProxy(); + String vmName = param.getVmName(); + long timestamp = System.currentTimeMillis() / 1000; + + final StringBuilder data = new StringBuilder(); + data.append(host).append(":").append(port).append(":").append(clientIp).append(":").append(timestamp); + data.append(":").append(password); + String dataHash = hash(key + data.toString()); + data.append("|").append(dataHash); + + String encryptedText = Hex.encodeHexString(encryptText(data.toString())); + + Map input = new HashMap(); + input.put("port", String.valueOf(ConsoleProxyManager.DEFAULT_NOVNC_PORT)); + input.put("host", proxy); + input.put("path", encryptedText); + input.put("displayName", vmName); + + return httpBuildQuery(input); + } catch (NoSuchAlgorithmException e) { + s_logger.error("Unexpected exception ", e); + return null; + } catch (InvalidCipherTextException e) { + s_logger.error("Unexpected exception ", e); + return null; + } + } + + protected static String httpBuildQuery(Map input) { + String result = ""; + Iterator it = input.entrySet().iterator(); + while (it.hasNext()){ + Map.Entry entry = (Map.Entry) it.next(); + String key = entry.getKey(); + String value = entry.getValue(); + result += key + "=" + value + "&"; + } + result = result.substring(0, result.length() - 1); + try { + result = URLEncoder.encode(result, "UTF-8"); + } catch (final UnsupportedEncodingException e) { + return null; + } + result = result.replace("%3D", "=").replace("%26", "&"); + return result; + } + + protected static final String hash(String text) throws NoSuchAlgorithmException { + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + byte[] textBytes = messageDigest.digest(text.getBytes(Charset.forName("UTF-8"))); + return Hex.encodeHexString(textBytes); + } + + protected static final byte[] encryptText(String text) throws InvalidCipherTextException { + if (text == null || text.isEmpty()) + return null; + + byte[] keyBytes = key.getBytes(Charset.forName("ASCII")); + final int keysize = 256; + byte[] keyData = new byte[keysize / Byte.SIZE]; + System.arraycopy(keyBytes, 0, keyData, 0, Math.min(keyBytes.length, keyData.length)); + KeyParameter keyParameter = new KeyParameter(keyData); + + PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher( + new RijndaelEngine(256), new ZeroBytePadding()); + cipher.init(true, keyParameter); + + byte[] plaintext = text.getBytes(Charset.forName("UTF-8")); + byte[] ciphertext = new byte[cipher.getOutputSize(plaintext.length)]; + int offset = cipher.processBytes(plaintext, 0, plaintext.length, ciphertext, 0); + offset += cipher.doFinal(ciphertext, offset); + return ciphertext; + } + + protected static final byte[] encryptTextCBC(String text) throws InvalidCipherTextException { + if (text == null || text.isEmpty()) + return null; + + byte[] keyBytes = key.getBytes(Charset.forName("ASCII")); + final int keysize = 256; + byte[] keyData = new byte[keysize / Byte.SIZE]; + System.arraycopy(keyBytes, 0, keyData, 0, Math.min(keyBytes.length, keyData.length)); + KeyParameter keyParameter = new KeyParameter(keyData); + + byte[] ivBytes = iv.getBytes(Charset.forName("ASCII")); + byte[] ivData = new byte[keysize / Byte.SIZE]; + System.arraycopy(ivBytes, 0, ivData, 0, Math.min(ivBytes.length, ivData.length)); + ParametersWithIV parametersWithIV = new ParametersWithIV(keyParameter, ivData); + + PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher( + new CBCBlockCipher(new RijndaelEngine(256)), new ZeroBytePadding()); + cipher.init(true, parametersWithIV); + + byte[] plaintext = text.getBytes(Charset.forName("UTF-8")); + byte[] ciphertext = new byte[cipher.getOutputSize(plaintext.length)]; + int offset = cipher.processBytes(plaintext, 0, plaintext.length, ciphertext, 0); + offset += cipher.doFinal(ciphertext, offset); + return ciphertext; + } + + protected static final String decryptText(byte[] text) throws InvalidCipherTextException { + if (text == null) + return null; + + byte[] keyBytes = key.getBytes(Charset.forName("ASCII")); + final int keysize = 256; + byte[] keyData = new byte[keysize / Byte.SIZE]; + System.arraycopy(keyBytes, 0, keyData, 0, Math.min(keyBytes.length, keyData.length)); + KeyParameter keyParameter = new KeyParameter(keyData); + + PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher( + new RijndaelEngine(256), new ZeroBytePadding()); + cipher.init(false, keyParameter); + + byte[] decryptedtext = new byte[cipher.getOutputSize(text.length)]; + int offset = cipher.processBytes(text, 0, text.length, decryptedtext, 0); + offset += cipher.doFinal(decryptedtext, offset); + byte[] result = new byte[offset]; + System.arraycopy(decryptedtext, 0, result, 0, offset); + + return new String(result); + } + + protected static final String decryptTextCBC(byte[] text) throws InvalidCipherTextException { + if (text == null) + return null; + + byte[] keyBytes = key.getBytes(Charset.forName("ASCII")); + final int keysize = 256; + byte[] keyData = new byte[keysize / Byte.SIZE]; + System.arraycopy(keyBytes, 0, keyData, 0, Math.min(keyBytes.length, keyData.length)); + KeyParameter keyParameter = new KeyParameter(keyData); + + byte[] ivBytes = iv.getBytes(Charset.forName("ASCII")); + byte[] ivData = new byte[keysize / Byte.SIZE]; + System.arraycopy(ivBytes, 0, ivData, 0, Math.min(ivBytes.length, ivData.length)); + ParametersWithIV parametersWithIV = new ParametersWithIV(keyParameter, ivData); + + PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher( + new CBCBlockCipher(new RijndaelEngine(256)), new ZeroBytePadding()); + cipher.init(false, parametersWithIV); + + byte[] decryptedtext = new byte[cipher.getOutputSize(text.length)]; + int offset = cipher.processBytes(text, 0, text.length, decryptedtext, 0); + offset += cipher.doFinal(decryptedtext, offset); + byte[] result = new byte[offset]; + System.arraycopy(decryptedtext, 0, result, 0, offset); + + return new String(result); + } + +} diff --git a/server/src/main/java/com/cloud/servlet/NoVncConsoleProxyServlet.java b/server/src/main/java/com/cloud/servlet/NoVncConsoleProxyServlet.java new file mode 100644 index 000000000000..4be95a4ce256 --- /dev/null +++ b/server/src/main/java/com/cloud/servlet/NoVncConsoleProxyServlet.java @@ -0,0 +1,529 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.servlet; + +import java.io.IOException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import javax.inject.Inject; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.apache.commons.codec.binary.Base64; +import org.apache.log4j.Logger; +import org.springframework.stereotype.Component; +import org.springframework.web.context.support.SpringBeanAutowiringSupport; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import com.cloud.consoleproxy.ConsoleProxyManager; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.host.HostVO; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.server.ManagementServer; +import com.cloud.user.Account; +import com.cloud.user.AccountManager; +import com.cloud.user.User; +import com.cloud.uservm.UserVm; +import com.cloud.utils.ConstantTimeComparator; +import com.cloud.utils.Pair; +import com.cloud.utils.Ternary; +import com.cloud.utils.db.EntityManager; +import com.cloud.utils.db.TransactionLegacy; +import com.cloud.utils.net.NetUtils; +import com.cloud.vm.UserVmDetailVO; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.dao.UserVmDetailsDao; + +/** + * Console access : /novncconsole?cmd=access&vm=xxx + */ +@Component("noVncConsoleServlet") +public class NoVncConsoleProxyServlet extends HttpServlet { + private static final long serialVersionUID = -5515382620323808169L; + public static final Logger s_logger = Logger.getLogger(NoVncConsoleProxyServlet.class.getName()); + private final static List s_clientAddressHeaders = Collections + .unmodifiableList(Arrays.asList("X-Forwarded-For", + "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR", "Remote_Addr")); + + @Inject + AccountManager _accountMgr; + @Inject + VirtualMachineManager _vmMgr; + @Inject + ManagementServer _ms; + @Inject + EntityManager _entityMgr; + @Inject + UserVmDetailsDao _userVmDetailsDao; + + private final Gson _gson = new GsonBuilder().create(); + + public NoVncConsoleProxyServlet() { + } + + @Override + public void init(ServletConfig config) throws ServletException { + SpringBeanAutowiringSupport.processInjectionBasedOnServletContext(this, config.getServletContext()); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) { + doGet(req, resp); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + + try { + if (_accountMgr == null || _vmMgr == null || _ms == null) { + sendResponse(resp, "Service is not ready"); + return; + } + + if (ConsoleProxyManager.NoVncEncryptionKey.value() == null || ConsoleProxyManager.NoVncEncryptionIV.value() == null) { + s_logger.debug("novnc Console access denied. novnc.security.key or novnc.security.iv is not set. Ticket service is not ready yet"); + sendResponse(resp, "Service is not ready"); + return; + } + + String userId = null; + String account = null; + Account accountObj = null; + + Map params = new HashMap(); + params.putAll(req.getParameterMap()); + + HttpSession session = req.getSession(false); + if (session == null) { + if (verifyRequest(params)) { + userId = (String)params.get("userid")[0]; + account = (String)params.get("account")[0]; + accountObj = (Account)params.get("accountobj")[0]; + } else { + s_logger.debug("Invalid web session or API key in request, reject novnc console access"); + sendResponse(resp, "Access denied. Invalid web session or API key in request"); + return; + } + } else { + // adjust to latest API refactoring changes + if (session.getAttribute("userid") != null) { + userId = ((Long)session.getAttribute("userid")).toString(); + } + + accountObj = (Account)session.getAttribute("accountobj"); + if (accountObj != null) { + account = "" + accountObj.getId(); + } + } + + // Do a sanity check here to make sure the user hasn't already been deleted + if ((userId == null) || (account == null) || (accountObj == null) || !verifyUser(Long.valueOf(userId))) { + s_logger.debug("Invalid user/account, reject novnc console access"); + sendResponse(resp, "Access denied. Invalid or inconsistent account is found"); + return; + } + + String cmd = req.getParameter("cmd"); + if (cmd == null || (!cmd.equalsIgnoreCase("access") && !cmd.equalsIgnoreCase("auth"))) { + s_logger.debug("invalid console servlet command: " + cmd); + sendResponse(resp, ""); + return; + } + + String vmIdString = req.getParameter("vm"); + VirtualMachine vm = _entityMgr.findByUuid(VirtualMachine.class, vmIdString); + if (vm == null) { + s_logger.info("invalid console servlet command parameter: " + vmIdString); + sendResponse(resp, ""); + return; + } + + Long vmId = vm.getId(); + + if (!checkSessionPermision(req, vmId, accountObj)) { + sendResponse(resp, "Permission denied"); + return; + } + + if (cmd.equalsIgnoreCase("access")) { + handleAccessRequest(req, resp, vmId); + } else { + handleAuthRequest(req, resp, vmId); + } + } catch (Throwable e) { + s_logger.error("Unexepected exception in NoVncConsoleProxyServlet", e); + sendResponse(resp, "Server Internal Error"); + } + } + + private void handleAccessRequest(HttpServletRequest req, HttpServletResponse resp, long vmId) { + VirtualMachine vm = _vmMgr.findById(vmId); + if (vm == null) { + s_logger.warn("VM " + vmId + " does not exist, sending blank response for console access request"); + sendResponse(resp, ""); + return; + } + + if (vm.getHostId() == null) { + s_logger.warn("VM " + vmId + " lost host info, sending blank response for console access request"); + sendResponse(resp, ""); + return; + } + + HostVO host = _ms.getHostBy(vm.getHostId()); + if (host == null) { + s_logger.warn("VM " + vmId + "'s host does not exist, sending blank response for console access request"); + sendResponse(resp, ""); + return; + } + + if (Hypervisor.HypervisorType.LXC.equals(vm.getHypervisorType())){ + sendResponse(resp, "

Console access is not supported for LXC

"); + return; + } + + String rootUrl = _ms.getConsoleAccessUrlRoot(vmId); + if (rootUrl == null) { + sendResponse(resp, "

Console access will be ready in a few minutes. Please try it again later.

"); + return; + } + + String vmName = vm.getHostName(); + if (vm.getType() == VirtualMachine.Type.User) { + UserVm userVm = _entityMgr.findById(UserVm.class, vmId); + String displayName = userVm.getDisplayName(); + if (displayName != null && !displayName.isEmpty() && !displayName.equals(vmName)) { + vmName += "(" + displayName + ")"; + } + } + + StringBuffer sb = new StringBuffer(); + sb.append("").append(ConsoleProxyServlet.escapeHTML(vmName)).append(""); + s_logger.debug("the console url is :: " + sb.toString()); + sendResponse(resp, sb.toString()); + } + + private void handleAuthRequest(HttpServletRequest req, HttpServletResponse resp, long vmId) { + + // TODO authentication channel between console proxy VM and management server needs to be secured, + // the data is now being sent through private network, but this is apparently not enough + VirtualMachine vm = _vmMgr.findById(vmId); + if (vm == null) { + s_logger.warn("VM " + vmId + " does not exist, sending failed response for authentication request from console proxy"); + sendResponse(resp, "failed"); + return; + } + + if (vm.getHostId() == null) { + s_logger.warn("VM " + vmId + " lost host info, failed response for authentication request from console proxy"); + sendResponse(resp, "failed"); + return; + } + + HostVO host = _ms.getHostBy(vm.getHostId()); + if (host == null) { + s_logger.warn("VM " + vmId + "'s host does not exist, sending failed response for authentication request from console proxy"); + sendResponse(resp, "failed"); + return; + } + + String sid = req.getParameter("sid"); + if (sid == null || !sid.equals(vm.getVncPassword())) { + s_logger.warn("sid " + sid + " in url does not match stored sid."); + sendResponse(resp, "failed"); + return; + } + + sendResponse(resp, "success"); + } + + private String composeNoVncConsoleAccessUrl(String rootUrl, VirtualMachine vm, HostVO hostVo, HttpServletRequest req) { + StringBuffer sb = new StringBuffer(rootUrl); + String host = hostVo.getPrivateIpAddress(); + + Pair portInfo = _ms.getVncPort(vm); + if (s_logger.isDebugEnabled()) + s_logger.debug("Port info " + portInfo.first()); + + Ternary parsedHostInfo = ConsoleProxyServlet.parseHostInfo(portInfo.first()); + + int port = -1; + if (portInfo.second() == -9) { + //for hyperv + port = Integer.parseInt(_ms.findDetail(hostVo.getId(), "rdp.server.port").getValue()); + } else { + port = portInfo.second(); + } + + String sid = vm.getVncPassword(); + UserVmDetailVO details = _userVmDetailsDao.findDetail(vm.getId(), "keyboard"); + + String tag = vm.getUuid(); + + String clientIp = getClientAddress(req); + + String ticket = ConsoleProxyServlet.genAccessTicket(parsedHostInfo.first(), String.valueOf(port), sid, tag); + NoVncConsoleProxyEncryptor encryptor = new NoVncConsoleProxyEncryptor(ConsoleProxyManager.NoVncEncryptionKey.value(), ConsoleProxyManager.NoVncEncryptionIV.value()); + NoVncConsoleProxyClientParam param = new NoVncConsoleProxyClientParam(); + param.setProxy(rootUrl.substring(rootUrl.lastIndexOf("/") + 1, rootUrl.length())); + param.setClientHostAddress(parsedHostInfo.first()); + param.setClientHostPort(port); + param.setClientHostPassword(sid); + param.setClientIp(clientIp); + param.setClientTag(tag); + param.setTicket(ticket); + + if (details != null) { + param.setLocale(details.getValue()); + } + + if (portInfo.second() == -9) { + //For Hyperv Clinet Host Address will send Instance id + param.setHypervHost(host); + param.setUsername(_ms.findDetail(hostVo.getId(), "username").getValue()); + param.setPassword(_ms.findDetail(hostVo.getId(), "password").getValue()); + } + if (parsedHostInfo.second() != null && parsedHostInfo.third() != null) { + param.setClientTunnelUrl(parsedHostInfo.second()); + param.setClientTunnelSession(parsedHostInfo.third()); + } + + String vmName = vm.getHostName(); + if (vm.getType() == VirtualMachine.Type.User) { + UserVm userVm = _entityMgr.findById(UserVm.class, vm.getId()); + String displayName = userVm.getDisplayName(); + if (displayName != null && !displayName.isEmpty() && !displayName.equals(vmName)) { + vmName += "(" + displayName + ")"; + } + } + param.setVmName(vmName); + + sb.append(":" + ConsoleProxyManager.DEFAULT_NOVNC_PORT + "/vnc_lite.html?" + encryptor.encrypt(param)); + + if (s_logger.isDebugEnabled()) { + s_logger.debug("Compose novnc console url: " + sb.toString()); + } + return sb.toString(); + } + + private void sendResponse(HttpServletResponse resp, String content) { + try { + resp.setContentType("text/html"); + resp.getWriter().print(content); + } catch (IOException e) { + s_logger.info("Client may already close the connection", e); + } + } + + // This is copied from ApiServlet + static String getClientAddress(final HttpServletRequest request) { + for(final String header : s_clientAddressHeaders) { + final String ip = getCorrectIPAddress(request.getHeader(header)); + if (ip != null) { + return ip; + } + } + + return request.getRemoteAddr(); + } + // This is copied from ApiServlet + private static String getCorrectIPAddress(String ip) { + if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + return null; + } + if(NetUtils.isValidIp4(ip) || NetUtils.isValidIp6(ip)) { + return ip; + } + String[] ips = ip.split(","); + for(String i : ips) { + if(NetUtils.isValidIp4(i.trim()) || NetUtils.isValidIp6(i.trim())) { + return i.trim(); + } + } + return null; + } + + // This is copied from ConsoleProxyServlet + private boolean checkSessionPermision(HttpServletRequest req, long vmId, Account accountObj) { + + VirtualMachine vm = _vmMgr.findById(vmId); + if (vm == null) { + s_logger.debug("Console/thumbnail access denied. VM " + vmId + " does not exist in system any more"); + return false; + } + + if (_accountMgr.isRootAdmin(accountObj.getId())) + return true; + + switch (vm.getType()) { + case User: + try { + _accountMgr.checkAccess(accountObj, null, true, vm); + } catch (PermissionDeniedException ex) { + if (_accountMgr.isNormalUser(accountObj.getId())) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("VM access is denied. VM owner account " + vm.getAccountId() + " does not match the account id in session " + + accountObj.getId() + " and caller is a normal user"); + } + } else if (_accountMgr.isDomainAdmin(accountObj.getId()) + || accountObj.getType() == Account.ACCOUNT_TYPE_READ_ONLY_ADMIN) { + if(s_logger.isDebugEnabled()) { + s_logger.debug("VM access is denied. VM owner account " + vm.getAccountId() + + " does not match the account id in session " + accountObj.getId() + " and the domain-admin caller does not manage the target domain"); + } + } + return false; + } + break; + + case DomainRouter: + case ConsoleProxy: + case SecondaryStorageVm: + return false; + + default: + s_logger.warn("Unrecoginized virtual machine type, deny access by default. type: " + vm.getType()); + return false; + } + + return true; + } + + // This is copied from ConsoleProxyServlet + public boolean verifyUser(Long userId) { + User user = _accountMgr.getUserIncludingRemoved(userId); + Account account = null; + if (user != null) { + account = _accountMgr.getAccount(user.getAccountId()); + } + + if ((user == null) || (user.getRemoved() != null) || !user.getState().equals(Account.State.enabled) || (account == null) || + !account.getState().equals(Account.State.enabled)) { + s_logger.warn("Deleted/Disabled/Locked user with id=" + userId + " attempting to access public API"); + return false; + } + return true; + } + + // This is copied from ConsoleProxyServlet + private boolean verifyRequest(Map requestParameters) { + try { + String apiKey = null; + String secretKey = null; + String signature = null; + String unsignedRequest = null; + + List parameterNames = new ArrayList(); + + for (Object paramNameObj : requestParameters.keySet()) { + parameterNames.add((String)paramNameObj); // put the name in a list that we'll sort later + } + + Collections.sort(parameterNames); + + for (String paramName : parameterNames) { + String paramValue = ((String[])requestParameters.get(paramName))[0]; + + if ("signature".equalsIgnoreCase(paramName)) { + signature = paramValue; + } else { + if ("apikey".equalsIgnoreCase(paramName)) { + apiKey = paramValue; + } + + if (unsignedRequest == null) { + unsignedRequest = paramName + "=" + URLEncoder.encode(paramValue, "UTF-8").replaceAll("\\+", "%20"); + } else { + unsignedRequest = unsignedRequest + "&" + paramName + "=" + URLEncoder.encode(paramValue, "UTF-8").replaceAll("\\+", "%20"); + } + } + } + + if ((signature == null) || (apiKey == null)) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("expired session, missing signature, or missing apiKey -- ignoring request...sig: " + signature + ", apiKey: " + apiKey); + } + return false; // no signature, bad request + } + + TransactionLegacy txn = TransactionLegacy.open(TransactionLegacy.CLOUD_DB); + txn.close(); + User user = null; + Pair userAcctPair = _accountMgr.findUserByApiKey(apiKey); + if (userAcctPair == null) { + s_logger.debug("apiKey does not map to a valid user -- ignoring request, apiKey: " + apiKey); + return false; + } + + user = userAcctPair.first(); + Account account = userAcctPair.second(); + + if (!user.getState().equals(Account.State.enabled) || !account.getState().equals(Account.State.enabled)) { + s_logger.debug("disabled or locked user accessing the api, userid = " + user.getId() + "; name = " + user.getUsername() + "; state: " + user.getState() + + "; accountState: " + account.getState()); + return false; + } + + secretKey = user.getSecretKey(); + if (secretKey == null) { + s_logger.debug("User does not have a secret key associated with the account -- ignoring request, username: " + user.getUsername()); + return false; + } + + unsignedRequest = unsignedRequest.toLowerCase(); + + Mac mac = Mac.getInstance("HmacSHA1"); + SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(), "HmacSHA1"); + mac.init(keySpec); + mac.update(unsignedRequest.getBytes()); + byte[] encryptedBytes = mac.doFinal(); + String computedSignature = Base64.encodeBase64String(encryptedBytes); + boolean equalSig = ConstantTimeComparator.compareStrings(signature, computedSignature); + if (!equalSig) { + s_logger.debug("User signature: " + signature + " is not equaled to computed signature: " + computedSignature); + } + + if (equalSig) { + requestParameters.put("userid", new Object[] {String.valueOf(user.getId())}); + requestParameters.put("account", new Object[] {account.getAccountName()}); + requestParameters.put("accountobj", new Object[] {account}); + } + return equalSig; + } catch (Exception ex) { + s_logger.error("unable to verifty request signature", ex); + } + return false; + } + + +} diff --git a/server/src/test/java/com/cloud/servlet/NoVncConsoleProxyEncryptorTest.java b/server/src/test/java/com/cloud/servlet/NoVncConsoleProxyEncryptorTest.java new file mode 100644 index 000000000000..a4549c64d6cc --- /dev/null +++ b/server/src/test/java/com/cloud/servlet/NoVncConsoleProxyEncryptorTest.java @@ -0,0 +1,222 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package com.cloud.servlet; + +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.binary.Hex; +import org.bouncycastle.crypto.engines.RijndaelEngine; +import org.bouncycastle.crypto.modes.CBCBlockCipher; +import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.bouncycastle.crypto.paddings.ZeroBytePadding; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; + +import java.security.spec.KeySpec; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; + +public class NoVncConsoleProxyEncryptorTest { + + @Test + public void testRijndael() throws Exception { + byte[] key = "default.key.for.novnc".getBytes(Charset.forName("ASCII")); + byte[] iv = "default.iv.for.novnc".getBytes(Charset.forName("ASCII")); + byte[] plaintext = "text-to-encode-with-rijndael-ecb".getBytes(Charset.forName("UTF-8")); + + PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher( + new RijndaelEngine(256), new ZeroBytePadding()); + + final int keysize = 256; + byte[] keyData = new byte[keysize / Byte.SIZE]; + System.arraycopy(key, 0, keyData, 0, Math.min(key.length, keyData.length)); + KeyParameter keyParameter = new KeyParameter(keyData); + + cipher.init(true, keyParameter); + byte[] encrypted = new byte[cipher.getOutputSize(plaintext.length)]; + int offset = cipher.processBytes(plaintext, 0, plaintext.length, encrypted, 0); + cipher.doFinal(encrypted, offset); + System.out.println("encrypted ECB is " + Base64.encodeBase64String(encrypted)); + + cipher.reset(); + cipher.init(false, keyParameter); + byte[] decryptedtext = new byte[cipher.getOutputSize(encrypted.length)]; + offset = cipher.processBytes(encrypted, 0, encrypted.length, decryptedtext, 0); + offset += cipher.doFinal(decryptedtext, offset); + byte[] result = new byte[offset]; + System.arraycopy(decryptedtext, 0, result, 0, offset); + + System.out.println("decrypted ECB is " + new String(result)); + Assert.assertEquals(new String(result), "text-to-encode-with-rijndael-ecb"); + + } + + @Test + public void testRijndaelCBC() throws Exception { + byte[] key = "default.key.for.novnc".getBytes(Charset.forName("ASCII")); + byte[] iv = "default.iv.for.novnc".getBytes(Charset.forName("ASCII")); + byte[] plaintext = "text-to-encode-with-rijndael-cbc".getBytes(Charset.forName("UTF-8")); + + PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher( + new CBCBlockCipher(new RijndaelEngine(256)), new ZeroBytePadding()); + + final int keysize = 256; + byte[] keyData = new byte[keysize / Byte.SIZE]; + System.arraycopy(key, 0, keyData, 0, Math.min(key.length, keyData.length)); + KeyParameter keyParameter = new KeyParameter(keyData); + + byte[] ivData = new byte[keysize / Byte.SIZE]; + System.arraycopy(iv, 0, ivData, 0, Math.min(iv.length, ivData.length)); + ParametersWithIV ivAndKey = new ParametersWithIV(keyParameter, ivData); + + cipher.init(true, ivAndKey); + byte[] encrypted = new byte[cipher.getOutputSize(plaintext.length)]; + int offset = cipher.processBytes(plaintext, 0, plaintext.length, encrypted, 0); + cipher.doFinal(encrypted, offset); + System.out.println("encrypted CBC is " + Base64.encodeBase64String(encrypted)); + + cipher.reset(); + cipher.init(false, ivAndKey); + byte[] decryptedtext = new byte[cipher.getOutputSize(encrypted.length)]; + offset = cipher.processBytes(encrypted, 0, encrypted.length, decryptedtext, 0); + offset += cipher.doFinal(decryptedtext, offset); + byte[] result = new byte[offset]; + System.arraycopy(decryptedtext, 0, result, 0, offset); + + System.out.println("decrypted CBC is " + new String(result)); + Assert.assertEquals(new String(result), "text-to-encode-with-rijndael-cbc"); + } + + @Test + public void testEncrypt() throws Exception { + NoVncConsoleProxyClientParam param = new NoVncConsoleProxyClientParam(); + param.setProxy("9.9.9.9"); + param.setVmName("testvm"); + param.setClientHostAddress("10.10.10.10"); + param.setClientHostPort(5901); + param.setClientHostPassword("vnc_password"); + param.setClientIp("11.11.11.11"); + param.setClientTag("7f304181-3242-4867-9a26-106063139cba"); + + String key = "default.key.for.novnc"; + String iv = "default.iv.for.novnc"; + String host = param.getClientHostAddress(); + int port = param.getClientHostPort(); + String password = param.getClientHostPassword(); + String clientIp = param.getClientIp(); + long timestamp = System.currentTimeMillis() / 1000; + String proxy = param.getProxy(); + String vmName = param.getVmName(); + + final StringBuilder data = new StringBuilder(); + data.append(host).append(":").append(port).append(":").append(clientIp).append(":").append(timestamp); + + String dataHash = NoVncConsoleProxyEncryptor.hash(key + data.toString()); + data.append("|").append(dataHash); + + System.out.println("data is " + data); + + // Rijndael ECB + NoVncConsoleProxyEncryptor encryptor = new NoVncConsoleProxyEncryptor(key, iv); + byte[] encrypted = encryptor.encryptText(data.toString()); + String encryptedText = Hex.encodeHexString(encrypted); + System.out.println("encrypted text is " + encryptedText); + String decrypted = encryptor.decryptText(encrypted); + System.out.println("decrypted is " + decrypted); + Assert.assertEquals(decrypted, data.toString()); + + // Rijndael + CBC + byte[] encryptedCBC = encryptor.encryptTextCBC(data.toString()); + String encryptedTextCBC = Hex.encodeHexString(encryptedCBC); + System.out.println("encryptedCBC text is " + encryptedTextCBC); + String decryptedCBC = encryptor.decryptTextCBC(encryptedCBC); + System.out.println("decryptedCBC is " + decryptedCBC); + Assert.assertEquals(decryptedCBC, data.toString()); + + Map input = new HashMap(); + input.put("port", "8080"); + input.put("host", proxy); + input.put("password", password); + input.put("path", encryptedText); + input.put("displayName", vmName); + + System.out.println("url is " + NoVncConsoleProxyEncryptor.httpBuildQuery(input)); + + } + + @Test + public void testDESencryption() throws Exception { + String password = "6YywcQKBEolQ4irUL_izbA"; + byte[] challenge = {(byte)222,118,(byte)180,(byte)183,(byte)234,(byte)141,48,(byte)249,(byte)182,80,(byte)234,(byte)248,(byte)218,105,(byte)234,(byte)137}; + byte[] response; + try { + System.out.println("challenge is " + Arrays.toString(challenge)); + response = encodePassword(challenge, password); + System.out.println("response is " + Arrays.toString(response)); + } catch (Exception e) { + throw e; + } + } + + private byte[] encodePassword(byte[] challenge, String password) throws Exception { + // VNC password consist of up to eight ASCII characters. + byte[] key = {0, 0, 0, 0, 0, 0, 0, 0}; // Padding + System.out.println("key is " + Arrays.toString(key)); + + byte[] passwordAsciiBytes = password.getBytes(Charset.forName("ASCII")); + System.arraycopy(passwordAsciiBytes, 0, key, 0, Math.min(password.length(), 8)); + System.out.println("key is " + Arrays.toString(key)); + + // Flip bytes (reverse bits) in key + for (int i = 0; i < key.length; i++) { + key[i] = flipByte(key[i]); + } + System.out.println("key is " + Arrays.toString(key)); + + KeySpec desKeySpec = new DESKeySpec(key); + SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("DES"); + SecretKey secretKey = secretKeyFactory.generateSecret(desKeySpec); + Cipher cipher = Cipher.getInstance("DES/ECB/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + + byte[] response = cipher.doFinal(challenge); + return response; + } + private static byte flipByte(byte b) { + int b1_8 = (b & 0x1) << 7; + int b2_7 = (b & 0x2) << 5; + int b3_6 = (b & 0x4) << 3; + int b4_5 = (b & 0x8) << 1; + int b5_4 = (b & 0x10) >>> 1; + int b6_3 = (b & 0x20) >>> 3; + int b7_2 = (b & 0x40) >>> 5; + int b8_1 = (b & 0x80) >>> 7; + byte c = (byte)(b1_8 | b2_7 | b3_6 | b4_5 | b5_4 | b6_3 | b7_2 | b8_1); + return c; + } + +} diff --git a/systemvm/agent/scripts/noVNC_daemons.sh b/systemvm/agent/scripts/noVNC_daemons.sh new file mode 100644 index 000000000000..b95dedef190d --- /dev/null +++ b/systemvm/agent/scripts/noVNC_daemons.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +isNumpyInstalled=$(dpkg -l |grep -w python-numpy) +if [ "$isNumpyInstalled" == "" ]; then + apt-get update + apt-get install -y python-numpy --force-yes +fi + +python /root/noVNC/utils/websockify/websockify/websocketproxy.py --web=/root/noVNC --log-file=/var/log/novnc.log --daemon 8080 + +DATA="echo \'\'" +/usr/bin/nohup /usr/bin/socat -T 1 TCP-L:843,reuseaddr,fork,crlf SYSTEM:"$DATA" 2> /var/log/socat.err & diff --git a/systemvm/agent/scripts/noVNC_daemons_ssl.sh b/systemvm/agent/scripts/noVNC_daemons_ssl.sh new file mode 100644 index 000000000000..873701d50387 --- /dev/null +++ b/systemvm/agent/scripts/noVNC_daemons_ssl.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +isNumpyInstalled=$(dpkg -l |grep -w python-numpy) +if [ "$isNumpyInstalled" == "" ]; then + apt-get update + apt-get install -y python-numpy --force-yes +fi + +python /root/noVNC/utils/websockify/websockify/websocketproxy.py --ssl-only --web=/root/noVNC --log-file=/var/log/novnc.log --daemon --cert=/usr/local/cloud/systemvm/certs/customssl.crt --key=/usr/local/cloud/systemvm/certs/customssl.key 8080 + +DATA="echo \'\'" +/usr/bin/nohup /usr/bin/socat -T 1 TCP-L:843,reuseaddr,fork,crlf SYSTEM:"$DATA" 2> /var/log/socat.err & diff --git a/systemvm/debian/etc/iptables/iptables-consoleproxy b/systemvm/debian/etc/iptables/iptables-consoleproxy index 9a1c9855eedc..7f364f2fa1a9 100644 --- a/systemvm/debian/etc/iptables/iptables-consoleproxy +++ b/systemvm/debian/etc/iptables/iptables-consoleproxy @@ -35,4 +35,6 @@ COMMIT -A INPUT -i eth1 -p tcp -m state --state NEW -m tcp --dport 8001 -j ACCEPT -A INPUT -i eth2 -p tcp -m state --state NEW -m tcp --dport 443 -j ACCEPT -A INPUT -i eth2 -p tcp -m state --state NEW -m tcp --dport 80 -j ACCEPT +-A INPUT -i eth2 -p tcp -m state --state NEW -m tcp --dport 8080 -j ACCEPT +-A INPUT -i eth2 -p tcp -m state --state NEW -m tcp --dport 843 -j ACCEPT COMMIT diff --git a/systemvm/debian/etc/logrotate.d/rsyslog b/systemvm/debian/etc/logrotate.d/rsyslog index 9291494481e8..dd7b785839da 100644 --- a/systemvm/debian/etc/logrotate.d/rsyslog +++ b/systemvm/debian/etc/logrotate.d/rsyslog @@ -21,6 +21,7 @@ /var/log/lpr.log /var/log/cron.log /var/log/debug +/var/log/novnc.log /var/log/messages { rotate 10 diff --git a/systemvm/debian/root/noVNC/.eslintignore b/systemvm/debian/root/noVNC/.eslintignore new file mode 100644 index 000000000000..d38162800ad7 --- /dev/null +++ b/systemvm/debian/root/noVNC/.eslintignore @@ -0,0 +1 @@ +**/xtscancodes.js diff --git a/systemvm/debian/root/noVNC/.eslintrc b/systemvm/debian/root/noVNC/.eslintrc new file mode 100644 index 000000000000..900a7186efc4 --- /dev/null +++ b/systemvm/debian/root/noVNC/.eslintrc @@ -0,0 +1,48 @@ +{ + "env": { + "browser": true, + "es6": true + }, + "parserOptions": { + "sourceType": "module" + }, + "extends": "eslint:recommended", + "rules": { + // Unsafe or confusing stuff that we forbid + + "no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": true }], + "no-constant-condition": ["error", { "checkLoops": false }], + "no-var": "error", + "no-useless-constructor": "error", + "object-shorthand": ["error", "methods", { "avoidQuotes": true }], + "prefer-arrow-callback": "error", + "arrow-body-style": ["error", "as-needed", { "requireReturnForObjectLiteral": false } ], + "arrow-parens": ["error", "as-needed", { "requireForBlockBody": true }], + "arrow-spacing": ["error"], + "no-confusing-arrow": ["error", { "allowParens": true }], + + // Enforced coding style + + "brace-style": ["error", "1tbs", { "allowSingleLine": true }], + "indent": ["error", 4, { "SwitchCase": 1, + "CallExpression": { "arguments": "first" }, + "ArrayExpression": "first", + "ObjectExpression": "first", + "ignoreComments": true }], + "comma-spacing": ["error"], + "comma-style": ["error"], + "curly": ["error", "multi-line"], + "func-call-spacing": ["error"], + "func-names": ["error"], + "func-style": ["error", "declaration", { "allowArrowFunctions": true }], + "key-spacing": ["error"], + "keyword-spacing": ["error"], + "no-trailing-spaces": ["error"], + "semi": ["error"], + "space-before-blocks": ["error"], + "space-before-function-paren": ["error", { "anonymous": "always", + "named": "never", + "asyncArrow": "always" }], + "switch-colon-spacing": ["error"], + } +} diff --git a/systemvm/debian/root/noVNC/.github/ISSUE_TEMPLATE/bug_report.md b/systemvm/debian/root/noVNC/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000000..94ac6f8dc6ec --- /dev/null +++ b/systemvm/debian/root/noVNC/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,34 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Client (please complete the following information):** + - OS: [e.g. iOS] + - Browser: [e.g. chrome, safari] + - Browser version: [e.g. 22] + +**Server (please complete the following information):** + - noVNC version: [e.g. 1.0.0 or git commit id] + - VNC server: [e.g. QEMU, TigerVNC] + - WebSocket proxy: [e.g. websockify] + +**Additional context** +Add any other context about the problem here. diff --git a/systemvm/debian/root/noVNC/.github/ISSUE_TEMPLATE/feature_request.md b/systemvm/debian/root/noVNC/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000000..066b2d920a28 --- /dev/null +++ b/systemvm/debian/root/noVNC/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/systemvm/debian/root/noVNC/.gitignore b/systemvm/debian/root/noVNC/.gitignore new file mode 100644 index 000000000000..5e74494830af --- /dev/null +++ b/systemvm/debian/root/noVNC/.gitignore @@ -0,0 +1,12 @@ +*.pyc +*.o +tests/data_*.js +#utils/rebind.so +#utils/websockify +/node_modules +/build +/lib +recordings +*.swp +*~ +noVNC-*.tgz diff --git a/systemvm/debian/root/noVNC/.gitmodules b/systemvm/debian/root/noVNC/.gitmodules new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/systemvm/debian/root/noVNC/.travis.yml b/systemvm/debian/root/noVNC/.travis.yml new file mode 100644 index 000000000000..78b521a80ba7 --- /dev/null +++ b/systemvm/debian/root/noVNC/.travis.yml @@ -0,0 +1,58 @@ +language: node_js +sudo: false +cache: + directories: + - node_modules +node_js: + - 6 +env: + matrix: + - TEST_BROWSER_NAME=chrome TEST_BROWSER_OS='Windows 10' +# FIXME Skip tests in Linux since Sauce Labs browser versions are ancient. +# - TEST_BROWSER_NAME=chrome TEST_BROWSER_OS='Linux' + - TEST_BROWSER_NAME=chrome TEST_BROWSER_OS='OS X 10.11' + - TEST_BROWSER_NAME=firefox TEST_BROWSER_OS='Windows 10' +# - TEST_BROWSER_NAME=firefox TEST_BROWSER_OS='Linux' + - TEST_BROWSER_NAME=firefox TEST_BROWSER_OS='OS X 10.11' + - TEST_BROWSER_NAME='internet explorer' TEST_BROWSER_OS='Windows 10' + - TEST_BROWSER_NAME='internet explorer' TEST_BROWSER_OS='Windows 7' + - TEST_BROWSER_NAME=microsoftedge TEST_BROWSER_OS='Windows 10' + - TEST_BROWSER_NAME=safari TEST_BROWSER_OS='OS X 10.13' +before_script: npm install -g karma-cli +addons: + sauce_connect: + username: "directxman12" + jwt: + secure: "d3ekMYslpn6R4f0ajtRMt9SUFmNGDiItHpqaXC5T4KI0KMEsxgvEOfJot5PiFFJWg1DSpJZH6oaW2UxGZ3duJLZrXIEd/JePY8a6NtT35BNgiDPgcp+eu2Bu3rhrSNg7/HEsD1ma+JeUTnv18Ai5oMFfCCQJx2J6osIxyl/ZVxA=" +stages: +- lint +- test +- name: deploy + if: tag is PRESENT +jobs: + include: + - stage: lint + env: + addons: + before_script: + script: npm run lint + - + env: + addons: + before_script: + script: git ls-tree --name-only -r HEAD | grep -E "[.](html|css)$" | xargs ./utils/validate + - stage: deploy + env: + addons: + script: skip + before_script: skip + deploy: + provider: npm + email: ossman@cendio.se + api_key: + secure: "Qq2Mi9xQawO2zlAigzshzMu2QMHvu1IaN9l0ZIivE99wHJj7eS5f4miJ9wB+/mWRRgb3E8uj9ZRV24+Oc36drlBTU9sz+lHhH0uFMfAIseceK64wZV9sLAZm472fmPp2xdUeTCCqPaRy7g1XBqiJ0LyZvEFLsRijqcLjPBF+b8w=" + on: + tags: true + repo: novnc/noVNC + + diff --git a/systemvm/debian/root/noVNC/AUTHORS b/systemvm/debian/root/noVNC/AUTHORS new file mode 100644 index 000000000000..dec0e89329a2 --- /dev/null +++ b/systemvm/debian/root/noVNC/AUTHORS @@ -0,0 +1,13 @@ +maintainers: +- Joel Martin (@kanaka) +- Solly Ross (@directxman12) +- Samuel Mannehed for Cendio AB (@samhed) +- Pierre Ossman for Cendio AB (@CendioOssman) +maintainersEmeritus: +- @astrand +contributors: +# There are a bunch of people that should be here. +# If you want to be on this list, feel free send a PR +# to add yourself. +- jalf +- NTT corp. diff --git a/systemvm/debian/root/noVNC/LICENSE.txt b/systemvm/debian/root/noVNC/LICENSE.txt new file mode 100644 index 000000000000..20f3eb025fe4 --- /dev/null +++ b/systemvm/debian/root/noVNC/LICENSE.txt @@ -0,0 +1,68 @@ +noVNC is Copyright (C) 2018 The noVNC Authors +(./AUTHORS) + +The noVNC core library files are licensed under the MPL 2.0 (Mozilla +Public License 2.0). The noVNC core library is composed of the +Javascript code necessary for full noVNC operation. This includes (but +is not limited to): + + core/**/*.js + app/*.js + test/playback.js + +The HTML, CSS, font and images files that included with the noVNC +source distibution (or repository) are not considered part of the +noVNC core library and are licensed under more permissive licenses. +The intent is to allow easy integration of noVNC into existing web +sites and web applications. + +The HTML, CSS, font and image files are licensed as follows: + + *.html : 2-Clause BSD license + + app/styles/*.css : 2-Clause BSD license + + app/styles/Orbitron* : SIL Open Font License 1.1 + (Copyright 2009 Matt McInerney) + + app/images/ : Creative Commons Attribution-ShareAlike + http://creativecommons.org/licenses/by-sa/3.0/ + +Some portions of noVNC are copyright to their individual authors. +Please refer to the individual source files and/or to the noVNC commit +history: https://github.com/novnc/noVNC/commits/master + +The are several files and projects that have been incorporated into +the noVNC core library. Here is a list of those files and the original +licenses (all MPL 2.0 compatible): + + core/base64.js : MPL 2.0 + + core/des.js : Various BSD style licenses + + vendor/pako/ : MIT + + vendor/browser-es-module-loader/src/ : MIT + + vendor/browser-es-module-loader/dist/ : Various BSD style licenses + + vendor/promise.js : MIT + +Any other files not mentioned above are typically marked with +a copyright/license header at the top of the file. The default noVNC +license is MPL-2.0. + +The following license texts are included: + + docs/LICENSE.MPL-2.0 + docs/LICENSE.OFL-1.1 + docs/LICENSE.BSD-3-Clause (New BSD) + docs/LICENSE.BSD-2-Clause (Simplified BSD / FreeBSD) + vendor/pako/LICENSE (MIT) + +Or alternatively the license texts may be found here: + + http://www.mozilla.org/MPL/2.0/ + http://scripts.sil.org/OFL + http://en.wikipedia.org/wiki/BSD_licenses + https://opensource.org/licenses/MIT diff --git a/systemvm/debian/root/noVNC/README.md b/systemvm/debian/root/noVNC/README.md new file mode 100644 index 000000000000..566b8e4f5afa --- /dev/null +++ b/systemvm/debian/root/noVNC/README.md @@ -0,0 +1,152 @@ +## noVNC: HTML VNC Client Library and Application + +[![Build Status](https://travis-ci.org/novnc/noVNC.svg?branch=master)](https://travis-ci.org/novnc/noVNC) + +### Description + +noVNC is both a HTML VNC client JavaScript library and an application built on +top of that library. noVNC runs well in any modern browser including mobile +browsers (iOS and Android). + +Many companies, projects and products have integrated noVNC including +[OpenStack](http://www.openstack.org), +[OpenNebula](http://opennebula.org/), +[LibVNCServer](http://libvncserver.sourceforge.net), and +[ThinLinc](https://cendio.com/thinlinc). See +[the Projects and Companies wiki page](https://github.com/novnc/noVNC/wiki/Projects-and-companies-using-noVNC) +for a more complete list with additional info and links. + +### Table of Contents + +- [News/help/contact](#newshelpcontact) +- [Features](#features) +- [Screenshots](#screenshots) +- [Browser Requirements](#browser-requirements) +- [Server Requirements](#server-requirements) +- [Quick Start](#quick-start) +- [Integration and Deployment](#integration-and-deployment) +- [Authors/Contributors](#authorscontributors) + +### News/help/contact + +The project website is found at [novnc.com](http://novnc.com). +Notable commits, announcements and news are posted to +[@noVNC](http://www.twitter.com/noVNC). + +If you are a noVNC developer/integrator/user (or want to be) please join the +[noVNC discussion group](https://groups.google.com/forum/?fromgroups#!forum/novnc). + +Bugs and feature requests can be submitted via +[github issues](https://github.com/novnc/noVNC/issues). If you have questions +about using noVNC then please first use the +[discussion group](https://groups.google.com/forum/?fromgroups#!forum/novnc). +We also have a [wiki](https://github.com/novnc/noVNC/wiki/) with lots of +helpful information. + +If you are looking for a place to start contributing to noVNC, a good place to +start would be the issues that are marked as +["patchwelcome"](https://github.com/novnc/noVNC/issues?labels=patchwelcome). +Please check our +[contribution guide](https://github.com/novnc/noVNC/wiki/Contributing) though. + +If you want to show appreciation for noVNC you could donate to a great non- +profits such as: +[Compassion International](http://www.compassion.com/), +[SIL](http://www.sil.org), +[Habitat for Humanity](http://www.habitat.org), +[Electronic Frontier Foundation](https://www.eff.org/), +[Against Malaria Foundation](http://www.againstmalaria.com/), +[Nothing But Nets](http://www.nothingbutnets.net/), etc. +Please tweet [@noVNC](http://www.twitter.com/noVNC) if you do. + + +### Features + +* Supports all modern browsers including mobile (iOS, Android) +* Supported VNC encodings: raw, copyrect, rre, hextile, tight, tightPNG +* Supports scaling, clipping and resizing the desktop +* Local cursor rendering +* Clipboard copy/paste +* Translations +* Licensed mainly under the [MPL 2.0](http://www.mozilla.org/MPL/2.0/), see + [the license document](LICENSE.txt) for details + +### Screenshots + +Running in Firefox before and after connecting: + +  + + +See more screenshots +[here](http://novnc.com/screenshots.html). + + +### Browser Requirements + +noVNC uses many modern web technologies so a formal requirement list is +not available. However these are the minimum versions we are currently +aware of: + +* Chrome 49, Firefox 44, Safari 10, Opera 36, IE 11, Edge 12 + + +### Server Requirements + +noVNC follows the standard VNC protocol, but unlike other VNC clients it does +require WebSockets support. Many servers include support (e.g. +[x11vnc/libvncserver](http://libvncserver.sourceforge.net/), +[QEMU](http://www.qemu.org/), and +[MobileVNC](http://www.smartlab.at/mobilevnc/)), but for the others you need to +use a WebSockets to TCP socket proxy. noVNC has a sister project +[websockify](https://github.com/novnc/websockify) that provides a simple such +proxy. + + +### Quick Start + +* Use the launch script to automatically download and start websockify, which + includes a mini-webserver and the WebSockets proxy. The `--vnc` option is + used to specify the location of a running VNC server: + + `./utils/launch.sh --vnc localhost:5901` + +* Point your browser to the cut-and-paste URL that is output by the launch + script. Hit the Connect button, enter a password if the VNC server has one + configured, and enjoy! + + +### Integration and Deployment + +Please see our other documents for how to integrate noVNC in your own software, +or deploying the noVNC application in production environments: + +* [Embedding](docs/EMBEDDING.md) - For the noVNC application +* [Library](docs/LIBRARY.md) - For the noVNC JavaScript library + + +### Authors/Contributors + +See [AUTHORS](AUTHORS) for a (full-ish) list of authors. If you're not on +that list and you think you should be, feel free to send a PR to fix that. + +* Core team: + * [Joel Martin](https://github.com/kanaka) + * [Samuel Mannehed](https://github.com/samhed) (Cendio) + * [Peter Åstrand](https://github.com/astrand) (Cendio) + * [Solly Ross](https://github.com/DirectXMan12) (Red Hat / OpenStack) + * [Pierre Ossman](https://github.com/CendioOssman) (Cendio) + +* Notable contributions: + * UI and Icons : Pierre Ossman, Chris Gordon + * Original Logo : Michael Sersen + * tight encoding : Michael Tinglof (Mercuri.ca) + +* Included libraries: + * base64 : Martijn Pieters (Digital Creations 2), Samuel Sieb (sieb.net) + * DES : Dave Zimmerman (Widget Workshop), Jef Poskanzer (ACME Labs) + * Pako : Vitaly Puzrin (https://github.com/nodeca/pako) + +Do you want to be on this list? Check out our +[contribution guide](https://github.com/novnc/noVNC/wiki/Contributing) and +start hacking! diff --git a/systemvm/debian/root/noVNC/VERSION b/systemvm/debian/root/noVNC/VERSION new file mode 100644 index 000000000000..9084fa2f716a --- /dev/null +++ b/systemvm/debian/root/noVNC/VERSION @@ -0,0 +1 @@ +1.1.0 diff --git a/systemvm/debian/root/noVNC/app/error-handler.js b/systemvm/debian/root/noVNC/app/error-handler.js new file mode 100644 index 000000000000..8e294166fc65 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/error-handler.js @@ -0,0 +1,58 @@ +// 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. + +// 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."); + } + // Don't return true since this would prevent the error + // from being printed to the browser console. + return false; + } + window.addEventListener('error', function onerror(evt) { handleError(evt, evt.error); }); + window.addEventListener('unhandledrejection', function onreject(evt) { handleError(evt.reason, evt.reason); }); +})(); diff --git a/systemvm/debian/root/noVNC/app/images/alt.svg b/systemvm/debian/root/noVNC/app/images/alt.svg new file mode 100644 index 000000000000..e5bb4612ece5 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/alt.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/clipboard.svg b/systemvm/debian/root/noVNC/app/images/clipboard.svg new file mode 100644 index 000000000000..79af27525048 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/clipboard.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/connect.svg b/systemvm/debian/root/noVNC/app/images/connect.svg new file mode 100644 index 000000000000..56cde414b460 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/connect.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/ctrl.svg b/systemvm/debian/root/noVNC/app/images/ctrl.svg new file mode 100644 index 000000000000..856e93958291 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/ctrl.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/ctrlaltdel.svg b/systemvm/debian/root/noVNC/app/images/ctrlaltdel.svg new file mode 100644 index 000000000000..d7744ea31da6 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/ctrlaltdel.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/disconnect.svg b/systemvm/debian/root/noVNC/app/images/disconnect.svg new file mode 100644 index 000000000000..6be7d1876575 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/disconnect.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/drag.svg b/systemvm/debian/root/noVNC/app/images/drag.svg new file mode 100644 index 000000000000..139caf947cd1 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/drag.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/error.svg b/systemvm/debian/root/noVNC/app/images/error.svg new file mode 100644 index 000000000000..8356d3f13749 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/error.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/esc.svg b/systemvm/debian/root/noVNC/app/images/esc.svg new file mode 100644 index 000000000000..830152b5f930 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/esc.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/expander.svg b/systemvm/debian/root/noVNC/app/images/expander.svg new file mode 100644 index 000000000000..e1635358be9c --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/expander.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/fullscreen.svg b/systemvm/debian/root/noVNC/app/images/fullscreen.svg new file mode 100644 index 000000000000..29bd05da14dd --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/fullscreen.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/handle.svg b/systemvm/debian/root/noVNC/app/images/handle.svg new file mode 100644 index 000000000000..4a7a126f9d3b --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/handle.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/handle_bg.svg b/systemvm/debian/root/noVNC/app/images/handle_bg.svg new file mode 100644 index 000000000000..7579c42cb78c --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/handle_bg.svg @@ -0,0 +1,172 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/icons/Makefile b/systemvm/debian/root/noVNC/app/images/icons/Makefile new file mode 100644 index 000000000000..be564b43b93e --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/icons/Makefile @@ -0,0 +1,42 @@ +ICONS := \ + novnc-16x16.png \ + novnc-24x24.png \ + novnc-32x32.png \ + novnc-48x48.png \ + novnc-64x64.png + +ANDROID_LAUNCHER := \ + novnc-48x48.png \ + novnc-72x72.png \ + novnc-96x96.png \ + novnc-144x144.png \ + novnc-192x192.png + +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: $(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 "$<" "$@" + +novnc-%.png: novnc-icon.svg + convert -density $$[`echo $* | cut -d x -f 1` * 90 / 48] \ + -background transparent "$<" "$@" + +clean: + rm -f *.png diff --git a/systemvm/debian/root/noVNC/app/images/icons/novnc-120x120.png b/systemvm/debian/root/noVNC/app/images/icons/novnc-120x120.png new file mode 100644 index 000000000000..40823efbadf2 Binary files /dev/null and b/systemvm/debian/root/noVNC/app/images/icons/novnc-120x120.png differ diff --git a/systemvm/debian/root/noVNC/app/images/icons/novnc-144x144.png b/systemvm/debian/root/noVNC/app/images/icons/novnc-144x144.png new file mode 100644 index 000000000000..eee71f11c74f Binary files /dev/null and b/systemvm/debian/root/noVNC/app/images/icons/novnc-144x144.png differ diff --git a/systemvm/debian/root/noVNC/app/images/icons/novnc-152x152.png b/systemvm/debian/root/noVNC/app/images/icons/novnc-152x152.png new file mode 100644 index 000000000000..0694b2de39b8 Binary files /dev/null and b/systemvm/debian/root/noVNC/app/images/icons/novnc-152x152.png differ diff --git a/systemvm/debian/root/noVNC/app/images/icons/novnc-16x16.png b/systemvm/debian/root/noVNC/app/images/icons/novnc-16x16.png new file mode 100644 index 000000000000..42108f409990 Binary files /dev/null and b/systemvm/debian/root/noVNC/app/images/icons/novnc-16x16.png differ diff --git a/systemvm/debian/root/noVNC/app/images/icons/novnc-192x192.png b/systemvm/debian/root/noVNC/app/images/icons/novnc-192x192.png new file mode 100644 index 000000000000..ef9201f4370f Binary files /dev/null and b/systemvm/debian/root/noVNC/app/images/icons/novnc-192x192.png differ diff --git a/systemvm/debian/root/noVNC/app/images/icons/novnc-24x24.png b/systemvm/debian/root/noVNC/app/images/icons/novnc-24x24.png new file mode 100644 index 000000000000..110613594b8e Binary files /dev/null and b/systemvm/debian/root/noVNC/app/images/icons/novnc-24x24.png differ diff --git a/systemvm/debian/root/noVNC/app/images/icons/novnc-32x32.png b/systemvm/debian/root/noVNC/app/images/icons/novnc-32x32.png new file mode 100644 index 000000000000..ff00dc305a75 Binary files /dev/null and b/systemvm/debian/root/noVNC/app/images/icons/novnc-32x32.png differ diff --git a/systemvm/debian/root/noVNC/app/images/icons/novnc-48x48.png b/systemvm/debian/root/noVNC/app/images/icons/novnc-48x48.png new file mode 100644 index 000000000000..f24cd6cc939a Binary files /dev/null and b/systemvm/debian/root/noVNC/app/images/icons/novnc-48x48.png differ diff --git a/systemvm/debian/root/noVNC/app/images/icons/novnc-60x60.png b/systemvm/debian/root/noVNC/app/images/icons/novnc-60x60.png new file mode 100644 index 000000000000..06b0d609a0f7 Binary files /dev/null and b/systemvm/debian/root/noVNC/app/images/icons/novnc-60x60.png differ diff --git a/systemvm/debian/root/noVNC/app/images/icons/novnc-64x64.png b/systemvm/debian/root/noVNC/app/images/icons/novnc-64x64.png new file mode 100644 index 000000000000..6d0fb34181b4 Binary files /dev/null and b/systemvm/debian/root/noVNC/app/images/icons/novnc-64x64.png differ diff --git a/systemvm/debian/root/noVNC/app/images/icons/novnc-72x72.png b/systemvm/debian/root/noVNC/app/images/icons/novnc-72x72.png new file mode 100644 index 000000000000..23163a22d060 Binary files /dev/null and b/systemvm/debian/root/noVNC/app/images/icons/novnc-72x72.png differ diff --git a/systemvm/debian/root/noVNC/app/images/icons/novnc-76x76.png b/systemvm/debian/root/noVNC/app/images/icons/novnc-76x76.png new file mode 100644 index 000000000000..aef61c480430 Binary files /dev/null and b/systemvm/debian/root/noVNC/app/images/icons/novnc-76x76.png differ diff --git a/systemvm/debian/root/noVNC/app/images/icons/novnc-96x96.png b/systemvm/debian/root/noVNC/app/images/icons/novnc-96x96.png new file mode 100644 index 000000000000..1a77c53f4cb2 Binary files /dev/null and b/systemvm/debian/root/noVNC/app/images/icons/novnc-96x96.png differ diff --git a/systemvm/debian/root/noVNC/app/images/icons/novnc-icon-sm.svg b/systemvm/debian/root/noVNC/app/images/icons/novnc-icon-sm.svg new file mode 100644 index 000000000000..aa1c6f185bcb --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/icons/novnc-icon-sm.svg @@ -0,0 +1,163 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/icons/novnc-icon.svg b/systemvm/debian/root/noVNC/app/images/icons/novnc-icon.svg new file mode 100644 index 000000000000..1efff912d48f --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/icons/novnc-icon.svg @@ -0,0 +1,163 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/info.svg b/systemvm/debian/root/noVNC/app/images/info.svg new file mode 100644 index 000000000000..557b772ff728 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/info.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/keyboard.svg b/systemvm/debian/root/noVNC/app/images/keyboard.svg new file mode 100644 index 000000000000..137b350ab5de --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/keyboard.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/mouse_left.svg b/systemvm/debian/root/noVNC/app/images/mouse_left.svg new file mode 100644 index 000000000000..ce4cca41c790 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/mouse_left.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/mouse_middle.svg b/systemvm/debian/root/noVNC/app/images/mouse_middle.svg new file mode 100644 index 000000000000..6603425cb3e7 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/mouse_middle.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/mouse_none.svg b/systemvm/debian/root/noVNC/app/images/mouse_none.svg new file mode 100644 index 000000000000..3e0f838a77af --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/mouse_none.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/mouse_right.svg b/systemvm/debian/root/noVNC/app/images/mouse_right.svg new file mode 100644 index 000000000000..f4bad76797cb --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/mouse_right.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/power.svg b/systemvm/debian/root/noVNC/app/images/power.svg new file mode 100644 index 000000000000..4925d3e8ebaa --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/power.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/settings.svg b/systemvm/debian/root/noVNC/app/images/settings.svg new file mode 100644 index 000000000000..dbb2e80a5b3f --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/settings.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/tab.svg b/systemvm/debian/root/noVNC/app/images/tab.svg new file mode 100644 index 000000000000..1ccb3229cddb --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/tab.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/toggleextrakeys.svg b/systemvm/debian/root/noVNC/app/images/toggleextrakeys.svg new file mode 100644 index 000000000000..b578c0d40621 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/toggleextrakeys.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/warning.svg b/systemvm/debian/root/noVNC/app/images/warning.svg new file mode 100644 index 000000000000..7114f9b1235b --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/warning.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/systemvm/debian/root/noVNC/app/images/windows.svg b/systemvm/debian/root/noVNC/app/images/windows.svg new file mode 100644 index 000000000000..270405c7ff2a --- /dev/null +++ b/systemvm/debian/root/noVNC/app/images/windows.svg @@ -0,0 +1,85 @@ + + + +image/svg+xml + + + + + + + + + + \ No newline at end of file diff --git a/systemvm/debian/root/noVNC/app/locale/cs.json b/systemvm/debian/root/noVNC/app/locale/cs.json new file mode 100644 index 000000000000..589145ef36e8 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/locale/cs.json @@ -0,0 +1,71 @@ +{ + "Connecting...": "Připojení...", + "Disconnecting...": "Odpojení...", + "Reconnecting...": "Obnova připojení...", + "Internal error": "Vnitřní chyba", + "Must set host": "Hostitel musí být nastavení", + "Connected (encrypted) to ": "Připojení (šifrované) k ", + "Connected (unencrypted) to ": "Připojení (nešifrované) k ", + "Something went wrong, connection is closed": "Něco se pokazilo, odpojeno", + "Failed to connect to server": "Chyba připojení k serveru", + "Disconnected": "Odpojeno", + "New connection has been rejected with reason: ": "Nové připojení bylo odmítnuto s odůvodněním: ", + "New connection has been rejected": "Nové připojení bylo odmítnuto", + "Password is required": "Je vyžadováno heslo", + "noVNC encountered an error:": "noVNC narazilo na chybu:", + "Hide/Show the control bar": "Skrýt/zobrazit ovládací panel", + "Move/Drag Viewport": "Přesunout/přetáhnout výřez", + "viewport drag": "přesun výřezu", + "Active Mouse Button": "Aktivní tlačítka myši", + "No mousebutton": "Žádné", + "Left mousebutton": "Levé tlačítko myši", + "Middle mousebutton": "Prostřední tlačítko myši", + "Right mousebutton": "Pravé tlačítko myši", + "Keyboard": "Klávesnice", + "Show Keyboard": "Zobrazit klávesnici", + "Extra keys": "Extra klávesy", + "Show Extra Keys": "Zobrazit extra klávesy", + "Ctrl": "Ctrl", + "Toggle Ctrl": "Přepnout Ctrl", + "Alt": "Alt", + "Toggle Alt": "Přepnout Alt", + "Send Tab": "Odeslat tabulátor", + "Tab": "Tab", + "Esc": "Esc", + "Send Escape": "Odeslat Esc", + "Ctrl+Alt+Del": "Ctrl+Alt+Del", + "Send Ctrl-Alt-Del": "Poslat Ctrl-Alt-Del", + "Shutdown/Reboot": "Vypnutí/Restart", + "Shutdown/Reboot...": "Vypnutí/Restart...", + "Power": "Napájení", + "Shutdown": "Vypnout", + "Reboot": "Restart", + "Reset": "Reset", + "Clipboard": "Schránka", + "Clear": "Vymazat", + "Fullscreen": "Celá obrazovka", + "Settings": "Nastavení", + "Shared Mode": "Sdílený režim", + "View Only": "Pouze prohlížení", + "Clip to Window": "Přizpůsobit oknu", + "Scaling Mode:": "Přizpůsobení velikosti", + "None": "Žádné", + "Local Scaling": "Místní", + "Remote Resizing": "Vzdálené", + "Advanced": "Pokročilé", + "Repeater ID:": "ID opakovače", + "WebSocket": "WebSocket", + "Encrypt": "Šifrování:", + "Host:": "Hostitel:", + "Port:": "Port:", + "Path:": "Cesta", + "Automatic Reconnect": "Automatická obnova připojení", + "Reconnect Delay (ms):": "Zpoždění připojení (ms)", + "Show Dot when No Cursor": "Tečka místo chybějícího kurzoru myši", + "Logging:": "Logování:", + "Disconnect": "Odpojit", + "Connect": "Připojit", + "Password:": "Heslo", + "Send Password": "Odeslat heslo", + "Cancel": "Zrušit" +} \ No newline at end of file diff --git a/systemvm/debian/root/noVNC/app/locale/de.json b/systemvm/debian/root/noVNC/app/locale/de.json new file mode 100644 index 000000000000..62e73360f50f --- /dev/null +++ b/systemvm/debian/root/noVNC/app/locale/de.json @@ -0,0 +1,69 @@ +{ + "Connecting...": "Verbinden...", + "Disconnecting...": "Verbindung trennen...", + "Reconnecting...": "Verbindung wiederherstellen...", + "Internal error": "Interner Fehler", + "Must set host": "Richten Sie den Server ein", + "Connected (encrypted) to ": "Verbunden mit (verschlüsselt) ", + "Connected (unencrypted) to ": "Verbunden mit (unverschlüsselt) ", + "Something went wrong, connection is closed": "Etwas lief schief, Verbindung wurde getrennt", + "Disconnected": "Verbindung zum Server getrennt", + "New connection has been rejected with reason: ": "Verbindung wurde aus folgendem Grund abgelehnt: ", + "New connection has been rejected": "Verbindung wurde abgelehnt", + "Password is required": "Passwort ist erforderlich", + "noVNC encountered an error:": "Ein Fehler ist aufgetreten:", + "Hide/Show the control bar": "Kontrollleiste verstecken/anzeigen", + "Move/Drag Viewport": "Ansichtsfenster verschieben/ziehen", + "viewport drag": "Ansichtsfenster ziehen", + "Active Mouse Button": "Aktive Maustaste", + "No mousebutton": "Keine Maustaste", + "Left mousebutton": "Linke Maustaste", + "Middle mousebutton": "Mittlere Maustaste", + "Right mousebutton": "Rechte Maustaste", + "Keyboard": "Tastatur", + "Show Keyboard": "Tastatur anzeigen", + "Extra keys": "Zusatztasten", + "Show Extra Keys": "Zusatztasten anzeigen", + "Ctrl": "Strg", + "Toggle Ctrl": "Strg umschalten", + "Alt": "Alt", + "Toggle Alt": "Alt umschalten", + "Send Tab": "Tab senden", + "Tab": "Tab", + "Esc": "Esc", + "Send Escape": "Escape senden", + "Ctrl+Alt+Del": "Strg+Alt+Entf", + "Send Ctrl-Alt-Del": "Strg+Alt+Entf senden", + "Shutdown/Reboot": "Herunterfahren/Neustarten", + "Shutdown/Reboot...": "Herunterfahren/Neustarten...", + "Power": "Energie", + "Shutdown": "Herunterfahren", + "Reboot": "Neustarten", + "Reset": "Zurücksetzen", + "Clipboard": "Zwischenablage", + "Clear": "Löschen", + "Fullscreen": "Vollbild", + "Settings": "Einstellungen", + "Shared Mode": "Geteilter Modus", + "View Only": "Nur betrachten", + "Clip to Window": "Auf Fenster begrenzen", + "Scaling Mode:": "Skalierungsmodus:", + "None": "Keiner", + "Local Scaling": "Lokales skalieren", + "Remote Resizing": "Serverseitiges skalieren", + "Advanced": "Erweitert", + "Repeater ID:": "Repeater ID:", + "WebSocket": "WebSocket", + "Encrypt": "Verschlüsselt", + "Host:": "Server:", + "Port:": "Port:", + "Path:": "Pfad:", + "Automatic Reconnect": "Automatisch wiederverbinden", + "Reconnect Delay (ms):": "Wiederverbindungsverzögerung (ms):", + "Logging:": "Protokollierung:", + "Disconnect": "Verbindung trennen", + "Connect": "Verbinden", + "Password:": "Passwort:", + "Cancel": "Abbrechen", + "Canvas not supported.": "Canvas nicht unterstützt." +} \ No newline at end of file diff --git a/systemvm/debian/root/noVNC/app/locale/el.json b/systemvm/debian/root/noVNC/app/locale/el.json new file mode 100644 index 000000000000..f801251c59ad --- /dev/null +++ b/systemvm/debian/root/noVNC/app/locale/el.json @@ -0,0 +1,69 @@ +{ + "Connecting...": "Συνδέεται...", + "Disconnecting...": "Aποσυνδέεται...", + "Reconnecting...": "Επανασυνδέεται...", + "Internal error": "Εσωτερικό σφάλμα", + "Must set host": "Πρέπει να οριστεί ο διακομιστής", + "Connected (encrypted) to ": "Συνδέθηκε (κρυπτογραφημένα) με το ", + "Connected (unencrypted) to ": "Συνδέθηκε (μη κρυπτογραφημένα) με το ", + "Something went wrong, connection is closed": "Κάτι πήγε στραβά, η σύνδεση διακόπηκε", + "Disconnected": "Αποσυνδέθηκε", + "New connection has been rejected with reason: ": "Η νέα σύνδεση απορρίφθηκε διότι: ", + "New connection has been rejected": "Η νέα σύνδεση απορρίφθηκε ", + "Password is required": "Απαιτείται ο κωδικός πρόσβασης", + "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": "Δεξί Πλήκτρο Ποντικιού", + "Keyboard": "Πληκτρολόγιο", + "Show Keyboard": "Εμφάνιση Πληκτρολογίου", + "Extra keys": "Επιπλέον πλήκτρα", + "Show Extra Keys": "Εμφάνιση Επιπλέον Πλήκτρων", + "Ctrl": "Ctrl", + "Toggle Ctrl": "Εναλλαγή Ctrl", + "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...": "Κλείσιμο/Επανεκκίνηση...", + "Power": "Απενεργοποίηση", + "Shutdown": "Κλείσιμο", + "Reboot": "Επανεκκίνηση", + "Reset": "Επαναφορά", + "Clipboard": "Πρόχειρο", + "Clear": "Καθάρισμα", + "Fullscreen": "Πλήρης Οθόνη", + "Settings": "Ρυθμίσεις", + "Shared Mode": "Κοινόχρηστη Λειτουργία", + "View Only": "Μόνο Θέαση", + "Clip to Window": "Αποκοπή στο όριο του Παράθυρου", + "Scaling Mode:": "Λειτουργία Κλιμάκωσης:", + "None": "Καμία", + "Local Scaling": "Τοπική Κλιμάκωση", + "Remote Resizing": "Απομακρυσμένη Αλλαγή μεγέθους", + "Advanced": "Για προχωρημένους", + "Repeater ID:": "Repeater ID:", + "WebSocket": "WebSocket", + "Encrypt": "Κρυπτογράφηση", + "Host:": "Όνομα διακομιστή:", + "Port:": "Πόρτα διακομιστή:", + "Path:": "Διαδρομή:", + "Automatic Reconnect": "Αυτόματη επανασύνδεση", + "Reconnect Delay (ms):": "Καθυστέρηση επανασύνδεσης (ms):", + "Logging:": "Καταγραφή:", + "Disconnect": "Αποσύνδεση", + "Connect": "Σύνδεση", + "Password:": "Κωδικός Πρόσβασης:", + "Cancel": "Ακύρωση", + "Canvas not supported.": "Δεν υποστηρίζεται το στοιχείο Canvas" +} \ No newline at end of file diff --git a/systemvm/debian/root/noVNC/app/locale/es.json b/systemvm/debian/root/noVNC/app/locale/es.json new file mode 100644 index 000000000000..23f23f4972f3 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/locale/es.json @@ -0,0 +1,68 @@ +{ + "Connecting...": "Conectando...", + "Connected (encrypted) to ": "Conectado (con encriptación) a", + "Connected (unencrypted) to ": "Conectado (sin encriptación) a", + "Disconnecting...": "Desconectando...", + "Disconnected": "Desconectado", + "Must set host": "Debes configurar el host", + "Reconnecting...": "Reconectando...", + "Password is required": "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", + "Move/Drag Viewport": "Mover/Arrastrar la ventana", + "viewport drag": "Arrastrar la ventana", + "Active Mouse Button": "Botón activo del ratón", + "No mousebutton": "Ningún botón del ratón", + "Left mousebutton": "Botón izquierdo del ratón", + "Middle mousebutton": "Botón central del ratón", + "Right mousebutton": "Botón derecho del ratón", + "Keyboard": "Teclado", + "Show Keyboard": "Mostrar teclado", + "Extra keys": "Teclas adicionales", + "Show Extra Keys": "Mostrar Teclas Adicionales", + "Ctrl": "Ctrl", + "Toggle Ctrl": "Pulsar/Soltar Ctrl", + "Alt": "Alt", + "Toggle Alt": "Pulsar/Soltar Alt", + "Send Tab": "Enviar Tabulación", + "Tab": "Tabulación", + "Esc": "Esc", + "Send Escape": "Enviar Escape", + "Ctrl+Alt+Del": "Ctrl+Alt+Del", + "Send Ctrl-Alt-Del": "Enviar Ctrl+Alt+Del", + "Shutdown/Reboot": "Apagar/Reiniciar", + "Shutdown/Reboot...": "Apagar/Reiniciar...", + "Power": "Encender", + "Shutdown": "Apagar", + "Reboot": "Reiniciar", + "Reset": "Restablecer", + "Clipboard": "Portapapeles", + "Clear": "Vaciar", + "Fullscreen": "Pantalla Completa", + "Settings": "Configuraciones", + "Shared Mode": "Modo Compartido", + "View Only": "Solo visualización", + "Clip to Window": "Recortar al tamaño de la ventana", + "Scaling Mode:": "Modo de escalado:", + "None": "Ninguno", + "Local Scaling": "Escalado Local", + "Local Downscaling": "Reducción de escala local", + "Remote Resizing": "Cambio de tamaño remoto", + "Advanced": "Avanzado", + "Local Cursor": "Cursor Local", + "Repeater ID:": "ID del Repetidor", + "WebSocket": "WebSocket", + "Encrypt": "", + "Host:": "Host", + "Port:": "Puesto", + "Path:": "Ruta", + "Automatic Reconnect": "Reconexión automática", + "Reconnect Delay (ms):": "Retraso en la reconexión (ms)", + "Logging:": "Logging", + "Disconnect": "Desconectar", + "Connect": "Conectar", + "Password:": "Contraseña", + "Cancel": "Cancelar", + "Canvas not supported.": "Canvas no está soportado" +} \ No newline at end of file diff --git a/systemvm/debian/root/noVNC/app/locale/ko.json b/systemvm/debian/root/noVNC/app/locale/ko.json new file mode 100644 index 000000000000..e4ecddcfda69 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/locale/ko.json @@ -0,0 +1,70 @@ +{ + "Connecting...": "연결중...", + "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": "새 연결이 거부되었습니다.", + "Password is required": "비밀번호가 필요합니다.", + "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": "오른쪽 마우스 버튼", + "Keyboard": "키보드", + "Show Keyboard": "키보드 보이기", + "Extra keys": "기타 키들", + "Show Extra Keys": "기타 키들 보이기", + "Ctrl": "Ctrl", + "Toggle Ctrl": "Ctrl 켜기/끄기", + "Alt": "Alt", + "Toggle Alt": "Alt 켜기/끄기", + "Send Tab": "Tab 보내기", + "Tab": "Tab", + "Esc": "Esc", + "Send Escape": "Esc 보내기", + "Ctrl+Alt+Del": "Ctrl+Alt+Del", + "Send Ctrl-Alt-Del": "Ctrl+Alt+Del 보내기", + "Shutdown/Reboot": "셧다운/리붓", + "Shutdown/Reboot...": "셧다운/리붓...", + "Power": "전원", + "Shutdown": "셧다운", + "Reboot": "리붓", + "Reset": "리셋", + "Clipboard": "클립보드", + "Clear": "지우기", + "Fullscreen": "전체화면", + "Settings": "설정", + "Shared Mode": "공유 모드", + "View Only": "보기 전용", + "Clip to Window": "창에 클립", + "Scaling Mode:": "스케일링 모드:", + "None": "없음", + "Local Scaling": "로컬 스케일링", + "Remote Resizing": "원격 크기 조절", + "Advanced": "고급", + "Repeater ID:": "중계 ID", + "WebSocket": "웹소켓", + "Encrypt": "암호화", + "Host:": "호스트:", + "Port:": "포트:", + "Path:": "위치:", + "Automatic Reconnect": "자동 재연결", + "Reconnect Delay (ms):": "재연결 지연 시간 (ms)", + "Logging:": "로깅", + "Disconnect": "연결 해제", + "Connect": "연결", + "Password:": "비밀번호:", + "Send Password": "비밀번호 전송", + "Cancel": "취소" +} \ No newline at end of file diff --git a/systemvm/debian/root/noVNC/app/locale/nl.json b/systemvm/debian/root/noVNC/app/locale/nl.json new file mode 100644 index 000000000000..0cdcc92a9b35 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/locale/nl.json @@ -0,0 +1,73 @@ +{ + "Connecting...": "Verbinden...", + "Disconnecting...": "Verbinding verbreken...", + "Reconnecting...": "Opnieuw verbinding maken...", + "Internal error": "Interne fout", + "Must set host": "Host moeten worden ingesteld", + "Connected (encrypted) to ": "Verbonden (versleuteld) met ", + "Connected (unencrypted) to ": "Verbonden (onversleuteld) met ", + "Something went wrong, connection is closed": "Er iets fout gelopen, verbinding werd verbroken", + "Failed to connect to server": "Verbinding maken met server is mislukt", + "Disconnected": "Verbinding verbroken", + "New connection has been rejected with reason: ": "Nieuwe verbinding is geweigerd omwille van de volgende reden: ", + "New connection has been rejected": "Nieuwe verbinding is geweigerd", + "Password is required": "Wachtwoord is vereist", + "noVNC encountered an error:": "noVNC heeft een fout bemerkt:", + "Hide/Show the control bar": "Verberg/Toon de bedieningsbalk", + "Move/Drag Viewport": "Verplaats/Versleep Kijkvenster", + "viewport drag": "kijkvenster slepen", + "Active Mouse Button": "Actieve Muisknop", + "No mousebutton": "Geen muisknop", + "Left mousebutton": "Linker muisknop", + "Middle mousebutton": "Middelste muisknop", + "Right mousebutton": "Rechter muisknop", + "Keyboard": "Toetsenbord", + "Show Keyboard": "Toon Toetsenbord", + "Extra keys": "Extra toetsen", + "Show Extra Keys": "Toon Extra Toetsen", + "Ctrl": "Ctrl", + "Toggle Ctrl": "Ctrl omschakelen", + "Alt": "Alt", + "Toggle Alt": "Alt omschakelen", + "Toggle Windows": "Windows omschakelen", + "Windows": "Windows", + "Send Tab": "Tab Sturen", + "Tab": "Tab", + "Esc": "Esc", + "Send Escape": "Escape Sturen", + "Ctrl+Alt+Del": "Ctrl-Alt-Del", + "Send Ctrl-Alt-Del": "Ctrl-Alt-Del Sturen", + "Shutdown/Reboot": "Uitschakelen/Herstarten", + "Shutdown/Reboot...": "Uitschakelen/Herstarten...", + "Power": "Systeem", + "Shutdown": "Uitschakelen", + "Reboot": "Herstarten", + "Reset": "Resetten", + "Clipboard": "Klembord", + "Clear": "Wissen", + "Fullscreen": "Volledig Scherm", + "Settings": "Instellingen", + "Shared Mode": "Gedeelde Modus", + "View Only": "Alleen Kijken", + "Clip to Window": "Randen buiten venster afsnijden", + "Scaling Mode:": "Schaalmodus:", + "None": "Geen", + "Local Scaling": "Lokaal Schalen", + "Remote Resizing": "Op Afstand Formaat Wijzigen", + "Advanced": "Geavanceerd", + "Repeater ID:": "Repeater ID:", + "WebSocket": "WebSocket", + "Encrypt": "Versleutelen", + "Host:": "Host:", + "Port:": "Poort:", + "Path:": "Pad:", + "Automatic Reconnect": "Automatisch Opnieuw Verbinden", + "Reconnect Delay (ms):": "Vertraging voor Opnieuw Verbinden (ms):", + "Show Dot when No Cursor": "Geef stip weer indien geen cursor", + "Logging:": "Logmeldingen:", + "Disconnect": "Verbinding verbreken", + "Connect": "Verbinden", + "Password:": "Wachtwoord:", + "Send Password": "Verzend Wachtwoord:", + "Cancel": "Annuleren" +} \ No newline at end of file diff --git a/systemvm/debian/root/noVNC/app/locale/pl.json b/systemvm/debian/root/noVNC/app/locale/pl.json new file mode 100644 index 000000000000..006ac7a55f54 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/locale/pl.json @@ -0,0 +1,69 @@ +{ + "Connecting...": "Łączenie...", + "Disconnecting...": "Rozłączanie...", + "Reconnecting...": "Łączenie...", + "Internal error": "Błąd wewnętrzny", + "Must set host": "Host i port są wymagane", + "Connected (encrypted) to ": "Połączenie (szyfrowane) z ", + "Connected (unencrypted) to ": "Połączenie (nieszyfrowane) z ", + "Something went wrong, connection is closed": "Coś poszło źle, połączenie zostało zamknięte", + "Disconnected": "Rozłączony", + "New connection has been rejected with reason: ": "Nowe połączenie zostało odrzucone z powodu: ", + "New connection has been rejected": "Nowe połączenie zostało odrzucone", + "Password is required": "Hasło jest wymagane", + "noVNC encountered an error:": "noVNC napotkało błąd:", + "Hide/Show the control bar": "Pokaż/Ukryj pasek ustawień", + "Move/Drag Viewport": "Ruszaj/Przeciągaj Viewport", + "viewport drag": "przeciągnij viewport", + "Active Mouse Button": "Aktywny Przycisk Myszy", + "No mousebutton": "Brak przycisku myszy", + "Left mousebutton": "Lewy przycisk myszy", + "Middle mousebutton": "Środkowy przycisk myszy", + "Right mousebutton": "Prawy przycisk myszy", + "Keyboard": "Klawiatura", + "Show Keyboard": "Pokaż klawiaturę", + "Extra keys": "Przyciski dodatkowe", + "Show Extra Keys": "Pokaż przyciski dodatkowe", + "Ctrl": "Ctrl", + "Toggle Ctrl": "Przełącz Ctrl", + "Alt": "Alt", + "Toggle Alt": "Przełącz Alt", + "Send Tab": "Wyślij Tab", + "Tab": "Tab", + "Esc": "Esc", + "Send Escape": "Wyślij Escape", + "Ctrl+Alt+Del": "Ctrl+Alt+Del", + "Send Ctrl-Alt-Del": "Wyślij Ctrl-Alt-Del", + "Shutdown/Reboot": "Wyłącz/Uruchom ponownie", + "Shutdown/Reboot...": "Wyłącz/Uruchom ponownie...", + "Power": "Włączony", + "Shutdown": "Wyłącz", + "Reboot": "Uruchom ponownie", + "Reset": "Resetuj", + "Clipboard": "Schowek", + "Clear": "Wyczyść", + "Fullscreen": "Pełny ekran", + "Settings": "Ustawienia", + "Shared Mode": "Tryb Współdzielenia", + "View Only": "Tylko Podgląd", + "Clip to Window": "Przytnij do Okna", + "Scaling Mode:": "Tryb Skalowania:", + "None": "Brak", + "Local Scaling": "Skalowanie lokalne", + "Remote Resizing": "Skalowanie zdalne", + "Advanced": "Zaawansowane", + "Repeater ID:": "ID Repeatera:", + "WebSocket": "WebSocket", + "Encrypt": "Szyfrowanie", + "Host:": "Host:", + "Port:": "Port:", + "Path:": "Ścieżka:", + "Automatic Reconnect": "Automatycznie wznawiaj połączenie", + "Reconnect Delay (ms):": "Opóźnienie wznawiania (ms):", + "Logging:": "Poziom logowania:", + "Disconnect": "Rozłącz", + "Connect": "Połącz", + "Password:": "Hasło:", + "Cancel": "Anuluj", + "Canvas not supported.": "Element Canvas nie jest wspierany." +} \ No newline at end of file diff --git a/systemvm/debian/root/noVNC/app/locale/ru.json b/systemvm/debian/root/noVNC/app/locale/ru.json new file mode 100644 index 000000000000..52e57f37f1b4 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/locale/ru.json @@ -0,0 +1,73 @@ +{ + "Connecting...": "Подключение...", + "Disconnecting...": "Отключение...", + "Reconnecting...": "Переподключение...", + "Internal error": "Внутренняя ошибка", + "Must set host": "Задайте имя сервера или IP", + "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": "Требуется пароль", + "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": "Правая кнопка мыши", + "Keyboard": "Клавиатура", + "Show Keyboard": "Показать клавиатуру", + "Extra keys": "Доп. кнопки", + "Show Extra Keys": "Показать дополнительные кнопки", + "Ctrl": "Ctrl", + "Toggle Ctrl": "Передать нажатие Ctrl", + "Alt": "Alt", + "Toggle Alt": "Передать нажатие Alt", + "Toggle Windows": "Переключение вкладок", + "Windows": "Вкладка", + "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...": "Выключить/Перезагрузить...", + "Power": "Питание", + "Shutdown": "Выключить", + "Reboot": "Перезагрузить", + "Reset": "Сброс", + "Clipboard": "Буфер обмена", + "Clear": "Очистить", + "Fullscreen": "Во весь экран", + "Settings": "Настройки", + "Shared Mode": "Общий режим", + "View Only": "Просмотр", + "Clip to Window": "В окно", + "Scaling Mode:": "Масштаб:", + "None": "Нет", + "Local Scaling": "Локльный масштаб", + "Remote Resizing": "Удаленный масштаб", + "Advanced": "Дополнительно", + "Repeater ID:": "Идентификатор ID:", + "WebSocket": "WebSocket", + "Encrypt": "Шифрование", + "Host:": "Сервер:", + "Port:": "Порт:", + "Path:": "Путь:", + "Automatic Reconnect": "Автоматическое переподключение", + "Reconnect Delay (ms):": "Задержка переподключения (мс):", + "Show Dot when No Cursor": "Показать точку вместо курсора", + "Logging:": "Лог:", + "Disconnect": "Отключение", + "Connect": "Подключение", + "Password:": "Пароль:", + "Send Password": "Пароль: ", + "Cancel": "Выход" +} \ No newline at end of file diff --git a/systemvm/debian/root/noVNC/app/locale/sv.json b/systemvm/debian/root/noVNC/app/locale/sv.json new file mode 100644 index 000000000000..d49ea540d931 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/locale/sv.json @@ -0,0 +1,73 @@ +{ + "Connecting...": "Ansluter...", + "Disconnecting...": "Kopplar ner...", + "Reconnecting...": "Återansluter...", + "Internal error": "Internt fel", + "Must set host": "Du måste specifiera en värd", + "Connected (encrypted) to ": "Ansluten (krypterat) till ", + "Connected (unencrypted) to ": "Ansluten (okrypterat) till ", + "Something went wrong, connection is closed": "Något gick fel, anslutningen avslutades", + "Failed to connect to server": "Misslyckades att ansluta till servern", + "Disconnected": "Frånkopplad", + "New connection has been rejected with reason: ": "Ny anslutning har blivit nekad med följande skäl: ", + "New connection has been rejected": "Ny anslutning har blivit nekad", + "Password is required": "Lösenord krävs", + "noVNC encountered an error:": "noVNC stötte på ett problem:", + "Hide/Show the control bar": "Göm/Visa kontrollbaren", + "Move/Drag Viewport": "Flytta/Dra Vyn", + "viewport drag": "dra vy", + "Active Mouse Button": "Aktiv musknapp", + "No mousebutton": "Ingen musknapp", + "Left mousebutton": "Vänster musknapp", + "Middle mousebutton": "Mitten-musknapp", + "Right mousebutton": "Höger musknapp", + "Keyboard": "Tangentbord", + "Show Keyboard": "Visa Tangentbord", + "Extra keys": "Extraknappar", + "Show Extra Keys": "Visa Extraknappar", + "Ctrl": "Ctrl", + "Toggle Ctrl": "Växla Ctrl", + "Alt": "Alt", + "Toggle Alt": "Växla Alt", + "Toggle Windows": "Växla Windows", + "Windows": "Windows", + "Send Tab": "Skicka Tab", + "Tab": "Tab", + "Esc": "Esc", + "Send Escape": "Skicka Escape", + "Ctrl+Alt+Del": "Ctrl+Alt+Del", + "Send Ctrl-Alt-Del": "Skicka Ctrl-Alt-Del", + "Shutdown/Reboot": "Stäng av/Boota om", + "Shutdown/Reboot...": "Stäng av/Boota om...", + "Power": "Ström", + "Shutdown": "Stäng av", + "Reboot": "Boota om", + "Reset": "Återställ", + "Clipboard": "Urklipp", + "Clear": "Rensa", + "Fullscreen": "Fullskärm", + "Settings": "Inställningar", + "Shared Mode": "Delat Läge", + "View Only": "Endast Visning", + "Clip to Window": "Begränsa till Fönster", + "Scaling Mode:": "Skalningsläge:", + "None": "Ingen", + "Local Scaling": "Lokal Skalning", + "Remote Resizing": "Ändra Storlek", + "Advanced": "Avancerat", + "Repeater ID:": "Repeater-ID:", + "WebSocket": "WebSocket", + "Encrypt": "Kryptera", + "Host:": "Värd:", + "Port:": "Port:", + "Path:": "Sökväg:", + "Automatic Reconnect": "Automatisk Återanslutning", + "Reconnect Delay (ms):": "Fördröjning (ms):", + "Show Dot when No Cursor": "Visa prick när ingen muspekare finns", + "Logging:": "Loggning:", + "Disconnect": "Koppla från", + "Connect": "Anslut", + "Password:": "Lösenord:", + "Send Password": "Skicka lösenord", + "Cancel": "Avbryt" +} \ No newline at end of file diff --git a/systemvm/debian/root/noVNC/app/locale/tr.json b/systemvm/debian/root/noVNC/app/locale/tr.json new file mode 100644 index 000000000000..451c1b8a6433 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/locale/tr.json @@ -0,0 +1,69 @@ +{ + "Connecting...": "Bağlanıyor...", + "Disconnecting...": "Bağlantı kesiliyor...", + "Reconnecting...": "Yeniden bağlantı kuruluyor...", + "Internal error": "İç hata", + "Must set host": "Sunucuyu kur", + "Connected (encrypted) to ": "Bağlı (şifrelenmiş)", + "Connected (unencrypted) to ": "Bağlandı (şifrelenmemiş)", + "Something went wrong, connection is closed": "Bir şeyler ters gitti, bağlantı kesildi", + "Disconnected": "Bağlantı kesildi", + "New connection has been rejected with reason: ": "Bağlantı aşağıdaki nedenlerden dolayı reddedildi: ", + "New connection has been rejected": "Bağlantı reddedildi", + "Password is required": "Şifre gerekli", + "noVNC encountered an error:": "Bir hata oluştu:", + "Hide/Show the control bar": "Denetim masasını Gizle/Göster", + "Move/Drag Viewport": "Görünümü Taşı/Sürükle", + "viewport drag": "Görüntü penceresini sürükle", + "Active Mouse Button": "Aktif Fare Düğmesi", + "No mousebutton": "Fare düğmesi yok", + "Left mousebutton": "Farenin sol düğmesi", + "Middle mousebutton": "Farenin orta düğmesi", + "Right mousebutton": "Farenin sağ düğmesi", + "Keyboard": "Klavye", + "Show Keyboard": "Klavye Düzenini Göster", + "Extra keys": "Ekstra tuşlar", + "Show Extra Keys": "Ekstra tuşları göster", + "Ctrl": "Ctrl", + "Toggle Ctrl": "Ctrl Değiştir ", + "Alt": "Alt", + "Toggle Alt": "Alt Değiştir", + "Send Tab": "Sekme Gönder", + "Tab": "Sekme", + "Esc": "Esc", + "Send Escape": "Boşluk Gönder", + "Ctrl+Alt+Del": "Ctrl + Alt + Del", + "Send Ctrl-Alt-Del": "Ctrl-Alt-Del Gönder", + "Shutdown/Reboot": "Kapat/Yeniden Başlat", + "Shutdown/Reboot...": "Kapat/Yeniden Başlat...", + "Power": "Güç", + "Shutdown": "Kapat", + "Reboot": "Yeniden Başlat", + "Reset": "Sıfırla", + "Clipboard": "Pano", + "Clear": "Temizle", + "Fullscreen": "Tam Ekran", + "Settings": "Ayarlar", + "Shared Mode": "Paylaşım Modu", + "View Only": "Sadece Görüntüle", + "Clip to Window": "Pencereye Tıkla", + "Scaling Mode:": "Ölçekleme Modu:", + "None": "Bilinmeyen", + "Local Scaling": "Yerel Ölçeklendirme", + "Remote Resizing": "Uzaktan Yeniden Boyutlandırma", + "Advanced": "Gelişmiş", + "Repeater ID:": "Tekralayıcı ID:", + "WebSocket": "WebSocket", + "Encrypt": "Şifrele", + "Host:": "Ana makine:", + "Port:": "Port:", + "Path:": "Yol:", + "Automatic Reconnect": "Otomatik Yeniden Bağlan", + "Reconnect Delay (ms):": "Yeniden Bağlanma Süreci (ms):", + "Logging:": "Giriş yapılıyor:", + "Disconnect": "Bağlantıyı Kes", + "Connect": "Bağlan", + "Password:": "Parola:", + "Cancel": "Vazgeç", + "Canvas not supported.": "Tuval desteklenmiyor." +} \ No newline at end of file diff --git a/systemvm/debian/root/noVNC/app/locale/zh_CN.json b/systemvm/debian/root/noVNC/app/locale/zh_CN.json new file mode 100644 index 000000000000..b66995620ff2 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/locale/zh_CN.json @@ -0,0 +1,69 @@ +{ + "Connecting...": "链接中...", + "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": "链接被拒绝", + "Password is required": "请提供密码", + "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": "鼠标右鍵", + "Keyboard": "键盘", + "Show Keyboard": "显示键盘", + "Extra keys": "额外按键", + "Show Extra Keys": "显示额外按键", + "Ctrl": "Ctrl", + "Toggle Ctrl": "切换 Ctrl", + "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...": "关机/重新启动...", + "Power": "电源", + "Shutdown": "关机", + "Reboot": "重新启动", + "Reset": "重置", + "Clipboard": "剪贴板", + "Clear": "清除", + "Fullscreen": "全屏幕", + "Settings": "设置", + "Shared Mode": "分享模式", + "View Only": "仅检视", + "Clip to Window": "限制/裁切窗口大小", + "Scaling Mode:": "缩放模式:", + "None": "无", + "Local Scaling": "本地缩放", + "Remote Resizing": "远程调整大小", + "Advanced": "高级", + "Repeater ID:": "中继站 ID", + "WebSocket": "WebSocket", + "Encrypt": "加密", + "Host:": "主机:", + "Port:": "端口:", + "Path:": "路径:", + "Automatic Reconnect": "自动重新链接", + "Reconnect Delay (ms):": "重新链接间隔 (ms):", + "Logging:": "日志级别:", + "Disconnect": "终端链接", + "Connect": "链接", + "Password:": "密码:", + "Cancel": "取消" +} \ No newline at end of file diff --git a/systemvm/debian/root/noVNC/app/locale/zh_TW.json b/systemvm/debian/root/noVNC/app/locale/zh_TW.json new file mode 100644 index 000000000000..8ddf813f084e --- /dev/null +++ b/systemvm/debian/root/noVNC/app/locale/zh_TW.json @@ -0,0 +1,69 @@ +{ + "Connecting...": "連線中...", + "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": "連線被拒絕", + "Password is required": "請提供密碼", + "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": "滑鼠右鍵", + "Keyboard": "鍵盤", + "Show Keyboard": "顯示鍵盤", + "Extra keys": "額外按鍵", + "Show Extra Keys": "顯示額外按鍵", + "Ctrl": "Ctrl", + "Toggle Ctrl": "切換 Ctrl", + "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...": "關機/重新啟動...", + "Power": "電源", + "Shutdown": "關機", + "Reboot": "重新啟動", + "Reset": "重設", + "Clipboard": "剪貼簿", + "Clear": "清除", + "Fullscreen": "全螢幕", + "Settings": "設定", + "Shared Mode": "分享模式", + "View Only": "僅檢視", + "Clip to Window": "限制/裁切視窗大小", + "Scaling Mode:": "縮放模式:", + "None": "無", + "Local Scaling": "本機縮放", + "Remote Resizing": "遠端調整大小", + "Advanced": "進階", + "Repeater ID:": "中繼站 ID", + "WebSocket": "WebSocket", + "Encrypt": "加密", + "Host:": "主機:", + "Port:": "連接埠:", + "Path:": "路徑:", + "Automatic Reconnect": "自動重新連線", + "Reconnect Delay (ms):": "重新連線間隔 (ms):", + "Logging:": "日誌級別:", + "Disconnect": "中斷連線", + "Connect": "連線", + "Password:": "密碼:", + "Cancel": "取消" +} \ No newline at end of file diff --git a/systemvm/debian/root/noVNC/app/localization.js b/systemvm/debian/root/noVNC/app/localization.js new file mode 100644 index 000000000000..100901c9d26f --- /dev/null +++ b/systemvm/debian/root/noVNC/app/localization.js @@ -0,0 +1,172 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +/* + * Localization Utilities + */ + +export class Localizer { + constructor() { + // Currently configured language + this.language = 'en'; + + // Current dictionary of translations + this.dictionary = undefined; + } + + // Configure suitable language based on user preferences + setup(supportedLanguages) { + this.language = 'en'; // Default: US English + + /* + * Navigator.languages only available in Chrome (32+) and FireFox (32+) + * Fall back to navigator.language for other browsers + */ + let userLanguages; + if (typeof window.navigator.languages == 'object') { + userLanguages = window.navigator.languages; + } else { + userLanguages = [navigator.language || navigator.userLanguage]; + } + + for (let i = 0;i < userLanguages.length;i++) { + const userLang = userLanguages[i] + .toLowerCase() + .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] + .toLowerCase() + .replace("_", "-") + .split("-"); + + if (userLang[0] !== supLang[0]) { + continue; + } + if (userLang[1] !== supLang[1]) { + continue; + } + + this.language = supportedLanguages[j]; + return; + } + + // Second pass: fallback + for (let j = 0;j < supportedLanguages.length;j++) { + const supLang = supportedLanguages[j] + .toLowerCase() + .replace("_", "-") + .split("-"); + + if (userLang[0] !== supLang[0]) { + continue; + } + if (supLang[1] !== undefined) { + continue; + } + + this.language = supportedLanguages[j]; + return; + } + } + } + + // Retrieve localised text + get(id) { + if (typeof this.dictionary !== 'undefined' && this.dictionary[id]) { + return this.dictionary[id]; + } else { + return id; + } + } + + // Traverses the DOM and translates relevant fields + // See https://html.spec.whatwg.org/multipage/dom.html#attr-translate + translateDOM() { + const self = this; + + function process(elem, enabled) { + function isAnyOf(searchElement, items) { + return items.indexOf(searchElement) !== -1; + } + + function translateAttribute(elem, attr) { + const str = self.get(elem.getAttribute(attr)); + elem.setAttribute(attr, str); + } + + function translateTextNode(node) { + const str = self.get(node.data.trim()); + node.data = str; + } + + if (elem.hasAttribute("translate")) { + if (isAnyOf(elem.getAttribute("translate"), ["", "yes"])) { + enabled = true; + } else if (isAnyOf(elem.getAttribute("translate"), ["no"])) { + enabled = false; + } + } + + if (enabled) { + if (elem.hasAttribute("abbr") && + elem.tagName === "TH") { + translateAttribute(elem, "abbr"); + } + if (elem.hasAttribute("alt") && + isAnyOf(elem.tagName, ["AREA", "IMG", "INPUT"])) { + translateAttribute(elem, "alt"); + } + if (elem.hasAttribute("download") && + isAnyOf(elem.tagName, ["A", "AREA"])) { + translateAttribute(elem, "download"); + } + if (elem.hasAttribute("label") && + isAnyOf(elem.tagName, ["MENUITEM", "MENU", "OPTGROUP", + "OPTION", "TRACK"])) { + translateAttribute(elem, "label"); + } + // FIXME: Should update "lang" + if (elem.hasAttribute("placeholder") && + isAnyOf(elem.tagName, ["INPUT", "TEXTAREA"])) { + translateAttribute(elem, "placeholder"); + } + if (elem.hasAttribute("title")) { + translateAttribute(elem, "title"); + } + if (elem.hasAttribute("value") && + elem.tagName === "INPUT" && + isAnyOf(elem.getAttribute("type"), ["reset", "button", "submit"])) { + translateAttribute(elem, "value"); + } + } + + for (let i = 0; i < elem.childNodes.length; i++) { + const node = elem.childNodes[i]; + if (node.nodeType === node.ELEMENT_NODE) { + process(node, enabled); + } else if (node.nodeType === node.TEXT_NODE && enabled) { + translateTextNode(node); + } + } + } + + process(document.body, true); + } +} + +export const l10n = new Localizer(); +export default l10n.get.bind(l10n); diff --git a/systemvm/debian/root/noVNC/app/sounds/CREDITS b/systemvm/debian/root/noVNC/app/sounds/CREDITS new file mode 100644 index 000000000000..ec1fb556b2b6 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/sounds/CREDITS @@ -0,0 +1,4 @@ +bell + Copyright: Dr. Richard Boulanger et al + URL: http://www.archive.org/details/Berklee44v12 + License: CC-BY Attribution 3.0 Unported diff --git a/systemvm/debian/root/noVNC/app/sounds/bell.mp3 b/systemvm/debian/root/noVNC/app/sounds/bell.mp3 new file mode 100644 index 000000000000..fdbf149a1e9e Binary files /dev/null and b/systemvm/debian/root/noVNC/app/sounds/bell.mp3 differ diff --git a/systemvm/debian/root/noVNC/app/sounds/bell.oga b/systemvm/debian/root/noVNC/app/sounds/bell.oga new file mode 100644 index 000000000000..144d2b3670e8 Binary files /dev/null and b/systemvm/debian/root/noVNC/app/sounds/bell.oga differ diff --git a/systemvm/debian/root/noVNC/app/styles/Orbitron700.ttf b/systemvm/debian/root/noVNC/app/styles/Orbitron700.ttf new file mode 100644 index 000000000000..e28729dc56b0 Binary files /dev/null and b/systemvm/debian/root/noVNC/app/styles/Orbitron700.ttf differ diff --git a/systemvm/debian/root/noVNC/app/styles/Orbitron700.woff b/systemvm/debian/root/noVNC/app/styles/Orbitron700.woff new file mode 100644 index 000000000000..61db630cce1c Binary files /dev/null and b/systemvm/debian/root/noVNC/app/styles/Orbitron700.woff differ diff --git a/systemvm/debian/root/noVNC/app/styles/base.css b/systemvm/debian/root/noVNC/app/styles/base.css new file mode 100644 index 000000000000..3ca9894dc742 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/styles/base.css @@ -0,0 +1,900 @@ +/* + * noVNC base CSS + * Copyright (C) 2018 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). + */ + +/* + * Z index layers: + * + * 0: Main screen + * 10: Control bar + * 50: Transition blocker + * 60: Connection popups + * 100: Status bar + * ... + * 1000: Javascript crash + * ... + * 10000: Max (used for polyfills) + */ + +body { + margin:0; + padding:0; + font-family: Helvetica; + /*Background image with light grey curve.*/ + background-color:#494949; + background-repeat:no-repeat; + background-position:right bottom; + height:100%; + touch-action: none; +} + +html { + height:100%; +} + +.noVNC_only_touch.noVNC_hidden { + display: none; +} + +.noVNC_disabled { + color: rgb(128, 128, 128); +} + +/* ---------------------------------------- + * Spinner + * ---------------------------------------- + */ + +.noVNC_spinner { + position: relative; +} +.noVNC_spinner, .noVNC_spinner::before, .noVNC_spinner::after { + width: 10px; + height: 10px; + border-radius: 2px; + box-shadow: -60px 10px 0 rgba(255, 255, 255, 0); + animation: noVNC_spinner 1.0s linear infinite; +} +.noVNC_spinner::before { + content: ""; + position: absolute; + left: 0px; + top: 0px; + animation-delay: -0.1s; +} +.noVNC_spinner::after { + content: ""; + position: absolute; + top: 0px; + left: 0px; + animation-delay: 0.1s; +} +@keyframes noVNC_spinner { + 0% { box-shadow: -60px 10px 0 rgba(255, 255, 255, 0); width: 20px; } + 25% { box-shadow: 20px 10px 0 rgba(255, 255, 255, 1); width: 10px; } + 50% { box-shadow: 60px 10px 0 rgba(255, 255, 255, 0); width: 10px; } +} + +/* ---------------------------------------- + * Input Elements + * ---------------------------------------- + */ + +input[type=input], input[type=password], input[type=number], +input:not([type]), 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=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=submit] { + padding-left: 20px; + padding-right: 20px; +} + +option { + color: black; + background: white; +} + +input[type=input]:focus, input[type=password]:focus, +input:not([type]):focus, input[type=button]:focus, +input[type=submit]:focus, +textarea:focus, select: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=submit]::-moz-focus-inner { + border: none; +} + +input[type=input]:disabled, input[type=password]:disabled, +input:not([type]):disabled, input[type=button]:disabled, +input[type=submit]:disabled, input[type=number]:disabled, +textarea:disabled, select:disabled { + color: rgb(128, 128, 128); + background: rgb(240, 240, 240); +} + +input[type=button]: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=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 + * ---------------------------------------- + */ + +.noVNC_center { + /* + * This is a workaround because webkit misrenders transforms and + * uses non-integer coordinates, resulting in blurry content. + * Ideally we'd use "top: 50%; transform: translateY(-50%);" on + * the objects instead. + */ + display: flex; + align-items: center; + justify-content: center; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; +} +.noVNC_center > * { + pointer-events: auto; +} +.noVNC_vcenter { + display: flex; + flex-direction: column; + justify-content: center; + position: fixed; + top: 0; + left: 0; + height: 100%; + pointer-events: none; +} +.noVNC_vcenter > * { + pointer-events: auto; +} + +/* ---------------------------------------- + * Layering + * ---------------------------------------- + */ + +.noVNC_connect_layer { + z-index: 60; +} + +/* ---------------------------------------- + * Fallback error + * ---------------------------------------- + */ + +#noVNC_fallback_error { + z-index: 1000; + visibility: hidden; +} +#noVNC_fallback_error.noVNC_open { + visibility: visible; +} + +#noVNC_fallback_error > div { + max-width: 90%; + padding: 15px; + + transition: 0.5s ease-in-out; + + transform: translateY(-50px); + opacity: 0; + + text-align: center; + font-weight: bold; + color: #fff; + + border-radius: 10px; + box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); + background: rgba(200,55,55,0.8); +} +#noVNC_fallback_error.noVNC_open > div { + transform: translateY(0); + opacity: 1; +} + +#noVNC_fallback_errormsg { + font-weight: normal; +} + +#noVNC_fallback_errormsg .noVNC_message { + display: inline-block; + text-align: left; + font-family: monospace; + white-space: pre-wrap; +} + +#noVNC_fallback_error .noVNC_location { + font-style: italic; + font-size: 0.8em; + color: rgba(255, 255, 255, 0.8); +} + +#noVNC_fallback_error .noVNC_stack { + max-height: 50vh; + padding: 10px; + margin: 10px; + font-size: 0.8em; + text-align: left; + font-family: monospace; + white-space: pre; + border: 1px solid rgba(0, 0, 0, 0.5); + background: rgba(0, 0, 0, 0.2); + overflow: auto; +} + +/* ---------------------------------------- + * Control Bar + * ---------------------------------------- + */ + +#noVNC_control_bar_anchor { + /* The anchor is needed to get z-stacking to work */ + position: fixed; + z-index: 10; + + transition: 0.5s ease-in-out; + + /* Edge misrenders animations wihthout this */ + transform: translateX(0); +} +:root.noVNC_connected #noVNC_control_bar_anchor.noVNC_idle { + opacity: 0.8; +} +#noVNC_control_bar_anchor.noVNC_right { + left: auto; + right: 0; +} + +#noVNC_control_bar { + position: relative; + left: -100%; + + transition: 0.5s ease-in-out; + + background-color: rgb(110, 132, 163); + border-radius: 0 10px 10px 0; + +} +#noVNC_control_bar.noVNC_open { + box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); + left: 0; +} +#noVNC_control_bar::before { + /* This extra element is to get a proper shadow */ + content: ""; + position: absolute; + z-index: -1; + height: 100%; + width: 30px; + left: -30px; + transition: box-shadow 0.5s ease-in-out; +} +#noVNC_control_bar.noVNC_open::before { + box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); +} +.noVNC_right #noVNC_control_bar { + left: 100%; + border-radius: 10px 0 0 10px; +} +.noVNC_right #noVNC_control_bar.noVNC_open { + left: 0; +} +.noVNC_right #noVNC_control_bar::before { + visibility: hidden; +} + +#noVNC_control_bar_handle { + position: absolute; + left: -15px; + top: 0; + transform: translateY(35px); + width: calc(100% + 30px); + height: 50px; + z-index: -1; + cursor: pointer; + border-radius: 5px; + background-color: rgb(83, 99, 122); + background-image: url("../images/handle_bg.svg"); + background-repeat: no-repeat; + background-position: right; + box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.5); +} +#noVNC_control_bar_handle:after { + content: ""; + transition: transform 0.5s ease-in-out; + background: url("../images/handle.svg"); + position: absolute; + top: 22px; /* (50px-6px)/2 */ + right: 5px; + width: 5px; + height: 6px; +} +#noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after { + transform: translateX(1px) rotate(180deg); +} +:root:not(.noVNC_connected) #noVNC_control_bar_handle { + display: none; +} +.noVNC_right #noVNC_control_bar_handle { + background-position: left; +} +.noVNC_right #noVNC_control_bar_handle:after { + left: 5px; + right: 0; + transform: translateX(1px) rotate(180deg); +} +.noVNC_right #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after { + transform: none; +} +#noVNC_control_bar_handle div { + position: absolute; + right: -35px; + top: 0; + width: 50px; + height: 50px; +} +:root:not(.noVNC_touch) #noVNC_control_bar_handle div { + display: none; +} +.noVNC_right #noVNC_control_bar_handle div { + left: -35px; + right: auto; +} + +#noVNC_control_bar .noVNC_scroll { + max-height: 100vh; /* Chrome is buggy with 100% */ + overflow-x: hidden; + overflow-y: auto; + padding: 0 10px 0 5px; +} +.noVNC_right #noVNC_control_bar .noVNC_scroll { + padding: 0 5px 0 10px; +} + +/* Control bar hint */ +#noVNC_control_bar_hint { + position: fixed; + left: calc(100vw - 50px); + right: auto; + top: 50%; + transform: translateY(-50%) scale(0); + width: 100px; + height: 50%; + max-height: 600px; + + visibility: hidden; + opacity: 0; + transition: 0.2s ease-in-out; + background: transparent; + box-shadow: 0 0 10px black, inset 0 0 10px 10px rgba(110, 132, 163, 0.8); + 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); +} + +/* General button style */ +.noVNC_button { + display: block; + padding: 4px 4px; + margin: 10px 0; + vertical-align: middle; + border:1px solid rgba(255, 255, 255, 0.2); + border-radius: 6px; +} +.noVNC_button.noVNC_selected { + border-color: rgba(0, 0, 0, 0.8); + background: rgba(0, 0, 0, 0.5); +} +.noVNC_button:disabled { + opacity: 0.4; +} +.noVNC_button:focus { + outline: none; +} +.noVNC_button: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); +} +:root:not(.noVNC_touch) .noVNC_button:hover, +.noVNC_button:focus { + background: rgba(255, 255, 255, 0.2); +} +.noVNC_button.noVNC_hidden { + display: none; +} + +/* Panels */ +.noVNC_panel { + transform: translateX(25px); + + transition: 0.5s ease-in-out; + + max-height: 100vh; /* Chrome is buggy with 100% */ + overflow-x: hidden; + overflow-y: auto; + + visibility: hidden; + opacity: 0; + + padding: 15px; + + background: #fff; + border-radius: 10px; + color: #000; + border: 2px solid #E0E0E0; + box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); +} +.noVNC_panel.noVNC_open { + visibility: visible; + opacity: 1; + transform: translateX(75px); +} +.noVNC_right .noVNC_vcenter { + left: auto; + right: 0; +} +.noVNC_right .noVNC_panel { + transform: translateX(-25px); +} +.noVNC_right .noVNC_panel.noVNC_open { + transform: translateX(-75px); +} + +.noVNC_panel hr { + border: none; + border-top: 1px solid rgb(192, 192, 192); +} + +.noVNC_panel label { + display: block; + white-space: nowrap; +} + +.noVNC_panel .noVNC_heading { + background-color: rgb(110, 132, 163); + border-radius: 5px; + padding: 5px; + /* Compensate for padding in image */ + padding-right: 8px; + color: white; + font-size: 20px; + margin-bottom: 10px; + white-space: nowrap; +} +.noVNC_panel .noVNC_heading img { + vertical-align: bottom; +} + +.noVNC_submit { + float: right; +} + +/* Expanders */ +.noVNC_expander { + cursor: pointer; +} +.noVNC_expander::before { + content: url("../images/expander.svg"); + display: inline-block; + margin-right: 5px; + transition: 0.2s ease-in-out; +} +.noVNC_expander.noVNC_open::before { + transform: rotateZ(90deg); +} +.noVNC_expander ~ * { + margin: 5px; + margin-left: 10px; + padding: 5px; + background: rgba(0, 0, 0, 0.05); + border-radius: 5px; +} +.noVNC_expander:not(.noVNC_open) ~ * { + display: none; +} + +/* Control bar content */ + +#noVNC_control_bar .noVNC_logo { + font-size: 13px; +} + +:root:not(.noVNC_connected) #noVNC_view_drag_button { + display: none; +} + +/* noVNC Touch Device only buttons */ +:root:not(.noVNC_connected) #noVNC_mobile_buttons { + display: none; +} +:root:not(.noVNC_touch) #noVNC_mobile_buttons { + display: none; +} + +/* Extra manual keys */ +:root:not(.noVNC_connected) #noVNC_extra_keys { + display: none; +} + +#noVNC_modifiers { + background-color: rgb(92, 92, 92); + border: none; + padding: 0 10px; +} + +/* Shutdown/Reboot */ +:root:not(.noVNC_connected) #noVNC_power_button { + display: none; +} +#noVNC_power { +} +#noVNC_power_buttons { + display: none; +} + +#noVNC_power input[type=button] { + width: 100%; +} + +/* Clipboard */ +: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; + max-width: 100%; +} + +/* Settings */ +#noVNC_settings { +} +#noVNC_settings ul { + list-style: none; + margin: 0px; + padding: 0px; +} +#noVNC_setting_port { + width: 80px; +} +#noVNC_setting_path { + width: 100px; +} + +/* Connection Controls */ +:root:not(.noVNC_connected) #noVNC_disconnect_button { + display: none; +} + +/* ---------------------------------------- + * Status Dialog + * ---------------------------------------- + */ + +#noVNC_status { + position: fixed; + top: 0; + left: 0; + width: 100%; + z-index: 100; + transform: translateY(-100%); + + cursor: pointer; + + transition: 0.5s ease-in-out; + + visibility: hidden; + opacity: 0; + + padding: 5px; + + display: flex; + flex-direction: row; + justify-content: center; + align-content: center; + + line-height: 25px; + word-wrap: break-word; + color: #fff; + + border-bottom: 1px solid rgba(0, 0, 0, 0.9); +} +#noVNC_status.noVNC_open { + transform: translateY(0); + visibility: visible; + opacity: 1; +} + +#noVNC_status::before { + content: ""; + display: inline-block; + width: 25px; + height: 25px; + margin-right: 5px; +} + +#noVNC_status.noVNC_status_normal { + background: rgba(128,128,128,0.9); +} +#noVNC_status.noVNC_status_normal::before { + content: url("../images/info.svg") " "; +} +#noVNC_status.noVNC_status_error { + background: rgba(200,55,55,0.9); +} +#noVNC_status.noVNC_status_error::before { + content: url("../images/error.svg") " "; +} +#noVNC_status.noVNC_status_warn { + background: rgba(180,180,30,0.9); +} +#noVNC_status.noVNC_status_warn::before { + content: url("../images/warning.svg") " "; +} + +/* ---------------------------------------- + * Connect Dialog + * ---------------------------------------- + */ + +#noVNC_connect_dlg { + transition: 0.5s ease-in-out; + + transform: scale(0, 0); + visibility: hidden; + opacity: 0; +} +#noVNC_connect_dlg.noVNC_open { + transform: scale(1, 1); + visibility: visible; + opacity: 1; +} +#noVNC_connect_dlg .noVNC_logo { + transition: 0.5s ease-in-out; + padding: 10px; + margin-bottom: 10px; + + font-size: 80px; + text-align: center; + + border-radius: 5px; +} +@media (max-width: 440px) { + #noVNC_connect_dlg { + max-width: calc(100vw - 100px); + } + #noVNC_connect_dlg .noVNC_logo { + font-size: calc(25vw - 30px); + } +} +#noVNC_connect_button { + cursor: pointer; + + 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; + padding: 5px 30px; + border: 1px solid rgb(83, 99, 122); + border-bottom-width: 2px; + border-radius: 5px; + background: linear-gradient(to top, rgb(110, 132, 163), rgb(99, 119, 147)); + + /* 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 { + background: linear-gradient(to top, rgb(110, 132, 163), rgb(105, 125, 155)); +} + +#noVNC_connect_button img { + vertical-align: bottom; + height: 1.3em; +} + +/* ---------------------------------------- + * Password Dialog + * ---------------------------------------- + */ + +#noVNC_password_dlg { + position: relative; + + transform: translateY(-50px); +} +#noVNC_password_dlg.noVNC_open { + transform: translateY(0); +} +#noVNC_password_dlg ul { + list-style: none; + margin: 0px; + padding: 0px; +} + +/* ---------------------------------------- + * Main Area + * ---------------------------------------- + */ + +/* Transition screen */ +#noVNC_transition { + display: none; + + position: fixed; + top: 0; + left: 0; + bottom: 0; + right: 0; + + color: white; + background: rgba(0, 0, 0, 0.5); + z-index: 50; + + /*display: flex;*/ + align-items: center; + justify-content: center; + flex-direction: column; +} +:root.noVNC_loading #noVNC_transition, +:root.noVNC_connecting #noVNC_transition, +:root.noVNC_disconnecting #noVNC_transition, +:root.noVNC_reconnecting #noVNC_transition { + display: flex; +} +:root:not(.noVNC_reconnecting) #noVNC_cancel_reconnect_button { + display: none; +} +#noVNC_transition_text { + font-size: 1.5em; +} + +/* Main container */ +#noVNC_container { + width: 100%; + height: 100%; + background-color: #313131; + border-bottom-right-radius: 800px 600px; + /*border-top-left-radius: 800px 600px;*/ +} + +#noVNC_keyboardinput { + width: 1px; + height: 1px; + background-color: #fff; + color: #fff; + border: 0; + position: absolute; + left: -40px; + z-index: -1; + ime-mode: disabled; +} + +/*Default noVNC logo.*/ +/* From: http://fonts.googleapis.com/css?family=Orbitron:700 */ +@font-face { + font-family: 'Orbitron'; + font-style: normal; + font-weight: 700; + src: local('?'), url('Orbitron700.woff') format('woff'), + url('Orbitron700.ttf') format('truetype'); +} + +.noVNC_logo { + color:yellow; + font-family: 'Orbitron', 'OrbitronTTF', sans-serif; + line-height:90%; + text-shadow: 0.1em 0.1em 0 black; +} +.noVNC_logo span{ + color:green; +} + +#noVNC_bell { + display: none; +} + +/* ---------------------------------------- + * Media sizing + * ---------------------------------------- + */ + +@media screen and (max-width: 640px){ + #noVNC_logo { + font-size: 150px; + } +} + +@media screen and (min-width: 321px) and (max-width: 480px) { + #noVNC_logo { + font-size: 110px; + } +} + +@media screen and (max-width: 320px) { + #noVNC_logo { + font-size: 90px; + } +} diff --git a/systemvm/debian/root/noVNC/app/ui.js b/systemvm/debian/root/noVNC/app/ui.js new file mode 100644 index 000000000000..17ec48d4bd04 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/ui.js @@ -0,0 +1,1654 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +import * as Log from '../core/util/logging.js'; +import _, { l10n } from './localization.js'; +import { isTouchDevice, isSafari, isIOS, isAndroid, dragThreshold } + from '../core/util/browser.js'; +import { setCapture, getPointerEvent } from '../core/util/events.js'; +import KeyTable from "../core/input/keysym.js"; +import keysyms from "../core/input/keysymdef.js"; +import Keyboard from "../core/input/keyboard.js"; +import RFB from "../core/rfb.js"; +import * as WebUtil from "./webutil.js"; + +const UI = { + + connected: false, + desktopName: "", + + statusTimeout: null, + hideKeyboardTimeout: null, + idleControlbarTimeout: null, + closeControlbarTimeout: null, + + controlbarGrabbed: false, + controlbarDrag: false, + controlbarMouseDownClientY: 0, + controlbarMouseDownOffsetY: 0, + + lastKeyboardinput: null, + defaultKeyboardinputLen: 100, + + inhibit_reconnect: true, + reconnect_callback: null, + reconnect_password: null, + + prime() { + return WebUtil.initSettings().then(() => { + if (document.readyState === "interactive" || document.readyState === "complete") { + return UI.start(); + } + + return new Promise((resolve, reject) => { + document.addEventListener('DOMContentLoaded', () => UI.start().then(resolve).catch(reject)); + }); + }); + }, + + // Render default UI and initialize settings menu + start() { + + UI.initSettings(); + + // Translate the DOM + l10n.translateDOM(); + + // 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); + } + + // Restore control bar position + if (WebUtil.readSetting('controlbar_pos') === 'right') { + UI.toggleControlbarSide(); + } + + UI.initFullscreen(); + + // Setup event handlers + UI.addControlbarHandlers(); + UI.addTouchSpecificHandlers(); + UI.addExtraKeysHandlers(); + UI.addMachineHandlers(); + UI.addConnectionControlHandlers(); + UI.addClipboardHandlers(); + UI.addSettingsHandlers(); + document.getElementById("noVNC_status") + .addEventListener('click', UI.hideStatus); + + // Bootstrap fallback input handler + UI.keyboardinputReset(); + + UI.openControlbar(); + + UI.updateVisualState('init'); + + document.documentElement.classList.remove("noVNC_loading"); + + let autoconnect = WebUtil.getConfigVar('autoconnect', false); + if (autoconnect === 'true' || autoconnect == '1') { + autoconnect = true; + UI.connect(); + } else { + autoconnect = false; + // Show the connect panel on first load unless autoconnecting + UI.openConnectPanel(); + } + + return Promise.resolve(UI.rfb); + }, + + initFullscreen() { + // Only show the button if fullscreen is properly supported + // * Safari doesn't support alphanumerical input while in fullscreen + if (!isSafari() && + (document.documentElement.requestFullscreen || + document.documentElement.mozRequestFullScreen || + document.documentElement.webkitRequestFullscreen || + document.body.msRequestFullscreen)) { + document.getElementById('noVNC_fullscreen_button') + .classList.remove("noVNC_hidden"); + UI.addFullscreenHandlers(); + } + }, + + initSettings() { + // Logging selection dropdown + const llevels = ['error', 'warn', 'info', 'debug']; + for (let i = 0; i < llevels.length; i += 1) { + UI.addOption(document.getElementById('noVNC_setting_logging'), llevels[i], llevels[i]); + } + + // Settings with immediate effects + UI.initSetting('logging', 'warn'); + UI.updateLogging(); + + // if port == 80 (or 443) then it won't be present and should be + // set manually + let port = window.location.port; + if (!port) { + if (window.location.protocol.substring(0, 5) == 'https') { + port = 443; + } else if (window.location.protocol.substring(0, 4) == 'http') { + port = 80; + } + } + + /* Populate the controls if defaults are provided in the URL */ + UI.initSetting('host', window.location.hostname); + UI.initSetting('port', port); + UI.initSetting('encrypt', (window.location.protocol === "https:")); + UI.initSetting('view_clip', false); + UI.initSetting('resize', 'off'); + UI.initSetting('shared', true); + UI.initSetting('view_only', false); + UI.initSetting('show_dot', false); + UI.initSetting('path', 'websockify'); + UI.initSetting('repeaterID', ''); + UI.initSetting('reconnect', false); + UI.initSetting('reconnect_delay', 5000); + + UI.setupSettingLabels(); + }, + // Adds a link to the label elements on the corresponding input elements + setupSettingLabels() { + const labels = document.getElementsByTagName('LABEL'); + for (let i = 0; i < labels.length; i++) { + const htmlFor = labels[i].htmlFor; + if (htmlFor != '') { + const elem = document.getElementById(htmlFor); + if (elem) elem.label = labels[i]; + } else { + // If 'for' isn't set, use the first input element child + const children = labels[i].children; + for (let j = 0; j < children.length; j++) { + if (children[j].form !== undefined) { + children[j].label = labels[i]; + break; + } + } + } + } + }, + +/* ------^------- +* /INIT +* ============== +* EVENT HANDLERS +* ------v------*/ + + addControlbarHandlers() { + document.getElementById("noVNC_control_bar") + .addEventListener('mousemove', UI.activateControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('mouseup', UI.activateControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('mousedown', UI.activateControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('keydown', UI.activateControlbar); + + document.getElementById("noVNC_control_bar") + .addEventListener('mousedown', UI.keepControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('keydown', UI.keepControlbar); + + document.getElementById("noVNC_view_drag_button") + .addEventListener('click', UI.toggleViewDrag); + + document.getElementById("noVNC_control_bar_handle") + .addEventListener('mousedown', UI.controlbarHandleMouseDown); + document.getElementById("noVNC_control_bar_handle") + .addEventListener('mouseup', UI.controlbarHandleMouseUp); + document.getElementById("noVNC_control_bar_handle") + .addEventListener('mousemove', UI.dragControlbarHandle); + // resize events aren't available for elements + window.addEventListener('resize', UI.updateControlbarHandle); + + const exps = document.getElementsByClassName("noVNC_expander"); + for (let i = 0;i < exps.length;i++) { + exps[i].addEventListener('click', UI.toggleExpander); + } + }, + + addTouchSpecificHandlers() { + document.getElementById("noVNC_mouse_button0") + .addEventListener('click', () => UI.setMouseButton(1)); + document.getElementById("noVNC_mouse_button1") + .addEventListener('click', () => UI.setMouseButton(2)); + document.getElementById("noVNC_mouse_button2") + .addEventListener('click', () => UI.setMouseButton(4)); + document.getElementById("noVNC_mouse_button4") + .addEventListener('click', () => UI.setMouseButton(0)); + document.getElementById("noVNC_keyboard_button") + .addEventListener('click', UI.toggleVirtualKeyboard); + + UI.touchKeyboard = new Keyboard(document.getElementById('noVNC_keyboardinput')); + UI.touchKeyboard.onkeyevent = UI.keyEvent; + UI.touchKeyboard.grab(); + document.getElementById("noVNC_keyboardinput") + .addEventListener('input', UI.keyInput); + document.getElementById("noVNC_keyboardinput") + .addEventListener('focus', UI.onfocusVirtualKeyboard); + document.getElementById("noVNC_keyboardinput") + .addEventListener('blur', UI.onblurVirtualKeyboard); + document.getElementById("noVNC_keyboardinput") + .addEventListener('submit', () => false); + + document.documentElement + .addEventListener('mousedown', UI.keepVirtualKeyboard, true); + + document.getElementById("noVNC_control_bar") + .addEventListener('touchstart', UI.activateControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('touchmove', UI.activateControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('touchend', UI.activateControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('input', UI.activateControlbar); + + document.getElementById("noVNC_control_bar") + .addEventListener('touchstart', UI.keepControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('input', UI.keepControlbar); + + document.getElementById("noVNC_control_bar_handle") + .addEventListener('touchstart', UI.controlbarHandleMouseDown); + document.getElementById("noVNC_control_bar_handle") + .addEventListener('touchend', UI.controlbarHandleMouseUp); + document.getElementById("noVNC_control_bar_handle") + .addEventListener('touchmove', UI.dragControlbarHandle); + }, + + addExtraKeysHandlers() { + document.getElementById("noVNC_toggle_extra_keys_button") + .addEventListener('click', UI.toggleExtraKeys); + document.getElementById("noVNC_toggle_ctrl_button") + .addEventListener('click', UI.toggleCtrl); + document.getElementById("noVNC_toggle_windows_button") + .addEventListener('click', UI.toggleWindows); + document.getElementById("noVNC_toggle_alt_button") + .addEventListener('click', UI.toggleAlt); + document.getElementById("noVNC_send_tab_button") + .addEventListener('click', UI.sendTab); + document.getElementById("noVNC_send_esc_button") + .addEventListener('click', UI.sendEsc); + document.getElementById("noVNC_send_ctrl_alt_del_button") + .addEventListener('click', UI.sendCtrlAltDel); + }, + + addMachineHandlers() { + document.getElementById("noVNC_shutdown_button") + .addEventListener('click', () => UI.rfb.machineShutdown()); + document.getElementById("noVNC_reboot_button") + .addEventListener('click', () => UI.rfb.machineReboot()); + document.getElementById("noVNC_reset_button") + .addEventListener('click', () => UI.rfb.machineReset()); + document.getElementById("noVNC_power_button") + .addEventListener('click', UI.togglePowerPanel); + }, + + addConnectionControlHandlers() { + document.getElementById("noVNC_disconnect_button") + .addEventListener('click', UI.disconnect); + document.getElementById("noVNC_connect_button") + .addEventListener('click', UI.connect); + document.getElementById("noVNC_cancel_reconnect_button") + .addEventListener('click', UI.cancelReconnect); + + document.getElementById("noVNC_password_button") + .addEventListener('click', UI.setPassword); + }, + + addClipboardHandlers() { + document.getElementById("noVNC_clipboard_button") + .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, + // unless the optional parameter changeFunc is used instead. + addSettingChangeHandler(name, changeFunc) { + const settingElem = document.getElementById("noVNC_setting_" + name); + if (changeFunc === undefined) { + changeFunc = () => UI.saveSetting(name); + } + settingElem.addEventListener('change', changeFunc); + }, + + addSettingsHandlers() { + document.getElementById("noVNC_settings_button") + .addEventListener('click', UI.toggleSettingsPanel); + + UI.addSettingChangeHandler('encrypt'); + UI.addSettingChangeHandler('resize'); + UI.addSettingChangeHandler('resize', UI.applyResizeMode); + UI.addSettingChangeHandler('resize', UI.updateViewClip); + UI.addSettingChangeHandler('view_clip'); + UI.addSettingChangeHandler('view_clip', UI.updateViewClip); + UI.addSettingChangeHandler('shared'); + UI.addSettingChangeHandler('view_only'); + UI.addSettingChangeHandler('view_only', UI.updateViewOnly); + UI.addSettingChangeHandler('show_dot'); + UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor); + UI.addSettingChangeHandler('host'); + UI.addSettingChangeHandler('port'); + UI.addSettingChangeHandler('path'); + UI.addSettingChangeHandler('repeaterID'); + UI.addSettingChangeHandler('logging'); + UI.addSettingChangeHandler('logging', UI.updateLogging); + UI.addSettingChangeHandler('reconnect'); + UI.addSettingChangeHandler('reconnect_delay'); + }, + + addFullscreenHandlers() { + document.getElementById("noVNC_fullscreen_button") + .addEventListener('click', UI.toggleFullscreen); + + window.addEventListener('fullscreenchange', UI.updateFullscreenButton); + window.addEventListener('mozfullscreenchange', UI.updateFullscreenButton); + window.addEventListener('webkitfullscreenchange', UI.updateFullscreenButton); + window.addEventListener('msfullscreenchange', UI.updateFullscreenButton); + }, + +/* ------^------- + * /EVENT HANDLERS + * ============== + * VISUAL + * ------v------*/ + + // Disable/enable controls depending on connection state + updateVisualState(state) { + + document.documentElement.classList.remove("noVNC_connecting"); + document.documentElement.classList.remove("noVNC_connected"); + document.documentElement.classList.remove("noVNC_disconnecting"); + document.documentElement.classList.remove("noVNC_reconnecting"); + + const transition_elem = document.getElementById("noVNC_transition_text"); + switch (state) { + case 'init': + break; + case 'connecting': + transition_elem.textContent = _("Connecting..."); + document.documentElement.classList.add("noVNC_connecting"); + break; + case 'connected': + document.documentElement.classList.add("noVNC_connected"); + break; + case 'disconnecting': + transition_elem.textContent = _("Disconnecting..."); + document.documentElement.classList.add("noVNC_disconnecting"); + break; + case 'disconnected': + break; + case 'reconnecting': + transition_elem.textContent = _("Reconnecting..."); + document.documentElement.classList.add("noVNC_reconnecting"); + break; + default: + Log.Error("Invalid visual state: " + state); + UI.showStatus(_("Internal error"), 'error'); + return; + } + + if (UI.connected) { + UI.updateViewClip(); + + UI.disableSetting('encrypt'); + UI.disableSetting('shared'); + UI.disableSetting('host'); + UI.disableSetting('port'); + UI.disableSetting('path'); + UI.disableSetting('repeaterID'); + UI.setMouseButton(1); + + // Hide the controlbar after 2 seconds + UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000); + } else { + UI.enableSetting('encrypt'); + UI.enableSetting('shared'); + UI.enableSetting('host'); + UI.enableSetting('port'); + UI.enableSetting('path'); + UI.enableSetting('repeaterID'); + UI.updatePowerButton(); + UI.keepControlbar(); + } + + // State change closes the password dialog + document.getElementById('noVNC_password_dlg') + .classList.remove('noVNC_open'); + }, + + showStatus(text, status_type, time) { + const statusElem = document.getElementById('noVNC_status'); + + clearTimeout(UI.statusTimeout); + + if (typeof status_type === 'undefined') { + status_type = 'normal'; + } + + // Don't overwrite more severe visible statuses and never + // errors. Only shows the first error. + let visible_status_type = 'none'; + if (statusElem.classList.contains("noVNC_open")) { + if (statusElem.classList.contains("noVNC_status_error")) { + visible_status_type = 'error'; + } else if (statusElem.classList.contains("noVNC_status_warn")) { + visible_status_type = 'warn'; + } else { + visible_status_type = 'normal'; + } + } + if (visible_status_type === 'error' || + (visible_status_type === 'warn' && status_type === 'normal')) { + return; + } + + switch (status_type) { + case 'error': + statusElem.classList.remove("noVNC_status_warn"); + statusElem.classList.remove("noVNC_status_normal"); + statusElem.classList.add("noVNC_status_error"); + break; + case 'warning': + case 'warn': + statusElem.classList.remove("noVNC_status_error"); + statusElem.classList.remove("noVNC_status_normal"); + statusElem.classList.add("noVNC_status_warn"); + break; + case 'normal': + case 'info': + default: + statusElem.classList.remove("noVNC_status_error"); + statusElem.classList.remove("noVNC_status_warn"); + statusElem.classList.add("noVNC_status_normal"); + break; + } + + statusElem.textContent = text; + statusElem.classList.add("noVNC_open"); + + // If no time was specified, show the status for 1.5 seconds + if (typeof time === 'undefined') { + time = 1500; + } + + // Error messages do not timeout + if (status_type !== 'error') { + UI.statusTimeout = window.setTimeout(UI.hideStatus, time); + } + }, + + hideStatus() { + clearTimeout(UI.statusTimeout); + document.getElementById('noVNC_status').classList.remove("noVNC_open"); + }, + + activateControlbar(event) { + clearTimeout(UI.idleControlbarTimeout); + // We manipulate the anchor instead of the actual control + // bar in order to avoid creating new a stacking group + document.getElementById('noVNC_control_bar_anchor') + .classList.remove("noVNC_idle"); + UI.idleControlbarTimeout = window.setTimeout(UI.idleControlbar, 2000); + }, + + idleControlbar() { + document.getElementById('noVNC_control_bar_anchor') + .classList.add("noVNC_idle"); + }, + + keepControlbar() { + clearTimeout(UI.closeControlbarTimeout); + }, + + openControlbar() { + document.getElementById('noVNC_control_bar') + .classList.add("noVNC_open"); + }, + + closeControlbar() { + UI.closeAllPanels(); + document.getElementById('noVNC_control_bar') + .classList.remove("noVNC_open"); + }, + + toggleControlbar() { + if (document.getElementById('noVNC_control_bar') + .classList.contains("noVNC_open")) { + UI.closeControlbar(); + } else { + UI.openControlbar(); + } + }, + + toggleControlbarSide() { + // Temporarily disable animation, if bar is displayed, to avoid weird + // movement. The transitionend-event will not fire when display=none. + const bar = document.getElementById('noVNC_control_bar'); + const barDisplayStyle = window.getComputedStyle(bar).display; + if (barDisplayStyle !== 'none') { + bar.style.transitionDuration = '0s'; + bar.addEventListener('transitionend', () => bar.style.transitionDuration = ''); + } + + const anchor = document.getElementById('noVNC_control_bar_anchor'); + if (anchor.classList.contains("noVNC_right")) { + WebUtil.writeSetting('controlbar_pos', 'left'); + anchor.classList.remove("noVNC_right"); + } else { + WebUtil.writeSetting('controlbar_pos', 'right'); + anchor.classList.add("noVNC_right"); + } + + // Consider this a movement of the handle + UI.controlbarDrag = true; + }, + + showControlbarHint(show) { + const hint = document.getElementById('noVNC_control_bar_hint'); + if (show) { + hint.classList.add("noVNC_active"); + } else { + hint.classList.remove("noVNC_active"); + } + }, + + dragControlbarHandle(e) { + if (!UI.controlbarGrabbed) return; + + const ptr = getPointerEvent(e); + + const anchor = document.getElementById('noVNC_control_bar_anchor'); + if (ptr.clientX < (window.innerWidth * 0.1)) { + if (anchor.classList.contains("noVNC_right")) { + UI.toggleControlbarSide(); + } + } else if (ptr.clientX > (window.innerWidth * 0.9)) { + if (!anchor.classList.contains("noVNC_right")) { + UI.toggleControlbarSide(); + } + } + + if (!UI.controlbarDrag) { + const dragDistance = Math.abs(ptr.clientY - UI.controlbarMouseDownClientY); + + if (dragDistance < dragThreshold) return; + + UI.controlbarDrag = true; + } + + const eventY = ptr.clientY - UI.controlbarMouseDownOffsetY; + + UI.moveControlbarHandle(eventY); + + e.preventDefault(); + e.stopPropagation(); + UI.keepControlbar(); + UI.activateControlbar(); + }, + + // Move the handle but don't allow any position outside the bounds + moveControlbarHandle(viewportRelativeY) { + const handle = document.getElementById("noVNC_control_bar_handle"); + const handleHeight = handle.getBoundingClientRect().height; + const controlbarBounds = document.getElementById("noVNC_control_bar") + .getBoundingClientRect(); + const margin = 10; + + // These heights need to be non-zero for the below logic to work + if (handleHeight === 0 || controlbarBounds.height === 0) { + return; + } + + let newY = viewportRelativeY; + + // Check if the coordinates are outside the control bar + if (newY < controlbarBounds.top + margin) { + // Force coordinates to be below the top of the control bar + newY = controlbarBounds.top + margin; + + } else if (newY > controlbarBounds.top + + controlbarBounds.height - handleHeight - margin) { + // Force coordinates to be above the bottom of the control bar + newY = controlbarBounds.top + + controlbarBounds.height - handleHeight - margin; + } + + // Corner case: control bar too small for stable position + if (controlbarBounds.height < (handleHeight + margin * 2)) { + newY = controlbarBounds.top + + (controlbarBounds.height - handleHeight) / 2; + } + + // The transform needs coordinates that are relative to the parent + const parentRelativeY = newY - controlbarBounds.top; + handle.style.transform = "translateY(" + parentRelativeY + "px)"; + }, + + updateControlbarHandle() { + // Since the control bar is fixed on the viewport and not the page, + // the move function expects coordinates relative the the viewport. + const handle = document.getElementById("noVNC_control_bar_handle"); + const handleBounds = handle.getBoundingClientRect(); + UI.moveControlbarHandle(handleBounds.top); + }, + + controlbarHandleMouseUp(e) { + if ((e.type == "mouseup") && (e.button != 0)) return; + + // mouseup and mousedown on the same place toggles the controlbar + if (UI.controlbarGrabbed && !UI.controlbarDrag) { + UI.toggleControlbar(); + e.preventDefault(); + e.stopPropagation(); + UI.keepControlbar(); + UI.activateControlbar(); + } + UI.controlbarGrabbed = false; + UI.showControlbarHint(false); + }, + + controlbarHandleMouseDown(e) { + if ((e.type == "mousedown") && (e.button != 0)) return; + + const ptr = getPointerEvent(e); + + const handle = document.getElementById("noVNC_control_bar_handle"); + const bounds = handle.getBoundingClientRect(); + + // Touch events have implicit capture + if (e.type === "mousedown") { + setCapture(handle); + } + + UI.controlbarGrabbed = true; + UI.controlbarDrag = false; + + UI.showControlbarHint(true); + + UI.controlbarMouseDownClientY = ptr.clientY; + UI.controlbarMouseDownOffsetY = ptr.clientY - bounds.top; + e.preventDefault(); + e.stopPropagation(); + UI.keepControlbar(); + UI.activateControlbar(); + }, + + toggleExpander(e) { + if (this.classList.contains("noVNC_open")) { + this.classList.remove("noVNC_open"); + } else { + this.classList.add("noVNC_open"); + } + }, + +/* ------^------- + * /VISUAL + * ============== + * SETTINGS + * ------v------*/ + + // Initial page load read/initialization of settings + initSetting(name, defVal) { + // Check Query string followed by cookie + let val = WebUtil.getConfigVar(name); + if (val === null) { + val = WebUtil.readSetting(name, defVal); + } + WebUtil.setSetting(name, val); + UI.updateSetting(name); + return val; + }, + + // Set the new value, update and disable form control setting + forceSetting(name, val) { + WebUtil.setSetting(name, val); + UI.updateSetting(name); + UI.disableSetting(name); + }, + + // Update cookie and form control setting. If value is not set, then + // updates from control to current cookie setting. + updateSetting(name) { + + // Update the settings control + let value = UI.getSetting(name); + + const ctrl = document.getElementById('noVNC_setting_' + name); + if (ctrl.type === 'checkbox') { + ctrl.checked = value; + + } else if (typeof ctrl.options !== 'undefined') { + for (let i = 0; i < ctrl.options.length; i += 1) { + if (ctrl.options[i].value === value) { + ctrl.selectedIndex = i; + break; + } + } + } else { + /*Weird IE9 error leads to 'null' appearring + in textboxes instead of ''.*/ + if (value === null) { + value = ""; + } + ctrl.value = value; + } + }, + + // Save control setting to cookie + saveSetting(name) { + const ctrl = document.getElementById('noVNC_setting_' + name); + let val; + if (ctrl.type === 'checkbox') { + val = ctrl.checked; + } else if (typeof ctrl.options !== 'undefined') { + val = ctrl.options[ctrl.selectedIndex].value; + } else { + val = ctrl.value; + } + WebUtil.writeSetting(name, val); + //Log.Debug("Setting saved '" + name + "=" + val + "'"); + return val; + }, + + // Read form control compatible setting from cookie + getSetting(name) { + const ctrl = document.getElementById('noVNC_setting_' + name); + let val = WebUtil.readSetting(name); + if (typeof val !== 'undefined' && val !== null && ctrl.type === 'checkbox') { + if (val.toString().toLowerCase() in {'0': 1, 'no': 1, 'false': 1}) { + val = false; + } else { + val = true; + } + } + return val; + }, + + // These helpers compensate for the lack of parent-selectors and + // previous-sibling-selectors in CSS which are needed when we want to + // disable the labels that belong to disabled input elements. + disableSetting(name) { + const ctrl = document.getElementById('noVNC_setting_' + name); + ctrl.disabled = true; + ctrl.label.classList.add('noVNC_disabled'); + }, + + enableSetting(name) { + const ctrl = document.getElementById('noVNC_setting_' + name); + ctrl.disabled = false; + ctrl.label.classList.remove('noVNC_disabled'); + }, + +/* ------^------- + * /SETTINGS + * ============== + * PANELS + * ------v------*/ + + closeAllPanels() { + UI.closeSettingsPanel(); + UI.closePowerPanel(); + UI.closeClipboardPanel(); + UI.closeExtraKeys(); + }, + +/* ------^------- + * /PANELS + * ============== + * SETTINGS (panel) + * ------v------*/ + + openSettingsPanel() { + UI.closeAllPanels(); + UI.openControlbar(); + + // Refresh UI elements from saved cookies + UI.updateSetting('encrypt'); + UI.updateSetting('view_clip'); + UI.updateSetting('resize'); + UI.updateSetting('shared'); + UI.updateSetting('view_only'); + UI.updateSetting('path'); + UI.updateSetting('repeaterID'); + UI.updateSetting('logging'); + UI.updateSetting('reconnect'); + UI.updateSetting('reconnect_delay'); + + document.getElementById('noVNC_settings') + .classList.add("noVNC_open"); + document.getElementById('noVNC_settings_button') + .classList.add("noVNC_selected"); + }, + + closeSettingsPanel() { + document.getElementById('noVNC_settings') + .classList.remove("noVNC_open"); + document.getElementById('noVNC_settings_button') + .classList.remove("noVNC_selected"); + }, + + toggleSettingsPanel() { + if (document.getElementById('noVNC_settings') + .classList.contains("noVNC_open")) { + UI.closeSettingsPanel(); + } else { + UI.openSettingsPanel(); + } + }, + +/* ------^------- + * /SETTINGS + * ============== + * POWER + * ------v------*/ + + openPowerPanel() { + UI.closeAllPanels(); + UI.openControlbar(); + + document.getElementById('noVNC_power') + .classList.add("noVNC_open"); + document.getElementById('noVNC_power_button') + .classList.add("noVNC_selected"); + }, + + closePowerPanel() { + document.getElementById('noVNC_power') + .classList.remove("noVNC_open"); + document.getElementById('noVNC_power_button') + .classList.remove("noVNC_selected"); + }, + + togglePowerPanel() { + if (document.getElementById('noVNC_power') + .classList.contains("noVNC_open")) { + UI.closePowerPanel(); + } else { + UI.openPowerPanel(); + } + }, + + // Disable/enable power button + updatePowerButton() { + if (UI.connected && + UI.rfb.capabilities.power && + !UI.rfb.viewOnly) { + document.getElementById('noVNC_power_button') + .classList.remove("noVNC_hidden"); + } else { + document.getElementById('noVNC_power_button') + .classList.add("noVNC_hidden"); + // Close power panel if open + UI.closePowerPanel(); + } + }, + +/* ------^------- + * /POWER + * ============== + * CLIPBOARD + * ------v------*/ + + openClipboardPanel() { + UI.closeAllPanels(); + UI.openControlbar(); + + document.getElementById('noVNC_clipboard') + .classList.add("noVNC_open"); + document.getElementById('noVNC_clipboard_button') + .classList.add("noVNC_selected"); + }, + + closeClipboardPanel() { + document.getElementById('noVNC_clipboard') + .classList.remove("noVNC_open"); + document.getElementById('noVNC_clipboard_button') + .classList.remove("noVNC_selected"); + }, + + toggleClipboardPanel() { + if (document.getElementById('noVNC_clipboard') + .classList.contains("noVNC_open")) { + UI.closeClipboardPanel(); + } else { + UI.openClipboardPanel(); + } + }, + + clipboardReceive(e) { + Log.Debug(">> UI.clipboardReceive: " + e.detail.text.substr(0, 40) + "..."); + document.getElementById('noVNC_clipboard_text').value = e.detail.text; + 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) + "..."); + UI.rfb.clipboardPasteFrom(text); + Log.Debug("<< UI.clipboardSend"); + }, + +/* ------^------- + * /CLIPBOARD + * ============== + * CONNECTION + * ------v------*/ + + openConnectPanel() { + document.getElementById('noVNC_connect_dlg') + .classList.add("noVNC_open"); + }, + + closeConnectPanel() { + document.getElementById('noVNC_connect_dlg') + .classList.remove("noVNC_open"); + }, + + connect(event, password) { + + // Ignore when rfb already exists + if (typeof UI.rfb !== 'undefined') { + return; + } + + const host = UI.getSetting('host'); + const port = UI.getSetting('port'); + const path = UI.getSetting('path'); + + if (typeof password === 'undefined') { + password = WebUtil.getConfigVar('password'); + UI.reconnect_password = password; + } + + if (password === null) { + password = undefined; + } + + UI.hideStatus(); + + if (!host) { + Log.Error("Can't connect when host is: " + host); + UI.showStatus(_("Must set host"), 'error'); + return; + } + + UI.closeAllPanels(); + UI.closeConnectPanel(); + + UI.updateVisualState('connecting'); + + let url; + + url = UI.getSetting('encrypt') ? 'wss' : 'ws'; + + url += '://' + host; + if (port) { + url += ':' + port; + } + url += '/' + path; + + UI.rfb = new RFB(document.getElementById('noVNC_container'), url, + { shared: UI.getSetting('shared'), + showDotCursor: UI.getSetting('show_dot'), + repeaterID: UI.getSetting('repeaterID'), + credentials: { password: password } }); + UI.rfb.addEventListener("connect", UI.connectFinished); + UI.rfb.addEventListener("disconnect", UI.disconnectFinished); + UI.rfb.addEventListener("credentialsrequired", UI.credentials); + UI.rfb.addEventListener("securityfailure", UI.securityFailed); + UI.rfb.addEventListener("capabilities", UI.updatePowerButton); + UI.rfb.addEventListener("clipboard", UI.clipboardReceive); + UI.rfb.addEventListener("bell", UI.bell); + UI.rfb.addEventListener("desktopname", UI.updateDesktopName); + UI.rfb.clipViewport = UI.getSetting('view_clip'); + UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale'; + UI.rfb.resizeSession = UI.getSetting('resize') === 'remote'; + + UI.updateViewOnly(); // requires UI.rfb + }, + + disconnect() { + UI.closeAllPanels(); + UI.rfb.disconnect(); + + UI.connected = false; + + // Disable automatic reconnecting + UI.inhibit_reconnect = true; + + UI.updateVisualState('disconnecting'); + + // Don't display the connection settings until we're actually disconnected + }, + + reconnect() { + UI.reconnect_callback = null; + + // if reconnect has been disabled in the meantime, do nothing. + if (UI.inhibit_reconnect) { + return; + } + + UI.connect(null, UI.reconnect_password); + }, + + cancelReconnect() { + if (UI.reconnect_callback !== null) { + clearTimeout(UI.reconnect_callback); + UI.reconnect_callback = null; + } + + UI.updateVisualState('disconnected'); + + UI.openControlbar(); + UI.openConnectPanel(); + }, + + connectFinished(e) { + UI.connected = true; + UI.inhibit_reconnect = false; + + let msg; + if (UI.getSetting('encrypt')) { + msg = _("Connected (encrypted) to ") + UI.desktopName; + } else { + msg = _("Connected (unencrypted) to ") + UI.desktopName; + } + UI.showStatus(msg); + UI.updateVisualState('connected'); + + // Do this last because it can only be used on rendered elements + UI.rfb.focus(); + }, + + disconnectFinished(e) { + const wasConnected = UI.connected; + + // This variable is ideally set when disconnection starts, but + // when the disconnection isn't clean or if it is initiated by + // the server, we need to do it here as well since + // UI.disconnect() won't be used in those cases. + UI.connected = false; + + UI.rfb = undefined; + + if (!e.detail.clean) { + UI.updateVisualState('disconnected'); + if (wasConnected) { + UI.showStatus(_("Something went wrong, connection is closed"), + 'error'); + } else { + UI.showStatus(_("Failed to connect to server"), 'error'); + } + } else if (UI.getSetting('reconnect', false) === true && !UI.inhibit_reconnect) { + UI.updateVisualState('reconnecting'); + + const delay = parseInt(UI.getSetting('reconnect_delay')); + UI.reconnect_callback = setTimeout(UI.reconnect, delay); + return; + } else { + UI.updateVisualState('disconnected'); + UI.showStatus(_("Disconnected"), 'normal'); + } + + UI.openControlbar(); + UI.openConnectPanel(); + }, + + securityFailed(e) { + let msg = ""; + // On security failures we might get a string with a reason + // directly from the server. Note that we can't control if + // this string is translated or not. + if ('reason' in e.detail) { + msg = _("New connection has been rejected with reason: ") + + e.detail.reason; + } else { + msg = _("New connection has been rejected"); + } + UI.showStatus(msg, 'error'); + }, + +/* ------^------- + * /CONNECTION + * ============== + * PASSWORD + * ------v------*/ + + credentials(e) { + // FIXME: handle more types + document.getElementById('noVNC_password_dlg') + .classList.add('noVNC_open'); + + setTimeout(() => document + .getElementById('noVNC_password_input').focus(), 100); + + Log.Warn("Server asked for a password"); + UI.showStatus(_("Password is required"), "warning"); + }, + + setPassword(e) { + // Prevent actually submitting the form + e.preventDefault(); + + const inputElem = document.getElementById('noVNC_password_input'); + const password = inputElem.value; + // Clear the input after reading the password + inputElem.value = ""; + UI.rfb.sendCredentials({ password: password }); + UI.reconnect_password = password; + document.getElementById('noVNC_password_dlg') + .classList.remove('noVNC_open'); + }, + +/* ------^------- + * /PASSWORD + * ============== + * FULLSCREEN + * ------v------*/ + + toggleFullscreen() { + if (document.fullscreenElement || // alternative standard method + document.mozFullScreenElement || // currently working methods + document.webkitFullscreenElement || + document.msFullscreenElement) { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } + } else { + if (document.documentElement.requestFullscreen) { + document.documentElement.requestFullscreen(); + } else if (document.documentElement.mozRequestFullScreen) { + document.documentElement.mozRequestFullScreen(); + } else if (document.documentElement.webkitRequestFullscreen) { + document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); + } else if (document.body.msRequestFullscreen) { + document.body.msRequestFullscreen(); + } + } + UI.updateFullscreenButton(); + }, + + updateFullscreenButton() { + if (document.fullscreenElement || // alternative standard method + document.mozFullScreenElement || // currently working methods + document.webkitFullscreenElement || + document.msFullscreenElement ) { + document.getElementById('noVNC_fullscreen_button') + .classList.add("noVNC_selected"); + } else { + document.getElementById('noVNC_fullscreen_button') + .classList.remove("noVNC_selected"); + } + }, + +/* ------^------- + * /FULLSCREEN + * ============== + * RESIZE + * ------v------*/ + + // Apply remote resizing or local scaling + applyResizeMode() { + if (!UI.rfb) return; + + UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale'; + UI.rfb.resizeSession = UI.getSetting('resize') === 'remote'; + }, + +/* ------^------- + * /RESIZE + * ============== + * VIEW CLIPPING + * ------v------*/ + + // Update viewport clipping property for the connection. The normal + // case is to get the value from the setting. There are special cases + // for when the viewport is scaled or when a touch device is used. + updateViewClip() { + if (!UI.rfb) return; + + const scaling = UI.getSetting('resize') === 'scale'; + + if (scaling) { + // Can't be clipping if viewport is scaled to fit + UI.forceSetting('view_clip', false); + UI.rfb.clipViewport = false; + } else if (isIOS() || isAndroid()) { + // iOS and Android usually have shit scrollbars + UI.forceSetting('view_clip', true); + UI.rfb.clipViewport = true; + } else { + UI.enableSetting('view_clip'); + UI.rfb.clipViewport = UI.getSetting('view_clip'); + } + + // Changing the viewport may change the state of + // the dragging button + UI.updateViewDrag(); + }, + +/* ------^------- + * /VIEW CLIPPING + * ============== + * VIEWDRAG + * ------v------*/ + + toggleViewDrag() { + if (!UI.rfb) return; + + UI.rfb.dragViewport = !UI.rfb.dragViewport; + UI.updateViewDrag(); + }, + + updateViewDrag() { + if (!UI.connected) return; + + const viewDragButton = document.getElementById('noVNC_view_drag_button'); + + if (!UI.rfb.clipViewport && 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; + } + + if (UI.rfb.dragViewport) { + viewDragButton.classList.add("noVNC_selected"); + } else { + viewDragButton.classList.remove("noVNC_selected"); + } + + // Different behaviour for touch vs non-touch + // The button is disabled instead of hidden on touch devices + if (isTouchDevice) { + viewDragButton.classList.remove("noVNC_hidden"); + + if (UI.rfb.clipViewport) { + viewDragButton.disabled = false; + } else { + viewDragButton.disabled = true; + } + } else { + viewDragButton.disabled = false; + + if (UI.rfb.clipViewport) { + viewDragButton.classList.remove("noVNC_hidden"); + } else { + viewDragButton.classList.add("noVNC_hidden"); + } + } + }, + +/* ------^------- + * /VIEWDRAG + * ============== + * KEYBOARD + * ------v------*/ + + showVirtualKeyboard() { + if (!isTouchDevice) return; + + const input = document.getElementById('noVNC_keyboardinput'); + + if (document.activeElement == input) return; + + input.focus(); + + try { + const l = input.value.length; + // Move the caret to the end + input.setSelectionRange(l, l); + } catch (err) { + // setSelectionRange is undefined in Google Chrome + } + }, + + hideVirtualKeyboard() { + if (!isTouchDevice) return; + + const input = document.getElementById('noVNC_keyboardinput'); + + if (document.activeElement != input) return; + + input.blur(); + }, + + toggleVirtualKeyboard() { + if (document.getElementById('noVNC_keyboard_button') + .classList.contains("noVNC_selected")) { + UI.hideVirtualKeyboard(); + } else { + UI.showVirtualKeyboard(); + } + }, + + onfocusVirtualKeyboard(event) { + document.getElementById('noVNC_keyboard_button') + .classList.add("noVNC_selected"); + if (UI.rfb) { + UI.rfb.focusOnClick = false; + } + }, + + onblurVirtualKeyboard(event) { + document.getElementById('noVNC_keyboard_button') + .classList.remove("noVNC_selected"); + if (UI.rfb) { + UI.rfb.focusOnClick = true; + } + }, + + keepVirtualKeyboard(event) { + const input = document.getElementById('noVNC_keyboardinput'); + + // Only prevent focus change if the virtual keyboard is active + if (document.activeElement != input) { + return; + } + + // Only allow focus to move to other elements that need + // focus to function properly + if (event.target.form !== undefined) { + switch (event.target.type) { + case 'text': + case 'email': + case 'search': + case 'password': + case 'tel': + case 'url': + case 'textarea': + case 'select-one': + case 'select-multiple': + return; + } + } + + event.preventDefault(); + }, + + keyboardinputReset() { + const kbi = document.getElementById('noVNC_keyboardinput'); + kbi.value = new Array(UI.defaultKeyboardinputLen).join("_"); + UI.lastKeyboardinput = kbi.value; + }, + + keyEvent(keysym, code, down) { + if (!UI.rfb) return; + + UI.rfb.sendKey(keysym, code, down); + }, + + // When normal keyboard events are left uncought, use the input events from + // the keyboardinput element instead and generate the corresponding key events. + // This code is required since some browsers on Android are inconsistent in + // sending keyCodes in the normal keyboard events when using on screen keyboards. + keyInput(event) { + + if (!UI.rfb) return; + + const newValue = event.target.value; + + if (!UI.lastKeyboardinput) { + UI.keyboardinputReset(); + } + const oldValue = UI.lastKeyboardinput; + + let newLen; + try { + // Try to check caret position since whitespace at the end + // will not be considered by value.length in some browsers + newLen = Math.max(event.target.selectionStart, newValue.length); + } catch (err) { + // selectionStart is undefined in Google Chrome + newLen = newValue.length; + } + const oldLen = oldValue.length; + + let inputs = newLen - oldLen; + let backspaces = inputs < 0 ? -inputs : 0; + + // Compare the old string with the new to account for + // text-corrections or other input that modify existing text + for (let i = 0; i < Math.min(oldLen, newLen); i++) { + if (newValue.charAt(i) != oldValue.charAt(i)) { + inputs = newLen - i; + backspaces = oldLen - i; + break; + } + } + + // Send the key events + for (let i = 0; i < backspaces; i++) { + UI.rfb.sendKey(KeyTable.XK_BackSpace, "Backspace"); + } + for (let i = newLen - inputs; i < newLen; i++) { + UI.rfb.sendKey(keysyms.lookup(newValue.charCodeAt(i))); + } + + // Control the text content length in the keyboardinput element + if (newLen > 2 * UI.defaultKeyboardinputLen) { + UI.keyboardinputReset(); + } else if (newLen < 1) { + // There always have to be some text in the keyboardinput + // element with which backspace can interact. + UI.keyboardinputReset(); + // This sometimes causes the keyboard to disappear for a second + // but it is required for the android keyboard to recognize that + // text has been added to the field + event.target.blur(); + // This has to be ran outside of the input handler in order to work + setTimeout(event.target.focus.bind(event.target), 0); + } else { + UI.lastKeyboardinput = newValue; + } + }, + +/* ------^------- + * /KEYBOARD + * ============== + * EXTRA KEYS + * ------v------*/ + + openExtraKeys() { + UI.closeAllPanels(); + UI.openControlbar(); + + document.getElementById('noVNC_modifiers') + .classList.add("noVNC_open"); + document.getElementById('noVNC_toggle_extra_keys_button') + .classList.add("noVNC_selected"); + }, + + closeExtraKeys() { + document.getElementById('noVNC_modifiers') + .classList.remove("noVNC_open"); + document.getElementById('noVNC_toggle_extra_keys_button') + .classList.remove("noVNC_selected"); + }, + + toggleExtraKeys() { + if (document.getElementById('noVNC_modifiers') + .classList.contains("noVNC_open")) { + UI.closeExtraKeys(); + } else { + UI.openExtraKeys(); + } + }, + + sendEsc() { + UI.rfb.sendKey(KeyTable.XK_Escape, "Escape"); + }, + + sendTab() { + UI.rfb.sendKey(KeyTable.XK_Tab); + }, + + toggleCtrl() { + const btn = document.getElementById('noVNC_toggle_ctrl_button'); + if (btn.classList.contains("noVNC_selected")) { + UI.rfb.sendKey(KeyTable.XK_Control_L, "ControlLeft", false); + btn.classList.remove("noVNC_selected"); + } else { + UI.rfb.sendKey(KeyTable.XK_Control_L, "ControlLeft", true); + btn.classList.add("noVNC_selected"); + } + }, + + toggleWindows() { + const btn = document.getElementById('noVNC_toggle_windows_button'); + if (btn.classList.contains("noVNC_selected")) { + UI.rfb.sendKey(KeyTable.XK_Super_L, "MetaLeft", false); + btn.classList.remove("noVNC_selected"); + } else { + UI.rfb.sendKey(KeyTable.XK_Super_L, "MetaLeft", true); + btn.classList.add("noVNC_selected"); + } + }, + + toggleAlt() { + const btn = document.getElementById('noVNC_toggle_alt_button'); + if (btn.classList.contains("noVNC_selected")) { + UI.rfb.sendKey(KeyTable.XK_Alt_L, "AltLeft", false); + btn.classList.remove("noVNC_selected"); + } else { + UI.rfb.sendKey(KeyTable.XK_Alt_L, "AltLeft", true); + btn.classList.add("noVNC_selected"); + } + }, + + sendCtrlAltDel() { + UI.rfb.sendCtrlAltDel(); + }, + +/* ------^------- + * /EXTRA KEYS + * ============== + * MISC + * ------v------*/ + + setMouseButton(num) { + const view_only = UI.rfb.viewOnly; + if (UI.rfb && !view_only) { + UI.rfb.touchButton = num; + } + + const blist = [0, 1, 2, 4]; + for (let b = 0; b < blist.length; b++) { + const button = document.getElementById('noVNC_mouse_button' + + blist[b]); + if (blist[b] === num && !view_only) { + button.classList.remove("noVNC_hidden"); + } else { + button.classList.add("noVNC_hidden"); + } + } + }, + + updateViewOnly() { + if (!UI.rfb) return; + UI.rfb.viewOnly = UI.getSetting('view_only'); + + // Hide input related buttons in view only mode + if (UI.rfb.viewOnly) { + document.getElementById('noVNC_keyboard_button') + .classList.add('noVNC_hidden'); + document.getElementById('noVNC_toggle_extra_keys_button') + .classList.add('noVNC_hidden'); + document.getElementById('noVNC_mouse_button' + UI.rfb.touchButton) + .classList.add('noVNC_hidden'); + } else { + document.getElementById('noVNC_keyboard_button') + .classList.remove('noVNC_hidden'); + document.getElementById('noVNC_toggle_extra_keys_button') + .classList.remove('noVNC_hidden'); + document.getElementById('noVNC_mouse_button' + UI.rfb.touchButton) + .classList.remove('noVNC_hidden'); + } + }, + + updateShowDotCursor() { + if (!UI.rfb) return; + UI.rfb.showDotCursor = UI.getSetting('show_dot'); + }, + + updateLogging() { + WebUtil.init_logging(UI.getSetting('logging')); + }, + + updateDesktopName(e) { + UI.desktopName = e.detail.name; + // Display the desktop name in the document title + document.title = e.detail.name + " - noVNC"; + }, + + bell(e) { + if (WebUtil.getConfigVar('bell', 'on') === 'on') { + const promise = document.getElementById('noVNC_bell').play(); + // The standards disagree on the return value here + if (promise) { + promise.catch((e) => { + if (e.name === "NotAllowedError") { + // Ignore when the browser doesn't let us play audio. + // It is common that the browsers require audio to be + // initiated from a user action. + } else { + Log.Error("Unable to play bell: " + e); + } + }); + } + } + }, + + //Helper to add options to dropdown. + addOption(selectbox, text, value) { + const optn = document.createElement("OPTION"); + optn.text = text; + optn.value = value; + selectbox.options.add(optn); + }, + +/* ------^------- + * /MISC + * ============== + */ +}; + +// Set up translations +const LINGUAS = ["cs", "de", "el", "es", "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); +} + +export default UI; diff --git a/systemvm/debian/root/noVNC/app/webutil.js b/systemvm/debian/root/noVNC/app/webutil.js new file mode 100644 index 000000000000..98e1d9e68da4 --- /dev/null +++ b/systemvm/debian/root/noVNC/app/webutil.js @@ -0,0 +1,239 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +import { init_logging as main_init_logging } from '../core/util/logging.js'; + +// init log level reading the logging HTTP param +export function init_logging(level) { + "use strict"; + if (typeof level !== "undefined") { + main_init_logging(level); + } else { + const param = document.location.href.match(/logging=([A-Za-z0-9._-]*)/); + main_init_logging(param || undefined); + } +} + +// Read a query string variable +export function getQueryVar(name, defVal) { + "use strict"; + const re = new RegExp('.*[?&]' + name + '=([^&#]*)'), + match = document.location.href.match(re); + if (typeof defVal === 'undefined') { defVal = null; } + + if (match) { + return decodeURIComponent(match[1]); + } + + return defVal; +} + +// Read a hash fragment variable +export function getHashVar(name, defVal) { + "use strict"; + const re = new RegExp('.*[&#]' + name + '=([^&]*)'), + match = document.location.hash.match(re); + if (typeof defVal === 'undefined') { defVal = null; } + + if (match) { + return decodeURIComponent(match[1]); + } + + return defVal; +} + +// Read a variable from the fragment or the query string +// Fragment takes precedence +export function getConfigVar(name, defVal) { + "use strict"; + const val = getHashVar(name); + + if (val === null) { + return getQueryVar(name, defVal); + } + + return val; +} + +/* + * Cookie handling. Dervied from: http://www.quirksmode.org/js/cookies.html + */ + +// No days means only for this browser session +export function createCookie(name, value, days) { + "use strict"; + let date, expires; + if (days) { + date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = "; expires=" + date.toGMTString(); + } else { + expires = ""; + } + + let secure; + if (document.location.protocol === "https:") { + secure = "; secure"; + } else { + secure = ""; + } + document.cookie = name + "=" + value + expires + "; path=/" + secure; +} + +export function readCookie(name, defaultValue) { + "use strict"; + const nameEQ = name + "="; + const ca = document.cookie.split(';'); + + for (let i = 0; i < ca.length; i += 1) { + let c = ca[i]; + while (c.charAt(0) === ' ') { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) === 0) { + return c.substring(nameEQ.length, c.length); + } + } + + return (typeof defaultValue !== 'undefined') ? defaultValue : null; +} + +export function eraseCookie(name) { + "use strict"; + createCookie(name, "", -1); +} + +/* + * Setting handling. + */ + +let settings = {}; + +export function initSettings() { + if (!window.chrome || !window.chrome.storage) { + settings = {}; + return Promise.resolve(); + } + + return new Promise(resolve => window.chrome.storage.sync.get(resolve)) + .then((cfg) => { settings = cfg; }); +} + +// Update the settings cache, but do not write to permanent storage +export function setSetting(name, value) { + settings[name] = value; +} + +// No days means only for this browser session +export function writeSetting(name, value) { + "use strict"; + if (settings[name] === value) return; + settings[name] = value; + if (window.chrome && window.chrome.storage) { + window.chrome.storage.sync.set(settings); + } else { + localStorage.setItem(name, value); + } +} + +export function readSetting(name, defaultValue) { + "use strict"; + let value; + if ((name in settings) || (window.chrome && window.chrome.storage)) { + value = settings[name]; + } else { + value = localStorage.getItem(name); + settings[name] = value; + } + if (typeof value === "undefined") { + value = null; + } + + if (value === null && typeof defaultValue !== "undefined") { + return defaultValue; + } + + return value; +} + +export function eraseSetting(name) { + "use strict"; + // Deleting here means that next time the setting is read when using local + // storage, it will be pulled from local storage again. + // If the setting in local storage is changed (e.g. in another tab) + // between this delete and the next read, it could lead to an unexpected + // value change. + delete settings[name]; + if (window.chrome && window.chrome.storage) { + window.chrome.storage.sync.remove(name); + } else { + localStorage.removeItem(name); + } +} + +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 param_eq = encodeURIComponent(param) + "="; + let query; + if (elem.search) { + query = elem.search.slice(1).split('&'); + } else { + query = []; + } + + if (!query.some(v => v.startsWith(param_eq))) { + query.push(param_eq + encodeURIComponent(value)); + elem.search = "?" + query.join("&"); + } + + // some browsers (e.g. IE11) may occasionally omit the leading slash + // in the elem.pathname string. Handle that case gracefully. + if (elem.pathname.charAt(0) == "/") { + return elem.pathname.slice(1) + elem.search + elem.hash; + } + + return elem.pathname + elem.search + elem.hash; +} + +// sadly, we can't use the Fetch API until we decide to drop +// IE11 support or polyfill promises and fetch in IE11. +// resolve will receive an object on success, while reject +// will receive either an event or an error on failure. +export function fetchJSON(path) { + return new Promise((resolve, reject) => { + // NB: IE11 doesn't support JSON as a responseType + const req = new XMLHttpRequest(); + req.open('GET', path); + + req.onload = () => { + if (req.status === 200) { + let resObj; + try { + resObj = JSON.parse(req.responseText); + } catch (err) { + reject(err); + } + resolve(resObj); + } else { + reject(new Error("XHR got non-200 status while trying to load '" + path + "': " + req.status)); + } + }; + + req.onerror = evt => reject(new Error("XHR encountered an error while trying to load '" + path + "': " + evt.message)); + + req.ontimeout = evt => reject(new Error("XHR timed out while trying to load '" + path + "'")); + + req.send(); + }); +} diff --git a/systemvm/debian/root/noVNC/core/base64.js b/systemvm/debian/root/noVNC/core/base64.js new file mode 100644 index 000000000000..88e745466e52 --- /dev/null +++ b/systemvm/debian/root/noVNC/core/base64.js @@ -0,0 +1,104 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// From: http://hg.mozilla.org/mozilla-central/raw-file/ec10630b1a54/js/src/devtools/jint/sunspider/string-base64.js + +import * as Log from './util/logging.js'; + +export default { + /* Convert data (an array of integers) to a Base64 string. */ + toBase64Table: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''), + base64Pad: '=', + + encode(data) { + "use strict"; + let result = ''; + const length = data.length; + const lengthpad = (length % 3); + // Convert every three bytes to 4 ascii characters. + + for (let i = 0; i < (length - 2); i += 3) { + result += this.toBase64Table[data[i] >> 2]; + result += this.toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)]; + result += this.toBase64Table[((data[i + 1] & 0x0f) << 2) + (data[i + 2] >> 6)]; + result += this.toBase64Table[data[i + 2] & 0x3f]; + } + + // Convert the remaining 1 or 2 bytes, pad out to 4 characters. + const j = length - lengthpad; + if (lengthpad === 2) { + result += this.toBase64Table[data[j] >> 2]; + result += this.toBase64Table[((data[j] & 0x03) << 4) + (data[j + 1] >> 4)]; + result += this.toBase64Table[(data[j + 1] & 0x0f) << 2]; + result += this.toBase64Table[64]; + } else if (lengthpad === 1) { + result += this.toBase64Table[data[j] >> 2]; + result += this.toBase64Table[(data[j] & 0x03) << 4]; + result += this.toBase64Table[64]; + result += this.toBase64Table[64]; + } + + return result; + }, + + /* Convert Base64 data to a string */ + /* eslint-disable comma-spacing */ + toBinaryTable: [ + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63, + 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, + 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1, + -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, + 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1 + ], + /* eslint-enable comma-spacing */ + + decode(data, offset = 0) { + let data_length = data.indexOf('=') - offset; + if (data_length < 0) { data_length = data.length - offset; } + + /* Every four characters is 3 resulting numbers */ + const result_length = (data_length >> 2) * 3 + Math.floor((data_length % 4) / 1.5); + const result = new Array(result_length); + + // Convert one by one. + + let leftbits = 0; // number of bits decoded, but yet to be appended + let leftdata = 0; // bits decoded, but yet to be appended + for (let idx = 0, i = offset; i < data.length; i++) { + const c = this.toBinaryTable[data.charCodeAt(i) & 0x7f]; + const padding = (data.charAt(i) === this.base64Pad); + // Skip illegal characters and whitespace + if (c === -1) { + Log.Error("Illegal character code " + data.charCodeAt(i) + " at position " + i); + continue; + } + + // Collect data into leftdata, update bitcount + leftdata = (leftdata << 6) | c; + leftbits += 6; + + // If we have 8 or more bits, append 8 bits to the result + if (leftbits >= 8) { + leftbits -= 8; + // Append if not padding. + if (!padding) { + result[idx++] = (leftdata >> leftbits) & 0xff; + } + leftdata &= (1 << leftbits) - 1; + } + } + + // If there are any bits left, the base64 string was corrupted + if (leftbits) { + const err = new Error('Corrupted base64 string'); + err.name = 'Base64-Error'; + throw err; + } + + return result; + } +}; /* End of Base64 namespace */ diff --git a/systemvm/debian/root/noVNC/core/decoders/copyrect.js b/systemvm/debian/root/noVNC/core/decoders/copyrect.js new file mode 100644 index 000000000000..a78ded754f9a --- /dev/null +++ b/systemvm/debian/root/noVNC/core/decoders/copyrect.js @@ -0,0 +1,24 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2018 Samuel Mannehed for Cendio AB + * Copyright (C) 2018 Pierre Ossman for Cendio AB + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +export default class CopyRectDecoder { + decodeRect(x, y, width, height, sock, display, depth) { + if (sock.rQwait("COPYRECT", 4)) { + return false; + } + + let deltaX = sock.rQshift16(); + let deltaY = sock.rQshift16(); + display.copyImage(deltaX, deltaY, x, y, width, height); + + return true; + } +} diff --git a/systemvm/debian/root/noVNC/core/decoders/hextile.js b/systemvm/debian/root/noVNC/core/decoders/hextile.js new file mode 100644 index 000000000000..aa76d2f37b1b --- /dev/null +++ b/systemvm/debian/root/noVNC/core/decoders/hextile.js @@ -0,0 +1,139 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2018 Samuel Mannehed for Cendio AB + * Copyright (C) 2018 Pierre Ossman for Cendio AB + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +import * as Log from '../util/logging.js'; + +export default class HextileDecoder { + constructor() { + this._tiles = 0; + this._lastsubencoding = 0; + } + + decodeRect(x, y, width, height, sock, display, depth) { + if (this._tiles === 0) { + this._tiles_x = Math.ceil(width / 16); + this._tiles_y = Math.ceil(height / 16); + this._total_tiles = this._tiles_x * this._tiles_y; + this._tiles = this._total_tiles; + } + + while (this._tiles > 0) { + let bytes = 1; + + if (sock.rQwait("HEXTILE", bytes)) { + return false; + } + + let rQ = sock.rQ; + let rQi = sock.rQi; + + let subencoding = rQ[rQi]; // Peek + if (subencoding > 30) { // Raw + throw new Error("Illegal hextile subencoding (subencoding: " + + subencoding + ")"); + } + + const curr_tile = this._total_tiles - this._tiles; + const tile_x = curr_tile % this._tiles_x; + const tile_y = Math.floor(curr_tile / this._tiles_x); + const tx = x + tile_x * 16; + const ty = y + tile_y * 16; + const tw = Math.min(16, (x + width) - tx); + const th = Math.min(16, (y + height) - ty); + + // Figure out how much we are expecting + if (subencoding & 0x01) { // Raw + bytes += tw * th * 4; + } else { + if (subencoding & 0x02) { // Background + bytes += 4; + } + if (subencoding & 0x04) { // Foreground + bytes += 4; + } + if (subencoding & 0x08) { // AnySubrects + bytes++; // Since we aren't shifting it off + + if (sock.rQwait("HEXTILE", bytes)) { + return false; + } + + let subrects = rQ[rQi + bytes - 1]; // Peek + if (subencoding & 0x10) { // SubrectsColoured + bytes += subrects * (4 + 2); + } else { + bytes += subrects * 2; + } + } + } + + if (sock.rQwait("HEXTILE", bytes)) { + return false; + } + + // We know the encoding and have a whole tile + rQi++; + if (subencoding === 0) { + if (this._lastsubencoding & 0x01) { + // Weird: ignore blanks are RAW + Log.Debug(" Ignoring blank after RAW"); + } else { + display.fillRect(tx, ty, tw, th, this._background); + } + } else if (subencoding & 0x01) { // Raw + display.blitImage(tx, ty, tw, th, rQ, rQi); + rQi += bytes - 1; + } else { + if (subencoding & 0x02) { // Background + this._background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; + rQi += 4; + } + if (subencoding & 0x04) { // Foreground + this._foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; + rQi += 4; + } + + display.startTile(tx, ty, tw, th, this._background); + if (subencoding & 0x08) { // AnySubrects + let subrects = rQ[rQi]; + rQi++; + + 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; + } else { + color = this._foreground; + } + const xy = rQ[rQi]; + rQi++; + const sx = (xy >> 4); + const sy = (xy & 0x0f); + + const wh = rQ[rQi]; + rQi++; + const sw = (wh >> 4) + 1; + const sh = (wh & 0x0f) + 1; + + display.subTile(sx, sy, sw, sh, color); + } + } + display.finishTile(); + } + sock.rQi = rQi; + this._lastsubencoding = subencoding; + this._tiles--; + } + + return true; + } +} diff --git a/systemvm/debian/root/noVNC/core/decoders/raw.js b/systemvm/debian/root/noVNC/core/decoders/raw.js new file mode 100644 index 000000000000..f676e0d941f7 --- /dev/null +++ b/systemvm/debian/root/noVNC/core/decoders/raw.js @@ -0,0 +1,58 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2018 Samuel Mannehed for Cendio AB + * Copyright (C) 2018 Pierre Ossman for Cendio AB + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +export default class RawDecoder { + constructor() { + this._lines = 0; + } + + decodeRect(x, y, width, height, sock, display, depth) { + if (this._lines === 0) { + this._lines = height; + } + + const pixelSize = depth == 8 ? 1 : 4; + const bytesPerLine = width * pixelSize; + + if (sock.rQwait("RAW", bytesPerLine)) { + return false; + } + + const cur_y = y + (height - this._lines); + const curr_height = 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 * curr_height; + 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; + } + data = newdata; + index = 0; + } + + display.blitImage(x, cur_y, width, curr_height, data, index); + sock.rQskipBytes(curr_height * bytesPerLine); + this._lines -= curr_height; + if (this._lines > 0) { + return false; + } + + return true; + } +} diff --git a/systemvm/debian/root/noVNC/core/decoders/rre.js b/systemvm/debian/root/noVNC/core/decoders/rre.js new file mode 100644 index 000000000000..57414a098ff7 --- /dev/null +++ b/systemvm/debian/root/noVNC/core/decoders/rre.js @@ -0,0 +1,46 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2018 Samuel Mannehed for Cendio AB + * Copyright (C) 2018 Pierre Ossman for Cendio AB + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +export default class RREDecoder { + constructor() { + this._subrects = 0; + } + + decodeRect(x, y, width, height, sock, display, depth) { + if (this._subrects === 0) { + if (sock.rQwait("RRE", 4 + 4)) { + return false; + } + + this._subrects = sock.rQshift32(); + + let color = sock.rQshiftBytes(4); // Background + display.fillRect(x, y, width, height, color); + } + + while (this._subrects > 0) { + if (sock.rQwait("RRE", 4 + 8)) { + return false; + } + + let color = sock.rQshiftBytes(4); + let sx = sock.rQshift16(); + let sy = sock.rQshift16(); + let swidth = sock.rQshift16(); + let sheight = sock.rQshift16(); + display.fillRect(x + sx, y + sy, swidth, sheight, color); + + this._subrects--; + } + + return true; + } +} diff --git a/systemvm/debian/root/noVNC/core/decoders/tight.js b/systemvm/debian/root/noVNC/core/decoders/tight.js new file mode 100644 index 000000000000..bcda04ce7f83 --- /dev/null +++ b/systemvm/debian/root/noVNC/core/decoders/tight.js @@ -0,0 +1,319 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca) + * Copyright (C) 2018 Samuel Mannehed for Cendio AB + * Copyright (C) 2018 Pierre Ossman for Cendio AB + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +import * as Log from '../util/logging.js'; +import Inflator from "../inflator.js"; + +export default class TightDecoder { + constructor() { + this._ctl = null; + this._filter = null; + this._numColors = 0; + this._palette = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel) + this._len = 0; + + this._zlibs = []; + for (let i = 0; i < 4; i++) { + this._zlibs[i] = new Inflator(); + } + } + + decodeRect(x, y, width, height, sock, display, depth) { + if (this._ctl === null) { + if (sock.rQwait("TIGHT compression-control", 1)) { + return false; + } + + this._ctl = sock.rQshift8(); + + // Reset streams if the server requests it + for (let i = 0; i < 4; i++) { + if ((this._ctl >> i) & 1) { + this._zlibs[i].reset(); + Log.Info("Reset zlib stream " + i); + } + } + + // Figure out filter + this._ctl = this._ctl >> 4; + } + + let ret; + + if (this._ctl === 0x08) { + ret = this._fillRect(x, y, width, height, + sock, display, depth); + } else if (this._ctl === 0x09) { + ret = this._jpegRect(x, y, width, height, + sock, display, depth); + } else if (this._ctl === 0x0A) { + ret = this._pngRect(x, y, width, height, + sock, display, depth); + } else if ((this._ctl & 0x80) == 0) { + ret = this._basicRect(this._ctl, x, y, width, height, + sock, display, depth); + } else { + throw new Error("Illegal tight compression received (ctl: " + + this._ctl + ")"); + } + + if (ret) { + this._ctl = null; + } + + return ret; + } + + _fillRect(x, y, width, height, sock, display, depth) { + if (sock.rQwait("TIGHT", 3)) { + 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); + + return true; + } + + _jpegRect(x, y, width, height, sock, display, depth) { + let data = this._readData(sock); + if (data === null) { + return false; + } + + display.imageRect(x, y, "image/jpeg", data); + + return true; + } + + _pngRect(x, y, width, height, sock, display, depth) { + throw new Error("PNG received in standard Tight rect"); + } + + _basicRect(ctl, x, y, width, height, sock, display, depth) { + if (this._filter === null) { + if (ctl & 0x4) { + if (sock.rQwait("TIGHT", 1)) { + return false; + } + + this._filter = sock.rQshift8(); + } else { + // Implicit CopyFilter + this._filter = 0; + } + } + + let streamId = ctl & 0x3; + + let ret; + + switch (this._filter) { + case 0: // CopyFilter + ret = this._copyFilter(streamId, x, y, width, height, + sock, display, depth); + break; + case 1: // PaletteFilter + ret = this._paletteFilter(streamId, x, y, width, height, + sock, display, depth); + break; + case 2: // GradientFilter + ret = this._gradientFilter(streamId, x, y, width, height, + sock, display, depth); + break; + default: + throw new Error("Illegal tight filter received (ctl: " + + this._filter + ")"); + } + + if (ret) { + this._filter = null; + } + + return ret; + } + + _copyFilter(streamId, x, y, width, height, sock, display, depth) { + const uncompressedSize = width * height * 3; + let data; + + if (uncompressedSize < 12) { + if (sock.rQwait("TIGHT", uncompressedSize)) { + return false; + } + + data = sock.rQshiftBytes(uncompressedSize); + } else { + data = this._readData(sock); + if (data === null) { + return false; + } + + data = this._zlibs[streamId].inflate(data, true, uncompressedSize); + if (data.length != uncompressedSize) { + throw new Error("Incomplete zlib block"); + } + } + + display.blitRgbImage(x, y, width, height, data, 0, false); + + return true; + } + + _paletteFilter(streamId, x, y, width, height, sock, display, depth) { + if (this._numColors === 0) { + if (sock.rQwait("TIGHT palette", 1)) { + return false; + } + + const numColors = sock.rQpeek8() + 1; + const paletteSize = numColors * 3; + + if (sock.rQwait("TIGHT palette", 1 + paletteSize)) { + return false; + } + + this._numColors = numColors; + sock.rQskipBytes(1); + + sock.rQshiftTo(this._palette, paletteSize); + } + + const bpp = (this._numColors <= 2) ? 1 : 8; + const rowSize = Math.floor((width * bpp + 7) / 8); + const uncompressedSize = rowSize * height; + + let data; + + if (uncompressedSize < 12) { + if (sock.rQwait("TIGHT", uncompressedSize)) { + return false; + } + + data = sock.rQshiftBytes(uncompressedSize); + } else { + data = this._readData(sock); + if (data === null) { + return false; + } + + data = this._zlibs[streamId].inflate(data, true, uncompressedSize); + if (data.length != uncompressedSize) { + throw new Error("Incomplete zlib block"); + } + } + + // Convert indexed (palette based) image data to RGB + if (this._numColors == 2) { + this._monoRect(x, y, width, height, data, this._palette, display); + } else { + this._paletteRect(x, y, width, height, data, this._palette, display); + } + + this._numColors = 0; + + return true; + } + + _monoRect(x, y, width, height, data, palette, display) { + // Convert indexed (palette based) image data to RGB + // TODO: reduce number of calculations inside loop + const dest = this._getScratchBuffer(width * height * 4); + const w = Math.floor((width + 7) / 8); + const w1 = Math.floor(width / 8); + + for (let y = 0; y < height; y++) { + let dp, sp, x; + for (x = 0; x < w1; x++) { + 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 + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + dest[dp + 3] = 255; + } + } + + 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 + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + dest[dp + 3] = 255; + } + } + + display.blitRgbxImage(x, y, width, height, dest, 0, false); + } + + _paletteRect(x, y, width, height, data, palette, display) { + // Convert indexed (palette based) image data to RGB + const dest = this._getScratchBuffer(width * height * 4); + 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 + 1] = palette[sp + 1]; + dest[i + 2] = palette[sp + 2]; + dest[i + 3] = 255; + } + + display.blitRgbxImage(x, y, width, height, dest, 0, false); + } + + _gradientFilter(streamId, x, y, width, height, sock, display, depth) { + throw new Error("Gradient filter not implemented"); + } + + _readData(sock) { + if (this._len === 0) { + if (sock.rQwait("TIGHT", 3)) { + return null; + } + + let byte; + + byte = sock.rQshift8(); + this._len = byte & 0x7f; + if (byte & 0x80) { + byte = sock.rQshift8(); + this._len |= (byte & 0x7f) << 7; + if (byte & 0x80) { + byte = sock.rQshift8(); + this._len |= byte << 14; + } + } + } + + if (sock.rQwait("TIGHT", this._len)) { + return null; + } + + let data = sock.rQshiftBytes(this._len); + this._len = 0; + + return data; + } + + _getScratchBuffer(size) { + if (!this._scratchBuffer || (this._scratchBuffer.length < size)) { + this._scratchBuffer = new Uint8Array(size); + } + return this._scratchBuffer; + } +} diff --git a/systemvm/debian/root/noVNC/core/decoders/tightpng.js b/systemvm/debian/root/noVNC/core/decoders/tightpng.js new file mode 100644 index 000000000000..7bbde3a43b58 --- /dev/null +++ b/systemvm/debian/root/noVNC/core/decoders/tightpng.js @@ -0,0 +1,29 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2012 Joel Martin + * Copyright (C) 2018 Samuel Mannehed for Cendio AB + * Copyright (C) 2018 Pierre Ossman for Cendio AB + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +import TightDecoder from './tight.js'; + +export default class TightPNGDecoder extends TightDecoder { + _pngRect(x, y, width, height, sock, display, depth) { + let data = this._readData(sock); + if (data === null) { + return false; + } + + display.imageRect(x, y, "image/png", data); + + return true; + } + + _basicRect(ctl, x, y, width, height, sock, display, depth) { + throw new Error("BasicCompression received in TightPNG rect"); + } +} diff --git a/systemvm/debian/root/noVNC/core/des.js b/systemvm/debian/root/noVNC/core/des.js new file mode 100644 index 000000000000..d2f807b828f4 --- /dev/null +++ b/systemvm/debian/root/noVNC/core/des.js @@ -0,0 +1,266 @@ +/* + * Ported from Flashlight VNC ActionScript implementation: + * http://www.wizhelp.com/flashlight-vnc/ + * + * Full attribution follows: + * + * ------------------------------------------------------------------------- + * + * This DES class has been extracted from package Acme.Crypto for use in VNC. + * The unnecessary odd parity code has been removed. + * + * These changes are: + * Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + + * DesCipher - the DES encryption method + * + * The meat of this code is by Dave Zimmerman , and is: + * + * Copyright (c) 1996 Widget Workshop, Inc. All Rights Reserved. + * + * Permission to use, copy, modify, and distribute this software + * and its documentation for NON-COMMERCIAL or COMMERCIAL purposes and + * without fee is hereby granted, provided that this copyright notice is kept + * intact. + * + * WIDGET WORKSHOP MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY + * OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + * TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE, OR NON-INFRINGEMENT. WIDGET WORKSHOP SHALL NOT BE LIABLE + * FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR + * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. + * + * THIS SOFTWARE IS NOT DESIGNED OR INTENDED FOR USE OR RESALE AS ON-LINE + * CONTROL EQUIPMENT IN HAZARDOUS ENVIRONMENTS REQUIRING FAIL-SAFE + * PERFORMANCE, SUCH AS IN THE OPERATION OF NUCLEAR FACILITIES, AIRCRAFT + * NAVIGATION OR COMMUNICATION SYSTEMS, AIR TRAFFIC CONTROL, DIRECT LIFE + * SUPPORT MACHINES, OR WEAPONS SYSTEMS, IN WHICH THE FAILURE OF THE + * SOFTWARE COULD LEAD DIRECTLY TO DEATH, PERSONAL INJURY, OR SEVERE + * PHYSICAL OR ENVIRONMENTAL DAMAGE ("HIGH RISK ACTIVITIES"). WIDGET WORKSHOP + * SPECIFICALLY DISCLAIMS ANY EXPRESS OR IMPLIED WARRANTY OF FITNESS FOR + * HIGH RISK ACTIVITIES. + * + * + * The rest is: + * + * Copyright (C) 1996 by Jef Poskanzer . All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * Visit the ACME Labs Java page for up-to-date versions of this and other + * fine Java utilities: http://www.acme.com/java/ + */ + +/* eslint-disable comma-spacing */ + +// Tables, permutations, S-boxes, etc. +const PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3, + 25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39, + 50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ], + totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28]; + +const z = 0x0; +let a,b,c,d,e,f; +a=1<<16; b=1<<24; c=a|b; d=1<<2; e=1<<10; f=d|e; +const SP1 = [c|e,z|z,a|z,c|f,c|d,a|f,z|d,a|z,z|e,c|e,c|f,z|e,b|f,c|d,b|z,z|d, + z|f,b|e,b|e,a|e,a|e,c|z,c|z,b|f,a|d,b|d,b|d,a|d,z|z,z|f,a|f,b|z, + a|z,c|f,z|d,c|z,c|e,b|z,b|z,z|e,c|d,a|z,a|e,b|d,z|e,z|d,b|f,a|f, + c|f,a|d,c|z,b|f,b|d,z|f,a|f,c|e,z|f,b|e,b|e,z|z,a|d,a|e,z|z,c|d]; +a=1<<20; b=1<<31; c=a|b; d=1<<5; e=1<<15; f=d|e; +const SP2 = [c|f,b|e,z|e,a|f,a|z,z|d,c|d,b|f,b|d,c|f,c|e,b|z,b|e,a|z,z|d,c|d, + a|e,a|d,b|f,z|z,b|z,z|e,a|f,c|z,a|d,b|d,z|z,a|e,z|f,c|e,c|z,z|f, + z|z,a|f,c|d,a|z,b|f,c|z,c|e,z|e,c|z,b|e,z|d,c|f,a|f,z|d,z|e,b|z, + z|f,c|e,a|z,b|d,a|d,b|f,b|d,a|d,a|e,z|z,b|e,z|f,b|z,c|d,c|f,a|e]; +a=1<<17; b=1<<27; c=a|b; d=1<<3; e=1<<9; f=d|e; +const SP3 = [z|f,c|e,z|z,c|d,b|e,z|z,a|f,b|e,a|d,b|d,b|d,a|z,c|f,a|d,c|z,z|f, + b|z,z|d,c|e,z|e,a|e,c|z,c|d,a|f,b|f,a|e,a|z,b|f,z|d,c|f,z|e,b|z, + c|e,b|z,a|d,z|f,a|z,c|e,b|e,z|z,z|e,a|d,c|f,b|e,b|d,z|e,z|z,c|d, + b|f,a|z,b|z,c|f,z|d,a|f,a|e,b|d,c|z,b|f,z|f,c|z,a|f,z|d,c|d,a|e]; +a=1<<13; b=1<<23; c=a|b; d=1<<0; e=1<<7; f=d|e; +const SP4 = [c|d,a|f,a|f,z|e,c|e,b|f,b|d,a|d,z|z,c|z,c|z,c|f,z|f,z|z,b|e,b|d, + z|d,a|z,b|z,c|d,z|e,b|z,a|d,a|e,b|f,z|d,a|e,b|e,a|z,c|e,c|f,z|f, + b|e,b|d,c|z,c|f,z|f,z|z,z|z,c|z,a|e,b|e,b|f,z|d,c|d,a|f,a|f,z|e, + c|f,z|f,z|d,a|z,b|d,a|d,c|e,b|f,a|d,a|e,b|z,c|d,z|e,b|z,a|z,c|e]; +a=1<<25; b=1<<30; c=a|b; d=1<<8; e=1<<19; f=d|e; +const SP5 = [z|d,a|f,a|e,c|d,z|e,z|d,b|z,a|e,b|f,z|e,a|d,b|f,c|d,c|e,z|f,b|z, + a|z,b|e,b|e,z|z,b|d,c|f,c|f,a|d,c|e,b|d,z|z,c|z,a|f,a|z,c|z,z|f, + z|e,c|d,z|d,a|z,b|z,a|e,c|d,b|f,a|d,b|z,c|e,a|f,b|f,z|d,a|z,c|e, + c|f,z|f,c|z,c|f,a|e,z|z,b|e,c|z,z|f,a|d,b|d,z|e,z|z,b|e,a|f,b|d]; +a=1<<22; b=1<<29; c=a|b; d=1<<4; e=1<<14; f=d|e; +const SP6 = [b|d,c|z,z|e,c|f,c|z,z|d,c|f,a|z,b|e,a|f,a|z,b|d,a|d,b|e,b|z,z|f, + z|z,a|d,b|f,z|e,a|e,b|f,z|d,c|d,c|d,z|z,a|f,c|e,z|f,a|e,c|e,b|z, + b|e,z|d,c|d,a|e,c|f,a|z,z|f,b|d,a|z,b|e,b|z,z|f,b|d,c|f,a|e,c|z, + a|f,c|e,z|z,c|d,z|d,z|e,c|z,a|f,z|e,a|d,b|f,z|z,c|e,b|z,a|d,b|f]; +a=1<<21; b=1<<26; c=a|b; d=1<<1; e=1<<11; f=d|e; +const SP7 = [a|z,c|d,b|f,z|z,z|e,b|f,a|f,c|e,c|f,a|z,z|z,b|d,z|d,b|z,c|d,z|f, + b|e,a|f,a|d,b|e,b|d,c|z,c|e,a|d,c|z,z|e,z|f,c|f,a|e,z|d,b|z,a|e, + b|z,a|e,a|z,b|f,b|f,c|d,c|d,z|d,a|d,b|z,b|e,a|z,c|e,z|f,a|f,c|e, + z|f,b|d,c|f,c|z,a|e,z|z,z|d,c|f,z|z,a|f,c|z,z|e,b|d,b|e,z|e,a|d]; +a=1<<18; b=1<<28; c=a|b; d=1<<6; e=1<<12; f=d|e; +const SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d, + c|z,b|d,b|e,z|f,a|e,a|d,c|d,c|e,z|f,z|z,z|z,c|d,b|d,b|e,a|f,a|z, + a|f,a|z,c|e,z|e,z|d,c|d,z|e,a|f,b|e,z|d,b|d,c|z,c|d,b|z,a|z,b|f, + z|z,c|f,a|d,b|d,c|z,b|e,b|f,z|z,c|f,a|e,a|e,z|f,z|f,a|d,b|z,c|e]; + +/* eslint-enable comma-spacing */ + +export default class DES { + constructor(password) { + this.keys = []; + + // Set the key. + const pc1m = [], pcr = [], kn = []; + + for (let j = 0, l = 56; j < 56; ++j, l -= 8) { + l += l < -5 ? 65 : l < -3 ? 31 : l < -1 ? 63 : l === 27 ? 35 : 0; // PC1 + const m = l & 0x7; + pc1m[j] = ((password[l >>> 3] & (1<>> 10; + this.keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6; + ++KnLi; + this.keys[KnLi] = (raw0 & 0x0003f000) << 12; + this.keys[KnLi] |= (raw0 & 0x0000003f) << 16; + this.keys[KnLi] |= (raw1 & 0x0003f000) >>> 4; + this.keys[KnLi] |= (raw1 & 0x0000003f); + ++KnLi; + } + } + + // Encrypt 8 bytes of text + enc8(text) { + const b = text.slice(); + let i = 0, l, r, x; // left, right, accumulator + + // Squash 8 bytes to 2 ints + l = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++]; + r = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++]; + + x = ((l >>> 4) ^ r) & 0x0f0f0f0f; + r ^= x; + l ^= (x << 4); + x = ((l >>> 16) ^ r) & 0x0000ffff; + r ^= x; + l ^= (x << 16); + x = ((r >>> 2) ^ l) & 0x33333333; + l ^= x; + r ^= (x << 2); + x = ((r >>> 8) ^ l) & 0x00ff00ff; + l ^= x; + r ^= (x << 8); + r = (r << 1) | ((r >>> 31) & 1); + x = (l ^ r) & 0xaaaaaaaa; + l ^= x; + r ^= x; + l = (l << 1) | ((l >>> 31) & 1); + + for (let i = 0, keysi = 0; i < 8; ++i) { + x = (r << 28) | (r >>> 4); + x ^= this.keys[keysi++]; + let fval = SP7[x & 0x3f]; + fval |= SP5[(x >>> 8) & 0x3f]; + fval |= SP3[(x >>> 16) & 0x3f]; + fval |= SP1[(x >>> 24) & 0x3f]; + x = r ^ this.keys[keysi++]; + fval |= SP8[x & 0x3f]; + fval |= SP6[(x >>> 8) & 0x3f]; + fval |= SP4[(x >>> 16) & 0x3f]; + fval |= SP2[(x >>> 24) & 0x3f]; + l ^= fval; + x = (l << 28) | (l >>> 4); + x ^= this.keys[keysi++]; + fval = SP7[x & 0x3f]; + fval |= SP5[(x >>> 8) & 0x3f]; + fval |= SP3[(x >>> 16) & 0x3f]; + fval |= SP1[(x >>> 24) & 0x3f]; + x = l ^ this.keys[keysi++]; + fval |= SP8[x & 0x0000003f]; + fval |= SP6[(x >>> 8) & 0x3f]; + fval |= SP4[(x >>> 16) & 0x3f]; + fval |= SP2[(x >>> 24) & 0x3f]; + r ^= fval; + } + + r = (r << 31) | (r >>> 1); + x = (l ^ r) & 0xaaaaaaaa; + l ^= x; + r ^= x; + l = (l << 31) | (l >>> 1); + x = ((l >>> 8) ^ r) & 0x00ff00ff; + r ^= x; + l ^= (x << 8); + x = ((l >>> 2) ^ r) & 0x33333333; + r ^= x; + l ^= (x << 2); + x = ((r >>> 16) ^ l) & 0x0000ffff; + l ^= x; + r ^= (x << 16); + x = ((r >>> 4) ^ l) & 0x0f0f0f0f; + l ^= x; + r ^= (x << 4); + + // Spread ints to bytes + x = [r, l]; + for (i = 0; i < 8; i++) { + b[i] = (x[i>>>2] >>> (8 * (3 - (i % 4)))) % 256; + if (b[i] < 0) { b[i] += 256; } // unsigned + } + return b; + } + + // Encrypt 16 bytes of text using passwd as key + encrypt(t) { + return this.enc8(t.slice(0, 8)).concat(this.enc8(t.slice(8, 16))); + } +} diff --git a/systemvm/debian/root/noVNC/core/display.js b/systemvm/debian/root/noVNC/core/display.js new file mode 100644 index 000000000000..1528384d3afd --- /dev/null +++ b/systemvm/debian/root/noVNC/core/display.js @@ -0,0 +1,654 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +import * as Log from './util/logging.js'; +import Base64 from "./base64.js"; +import { supportsImageMetadata } from './util/browser.js'; + +export default class Display { + constructor(target) { + this._drawCtx = null; + this._c_forceCanvas = false; + + this._renderQ = []; // queue drawing actions for in-oder rendering + this._flushing = false; + + // the full frame buffer (logical canvas) size + this._fb_width = 0; + this._fb_height = 0; + + this._prevDrawStyle = ""; + this._tile = null; + this._tile16x16 = null; + this._tile_x = 0; + this._tile_y = 0; + + Log.Debug(">> Display.constructor"); + + // The visible canvas + this._target = target; + + if (!this._target) { + throw new Error("Target must be set"); + } + + if (typeof this._target === 'string') { + throw new Error('target must be a DOM element'); + } + + if (!this._target.getContext) { + throw new Error("no getContext method"); + } + + this._targetCtx = this._target.getContext('2d'); + + // the visible canvas viewport (i.e. what actually gets seen) + this._viewportLoc = { 'x': 0, 'y': 0, 'w': this._target.width, 'h': this._target.height }; + + // The hidden canvas, where we do the actual rendering + this._backbuffer = document.createElement('canvas'); + this._drawCtx = this._backbuffer.getContext('2d'); + + this._damageBounds = { left: 0, top: 0, + right: this._backbuffer.width, + bottom: this._backbuffer.height }; + + Log.Debug("User Agent: " + navigator.userAgent); + + this.clear(); + + // 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; + this.logo = null; + + // ===== EVENT HANDLERS ===== + + this.onflush = () => {}; // A flush request has finished + } + + // ===== PROPERTIES ===== + + get scale() { return this._scale; } + set scale(scale) { + this._rescale(scale); + } + + get clipViewport() { return this._clipViewport; } + set clipViewport(viewport) { + this._clipViewport = viewport; + // May need to readjust the viewport dimensions + const vp = this._viewportLoc; + this.viewportChangeSize(vp.w, vp.h); + this.viewportChangePos(0, 0); + } + + get width() { + return this._fb_width; + } + + get height() { + return this._fb_height; + } + + // ===== PUBLIC METHODS ===== + + viewportChangePos(deltaX, deltaY) { + const vp = this._viewportLoc; + deltaX = Math.floor(deltaX); + deltaY = Math.floor(deltaY); + + if (!this._clipViewport) { + deltaX = -vp.w; // clamped later of out of bounds + deltaY = -vp.h; + } + + const vx2 = vp.x + vp.w - 1; + const vy2 = vp.y + vp.h - 1; + + // Position change + + if (deltaX < 0 && vp.x + deltaX < 0) { + deltaX = -vp.x; + } + if (vx2 + deltaX >= this._fb_width) { + deltaX -= vx2 + deltaX - this._fb_width + 1; + } + + if (vp.y + deltaY < 0) { + deltaY = -vp.y; + } + if (vy2 + deltaY >= this._fb_height) { + deltaY -= (vy2 + deltaY - this._fb_height + 1); + } + + if (deltaX === 0 && deltaY === 0) { + return; + } + Log.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY); + + vp.x += deltaX; + vp.y += deltaY; + + this._damage(vp.x, vp.y, vp.w, vp.h); + + this.flip(); + } + + viewportChangeSize(width, height) { + + if (!this._clipViewport || + typeof(width) === "undefined" || + typeof(height) === "undefined") { + + Log.Debug("Setting viewport to full display region"); + width = this._fb_width; + height = this._fb_height; + } + + width = Math.floor(width); + height = Math.floor(height); + + if (width > this._fb_width) { + width = this._fb_width; + } + if (height > this._fb_height) { + height = this._fb_height; + } + + const vp = this._viewportLoc; + if (vp.w !== width || vp.h !== height) { + vp.w = width; + vp.h = height; + + const canvas = this._target; + canvas.width = width; + canvas.height = height; + + // The position might need to be updated if we've grown + this.viewportChangePos(0, 0); + + this._damage(vp.x, vp.y, vp.w, vp.h); + this.flip(); + + // Update the visible size of the target canvas + this._rescale(this._scale); + } + } + + absX(x) { + if (this._scale === 0) { + return 0; + } + return x / this._scale + this._viewportLoc.x; + } + + absY(y) { + if (this._scale === 0) { + return 0; + } + return y / this._scale + this._viewportLoc.y; + } + + resize(width, height) { + this._prevDrawStyle = ""; + + this._fb_width = width; + this._fb_height = height; + + const canvas = this._backbuffer; + if (canvas.width !== width || canvas.height !== height) { + + // We have to save the canvas data since changing the size will clear it + let saveImg = null; + if (canvas.width > 0 && canvas.height > 0) { + saveImg = this._drawCtx.getImageData(0, 0, canvas.width, canvas.height); + } + + if (canvas.width !== width) { + canvas.width = width; + } + if (canvas.height !== height) { + canvas.height = height; + } + + if (saveImg) { + this._drawCtx.putImageData(saveImg, 0, 0); + } + } + + // Readjust the viewport as it may be incorrectly sized + // and positioned + const vp = this._viewportLoc; + this.viewportChangeSize(vp.w, vp.h); + this.viewportChangePos(0, 0); + } + + // Track what parts of the visible canvas that need updating + _damage(x, y, w, h) { + if (x < this._damageBounds.left) { + this._damageBounds.left = x; + } + if (y < this._damageBounds.top) { + this._damageBounds.top = y; + } + if ((x + w) > this._damageBounds.right) { + this._damageBounds.right = x + w; + } + if ((y + h) > this._damageBounds.bottom) { + this._damageBounds.bottom = y + h; + } + } + + // Update the visible canvas with the contents of the + // rendering canvas + flip(from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + this._renderQ_push({ + 'type': 'flip' + }); + } else { + let x = this._damageBounds.left; + let y = this._damageBounds.top; + let w = this._damageBounds.right - x; + let h = this._damageBounds.bottom - y; + + let vx = x - this._viewportLoc.x; + let vy = y - this._viewportLoc.y; + + if (vx < 0) { + w += vx; + x -= vx; + vx = 0; + } + if (vy < 0) { + h += vy; + y -= vy; + vy = 0; + } + + if ((vx + w) > this._viewportLoc.w) { + w = this._viewportLoc.w - vx; + } + if ((vy + h) > this._viewportLoc.h) { + h = this._viewportLoc.h - vy; + } + + if ((w > 0) && (h > 0)) { + // FIXME: We may need to disable image smoothing here + // as well (see copyImage()), but we haven't + // noticed any problem yet. + this._targetCtx.drawImage(this._backbuffer, + x, y, w, h, + vx, vy, w, h); + } + + this._damageBounds.left = this._damageBounds.top = 65535; + this._damageBounds.right = this._damageBounds.bottom = 0; + } + } + + clear() { + if (this._logo) { + this.resize(this._logo.width, this._logo.height); + this.imageRect(0, 0, this._logo.type, this._logo.data); + } else { + this.resize(240, 20); + this._drawCtx.clearRect(0, 0, this._fb_width, this._fb_height); + } + this.flip(); + } + + pending() { + return this._renderQ.length > 0; + } + + flush() { + if (this._renderQ.length === 0) { + this.onflush(); + } else { + this._flushing = true; + } + } + + fillRect(x, y, width, height, color, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + this._renderQ_push({ + 'type': 'fill', + 'x': x, + 'y': y, + 'width': width, + 'height': height, + 'color': color + }); + } else { + this._setFillColor(color); + this._drawCtx.fillRect(x, y, width, height); + this._damage(x, y, width, height); + } + } + + copyImage(old_x, old_y, new_x, new_y, w, h, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + this._renderQ_push({ + 'type': 'copy', + 'old_x': old_x, + 'old_y': old_y, + 'x': new_x, + 'y': new_y, + 'width': w, + 'height': h, + }); + } else { + // Due to this bug among others [1] we need to disable the image-smoothing to + // avoid getting a blur effect when copying data. + // + // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719 + // + // We need to set these every time since all properties are reset + // when the the size is changed + this._drawCtx.mozImageSmoothingEnabled = false; + this._drawCtx.webkitImageSmoothingEnabled = false; + this._drawCtx.msImageSmoothingEnabled = false; + this._drawCtx.imageSmoothingEnabled = false; + + this._drawCtx.drawImage(this._backbuffer, + old_x, old_y, w, h, + new_x, new_y, w, h); + this._damage(new_x, new_y, w, h); + } + } + + imageRect(x, y, mime, arr) { + const img = new Image(); + img.src = "data: " + mime + ";base64," + Base64.encode(arr); + this._renderQ_push({ + 'type': 'img', + 'img': img, + 'x': x, + 'y': y + }); + } + + // start updating a tile + startTile(x, y, width, height, color) { + this._tile_x = x; + this._tile_y = 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._tile_x, this._tile_y); + this._damage(this._tile_x, this._tile_y, + this._tile.width, this._tile.height); + } + + blitImage(x, y, width, height, arr, offset, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + // 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 new_arr = new Uint8Array(width * height * 4); + new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); + this._renderQ_push({ + 'type': 'blit', + 'data': new_arr, + 'x': x, + 'y': y, + 'width': width, + 'height': height, + }); + } else { + this._bgrxImageData(x, y, width, height, arr, offset); + } + } + + blitRgbImage(x, y, width, height, arr, offset, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + // 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 new_arr = new Uint8Array(width * height * 3); + new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); + this._renderQ_push({ + 'type': 'blitRgb', + 'data': new_arr, + 'x': x, + 'y': y, + 'width': width, + 'height': height, + }); + } else { + this._rgbImageData(x, y, width, height, arr, offset); + } + } + + blitRgbxImage(x, y, width, height, arr, offset, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + // 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 new_arr = new Uint8Array(width * height * 4); + new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); + this._renderQ_push({ + 'type': 'blitRgbx', + 'data': new_arr, + 'x': x, + 'y': y, + 'width': width, + 'height': height, + }); + } else { + this._rgbxImageData(x, y, width, height, arr, offset); + } + } + + drawImage(img, x, y) { + this._drawCtx.drawImage(img, x, y); + this._damage(x, y, img.width, img.height); + } + + autoscale(containerWidth, containerHeight) { + let scaleRatio; + + if (containerWidth === 0 || containerHeight === 0) { + scaleRatio = 0; + + } else { + + const vp = this._viewportLoc; + const targetAspectRatio = containerWidth / containerHeight; + const fbAspectRatio = vp.w / vp.h; + + if (fbAspectRatio >= targetAspectRatio) { + scaleRatio = containerWidth / vp.w; + } else { + scaleRatio = containerHeight / vp.h; + } + } + + this._rescale(scaleRatio); + } + + // ===== PRIVATE METHODS ===== + + _rescale(factor) { + this._scale = factor; + const vp = this._viewportLoc; + + // NB(directxman12): If you set the width directly, or set the + // style width to a number, the canvas is cleared. + // However, if you set the style width to a string + // ('NNNpx'), the canvas is scaled without clearing. + const width = factor * vp.w + 'px'; + const height = factor * vp.h + 'px'; + + if ((this._target.style.width !== width) || + (this._target.style.height !== height)) { + this._target.style.width = width; + this._target.style.height = height; + } + } + + _setFillColor(color) { + const newStyle = 'rgb(' + color[2] + ',' + color[1] + ',' + color[0] + ')'; + 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); + } + + _renderQ_push(action) { + this._renderQ.push(action); + if (this._renderQ.length === 1) { + // If this can be rendered immediately it will be, otherwise + // the scanner will wait for the relevant event + this._scan_renderQ(); + } + } + + _resume_renderQ() { + // "this" is the object that is ready, not the + // display object + this.removeEventListener('load', this._noVNC_display._resume_renderQ); + this._noVNC_display._scan_renderQ(); + } + + _scan_renderQ() { + let ready = true; + while (ready && this._renderQ.length > 0) { + const a = this._renderQ[0]; + switch (a.type) { + case 'flip': + this.flip(true); + break; + case 'copy': + this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height, true); + break; + case 'fill': + this.fillRect(a.x, a.y, a.width, a.height, a.color, true); + break; + 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': + if (a.img.complete) { + this.drawImage(a.img, a.x, a.y); + } else { + a.img._noVNC_display = this; + a.img.addEventListener('load', this._resume_renderQ); + // We need to wait for this image to 'load' + // to keep things in-order + ready = false; + } + break; + } + + if (ready) { + this._renderQ.shift(); + } + } + + if (this._renderQ.length === 0 && this._flushing) { + this._flushing = false; + this.onflush(); + } + } +} diff --git a/systemvm/debian/root/noVNC/core/encodings.js b/systemvm/debian/root/noVNC/core/encodings.js new file mode 100644 index 000000000000..9fd38d58fcc3 --- /dev/null +++ b/systemvm/debian/root/noVNC/core/encodings.js @@ -0,0 +1,41 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +export const encodings = { + encodingRaw: 0, + encodingCopyRect: 1, + encodingRRE: 2, + encodingHextile: 5, + encodingTight: 7, + encodingTightPNG: -260, + + pseudoEncodingQualityLevel9: -23, + pseudoEncodingQualityLevel0: -32, + pseudoEncodingDesktopSize: -223, + pseudoEncodingLastRect: -224, + pseudoEncodingCursor: -239, + pseudoEncodingQEMUExtendedKeyEvent: -258, + pseudoEncodingExtendedDesktopSize: -308, + pseudoEncodingXvp: -309, + pseudoEncodingFence: -312, + pseudoEncodingContinuousUpdates: -313, + pseudoEncodingCompressLevel9: -247, + pseudoEncodingCompressLevel0: -256, +}; + +export function encodingName(num) { + switch (num) { + case encodings.encodingRaw: return "Raw"; + case encodings.encodingCopyRect: return "CopyRect"; + case encodings.encodingRRE: return "RRE"; + case encodings.encodingHextile: return "Hextile"; + case encodings.encodingTight: return "Tight"; + case encodings.encodingTightPNG: return "TightPNG"; + default: return "[unknown encoding " + num + "]"; + } +} diff --git a/systemvm/debian/root/noVNC/core/inflator.js b/systemvm/debian/root/noVNC/core/inflator.js new file mode 100644 index 000000000000..0eab8fe48c2c --- /dev/null +++ b/systemvm/debian/root/noVNC/core/inflator.js @@ -0,0 +1,38 @@ +import { inflateInit, inflate, inflateReset } from "../vendor/pako/lib/zlib/inflate.js"; +import ZStream from "../vendor/pako/lib/zlib/zstream.js"; + +export default class Inflate { + constructor() { + this.strm = new ZStream(); + this.chunkSize = 1024 * 10 * 10; + this.strm.output = new Uint8Array(this.chunkSize); + this.windowBits = 5; + + inflateInit(this.strm, this.windowBits); + } + + inflate(data, flush, expected) { + this.strm.input = data; + this.strm.avail_in = this.strm.input.length; + this.strm.next_in = 0; + this.strm.next_out = 0; + + // resize our output buffer if it's too small + // (we could just use multiple chunks, but that would cause an extra + // allocation each time to flatten the chunks) + if (expected > this.chunkSize) { + this.chunkSize = expected; + this.strm.output = new Uint8Array(this.chunkSize); + } + + this.strm.avail_out = this.chunkSize; + + inflate(this.strm, flush); + + return new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out); + } + + reset() { + inflateReset(this.strm); + } +} diff --git a/systemvm/debian/root/noVNC/core/input/domkeytable.js b/systemvm/debian/root/noVNC/core/input/domkeytable.js new file mode 100644 index 000000000000..60ae3f91902e --- /dev/null +++ b/systemvm/debian/root/noVNC/core/input/domkeytable.js @@ -0,0 +1,307 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 or any later version (see LICENSE.txt) + */ + +import KeyTable from "./keysym.js"; + +/* + * Mapping between HTML key values and VNC/X11 keysyms for "special" + * keys that cannot be handled via their Unicode codepoint. + * + * See https://www.w3.org/TR/uievents-key/ for possible values. + */ + +const DOMKeyTable = {}; + +function addStandard(key, standard) { + if (standard === undefined) throw new Error("Undefined keysym for key \"" + key + "\""); + if (key in DOMKeyTable) throw new Error("Duplicate entry for key \"" + key + "\""); + DOMKeyTable[key] = [standard, standard, standard, standard]; +} + +function addLeftRight(key, left, right) { + if (left === undefined) throw new Error("Undefined keysym for key \"" + key + "\""); + if (right === undefined) throw new Error("Undefined keysym for key \"" + key + "\""); + if (key in DOMKeyTable) throw new Error("Duplicate entry for key \"" + key + "\""); + DOMKeyTable[key] = [left, left, right, left]; +} + +function addNumpad(key, standard, numpad) { + if (standard === undefined) throw new Error("Undefined keysym for key \"" + key + "\""); + if (numpad === undefined) throw new Error("Undefined keysym for key \"" + key + "\""); + if (key in DOMKeyTable) throw new Error("Duplicate entry for key \"" + key + "\""); + DOMKeyTable[key] = [standard, standard, standard, numpad]; +} + +// 2.2. Modifier Keys + +addLeftRight("Alt", KeyTable.XK_Alt_L, KeyTable.XK_Alt_R); +addStandard("AltGraph", KeyTable.XK_ISO_Level3_Shift); +addStandard("CapsLock", KeyTable.XK_Caps_Lock); +addLeftRight("Control", KeyTable.XK_Control_L, KeyTable.XK_Control_R); +// - Fn +// - FnLock +addLeftRight("Hyper", KeyTable.XK_Super_L, KeyTable.XK_Super_R); +addLeftRight("Meta", KeyTable.XK_Super_L, KeyTable.XK_Super_R); +addStandard("NumLock", KeyTable.XK_Num_Lock); +addStandard("ScrollLock", KeyTable.XK_Scroll_Lock); +addLeftRight("Shift", KeyTable.XK_Shift_L, KeyTable.XK_Shift_R); +addLeftRight("Super", KeyTable.XK_Super_L, KeyTable.XK_Super_R); +// - Symbol +// - SymbolLock + +// 2.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 + +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("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 + +addStandard("Backspace", KeyTable.XK_BackSpace); +addNumpad("Clear", KeyTable.XK_Clear, KeyTable.XK_KP_Begin); +addStandard("Copy", KeyTable.XF86XK_Copy); +// - CrSel +addStandard("Cut", KeyTable.XF86XK_Cut); +addNumpad("Delete", KeyTable.XK_Delete, KeyTable.XK_KP_Delete); +// - EraseEof +// - ExSel +addNumpad("Insert", KeyTable.XK_Insert, KeyTable.XK_KP_Insert); +addStandard("Paste", KeyTable.XF86XK_Paste); +addStandard("Redo", KeyTable.XK_Redo); +addStandard("Undo", KeyTable.XK_Undo); + +// 2.6. UI Keys + +// - Accept +// - Again (could just be XK_Redo) +// - Attn +addStandard("Cancel", KeyTable.XK_Cancel); +addStandard("ContextMenu", KeyTable.XK_Menu); +addStandard("Escape", KeyTable.XK_Escape); +addStandard("Execute", KeyTable.XK_Execute); +addStandard("Find", KeyTable.XK_Find); +addStandard("Help", KeyTable.XK_Help); +addStandard("Pause", KeyTable.XK_Pause); +// - Play +// - Props +addStandard("Select", KeyTable.XK_Select); +addStandard("ZoomIn", KeyTable.XF86XK_ZoomIn); +addStandard("ZoomOut", KeyTable.XF86XK_ZoomOut); + +// 2.7. Device Keys + +addStandard("BrightnessDown", KeyTable.XF86XK_MonBrightnessDown); +addStandard("BrightnessUp", KeyTable.XF86XK_MonBrightnessUp); +addStandard("Eject", KeyTable.XF86XK_Eject); +addStandard("LogOff", KeyTable.XF86XK_LogOff); +addStandard("Power", KeyTable.XF86XK_PowerOff); +addStandard("PowerOff", KeyTable.XF86XK_PowerDown); +addStandard("PrintScreen", KeyTable.XK_Print); +addStandard("Hibernate", KeyTable.XF86XK_Hibernate); +addStandard("Standby", KeyTable.XF86XK_Standby); +addStandard("WakeUp", KeyTable.XF86XK_WakeUp); + +// 2.8. IME and Composition Keys + +addStandard("AllCandidates", KeyTable.XK_MultipleCandidate); +addStandard("Alphanumeric", KeyTable.XK_Eisu_Shift); // could also be _Eisu_Toggle +addStandard("CodeInput", KeyTable.XK_Codeinput); +addStandard("Compose", KeyTable.XK_Multi_key); +addStandard("Convert", KeyTable.XK_Henkan); +// - Dead +// - FinalMode +addStandard("GroupFirst", KeyTable.XK_ISO_First_Group); +addStandard("GroupLast", KeyTable.XK_ISO_Last_Group); +addStandard("GroupNext", KeyTable.XK_ISO_Next_Group); +addStandard("GroupPrevious", KeyTable.XK_ISO_Prev_Group); +// - ModeChange (XK_Mode_switch is often used for AltGr) +// - NextCandidate +addStandard("NonConvert", KeyTable.XK_Muhenkan); +addStandard("PreviousCandidate", KeyTable.XK_PreviousCandidate); +// - Process +addStandard("SingleCandidate", KeyTable.XK_SingleCandidate); +addStandard("HangulMode", KeyTable.XK_Hangul); +addStandard("HanjaMode", KeyTable.XK_Hangul_Hanja); +addStandard("JunjuaMode", KeyTable.XK_Hangul_Jeonja); +addStandard("Eisu", KeyTable.XK_Eisu_toggle); +addStandard("Hankaku", KeyTable.XK_Hankaku); +addStandard("Hiragana", KeyTable.XK_Hiragana); +addStandard("HiraganaKatakana", KeyTable.XK_Hiragana_Katakana); +addStandard("KanaMode", KeyTable.XK_Kana_Shift); // could also be _Kana_Lock +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); + +// 2.9. General-Purpose Function Keys + +addStandard("F1", KeyTable.XK_F1); +addStandard("F2", KeyTable.XK_F2); +addStandard("F3", KeyTable.XK_F3); +addStandard("F4", KeyTable.XK_F4); +addStandard("F5", KeyTable.XK_F5); +addStandard("F6", KeyTable.XK_F6); +addStandard("F7", KeyTable.XK_F7); +addStandard("F8", KeyTable.XK_F8); +addStandard("F9", KeyTable.XK_F9); +addStandard("F10", KeyTable.XK_F10); +addStandard("F11", KeyTable.XK_F11); +addStandard("F12", KeyTable.XK_F12); +addStandard("F13", KeyTable.XK_F13); +addStandard("F14", KeyTable.XK_F14); +addStandard("F15", KeyTable.XK_F15); +addStandard("F16", KeyTable.XK_F16); +addStandard("F17", KeyTable.XK_F17); +addStandard("F18", KeyTable.XK_F18); +addStandard("F19", KeyTable.XK_F19); +addStandard("F20", KeyTable.XK_F20); +addStandard("F21", KeyTable.XK_F21); +addStandard("F22", KeyTable.XK_F22); +addStandard("F23", KeyTable.XK_F23); +addStandard("F24", KeyTable.XK_F24); +addStandard("F25", KeyTable.XK_F25); +addStandard("F26", KeyTable.XK_F26); +addStandard("F27", KeyTable.XK_F27); +addStandard("F28", KeyTable.XK_F28); +addStandard("F29", KeyTable.XK_F29); +addStandard("F30", KeyTable.XK_F30); +addStandard("F31", KeyTable.XK_F31); +addStandard("F32", KeyTable.XK_F32); +addStandard("F33", KeyTable.XK_F33); +addStandard("F34", KeyTable.XK_F34); +addStandard("F35", KeyTable.XK_F35); +// - Soft1... + +// 2.10. Multimedia Keys + +// - ChannelDown +// - ChannelUp +addStandard("Close", KeyTable.XF86XK_Close); +addStandard("MailForward", KeyTable.XF86XK_MailForward); +addStandard("MailReply", KeyTable.XF86XK_Reply); +addStandard("MainSend", KeyTable.XF86XK_Send); +addStandard("MediaFastForward", KeyTable.XF86XK_AudioForward); +addStandard("MediaPause", KeyTable.XF86XK_AudioPause); +addStandard("MediaPlay", KeyTable.XF86XK_AudioPlay); +addStandard("MediaRecord", KeyTable.XF86XK_AudioRecord); +addStandard("MediaRewind", KeyTable.XF86XK_AudioRewind); +addStandard("MediaStop", KeyTable.XF86XK_AudioStop); +addStandard("MediaTrackNext", KeyTable.XF86XK_AudioNext); +addStandard("MediaTrackPrevious", KeyTable.XF86XK_AudioPrev); +addStandard("New", KeyTable.XF86XK_New); +addStandard("Open", KeyTable.XF86XK_Open); +addStandard("Print", KeyTable.XK_Print); +addStandard("Save", KeyTable.XF86XK_Save); +addStandard("SpellCheck", KeyTable.XF86XK_Spell); + +// 2.11. Multimedia Numpad Keys + +// - Key11 +// - Key12 + +// 2.12. Audio Keys + +// - AudioBalanceLeft +// - AudioBalanceRight +// - AudioBassDown +// - AudioBassBoostDown +// - AudioBassBoostToggle +// - AudioBassBoostUp +// - AudioBassUp +// - AudioFaderFront +// - AudioFaderRear +// - AudioSurroundModeNext +// - AudioTrebleDown +// - AudioTrebleUp +addStandard("AudioVolumeDown", KeyTable.XF86XK_AudioLowerVolume); +addStandard("AudioVolumeUp", KeyTable.XF86XK_AudioRaiseVolume); +addStandard("AudioVolumeMute", KeyTable.XF86XK_AudioMute); +// - MicrophoneToggle +// - MicrophoneVolumeDown +// - MicrophoneVolumeUp +addStandard("MicrophoneVolumeMute", KeyTable.XF86XK_AudioMicMute); + +// 2.13. Speech Keys + +// - SpeechCorrectionList +// - SpeechInputToggle + +// 2.14. Application Keys + +addStandard("LaunchCalculator", KeyTable.XF86XK_Calculator); +addStandard("LaunchCalendar", KeyTable.XF86XK_Calendar); +addStandard("LaunchMail", KeyTable.XF86XK_Mail); +addStandard("LaunchMediaPlayer", KeyTable.XF86XK_AudioMedia); +addStandard("LaunchMusicPlayer", KeyTable.XF86XK_Music); +addStandard("LaunchMyComputer", KeyTable.XF86XK_MyComputer); +addStandard("LaunchPhone", KeyTable.XF86XK_Phone); +addStandard("LaunchScreenSaver", KeyTable.XF86XK_ScreenSaver); +addStandard("LaunchSpreadsheet", KeyTable.XF86XK_Excel); +addStandard("LaunchWebBrowser", KeyTable.XF86XK_WWW); +addStandard("LaunchWebCam", KeyTable.XF86XK_WebCam); +addStandard("LaunchWordProcessor", KeyTable.XF86XK_Word); + +// 2.15. Browser Keys + +addStandard("BrowserBack", KeyTable.XF86XK_Back); +addStandard("BrowserFavorites", KeyTable.XF86XK_Favorites); +addStandard("BrowserForward", KeyTable.XF86XK_Forward); +addStandard("BrowserHome", KeyTable.XF86XK_HomePage); +addStandard("BrowserRefresh", KeyTable.XF86XK_Refresh); +addStandard("BrowserSearch", KeyTable.XF86XK_Search); +addStandard("BrowserStop", KeyTable.XF86XK_Stop); + +// 2.16. Mobile Phone Keys + +// - A whole bunch... + +// 2.17. TV Keys + +// - A whole bunch... + +// 2.18. Media Controller Keys + +// - A whole bunch... +addStandard("Dimmer", KeyTable.XF86XK_BrightnessAdjust); +addStandard("MediaAudioTrack", KeyTable.XF86XK_AudioCycleTrack); +addStandard("RandomToggle", KeyTable.XF86XK_AudioRandomPlay); +addStandard("SplitScreenToggle", KeyTable.XF86XK_SplitScreen); +addStandard("Subtitle", KeyTable.XF86XK_Subtitle); +addStandard("VideoModeNext", KeyTable.XF86XK_Next_VMode); + +// Extra: Numpad + +addNumpad("=", KeyTable.XK_equal, KeyTable.XK_KP_Equal); +addNumpad("+", KeyTable.XK_plus, KeyTable.XK_KP_Add); +addNumpad("-", KeyTable.XK_minus, KeyTable.XK_KP_Subtract); +addNumpad("*", KeyTable.XK_asterisk, KeyTable.XK_KP_Multiply); +addNumpad("/", KeyTable.XK_slash, KeyTable.XK_KP_Divide); +addNumpad(".", KeyTable.XK_period, KeyTable.XK_KP_Decimal); +addNumpad(",", KeyTable.XK_comma, KeyTable.XK_KP_Separator); +addNumpad("0", KeyTable.XK_0, KeyTable.XK_KP_0); +addNumpad("1", KeyTable.XK_1, KeyTable.XK_KP_1); +addNumpad("2", KeyTable.XK_2, KeyTable.XK_KP_2); +addNumpad("3", KeyTable.XK_3, KeyTable.XK_KP_3); +addNumpad("4", KeyTable.XK_4, KeyTable.XK_KP_4); +addNumpad("5", KeyTable.XK_5, KeyTable.XK_KP_5); +addNumpad("6", KeyTable.XK_6, KeyTable.XK_KP_6); +addNumpad("7", KeyTable.XK_7, KeyTable.XK_KP_7); +addNumpad("8", KeyTable.XK_8, KeyTable.XK_KP_8); +addNumpad("9", KeyTable.XK_9, KeyTable.XK_KP_9); + +export default DOMKeyTable; diff --git a/systemvm/debian/root/noVNC/core/input/fixedkeys.js b/systemvm/debian/root/noVNC/core/input/fixedkeys.js new file mode 100644 index 000000000000..4d09f2f7e081 --- /dev/null +++ b/systemvm/debian/root/noVNC/core/input/fixedkeys.js @@ -0,0 +1,129 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 or any later version (see LICENSE.txt) + */ + +/* + * Fallback mapping between HTML key codes (physical keys) and + * HTML key values. This only works for keys that don't vary + * between layouts. We also omit those who manage fine by mapping the + * Unicode representation. + * + * See https://www.w3.org/TR/uievents-code/ for possible codes. + * See https://www.w3.org/TR/uievents-key/ for possible values. + */ + +/* eslint-disable key-spacing */ + +export default { + +// 3.1.1.1. Writing System Keys + + 'Backspace': 'Backspace', + +// 3.1.1.2. Functional Keys + + 'AltLeft': 'Alt', + 'AltRight': 'Alt', // This could also be 'AltGraph' + 'CapsLock': 'CapsLock', + 'ContextMenu': 'ContextMenu', + 'ControlLeft': 'Control', + 'ControlRight': 'Control', + 'Enter': 'Enter', + 'MetaLeft': 'Meta', + 'MetaRight': 'Meta', + 'ShiftLeft': 'Shift', + 'ShiftRight': 'Shift', + 'Tab': 'Tab', + // FIXME: Japanese/Korean keys + +// 3.1.2. Control Pad Section + + 'Delete': 'Delete', + 'End': 'End', + 'Help': 'Help', + 'Home': 'Home', + 'Insert': 'Insert', + 'PageDown': 'PageDown', + 'PageUp': 'PageUp', + +// 3.1.3. Arrow Pad Section + + 'ArrowDown': 'ArrowDown', + 'ArrowLeft': 'ArrowLeft', + 'ArrowRight': 'ArrowRight', + 'ArrowUp': 'ArrowUp', + +// 3.1.4. Numpad Section + + 'NumLock': 'NumLock', + 'NumpadBackspace': 'Backspace', + 'NumpadClear': 'Clear', + +// 3.1.5. Function Section + + 'Escape': 'Escape', + 'F1': 'F1', + 'F2': 'F2', + 'F3': 'F3', + 'F4': 'F4', + 'F5': 'F5', + 'F6': 'F6', + 'F7': 'F7', + 'F8': 'F8', + 'F9': 'F9', + 'F10': 'F10', + 'F11': 'F11', + 'F12': 'F12', + 'F13': 'F13', + 'F14': 'F14', + 'F15': 'F15', + 'F16': 'F16', + 'F17': 'F17', + 'F18': 'F18', + 'F19': 'F19', + 'F20': 'F20', + 'F21': 'F21', + 'F22': 'F22', + 'F23': 'F23', + 'F24': 'F24', + 'F25': 'F25', + 'F26': 'F26', + 'F27': 'F27', + 'F28': 'F28', + 'F29': 'F29', + 'F30': 'F30', + 'F31': 'F31', + 'F32': 'F32', + 'F33': 'F33', + 'F34': 'F34', + 'F35': 'F35', + 'PrintScreen': 'PrintScreen', + 'ScrollLock': 'ScrollLock', + 'Pause': 'Pause', + +// 3.1.6. Media Keys + + 'BrowserBack': 'BrowserBack', + 'BrowserFavorites': 'BrowserFavorites', + 'BrowserForward': 'BrowserForward', + 'BrowserHome': 'BrowserHome', + 'BrowserRefresh': 'BrowserRefresh', + 'BrowserSearch': 'BrowserSearch', + 'BrowserStop': 'BrowserStop', + 'Eject': 'Eject', + 'LaunchApp1': 'LaunchMyComputer', + 'LaunchApp2': 'LaunchCalendar', + 'LaunchMail': 'LaunchMail', + 'MediaPlayPause': 'MediaPlay', + 'MediaStop': 'MediaStop', + 'MediaTrackNext': 'MediaTrackNext', + 'MediaTrackPrevious': 'MediaTrackPrevious', + 'Power': 'Power', + 'Sleep': 'Sleep', + 'AudioVolumeDown': 'AudioVolumeDown', + 'AudioVolumeMute': 'AudioVolumeMute', + 'AudioVolumeUp': 'AudioVolumeUp', + 'WakeUp': 'WakeUp', +}; diff --git a/systemvm/debian/root/noVNC/core/input/keyboard.js b/systemvm/debian/root/noVNC/core/input/keyboard.js new file mode 100644 index 000000000000..9dbc8d6e6ed1 --- /dev/null +++ b/systemvm/debian/root/noVNC/core/input/keyboard.js @@ -0,0 +1,370 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 or any later version (see LICENSE.txt) + */ + +import * as Log from '../util/logging.js'; +import { stopEvent } from '../util/events.js'; +import * as KeyboardUtil from "./util.js"; +import KeyTable from "./keysym.js"; +import * as browser from "../util/browser.js"; + +// +// Keyboard event handler +// + +export default class Keyboard { + constructor(target) { + this._target = target || null; + + 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 ===== + + this.onkeyevent = () => {}; // Handler for key press/release + } + + // ===== PRIVATE METHODS ===== + + _sendKeyEvent(keysym, code, down) { + if (down) { + this._keyDownList[code] = keysym; + } else { + // Do we really think this key is down? + if (!(code in this._keyDownList)) { + return; + } + delete this._keyDownList[code]; + } + + Log.Debug("onkeyevent " + (down ? "down" : "up") + + ", keysym: " + keysym, ", code: " + code); + this.onkeyevent(keysym, code, down); + } + + _getKeyCode(e) { + const code = KeyboardUtil.getKeycode(e); + if (code !== 'Unidentified') { + return code; + } + + // 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')) { + // 229 is used for composition events + if (e.keyCode !== 229) { + return 'Platform' + e.keyCode; + } + } + + // A precursor to the final DOM3 standard. Unfortunately it + // is not layout independent, so it is as bad as using keyCode + if (e.keyIdentifier) { + // Non-character key? + if (e.keyIdentifier.substr(0, 2) !== 'U+') { + return e.keyIdentifier; + } + + const codepoint = parseInt(e.keyIdentifier.substr(2), 16); + const char = String.fromCharCode(codepoint).toUpperCase(); + + return 'Platform' + char.charCodeAt(); + } + + return 'Unidentified'; + } + + _handleKeyDown(e) { + const code = this._getKeyCode(e); + let keysym = KeyboardUtil.getKeysym(e); + + // Windows doesn't have a proper AltGr, but handles it using + // fake Ctrl+Alt. However the remote end might not be Windows, + // so we need to merge those in to a single AltGr event. We + // detect this case by seeing the two key events directly after + // each other with a very short time between them (<50ms). + if (this._altGrArmed) { + this._altGrArmed = false; + clearTimeout(this._altGrTimeout); + + if ((code === "AltRight") && + ((e.timeStamp - this._altGrCtrlTime) < 50)) { + // FIXME: We fail to detect this if either Ctrl key is + // first manually pressed as Windows then no + // longer sends the fake Ctrl down event. It + // does however happily send real Ctrl events + // even when AltGr is already down. Some + // browsers detect this for us though and set the + // key to "AltGraph". + keysym = KeyTable.XK_ISO_Level3_Shift; + } else { + this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); + } + } + + // We cannot handle keys we cannot track, but we also need + // to deal with virtual keyboards which omit key info + // (iOS omits tracking info on keyup events, which forces us to + // special treat that platform here) + if ((code === 'Unidentified') || browser.isIOS()) { + if (keysym) { + // 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); + } + + stopEvent(e); + return; + } + + // Alt behaves more like AltGraph on macOS, so shuffle the + // keys around a bit to make things more sane for the remote + // server. This method is used by RealVNC and TigerVNC (and + // possibly others). + if (browser.isMac()) { + switch (keysym) { + case KeyTable.XK_Super_L: + keysym = KeyTable.XK_Alt_L; + break; + case KeyTable.XK_Super_R: + keysym = KeyTable.XK_Super_L; + break; + case KeyTable.XK_Alt_L: + keysym = KeyTable.XK_Mode_switch; + break; + case KeyTable.XK_Alt_R: + keysym = KeyTable.XK_ISO_Level3_Shift; + break; + } + } + + // Is this key already pressed? If so, then we must use the + // same keysym or we'll confuse the server + if (code in this._keyDownList) { + keysym = this._keyDownList[code]; + } + + // 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() && (code === 'CapsLock')) { + this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true); + this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false); + 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); + return; + } + + this._pendingKey = null; + stopEvent(e); + + // Possible start of AltGr sequence? (see above) + if ((code === "ControlLeft") && browser.isWindows() && + !("ControlLeft" in this._keyDownList)) { + this._altGrArmed = true; + this._altGrTimeout = setTimeout(this._handleAltGrTimeout.bind(this), 100); + this._altGrCtrlTime = e.timeStamp; + 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); + } + + _handleKeyUp(e) { + stopEvent(e); + + const code = this._getKeyCode(e); + + // We can't get a release in the middle of an AltGr sequence, so + // abort that detection + if (this._altGrArmed) { + this._altGrArmed = false; + clearTimeout(this._altGrTimeout); + this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); + } + + // See comment in _handleKeyDown() + if (browser.isMac() && (code === 'CapsLock')) { + this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true); + this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false); + return; + } + + this._sendKeyEvent(this._keyDownList[code], code, false); + } + + _handleAltGrTimeout() { + this._altGrArmed = false; + clearTimeout(this._altGrTimeout); + this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); + } + + _allKeysUp() { + Log.Debug(">> Keyboard.allKeysUp"); + for (let code in this._keyDownList) { + this._sendKeyEvent(this._keyDownList[code], code, false); + } + Log.Debug("<< Keyboard.allKeysUp"); + } + + // Firefox Alt workaround, see below + _checkAlt(e) { + 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 }); + target.dispatchEvent(event); + }); + } + + // ===== PUBLIC METHODS ===== + + grab() { + //Log.Debug(">> Keyboard.grab"); + + 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 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 + this._allKeysUp(); + + //Log.Debug(">> Keyboard.ungrab"); + } +} diff --git a/systemvm/debian/root/noVNC/core/input/keysym.js b/systemvm/debian/root/noVNC/core/input/keysym.js new file mode 100644 index 000000000000..22ba0584eae1 --- /dev/null +++ b/systemvm/debian/root/noVNC/core/input/keysym.js @@ -0,0 +1,616 @@ +/* eslint-disable key-spacing */ + +export default { + XK_VoidSymbol: 0xffffff, /* Void symbol */ + + XK_BackSpace: 0xff08, /* Back space, back char */ + XK_Tab: 0xff09, + XK_Linefeed: 0xff0a, /* Linefeed, LF */ + XK_Clear: 0xff0b, + XK_Return: 0xff0d, /* Return, enter */ + XK_Pause: 0xff13, /* Pause, hold */ + XK_Scroll_Lock: 0xff14, + XK_Sys_Req: 0xff15, + XK_Escape: 0xff1b, + XK_Delete: 0xffff, /* Delete, rubout */ + + /* International & multi-key character composition */ + + XK_Multi_key: 0xff20, /* Multi-key character compose */ + XK_Codeinput: 0xff37, + XK_SingleCandidate: 0xff3c, + XK_MultipleCandidate: 0xff3d, + XK_PreviousCandidate: 0xff3e, + + /* Japanese keyboard support */ + + XK_Kanji: 0xff21, /* Kanji, Kanji convert */ + XK_Muhenkan: 0xff22, /* Cancel Conversion */ + XK_Henkan_Mode: 0xff23, /* Start/Stop Conversion */ + XK_Henkan: 0xff23, /* Alias for Henkan_Mode */ + XK_Romaji: 0xff24, /* to Romaji */ + XK_Hiragana: 0xff25, /* to Hiragana */ + XK_Katakana: 0xff26, /* to Katakana */ + XK_Hiragana_Katakana: 0xff27, /* Hiragana/Katakana toggle */ + XK_Zenkaku: 0xff28, /* to Zenkaku */ + XK_Hankaku: 0xff29, /* to Hankaku */ + XK_Zenkaku_Hankaku: 0xff2a, /* Zenkaku/Hankaku toggle */ + XK_Touroku: 0xff2b, /* Add to Dictionary */ + XK_Massyo: 0xff2c, /* Delete from Dictionary */ + XK_Kana_Lock: 0xff2d, /* Kana Lock */ + XK_Kana_Shift: 0xff2e, /* Kana Shift */ + XK_Eisu_Shift: 0xff2f, /* Alphanumeric Shift */ + XK_Eisu_toggle: 0xff30, /* Alphanumeric toggle */ + XK_Kanji_Bangou: 0xff37, /* Codeinput */ + XK_Zen_Koho: 0xff3d, /* Multiple/All Candidate(s) */ + XK_Mae_Koho: 0xff3e, /* Previous Candidate */ + + /* Cursor control & motion */ + + XK_Home: 0xff50, + XK_Left: 0xff51, /* Move left, left arrow */ + XK_Up: 0xff52, /* Move up, up arrow */ + XK_Right: 0xff53, /* Move right, right arrow */ + XK_Down: 0xff54, /* Move down, down arrow */ + XK_Prior: 0xff55, /* Prior, previous */ + XK_Page_Up: 0xff55, + XK_Next: 0xff56, /* Next */ + XK_Page_Down: 0xff56, + XK_End: 0xff57, /* EOL */ + XK_Begin: 0xff58, /* BOL */ + + + /* Misc functions */ + + XK_Select: 0xff60, /* Select, mark */ + XK_Print: 0xff61, + XK_Execute: 0xff62, /* Execute, run, do */ + XK_Insert: 0xff63, /* Insert, insert here */ + XK_Undo: 0xff65, + XK_Redo: 0xff66, /* Redo, again */ + XK_Menu: 0xff67, + XK_Find: 0xff68, /* Find, search */ + XK_Cancel: 0xff69, /* Cancel, stop, abort, exit */ + XK_Help: 0xff6a, /* Help */ + XK_Break: 0xff6b, + XK_Mode_switch: 0xff7e, /* Character set switch */ + XK_script_switch: 0xff7e, /* Alias for mode_switch */ + XK_Num_Lock: 0xff7f, + + /* Keypad functions, keypad numbers cleverly chosen to map to ASCII */ + + XK_KP_Space: 0xff80, /* Space */ + XK_KP_Tab: 0xff89, + XK_KP_Enter: 0xff8d, /* Enter */ + XK_KP_F1: 0xff91, /* PF1, KP_A, ... */ + XK_KP_F2: 0xff92, + XK_KP_F3: 0xff93, + XK_KP_F4: 0xff94, + XK_KP_Home: 0xff95, + XK_KP_Left: 0xff96, + XK_KP_Up: 0xff97, + XK_KP_Right: 0xff98, + XK_KP_Down: 0xff99, + XK_KP_Prior: 0xff9a, + XK_KP_Page_Up: 0xff9a, + XK_KP_Next: 0xff9b, + XK_KP_Page_Down: 0xff9b, + XK_KP_End: 0xff9c, + XK_KP_Begin: 0xff9d, + XK_KP_Insert: 0xff9e, + XK_KP_Delete: 0xff9f, + XK_KP_Equal: 0xffbd, /* Equals */ + XK_KP_Multiply: 0xffaa, + XK_KP_Add: 0xffab, + XK_KP_Separator: 0xffac, /* Separator, often comma */ + XK_KP_Subtract: 0xffad, + XK_KP_Decimal: 0xffae, + XK_KP_Divide: 0xffaf, + + XK_KP_0: 0xffb0, + XK_KP_1: 0xffb1, + XK_KP_2: 0xffb2, + XK_KP_3: 0xffb3, + XK_KP_4: 0xffb4, + XK_KP_5: 0xffb5, + XK_KP_6: 0xffb6, + XK_KP_7: 0xffb7, + XK_KP_8: 0xffb8, + XK_KP_9: 0xffb9, + + /* + * Auxiliary functions; note the duplicate definitions for left and right + * function keys; Sun keyboards and a few other manufacturers have such + * function key groups on the left and/or right sides of the keyboard. + * We've not found a keyboard with more than 35 function keys total. + */ + + XK_F1: 0xffbe, + XK_F2: 0xffbf, + XK_F3: 0xffc0, + XK_F4: 0xffc1, + XK_F5: 0xffc2, + XK_F6: 0xffc3, + XK_F7: 0xffc4, + XK_F8: 0xffc5, + XK_F9: 0xffc6, + XK_F10: 0xffc7, + XK_F11: 0xffc8, + XK_L1: 0xffc8, + XK_F12: 0xffc9, + XK_L2: 0xffc9, + XK_F13: 0xffca, + XK_L3: 0xffca, + XK_F14: 0xffcb, + XK_L4: 0xffcb, + XK_F15: 0xffcc, + XK_L5: 0xffcc, + XK_F16: 0xffcd, + XK_L6: 0xffcd, + XK_F17: 0xffce, + XK_L7: 0xffce, + XK_F18: 0xffcf, + XK_L8: 0xffcf, + XK_F19: 0xffd0, + XK_L9: 0xffd0, + XK_F20: 0xffd1, + XK_L10: 0xffd1, + XK_F21: 0xffd2, + XK_R1: 0xffd2, + XK_F22: 0xffd3, + XK_R2: 0xffd3, + XK_F23: 0xffd4, + XK_R3: 0xffd4, + XK_F24: 0xffd5, + XK_R4: 0xffd5, + XK_F25: 0xffd6, + XK_R5: 0xffd6, + XK_F26: 0xffd7, + XK_R6: 0xffd7, + XK_F27: 0xffd8, + XK_R7: 0xffd8, + XK_F28: 0xffd9, + XK_R8: 0xffd9, + XK_F29: 0xffda, + XK_R9: 0xffda, + XK_F30: 0xffdb, + XK_R10: 0xffdb, + XK_F31: 0xffdc, + XK_R11: 0xffdc, + XK_F32: 0xffdd, + XK_R12: 0xffdd, + XK_F33: 0xffde, + XK_R13: 0xffde, + XK_F34: 0xffdf, + XK_R14: 0xffdf, + XK_F35: 0xffe0, + XK_R15: 0xffe0, + + /* Modifiers */ + + XK_Shift_L: 0xffe1, /* Left shift */ + XK_Shift_R: 0xffe2, /* Right shift */ + XK_Control_L: 0xffe3, /* Left control */ + XK_Control_R: 0xffe4, /* Right control */ + XK_Caps_Lock: 0xffe5, /* Caps lock */ + XK_Shift_Lock: 0xffe6, /* Shift lock */ + + XK_Meta_L: 0xffe7, /* Left meta */ + XK_Meta_R: 0xffe8, /* Right meta */ + XK_Alt_L: 0xffe9, /* Left alt */ + XK_Alt_R: 0xffea, /* Right alt */ + XK_Super_L: 0xffeb, /* Left super */ + XK_Super_R: 0xffec, /* Right super */ + XK_Hyper_L: 0xffed, /* Left hyper */ + XK_Hyper_R: 0xffee, /* Right hyper */ + + /* + * Keyboard (XKB) Extension function and modifier keys + * (from Appendix C of "The X Keyboard Extension: Protocol Specification") + * Byte 3 = 0xfe + */ + + XK_ISO_Level3_Shift: 0xfe03, /* AltGr */ + XK_ISO_Next_Group: 0xfe08, + XK_ISO_Prev_Group: 0xfe0a, + XK_ISO_First_Group: 0xfe0c, + XK_ISO_Last_Group: 0xfe0e, + + /* + * Latin 1 + * (ISO/IEC 8859-1: Unicode U+0020..U+00FF) + * Byte 3: 0 + */ + + XK_space: 0x0020, /* U+0020 SPACE */ + XK_exclam: 0x0021, /* U+0021 EXCLAMATION MARK */ + XK_quotedbl: 0x0022, /* U+0022 QUOTATION MARK */ + XK_numbersign: 0x0023, /* U+0023 NUMBER SIGN */ + XK_dollar: 0x0024, /* U+0024 DOLLAR SIGN */ + XK_percent: 0x0025, /* U+0025 PERCENT SIGN */ + XK_ampersand: 0x0026, /* U+0026 AMPERSAND */ + XK_apostrophe: 0x0027, /* U+0027 APOSTROPHE */ + XK_quoteright: 0x0027, /* deprecated */ + XK_parenleft: 0x0028, /* U+0028 LEFT PARENTHESIS */ + XK_parenright: 0x0029, /* U+0029 RIGHT PARENTHESIS */ + XK_asterisk: 0x002a, /* U+002A ASTERISK */ + XK_plus: 0x002b, /* U+002B PLUS SIGN */ + XK_comma: 0x002c, /* U+002C COMMA */ + XK_minus: 0x002d, /* U+002D HYPHEN-MINUS */ + XK_period: 0x002e, /* U+002E FULL STOP */ + XK_slash: 0x002f, /* U+002F SOLIDUS */ + XK_0: 0x0030, /* U+0030 DIGIT ZERO */ + XK_1: 0x0031, /* U+0031 DIGIT ONE */ + XK_2: 0x0032, /* U+0032 DIGIT TWO */ + XK_3: 0x0033, /* U+0033 DIGIT THREE */ + XK_4: 0x0034, /* U+0034 DIGIT FOUR */ + XK_5: 0x0035, /* U+0035 DIGIT FIVE */ + XK_6: 0x0036, /* U+0036 DIGIT SIX */ + XK_7: 0x0037, /* U+0037 DIGIT SEVEN */ + XK_8: 0x0038, /* U+0038 DIGIT EIGHT */ + XK_9: 0x0039, /* U+0039 DIGIT NINE */ + XK_colon: 0x003a, /* U+003A COLON */ + XK_semicolon: 0x003b, /* U+003B SEMICOLON */ + XK_less: 0x003c, /* U+003C LESS-THAN SIGN */ + XK_equal: 0x003d, /* U+003D EQUALS SIGN */ + XK_greater: 0x003e, /* U+003E GREATER-THAN SIGN */ + XK_question: 0x003f, /* U+003F QUESTION MARK */ + XK_at: 0x0040, /* U+0040 COMMERCIAL AT */ + XK_A: 0x0041, /* U+0041 LATIN CAPITAL LETTER A */ + XK_B: 0x0042, /* U+0042 LATIN CAPITAL LETTER B */ + XK_C: 0x0043, /* U+0043 LATIN CAPITAL LETTER C */ + XK_D: 0x0044, /* U+0044 LATIN CAPITAL LETTER D */ + XK_E: 0x0045, /* U+0045 LATIN CAPITAL LETTER E */ + XK_F: 0x0046, /* U+0046 LATIN CAPITAL LETTER F */ + XK_G: 0x0047, /* U+0047 LATIN CAPITAL LETTER G */ + XK_H: 0x0048, /* U+0048 LATIN CAPITAL LETTER H */ + XK_I: 0x0049, /* U+0049 LATIN CAPITAL LETTER I */ + XK_J: 0x004a, /* U+004A LATIN CAPITAL LETTER J */ + XK_K: 0x004b, /* U+004B LATIN CAPITAL LETTER K */ + XK_L: 0x004c, /* U+004C LATIN CAPITAL LETTER L */ + XK_M: 0x004d, /* U+004D LATIN CAPITAL LETTER M */ + XK_N: 0x004e, /* U+004E LATIN CAPITAL LETTER N */ + XK_O: 0x004f, /* U+004F LATIN CAPITAL LETTER O */ + XK_P: 0x0050, /* U+0050 LATIN CAPITAL LETTER P */ + XK_Q: 0x0051, /* U+0051 LATIN CAPITAL LETTER Q */ + XK_R: 0x0052, /* U+0052 LATIN CAPITAL LETTER R */ + XK_S: 0x0053, /* U+0053 LATIN CAPITAL LETTER S */ + XK_T: 0x0054, /* U+0054 LATIN CAPITAL LETTER T */ + XK_U: 0x0055, /* U+0055 LATIN CAPITAL LETTER U */ + XK_V: 0x0056, /* U+0056 LATIN CAPITAL LETTER V */ + XK_W: 0x0057, /* U+0057 LATIN CAPITAL LETTER W */ + XK_X: 0x0058, /* U+0058 LATIN CAPITAL LETTER X */ + XK_Y: 0x0059, /* U+0059 LATIN CAPITAL LETTER Y */ + XK_Z: 0x005a, /* U+005A LATIN CAPITAL LETTER Z */ + XK_bracketleft: 0x005b, /* U+005B LEFT SQUARE BRACKET */ + XK_backslash: 0x005c, /* U+005C REVERSE SOLIDUS */ + XK_bracketright: 0x005d, /* U+005D RIGHT SQUARE BRACKET */ + XK_asciicircum: 0x005e, /* U+005E CIRCUMFLEX ACCENT */ + XK_underscore: 0x005f, /* U+005F LOW LINE */ + XK_grave: 0x0060, /* U+0060 GRAVE ACCENT */ + XK_quoteleft: 0x0060, /* deprecated */ + XK_a: 0x0061, /* U+0061 LATIN SMALL LETTER A */ + XK_b: 0x0062, /* U+0062 LATIN SMALL LETTER B */ + XK_c: 0x0063, /* U+0063 LATIN SMALL LETTER C */ + XK_d: 0x0064, /* U+0064 LATIN SMALL LETTER D */ + XK_e: 0x0065, /* U+0065 LATIN SMALL LETTER E */ + XK_f: 0x0066, /* U+0066 LATIN SMALL LETTER F */ + XK_g: 0x0067, /* U+0067 LATIN SMALL LETTER G */ + XK_h: 0x0068, /* U+0068 LATIN SMALL LETTER H */ + XK_i: 0x0069, /* U+0069 LATIN SMALL LETTER I */ + XK_j: 0x006a, /* U+006A LATIN SMALL LETTER J */ + XK_k: 0x006b, /* U+006B LATIN SMALL LETTER K */ + XK_l: 0x006c, /* U+006C LATIN SMALL LETTER L */ + XK_m: 0x006d, /* U+006D LATIN SMALL LETTER M */ + XK_n: 0x006e, /* U+006E LATIN SMALL LETTER N */ + XK_o: 0x006f, /* U+006F LATIN SMALL LETTER O */ + XK_p: 0x0070, /* U+0070 LATIN SMALL LETTER P */ + XK_q: 0x0071, /* U+0071 LATIN SMALL LETTER Q */ + XK_r: 0x0072, /* U+0072 LATIN SMALL LETTER R */ + XK_s: 0x0073, /* U+0073 LATIN SMALL LETTER S */ + XK_t: 0x0074, /* U+0074 LATIN SMALL LETTER T */ + XK_u: 0x0075, /* U+0075 LATIN SMALL LETTER U */ + XK_v: 0x0076, /* U+0076 LATIN SMALL LETTER V */ + XK_w: 0x0077, /* U+0077 LATIN SMALL LETTER W */ + XK_x: 0x0078, /* U+0078 LATIN SMALL LETTER X */ + XK_y: 0x0079, /* U+0079 LATIN SMALL LETTER Y */ + XK_z: 0x007a, /* U+007A LATIN SMALL LETTER Z */ + XK_braceleft: 0x007b, /* U+007B LEFT CURLY BRACKET */ + XK_bar: 0x007c, /* U+007C VERTICAL LINE */ + XK_braceright: 0x007d, /* U+007D RIGHT CURLY BRACKET */ + XK_asciitilde: 0x007e, /* U+007E TILDE */ + + XK_nobreakspace: 0x00a0, /* U+00A0 NO-BREAK SPACE */ + XK_exclamdown: 0x00a1, /* U+00A1 INVERTED EXCLAMATION MARK */ + XK_cent: 0x00a2, /* U+00A2 CENT SIGN */ + XK_sterling: 0x00a3, /* U+00A3 POUND SIGN */ + XK_currency: 0x00a4, /* U+00A4 CURRENCY SIGN */ + XK_yen: 0x00a5, /* U+00A5 YEN SIGN */ + XK_brokenbar: 0x00a6, /* U+00A6 BROKEN BAR */ + XK_section: 0x00a7, /* U+00A7 SECTION SIGN */ + XK_diaeresis: 0x00a8, /* U+00A8 DIAERESIS */ + XK_copyright: 0x00a9, /* U+00A9 COPYRIGHT SIGN */ + XK_ordfeminine: 0x00aa, /* U+00AA FEMININE ORDINAL INDICATOR */ + XK_guillemotleft: 0x00ab, /* U+00AB LEFT-POINTING DOUBLE ANGLE QUOTATION MARK */ + XK_notsign: 0x00ac, /* U+00AC NOT SIGN */ + XK_hyphen: 0x00ad, /* U+00AD SOFT HYPHEN */ + XK_registered: 0x00ae, /* U+00AE REGISTERED SIGN */ + XK_macron: 0x00af, /* U+00AF MACRON */ + XK_degree: 0x00b0, /* U+00B0 DEGREE SIGN */ + XK_plusminus: 0x00b1, /* U+00B1 PLUS-MINUS SIGN */ + XK_twosuperior: 0x00b2, /* U+00B2 SUPERSCRIPT TWO */ + XK_threesuperior: 0x00b3, /* U+00B3 SUPERSCRIPT THREE */ + XK_acute: 0x00b4, /* U+00B4 ACUTE ACCENT */ + XK_mu: 0x00b5, /* U+00B5 MICRO SIGN */ + XK_paragraph: 0x00b6, /* U+00B6 PILCROW SIGN */ + XK_periodcentered: 0x00b7, /* U+00B7 MIDDLE DOT */ + XK_cedilla: 0x00b8, /* U+00B8 CEDILLA */ + XK_onesuperior: 0x00b9, /* U+00B9 SUPERSCRIPT ONE */ + XK_masculine: 0x00ba, /* U+00BA MASCULINE ORDINAL INDICATOR */ + XK_guillemotright: 0x00bb, /* U+00BB RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK */ + XK_onequarter: 0x00bc, /* U+00BC VULGAR FRACTION ONE QUARTER */ + XK_onehalf: 0x00bd, /* U+00BD VULGAR FRACTION ONE HALF */ + XK_threequarters: 0x00be, /* U+00BE VULGAR FRACTION THREE QUARTERS */ + XK_questiondown: 0x00bf, /* U+00BF INVERTED QUESTION MARK */ + XK_Agrave: 0x00c0, /* U+00C0 LATIN CAPITAL LETTER A WITH GRAVE */ + XK_Aacute: 0x00c1, /* U+00C1 LATIN CAPITAL LETTER A WITH ACUTE */ + XK_Acircumflex: 0x00c2, /* U+00C2 LATIN CAPITAL LETTER A WITH CIRCUMFLEX */ + XK_Atilde: 0x00c3, /* U+00C3 LATIN CAPITAL LETTER A WITH TILDE */ + XK_Adiaeresis: 0x00c4, /* U+00C4 LATIN CAPITAL LETTER A WITH DIAERESIS */ + XK_Aring: 0x00c5, /* U+00C5 LATIN CAPITAL LETTER A WITH RING ABOVE */ + XK_AE: 0x00c6, /* U+00C6 LATIN CAPITAL LETTER AE */ + XK_Ccedilla: 0x00c7, /* U+00C7 LATIN CAPITAL LETTER C WITH CEDILLA */ + XK_Egrave: 0x00c8, /* U+00C8 LATIN CAPITAL LETTER E WITH GRAVE */ + XK_Eacute: 0x00c9, /* U+00C9 LATIN CAPITAL LETTER E WITH ACUTE */ + XK_Ecircumflex: 0x00ca, /* U+00CA LATIN CAPITAL LETTER E WITH CIRCUMFLEX */ + XK_Ediaeresis: 0x00cb, /* U+00CB LATIN CAPITAL LETTER E WITH DIAERESIS */ + XK_Igrave: 0x00cc, /* U+00CC LATIN CAPITAL LETTER I WITH GRAVE */ + XK_Iacute: 0x00cd, /* U+00CD LATIN CAPITAL LETTER I WITH ACUTE */ + XK_Icircumflex: 0x00ce, /* U+00CE LATIN CAPITAL LETTER I WITH CIRCUMFLEX */ + XK_Idiaeresis: 0x00cf, /* U+00CF LATIN CAPITAL LETTER I WITH DIAERESIS */ + XK_ETH: 0x00d0, /* U+00D0 LATIN CAPITAL LETTER ETH */ + XK_Eth: 0x00d0, /* deprecated */ + XK_Ntilde: 0x00d1, /* U+00D1 LATIN CAPITAL LETTER N WITH TILDE */ + XK_Ograve: 0x00d2, /* U+00D2 LATIN CAPITAL LETTER O WITH GRAVE */ + XK_Oacute: 0x00d3, /* U+00D3 LATIN CAPITAL LETTER O WITH ACUTE */ + XK_Ocircumflex: 0x00d4, /* U+00D4 LATIN CAPITAL LETTER O WITH CIRCUMFLEX */ + XK_Otilde: 0x00d5, /* U+00D5 LATIN CAPITAL LETTER O WITH TILDE */ + XK_Odiaeresis: 0x00d6, /* U+00D6 LATIN CAPITAL LETTER O WITH DIAERESIS */ + XK_multiply: 0x00d7, /* U+00D7 MULTIPLICATION SIGN */ + XK_Oslash: 0x00d8, /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */ + XK_Ooblique: 0x00d8, /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */ + XK_Ugrave: 0x00d9, /* U+00D9 LATIN CAPITAL LETTER U WITH GRAVE */ + XK_Uacute: 0x00da, /* U+00DA LATIN CAPITAL LETTER U WITH ACUTE */ + XK_Ucircumflex: 0x00db, /* U+00DB LATIN CAPITAL LETTER U WITH CIRCUMFLEX */ + XK_Udiaeresis: 0x00dc, /* U+00DC LATIN CAPITAL LETTER U WITH DIAERESIS */ + XK_Yacute: 0x00dd, /* U+00DD LATIN CAPITAL LETTER Y WITH ACUTE */ + XK_THORN: 0x00de, /* U+00DE LATIN CAPITAL LETTER THORN */ + XK_Thorn: 0x00de, /* deprecated */ + XK_ssharp: 0x00df, /* U+00DF LATIN SMALL LETTER SHARP S */ + XK_agrave: 0x00e0, /* U+00E0 LATIN SMALL LETTER A WITH GRAVE */ + XK_aacute: 0x00e1, /* U+00E1 LATIN SMALL LETTER A WITH ACUTE */ + XK_acircumflex: 0x00e2, /* U+00E2 LATIN SMALL LETTER A WITH CIRCUMFLEX */ + XK_atilde: 0x00e3, /* U+00E3 LATIN SMALL LETTER A WITH TILDE */ + XK_adiaeresis: 0x00e4, /* U+00E4 LATIN SMALL LETTER A WITH DIAERESIS */ + XK_aring: 0x00e5, /* U+00E5 LATIN SMALL LETTER A WITH RING ABOVE */ + XK_ae: 0x00e6, /* U+00E6 LATIN SMALL LETTER AE */ + XK_ccedilla: 0x00e7, /* U+00E7 LATIN SMALL LETTER C WITH CEDILLA */ + XK_egrave: 0x00e8, /* U+00E8 LATIN SMALL LETTER E WITH GRAVE */ + XK_eacute: 0x00e9, /* U+00E9 LATIN SMALL LETTER E WITH ACUTE */ + XK_ecircumflex: 0x00ea, /* U+00EA LATIN SMALL LETTER E WITH CIRCUMFLEX */ + XK_ediaeresis: 0x00eb, /* U+00EB LATIN SMALL LETTER E WITH DIAERESIS */ + XK_igrave: 0x00ec, /* U+00EC LATIN SMALL LETTER I WITH GRAVE */ + XK_iacute: 0x00ed, /* U+00ED LATIN SMALL LETTER I WITH ACUTE */ + XK_icircumflex: 0x00ee, /* U+00EE LATIN SMALL LETTER I WITH CIRCUMFLEX */ + XK_idiaeresis: 0x00ef, /* U+00EF LATIN SMALL LETTER I WITH DIAERESIS */ + XK_eth: 0x00f0, /* U+00F0 LATIN SMALL LETTER ETH */ + XK_ntilde: 0x00f1, /* U+00F1 LATIN SMALL LETTER N WITH TILDE */ + XK_ograve: 0x00f2, /* U+00F2 LATIN SMALL LETTER O WITH GRAVE */ + XK_oacute: 0x00f3, /* U+00F3 LATIN SMALL LETTER O WITH ACUTE */ + XK_ocircumflex: 0x00f4, /* U+00F4 LATIN SMALL LETTER O WITH CIRCUMFLEX */ + XK_otilde: 0x00f5, /* U+00F5 LATIN SMALL LETTER O WITH TILDE */ + XK_odiaeresis: 0x00f6, /* U+00F6 LATIN SMALL LETTER O WITH DIAERESIS */ + XK_division: 0x00f7, /* U+00F7 DIVISION SIGN */ + XK_oslash: 0x00f8, /* U+00F8 LATIN SMALL LETTER O WITH STROKE */ + XK_ooblique: 0x00f8, /* U+00F8 LATIN SMALL LETTER O WITH STROKE */ + XK_ugrave: 0x00f9, /* U+00F9 LATIN SMALL LETTER U WITH GRAVE */ + XK_uacute: 0x00fa, /* U+00FA LATIN SMALL LETTER U WITH ACUTE */ + XK_ucircumflex: 0x00fb, /* U+00FB LATIN SMALL LETTER U WITH CIRCUMFLEX */ + XK_udiaeresis: 0x00fc, /* U+00FC LATIN SMALL LETTER U WITH DIAERESIS */ + XK_yacute: 0x00fd, /* U+00FD LATIN SMALL LETTER Y WITH ACUTE */ + XK_thorn: 0x00fe, /* U+00FE LATIN SMALL LETTER THORN */ + XK_ydiaeresis: 0x00ff, /* U+00FF LATIN SMALL LETTER Y WITH DIAERESIS */ + + /* + * Korean + * Byte 3 = 0x0e + */ + + XK_Hangul: 0xff31, /* Hangul start/stop(toggle) */ + XK_Hangul_Hanja: 0xff34, /* Start Hangul->Hanja Conversion */ + XK_Hangul_Jeonja: 0xff38, /* Jeonja mode */ + + /* + * XFree86 vendor specific keysyms. + * + * The XFree86 keysym range is 0x10080001 - 0x1008FFFF. + */ + + XF86XK_ModeLock: 0x1008FF01, + XF86XK_MonBrightnessUp: 0x1008FF02, + XF86XK_MonBrightnessDown: 0x1008FF03, + XF86XK_KbdLightOnOff: 0x1008FF04, + XF86XK_KbdBrightnessUp: 0x1008FF05, + XF86XK_KbdBrightnessDown: 0x1008FF06, + XF86XK_Standby: 0x1008FF10, + XF86XK_AudioLowerVolume: 0x1008FF11, + XF86XK_AudioMute: 0x1008FF12, + XF86XK_AudioRaiseVolume: 0x1008FF13, + XF86XK_AudioPlay: 0x1008FF14, + XF86XK_AudioStop: 0x1008FF15, + XF86XK_AudioPrev: 0x1008FF16, + XF86XK_AudioNext: 0x1008FF17, + XF86XK_HomePage: 0x1008FF18, + XF86XK_Mail: 0x1008FF19, + XF86XK_Start: 0x1008FF1A, + XF86XK_Search: 0x1008FF1B, + XF86XK_AudioRecord: 0x1008FF1C, + XF86XK_Calculator: 0x1008FF1D, + XF86XK_Memo: 0x1008FF1E, + XF86XK_ToDoList: 0x1008FF1F, + XF86XK_Calendar: 0x1008FF20, + XF86XK_PowerDown: 0x1008FF21, + XF86XK_ContrastAdjust: 0x1008FF22, + XF86XK_RockerUp: 0x1008FF23, + XF86XK_RockerDown: 0x1008FF24, + XF86XK_RockerEnter: 0x1008FF25, + XF86XK_Back: 0x1008FF26, + XF86XK_Forward: 0x1008FF27, + XF86XK_Stop: 0x1008FF28, + XF86XK_Refresh: 0x1008FF29, + XF86XK_PowerOff: 0x1008FF2A, + XF86XK_WakeUp: 0x1008FF2B, + XF86XK_Eject: 0x1008FF2C, + XF86XK_ScreenSaver: 0x1008FF2D, + XF86XK_WWW: 0x1008FF2E, + XF86XK_Sleep: 0x1008FF2F, + XF86XK_Favorites: 0x1008FF30, + XF86XK_AudioPause: 0x1008FF31, + XF86XK_AudioMedia: 0x1008FF32, + XF86XK_MyComputer: 0x1008FF33, + XF86XK_VendorHome: 0x1008FF34, + XF86XK_LightBulb: 0x1008FF35, + XF86XK_Shop: 0x1008FF36, + XF86XK_History: 0x1008FF37, + XF86XK_OpenURL: 0x1008FF38, + XF86XK_AddFavorite: 0x1008FF39, + XF86XK_HotLinks: 0x1008FF3A, + XF86XK_BrightnessAdjust: 0x1008FF3B, + XF86XK_Finance: 0x1008FF3C, + XF86XK_Community: 0x1008FF3D, + XF86XK_AudioRewind: 0x1008FF3E, + XF86XK_BackForward: 0x1008FF3F, + XF86XK_Launch0: 0x1008FF40, + XF86XK_Launch1: 0x1008FF41, + XF86XK_Launch2: 0x1008FF42, + XF86XK_Launch3: 0x1008FF43, + XF86XK_Launch4: 0x1008FF44, + XF86XK_Launch5: 0x1008FF45, + XF86XK_Launch6: 0x1008FF46, + XF86XK_Launch7: 0x1008FF47, + XF86XK_Launch8: 0x1008FF48, + XF86XK_Launch9: 0x1008FF49, + XF86XK_LaunchA: 0x1008FF4A, + XF86XK_LaunchB: 0x1008FF4B, + XF86XK_LaunchC: 0x1008FF4C, + XF86XK_LaunchD: 0x1008FF4D, + XF86XK_LaunchE: 0x1008FF4E, + XF86XK_LaunchF: 0x1008FF4F, + XF86XK_ApplicationLeft: 0x1008FF50, + XF86XK_ApplicationRight: 0x1008FF51, + XF86XK_Book: 0x1008FF52, + XF86XK_CD: 0x1008FF53, + XF86XK_Calculater: 0x1008FF54, + XF86XK_Clear: 0x1008FF55, + XF86XK_Close: 0x1008FF56, + XF86XK_Copy: 0x1008FF57, + XF86XK_Cut: 0x1008FF58, + XF86XK_Display: 0x1008FF59, + XF86XK_DOS: 0x1008FF5A, + XF86XK_Documents: 0x1008FF5B, + XF86XK_Excel: 0x1008FF5C, + XF86XK_Explorer: 0x1008FF5D, + XF86XK_Game: 0x1008FF5E, + XF86XK_Go: 0x1008FF5F, + XF86XK_iTouch: 0x1008FF60, + XF86XK_LogOff: 0x1008FF61, + XF86XK_Market: 0x1008FF62, + XF86XK_Meeting: 0x1008FF63, + XF86XK_MenuKB: 0x1008FF65, + XF86XK_MenuPB: 0x1008FF66, + XF86XK_MySites: 0x1008FF67, + XF86XK_New: 0x1008FF68, + XF86XK_News: 0x1008FF69, + XF86XK_OfficeHome: 0x1008FF6A, + XF86XK_Open: 0x1008FF6B, + XF86XK_Option: 0x1008FF6C, + XF86XK_Paste: 0x1008FF6D, + XF86XK_Phone: 0x1008FF6E, + XF86XK_Q: 0x1008FF70, + XF86XK_Reply: 0x1008FF72, + XF86XK_Reload: 0x1008FF73, + XF86XK_RotateWindows: 0x1008FF74, + XF86XK_RotationPB: 0x1008FF75, + XF86XK_RotationKB: 0x1008FF76, + XF86XK_Save: 0x1008FF77, + XF86XK_ScrollUp: 0x1008FF78, + XF86XK_ScrollDown: 0x1008FF79, + XF86XK_ScrollClick: 0x1008FF7A, + XF86XK_Send: 0x1008FF7B, + XF86XK_Spell: 0x1008FF7C, + XF86XK_SplitScreen: 0x1008FF7D, + XF86XK_Support: 0x1008FF7E, + XF86XK_TaskPane: 0x1008FF7F, + XF86XK_Terminal: 0x1008FF80, + XF86XK_Tools: 0x1008FF81, + XF86XK_Travel: 0x1008FF82, + XF86XK_UserPB: 0x1008FF84, + XF86XK_User1KB: 0x1008FF85, + XF86XK_User2KB: 0x1008FF86, + XF86XK_Video: 0x1008FF87, + XF86XK_WheelButton: 0x1008FF88, + XF86XK_Word: 0x1008FF89, + XF86XK_Xfer: 0x1008FF8A, + XF86XK_ZoomIn: 0x1008FF8B, + XF86XK_ZoomOut: 0x1008FF8C, + XF86XK_Away: 0x1008FF8D, + XF86XK_Messenger: 0x1008FF8E, + XF86XK_WebCam: 0x1008FF8F, + XF86XK_MailForward: 0x1008FF90, + XF86XK_Pictures: 0x1008FF91, + XF86XK_Music: 0x1008FF92, + XF86XK_Battery: 0x1008FF93, + XF86XK_Bluetooth: 0x1008FF94, + XF86XK_WLAN: 0x1008FF95, + XF86XK_UWB: 0x1008FF96, + XF86XK_AudioForward: 0x1008FF97, + XF86XK_AudioRepeat: 0x1008FF98, + XF86XK_AudioRandomPlay: 0x1008FF99, + XF86XK_Subtitle: 0x1008FF9A, + XF86XK_AudioCycleTrack: 0x1008FF9B, + XF86XK_CycleAngle: 0x1008FF9C, + XF86XK_FrameBack: 0x1008FF9D, + XF86XK_FrameForward: 0x1008FF9E, + XF86XK_Time: 0x1008FF9F, + XF86XK_Select: 0x1008FFA0, + XF86XK_View: 0x1008FFA1, + XF86XK_TopMenu: 0x1008FFA2, + XF86XK_Red: 0x1008FFA3, + XF86XK_Green: 0x1008FFA4, + XF86XK_Yellow: 0x1008FFA5, + XF86XK_Blue: 0x1008FFA6, + XF86XK_Suspend: 0x1008FFA7, + XF86XK_Hibernate: 0x1008FFA8, + XF86XK_TouchpadToggle: 0x1008FFA9, + XF86XK_TouchpadOn: 0x1008FFB0, + XF86XK_TouchpadOff: 0x1008FFB1, + XF86XK_AudioMicMute: 0x1008FFB2, + XF86XK_Switch_VT_1: 0x1008FE01, + XF86XK_Switch_VT_2: 0x1008FE02, + XF86XK_Switch_VT_3: 0x1008FE03, + XF86XK_Switch_VT_4: 0x1008FE04, + XF86XK_Switch_VT_5: 0x1008FE05, + XF86XK_Switch_VT_6: 0x1008FE06, + XF86XK_Switch_VT_7: 0x1008FE07, + XF86XK_Switch_VT_8: 0x1008FE08, + XF86XK_Switch_VT_9: 0x1008FE09, + XF86XK_Switch_VT_10: 0x1008FE0A, + XF86XK_Switch_VT_11: 0x1008FE0B, + XF86XK_Switch_VT_12: 0x1008FE0C, + XF86XK_Ungrab: 0x1008FE20, + XF86XK_ClearGrab: 0x1008FE21, + XF86XK_Next_VMode: 0x1008FE22, + XF86XK_Prev_VMode: 0x1008FE23, + XF86XK_LogWindowTree: 0x1008FE24, + XF86XK_LogGrabInfo: 0x1008FE25, +}; diff --git a/systemvm/debian/root/noVNC/core/input/keysymdef.js b/systemvm/debian/root/noVNC/core/input/keysymdef.js new file mode 100644 index 000000000000..951cacab673f --- /dev/null +++ b/systemvm/debian/root/noVNC/core/input/keysymdef.js @@ -0,0 +1,688 @@ +/* + * Mapping from Unicode codepoints to X11/RFB keysyms + * + * This file was automatically generated from keysymdef.h + * DO NOT EDIT! + */ + +/* Functions at the bottom */ + +const codepoints = { + 0x0100: 0x03c0, // XK_Amacron + 0x0101: 0x03e0, // XK_amacron + 0x0102: 0x01c3, // XK_Abreve + 0x0103: 0x01e3, // XK_abreve + 0x0104: 0x01a1, // XK_Aogonek + 0x0105: 0x01b1, // XK_aogonek + 0x0106: 0x01c6, // XK_Cacute + 0x0107: 0x01e6, // XK_cacute + 0x0108: 0x02c6, // XK_Ccircumflex + 0x0109: 0x02e6, // XK_ccircumflex + 0x010a: 0x02c5, // XK_Cabovedot + 0x010b: 0x02e5, // XK_cabovedot + 0x010c: 0x01c8, // XK_Ccaron + 0x010d: 0x01e8, // XK_ccaron + 0x010e: 0x01cf, // XK_Dcaron + 0x010f: 0x01ef, // XK_dcaron + 0x0110: 0x01d0, // XK_Dstroke + 0x0111: 0x01f0, // XK_dstroke + 0x0112: 0x03aa, // XK_Emacron + 0x0113: 0x03ba, // XK_emacron + 0x0116: 0x03cc, // XK_Eabovedot + 0x0117: 0x03ec, // XK_eabovedot + 0x0118: 0x01ca, // XK_Eogonek + 0x0119: 0x01ea, // XK_eogonek + 0x011a: 0x01cc, // XK_Ecaron + 0x011b: 0x01ec, // XK_ecaron + 0x011c: 0x02d8, // XK_Gcircumflex + 0x011d: 0x02f8, // XK_gcircumflex + 0x011e: 0x02ab, // XK_Gbreve + 0x011f: 0x02bb, // XK_gbreve + 0x0120: 0x02d5, // XK_Gabovedot + 0x0121: 0x02f5, // XK_gabovedot + 0x0122: 0x03ab, // XK_Gcedilla + 0x0123: 0x03bb, // XK_gcedilla + 0x0124: 0x02a6, // XK_Hcircumflex + 0x0125: 0x02b6, // XK_hcircumflex + 0x0126: 0x02a1, // XK_Hstroke + 0x0127: 0x02b1, // XK_hstroke + 0x0128: 0x03a5, // XK_Itilde + 0x0129: 0x03b5, // XK_itilde + 0x012a: 0x03cf, // XK_Imacron + 0x012b: 0x03ef, // XK_imacron + 0x012e: 0x03c7, // XK_Iogonek + 0x012f: 0x03e7, // XK_iogonek + 0x0130: 0x02a9, // XK_Iabovedot + 0x0131: 0x02b9, // XK_idotless + 0x0134: 0x02ac, // XK_Jcircumflex + 0x0135: 0x02bc, // XK_jcircumflex + 0x0136: 0x03d3, // XK_Kcedilla + 0x0137: 0x03f3, // XK_kcedilla + 0x0138: 0x03a2, // XK_kra + 0x0139: 0x01c5, // XK_Lacute + 0x013a: 0x01e5, // XK_lacute + 0x013b: 0x03a6, // XK_Lcedilla + 0x013c: 0x03b6, // XK_lcedilla + 0x013d: 0x01a5, // XK_Lcaron + 0x013e: 0x01b5, // XK_lcaron + 0x0141: 0x01a3, // XK_Lstroke + 0x0142: 0x01b3, // XK_lstroke + 0x0143: 0x01d1, // XK_Nacute + 0x0144: 0x01f1, // XK_nacute + 0x0145: 0x03d1, // XK_Ncedilla + 0x0146: 0x03f1, // XK_ncedilla + 0x0147: 0x01d2, // XK_Ncaron + 0x0148: 0x01f2, // XK_ncaron + 0x014a: 0x03bd, // XK_ENG + 0x014b: 0x03bf, // XK_eng + 0x014c: 0x03d2, // XK_Omacron + 0x014d: 0x03f2, // XK_omacron + 0x0150: 0x01d5, // XK_Odoubleacute + 0x0151: 0x01f5, // XK_odoubleacute + 0x0152: 0x13bc, // XK_OE + 0x0153: 0x13bd, // XK_oe + 0x0154: 0x01c0, // XK_Racute + 0x0155: 0x01e0, // XK_racute + 0x0156: 0x03a3, // XK_Rcedilla + 0x0157: 0x03b3, // XK_rcedilla + 0x0158: 0x01d8, // XK_Rcaron + 0x0159: 0x01f8, // XK_rcaron + 0x015a: 0x01a6, // XK_Sacute + 0x015b: 0x01b6, // XK_sacute + 0x015c: 0x02de, // XK_Scircumflex + 0x015d: 0x02fe, // XK_scircumflex + 0x015e: 0x01aa, // XK_Scedilla + 0x015f: 0x01ba, // XK_scedilla + 0x0160: 0x01a9, // XK_Scaron + 0x0161: 0x01b9, // XK_scaron + 0x0162: 0x01de, // XK_Tcedilla + 0x0163: 0x01fe, // XK_tcedilla + 0x0164: 0x01ab, // XK_Tcaron + 0x0165: 0x01bb, // XK_tcaron + 0x0166: 0x03ac, // XK_Tslash + 0x0167: 0x03bc, // XK_tslash + 0x0168: 0x03dd, // XK_Utilde + 0x0169: 0x03fd, // XK_utilde + 0x016a: 0x03de, // XK_Umacron + 0x016b: 0x03fe, // XK_umacron + 0x016c: 0x02dd, // XK_Ubreve + 0x016d: 0x02fd, // XK_ubreve + 0x016e: 0x01d9, // XK_Uring + 0x016f: 0x01f9, // XK_uring + 0x0170: 0x01db, // XK_Udoubleacute + 0x0171: 0x01fb, // XK_udoubleacute + 0x0172: 0x03d9, // XK_Uogonek + 0x0173: 0x03f9, // XK_uogonek + 0x0178: 0x13be, // XK_Ydiaeresis + 0x0179: 0x01ac, // XK_Zacute + 0x017a: 0x01bc, // XK_zacute + 0x017b: 0x01af, // XK_Zabovedot + 0x017c: 0x01bf, // XK_zabovedot + 0x017d: 0x01ae, // XK_Zcaron + 0x017e: 0x01be, // XK_zcaron + 0x0192: 0x08f6, // XK_function + 0x01d2: 0x10001d1, // XK_Ocaron + 0x02c7: 0x01b7, // XK_caron + 0x02d8: 0x01a2, // XK_breve + 0x02d9: 0x01ff, // XK_abovedot + 0x02db: 0x01b2, // XK_ogonek + 0x02dd: 0x01bd, // XK_doubleacute + 0x0385: 0x07ae, // XK_Greek_accentdieresis + 0x0386: 0x07a1, // XK_Greek_ALPHAaccent + 0x0388: 0x07a2, // XK_Greek_EPSILONaccent + 0x0389: 0x07a3, // XK_Greek_ETAaccent + 0x038a: 0x07a4, // XK_Greek_IOTAaccent + 0x038c: 0x07a7, // XK_Greek_OMICRONaccent + 0x038e: 0x07a8, // XK_Greek_UPSILONaccent + 0x038f: 0x07ab, // XK_Greek_OMEGAaccent + 0x0390: 0x07b6, // XK_Greek_iotaaccentdieresis + 0x0391: 0x07c1, // XK_Greek_ALPHA + 0x0392: 0x07c2, // XK_Greek_BETA + 0x0393: 0x07c3, // XK_Greek_GAMMA + 0x0394: 0x07c4, // XK_Greek_DELTA + 0x0395: 0x07c5, // XK_Greek_EPSILON + 0x0396: 0x07c6, // XK_Greek_ZETA + 0x0397: 0x07c7, // XK_Greek_ETA + 0x0398: 0x07c8, // XK_Greek_THETA + 0x0399: 0x07c9, // XK_Greek_IOTA + 0x039a: 0x07ca, // XK_Greek_KAPPA + 0x039b: 0x07cb, // XK_Greek_LAMDA + 0x039c: 0x07cc, // XK_Greek_MU + 0x039d: 0x07cd, // XK_Greek_NU + 0x039e: 0x07ce, // XK_Greek_XI + 0x039f: 0x07cf, // XK_Greek_OMICRON + 0x03a0: 0x07d0, // XK_Greek_PI + 0x03a1: 0x07d1, // XK_Greek_RHO + 0x03a3: 0x07d2, // XK_Greek_SIGMA + 0x03a4: 0x07d4, // XK_Greek_TAU + 0x03a5: 0x07d5, // XK_Greek_UPSILON + 0x03a6: 0x07d6, // XK_Greek_PHI + 0x03a7: 0x07d7, // XK_Greek_CHI + 0x03a8: 0x07d8, // XK_Greek_PSI + 0x03a9: 0x07d9, // XK_Greek_OMEGA + 0x03aa: 0x07a5, // XK_Greek_IOTAdieresis + 0x03ab: 0x07a9, // XK_Greek_UPSILONdieresis + 0x03ac: 0x07b1, // XK_Greek_alphaaccent + 0x03ad: 0x07b2, // XK_Greek_epsilonaccent + 0x03ae: 0x07b3, // XK_Greek_etaaccent + 0x03af: 0x07b4, // XK_Greek_iotaaccent + 0x03b0: 0x07ba, // XK_Greek_upsilonaccentdieresis + 0x03b1: 0x07e1, // XK_Greek_alpha + 0x03b2: 0x07e2, // XK_Greek_beta + 0x03b3: 0x07e3, // XK_Greek_gamma + 0x03b4: 0x07e4, // XK_Greek_delta + 0x03b5: 0x07e5, // XK_Greek_epsilon + 0x03b6: 0x07e6, // XK_Greek_zeta + 0x03b7: 0x07e7, // XK_Greek_eta + 0x03b8: 0x07e8, // XK_Greek_theta + 0x03b9: 0x07e9, // XK_Greek_iota + 0x03ba: 0x07ea, // XK_Greek_kappa + 0x03bb: 0x07eb, // XK_Greek_lamda + 0x03bc: 0x07ec, // XK_Greek_mu + 0x03bd: 0x07ed, // XK_Greek_nu + 0x03be: 0x07ee, // XK_Greek_xi + 0x03bf: 0x07ef, // XK_Greek_omicron + 0x03c0: 0x07f0, // XK_Greek_pi + 0x03c1: 0x07f1, // XK_Greek_rho + 0x03c2: 0x07f3, // XK_Greek_finalsmallsigma + 0x03c3: 0x07f2, // XK_Greek_sigma + 0x03c4: 0x07f4, // XK_Greek_tau + 0x03c5: 0x07f5, // XK_Greek_upsilon + 0x03c6: 0x07f6, // XK_Greek_phi + 0x03c7: 0x07f7, // XK_Greek_chi + 0x03c8: 0x07f8, // XK_Greek_psi + 0x03c9: 0x07f9, // XK_Greek_omega + 0x03ca: 0x07b5, // XK_Greek_iotadieresis + 0x03cb: 0x07b9, // XK_Greek_upsilondieresis + 0x03cc: 0x07b7, // XK_Greek_omicronaccent + 0x03cd: 0x07b8, // XK_Greek_upsilonaccent + 0x03ce: 0x07bb, // XK_Greek_omegaaccent + 0x0401: 0x06b3, // XK_Cyrillic_IO + 0x0402: 0x06b1, // XK_Serbian_DJE + 0x0403: 0x06b2, // XK_Macedonia_GJE + 0x0404: 0x06b4, // XK_Ukrainian_IE + 0x0405: 0x06b5, // XK_Macedonia_DSE + 0x0406: 0x06b6, // XK_Ukrainian_I + 0x0407: 0x06b7, // XK_Ukrainian_YI + 0x0408: 0x06b8, // XK_Cyrillic_JE + 0x0409: 0x06b9, // XK_Cyrillic_LJE + 0x040a: 0x06ba, // XK_Cyrillic_NJE + 0x040b: 0x06bb, // XK_Serbian_TSHE + 0x040c: 0x06bc, // XK_Macedonia_KJE + 0x040e: 0x06be, // XK_Byelorussian_SHORTU + 0x040f: 0x06bf, // XK_Cyrillic_DZHE + 0x0410: 0x06e1, // XK_Cyrillic_A + 0x0411: 0x06e2, // XK_Cyrillic_BE + 0x0412: 0x06f7, // XK_Cyrillic_VE + 0x0413: 0x06e7, // XK_Cyrillic_GHE + 0x0414: 0x06e4, // XK_Cyrillic_DE + 0x0415: 0x06e5, // XK_Cyrillic_IE + 0x0416: 0x06f6, // XK_Cyrillic_ZHE + 0x0417: 0x06fa, // XK_Cyrillic_ZE + 0x0418: 0x06e9, // XK_Cyrillic_I + 0x0419: 0x06ea, // XK_Cyrillic_SHORTI + 0x041a: 0x06eb, // XK_Cyrillic_KA + 0x041b: 0x06ec, // XK_Cyrillic_EL + 0x041c: 0x06ed, // XK_Cyrillic_EM + 0x041d: 0x06ee, // XK_Cyrillic_EN + 0x041e: 0x06ef, // XK_Cyrillic_O + 0x041f: 0x06f0, // XK_Cyrillic_PE + 0x0420: 0x06f2, // XK_Cyrillic_ER + 0x0421: 0x06f3, // XK_Cyrillic_ES + 0x0422: 0x06f4, // XK_Cyrillic_TE + 0x0423: 0x06f5, // XK_Cyrillic_U + 0x0424: 0x06e6, // XK_Cyrillic_EF + 0x0425: 0x06e8, // XK_Cyrillic_HA + 0x0426: 0x06e3, // XK_Cyrillic_TSE + 0x0427: 0x06fe, // XK_Cyrillic_CHE + 0x0428: 0x06fb, // XK_Cyrillic_SHA + 0x0429: 0x06fd, // XK_Cyrillic_SHCHA + 0x042a: 0x06ff, // XK_Cyrillic_HARDSIGN + 0x042b: 0x06f9, // XK_Cyrillic_YERU + 0x042c: 0x06f8, // XK_Cyrillic_SOFTSIGN + 0x042d: 0x06fc, // XK_Cyrillic_E + 0x042e: 0x06e0, // XK_Cyrillic_YU + 0x042f: 0x06f1, // XK_Cyrillic_YA + 0x0430: 0x06c1, // XK_Cyrillic_a + 0x0431: 0x06c2, // XK_Cyrillic_be + 0x0432: 0x06d7, // XK_Cyrillic_ve + 0x0433: 0x06c7, // XK_Cyrillic_ghe + 0x0434: 0x06c4, // XK_Cyrillic_de + 0x0435: 0x06c5, // XK_Cyrillic_ie + 0x0436: 0x06d6, // XK_Cyrillic_zhe + 0x0437: 0x06da, // XK_Cyrillic_ze + 0x0438: 0x06c9, // XK_Cyrillic_i + 0x0439: 0x06ca, // XK_Cyrillic_shorti + 0x043a: 0x06cb, // XK_Cyrillic_ka + 0x043b: 0x06cc, // XK_Cyrillic_el + 0x043c: 0x06cd, // XK_Cyrillic_em + 0x043d: 0x06ce, // XK_Cyrillic_en + 0x043e: 0x06cf, // XK_Cyrillic_o + 0x043f: 0x06d0, // XK_Cyrillic_pe + 0x0440: 0x06d2, // XK_Cyrillic_er + 0x0441: 0x06d3, // XK_Cyrillic_es + 0x0442: 0x06d4, // XK_Cyrillic_te + 0x0443: 0x06d5, // XK_Cyrillic_u + 0x0444: 0x06c6, // XK_Cyrillic_ef + 0x0445: 0x06c8, // XK_Cyrillic_ha + 0x0446: 0x06c3, // XK_Cyrillic_tse + 0x0447: 0x06de, // XK_Cyrillic_che + 0x0448: 0x06db, // XK_Cyrillic_sha + 0x0449: 0x06dd, // XK_Cyrillic_shcha + 0x044a: 0x06df, // XK_Cyrillic_hardsign + 0x044b: 0x06d9, // XK_Cyrillic_yeru + 0x044c: 0x06d8, // XK_Cyrillic_softsign + 0x044d: 0x06dc, // XK_Cyrillic_e + 0x044e: 0x06c0, // XK_Cyrillic_yu + 0x044f: 0x06d1, // XK_Cyrillic_ya + 0x0451: 0x06a3, // XK_Cyrillic_io + 0x0452: 0x06a1, // XK_Serbian_dje + 0x0453: 0x06a2, // XK_Macedonia_gje + 0x0454: 0x06a4, // XK_Ukrainian_ie + 0x0455: 0x06a5, // XK_Macedonia_dse + 0x0456: 0x06a6, // XK_Ukrainian_i + 0x0457: 0x06a7, // XK_Ukrainian_yi + 0x0458: 0x06a8, // XK_Cyrillic_je + 0x0459: 0x06a9, // XK_Cyrillic_lje + 0x045a: 0x06aa, // XK_Cyrillic_nje + 0x045b: 0x06ab, // XK_Serbian_tshe + 0x045c: 0x06ac, // XK_Macedonia_kje + 0x045e: 0x06ae, // XK_Byelorussian_shortu + 0x045f: 0x06af, // XK_Cyrillic_dzhe + 0x0490: 0x06bd, // XK_Ukrainian_GHE_WITH_UPTURN + 0x0491: 0x06ad, // XK_Ukrainian_ghe_with_upturn + 0x05d0: 0x0ce0, // XK_hebrew_aleph + 0x05d1: 0x0ce1, // XK_hebrew_bet + 0x05d2: 0x0ce2, // XK_hebrew_gimel + 0x05d3: 0x0ce3, // XK_hebrew_dalet + 0x05d4: 0x0ce4, // XK_hebrew_he + 0x05d5: 0x0ce5, // XK_hebrew_waw + 0x05d6: 0x0ce6, // XK_hebrew_zain + 0x05d7: 0x0ce7, // XK_hebrew_chet + 0x05d8: 0x0ce8, // XK_hebrew_tet + 0x05d9: 0x0ce9, // XK_hebrew_yod + 0x05da: 0x0cea, // XK_hebrew_finalkaph + 0x05db: 0x0ceb, // XK_hebrew_kaph + 0x05dc: 0x0cec, // XK_hebrew_lamed + 0x05dd: 0x0ced, // XK_hebrew_finalmem + 0x05de: 0x0cee, // XK_hebrew_mem + 0x05df: 0x0cef, // XK_hebrew_finalnun + 0x05e0: 0x0cf0, // XK_hebrew_nun + 0x05e1: 0x0cf1, // XK_hebrew_samech + 0x05e2: 0x0cf2, // XK_hebrew_ayin + 0x05e3: 0x0cf3, // XK_hebrew_finalpe + 0x05e4: 0x0cf4, // XK_hebrew_pe + 0x05e5: 0x0cf5, // XK_hebrew_finalzade + 0x05e6: 0x0cf6, // XK_hebrew_zade + 0x05e7: 0x0cf7, // XK_hebrew_qoph + 0x05e8: 0x0cf8, // XK_hebrew_resh + 0x05e9: 0x0cf9, // XK_hebrew_shin + 0x05ea: 0x0cfa, // XK_hebrew_taw + 0x060c: 0x05ac, // XK_Arabic_comma + 0x061b: 0x05bb, // XK_Arabic_semicolon + 0x061f: 0x05bf, // XK_Arabic_question_mark + 0x0621: 0x05c1, // XK_Arabic_hamza + 0x0622: 0x05c2, // XK_Arabic_maddaonalef + 0x0623: 0x05c3, // XK_Arabic_hamzaonalef + 0x0624: 0x05c4, // XK_Arabic_hamzaonwaw + 0x0625: 0x05c5, // XK_Arabic_hamzaunderalef + 0x0626: 0x05c6, // XK_Arabic_hamzaonyeh + 0x0627: 0x05c7, // XK_Arabic_alef + 0x0628: 0x05c8, // XK_Arabic_beh + 0x0629: 0x05c9, // XK_Arabic_tehmarbuta + 0x062a: 0x05ca, // XK_Arabic_teh + 0x062b: 0x05cb, // XK_Arabic_theh + 0x062c: 0x05cc, // XK_Arabic_jeem + 0x062d: 0x05cd, // XK_Arabic_hah + 0x062e: 0x05ce, // XK_Arabic_khah + 0x062f: 0x05cf, // XK_Arabic_dal + 0x0630: 0x05d0, // XK_Arabic_thal + 0x0631: 0x05d1, // XK_Arabic_ra + 0x0632: 0x05d2, // XK_Arabic_zain + 0x0633: 0x05d3, // XK_Arabic_seen + 0x0634: 0x05d4, // XK_Arabic_sheen + 0x0635: 0x05d5, // XK_Arabic_sad + 0x0636: 0x05d6, // XK_Arabic_dad + 0x0637: 0x05d7, // XK_Arabic_tah + 0x0638: 0x05d8, // XK_Arabic_zah + 0x0639: 0x05d9, // XK_Arabic_ain + 0x063a: 0x05da, // XK_Arabic_ghain + 0x0640: 0x05e0, // XK_Arabic_tatweel + 0x0641: 0x05e1, // XK_Arabic_feh + 0x0642: 0x05e2, // XK_Arabic_qaf + 0x0643: 0x05e3, // XK_Arabic_kaf + 0x0644: 0x05e4, // XK_Arabic_lam + 0x0645: 0x05e5, // XK_Arabic_meem + 0x0646: 0x05e6, // XK_Arabic_noon + 0x0647: 0x05e7, // XK_Arabic_ha + 0x0648: 0x05e8, // XK_Arabic_waw + 0x0649: 0x05e9, // XK_Arabic_alefmaksura + 0x064a: 0x05ea, // XK_Arabic_yeh + 0x064b: 0x05eb, // XK_Arabic_fathatan + 0x064c: 0x05ec, // XK_Arabic_dammatan + 0x064d: 0x05ed, // XK_Arabic_kasratan + 0x064e: 0x05ee, // XK_Arabic_fatha + 0x064f: 0x05ef, // XK_Arabic_damma + 0x0650: 0x05f0, // XK_Arabic_kasra + 0x0651: 0x05f1, // XK_Arabic_shadda + 0x0652: 0x05f2, // XK_Arabic_sukun + 0x0e01: 0x0da1, // XK_Thai_kokai + 0x0e02: 0x0da2, // XK_Thai_khokhai + 0x0e03: 0x0da3, // XK_Thai_khokhuat + 0x0e04: 0x0da4, // XK_Thai_khokhwai + 0x0e05: 0x0da5, // XK_Thai_khokhon + 0x0e06: 0x0da6, // XK_Thai_khorakhang + 0x0e07: 0x0da7, // XK_Thai_ngongu + 0x0e08: 0x0da8, // XK_Thai_chochan + 0x0e09: 0x0da9, // XK_Thai_choching + 0x0e0a: 0x0daa, // XK_Thai_chochang + 0x0e0b: 0x0dab, // XK_Thai_soso + 0x0e0c: 0x0dac, // XK_Thai_chochoe + 0x0e0d: 0x0dad, // XK_Thai_yoying + 0x0e0e: 0x0dae, // XK_Thai_dochada + 0x0e0f: 0x0daf, // XK_Thai_topatak + 0x0e10: 0x0db0, // XK_Thai_thothan + 0x0e11: 0x0db1, // XK_Thai_thonangmontho + 0x0e12: 0x0db2, // XK_Thai_thophuthao + 0x0e13: 0x0db3, // XK_Thai_nonen + 0x0e14: 0x0db4, // XK_Thai_dodek + 0x0e15: 0x0db5, // XK_Thai_totao + 0x0e16: 0x0db6, // XK_Thai_thothung + 0x0e17: 0x0db7, // XK_Thai_thothahan + 0x0e18: 0x0db8, // XK_Thai_thothong + 0x0e19: 0x0db9, // XK_Thai_nonu + 0x0e1a: 0x0dba, // XK_Thai_bobaimai + 0x0e1b: 0x0dbb, // XK_Thai_popla + 0x0e1c: 0x0dbc, // XK_Thai_phophung + 0x0e1d: 0x0dbd, // XK_Thai_fofa + 0x0e1e: 0x0dbe, // XK_Thai_phophan + 0x0e1f: 0x0dbf, // XK_Thai_fofan + 0x0e20: 0x0dc0, // XK_Thai_phosamphao + 0x0e21: 0x0dc1, // XK_Thai_moma + 0x0e22: 0x0dc2, // XK_Thai_yoyak + 0x0e23: 0x0dc3, // XK_Thai_rorua + 0x0e24: 0x0dc4, // XK_Thai_ru + 0x0e25: 0x0dc5, // XK_Thai_loling + 0x0e26: 0x0dc6, // XK_Thai_lu + 0x0e27: 0x0dc7, // XK_Thai_wowaen + 0x0e28: 0x0dc8, // XK_Thai_sosala + 0x0e29: 0x0dc9, // XK_Thai_sorusi + 0x0e2a: 0x0dca, // XK_Thai_sosua + 0x0e2b: 0x0dcb, // XK_Thai_hohip + 0x0e2c: 0x0dcc, // XK_Thai_lochula + 0x0e2d: 0x0dcd, // XK_Thai_oang + 0x0e2e: 0x0dce, // XK_Thai_honokhuk + 0x0e2f: 0x0dcf, // XK_Thai_paiyannoi + 0x0e30: 0x0dd0, // XK_Thai_saraa + 0x0e31: 0x0dd1, // XK_Thai_maihanakat + 0x0e32: 0x0dd2, // XK_Thai_saraaa + 0x0e33: 0x0dd3, // XK_Thai_saraam + 0x0e34: 0x0dd4, // XK_Thai_sarai + 0x0e35: 0x0dd5, // XK_Thai_saraii + 0x0e36: 0x0dd6, // XK_Thai_saraue + 0x0e37: 0x0dd7, // XK_Thai_sarauee + 0x0e38: 0x0dd8, // XK_Thai_sarau + 0x0e39: 0x0dd9, // XK_Thai_sarauu + 0x0e3a: 0x0dda, // XK_Thai_phinthu + 0x0e3f: 0x0ddf, // XK_Thai_baht + 0x0e40: 0x0de0, // XK_Thai_sarae + 0x0e41: 0x0de1, // XK_Thai_saraae + 0x0e42: 0x0de2, // XK_Thai_sarao + 0x0e43: 0x0de3, // XK_Thai_saraaimaimuan + 0x0e44: 0x0de4, // XK_Thai_saraaimaimalai + 0x0e45: 0x0de5, // XK_Thai_lakkhangyao + 0x0e46: 0x0de6, // XK_Thai_maiyamok + 0x0e47: 0x0de7, // XK_Thai_maitaikhu + 0x0e48: 0x0de8, // XK_Thai_maiek + 0x0e49: 0x0de9, // XK_Thai_maitho + 0x0e4a: 0x0dea, // XK_Thai_maitri + 0x0e4b: 0x0deb, // XK_Thai_maichattawa + 0x0e4c: 0x0dec, // XK_Thai_thanthakhat + 0x0e4d: 0x0ded, // XK_Thai_nikhahit + 0x0e50: 0x0df0, // XK_Thai_leksun + 0x0e51: 0x0df1, // XK_Thai_leknung + 0x0e52: 0x0df2, // XK_Thai_leksong + 0x0e53: 0x0df3, // XK_Thai_leksam + 0x0e54: 0x0df4, // XK_Thai_leksi + 0x0e55: 0x0df5, // XK_Thai_lekha + 0x0e56: 0x0df6, // XK_Thai_lekhok + 0x0e57: 0x0df7, // XK_Thai_lekchet + 0x0e58: 0x0df8, // XK_Thai_lekpaet + 0x0e59: 0x0df9, // XK_Thai_lekkao + 0x2002: 0x0aa2, // XK_enspace + 0x2003: 0x0aa1, // XK_emspace + 0x2004: 0x0aa3, // XK_em3space + 0x2005: 0x0aa4, // XK_em4space + 0x2007: 0x0aa5, // XK_digitspace + 0x2008: 0x0aa6, // XK_punctspace + 0x2009: 0x0aa7, // XK_thinspace + 0x200a: 0x0aa8, // XK_hairspace + 0x2012: 0x0abb, // XK_figdash + 0x2013: 0x0aaa, // XK_endash + 0x2014: 0x0aa9, // XK_emdash + 0x2015: 0x07af, // XK_Greek_horizbar + 0x2017: 0x0cdf, // XK_hebrew_doublelowline + 0x2018: 0x0ad0, // XK_leftsinglequotemark + 0x2019: 0x0ad1, // XK_rightsinglequotemark + 0x201a: 0x0afd, // XK_singlelowquotemark + 0x201c: 0x0ad2, // XK_leftdoublequotemark + 0x201d: 0x0ad3, // XK_rightdoublequotemark + 0x201e: 0x0afe, // XK_doublelowquotemark + 0x2020: 0x0af1, // XK_dagger + 0x2021: 0x0af2, // XK_doubledagger + 0x2022: 0x0ae6, // XK_enfilledcircbullet + 0x2025: 0x0aaf, // XK_doubbaselinedot + 0x2026: 0x0aae, // XK_ellipsis + 0x2030: 0x0ad5, // XK_permille + 0x2032: 0x0ad6, // XK_minutes + 0x2033: 0x0ad7, // XK_seconds + 0x2038: 0x0afc, // XK_caret + 0x203e: 0x047e, // XK_overline + 0x20a9: 0x0eff, // XK_Korean_Won + 0x20ac: 0x20ac, // XK_EuroSign + 0x2105: 0x0ab8, // XK_careof + 0x2116: 0x06b0, // XK_numerosign + 0x2117: 0x0afb, // XK_phonographcopyright + 0x211e: 0x0ad4, // XK_prescription + 0x2122: 0x0ac9, // XK_trademark + 0x2153: 0x0ab0, // XK_onethird + 0x2154: 0x0ab1, // XK_twothirds + 0x2155: 0x0ab2, // XK_onefifth + 0x2156: 0x0ab3, // XK_twofifths + 0x2157: 0x0ab4, // XK_threefifths + 0x2158: 0x0ab5, // XK_fourfifths + 0x2159: 0x0ab6, // XK_onesixth + 0x215a: 0x0ab7, // XK_fivesixths + 0x215b: 0x0ac3, // XK_oneeighth + 0x215c: 0x0ac4, // XK_threeeighths + 0x215d: 0x0ac5, // XK_fiveeighths + 0x215e: 0x0ac6, // XK_seveneighths + 0x2190: 0x08fb, // XK_leftarrow + 0x2191: 0x08fc, // XK_uparrow + 0x2192: 0x08fd, // XK_rightarrow + 0x2193: 0x08fe, // XK_downarrow + 0x21d2: 0x08ce, // XK_implies + 0x21d4: 0x08cd, // XK_ifonlyif + 0x2202: 0x08ef, // XK_partialderivative + 0x2207: 0x08c5, // XK_nabla + 0x2218: 0x0bca, // XK_jot + 0x221a: 0x08d6, // XK_radical + 0x221d: 0x08c1, // XK_variation + 0x221e: 0x08c2, // XK_infinity + 0x2227: 0x08de, // XK_logicaland + 0x2228: 0x08df, // XK_logicalor + 0x2229: 0x08dc, // XK_intersection + 0x222a: 0x08dd, // XK_union + 0x222b: 0x08bf, // XK_integral + 0x2234: 0x08c0, // XK_therefore + 0x223c: 0x08c8, // XK_approximate + 0x2243: 0x08c9, // XK_similarequal + 0x2245: 0x1002248, // XK_approxeq + 0x2260: 0x08bd, // XK_notequal + 0x2261: 0x08cf, // XK_identical + 0x2264: 0x08bc, // XK_lessthanequal + 0x2265: 0x08be, // XK_greaterthanequal + 0x2282: 0x08da, // XK_includedin + 0x2283: 0x08db, // XK_includes + 0x22a2: 0x0bfc, // XK_righttack + 0x22a3: 0x0bdc, // XK_lefttack + 0x22a4: 0x0bc2, // XK_downtack + 0x22a5: 0x0bce, // XK_uptack + 0x2308: 0x0bd3, // XK_upstile + 0x230a: 0x0bc4, // XK_downstile + 0x2315: 0x0afa, // XK_telephonerecorder + 0x2320: 0x08a4, // XK_topintegral + 0x2321: 0x08a5, // XK_botintegral + 0x2395: 0x0bcc, // XK_quad + 0x239b: 0x08ab, // XK_topleftparens + 0x239d: 0x08ac, // XK_botleftparens + 0x239e: 0x08ad, // XK_toprightparens + 0x23a0: 0x08ae, // XK_botrightparens + 0x23a1: 0x08a7, // XK_topleftsqbracket + 0x23a3: 0x08a8, // XK_botleftsqbracket + 0x23a4: 0x08a9, // XK_toprightsqbracket + 0x23a6: 0x08aa, // XK_botrightsqbracket + 0x23a8: 0x08af, // XK_leftmiddlecurlybrace + 0x23ac: 0x08b0, // XK_rightmiddlecurlybrace + 0x23b7: 0x08a1, // XK_leftradical + 0x23ba: 0x09ef, // XK_horizlinescan1 + 0x23bb: 0x09f0, // XK_horizlinescan3 + 0x23bc: 0x09f2, // XK_horizlinescan7 + 0x23bd: 0x09f3, // XK_horizlinescan9 + 0x2409: 0x09e2, // XK_ht + 0x240a: 0x09e5, // XK_lf + 0x240b: 0x09e9, // XK_vt + 0x240c: 0x09e3, // XK_ff + 0x240d: 0x09e4, // XK_cr + 0x2423: 0x0aac, // XK_signifblank + 0x2424: 0x09e8, // XK_nl + 0x2500: 0x08a3, // XK_horizconnector + 0x2502: 0x08a6, // XK_vertconnector + 0x250c: 0x08a2, // XK_topleftradical + 0x2510: 0x09eb, // XK_uprightcorner + 0x2514: 0x09ed, // XK_lowleftcorner + 0x2518: 0x09ea, // XK_lowrightcorner + 0x251c: 0x09f4, // XK_leftt + 0x2524: 0x09f5, // XK_rightt + 0x252c: 0x09f7, // XK_topt + 0x2534: 0x09f6, // XK_bott + 0x253c: 0x09ee, // XK_crossinglines + 0x2592: 0x09e1, // XK_checkerboard + 0x25aa: 0x0ae7, // XK_enfilledsqbullet + 0x25ab: 0x0ae1, // XK_enopensquarebullet + 0x25ac: 0x0adb, // XK_filledrectbullet + 0x25ad: 0x0ae2, // XK_openrectbullet + 0x25ae: 0x0adf, // XK_emfilledrect + 0x25af: 0x0acf, // XK_emopenrectangle + 0x25b2: 0x0ae8, // XK_filledtribulletup + 0x25b3: 0x0ae3, // XK_opentribulletup + 0x25b6: 0x0add, // XK_filledrighttribullet + 0x25b7: 0x0acd, // XK_rightopentriangle + 0x25bc: 0x0ae9, // XK_filledtribulletdown + 0x25bd: 0x0ae4, // XK_opentribulletdown + 0x25c0: 0x0adc, // XK_filledlefttribullet + 0x25c1: 0x0acc, // XK_leftopentriangle + 0x25c6: 0x09e0, // XK_soliddiamond + 0x25cb: 0x0ace, // XK_emopencircle + 0x25cf: 0x0ade, // XK_emfilledcircle + 0x25e6: 0x0ae0, // XK_enopencircbullet + 0x2606: 0x0ae5, // XK_openstar + 0x260e: 0x0af9, // XK_telephone + 0x2613: 0x0aca, // XK_signaturemark + 0x261c: 0x0aea, // XK_leftpointer + 0x261e: 0x0aeb, // XK_rightpointer + 0x2640: 0x0af8, // XK_femalesymbol + 0x2642: 0x0af7, // XK_malesymbol + 0x2663: 0x0aec, // XK_club + 0x2665: 0x0aee, // XK_heart + 0x2666: 0x0aed, // XK_diamond + 0x266d: 0x0af6, // XK_musicalflat + 0x266f: 0x0af5, // XK_musicalsharp + 0x2713: 0x0af3, // XK_checkmark + 0x2717: 0x0af4, // XK_ballotcross + 0x271d: 0x0ad9, // XK_latincross + 0x2720: 0x0af0, // XK_maltesecross + 0x27e8: 0x0abc, // XK_leftanglebracket + 0x27e9: 0x0abe, // XK_rightanglebracket + 0x3001: 0x04a4, // XK_kana_comma + 0x3002: 0x04a1, // XK_kana_fullstop + 0x300c: 0x04a2, // XK_kana_openingbracket + 0x300d: 0x04a3, // XK_kana_closingbracket + 0x309b: 0x04de, // XK_voicedsound + 0x309c: 0x04df, // XK_semivoicedsound + 0x30a1: 0x04a7, // XK_kana_a + 0x30a2: 0x04b1, // XK_kana_A + 0x30a3: 0x04a8, // XK_kana_i + 0x30a4: 0x04b2, // XK_kana_I + 0x30a5: 0x04a9, // XK_kana_u + 0x30a6: 0x04b3, // XK_kana_U + 0x30a7: 0x04aa, // XK_kana_e + 0x30a8: 0x04b4, // XK_kana_E + 0x30a9: 0x04ab, // XK_kana_o + 0x30aa: 0x04b5, // XK_kana_O + 0x30ab: 0x04b6, // XK_kana_KA + 0x30ad: 0x04b7, // XK_kana_KI + 0x30af: 0x04b8, // XK_kana_KU + 0x30b1: 0x04b9, // XK_kana_KE + 0x30b3: 0x04ba, // XK_kana_KO + 0x30b5: 0x04bb, // XK_kana_SA + 0x30b7: 0x04bc, // XK_kana_SHI + 0x30b9: 0x04bd, // XK_kana_SU + 0x30bb: 0x04be, // XK_kana_SE + 0x30bd: 0x04bf, // XK_kana_SO + 0x30bf: 0x04c0, // XK_kana_TA + 0x30c1: 0x04c1, // XK_kana_CHI + 0x30c3: 0x04af, // XK_kana_tsu + 0x30c4: 0x04c2, // XK_kana_TSU + 0x30c6: 0x04c3, // XK_kana_TE + 0x30c8: 0x04c4, // XK_kana_TO + 0x30ca: 0x04c5, // XK_kana_NA + 0x30cb: 0x04c6, // XK_kana_NI + 0x30cc: 0x04c7, // XK_kana_NU + 0x30cd: 0x04c8, // XK_kana_NE + 0x30ce: 0x04c9, // XK_kana_NO + 0x30cf: 0x04ca, // XK_kana_HA + 0x30d2: 0x04cb, // XK_kana_HI + 0x30d5: 0x04cc, // XK_kana_FU + 0x30d8: 0x04cd, // XK_kana_HE + 0x30db: 0x04ce, // XK_kana_HO + 0x30de: 0x04cf, // XK_kana_MA + 0x30df: 0x04d0, // XK_kana_MI + 0x30e0: 0x04d1, // XK_kana_MU + 0x30e1: 0x04d2, // XK_kana_ME + 0x30e2: 0x04d3, // XK_kana_MO + 0x30e3: 0x04ac, // XK_kana_ya + 0x30e4: 0x04d4, // XK_kana_YA + 0x30e5: 0x04ad, // XK_kana_yu + 0x30e6: 0x04d5, // XK_kana_YU + 0x30e7: 0x04ae, // XK_kana_yo + 0x30e8: 0x04d6, // XK_kana_YO + 0x30e9: 0x04d7, // XK_kana_RA + 0x30ea: 0x04d8, // XK_kana_RI + 0x30eb: 0x04d9, // XK_kana_RU + 0x30ec: 0x04da, // XK_kana_RE + 0x30ed: 0x04db, // XK_kana_RO + 0x30ef: 0x04dc, // XK_kana_WA + 0x30f2: 0x04a6, // XK_kana_WO + 0x30f3: 0x04dd, // XK_kana_N + 0x30fb: 0x04a5, // XK_kana_conjunctive + 0x30fc: 0x04b0, // XK_prolongedsound +}; + +export default { + lookup(u) { + // Latin-1 is one-to-one mapping + if ((u >= 0x20) && (u <= 0xff)) { + return u; + } + + // Lookup table (fairly random) + const keysym = codepoints[u]; + if (keysym !== undefined) { + return keysym; + } + + // General mapping as final fallback + return 0x01000000 | u; + }, +}; diff --git a/systemvm/debian/root/noVNC/core/input/mouse.js b/systemvm/debian/root/noVNC/core/input/mouse.js new file mode 100644 index 000000000000..58a2982a9614 --- /dev/null +++ b/systemvm/debian/root/noVNC/core/input/mouse.js @@ -0,0 +1,276 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 or any later version (see LICENSE.txt) + */ + +import * as Log from '../util/logging.js'; +import { isTouchDevice } from '../util/browser.js'; +import { setCapture, stopEvent, getPointerEvent } from '../util/events.js'; + +const WHEEL_STEP = 10; // Delta threshold for a mouse wheel step +const WHEEL_STEP_TIMEOUT = 50; // ms +const WHEEL_LINE_HEIGHT = 19; + +export default class Mouse { + constructor(target) { + this._target = target || document; + + this._doubleClickTimer = null; + this._lastTouchPos = null; + + this._pos = null; + this._wheelStepXTimer = null; + this._wheelStepYTimer = null; + this._accumulatedWheelDeltaX = 0; + this._accumulatedWheelDeltaY = 0; + + this._eventHandlers = { + 'mousedown': this._handleMouseDown.bind(this), + 'mouseup': this._handleMouseUp.bind(this), + 'mousemove': this._handleMouseMove.bind(this), + 'mousewheel': this._handleMouseWheel.bind(this), + 'mousedisable': this._handleMouseDisable.bind(this) + }; + + // ===== PROPERTIES ===== + + this.touchButton = 1; // Button mask (1, 2, 4) for touch devices (0 means ignore clicks) + + // ===== EVENT HANDLERS ===== + + this.onmousebutton = () => {}; // Handler for mouse button click/release + this.onmousemove = () => {}; // Handler for mouse movement + } + + // ===== PRIVATE METHODS ===== + + _resetDoubleClickTimer() { + this._doubleClickTimer = null; + } + + _handleMouseButton(e, down) { + this._updateMousePosition(e); + let pos = this._pos; + + let bmask; + if (e.touches || e.changedTouches) { + // Touch device + + // When two touches occur within 500 ms of each other and are + // close enough together a double click is triggered. + if (down == 1) { + if (this._doubleClickTimer === null) { + this._lastTouchPos = pos; + } else { + clearTimeout(this._doubleClickTimer); + + // When the distance between the two touches is small enough + // force the position of the latter touch to the position of + // the first. + + const xs = this._lastTouchPos.x - pos.x; + const ys = this._lastTouchPos.y - pos.y; + const d = Math.sqrt((xs * xs) + (ys * ys)); + + // The goal is to trigger on a certain physical width, the + // devicePixelRatio brings us a bit closer but is not optimal. + const threshold = 20 * (window.devicePixelRatio || 1); + if (d < threshold) { + pos = this._lastTouchPos; + } + } + this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500); + } + bmask = this.touchButton; + // If bmask is set + } else if (e.which) { + /* everything except IE */ + bmask = 1 << e.button; + } else { + /* IE including 9 */ + bmask = (e.button & 0x1) + // Left + (e.button & 0x2) * 2 + // Right + (e.button & 0x4) / 2; // Middle + } + + Log.Debug("onmousebutton " + (down ? "down" : "up") + + ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask); + this.onmousebutton(pos.x, pos.y, down, bmask); + + stopEvent(e); + } + + _handleMouseDown(e) { + // Touch events have implicit capture + if (e.type === "mousedown") { + setCapture(this._target); + } + + this._handleMouseButton(e, 1); + } + + _handleMouseUp(e) { + this._handleMouseButton(e, 0); + } + + // Mouse wheel events are sent in steps over VNC. This means that the VNC + // protocol can't handle a wheel event with specific distance or speed. + // Therefor, if we get a lot of small mouse wheel events we combine them. + _generateWheelStepX() { + + if (this._accumulatedWheelDeltaX < 0) { + this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 5); + this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 5); + } else if (this._accumulatedWheelDeltaX > 0) { + this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 6); + this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 6); + } + + this._accumulatedWheelDeltaX = 0; + } + + _generateWheelStepY() { + + if (this._accumulatedWheelDeltaY < 0) { + this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 3); + this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 3); + } else if (this._accumulatedWheelDeltaY > 0) { + this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 4); + this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 4); + } + + this._accumulatedWheelDeltaY = 0; + } + + _resetWheelStepTimers() { + window.clearTimeout(this._wheelStepXTimer); + window.clearTimeout(this._wheelStepYTimer); + this._wheelStepXTimer = null; + this._wheelStepYTimer = null; + } + + _handleMouseWheel(e) { + this._resetWheelStepTimers(); + + this._updateMousePosition(e); + + let dX = e.deltaX; + let dY = e.deltaY; + + // Pixel units unless it's non-zero. + // Note that if deltamode is line or page won't matter since we aren't + // sending the mouse wheel delta to the server anyway. + // The difference between pixel and line can be important however since + // we have a threshold that can be smaller than the line height. + if (e.deltaMode !== 0) { + dX *= WHEEL_LINE_HEIGHT; + dY *= WHEEL_LINE_HEIGHT; + } + + this._accumulatedWheelDeltaX += dX; + this._accumulatedWheelDeltaY += dY; + + // Generate a mouse wheel step event when the accumulated delta + // for one of the axes is large enough. + // Small delta events that do not pass the threshold get sent + // after a timeout. + if (Math.abs(this._accumulatedWheelDeltaX) > WHEEL_STEP) { + this._generateWheelStepX(); + } else { + this._wheelStepXTimer = + window.setTimeout(this._generateWheelStepX.bind(this), + WHEEL_STEP_TIMEOUT); + } + if (Math.abs(this._accumulatedWheelDeltaY) > WHEEL_STEP) { + this._generateWheelStepY(); + } else { + this._wheelStepYTimer = + window.setTimeout(this._generateWheelStepY.bind(this), + WHEEL_STEP_TIMEOUT); + } + + stopEvent(e); + } + + _handleMouseMove(e) { + this._updateMousePosition(e); + this.onmousemove(this._pos.x, this._pos.y); + stopEvent(e); + } + + _handleMouseDisable(e) { + /* + * Stop propagation if inside canvas area + * Note: This is only needed for the 'click' event as it fails + * to fire properly for the target element so we have + * to listen on the document element instead. + */ + if (e.target == this._target) { + stopEvent(e); + } + } + + // Update coordinates relative to target + _updateMousePosition(e) { + e = getPointerEvent(e); + const bounds = this._target.getBoundingClientRect(); + let x; + let y; + // Clip to target bounds + if (e.clientX < bounds.left) { + x = 0; + } else if (e.clientX >= bounds.right) { + x = bounds.width - 1; + } else { + x = e.clientX - bounds.left; + } + if (e.clientY < bounds.top) { + y = 0; + } else if (e.clientY >= bounds.bottom) { + y = bounds.height - 1; + } else { + y = e.clientY - bounds.top; + } + this._pos = {x: x, y: y}; + } + + // ===== PUBLIC METHODS ===== + + grab() { + if (isTouchDevice) { + this._target.addEventListener('touchstart', this._eventHandlers.mousedown); + this._target.addEventListener('touchend', this._eventHandlers.mouseup); + this._target.addEventListener('touchmove', this._eventHandlers.mousemove); + } + this._target.addEventListener('mousedown', this._eventHandlers.mousedown); + this._target.addEventListener('mouseup', this._eventHandlers.mouseup); + this._target.addEventListener('mousemove', this._eventHandlers.mousemove); + this._target.addEventListener('wheel', this._eventHandlers.mousewheel); + + /* Prevent middle-click pasting (see above for why we bind to document) */ + document.addEventListener('click', this._eventHandlers.mousedisable); + + /* preventDefault() on mousedown doesn't stop this event for some + reason so we have to explicitly block it */ + this._target.addEventListener('contextmenu', this._eventHandlers.mousedisable); + } + + ungrab() { + this._resetWheelStepTimers(); + + if (isTouchDevice) { + this._target.removeEventListener('touchstart', this._eventHandlers.mousedown); + this._target.removeEventListener('touchend', this._eventHandlers.mouseup); + this._target.removeEventListener('touchmove', this._eventHandlers.mousemove); + } + this._target.removeEventListener('mousedown', this._eventHandlers.mousedown); + this._target.removeEventListener('mouseup', this._eventHandlers.mouseup); + this._target.removeEventListener('mousemove', this._eventHandlers.mousemove); + this._target.removeEventListener('wheel', this._eventHandlers.mousewheel); + + document.removeEventListener('click', this._eventHandlers.mousedisable); + + this._target.removeEventListener('contextmenu', this._eventHandlers.mousedisable); + } +} diff --git a/systemvm/debian/root/noVNC/core/input/util.js b/systemvm/debian/root/noVNC/core/input/util.js new file mode 100644 index 000000000000..f177ef53d36c --- /dev/null +++ b/systemvm/debian/root/noVNC/core/input/util.js @@ -0,0 +1,164 @@ +import keysyms from "./keysymdef.js"; +import vkeys from "./vkeys.js"; +import fixedkeys from "./fixedkeys.js"; +import DOMKeyTable from "./domkeytable.js"; +import * as browser from "../util/browser.js"; + +// Get 'KeyboardEvent.code', handling legacy browsers +export function getKeycode(evt) { + // Are we getting proper key identifiers? + // (unfortunately Firefox and Chrome are crappy here and gives + // us an empty string on some platforms, rather than leaving it + // undefined) + if (evt.code) { + // Mozilla isn't fully in sync with the spec yet + switch (evt.code) { + case 'OSLeft': return 'MetaLeft'; + case 'OSRight': return 'MetaRight'; + } + + return evt.code; + } + + // 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)) { + let code = vkeys[evt.keyCode]; + + // macOS has messed up this code for some reason + if (browser.isMac() && (code === 'ContextMenu')) { + code = 'MetaRight'; + } + + // The keyCode doesn't distinguish between left and right + // for the standard modifiers + if (evt.location === 2) { + switch (code) { + case 'ShiftLeft': return 'ShiftRight'; + case 'ControlLeft': return 'ControlRight'; + case 'AltLeft': return 'AltRight'; + } + } + + // Nor a bunch of the numpad keys + if (evt.location === 3) { + switch (code) { + case 'Delete': return 'NumpadDecimal'; + case 'Insert': return 'Numpad0'; + case 'End': return 'Numpad1'; + case 'ArrowDown': return 'Numpad2'; + case 'PageDown': return 'Numpad3'; + case 'ArrowLeft': return 'Numpad4'; + case 'ArrowRight': return 'Numpad6'; + case 'Home': return 'Numpad7'; + case 'ArrowUp': return 'Numpad8'; + case 'PageUp': return 'Numpad9'; + case 'Enter': return 'NumpadEnter'; + } + } + + return code; + } + + return 'Unidentified'; +} + +// 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; + } + + // Mozilla isn't fully in sync with the spec yet + switch (evt.key) { + case 'OS': return 'Meta'; + } + + // iOS leaks some OS names + switch (evt.key) { + case 'UIKeyInputUpArrow': return 'ArrowUp'; + case 'UIKeyInputDownArrow': return 'ArrowDown'; + case 'UIKeyInputLeftArrow': return 'ArrowLeft'; + case 'UIKeyInputRightArrow': return 'ArrowRight'; + case 'UIKeyInputEscape': return 'Escape'; + } + + // IE and Edge have broken handling of AltGraph so we cannot + // trust them for printable characters + if ((evt.key.length !== 1) || (!browser.isIE() && !browser.isEdge())) { + return evt.key; + } + } + + // Try to deduce it based on the physical key + const code = getKeycode(evt); + if (code in fixedkeys) { + return fixedkeys[code]; + } + + // If that failed, then see if we have a printable character + if (evt.charCode) { + return String.fromCharCode(evt.charCode); + } + + // At this point we have nothing left to go on + return 'Unidentified'; +} + +// Get the most reliable keysym value we can get from a key event +export function getKeysym(evt) { + const key = getKey(evt); + + if (key === 'Unidentified') { + return null; + } + + // First look up special keys + if (key in DOMKeyTable) { + let location = evt.location; + + // Safari screws up location for the right cmd key + if ((key === 'Meta') && (location === 0)) { + location = 2; + } + + if ((location === undefined) || (location > 3)) { + location = 0; + } + + return DOMKeyTable[key][location]; + } + + // Now we need to look at the Unicode symbol instead + + // Special key? (FIXME: Should have been caught earlier) + if (key.length !== 1) { + return null; + } + + const codepoint = key.charCodeAt(); + if (codepoint) { + return keysyms.lookup(codepoint); + } + + return null; +} diff --git a/systemvm/debian/root/noVNC/core/input/vkeys.js b/systemvm/debian/root/noVNC/core/input/vkeys.js new file mode 100644 index 000000000000..f84109b2559e --- /dev/null +++ b/systemvm/debian/root/noVNC/core/input/vkeys.js @@ -0,0 +1,117 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 or any later version (see LICENSE.txt) + */ + +/* + * Mapping between Microsoft® Windows® Virtual-Key codes and + * HTML key codes. + */ + +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', + 0x12: 'AltLeft', + 0x13: 'Pause', + 0x14: 'CapsLock', + 0x15: 'Lang1', + 0x19: 'Lang2', + 0x1b: 'Escape', + 0x1c: 'Convert', + 0x1d: 'NonConvert', + 0x20: 'Space', + 0x21: 'PageUp', + 0x22: 'PageDown', + 0x23: 'End', + 0x24: 'Home', + 0x25: 'ArrowLeft', + 0x26: 'ArrowUp', + 0x27: 'ArrowRight', + 0x28: 'ArrowDown', + 0x29: 'Select', + 0x2c: 'PrintScreen', + 0x2d: 'Insert', + 0x2e: 'Delete', + 0x2f: 'Help', + 0x30: 'Digit0', + 0x31: 'Digit1', + 0x32: 'Digit2', + 0x33: 'Digit3', + 0x34: 'Digit4', + 0x35: 'Digit5', + 0x36: 'Digit6', + 0x37: 'Digit7', + 0x38: 'Digit8', + 0x39: 'Digit9', + 0x5b: 'MetaLeft', + 0x5c: 'MetaRight', + 0x5d: 'ContextMenu', + 0x5f: 'Sleep', + 0x60: 'Numpad0', + 0x61: 'Numpad1', + 0x62: 'Numpad2', + 0x63: 'Numpad3', + 0x64: 'Numpad4', + 0x65: 'Numpad5', + 0x66: 'Numpad6', + 0x67: 'Numpad7', + 0x68: 'Numpad8', + 0x69: 'Numpad9', + 0x6a: 'NumpadMultiply', + 0x6b: 'NumpadAdd', + 0x6c: 'NumpadDecimal', + 0x6d: 'NumpadSubtract', + 0x6e: 'NumpadDecimal', // Duplicate, because buggy on Windows + 0x6f: 'NumpadDivide', + 0x70: 'F1', + 0x71: 'F2', + 0x72: 'F3', + 0x73: 'F4', + 0x74: 'F5', + 0x75: 'F6', + 0x76: 'F7', + 0x77: 'F8', + 0x78: 'F9', + 0x79: 'F10', + 0x7a: 'F11', + 0x7b: 'F12', + 0x7c: 'F13', + 0x7d: 'F14', + 0x7e: 'F15', + 0x7f: 'F16', + 0x80: 'F17', + 0x81: 'F18', + 0x82: 'F19', + 0x83: 'F20', + 0x84: 'F21', + 0x85: 'F22', + 0x86: 'F23', + 0x87: 'F24', + 0x90: 'NumLock', + 0x91: 'ScrollLock', + 0xa6: 'BrowserBack', + 0xa7: 'BrowserForward', + 0xa8: 'BrowserRefresh', + 0xa9: 'BrowserStop', + 0xaa: 'BrowserSearch', + 0xab: 'BrowserFavorites', + 0xac: 'BrowserHome', + 0xad: 'AudioVolumeMute', + 0xae: 'AudioVolumeDown', + 0xaf: 'AudioVolumeUp', + 0xb0: 'MediaTrackNext', + 0xb1: 'MediaTrackPrevious', + 0xb2: 'MediaStop', + 0xb3: 'MediaPlayPause', + 0xb4: 'LaunchMail', + 0xb5: 'MediaSelect', + 0xb6: 'LaunchApp1', + 0xb7: 'LaunchApp2', + 0xe1: 'AltRight', // Only when it is AltGraph +}; diff --git a/systemvm/debian/root/noVNC/core/input/xtscancodes.js b/systemvm/debian/root/noVNC/core/input/xtscancodes.js new file mode 100644 index 000000000000..514809c6fa23 --- /dev/null +++ b/systemvm/debian/root/noVNC/core/input/xtscancodes.js @@ -0,0 +1,171 @@ +/* + * This file is auto-generated from keymaps.csv on 2017-05-31 16:20 + * Database checksum sha256(92fd165507f2a3b8c5b3fa56e425d45788dbcb98cf067a307527d91ce22cab94) + * To re-generate, run: + * keymap-gen --lang=js code-map keymaps.csv html atset1 +*/ +export default { + "Again": 0xe005, /* html:Again (Again) -> linux:129 (KEY_AGAIN) -> atset1:57349 */ + "AltLeft": 0x38, /* html:AltLeft (AltLeft) -> linux:56 (KEY_LEFTALT) -> atset1:56 */ + "AltRight": 0xe038, /* html:AltRight (AltRight) -> linux:100 (KEY_RIGHTALT) -> atset1:57400 */ + "ArrowDown": 0xe050, /* html:ArrowDown (ArrowDown) -> linux:108 (KEY_DOWN) -> atset1:57424 */ + "ArrowLeft": 0xe04b, /* html:ArrowLeft (ArrowLeft) -> linux:105 (KEY_LEFT) -> atset1:57419 */ + "ArrowRight": 0xe04d, /* html:ArrowRight (ArrowRight) -> linux:106 (KEY_RIGHT) -> atset1:57421 */ + "ArrowUp": 0xe048, /* html:ArrowUp (ArrowUp) -> linux:103 (KEY_UP) -> atset1:57416 */ + "AudioVolumeDown": 0xe02e, /* html:AudioVolumeDown (AudioVolumeDown) -> linux:114 (KEY_VOLUMEDOWN) -> atset1:57390 */ + "AudioVolumeMute": 0xe020, /* html:AudioVolumeMute (AudioVolumeMute) -> linux:113 (KEY_MUTE) -> atset1:57376 */ + "AudioVolumeUp": 0xe030, /* html:AudioVolumeUp (AudioVolumeUp) -> linux:115 (KEY_VOLUMEUP) -> atset1:57392 */ + "Backquote": 0x29, /* html:Backquote (Backquote) -> linux:41 (KEY_GRAVE) -> atset1:41 */ + "Backslash": 0x2b, /* html:Backslash (Backslash) -> linux:43 (KEY_BACKSLASH) -> atset1:43 */ + "Backspace": 0xe, /* html:Backspace (Backspace) -> linux:14 (KEY_BACKSPACE) -> atset1:14 */ + "BracketLeft": 0x1a, /* html:BracketLeft (BracketLeft) -> linux:26 (KEY_LEFTBRACE) -> atset1:26 */ + "BracketRight": 0x1b, /* html:BracketRight (BracketRight) -> linux:27 (KEY_RIGHTBRACE) -> atset1:27 */ + "BrowserBack": 0xe06a, /* html:BrowserBack (BrowserBack) -> linux:158 (KEY_BACK) -> atset1:57450 */ + "BrowserFavorites": 0xe066, /* html:BrowserFavorites (BrowserFavorites) -> linux:156 (KEY_BOOKMARKS) -> atset1:57446 */ + "BrowserForward": 0xe069, /* html:BrowserForward (BrowserForward) -> linux:159 (KEY_FORWARD) -> atset1:57449 */ + "BrowserHome": 0xe032, /* html:BrowserHome (BrowserHome) -> linux:172 (KEY_HOMEPAGE) -> atset1:57394 */ + "BrowserRefresh": 0xe067, /* html:BrowserRefresh (BrowserRefresh) -> linux:173 (KEY_REFRESH) -> atset1:57447 */ + "BrowserSearch": 0xe065, /* html:BrowserSearch (BrowserSearch) -> linux:217 (KEY_SEARCH) -> atset1:57445 */ + "BrowserStop": 0xe068, /* html:BrowserStop (BrowserStop) -> linux:128 (KEY_STOP) -> atset1:57448 */ + "CapsLock": 0x3a, /* html:CapsLock (CapsLock) -> linux:58 (KEY_CAPSLOCK) -> atset1:58 */ + "Comma": 0x33, /* html:Comma (Comma) -> linux:51 (KEY_COMMA) -> atset1:51 */ + "ContextMenu": 0xe05d, /* html:ContextMenu (ContextMenu) -> linux:127 (KEY_COMPOSE) -> atset1:57437 */ + "ControlLeft": 0x1d, /* html:ControlLeft (ControlLeft) -> linux:29 (KEY_LEFTCTRL) -> atset1:29 */ + "ControlRight": 0xe01d, /* html:ControlRight (ControlRight) -> linux:97 (KEY_RIGHTCTRL) -> atset1:57373 */ + "Convert": 0x79, /* html:Convert (Convert) -> linux:92 (KEY_HENKAN) -> atset1:121 */ + "Copy": 0xe078, /* html:Copy (Copy) -> linux:133 (KEY_COPY) -> atset1:57464 */ + "Cut": 0xe03c, /* html:Cut (Cut) -> linux:137 (KEY_CUT) -> atset1:57404 */ + "Delete": 0xe053, /* html:Delete (Delete) -> linux:111 (KEY_DELETE) -> atset1:57427 */ + "Digit0": 0xb, /* html:Digit0 (Digit0) -> linux:11 (KEY_0) -> atset1:11 */ + "Digit1": 0x2, /* html:Digit1 (Digit1) -> linux:2 (KEY_1) -> atset1:2 */ + "Digit2": 0x3, /* html:Digit2 (Digit2) -> linux:3 (KEY_2) -> atset1:3 */ + "Digit3": 0x4, /* html:Digit3 (Digit3) -> linux:4 (KEY_3) -> atset1:4 */ + "Digit4": 0x5, /* html:Digit4 (Digit4) -> linux:5 (KEY_4) -> atset1:5 */ + "Digit5": 0x6, /* html:Digit5 (Digit5) -> linux:6 (KEY_5) -> atset1:6 */ + "Digit6": 0x7, /* html:Digit6 (Digit6) -> linux:7 (KEY_6) -> atset1:7 */ + "Digit7": 0x8, /* html:Digit7 (Digit7) -> linux:8 (KEY_7) -> atset1:8 */ + "Digit8": 0x9, /* html:Digit8 (Digit8) -> linux:9 (KEY_8) -> atset1:9 */ + "Digit9": 0xa, /* html:Digit9 (Digit9) -> linux:10 (KEY_9) -> atset1:10 */ + "Eject": 0xe07d, /* html:Eject (Eject) -> linux:162 (KEY_EJECTCLOSECD) -> atset1:57469 */ + "End": 0xe04f, /* html:End (End) -> linux:107 (KEY_END) -> atset1:57423 */ + "Enter": 0x1c, /* html:Enter (Enter) -> linux:28 (KEY_ENTER) -> atset1:28 */ + "Equal": 0xd, /* html:Equal (Equal) -> linux:13 (KEY_EQUAL) -> atset1:13 */ + "Escape": 0x1, /* html:Escape (Escape) -> linux:1 (KEY_ESC) -> atset1:1 */ + "F1": 0x3b, /* html:F1 (F1) -> linux:59 (KEY_F1) -> atset1:59 */ + "F10": 0x44, /* html:F10 (F10) -> linux:68 (KEY_F10) -> atset1:68 */ + "F11": 0x57, /* html:F11 (F11) -> linux:87 (KEY_F11) -> atset1:87 */ + "F12": 0x58, /* html:F12 (F12) -> linux:88 (KEY_F12) -> atset1:88 */ + "F13": 0x5d, /* html:F13 (F13) -> linux:183 (KEY_F13) -> atset1:93 */ + "F14": 0x5e, /* html:F14 (F14) -> linux:184 (KEY_F14) -> atset1:94 */ + "F15": 0x5f, /* html:F15 (F15) -> linux:185 (KEY_F15) -> atset1:95 */ + "F16": 0x55, /* html:F16 (F16) -> linux:186 (KEY_F16) -> atset1:85 */ + "F17": 0xe003, /* html:F17 (F17) -> linux:187 (KEY_F17) -> atset1:57347 */ + "F18": 0xe077, /* html:F18 (F18) -> linux:188 (KEY_F18) -> atset1:57463 */ + "F19": 0xe004, /* html:F19 (F19) -> linux:189 (KEY_F19) -> atset1:57348 */ + "F2": 0x3c, /* html:F2 (F2) -> linux:60 (KEY_F2) -> atset1:60 */ + "F20": 0x5a, /* html:F20 (F20) -> linux:190 (KEY_F20) -> atset1:90 */ + "F21": 0x74, /* html:F21 (F21) -> linux:191 (KEY_F21) -> atset1:116 */ + "F22": 0xe079, /* html:F22 (F22) -> linux:192 (KEY_F22) -> atset1:57465 */ + "F23": 0x6d, /* html:F23 (F23) -> linux:193 (KEY_F23) -> atset1:109 */ + "F24": 0x6f, /* html:F24 (F24) -> linux:194 (KEY_F24) -> atset1:111 */ + "F3": 0x3d, /* html:F3 (F3) -> linux:61 (KEY_F3) -> atset1:61 */ + "F4": 0x3e, /* html:F4 (F4) -> linux:62 (KEY_F4) -> atset1:62 */ + "F5": 0x3f, /* html:F5 (F5) -> linux:63 (KEY_F5) -> atset1:63 */ + "F6": 0x40, /* html:F6 (F6) -> linux:64 (KEY_F6) -> atset1:64 */ + "F7": 0x41, /* html:F7 (F7) -> linux:65 (KEY_F7) -> atset1:65 */ + "F8": 0x42, /* html:F8 (F8) -> linux:66 (KEY_F8) -> atset1:66 */ + "F9": 0x43, /* html:F9 (F9) -> linux:67 (KEY_F9) -> atset1:67 */ + "Find": 0xe041, /* html:Find (Find) -> linux:136 (KEY_FIND) -> atset1:57409 */ + "Help": 0xe075, /* html:Help (Help) -> linux:138 (KEY_HELP) -> atset1:57461 */ + "Hiragana": 0x77, /* html:Hiragana (Lang4) -> linux:91 (KEY_HIRAGANA) -> atset1:119 */ + "Home": 0xe047, /* html:Home (Home) -> linux:102 (KEY_HOME) -> atset1:57415 */ + "Insert": 0xe052, /* html:Insert (Insert) -> linux:110 (KEY_INSERT) -> atset1:57426 */ + "IntlBackslash": 0x56, /* html:IntlBackslash (IntlBackslash) -> linux:86 (KEY_102ND) -> atset1:86 */ + "IntlRo": 0x73, /* html:IntlRo (IntlRo) -> linux:89 (KEY_RO) -> atset1:115 */ + "IntlYen": 0x7d, /* html:IntlYen (IntlYen) -> linux:124 (KEY_YEN) -> atset1:125 */ + "KanaMode": 0x70, /* html:KanaMode (KanaMode) -> linux:93 (KEY_KATAKANAHIRAGANA) -> atset1:112 */ + "Katakana": 0x78, /* html:Katakana (Lang3) -> linux:90 (KEY_KATAKANA) -> atset1:120 */ + "KeyA": 0x1e, /* html:KeyA (KeyA) -> linux:30 (KEY_A) -> atset1:30 */ + "KeyB": 0x30, /* html:KeyB (KeyB) -> linux:48 (KEY_B) -> atset1:48 */ + "KeyC": 0x2e, /* html:KeyC (KeyC) -> linux:46 (KEY_C) -> atset1:46 */ + "KeyD": 0x20, /* html:KeyD (KeyD) -> linux:32 (KEY_D) -> atset1:32 */ + "KeyE": 0x12, /* html:KeyE (KeyE) -> linux:18 (KEY_E) -> atset1:18 */ + "KeyF": 0x21, /* html:KeyF (KeyF) -> linux:33 (KEY_F) -> atset1:33 */ + "KeyG": 0x22, /* html:KeyG (KeyG) -> linux:34 (KEY_G) -> atset1:34 */ + "KeyH": 0x23, /* html:KeyH (KeyH) -> linux:35 (KEY_H) -> atset1:35 */ + "KeyI": 0x17, /* html:KeyI (KeyI) -> linux:23 (KEY_I) -> atset1:23 */ + "KeyJ": 0x24, /* html:KeyJ (KeyJ) -> linux:36 (KEY_J) -> atset1:36 */ + "KeyK": 0x25, /* html:KeyK (KeyK) -> linux:37 (KEY_K) -> atset1:37 */ + "KeyL": 0x26, /* html:KeyL (KeyL) -> linux:38 (KEY_L) -> atset1:38 */ + "KeyM": 0x32, /* html:KeyM (KeyM) -> linux:50 (KEY_M) -> atset1:50 */ + "KeyN": 0x31, /* html:KeyN (KeyN) -> linux:49 (KEY_N) -> atset1:49 */ + "KeyO": 0x18, /* html:KeyO (KeyO) -> linux:24 (KEY_O) -> atset1:24 */ + "KeyP": 0x19, /* html:KeyP (KeyP) -> linux:25 (KEY_P) -> atset1:25 */ + "KeyQ": 0x10, /* html:KeyQ (KeyQ) -> linux:16 (KEY_Q) -> atset1:16 */ + "KeyR": 0x13, /* html:KeyR (KeyR) -> linux:19 (KEY_R) -> atset1:19 */ + "KeyS": 0x1f, /* html:KeyS (KeyS) -> linux:31 (KEY_S) -> atset1:31 */ + "KeyT": 0x14, /* html:KeyT (KeyT) -> linux:20 (KEY_T) -> atset1:20 */ + "KeyU": 0x16, /* html:KeyU (KeyU) -> linux:22 (KEY_U) -> atset1:22 */ + "KeyV": 0x2f, /* html:KeyV (KeyV) -> linux:47 (KEY_V) -> atset1:47 */ + "KeyW": 0x11, /* html:KeyW (KeyW) -> linux:17 (KEY_W) -> atset1:17 */ + "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 */ + "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 */ + "LaunchApp1": 0xe06b, /* html:LaunchApp1 (LaunchApp1) -> linux:157 (KEY_COMPUTER) -> atset1:57451 */ + "LaunchApp2": 0xe021, /* html:LaunchApp2 (LaunchApp2) -> linux:140 (KEY_CALC) -> atset1:57377 */ + "LaunchMail": 0xe06c, /* html:LaunchMail (LaunchMail) -> linux:155 (KEY_MAIL) -> atset1:57452 */ + "MediaPlayPause": 0xe022, /* html:MediaPlayPause (MediaPlayPause) -> linux:164 (KEY_PLAYPAUSE) -> atset1:57378 */ + "MediaSelect": 0xe06d, /* html:MediaSelect (MediaSelect) -> linux:226 (KEY_MEDIA) -> atset1:57453 */ + "MediaStop": 0xe024, /* html:MediaStop (MediaStop) -> linux:166 (KEY_STOPCD) -> atset1:57380 */ + "MediaTrackNext": 0xe019, /* html:MediaTrackNext (MediaTrackNext) -> linux:163 (KEY_NEXTSONG) -> atset1:57369 */ + "MediaTrackPrevious": 0xe010, /* html:MediaTrackPrevious (MediaTrackPrevious) -> linux:165 (KEY_PREVIOUSSONG) -> atset1:57360 */ + "MetaLeft": 0xe05b, /* html:MetaLeft (MetaLeft) -> linux:125 (KEY_LEFTMETA) -> atset1:57435 */ + "MetaRight": 0xe05c, /* html:MetaRight (MetaRight) -> linux:126 (KEY_RIGHTMETA) -> atset1:57436 */ + "Minus": 0xc, /* html:Minus (Minus) -> linux:12 (KEY_MINUS) -> atset1:12 */ + "NonConvert": 0x7b, /* html:NonConvert (NonConvert) -> linux:94 (KEY_MUHENKAN) -> atset1:123 */ + "NumLock": 0x45, /* html:NumLock (NumLock) -> linux:69 (KEY_NUMLOCK) -> atset1:69 */ + "Numpad0": 0x52, /* html:Numpad0 (Numpad0) -> linux:82 (KEY_KP0) -> atset1:82 */ + "Numpad1": 0x4f, /* html:Numpad1 (Numpad1) -> linux:79 (KEY_KP1) -> atset1:79 */ + "Numpad2": 0x50, /* html:Numpad2 (Numpad2) -> linux:80 (KEY_KP2) -> atset1:80 */ + "Numpad3": 0x51, /* html:Numpad3 (Numpad3) -> linux:81 (KEY_KP3) -> atset1:81 */ + "Numpad4": 0x4b, /* html:Numpad4 (Numpad4) -> linux:75 (KEY_KP4) -> atset1:75 */ + "Numpad5": 0x4c, /* html:Numpad5 (Numpad5) -> linux:76 (KEY_KP5) -> atset1:76 */ + "Numpad6": 0x4d, /* html:Numpad6 (Numpad6) -> linux:77 (KEY_KP6) -> atset1:77 */ + "Numpad7": 0x47, /* html:Numpad7 (Numpad7) -> linux:71 (KEY_KP7) -> atset1:71 */ + "Numpad8": 0x48, /* html:Numpad8 (Numpad8) -> linux:72 (KEY_KP8) -> atset1:72 */ + "Numpad9": 0x49, /* html:Numpad9 (Numpad9) -> linux:73 (KEY_KP9) -> atset1:73 */ + "NumpadAdd": 0x4e, /* html:NumpadAdd (NumpadAdd) -> linux:78 (KEY_KPPLUS) -> atset1:78 */ + "NumpadComma": 0x7e, /* html:NumpadComma (NumpadComma) -> linux:121 (KEY_KPCOMMA) -> atset1:126 */ + "NumpadDecimal": 0x53, /* html:NumpadDecimal (NumpadDecimal) -> linux:83 (KEY_KPDOT) -> atset1:83 */ + "NumpadDivide": 0xe035, /* html:NumpadDivide (NumpadDivide) -> linux:98 (KEY_KPSLASH) -> atset1:57397 */ + "NumpadEnter": 0xe01c, /* html:NumpadEnter (NumpadEnter) -> linux:96 (KEY_KPENTER) -> atset1:57372 */ + "NumpadEqual": 0x59, /* html:NumpadEqual (NumpadEqual) -> linux:117 (KEY_KPEQUAL) -> atset1:89 */ + "NumpadMultiply": 0x37, /* html:NumpadMultiply (NumpadMultiply) -> linux:55 (KEY_KPASTERISK) -> atset1:55 */ + "NumpadParenLeft": 0xe076, /* html:NumpadParenLeft (NumpadParenLeft) -> linux:179 (KEY_KPLEFTPAREN) -> atset1:57462 */ + "NumpadParenRight": 0xe07b, /* html:NumpadParenRight (NumpadParenRight) -> linux:180 (KEY_KPRIGHTPAREN) -> atset1:57467 */ + "NumpadSubtract": 0x4a, /* html:NumpadSubtract (NumpadSubtract) -> linux:74 (KEY_KPMINUS) -> atset1:74 */ + "Open": 0x64, /* html:Open (Open) -> linux:134 (KEY_OPEN) -> atset1:100 */ + "PageDown": 0xe051, /* html:PageDown (PageDown) -> linux:109 (KEY_PAGEDOWN) -> atset1:57425 */ + "PageUp": 0xe049, /* html:PageUp (PageUp) -> linux:104 (KEY_PAGEUP) -> atset1:57417 */ + "Paste": 0x65, /* html:Paste (Paste) -> linux:135 (KEY_PASTE) -> atset1:101 */ + "Pause": 0xe046, /* html:Pause (Pause) -> linux:119 (KEY_PAUSE) -> atset1:57414 */ + "Period": 0x34, /* html:Period (Period) -> linux:52 (KEY_DOT) -> atset1:52 */ + "Power": 0xe05e, /* html:Power (Power) -> linux:116 (KEY_POWER) -> atset1:57438 */ + "PrintScreen": 0x54, /* html:PrintScreen (PrintScreen) -> linux:99 (KEY_SYSRQ) -> atset1:84 */ + "Props": 0xe006, /* html:Props (Props) -> linux:130 (KEY_PROPS) -> atset1:57350 */ + "Quote": 0x28, /* html:Quote (Quote) -> linux:40 (KEY_APOSTROPHE) -> atset1:40 */ + "ScrollLock": 0x46, /* html:ScrollLock (ScrollLock) -> linux:70 (KEY_SCROLLLOCK) -> atset1:70 */ + "Semicolon": 0x27, /* html:Semicolon (Semicolon) -> linux:39 (KEY_SEMICOLON) -> atset1:39 */ + "ShiftLeft": 0x2a, /* html:ShiftLeft (ShiftLeft) -> linux:42 (KEY_LEFTSHIFT) -> atset1:42 */ + "ShiftRight": 0x36, /* html:ShiftRight (ShiftRight) -> linux:54 (KEY_RIGHTSHIFT) -> atset1:54 */ + "Slash": 0x35, /* html:Slash (Slash) -> linux:53 (KEY_SLASH) -> atset1:53 */ + "Sleep": 0xe05f, /* html:Sleep (Sleep) -> linux:142 (KEY_SLEEP) -> atset1:57439 */ + "Space": 0x39, /* html:Space (Space) -> linux:57 (KEY_SPACE) -> atset1:57 */ + "Suspend": 0xe025, /* html:Suspend (Suspend) -> linux:205 (KEY_SUSPEND) -> atset1:57381 */ + "Tab": 0xf, /* html:Tab (Tab) -> linux:15 (KEY_TAB) -> atset1:15 */ + "Undo": 0xe007, /* html:Undo (Undo) -> linux:131 (KEY_UNDO) -> atset1:57351 */ + "WakeUp": 0xe063, /* html:WakeUp (WakeUp) -> linux:143 (KEY_WAKEUP) -> atset1:57443 */ +}; diff --git a/systemvm/debian/root/noVNC/core/keymap.js b/systemvm/debian/root/noVNC/core/keymap.js new file mode 100644 index 000000000000..b6391958d8c6 --- /dev/null +++ b/systemvm/debian/root/noVNC/core/keymap.js @@ -0,0 +1,96 @@ +var shifted = {}; +shifted[' ']=0; +shifted['!']=1; +shifted['"']=1; +shifted['#']=1; +shifted['$']=1; +shifted['%']=1; +shifted['&']=1; +shifted['\'']=0; +shifted['(']=1; +shifted[')']=1; +shifted['*']=1; +shifted['+']=1; +shifted[',']=0; +shifted['-']=0; +shifted['.']=0; +shifted['/']=0; +shifted['0']=0; +shifted['1']=0; +shifted['2']=0; +shifted['3']=0; +shifted['4']=0; +shifted['5']=0; +shifted['6']=0; +shifted['7']=0; +shifted['8']=0; +shifted['9']=0; +shifted[':']=1; +shifted[';']=0; +shifted['<']=1; +shifted['=']=0; +shifted['>']=1; +shifted['?']=1; +shifted['@']=1; +shifted['A']=1; +shifted['B']=1; +shifted['C']=1; +shifted['D']=1; +shifted['E']=1; +shifted['F']=1; +shifted['G']=1; +shifted['H']=1; +shifted['I']=1; +shifted['J']=1; +shifted['K']=1; +shifted['L']=1; +shifted['M']=1; +shifted['N']=1; +shifted['O']=1; +shifted['P']=1; +shifted['Q']=1; +shifted['R']=1; +shifted['S']=1; +shifted['T']=1; +shifted['U']=1; +shifted['V']=1; +shifted['W']=1; +shifted['X']=1; +shifted['Y']=1; +shifted['Z']=1; +shifted['[']=0; +shifted['\\']=0; +shifted[']']=0; +shifted['^']=1; +shifted['_']=1; +shifted['`']=0; +shifted['a']=0; +shifted['b']=0; +shifted['c']=0; +shifted['d']=0; +shifted['e']=0; +shifted['f']=0; +shifted['g']=0; +shifted['h']=0; +shifted['i']=0; +shifted['j']=0; +shifted['k']=0; +shifted['l']=0; +shifted['m']=0; +shifted['n']=0; +shifted['o']=0; +shifted['p']=0; +shifted['q']=0; +shifted['r']=0; +shifted['s']=0; +shifted['t']=0; +shifted['u']=0; +shifted['v']=0; +shifted['w']=0; +shifted['x']=0; +shifted['y']=0; +shifted['z']=0; +shifted['{']=1; +shifted['|']=1; +shifted['}']=1; +shifted['~']=1; diff --git a/systemvm/debian/root/noVNC/core/rfb.js b/systemvm/debian/root/noVNC/core/rfb.js new file mode 100644 index 000000000000..b619fad34317 --- /dev/null +++ b/systemvm/debian/root/noVNC/core/rfb.js @@ -0,0 +1,2078 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +import * as Log from './util/logging.js'; +import { decodeUTF8 } from './util/strings.js'; +import { dragThreshold } from './util/browser.js'; +import EventTargetMixin from './util/eventtarget.js'; +import Display from "./display.js"; +import Keyboard from "./input/keyboard.js"; +import Mouse from "./input/mouse.js"; +import Cursor from "./util/cursor.js"; +import Websock from "./websock.js"; +import DES from "./des.js"; +import KeyTable from "./input/keysym.js"; +import XtScancode from "./input/xtscancodes.js"; +import { encodings } from "./encodings.js"; +import "./util/polyfill.js"; +import "./keymap.js" + +import RawDecoder from "./decoders/raw.js"; +import CopyRectDecoder from "./decoders/copyrect.js"; +import RREDecoder from "./decoders/rre.js"; +import HextileDecoder from "./decoders/hextile.js"; +import TightDecoder from "./decoders/tight.js"; +import TightPNGDecoder from "./decoders/tightpng.js"; + +// How many seconds to wait for a disconnect to finish +const DISCONNECT_TIMEOUT = 3; +const DEFAULT_BACKGROUND = 'rgb(40, 40, 40)'; + +export default class RFB extends EventTargetMixin { + constructor(target, url, options) { + if (!target) { + throw new Error("Must specify target"); + } + if (!url) { + throw new Error("Must specify URL"); + } + + super(); + + this._target = target; + this._url = url; + + // Connection details + options = options || {}; + this._rfb_credentials = options.credentials || {}; + this._shared = 'shared' in options ? !!options.shared : true; + this._repeaterID = options.repeaterID || ''; + this._showDotCursor = options.showDotCursor || false; + + // Internal state + this._rfb_connection_state = ''; + this._rfb_init_state = ''; + this._rfb_auth_scheme = -1; + this._rfb_clean_disconnect = true; + + // Server capabilities + this._rfb_version = 0; + this._rfb_max_version = 3.8; + this._rfb_tightvnc = false; + this._rfb_xvp_ver = 0; + + this._fb_width = 0; + this._fb_height = 0; + + this._fb_name = ""; + + this._capabilities = { power: false }; + + this._supportsFence = false; + + this._supportsContinuousUpdates = false; + this._enabledContinuousUpdates = false; + + this._supportsSetDesktopSize = false; + this._screen_id = 0; + this._screen_flags = 0; + + this._qemuExtKeyEventSupported = false; + + // Internal objects + this._sock = null; // Websock object + this._display = null; // Display object + this._flushing = false; // Display flushing state + this._keyboard = null; // Keyboard input handler object + this._mouse = null; // Mouse input handler object + + // Timers + this._disconnTimer = null; // disconnection timer + this._resizeTimeout = null; // resize rate limiting + + // Decoder states + this._decoders = {}; + + this._FBU = { + rects: 0, + x: 0, + y: 0, + width: 0, + height: 0, + encoding: null, + }; + + // Mouse state + this._mouse_buttonMask = 0; + this._mouse_arr = []; + this._viewportDragging = false; + this._viewportDragPos = {}; + this._viewportHasMoved = false; + + // Bound event handlers + this._eventHandlers = { + focusCanvas: this._focusCanvas.bind(this), + windowResize: this._windowResize.bind(this), + }; + + // main setup + Log.Debug(">> RFB.constructor"); + + // Create DOM elements + this._screen = document.createElement('div'); + this._screen.style.display = 'flex'; + this._screen.style.width = '100%'; + this._screen.style.height = '100%'; + this._screen.style.overflow = 'auto'; + this._screen.style.background = DEFAULT_BACKGROUND; + this._canvas = document.createElement('canvas'); + this._canvas.style.margin = 'auto'; + // Some browsers add an outline on focus + this._canvas.style.outline = 'none'; + // IE miscalculates width without this :( + this._canvas.style.flexShrink = '0'; + this._canvas.width = 0; + this._canvas.height = 0; + this._canvas.tabIndex = -1; + this._screen.appendChild(this._canvas); + + // Cursor + this._cursor = new Cursor(); + + // XXX: TightVNC 2.8.11 sends no cursor at all until Windows changes + // it. Result: no cursor at all until a window border or an edit field + // is hit blindly. But there are also VNC servers that draw the cursor + // in the framebuffer and don't send the empty local cursor. There is + // no way to satisfy both sides. + // + // The spec is unclear on this "initial cursor" issue. Many other + // viewers (TigerVNC, RealVNC, Remmina) display an arrow as the + // initial cursor instead. + this._cursorImage = RFB.cursors.none; + + // populate decoder array with objects + this._decoders[encodings.encodingRaw] = new RawDecoder(); + this._decoders[encodings.encodingCopyRect] = new CopyRectDecoder(); + this._decoders[encodings.encodingRRE] = new RREDecoder(); + this._decoders[encodings.encodingHextile] = new HextileDecoder(); + this._decoders[encodings.encodingTight] = new TightDecoder(); + this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder(); + + // NB: nothing that needs explicit teardown should be done + // before this point, since this can throw an exception + try { + this._display = new Display(this._canvas); + } catch (exc) { + Log.Error("Display exception: " + exc); + throw exc; + } + this._display.onflush = this._onFlush.bind(this); + this._display.clear(); + + this._keyboard = new Keyboard(this._canvas); + this._keyboard.onkeyevent = this._handleKeyEvent.bind(this); + + this._mouse = new Mouse(this._canvas); + this._mouse.onmousebutton = this._handleMouseButton.bind(this); + this._mouse.onmousemove = this._handleMouseMove.bind(this); + + this._sock = new Websock(); + this._sock.on('message', () => { + this._handle_message(); + }); + this._sock.on('open', () => { + if ((this._rfb_connection_state === 'connecting') && + (this._rfb_init_state === '')) { + this._rfb_init_state = 'ProtocolVersion'; + Log.Debug("Starting VNC handshake"); + } else { + this._fail("Unexpected server connection while " + + this._rfb_connection_state); + } + }); + this._sock.on('close', (e) => { + Log.Debug("WebSocket on-close event"); + let msg = ""; + if (e.code) { + msg = "(code: " + e.code; + if (e.reason) { + msg += ", reason: " + e.reason; + } + msg += ")"; + } + switch (this._rfb_connection_state) { + case 'connecting': + this._fail("Connection closed " + msg); + break; + case 'connected': + // Handle disconnects that were initiated server-side + this._updateConnectionState('disconnecting'); + this._updateConnectionState('disconnected'); + break; + case 'disconnecting': + // Normal disconnection path + this._updateConnectionState('disconnected'); + break; + case 'disconnected': + this._fail("Unexpected server disconnect " + + "when already disconnected " + msg); + break; + default: + this._fail("Unexpected server disconnect before connecting " + + msg); + break; + } + this._sock.off('close'); + }); + this._sock.on('error', e => Log.Warn("WebSocket on-error event")); + + // Slight delay of the actual connection so that the caller has + // time to set up callbacks + setTimeout(this._updateConnectionState.bind(this, 'connecting')); + + Log.Debug("<< RFB.constructor"); + + // ===== PROPERTIES ===== + + this.dragViewport = false; + this.focusOnClick = true; + + this._viewOnly = false; + this._clipViewport = false; + this._scaleViewport = false; + this._resizeSession = false; + } + + // ===== PROPERTIES ===== + + get viewOnly() { return this._viewOnly; } + set viewOnly(viewOnly) { + this._viewOnly = viewOnly; + + if (this._rfb_connection_state === "connecting" || + this._rfb_connection_state === "connected") { + if (viewOnly) { + this._keyboard.ungrab(); + this._mouse.ungrab(); + } else { + this._keyboard.grab(); + this._mouse.grab(); + } + } + } + + get capabilities() { return this._capabilities; } + + get touchButton() { return this._mouse.touchButton; } + set touchButton(button) { this._mouse.touchButton = button; } + + get clipViewport() { return this._clipViewport; } + set clipViewport(viewport) { + this._clipViewport = viewport; + this._updateClip(); + } + + get scaleViewport() { return this._scaleViewport; } + set scaleViewport(scale) { + this._scaleViewport = scale; + // Scaling trumps clipping, so we may need to adjust + // clipping when enabling or disabling scaling + if (scale && this._clipViewport) { + this._updateClip(); + } + this._updateScale(); + if (!scale && this._clipViewport) { + this._updateClip(); + } + } + + get resizeSession() { return this._resizeSession; } + set resizeSession(resize) { + this._resizeSession = resize; + if (resize) { + this._requestRemoteResize(); + } + } + + get showDotCursor() { return this._showDotCursor; } + set showDotCursor(show) { + this._showDotCursor = show; + this._refreshCursor(); + } + + get background() { return this._screen.style.background; } + set background(cssValue) { this._screen.style.background = cssValue; } + + // ===== PUBLIC METHODS ===== + + disconnect() { + this._updateConnectionState('disconnecting'); + this._sock.off('error'); + this._sock.off('message'); + this._sock.off('open'); + } + + sendCredentials(creds) { + this._rfb_credentials = creds; + setTimeout(this._init_msg.bind(this), 0); + } + + sendString(str) { + if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; } + var arr = []; + for (var i = 0; i < str.length; i++ ) { + var char = str.substring(i, i + 1); + var code = char.charCodeAt(0); + if (shifted[char]) RFB.messages.keyEvent(this._sock, KeyTable.XK_Shift_L, 1); // Shift + RFB.messages.keyEvent(this._sock, code, 1); // (key) + RFB.messages.keyEvent(this._sock, code, 0); // (key) + if (shifted[char]) RFB.messages.keyEvent(this._sock, KeyTable.XK_Shift_L, 0); // Shift + } + + this._sock.flush(); + } + + sendCtrlAltDel() { + if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; } + Log.Info("Sending Ctrl-Alt-Del"); + + this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true); + this.sendKey(KeyTable.XK_Alt_L, "AltLeft", true); + this.sendKey(KeyTable.XK_Delete, "Delete", true); + this.sendKey(KeyTable.XK_Delete, "Delete", false); + this.sendKey(KeyTable.XK_Alt_L, "AltLeft", false); + this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false); + } + + machineShutdown() { + this._xvpOp(1, 2); + } + + machineReboot() { + this._xvpOp(1, 3); + } + + machineReset() { + this._xvpOp(1, 4); + } + + // Send a key press. If 'down' is not specified then send a down key + // followed by an up key. + sendKey(keysym, code, down) { + if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; } + + if (down === undefined) { + this.sendKey(keysym, code, true); + this.sendKey(keysym, code, false); + return; + } + + const scancode = XtScancode[code]; + + if (this._qemuExtKeyEventSupported && scancode) { + // 0 is NoSymbol + keysym = keysym || 0; + + Log.Info("Sending key (" + (down ? "down" : "up") + "): keysym " + keysym + ", scancode " + scancode); + + RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode); + } else { + if (!keysym) { + return; + } + Log.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym); + RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0); + } + } + + focus() { + this._canvas.focus(); + } + + blur() { + this._canvas.blur(); + } + + clipboardPasteFrom(text) { + if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; } + RFB.messages.clientCutText(this._sock, text); + } + + // ===== PRIVATE METHODS ===== + + _connect() { + Log.Debug(">> RFB.connect"); + + Log.Info("connecting to " + this._url); + + try { + // WebSocket.onopen transitions to the RFB init states + this._sock.open(this._url, ['binary']); + } catch (e) { + if (e.name === 'SyntaxError') { + this._fail("Invalid host or port (" + e + ")"); + } else { + this._fail("Error when opening socket (" + e + ")"); + } + } + + // Make our elements part of the page + this._target.appendChild(this._screen); + + this._cursor.attach(this._canvas); + this._refreshCursor(); + + // Monitor size changes of the screen + // FIXME: Use ResizeObserver, or hidden overflow + window.addEventListener('resize', this._eventHandlers.windowResize); + + // Always grab focus on some kind of click event + this._canvas.addEventListener("mousedown", this._eventHandlers.focusCanvas); + this._canvas.addEventListener("touchstart", this._eventHandlers.focusCanvas); + + Log.Debug("<< RFB.connect"); + } + + _disconnect() { + Log.Debug(">> RFB.disconnect"); + this._cursor.detach(); + this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas); + this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas); + window.removeEventListener('resize', this._eventHandlers.windowResize); + this._keyboard.ungrab(); + this._mouse.ungrab(); + this._sock.close(); + try { + this._target.removeChild(this._screen); + } catch (e) { + if (e.name === 'NotFoundError') { + // Some cases where the initial connection fails + // can disconnect before the _screen is created + } else { + throw e; + } + } + clearTimeout(this._resizeTimeout); + Log.Debug("<< RFB.disconnect"); + } + + _focusCanvas(event) { + // Respect earlier handlers' request to not do side-effects + if (event.defaultPrevented) { + return; + } + + if (!this.focusOnClick) { + return; + } + + this.focus(); + } + + _windowResize(event) { + // If the window resized then our screen element might have + // as well. Update the viewport dimensions. + window.requestAnimationFrame(() => { + this._updateClip(); + this._updateScale(); + }); + + if (this._resizeSession) { + // Request changing the resolution of the remote display to + // the size of the local browser viewport. + + // In order to not send multiple requests before the browser-resize + // is finished we wait 0.5 seconds before sending the request. + clearTimeout(this._resizeTimeout); + this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this), 500); + } + } + + // Update state of clipping in Display object, and make sure the + // configured viewport matches the current screen size + _updateClip() { + const cur_clip = this._display.clipViewport; + let new_clip = this._clipViewport; + + if (this._scaleViewport) { + // Disable viewport clipping if we are scaling + new_clip = false; + } + + if (cur_clip !== new_clip) { + this._display.clipViewport = new_clip; + } + + if (new_clip) { + // When clipping is enabled, the screen is limited to + // the size of the container. + const size = this._screenSize(); + this._display.viewportChangeSize(size.w, size.h); + this._fixScrollbars(); + } + } + + _updateScale() { + if (!this._scaleViewport) { + this._display.scale = 1.0; + } else { + const size = this._screenSize(); + this._display.autoscale(size.w, size.h); + } + this._fixScrollbars(); + } + + // Requests a change of remote desktop size. This message is an extension + // and may only be sent if we have received an ExtendedDesktopSize message + _requestRemoteResize() { + clearTimeout(this._resizeTimeout); + this._resizeTimeout = null; + + if (!this._resizeSession || this._viewOnly || + !this._supportsSetDesktopSize) { + return; + } + + const size = this._screenSize(); + RFB.messages.setDesktopSize(this._sock, + Math.floor(size.w), Math.floor(size.h), + this._screen_id, this._screen_flags); + + Log.Debug('Requested new desktop size: ' + + size.w + 'x' + size.h); + } + + // Gets the the size of the available screen + _screenSize() { + let r = this._screen.getBoundingClientRect(); + return { w: r.width, h: r.height }; + } + + _fixScrollbars() { + // This is a hack because Chrome screws up the calculation + // for when scrollbars are needed. So to fix it we temporarily + // toggle them off and on. + const orig = this._screen.style.overflow; + this._screen.style.overflow = 'hidden'; + // Force Chrome to recalculate the layout by asking for + // an element's dimensions + this._screen.getBoundingClientRect(); + this._screen.style.overflow = orig; + } + + /* + * Connection states: + * connecting + * connected + * disconnecting + * disconnected - permanent state + */ + _updateConnectionState(state) { + const oldstate = this._rfb_connection_state; + + if (state === oldstate) { + Log.Debug("Already in state '" + state + "', ignoring"); + return; + } + + // The 'disconnected' state is permanent for each RFB object + if (oldstate === 'disconnected') { + Log.Error("Tried changing state of a disconnected RFB object"); + return; + } + + // Ensure proper transitions before doing anything + switch (state) { + case 'connected': + if (oldstate !== 'connecting') { + Log.Error("Bad transition to connected state, " + + "previous connection state: " + oldstate); + return; + } + break; + + case 'disconnected': + if (oldstate !== 'disconnecting') { + Log.Error("Bad transition to disconnected state, " + + "previous connection state: " + oldstate); + return; + } + break; + + case 'connecting': + if (oldstate !== '') { + Log.Error("Bad transition to connecting state, " + + "previous connection state: " + oldstate); + return; + } + break; + + case 'disconnecting': + if (oldstate !== 'connected' && oldstate !== 'connecting') { + Log.Error("Bad transition to disconnecting state, " + + "previous connection state: " + oldstate); + return; + } + break; + + default: + Log.Error("Unknown connection state: " + state); + return; + } + + // State change actions + + this._rfb_connection_state = state; + + Log.Debug("New state '" + state + "', was '" + oldstate + "'."); + + if (this._disconnTimer && state !== 'disconnecting') { + Log.Debug("Clearing disconnect timer"); + clearTimeout(this._disconnTimer); + this._disconnTimer = null; + + // make sure we don't get a double event + this._sock.off('close'); + } + + switch (state) { + case 'connecting': + this._connect(); + break; + + case 'connected': + this.dispatchEvent(new CustomEvent("connect", { detail: {} })); + break; + + case 'disconnecting': + this._disconnect(); + + this._disconnTimer = setTimeout(() => { + Log.Error("Disconnection timed out."); + this._updateConnectionState('disconnected'); + }, DISCONNECT_TIMEOUT * 1000); + break; + + case 'disconnected': + this.dispatchEvent(new CustomEvent( + "disconnect", { detail: + { clean: this._rfb_clean_disconnect } })); + break; + } + } + + /* Print errors and disconnect + * + * The parameter 'details' is used for information that + * should be logged but not sent to the user interface. + */ + _fail(details) { + switch (this._rfb_connection_state) { + case 'disconnecting': + Log.Error("Failed when disconnecting: " + details); + break; + case 'connected': + Log.Error("Failed while connected: " + details); + break; + case 'connecting': + Log.Error("Failed when connecting: " + details); + break; + default: + Log.Error("RFB failure: " + details); + break; + } + this._rfb_clean_disconnect = false; //This is sent to the UI + + // Transition to disconnected without waiting for socket to close + this._updateConnectionState('disconnecting'); + this._updateConnectionState('disconnected'); + + return false; + } + + _setCapability(cap, val) { + this._capabilities[cap] = val; + this.dispatchEvent(new CustomEvent("capabilities", + { detail: { capabilities: this._capabilities } })); + } + + _handle_message() { + if (this._sock.rQlen === 0) { + Log.Warn("handle_message called on an empty receive queue"); + return; + } + + switch (this._rfb_connection_state) { + case 'disconnected': + Log.Error("Got data while disconnected"); + break; + case 'connected': + while (true) { + if (this._flushing) { + break; + } + if (!this._normal_msg()) { + break; + } + if (this._sock.rQlen === 0) { + break; + } + } + break; + default: + this._init_msg(); + break; + } + } + + _handleKeyEvent(keysym, code, down) { + this.sendKey(keysym, code, down); + } + + _handleMouseButton(x, y, down, bmask) { + if (down) { + this._mouse_buttonMask |= bmask; + } else { + this._mouse_buttonMask &= ~bmask; + } + + if (this.dragViewport) { + if (down && !this._viewportDragging) { + this._viewportDragging = true; + this._viewportDragPos = {'x': x, 'y': y}; + this._viewportHasMoved = false; + + // Skip sending mouse events + return; + } else { + this._viewportDragging = false; + + // If we actually performed a drag then we are done + // here and should not send any mouse events + if (this._viewportHasMoved) { + return; + } + + // Otherwise we treat this as a mouse click event. + // Send the button down event here, as the button up + // event is sent at the end of this function. + RFB.messages.pointerEvent(this._sock, + this._display.absX(x), + this._display.absY(y), + bmask); + } + } + + if (this._viewOnly) { return; } // View only, skip mouse events + + if (this._rfb_connection_state !== 'connected') { return; } + RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); + } + + _handleMouseMove(x, y) { + if (this._viewportDragging) { + const deltaX = this._viewportDragPos.x - x; + const deltaY = this._viewportDragPos.y - y; + + if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold || + Math.abs(deltaY) > dragThreshold)) { + this._viewportHasMoved = true; + + this._viewportDragPos = {'x': x, 'y': y}; + this._display.viewportChangePos(deltaX, deltaY); + } + + // Skip sending mouse events + return; + } + + if (this._viewOnly) { return; } // View only, skip mouse events + + if (this._rfb_connection_state !== 'connected') { return; } + RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); + } + + // Message Handlers + + _negotiate_protocol_version() { + if (this._sock.rQwait("version", 12)) { + return false; + } + + const header = String.fromCharCode.apply(null, this._sock.rQslice(0)).substr(0, 3); + if (header !== 'RFB') { + this._rfb_init_state = 'ServerInitialisation'; + Log.Debug("Starting Server Initialisation"); + return this._negotiate_server_init(); + } + + const sversion = this._sock.rQshiftStr(12).substr(4, 7); + Log.Info("Server ProtocolVersion: " + sversion); + let is_repeater = 0; + switch (sversion) { + case "000.000": // UltraVNC repeater + is_repeater = 1; + break; + case "003.003": + case "003.006": // UltraVNC + case "003.889": // Apple Remote Desktop + this._rfb_version = 3.3; + break; + case "003.007": + this._rfb_version = 3.7; + break; + case "003.008": + case "004.000": // Intel AMT KVM + case "004.001": // RealVNC 4.6 + case "005.000": // RealVNC 5.3 + this._rfb_version = 3.8; + break; + default: + return this._fail("Invalid server version " + sversion); + } + + if (is_repeater) { + let repeaterID = "ID:" + this._repeaterID; + while (repeaterID.length < 250) { + repeaterID += "\0"; + } + this._sock.send_string(repeaterID); + return true; + } + + if (this._rfb_version > this._rfb_max_version) { + this._rfb_version = this._rfb_max_version; + } + + const cversion = "00" + parseInt(this._rfb_version, 10) + + ".00" + ((this._rfb_version * 10) % 10); + this._sock.send_string("RFB " + cversion + "\n"); + Log.Debug('Sent ProtocolVersion: ' + cversion); + + this._rfb_init_state = 'Security'; + } + + _negotiate_security() { + // Polyfill since IE and PhantomJS doesn't have + // TypedArray.includes() + function includes(item, array) { + for (let i = 0; i < array.length; i++) { + if (array[i] === item) { + return true; + } + } + return false; + } + + if (this._rfb_version >= 3.7) { + // Server sends supported list, client decides + const num_types = this._sock.rQshift8(); + if (this._sock.rQwait("security type", num_types, 1)) { return false; } + + if (num_types === 0) { + this._rfb_init_state = "SecurityReason"; + this._security_context = "no security types"; + this._security_status = 1; + return this._init_msg(); + } + + const types = this._sock.rQshiftBytes(num_types); + Log.Debug("Server security types: " + types); + + // Look for each auth in preferred order + if (includes(1, types)) { + this._rfb_auth_scheme = 1; // None + } else if (includes(22, types)) { + this._rfb_auth_scheme = 22; // XVP + } else if (includes(16, types)) { + this._rfb_auth_scheme = 16; // Tight + } else if (includes(2, types)) { + this._rfb_auth_scheme = 2; // VNC Auth + } else { + return this._fail("Unsupported security types (types: " + types + ")"); + } + + this._sock.send([this._rfb_auth_scheme]); + } else { + // Server decides + if (this._sock.rQwait("security scheme", 4)) { return false; } + this._rfb_auth_scheme = this._sock.rQshift32(); + + if (this._rfb_auth_scheme == 0) { + this._rfb_init_state = "SecurityReason"; + this._security_context = "authentication scheme"; + this._security_status = 1; + return this._init_msg(); + } + } + + this._rfb_init_state = 'Authentication'; + Log.Debug('Authenticating using scheme: ' + this._rfb_auth_scheme); + + return this._init_msg(); // jump to authentication + } + + _handle_security_reason() { + if (this._sock.rQwait("reason length", 4)) { + return false; + } + const strlen = this._sock.rQshift32(); + let reason = ""; + + if (strlen > 0) { + if (this._sock.rQwait("reason", strlen, 4)) { return false; } + reason = this._sock.rQshiftStr(strlen); + } + + if (reason !== "") { + this.dispatchEvent(new CustomEvent( + "securityfailure", + { detail: { status: this._security_status, + reason: reason } })); + + return this._fail("Security negotiation failed on " + + this._security_context + + " (reason: " + reason + ")"); + } else { + this.dispatchEvent(new CustomEvent( + "securityfailure", + { detail: { status: this._security_status } })); + + return this._fail("Security negotiation failed on " + + this._security_context); + } + } + + // authentication + _negotiate_xvp_auth() { + if (!this._rfb_credentials.username || + !this._rfb_credentials.password || + !this._rfb_credentials.target) { + this.dispatchEvent(new CustomEvent( + "credentialsrequired", + { detail: { types: ["username", "password", "target"] } })); + return false; + } + + const xvp_auth_str = String.fromCharCode(this._rfb_credentials.username.length) + + String.fromCharCode(this._rfb_credentials.target.length) + + this._rfb_credentials.username + + this._rfb_credentials.target; + this._sock.send_string(xvp_auth_str); + this._rfb_auth_scheme = 2; + return this._negotiate_authentication(); + } + + _negotiate_std_vnc_auth() { + if (this._sock.rQwait("auth challenge", 16)) { return false; } + + if (!this._rfb_credentials.password) { + this.dispatchEvent(new CustomEvent( + "credentialsrequired", + { detail: { types: ["password"] } })); + return false; + } + + // TODO(directxman12): make genDES not require an Array + const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16)); + const response = RFB.genDES(this._rfb_credentials.password, challenge); + this._sock.send(response); + this._rfb_init_state = "SecurityResult"; + return true; + } + + _negotiate_tight_tunnels(numTunnels) { + const clientSupportedTunnelTypes = { + 0: { vendor: 'TGHT', signature: 'NOTUNNEL' } + }; + const serverSupportedTunnelTypes = {}; + // receive tunnel capabilities + for (let i = 0; i < numTunnels; i++) { + const cap_code = this._sock.rQshift32(); + const cap_vendor = this._sock.rQshiftStr(4); + const cap_signature = this._sock.rQshiftStr(8); + serverSupportedTunnelTypes[cap_code] = { vendor: cap_vendor, signature: cap_signature }; + } + + Log.Debug("Server Tight tunnel types: " + serverSupportedTunnelTypes); + + // Siemens touch panels have a VNC server that supports NOTUNNEL, + // but forgets to advertise it. Try to detect such servers by + // looking for their custom tunnel type. + if (serverSupportedTunnelTypes[1] && + (serverSupportedTunnelTypes[1].vendor === "SICR") && + (serverSupportedTunnelTypes[1].signature === "SCHANNEL")) { + Log.Debug("Detected Siemens server. Assuming NOTUNNEL support."); + serverSupportedTunnelTypes[0] = { vendor: 'TGHT', signature: 'NOTUNNEL' }; + } + + // choose the notunnel type + if (serverSupportedTunnelTypes[0]) { + if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor || + serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) { + return this._fail("Client's tunnel type had the incorrect " + + "vendor or signature"); + } + Log.Debug("Selected tunnel type: " + clientSupportedTunnelTypes[0]); + this._sock.send([0, 0, 0, 0]); // use NOTUNNEL + return false; // wait until we receive the sub auth count to continue + } else { + return this._fail("Server wanted tunnels, but doesn't support " + + "the notunnel type"); + } + } + + _negotiate_tight_auth() { + if (!this._rfb_tightvnc) { // first pass, do the tunnel negotiation + if (this._sock.rQwait("num tunnels", 4)) { return false; } + const numTunnels = this._sock.rQshift32(); + if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; } + + this._rfb_tightvnc = true; + + if (numTunnels > 0) { + this._negotiate_tight_tunnels(numTunnels); + return false; // wait until we receive the sub auth to continue + } + } + + // second pass, do the sub-auth negotiation + if (this._sock.rQwait("sub auth count", 4)) { return false; } + const subAuthCount = this._sock.rQshift32(); + if (subAuthCount === 0) { // empty sub-auth list received means 'no auth' subtype selected + this._rfb_init_state = 'SecurityResult'; + return true; + } + + if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; } + + const clientSupportedTypes = { + 'STDVNOAUTH__': 1, + 'STDVVNCAUTH_': 2 + }; + + const serverSupportedTypes = []; + + for (let i = 0; i < subAuthCount; i++) { + this._sock.rQshift32(); // capNum + const capabilities = this._sock.rQshiftStr(12); + serverSupportedTypes.push(capabilities); + } + + Log.Debug("Server Tight authentication types: " + serverSupportedTypes); + + for (let authType in clientSupportedTypes) { + if (serverSupportedTypes.indexOf(authType) != -1) { + this._sock.send([0, 0, 0, clientSupportedTypes[authType]]); + Log.Debug("Selected authentication type: " + authType); + + switch (authType) { + case 'STDVNOAUTH__': // no auth + this._rfb_init_state = 'SecurityResult'; + return true; + case 'STDVVNCAUTH_': // VNC auth + this._rfb_auth_scheme = 2; + return this._init_msg(); + default: + return this._fail("Unsupported tiny auth scheme " + + "(scheme: " + authType + ")"); + } + } + } + + return this._fail("No supported sub-auth types!"); + } + + _negotiate_authentication() { + switch (this._rfb_auth_scheme) { + case 1: // no auth + if (this._rfb_version >= 3.8) { + this._rfb_init_state = 'SecurityResult'; + return true; + } + this._rfb_init_state = 'ClientInitialisation'; + return this._init_msg(); + + case 22: // XVP auth + return this._negotiate_xvp_auth(); + + case 2: // VNC authentication + return this._negotiate_std_vnc_auth(); + + case 16: // TightVNC Security Type + return this._negotiate_tight_auth(); + + default: + return this._fail("Unsupported auth scheme (scheme: " + + this._rfb_auth_scheme + ")"); + } + } + + _handle_security_result() { + if (this._sock.rQwait('VNC auth response ', 4)) { return false; } + + const status = this._sock.rQshift32(); + + if (status === 0) { // OK + this._rfb_init_state = 'ClientInitialisation'; + Log.Debug('Authentication OK'); + return this._init_msg(); + } else { + if (this._rfb_version >= 3.8) { + this._rfb_init_state = "SecurityReason"; + this._security_context = "security result"; + this._security_status = status; + return this._init_msg(); + } else { + this.dispatchEvent(new CustomEvent( + "securityfailure", + { detail: { status: status } })); + + return this._fail("Security handshake failed"); + } + } + } + + _negotiate_server_init() { + if (this._sock.rQwait("server initialization", 24)) { return false; } + + /* Screen size */ + const width = this._sock.rQshift16(); + const height = this._sock.rQshift16(); + + /* PIXEL_FORMAT */ + const bpp = this._sock.rQshift8(); + const depth = this._sock.rQshift8(); + const big_endian = this._sock.rQshift8(); + const true_color = this._sock.rQshift8(); + + const red_max = this._sock.rQshift16(); + const green_max = this._sock.rQshift16(); + const blue_max = this._sock.rQshift16(); + const red_shift = this._sock.rQshift8(); + const green_shift = this._sock.rQshift8(); + const blue_shift = this._sock.rQshift8(); + this._sock.rQskipBytes(3); // padding + + // NB(directxman12): we don't want to call any callbacks or print messages until + // *after* we're past the point where we could backtrack + + /* Connection name/title */ + const name_length = this._sock.rQshift32(); + if (this._sock.rQwait('server init name', name_length, 24)) { return false; } + this._fb_name = decodeUTF8(this._sock.rQshiftStr(name_length)); + + if (this._rfb_tightvnc) { + if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + name_length)) { return false; } + // In TightVNC mode, ServerInit message is extended + const numServerMessages = this._sock.rQshift16(); + const numClientMessages = this._sock.rQshift16(); + const numEncodings = this._sock.rQshift16(); + this._sock.rQskipBytes(2); // padding + + const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16; + if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + name_length)) { return false; } + + // we don't actually do anything with the capability information that TIGHT sends, + // so we just skip the all of this. + + // TIGHT server message capabilities + this._sock.rQskipBytes(16 * numServerMessages); + + // TIGHT client message capabilities + this._sock.rQskipBytes(16 * numClientMessages); + + // TIGHT encoding capabilities + this._sock.rQskipBytes(16 * numEncodings); + } + + // NB(directxman12): these are down here so that we don't run them multiple times + // if we backtrack + Log.Info("Screen: " + width + "x" + height + + ", bpp: " + bpp + ", depth: " + depth + + ", big_endian: " + big_endian + + ", true_color: " + true_color + + ", red_max: " + red_max + + ", green_max: " + green_max + + ", blue_max: " + blue_max + + ", red_shift: " + red_shift + + ", green_shift: " + green_shift + + ", blue_shift: " + blue_shift); + + if (big_endian !== 0) { + Log.Warn("Server native endian is not little endian"); + } + + if (red_shift !== 16) { + Log.Warn("Server native red-shift is not 16"); + } + + if (blue_shift !== 0) { + Log.Warn("Server native blue-shift is not 0"); + } + + // we're past the point where we could backtrack, so it's safe to call this + this.dispatchEvent(new CustomEvent( + "desktopname", + { detail: { name: this._fb_name } })); + + this._resize(width, height); + + if (!this._viewOnly) { this._keyboard.grab(); } + if (!this._viewOnly) { this._mouse.grab(); } + + this._fb_depth = 24; + + if (this._fb_name === "Intel(r) AMT KVM") { + Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Using low color mode."); + this._fb_depth = 8; + } + + RFB.messages.pixelFormat(this._sock, this._fb_depth, true); + this._sendEncodings(); + RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fb_width, this._fb_height); + + this._updateConnectionState('connected'); + return true; + } + + _sendEncodings() { + const encs = []; + + // In preference order + encs.push(encodings.encodingCopyRect); + // Only supported with full depth support + if (this._fb_depth == 24) { + encs.push(encodings.encodingTight); + encs.push(encodings.encodingTightPNG); + encs.push(encodings.encodingHextile); + encs.push(encodings.encodingRRE); + } + encs.push(encodings.encodingRaw); + + // Psuedo-encoding settings + encs.push(encodings.pseudoEncodingQualityLevel0 + 6); + encs.push(encodings.pseudoEncodingCompressLevel0 + 2); + + encs.push(encodings.pseudoEncodingDesktopSize); + encs.push(encodings.pseudoEncodingLastRect); + encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent); + encs.push(encodings.pseudoEncodingExtendedDesktopSize); + encs.push(encodings.pseudoEncodingXvp); + encs.push(encodings.pseudoEncodingFence); + encs.push(encodings.pseudoEncodingContinuousUpdates); + + if (this._fb_depth == 24) { + encs.push(encodings.pseudoEncodingCursor); + } + + RFB.messages.clientEncodings(this._sock, encs); + } + + /* RFB protocol initialization states: + * ProtocolVersion + * Security + * Authentication + * SecurityResult + * ClientInitialization - not triggered by server message + * ServerInitialization + */ + _init_msg() { + switch (this._rfb_init_state) { + case 'ProtocolVersion': + return this._negotiate_protocol_version(); + + case 'Security': + return this._negotiate_security(); + + case 'Authentication': + return this._negotiate_authentication(); + + case 'SecurityResult': + return this._handle_security_result(); + + case 'SecurityReason': + return this._handle_security_reason(); + + case 'ClientInitialisation': + this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation + this._rfb_init_state = 'ServerInitialisation'; + return true; + + case 'ServerInitialisation': + return this._negotiate_server_init(); + + default: + return this._fail("Unknown init state (state: " + + this._rfb_init_state + ")"); + } + } + + _handle_set_colour_map_msg() { + Log.Debug("SetColorMapEntries"); + + return this._fail("Unexpected SetColorMapEntries message"); + } + + _handle_server_cut_text() { + Log.Debug("ServerCutText"); + + if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; } + this._sock.rQskipBytes(3); // Padding + const length = this._sock.rQshift32(); + if (this._sock.rQwait("ServerCutText", length, 8)) { return false; } + + const text = this._sock.rQshiftStr(length); + + if (this._viewOnly) { return true; } + + this.dispatchEvent(new CustomEvent( + "clipboard", + { detail: { text: text } })); + + return true; + } + + _handle_server_fence_msg() { + if (this._sock.rQwait("ServerFence header", 8, 1)) { return false; } + this._sock.rQskipBytes(3); // Padding + let flags = this._sock.rQshift32(); + let length = this._sock.rQshift8(); + + if (this._sock.rQwait("ServerFence payload", length, 9)) { return false; } + + if (length > 64) { + Log.Warn("Bad payload length (" + length + ") in fence response"); + length = 64; + } + + const payload = this._sock.rQshiftStr(length); + + this._supportsFence = true; + + /* + * Fence flags + * + * (1<<0) - BlockBefore + * (1<<1) - BlockAfter + * (1<<2) - SyncNext + * (1<<31) - Request + */ + + if (!(flags & (1<<31))) { + return this._fail("Unexpected fence response"); + } + + // Filter out unsupported flags + // FIXME: support syncNext + flags &= (1<<0) | (1<<1); + + // BlockBefore and BlockAfter are automatically handled by + // the fact that we process each incoming message + // synchronuosly. + RFB.messages.clientFence(this._sock, flags, payload); + + return true; + } + + _handle_xvp_msg() { + if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; } + this._sock.rQskipBytes(1); // Padding + const xvp_ver = this._sock.rQshift8(); + const xvp_msg = this._sock.rQshift8(); + + switch (xvp_msg) { + case 0: // XVP_FAIL + Log.Error("XVP Operation Failed"); + break; + case 1: // XVP_INIT + this._rfb_xvp_ver = xvp_ver; + Log.Info("XVP extensions enabled (version " + this._rfb_xvp_ver + ")"); + this._setCapability("power", true); + break; + default: + this._fail("Illegal server XVP message (msg: " + xvp_msg + ")"); + break; + } + + return true; + } + + _normal_msg() { + let msg_type; + if (this._FBU.rects > 0) { + msg_type = 0; + } else { + msg_type = this._sock.rQshift8(); + } + + let first, ret; + switch (msg_type) { + case 0: // FramebufferUpdate + ret = this._framebufferUpdate(); + if (ret && !this._enabledContinuousUpdates) { + RFB.messages.fbUpdateRequest(this._sock, true, 0, 0, + this._fb_width, this._fb_height); + } + return ret; + + case 1: // SetColorMapEntries + return this._handle_set_colour_map_msg(); + + case 2: // Bell + Log.Debug("Bell"); + this.dispatchEvent(new CustomEvent( + "bell", + { detail: {} })); + return true; + + case 3: // ServerCutText + return this._handle_server_cut_text(); + + case 150: // EndOfContinuousUpdates + first = !this._supportsContinuousUpdates; + this._supportsContinuousUpdates = true; + this._enabledContinuousUpdates = false; + if (first) { + this._enabledContinuousUpdates = true; + this._updateContinuousUpdates(); + Log.Info("Enabling continuous updates."); + } else { + // FIXME: We need to send a framebufferupdaterequest here + // if we add support for turning off continuous updates + } + return true; + + case 248: // ServerFence + return this._handle_server_fence_msg(); + + case 250: // XVP + return this._handle_xvp_msg(); + + default: + this._fail("Unexpected server message (type " + msg_type + ")"); + Log.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30)); + return true; + } + } + + _onFlush() { + this._flushing = false; + // Resume processing + if (this._sock.rQlen > 0) { + this._handle_message(); + } + } + + _framebufferUpdate() { + if (this._FBU.rects === 0) { + if (this._sock.rQwait("FBU header", 3, 1)) { return false; } + this._sock.rQskipBytes(1); // Padding + this._FBU.rects = this._sock.rQshift16(); + + // Make sure the previous frame is fully rendered first + // to avoid building up an excessive queue + if (this._display.pending()) { + this._flushing = true; + this._display.flush(); + return false; + } + } + + while (this._FBU.rects > 0) { + if (this._FBU.encoding === null) { + if (this._sock.rQwait("rect header", 12)) { return false; } + /* New FramebufferUpdate */ + + const hdr = this._sock.rQshiftBytes(12); + this._FBU.x = (hdr[0] << 8) + hdr[1]; + this._FBU.y = (hdr[2] << 8) + hdr[3]; + this._FBU.width = (hdr[4] << 8) + hdr[5]; + this._FBU.height = (hdr[6] << 8) + hdr[7]; + this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) + + (hdr[10] << 8) + hdr[11], 10); + } + + if (!this._handleRect()) { + return false; + } + + this._FBU.rects--; + this._FBU.encoding = null; + } + + this._display.flip(); + + return true; // We finished this FBU + } + + _handleRect() { + switch (this._FBU.encoding) { + case encodings.pseudoEncodingLastRect: + this._FBU.rects = 1; // Will be decreased when we return + return true; + + case encodings.pseudoEncodingCursor: + return this._handleCursor(); + + case encodings.pseudoEncodingQEMUExtendedKeyEvent: + // Old Safari doesn't support creating keyboard events + try { + const keyboardEvent = document.createEvent("keyboardEvent"); + if (keyboardEvent.code !== undefined) { + this._qemuExtKeyEventSupported = true; + } + } catch (err) { + // Do nothing + } + return true; + + case encodings.pseudoEncodingDesktopSize: + this._resize(this._FBU.width, this._FBU.height); + return true; + + case encodings.pseudoEncodingExtendedDesktopSize: + return this._handleExtendedDesktopSize(); + + default: + return this._handleDataRect(); + } + } + + _handleCursor() { + const hotx = this._FBU.x; // hotspot-x + const hoty = this._FBU.y; // hotspot-y + const w = this._FBU.width; + const h = this._FBU.height; + + const pixelslength = w * h * 4; + const masklength = Math.ceil(w / 8) * h; + + let bytes = pixelslength + masklength; + if (this._sock.rQwait("cursor encoding", bytes)) { + return false; + } + + // Decode from BGRX pixels + bit mask to RGBA + const pixels = this._sock.rQshiftBytes(pixelslength); + const mask = this._sock.rQshiftBytes(masklength); + let rgba = new Uint8Array(w * h * 4); + + let pix_idx = 0; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + let mask_idx = y * Math.ceil(w / 8) + Math.floor(x / 8); + let alpha = (mask[mask_idx] << (x % 8)) & 0x80 ? 255 : 0; + rgba[pix_idx ] = pixels[pix_idx + 2]; + rgba[pix_idx + 1] = pixels[pix_idx + 1]; + rgba[pix_idx + 2] = pixels[pix_idx]; + rgba[pix_idx + 3] = alpha; + pix_idx += 4; + } + } + + this._updateCursor(rgba, hotx, hoty, w, h); + + return true; + } + + _handleExtendedDesktopSize() { + if (this._sock.rQwait("ExtendedDesktopSize", 4)) { + return false; + } + + const number_of_screens = this._sock.rQpeek8(); + + let bytes = 4 + (number_of_screens * 16); + if (this._sock.rQwait("ExtendedDesktopSize", bytes)) { + return false; + } + + const firstUpdate = !this._supportsSetDesktopSize; + this._supportsSetDesktopSize = true; + + // Normally we only apply the current resize mode after a + // window resize event. However there is no such trigger on the + // initial connect. And we don't know if the server supports + // resizing until we've gotten here. + if (firstUpdate) { + this._requestRemoteResize(); + } + + this._sock.rQskipBytes(1); // number-of-screens + this._sock.rQskipBytes(3); // padding + + for (let i = 0; i < number_of_screens; i += 1) { + // Save the id and flags of the first screen + if (i === 0) { + this._screen_id = this._sock.rQshiftBytes(4); // id + this._sock.rQskipBytes(2); // x-position + this._sock.rQskipBytes(2); // y-position + this._sock.rQskipBytes(2); // width + this._sock.rQskipBytes(2); // height + this._screen_flags = this._sock.rQshiftBytes(4); // flags + } else { + this._sock.rQskipBytes(16); + } + } + + /* + * The x-position indicates the reason for the change: + * + * 0 - server resized on its own + * 1 - this client requested the resize + * 2 - another client requested the resize + */ + + // We need to handle errors when we requested the resize. + if (this._FBU.x === 1 && this._FBU.y !== 0) { + let msg = ""; + // The y-position indicates the status code from the server + switch (this._FBU.y) { + case 1: + msg = "Resize is administratively prohibited"; + break; + case 2: + msg = "Out of resources"; + break; + case 3: + msg = "Invalid screen layout"; + break; + default: + msg = "Unknown reason"; + break; + } + Log.Warn("Server did not accept the resize request: " + + msg); + } else { + this._resize(this._FBU.width, this._FBU.height); + } + + return true; + } + + _handleDataRect() { + let decoder = this._decoders[this._FBU.encoding]; + if (!decoder) { + this._fail("Unsupported encoding (encoding: " + + this._FBU.encoding + ")"); + return false; + } + + try { + return decoder.decodeRect(this._FBU.x, this._FBU.y, + this._FBU.width, this._FBU.height, + this._sock, this._display, + this._fb_depth); + } catch (err) { + this._fail("Error decoding rect: " + err); + return false; + } + } + + _updateContinuousUpdates() { + if (!this._enabledContinuousUpdates) { return; } + + RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0, + this._fb_width, this._fb_height); + } + + _resize(width, height) { + this._fb_width = width; + this._fb_height = height; + + this._display.resize(this._fb_width, this._fb_height); + + // Adjust the visible viewport based on the new dimensions + this._updateClip(); + this._updateScale(); + + this._updateContinuousUpdates(); + } + + _xvpOp(ver, op) { + if (this._rfb_xvp_ver < ver) { return; } + Log.Info("Sending XVP operation " + op + " (version " + ver + ")"); + RFB.messages.xvpOp(this._sock, ver, op); + } + + _updateCursor(rgba, hotx, hoty, w, h) { + this._cursorImage = { + rgbaPixels: rgba, + hotx: hotx, hoty: hoty, w: w, h: h, + }; + this._refreshCursor(); + } + + _shouldShowDotCursor() { + // Called when this._cursorImage is updated + if (!this._showDotCursor) { + // User does not want to see the dot, so... + return false; + } + + // The dot should not be shown if the cursor is already visible, + // i.e. contains at least one not-fully-transparent pixel. + // So iterate through all alpha bytes in rgba and stop at the + // first non-zero. + for (let i = 3; i < this._cursorImage.rgbaPixels.length; i += 4) { + if (this._cursorImage.rgbaPixels[i]) { + return false; + } + } + + // At this point, we know that the cursor is fully transparent, and + // the user wants to see the dot instead of this. + return true; + } + + _refreshCursor() { + const image = this._shouldShowDotCursor() ? RFB.cursors.dot : this._cursorImage; + this._cursor.change(image.rgbaPixels, + image.hotx, image.hoty, + image.w, image.h + ); + } + + static genDES(password, challenge) { + const passwordChars = password.split('').map(c => c.charCodeAt(0)); + return (new DES(passwordChars)).encrypt(challenge); + } +} + +// Class Methods +RFB.messages = { + keyEvent(sock, keysym, down) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 4; // msg-type + buff[offset + 1] = down; + + buff[offset + 2] = 0; + buff[offset + 3] = 0; + + buff[offset + 4] = (keysym >> 24); + buff[offset + 5] = (keysym >> 16); + buff[offset + 6] = (keysym >> 8); + buff[offset + 7] = keysym; + + sock._sQlen += 8; + sock.flush(); + }, + + QEMUExtendedKeyEvent(sock, keysym, down, keycode) { + function getRFBkeycode(xt_scancode) { + const upperByte = (keycode >> 8); + const lowerByte = (keycode & 0x00ff); + if (upperByte === 0xe0 && lowerByte < 0x7f) { + return lowerByte | 0x80; + } + return xt_scancode; + } + + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 255; // msg-type + buff[offset + 1] = 0; // sub msg-type + + buff[offset + 2] = (down >> 8); + buff[offset + 3] = down; + + buff[offset + 4] = (keysym >> 24); + buff[offset + 5] = (keysym >> 16); + buff[offset + 6] = (keysym >> 8); + buff[offset + 7] = keysym; + + const RFBkeycode = getRFBkeycode(keycode); + + buff[offset + 8] = (RFBkeycode >> 24); + buff[offset + 9] = (RFBkeycode >> 16); + buff[offset + 10] = (RFBkeycode >> 8); + buff[offset + 11] = RFBkeycode; + + sock._sQlen += 12; + sock.flush(); + }, + + pointerEvent(sock, x, y, mask) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 5; // msg-type + + buff[offset + 1] = mask; + + buff[offset + 2] = x >> 8; + buff[offset + 3] = x; + + buff[offset + 4] = y >> 8; + buff[offset + 5] = y; + + sock._sQlen += 6; + sock.flush(); + }, + + // TODO(directxman12): make this unicode compatible? + clientCutText(sock, text) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 6; // msg-type + + buff[offset + 1] = 0; // padding + buff[offset + 2] = 0; // padding + buff[offset + 3] = 0; // padding + + let length = text.length; + + buff[offset + 4] = length >> 24; + buff[offset + 5] = length >> 16; + buff[offset + 6] = length >> 8; + buff[offset + 7] = length; + + sock._sQlen += 8; + + // We have to keep track of from where in the text we begin creating the + // buffer for the flush in the next iteration. + let textOffset = 0; + + let remaining = length; + while (remaining > 0) { + + let flushSize = Math.min(remaining, (sock._sQbufferSize - sock._sQlen)); + for (let i = 0; i < flushSize; i++) { + buff[sock._sQlen + i] = text.charCodeAt(textOffset + i); + } + + sock._sQlen += flushSize; + sock.flush(); + + remaining -= flushSize; + textOffset += flushSize; + } + }, + + setDesktopSize(sock, width, height, id, flags) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 251; // msg-type + buff[offset + 1] = 0; // padding + buff[offset + 2] = width >> 8; // width + buff[offset + 3] = width; + buff[offset + 4] = height >> 8; // height + buff[offset + 5] = height; + + buff[offset + 6] = 1; // number-of-screens + buff[offset + 7] = 0; // padding + + // screen array + buff[offset + 8] = id >> 24; // id + buff[offset + 9] = id >> 16; + buff[offset + 10] = id >> 8; + buff[offset + 11] = id; + buff[offset + 12] = 0; // x-position + buff[offset + 13] = 0; + buff[offset + 14] = 0; // y-position + buff[offset + 15] = 0; + buff[offset + 16] = width >> 8; // width + buff[offset + 17] = width; + buff[offset + 18] = height >> 8; // height + buff[offset + 19] = height; + buff[offset + 20] = flags >> 24; // flags + buff[offset + 21] = flags >> 16; + buff[offset + 22] = flags >> 8; + buff[offset + 23] = flags; + + sock._sQlen += 24; + sock.flush(); + }, + + clientFence(sock, flags, payload) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 248; // msg-type + + buff[offset + 1] = 0; // padding + buff[offset + 2] = 0; // padding + buff[offset + 3] = 0; // padding + + buff[offset + 4] = flags >> 24; // flags + buff[offset + 5] = flags >> 16; + buff[offset + 6] = flags >> 8; + buff[offset + 7] = flags; + + const n = payload.length; + + buff[offset + 8] = n; // length + + for (let i = 0; i < n; i++) { + buff[offset + 9 + i] = payload.charCodeAt(i); + } + + sock._sQlen += 9 + n; + sock.flush(); + }, + + enableContinuousUpdates(sock, enable, x, y, width, height) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 150; // msg-type + buff[offset + 1] = enable; // enable-flag + + buff[offset + 2] = x >> 8; // x + buff[offset + 3] = x; + buff[offset + 4] = y >> 8; // y + buff[offset + 5] = y; + buff[offset + 6] = width >> 8; // width + buff[offset + 7] = width; + buff[offset + 8] = height >> 8; // height + buff[offset + 9] = height; + + sock._sQlen += 10; + sock.flush(); + }, + + pixelFormat(sock, depth, true_color) { + const buff = sock._sQ; + const offset = sock._sQlen; + + let bpp; + + if (depth > 16) { + bpp = 32; + } else if (depth > 8) { + bpp = 16; + } else { + bpp = 8; + } + + const bits = Math.floor(depth/3); + + buff[offset] = 0; // msg-type + + buff[offset + 1] = 0; // padding + buff[offset + 2] = 0; // padding + buff[offset + 3] = 0; // padding + + buff[offset + 4] = bpp; // bits-per-pixel + buff[offset + 5] = depth; // depth + buff[offset + 6] = 0; // little-endian + buff[offset + 7] = true_color ? 1 : 0; // true-color + + buff[offset + 8] = 0; // red-max + buff[offset + 9] = (1 << bits) - 1; // red-max + + buff[offset + 10] = 0; // green-max + buff[offset + 11] = (1 << bits) - 1; // green-max + + buff[offset + 12] = 0; // blue-max + buff[offset + 13] = (1 << bits) - 1; // blue-max + + buff[offset + 14] = bits * 2; // red-shift + buff[offset + 15] = bits * 1; // green-shift + buff[offset + 16] = bits * 0; // blue-shift + + buff[offset + 17] = 0; // padding + buff[offset + 18] = 0; // padding + buff[offset + 19] = 0; // padding + + sock._sQlen += 20; + sock.flush(); + }, + + clientEncodings(sock, encodings) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 2; // msg-type + buff[offset + 1] = 0; // padding + + buff[offset + 2] = encodings.length >> 8; + buff[offset + 3] = encodings.length; + + let j = offset + 4; + for (let i = 0; i < encodings.length; i++) { + const enc = encodings[i]; + buff[j] = enc >> 24; + buff[j + 1] = enc >> 16; + buff[j + 2] = enc >> 8; + buff[j + 3] = enc; + + j += 4; + } + + sock._sQlen += j - offset; + sock.flush(); + }, + + fbUpdateRequest(sock, incremental, x, y, w, h) { + const buff = sock._sQ; + const offset = sock._sQlen; + + if (typeof(x) === "undefined") { x = 0; } + if (typeof(y) === "undefined") { y = 0; } + + buff[offset] = 3; // msg-type + buff[offset + 1] = incremental ? 1 : 0; + + buff[offset + 2] = (x >> 8) & 0xFF; + buff[offset + 3] = x & 0xFF; + + buff[offset + 4] = (y >> 8) & 0xFF; + buff[offset + 5] = y & 0xFF; + + buff[offset + 6] = (w >> 8) & 0xFF; + buff[offset + 7] = w & 0xFF; + + buff[offset + 8] = (h >> 8) & 0xFF; + buff[offset + 9] = h & 0xFF; + + sock._sQlen += 10; + sock.flush(); + }, + + xvpOp(sock, ver, op) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 250; // msg-type + buff[offset + 1] = 0; // padding + + buff[offset + 2] = ver; + buff[offset + 3] = op; + + sock._sQlen += 4; + sock.flush(); + } +}; + +RFB.cursors = { + none: { + rgbaPixels: new Uint8Array(), + w: 0, h: 0, + hotx: 0, hoty: 0, + }, + + dot: { + /* eslint-disable indent */ + rgbaPixels: new Uint8Array([ + 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, + 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, + 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, + ]), + /* eslint-enable indent */ + w: 3, h: 3, + hotx: 1, hoty: 1, + } +}; diff --git a/systemvm/debian/root/noVNC/core/util/browser.js b/systemvm/debian/root/noVNC/core/util/browser.js new file mode 100644 index 000000000000..8996cfeda712 --- /dev/null +++ b/systemvm/debian/root/noVNC/core/util/browser.js @@ -0,0 +1,90 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +import * as Log from './logging.js'; + +// Touch detection +export let isTouchDevice = ('ontouchstart' in document.documentElement) || + // requried for Chrome debugger + (document.ontouchstart !== undefined) || + // required for MS Surface + (navigator.maxTouchPoints > 0) || + (navigator.msMaxTouchPoints > 0); +window.addEventListener('touchstart', function onFirstTouch() { + isTouchDevice = true; + window.removeEventListener('touchstart', onFirstTouch, false); +}, false); + + +// The goal is to find a certain physical width, the devicePixelRatio +// brings us a bit closer but is not optimal. +export let dragThreshold = 10 * (window.devicePixelRatio || 1); + +let _supportsCursorURIs = false; + +try { + const target = document.createElement('canvas'); + target.style.cursor = 'url("data:image/x-icon;base64,AAACAAEACAgAAAIAAgA4AQAAFgAAACgAAAAIAAAAEAAAAAEAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAA==") 2 2, default'; + + if (target.style.cursor) { + Log.Info("Data URI scheme cursor supported"); + _supportsCursorURIs = true; + } else { + Log.Warn("Data URI scheme cursor not supported"); + } +} catch (exc) { + Log.Error("Data URI scheme cursor test exception: " + exc); +} + +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; + +export function isMac() { + return navigator && !!(/mac/i).exec(navigator.platform); +} + +export function isWindows() { + return navigator && !!(/win/i).exec(navigator.platform); +} + +export function isIOS() { + return navigator && + (!!(/ipad/i).exec(navigator.platform) || + !!(/iphone/i).exec(navigator.platform) || + !!(/ipod/i).exec(navigator.platform)); +} + +export function isAndroid() { + return navigator && !!(/android/i).exec(navigator.userAgent); +} + +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); +} + +export function isFirefox() { + return navigator && !!(/firefox/i).exec(navigator.userAgent); +} + diff --git a/systemvm/debian/root/noVNC/core/util/cursor.js b/systemvm/debian/root/noVNC/core/util/cursor.js new file mode 100644 index 000000000000..0d0b754a863f --- /dev/null +++ b/systemvm/debian/root/noVNC/core/util/cursor.js @@ -0,0 +1,221 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 or any later version (see LICENSE.txt) + */ + +import { supportsCursorURIs, isTouchDevice } from './browser.js'; + +const useFallback = !supportsCursorURIs || isTouchDevice; + +export default class Cursor { + constructor() { + this._target = null; + + this._canvas = document.createElement('canvas'); + + if (useFallback) { + this._canvas.style.position = 'fixed'; + this._canvas.style.zIndex = '65535'; + this._canvas.style.pointerEvents = 'none'; + // Can't use "display" because of Firefox bug #1445997 + this._canvas.style.visibility = 'hidden'; + document.body.appendChild(this._canvas); + } + + this._position = { x: 0, y: 0 }; + this._hotSpot = { x: 0, y: 0 }; + + this._eventHandlers = { + 'mouseover': this._handleMouseOver.bind(this), + 'mouseleave': this._handleMouseLeave.bind(this), + 'mousemove': this._handleMouseMove.bind(this), + 'mouseup': this._handleMouseUp.bind(this), + 'touchstart': this._handleTouchStart.bind(this), + 'touchmove': this._handleTouchMove.bind(this), + 'touchend': this._handleTouchEnd.bind(this), + }; + } + + attach(target) { + if (this._target) { + this.detach(); + } + + this._target = target; + + if (useFallback) { + // 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); + this._target.addEventListener('mousemove', this._eventHandlers.mousemove, options); + this._target.addEventListener('mouseup', this._eventHandlers.mouseup, options); + + // There is no "touchleave" so we monitor touchstart globally + window.addEventListener('touchstart', this._eventHandlers.touchstart, options); + this._target.addEventListener('touchmove', this._eventHandlers.touchmove, options); + this._target.addEventListener('touchend', this._eventHandlers.touchend, options); + } + + this.clear(); + } + + detach() { + if (useFallback) { + const options = { capture: true, passive: true }; + this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options); + this._target.removeEventListener('mouseleave', this._eventHandlers.mouseleave, options); + this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options); + this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options); + + window.removeEventListener('touchstart', this._eventHandlers.touchstart, options); + this._target.removeEventListener('touchmove', this._eventHandlers.touchmove, options); + this._target.removeEventListener('touchend', this._eventHandlers.touchend, options); + } + + this._target = null; + } + + change(rgba, hotx, hoty, w, h) { + if ((w === 0) || (h === 0)) { + this.clear(); + return; + } + + this._position.x = this._position.x + this._hotSpot.x - hotx; + this._position.y = this._position.y + this._hotSpot.y - hoty; + this._hotSpot.x = hotx; + this._hotSpot.y = hoty; + + let ctx = this._canvas.getContext('2d'); + + 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)); + } + ctx.clearRect(0, 0, w, h); + ctx.putImageData(img, 0, 0); + + if (useFallback) { + this._updatePosition(); + } else { + let url = this._canvas.toDataURL(); + this._target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default'; + } + } + + clear() { + this._target.style.cursor = 'none'; + this._canvas.width = 0; + this._canvas.height = 0; + this._position.x = this._position.x + this._hotSpot.x; + this._position.y = this._position.y + this._hotSpot.y; + this._hotSpot.x = 0; + this._hotSpot.y = 0; + } + + _handleMouseOver(event) { + // This event could be because we're entering the target, or + // moving around amongst its sub elements. Let the move handler + // sort things out. + this._handleMouseMove(event); + } + + _handleMouseLeave(event) { + this._hideCursor(); + } + + _handleMouseMove(event) { + this._updateVisibility(event.target); + + this._position.x = event.clientX - this._hotSpot.x; + this._position.y = event.clientY - this._hotSpot.y; + + this._updatePosition(); + } + + _handleMouseUp(event) { + // We might get this event because of a drag operation that + // moved outside of the target. Check what's under the cursor + // now and adjust visibility based on that. + let target = document.elementFromPoint(event.clientX, event.clientY); + this._updateVisibility(target); + } + + _handleTouchStart(event) { + // Just as for mouseover, we let the move handler deal with it + this._handleTouchMove(event); + } + + _handleTouchMove(event) { + this._updateVisibility(event.target); + + this._position.x = event.changedTouches[0].clientX - this._hotSpot.x; + this._position.y = event.changedTouches[0].clientY - this._hotSpot.y; + + this._updatePosition(); + } + + _handleTouchEnd(event) { + // Same principle as for mouseup + let target = document.elementFromPoint(event.changedTouches[0].clientX, + event.changedTouches[0].clientY); + this._updateVisibility(target); + } + + _showCursor() { + if (this._canvas.style.visibility === 'hidden') { + this._canvas.style.visibility = ''; + } + } + + _hideCursor() { + if (this._canvas.style.visibility !== 'hidden') { + this._canvas.style.visibility = 'hidden'; + } + } + + // Should we currently display the cursor? + // (i.e. are we over the target, or a child of the target without a + // different cursor set) + _shouldShowCursor(target) { + // Easy case + if (target === this._target) { + return true; + } + // Other part of the DOM? + if (!this._target.contains(target)) { + return false; + } + // Has the child its own cursor? + // FIXME: How can we tell that a sub element has an + // explicit "cursor: none;"? + if (window.getComputedStyle(target).cursor !== 'none') { + return false; + } + return true; + } + + _updateVisibility(target) { + if (this._shouldShowCursor(target)) { + this._showCursor(); + } else { + this._hideCursor(); + } + } + + _updatePosition() { + this._canvas.style.left = this._position.x + "px"; + this._canvas.style.top = this._position.y + "px"; + } +} diff --git a/systemvm/debian/root/noVNC/core/util/events.js b/systemvm/debian/root/noVNC/core/util/events.js new file mode 100644 index 000000000000..f1222796a7e9 --- /dev/null +++ b/systemvm/debian/root/noVNC/core/util/events.js @@ -0,0 +1,139 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +/* + * Cross-browser event and position routines + */ + +export function getPointerEvent(e) { + return e.changedTouches ? e.changedTouches[0] : e.touches ? e.touches[0] : e; +} + +export function stopEvent(e) { + e.stopPropagation(); + e.preventDefault(); +} + +// Emulate Element.setCapture() when not supported +let _captureRecursion = false; +let _captureElem = null; +function _captureProxy(e) { + // Recursion protection as we'll see our own event + if (_captureRecursion) return; + + // Clone the event as we cannot dispatch an already dispatched event + const newEv = new e.constructor(e.type, e); + + _captureRecursion = true; + _captureElem.dispatchEvent(newEv); + _captureRecursion = false; + + // Avoid double events + e.stopPropagation(); + + // Respect the wishes of the redirected event handlers + if (newEv.defaultPrevented) { + e.preventDefault(); + } + + // Implicitly release the capture on button release + if (e.type === "mouseup") { + releaseCapture(); + } +} + +// Follow cursor style of target element +function _captureElemChanged() { + const captureElem = document.getElementById("noVNC_mouse_capture_elem"); + captureElem.style.cursor = window.getComputedStyle(_captureElem).cursor; +} + +const _captureObserver = new MutationObserver(_captureElemChanged); + +let _captureIndex = 0; + +export function setCapture(elem) { + if (elem.setCapture) { + + elem.setCapture(); + + // IE releases capture on 'click' events which might not trigger + elem.addEventListener('mouseup', releaseCapture); + + } else { + // Release any existing capture in case this method is + // called multiple times without coordination + releaseCapture(); + + let captureElem = document.getElementById("noVNC_mouse_capture_elem"); + + if (captureElem === null) { + captureElem = document.createElement("div"); + captureElem.id = "noVNC_mouse_capture_elem"; + captureElem.style.position = "fixed"; + captureElem.style.top = "0px"; + captureElem.style.left = "0px"; + captureElem.style.width = "100%"; + captureElem.style.height = "100%"; + captureElem.style.zIndex = 10000; + captureElem.style.display = "none"; + document.body.appendChild(captureElem); + + // This is to make sure callers don't get confused by having + // our blocking element as the target + captureElem.addEventListener('contextmenu', _captureProxy); + + captureElem.addEventListener('mousemove', _captureProxy); + captureElem.addEventListener('mouseup', _captureProxy); + } + + _captureElem = elem; + _captureIndex++; + + // Track cursor and get initial cursor + _captureObserver.observe(elem, {attributes: true}); + _captureElemChanged(); + + captureElem.style.display = ""; + + // We listen to events on window in order to keep tracking if it + // happens to leave the viewport + window.addEventListener('mousemove', _captureProxy); + window.addEventListener('mouseup', _captureProxy); + } +} + +export function releaseCapture() { + if (document.releaseCapture) { + + document.releaseCapture(); + + } else { + if (!_captureElem) { + return; + } + + // There might be events already queued, so we need to wait for + // them to flush. E.g. contextmenu in Microsoft Edge + window.setTimeout((expected) => { + // Only clear it if it's the expected grab (i.e. no one + // else has initiated a new grab) + if (_captureIndex === expected) { + _captureElem = null; + } + }, 0, _captureIndex); + + _captureObserver.disconnect(); + + const captureElem = document.getElementById("noVNC_mouse_capture_elem"); + captureElem.style.display = "none"; + + window.removeEventListener('mousemove', _captureProxy); + window.removeEventListener('mouseup', _captureProxy); + } +} diff --git a/systemvm/debian/root/noVNC/core/util/eventtarget.js b/systemvm/debian/root/noVNC/core/util/eventtarget.js new file mode 100644 index 000000000000..f54ca9bf1122 --- /dev/null +++ b/systemvm/debian/root/noVNC/core/util/eventtarget.js @@ -0,0 +1,35 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +export default class EventTargetMixin { + constructor() { + this._listeners = new Map(); + } + + addEventListener(type, callback) { + if (!this._listeners.has(type)) { + this._listeners.set(type, new Set()); + } + this._listeners.get(type).add(callback); + } + + removeEventListener(type, callback) { + if (this._listeners.has(type)) { + this._listeners.get(type).delete(callback); + } + } + + dispatchEvent(event) { + if (!this._listeners.has(event.type)) { + return true; + } + this._listeners.get(event.type) + .forEach(callback => callback.call(this, event)); + return !event.defaultPrevented; + } +} diff --git a/systemvm/debian/root/noVNC/core/util/logging.js b/systemvm/debian/root/noVNC/core/util/logging.js new file mode 100644 index 000000000000..4c8943d00546 --- /dev/null +++ b/systemvm/debian/root/noVNC/core/util/logging.js @@ -0,0 +1,56 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +/* + * Logging/debug routines + */ + +let _log_level = 'warn'; + +let Debug = () => {}; +let Info = () => {}; +let Warn = () => {}; +let Error = () => {}; + +export function init_logging(level) { + if (typeof level === 'undefined') { + level = _log_level; + } else { + _log_level = level; + } + + Debug = Info = Warn = Error = () => {}; + + if (typeof window.console !== "undefined") { + /* eslint-disable no-console, no-fallthrough */ + switch (level) { + case 'debug': + Debug = console.debug.bind(window.console); + case 'info': + Info = console.info.bind(window.console); + case 'warn': + Warn = console.warn.bind(window.console); + case 'error': + Error = console.error.bind(window.console); + case 'none': + break; + default: + throw new window.Error("invalid logging type '" + level + "'"); + } + /* eslint-enable no-console, no-fallthrough */ + } +} + +export function get_logging() { + return _log_level; +} + +export { Debug, Info, Warn, Error }; + +// Initialize logging level +init_logging(); diff --git a/systemvm/debian/root/noVNC/core/util/polyfill.js b/systemvm/debian/root/noVNC/core/util/polyfill.js new file mode 100644 index 000000000000..648ceebc3c22 --- /dev/null +++ b/systemvm/debian/root/noVNC/core/util/polyfill.js @@ -0,0 +1,54 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 or any later version (see LICENSE.txt) + */ + +/* Polyfills to provide new APIs in old browsers */ + +/* Object.assign() (taken from MDN) */ +if (typeof Object.assign != 'function') { + // Must be writable: true, enumerable: false, configurable: true + Object.defineProperty(Object, "assign", { + value: function assign(target, varArgs) { // .length of function is 2 + 'use strict'; + if (target == null) { // TypeError if undefined or null + throw new TypeError('Cannot convert undefined or null to object'); + } + + const to = Object(target); + + for (let index = 1; index < arguments.length; index++) { + const nextSource = arguments[index]; + + if (nextSource != null) { // Skip over if undefined or null + for (let nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }, + writable: true, + configurable: true + }); +} + +/* CustomEvent constructor (taken from MDN) */ +(() => { + function CustomEvent(event, params) { + params = params || { bubbles: false, cancelable: false, detail: undefined }; + const evt = document.createEvent( 'CustomEvent' ); + evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); + return evt; + } + + CustomEvent.prototype = window.Event.prototype; + + if (typeof window.CustomEvent !== "function") { + window.CustomEvent = CustomEvent; + } +})(); diff --git a/systemvm/debian/root/noVNC/core/util/strings.js b/systemvm/debian/root/noVNC/core/util/strings.js new file mode 100644 index 000000000000..61f4f237d937 --- /dev/null +++ b/systemvm/debian/root/noVNC/core/util/strings.js @@ -0,0 +1,14 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +/* + * Decode from UTF-8 + */ +export function decodeUTF8(utf8string) { + return decodeURIComponent(escape(utf8string)); +} diff --git a/systemvm/debian/root/noVNC/core/websock.js b/systemvm/debian/root/noVNC/core/websock.js new file mode 100644 index 000000000000..51b9a66fb682 --- /dev/null +++ b/systemvm/debian/root/noVNC/core/websock.js @@ -0,0 +1,290 @@ +/* + * Websock: high-performance binary WebSockets + * Copyright (C) 2018 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 has built-in receive queue buffering; the message event + * does not contain actual data but is simply a notification that + * there is new data available. Several rQ* methods are available to + * read binary data off of the receive queue. + */ + +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. +const ENABLE_COPYWITHIN = false; +const MAX_RQ_GROW_SIZE = 40 * 1024 * 1024; // 40 MiB + +export default class Websock { + constructor() { + this._websocket = null; // WebSocket object + + this._rQi = 0; // Receive queue index + this._rQlen = 0; // Next write position in the receive queue + this._rQbufferSize = 1024 * 1024 * 4; // Receive queue buffer size (4 MiB) + this._rQmax = this._rQbufferSize / 8; + // called in init: this._rQ = new Uint8Array(this._rQbufferSize); + this._rQ = null; // Receive queue + + this._sQbufferSize = 1024 * 10; // 10 KiB + // called in init: this._sQ = new Uint8Array(this._sQbufferSize); + this._sQlen = 0; + this._sQ = null; // Send queue + + this._eventHandlers = { + message: () => {}, + open: () => {}, + close: () => {}, + error: () => {} + }; + } + + // Getters and Setters + get sQ() { + return this._sQ; + } + + get rQ() { + return this._rQ; + } + + get rQi() { + return this._rQi; + } + + set rQi(val) { + this._rQi = val; + } + + // Receive Queue + get rQlen() { + return this._rQlen - this._rQi; + } + + rQpeek8() { + return this._rQ[this._rQi]; + } + + rQskipBytes(bytes) { + this._rQi += bytes; + } + + rQshift8() { + return this._rQshift(1); + } + + rQshift16() { + return this._rQshift(2); + } + + rQshift32() { + return this._rQshift(4); + } + + // TODO(directxman12): test performance with these vs a DataView + _rQshift(bytes) { + let res = 0; + for (let byte = bytes - 1; byte >= 0; byte--) { + res += this._rQ[this._rQi++] << (byte * 8); + } + return res; + } + + 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)); + str += String.fromCharCode.apply(null, part); + } + return str; + } + + rQshiftBytes(len) { + if (typeof(len) === 'undefined') { len = this.rQlen; } + this._rQi += len; + return new Uint8Array(this._rQ.buffer, this._rQi - len, len); + } + + 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); + } + + // 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 (goback) { + if (this._rQi < goback) { + throw new Error("rQwait cannot backup " + goback + " bytes"); + } + this._rQi -= goback; + } + return true; // true means need more data + } + return false; + } + + // Send Queue + + flush() { + if (this._sQlen > 0 && this._websocket.readyState === WebSocket.OPEN) { + this._websocket.send(this._encode_message()); + this._sQlen = 0; + } + } + + send(arr) { + this._sQ.set(arr, this._sQlen); + this._sQlen += arr.length; + this.flush(); + } + + send_string(str) { + this.send(str.split('').map(chr => chr.charCodeAt(0))); + } + + // Event Handlers + off(evt) { + this._eventHandlers[evt] = () => {}; + } + + on(evt, handler) { + this._eventHandlers[evt] = handler; + } + + _allocate_buffers() { + this._rQ = new Uint8Array(this._rQbufferSize); + this._sQ = new Uint8Array(this._sQbufferSize); + } + + init() { + this._allocate_buffers(); + this._rQi = 0; + this._websocket = null; + } + + open(uri, protocols) { + this.init(); + + this._websocket = new WebSocket(uri, protocols); + this._websocket.binaryType = 'arraybuffer'; + + this._websocket.onmessage = this._recv_message.bind(this); + this._websocket.onopen = () => { + Log.Debug('>> WebSock.onopen'); + if (this._websocket.protocol) { + Log.Info("Server choose sub-protocol: " + this._websocket.protocol); + } + + 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); + Log.Debug("<< WebSock.onerror: " + e); + }; + } + + close() { + if (this._websocket) { + if ((this._websocket.readyState === WebSocket.OPEN) || + (this._websocket.readyState === WebSocket.CONNECTING)) { + Log.Info("Closing WebSocket connection"); + this._websocket.close(); + } + + this._websocket.onmessage = () => {}; + } + } + + // private methods + _encode_message() { + // 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); + } + + _expand_compact_rQ(min_fit) { + const resizeNeeded = min_fit || this.rQlen > this._rQbufferSize / 2; + if (resizeNeeded) { + if (!min_fit) { + // just double the size if we need to do compaction + this._rQbufferSize *= 2; + } else { + // otherwise, make sure we satisy rQlen - rQi + min_fit < rQbufferSize / 8 + this._rQbufferSize = (this.rQlen + min_fit) * 8; + } + } + + // 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 < min_fit) { + throw new Error("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit"); + } + } + + if (resizeNeeded) { + const old_rQbuffer = this._rQ.buffer; + this._rQmax = this._rQbufferSize / 8; + this._rQ = new Uint8Array(this._rQbufferSize); + this._rQ.set(new Uint8Array(old_rQbuffer, this._rQi)); + } else { + if (ENABLE_COPYWITHIN) { + this._rQ.copyWithin(0, this._rQi); + } else { + this._rQ.set(new Uint8Array(this._rQ.buffer, this._rQi)); + } + } + + this._rQlen = this._rQlen - this._rQi; + this._rQi = 0; + } + + _decode_message(data) { + // push arraybuffer values onto the end + const u8 = new Uint8Array(data); + if (u8.length > this._rQbufferSize - this._rQlen) { + this._expand_compact_rQ(u8.length); + } + this._rQ.set(u8, this._rQlen); + this._rQlen += u8.length; + } + + _recv_message(e) { + this._decode_message(e.data); + if (this.rQlen > 0) { + this._eventHandlers.message(); + // Compact the receive queue + if (this._rQlen == this._rQi) { + this._rQlen = 0; + this._rQi = 0; + } else if (this._rQlen > this._rQmax) { + this._expand_compact_rQ(); + } + } else { + Log.Debug("Ignoring empty message"); + } + } +} diff --git a/systemvm/debian/root/noVNC/docs/API-internal.md b/systemvm/debian/root/noVNC/docs/API-internal.md new file mode 100644 index 000000000000..0b29afb61fc2 --- /dev/null +++ b/systemvm/debian/root/noVNC/docs/API-internal.md @@ -0,0 +1,122 @@ +# 1. Internal Modules + +The noVNC client is composed of several internal modules that handle +rendering, input, networking, etc. Each of the modules is designed to +be cross-browser and independent from each other. + +Note however that the API of these modules is not guaranteed to be +stable, and this documentation is not maintained as well as the +official external API. + + +## 1.1 Module List + +* __Mouse__ (core/input/mouse.js): Mouse input event handler with +limited touch support. + +* __Keyboard__ (core/input/keyboard.js): Keyboard input event handler with +non-US keyboard support. Translates keyDown and keyUp events to X11 +keysym values. + +* __Display__ (core/display.js): Efficient 2D rendering abstraction +layered on the HTML5 canvas element. + +* __Websock__ (core/websock.js): Websock client from websockify +with transparent binary data support. +[Websock API](https://github.com/novnc/websockify/wiki/websock.js) wiki page. + + +## 1.2 Callbacks + +For the Mouse, Keyboard and Display objects the callback functions are +assigned to configuration attributes, just as for the RFB object. The +WebSock module has a method named 'on' that takes two parameters: the +callback event name, and the callback function. + +## 2. Modules + +## 2.1 Mouse Module + +### 2.1.1 Configuration Attributes + +| name | type | mode | default | description +| ----------- | ---- | ---- | -------- | ------------ +| touchButton | int | RW | 1 | Button mask (1, 2, 4) for which click to send on touch devices. 0 means ignore clicks. + +### 2.1.2 Methods + +| name | parameters | description +| ------ | ---------- | ------------ +| grab | () | Begin capturing mouse events +| ungrab | () | Stop capturing mouse events + +### 2.1.2 Callbacks + +| name | parameters | description +| ------------- | ------------------- | ------------ +| onmousebutton | (x, y, down, bmask) | Handler for mouse button click/release +| onmousemove | (x, y) | Handler for mouse movement + + +## 2.2 Keyboard Module + +### 2.2.1 Configuration Attributes + +None + +### 2.2.2 Methods + +| name | parameters | description +| ------ | ---------- | ------------ +| grab | () | Begin capturing keyboard events +| ungrab | () | Stop capturing keyboard events + +### 2.2.3 Callbacks + +| name | parameters | description +| ---------- | -------------------- | ------------ +| onkeypress | (keysym, code, down) | Handler for key press/release + + +## 2.3 Display Module + +### 2.3.1 Configuration Attributes + +| name | type | mode | default | description +| ------------ | ----- | ---- | ------- | ------------ +| logo | raw | RW | | Logo to display when cleared: {"width": width, "height": height, "type": mime-type, "data": data} +| scale | float | RW | 1.0 | Display area scale factor 0.0 - 1.0 +| clipViewport | bool | RW | false | Use viewport clipping +| width | int | RO | | Display area width +| height | int | RO | | Display area height + +### 2.3.2 Methods + +| name | parameters | description +| ------------------ | ------------------------------------------------------- | ------------ +| viewportChangePos | (deltaX, deltaY) | Move the viewport relative to the current location +| viewportChangeSize | (width, height) | Change size of the viewport +| absX | (x) | Return X relative to the remote display +| absY | (y) | Return Y relative to the remote display +| resize | (width, height) | Set width and height +| flip | (from_queue) | Update the visible canvas with the contents of the rendering canvas +| clear | () | Clear the display (show logo if set) +| pending | () | Check if there are waiting items in the render queue +| flush | () | Resume processing the render queue unless it's empty +| fillRect | (x, y, width, height, color, from_queue) | Draw a filled in rectangle +| copyImage | (old_x, old_y, new_x, new_y, width, height, from_queue) | Copy a rectangular area +| imageRect | (x, y, mime, arr) | Draw a rectangle with an image +| startTile | (x, y, width, height, color) | Begin updating a tile +| subTile | (tile, x, y, w, h, color) | Update a sub-rectangle within the given tile +| finishTile | () | Draw the current tile to the display +| blitImage | (x, y, width, height, arr, offset, from_queue) | Blit pixels (of R,G,B,A) to the display +| blitRgbImage | (x, y, width, height, arr, offset, from_queue) | Blit RGB encoded image to display +| blitRgbxImage | (x, y, width, height, arr, offset, from_queue) | Blit RGBX encoded image to display +| drawImage | (img, x, y) | Draw image and track damage +| autoscale | (containerWidth, containerHeight) | Scale the display + +### 2.3.3 Callbacks + +| name | parameters | description +| ------- | ---------- | ------------ +| onflush | () | A display flush has been requested and we are now ready to resume FBU processing diff --git a/systemvm/debian/root/noVNC/docs/API.md b/systemvm/debian/root/noVNC/docs/API.md new file mode 100644 index 000000000000..d587429c176f --- /dev/null +++ b/systemvm/debian/root/noVNC/docs/API.md @@ -0,0 +1,375 @@ +# noVNC API + +The interface of the noVNC client consists of a single RFB object that +is instantiated once per connection. + +## RFB + +The `RFB` object represents a single connection to a VNC server. It +communicates using a WebSocket that must provide a standard RFB +protocol stream. + +### Constructor + +[`RFB()`](#rfb-1) + - Creates and returns a new `RFB` object. + +### Properties + +`viewOnly` + - Is a `boolean` indicating if any events (e.g. key presses or mouse + movement) should be prevented from being sent to the server. + Disabled by default. + +`focusOnClick` + - Is a `boolean` indicating if keyboard focus should automatically be + moved to the remote session when a `mousedown` or `touchstart` + event is received. + +`touchButton` + - Is a `long` controlling the button mask that should be simulated + when a touch event is recieved. Uses the same values as + [`MouseEvent.button`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button). + Is set to `1` by default. + +`clipViewport` + - Is a `boolean` indicating if the remote session should be clipped + to its container. When disabled scrollbars will be shown to handle + the resulting overflow. Disabled by default. + +`dragViewport` + - Is a `boolean` indicating if mouse events should control the + relative position of a clipped remote session. Only relevant if + `clipViewport` is enabled. Disabled by default. + +`scaleViewport` + - Is a `boolean` indicating if the remote session should be scaled + locally so it fits its container. When disabled it will be centered + if the remote session is smaller than its container, or handled + according to `clipViewport` if it is larger. Disabled by default. + +`resizeSession` + - Is a `boolean` indicating if a request to resize the remote session + should be sent whenever the container changes dimensions. Disabled + by default. + +`showDotCursor` + - Is a `boolean` indicating whether a dot cursor should be shown + instead of a zero-sized or fully-transparent cursor if the server + sets such invisible cursor. Disabled by default. + +`background` + - Is a valid CSS [background](https://developer.mozilla.org/en-US/docs/Web/CSS/background) + style value indicating which background style should be applied + to the element containing the remote session screen. The default value is `rgb(40, 40, 40)` + (solid gray color). + +`capabilities` *Read only* + - Is an `Object` indicating which optional extensions are available + on the server. Some methods may only be called if the corresponding + capability is set. The following capabilities are defined: + + | name | type | description + | -------- | --------- | ----------- + | `power` | `boolean` | Machine power control is available + +### Events + +[`connect`](#connect) + - The `connect` event is fired when the `RFB` object has completed + the connection and handshaking with the server. + +[`disconnect`](#disconnected) + - The `disconnect` event is fired when the `RFB` object disconnects. + +[`credentialsrequired`](#credentialsrequired) + - The `credentialsrequired` event is fired when more credentials must + be given to continue. + +[`securityfailure`](#securityfailure) + - The `securityfailure` event is fired when the security negotiation + with the server fails. + +[`clipboard`](#clipboard) + - The `clipboard` event is fired when clipboard data is received from + the server. + +[`bell`](#bell) + - The `bell` event is fired when a audible bell request is received + from the server. + +[`desktopname`](#desktopname) + - The `desktopname` event is fired when the remote desktop name + changes. + +[`capabilities`](#capabilities) + - The `capabilities` event is fired when `RFB.capabilities` is + updated. + +### Methods + +[`RFB.disconnect()`](#rfbdisconnect) + - Disconnect from the server. + +[`RFB.sendCredentials()`](#rfbsendcredentials) + - Send credentials to server. Should be called after the + [`credentialsrequired`](#credentialsrequired) event has fired. + +[`RFB.sendKey()`](#rfbsendKey) + - Send a key event. + +[`RFB.sendCtrlAltDel()`](#rfbsendctrlaltdel) + - Send Ctrl-Alt-Del key sequence. + +[`RFB.focus()`](#rfbfocus) + - Move keyboard focus to the remote session. + +[`RFB.blur()`](#rfbblur) + - Remove keyboard focus from the remote session. + +[`RFB.machineShutdown()`](#rfbmachineshutdown) + - Request a shutdown of the remote machine. + +[`RFB.machineReboot()`](#rfbmachinereboot) + - Request a reboot of the remote machine. + +[`RFB.machineReset()`](#rfbmachinereset) + - Request a reset of the remote machine. + +[`RFB.clipboardPasteFrom()`](#rfbclipboardPasteFrom) + - Send clipboard contents to server. + +### Details + +#### RFB() + +The `RFB()` constructor returns a new `RFB` object and initiates a new +connection to a specified VNC server. + +##### Syntax + + let rfb = new RFB( target, url [, options] ); + +###### Parameters + +**`target`** + - A block [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) + that specifies where the `RFB` object should attach itself. The + existing contents of the `HTMLElement` will be untouched, but new + elements will be added during the lifetime of the `RFB` object. + +**`url`** + - A `DOMString` specifying the VNC server to connect to. This must be + a valid WebSocket URL. + +**`options`** *Optional* + - An `Object` specifying extra details about how the connection + should be made. + + Possible options: + + `shared` + - A `boolean` indicating if the remote server should be shared or + if any other connected clients should be disconnected. Enabled + by default. + + `credentials` + - An `Object` specifying the credentials to provide to the server + when authenticating. The following credentials are possible: + + | name | type | description + | ------------ | ----------- | ----------- + | `"username"` | `DOMString` | The user that authenticates + | `"password"` | `DOMString` | Password for the user + | `"target"` | `DOMString` | Target machine or session + + `repeaterID` + - A `DOMString` specifying the ID to provide to any VNC repeater + encountered. + +#### connect + +The `connect` event is fired after all the handshaking with the server +is completed and the connection is fully established. After this event +the `RFB` object is ready to recieve graphics updates and to send input. + +#### disconnect + +The `disconnect` event is fired when the connection has been +terminated. The `detail` property is an `Object` that contains the +property `clean`. `clean` is a `boolean` indicating if the termination +was clean or not. In the event of an unexpected termination or an error +`clean` will be set to false. + +#### credentialsrequired + +The `credentialsrequired` event is fired when the server requests more +credentials than were specified to [`RFB()`](#rfb-1). The `detail` +property is an `Object` containing the property `types` which is an +`Array` of `DOMString` listing the credentials that are required. + +#### securityfailure + +The `securityfailure` event is fired when the handshaking process with +the server fails during the security negotiation step. The `detail` +property is an `Object` containing the following properties: + +| Property | Type | Description +| -------- | ----------- | ----------- +| `status` | `long` | The failure status code +| `reason` | `DOMString` | The **optional** reason for the failure + +The property `status` corresponds to the +[SecurityResult](https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#securityresult) +status code in cases of failure. A status of zero will not be sent in +this event since that indicates a successful security handshaking +process. The optional property `reason` is provided by the server and +thus the language of the string is not known. However most servers will +probably send English strings. The server can choose to not send a +reason and in these cases the `reason` property will be omitted. + +#### clipboard + +The `clipboard` event is fired when the server has sent clipboard data. +The `detail` property is an `Object` containing the property `text` +which is a `DOMString` with the clipboard data. + +#### bell + +The `bell` event is fired when the server has requested an audible +bell. + +#### desktopname + +The `desktopname` event is fired when the name of the remote desktop +changes. The `detail` property is an `Object` with the property `name` +which is a `DOMString` specifying the new name. + +#### capabilities + +The `capabilities` event is fired whenever an entry is added or removed +from `RFB.capabilities`. The `detail` property is an `Object` with the +property `capabilities` containing the new value of `RFB.capabilities`. + +#### RFB.disconnect() + +The `RFB.disconnect()` method is used to disconnect from the currently +connected server. + +##### Syntax + + RFB.disconnect( ); + +#### RFB.sendCredentials() + +The `RFB.sendCredentials()` method is used to provide the missing +credentials after a `credentialsrequired` event has been fired. + +##### Syntax + + RFB.sendCredentials( credentials ); + +###### Parameters + +**`credentials`** + - An `Object` specifying the credentials to provide to the server + when authenticating. See [`RFB()`](#rfb-1) for details. + +#### RFB.sendKey() + +The `RFB.sendKey()` method is used to send a key event to the server. + +##### Syntax + + RFB.sendKey( keysym, code [, down] ); + +###### Parameters + +**`keysym`** + - A `long` specifying the RFB keysym to send. Can be `0` if a valid + **`code`** is specified. + +**`code`** + - A `DOMString` specifying the physical key to send. Valid values are + those that can be specified to + [`KeyboardEvent.code`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code). + If the physical key cannot be determined then `null` shall be + specified. + +**`down`** *Optional* + - A `boolean` specifying if a press or a release event should be + sent. If omitted then both a press and release event are sent. + +#### RFB.sendCtrlAltDel() + +The `RFB.sendCtrlAltDel()` method is used to send the key sequence +*left Control*, *left Alt*, *Delete*. This is a convenience wrapper +around [`RFB.sendKey()`](#rfbsendkey). + +##### Syntax + + RFB.sendCtrlAltDel( ); + +#### RFB.focus() + +The `RFB.focus()` method sets the keyboard focus on the remote session. +Keyboard events will be sent to the remote server after this point. + +##### Syntax + + RFB.focus( ); + +#### RFB.blur() + +The `RFB.blur()` method remove keyboard focus on the remote session. +Keyboard events will no longer be sent to the remote server after this +point. + +##### Syntax + + RFB.blur( ); + +#### RFB.machineShutdown() + +The `RFB.machineShutdown()` method is used to request to shut down the +remote machine. The capability `power` must be set for this method to +have any effect. + +##### Syntax + + RFB.machineShutdown( ); + +#### RFB.machineReboot() + +The `RFB.machineReboot()` method is used to request a clean reboot of +the remote machine. The capability `power` must be set for this method +to have any effect. + +##### Syntax + + RFB.machineReboot( ); + +#### RFB.machineReset() + +The `RFB.machineReset()` method is used to request a forced reset of +the remote machine. The capability `power` must be set for this method +to have any effect. + +##### Syntax + + RFB.machineReset( ); + +#### RFB.clipboardPasteFrom() + +The `RFB.clipboardPasteFrom()` method is used to send clipboard data +to the remote server. + +##### Syntax + + RFB.clipboardPasteFrom( text ); + +###### Parameters + +**`text`** + - A `DOMString` specifying the clipboard data to send. Currently only + characters from ISO 8859-1 are supported. diff --git a/systemvm/debian/root/noVNC/docs/EMBEDDING.md b/systemvm/debian/root/noVNC/docs/EMBEDDING.md new file mode 100644 index 000000000000..5399b48ba76c --- /dev/null +++ b/systemvm/debian/root/noVNC/docs/EMBEDDING.md @@ -0,0 +1,119 @@ +# Embedding and Deploying noVNC Application + +This document describes how to embed and deploy the noVNC application, which +includes settings and a full user interface. If you are looking for +documentation on how to use the core noVNC library in your own application, +then please see our [library documentation](LIBRARY.md). + +## Files + +The noVNC application consists of the following files and directories: + +* `vnc.html` - The main page for the application and where users should go. It + is possible to rename this file. + +* `app/` - Support files for the application. Contains code, images, styles and + translations. + +* `core/` - The core noVNC library. + +* `vendor/` - Third party support libraries used by the application and the + core library. + +The most basic deployment consists of simply serving these files from a web +server and setting up a WebSocket proxy to the VNC server. + +## Parameters + +The noVNC application can be controlled by including certain settings in the +query string. Currently the following options are available: + +* `autoconnect` - Automatically connect as soon as the page has finished + loading. + +* `reconnect` - If noVNC should automatically reconnect if the connection is + dropped. + +* `reconnect_delay` - How long to wait in milliseconds before attempting to + reconnect. + +* `host` - The WebSocket host to connect to. + +* `port` - The WebSocket port to connect to. + +* `encrypt` - If TLS should be used for the WebSocket connection. + +* `path` - The WebSocket path to use. + +* `password` - The password sent to the server, if required. + +* `repeaterID` - The repeater ID to use if a VNC repeater is detected. + +* `shared` - If other VNC clients should be disconnected when noVNC connects. + +* `bell` - If the keyboard bell should be enabled or not. + +* `view_only` - If the remote session should be in non-interactive mode. + +* `view_clip` - If the remote session should be clipped or use scrollbars if + it cannot fit in the browser. + +* `resize` - How to resize the remote session if it is not the same size as + the browser window. Can be one of `off`, `scale` and `remote`. + +* `show_dot` - If a dot cursor should be shown when the remote server provides + no local cursor, or provides a fully-transparent (invisible) cursor. + +* `logging` - The console log level. Can be one of `error`, `warn`, `info` or + `debug`. + +## Pre-conversion of Modules + +noVNC is written using ECMAScript 6 modules. Many of the major browsers support +these modules natively, but not all. By default the noVNC application includes +a script that can convert these modules to an older format as they are being +loaded. However this process can be slow and severely increases the load time +for the application. + +It is possible to perform this conversion ahead of time, avoiding the extra +load times. To do this please follow these steps: + + 1. Install Node.js + 2. Run `npm install` in the noVNC directory + 3. Run `./utils/use_require.js --with-app --as commonjs` + +This will produce a `build/` directory that includes everything needed to run +the noVNC application. + +## HTTP Serving Considerations +### Browser Cache Issue + +If you serve noVNC files using a web server that provides an ETag header, and +include any options in the query string, a nasty browser cache issue can bite +you on upgrade, resulting in a red error box. The issue is caused by a mismatch +between the new vnc.html (which is reloaded because the user has used it with +new query string after the upgrade) and the old javascript files (that the +browser reuses from its cache). To avoid this issue, the browser must be told +to always revalidate cached files using conditional requests. The correct +semantics are achieved via the (confusingly named) `Cache-Control: no-cache` +header that needs to be provided in the web server responses. + +### Example Server Configurations + +Apache: + +``` + # In the main configuration file + # (Debian/Ubuntu users: use "a2enmod headers" instead) + LoadModule headers_module modules/mod_headers.so + + # In the or block related to noVNC + Header set Cache-Control "no-cache" +``` + +Nginx: + +``` + # In the location block related to noVNC + add_header Cache-Control no-cache; +``` diff --git a/systemvm/debian/root/noVNC/docs/LIBRARY.md b/systemvm/debian/root/noVNC/docs/LIBRARY.md new file mode 100644 index 000000000000..63f55e8f1798 --- /dev/null +++ b/systemvm/debian/root/noVNC/docs/LIBRARY.md @@ -0,0 +1,35 @@ +# Using the noVNC JavaScript library + +This document describes how to make use of the noVNC JavaScript library for +integration in your own VNC client application. If you wish to embed the more +complete noVNC application with its included user interface then please see +our [embedding documentation](EMBEDDING.md). + +## API + +The API of noVNC consists of a single object called `RFB`. The formal +documentation for that object can be found in our [API documentation](API.md). + +## Example + +noVNC includes a small example application called `vnc_lite.html`. This does +not make use of all the features of noVNC, but is a good start to see how to +do things. + +## Conversion of Modules + +noVNC is written using ECMAScript 6 modules. Many of the major browsers support +these modules natively, but not all. They are also not supported by Node.js. To +use noVNC in these places the library must first be converted. + +Fortunately noVNC includes a script to handle this conversion. Please follow +the following steps: + + 1. Install Node.js + 2. Run `npm install` in the noVNC directory + 3. Run `./utils/use_require.js --as ` + +Several module formats are available. Please run +`./utils/use_require.js --help` to see them all. + +The result of the conversion is available in the `lib/` directory. diff --git a/systemvm/debian/root/noVNC/docs/LICENSE.BSD-2-Clause b/systemvm/debian/root/noVNC/docs/LICENSE.BSD-2-Clause new file mode 100644 index 000000000000..9d66ec911bf4 --- /dev/null +++ b/systemvm/debian/root/noVNC/docs/LICENSE.BSD-2-Clause @@ -0,0 +1,22 @@ +Copyright (c) , +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * 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 COPYRIGHT HOLDERS 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 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. diff --git a/systemvm/debian/root/noVNC/docs/LICENSE.BSD-3-Clause b/systemvm/debian/root/noVNC/docs/LICENSE.BSD-3-Clause new file mode 100644 index 000000000000..e160466c4e06 --- /dev/null +++ b/systemvm/debian/root/noVNC/docs/LICENSE.BSD-3-Clause @@ -0,0 +1,24 @@ +Copyright (c) , +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * 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. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 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. diff --git a/systemvm/debian/root/noVNC/docs/LICENSE.MPL-2.0 b/systemvm/debian/root/noVNC/docs/LICENSE.MPL-2.0 new file mode 100644 index 000000000000..14e2f777f6c3 --- /dev/null +++ b/systemvm/debian/root/noVNC/docs/LICENSE.MPL-2.0 @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/systemvm/debian/root/noVNC/docs/LICENSE.OFL-1.1 b/systemvm/debian/root/noVNC/docs/LICENSE.OFL-1.1 new file mode 100644 index 000000000000..77b17316cf1e --- /dev/null +++ b/systemvm/debian/root/noVNC/docs/LICENSE.OFL-1.1 @@ -0,0 +1,91 @@ +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/systemvm/debian/root/noVNC/docs/flash_policy.txt b/systemvm/debian/root/noVNC/docs/flash_policy.txt new file mode 100644 index 000000000000..df325c0ddf50 --- /dev/null +++ b/systemvm/debian/root/noVNC/docs/flash_policy.txt @@ -0,0 +1,4 @@ +Manual setup: + +DATA="echo \'\'" +/usr/bin/socat -T 1 TCP-L:843,reuseaddr,fork,crlf SYSTEM:"$DATA" diff --git a/systemvm/debian/root/noVNC/docs/links b/systemvm/debian/root/noVNC/docs/links new file mode 100644 index 000000000000..31544ce0e12b --- /dev/null +++ b/systemvm/debian/root/noVNC/docs/links @@ -0,0 +1,76 @@ +New tight PNG protocol: + http://wiki.qemu.org/VNC_Tight_PNG + http://xf.iksaif.net/blog/index.php?post/2010/06/14/QEMU:-Tight-PNG-and-some-profiling + +RFB protocol and extensions: + http://tigervnc.org/cgi-bin/rfbproto + +Canvas Browser Compatibility: + http://philip.html5.org/tests/canvas/suite/tests/results.html + +WebSockets API standard: + http://www.whatwg.org/specs/web-apps/current-work/complete.html#websocket + http://dev.w3.org/html5/websockets/ + http://www.ietf.org/id/draft-ietf-hybi-thewebsocketprotocol-00.txt + +Browser Keyboard Events detailed: + http://unixpapa.com/js/key.html + +ActionScript (Flash) WebSocket implementation: + http://github.com/gimite/web-socket-js + +ActionScript (Flash) crypto/TLS library: + http://code.google.com/p/as3crypto + http://github.com/lyokato/as3crypto_patched + +TLS Protocol: + http://en.wikipedia.org/wiki/Transport_Layer_Security + +Generate self-signed certificate: + http://docs.python.org/dev/library/ssl.html#certificates + +Cursor appearance/style (for Cursor pseudo-encoding): + http://en.wikipedia.org/wiki/ICO_(file_format) + http://www.daubnet.com/en/file-format-cur + https://developer.mozilla.org/en/Using_URL_values_for_the_cursor_property + http://www.fileformat.info/format/bmp/egff.htm + +Icon/Cursor file format: + http://msdn.microsoft.com/en-us/library/ms997538 + http://msdn.microsoft.com/en-us/library/aa921550.aspx + http://msdn.microsoft.com/en-us/library/aa930622.aspx + + +RDP Protocol specification: + http://msdn.microsoft.com/en-us/library/cc240445(v=PROT.10).aspx + + +Related projects: + + guacamole: http://guacamole.sourceforge.net/ + + - Web client, but Java servlet does pre-processing + + jsvnc: http://code.google.com/p/jsvnc/ + + - No releases + + webvnc: http://code.google.com/p/webvnc/ + + - Jetty web server gateway, no updates since April 2008. + + RealVNC Java applet: http://www.realvnc.com/support/javavncviewer.html + + - Java applet + + Flashlight-VNC: http://www.wizhelp.com/flashlight-vnc/ + + - Adobe Flash implementation + + FVNC: http://osflash.org/fvnc + + - Adbove Flash implementation + + CanVNC: http://canvnc.sourceforge.net/ + + - HTML client with REST to VNC python proxy. Mostly vapor. diff --git a/systemvm/debian/root/noVNC/docs/notes b/systemvm/debian/root/noVNC/docs/notes new file mode 100644 index 000000000000..dfef0bd6afe0 --- /dev/null +++ b/systemvm/debian/root/noVNC/docs/notes @@ -0,0 +1,5 @@ +Rebuilding inflator.js + +- Download pako from npm +- Install browserify using npm +- browserify core/inflator.mod.js -o core/inflator.js -s Inflator diff --git a/systemvm/debian/root/noVNC/docs/rfb_notes b/systemvm/debian/root/noVNC/docs/rfb_notes new file mode 100644 index 000000000000..643e16c01e72 --- /dev/null +++ b/systemvm/debian/root/noVNC/docs/rfb_notes @@ -0,0 +1,147 @@ +5.1.1 ProtocolVersion: 12, 12 bytes + + - Sent by server, max supported + 12 ascii - "RFB 003.008\n" + - Response by client, version to use + 12 ascii - "RFB 003.003\n" + +5.1.2 Authentication: >=4, [16, 4] bytes + + - Sent by server + CARD32 - authentication-scheme + 0 - connection failed + CARD32 - length + length - reason + 1 - no authentication + + 2 - VNC authentication + 16 CARD8 - challenge (random bytes) + + - Response by client (if VNC authentication) + 16 CARD8 - client encrypts the challenge with DES, using user + password as key, sends resulting 16 byte response + + - Response by server (if VNC authentication) + CARD32 - 0 - OK + 1 - failed + 2 - too-many + +5.1.3 ClientInitialisation: 1 byte + - Sent by client + CARD8 - shared-flag, 0 exclusive, non-zero shared + +5.1.4 ServerInitialisation: >=24 bytes + - Sent by server + CARD16 - framebuffer-width + CARD16 - framebuffer-height + 16 byte PIXEL_FORMAT - server-pixel-format + CARD8 - bits-per-pixel + CARD8 - depth + CARD8 - big-endian-flag, non-zero is big endian + CARD8 - true-color-flag, non-zero then next 6 apply + CARD16 - red-max + CARD16 - green-max + CARD16 - blue-max + CARD8 - red-shift + CARD8 - green-shift + CARD8 - blue-shift + 3 bytes - padding + CARD32 - name-length + + CARD8[length] - name-string + + + +Client to Server Messages: + +5.2.1 SetPixelFormat: 20 bytes + CARD8: 0 - message-type + ... + +5.2.2 FixColourMapEntries: >=6 bytes + CARD8: 1 - message-type + ... + +5.2.3 SetEncodings: >=8 bytes + CARD8: 2 - message-type + CARD8 - padding + CARD16 - numer-of-encodings + + CARD32 - encoding-type in preference order + 0 - raw + 1 - copy-rectangle + 2 - RRE + 4 - CoRRE + 5 - hextile + +5.2.4 FramebufferUpdateRequest (10 bytes) + CARD8: 3 - message-type + CARD8 - incremental (0 for full-update, non-zero for incremental) + CARD16 - x-position + CARD16 - y-position + CARD16 - width + CARD16 - height + + +5.2.5 KeyEvent: 8 bytes + CARD8: 4 - message-type + CARD8 - down-flag + 2 bytes - padding + CARD32 - key (X-Windows keysym values) + +5.2.6 PointerEvent: 6 bytes + CARD8: 5 - message-type + CARD8 - button-mask + CARD16 - x-position + CARD16 - y-position + +5.2.7 ClientCutText: >=9 bytes + CARD8: 6 - message-type + ... + + +Server to Client Messages: + +5.3.1 FramebufferUpdate + CARD8: 0 - message-type + 1 byte - padding + CARD16 - number-of-rectangles + + CARD16 - x-position + CARD16 - y-position + CARD16 - width + CARD16 - height + CARD16 - encoding-type: + 0 - raw + 1 - copy rectangle + 2 - RRE + 4 - CoRRE + 5 - hextile + + raw: + - width x height pixel values + + copy rectangle: + CARD16 - src-x-position + CARD16 - src-y-position + + RRE: + CARD32 - N number-of-subrectangles + Nxd bytes - background-pixel-value (d bits-per-pixel) + + ... + +5.3.2 SetColourMapEntries (no support) + CARD8: 1 - message-type + ... + +5.3.3 Bell + CARD8: 2 - message-type + +5.3.4 ServerCutText + CARD8: 3 - message-type + + + + + diff --git a/systemvm/debian/root/noVNC/docs/rfbproto-3.3.pdf b/systemvm/debian/root/noVNC/docs/rfbproto-3.3.pdf new file mode 100644 index 000000000000..56b876436a9b Binary files /dev/null and b/systemvm/debian/root/noVNC/docs/rfbproto-3.3.pdf differ diff --git a/systemvm/debian/root/noVNC/docs/rfbproto-3.7.pdf b/systemvm/debian/root/noVNC/docs/rfbproto-3.7.pdf new file mode 100644 index 000000000000..1ef54623c179 Binary files /dev/null and b/systemvm/debian/root/noVNC/docs/rfbproto-3.7.pdf differ diff --git a/systemvm/debian/root/noVNC/docs/rfbproto-3.8.pdf b/systemvm/debian/root/noVNC/docs/rfbproto-3.8.pdf new file mode 100644 index 000000000000..8f0730fb7f94 Binary files /dev/null and b/systemvm/debian/root/noVNC/docs/rfbproto-3.8.pdf differ diff --git a/systemvm/debian/root/noVNC/images/error.png b/systemvm/debian/root/noVNC/images/error.png new file mode 100644 index 000000000000..6631294a5938 Binary files /dev/null and b/systemvm/debian/root/noVNC/images/error.png differ diff --git a/systemvm/debian/root/noVNC/images/exclamation.png b/systemvm/debian/root/noVNC/images/exclamation.png new file mode 100644 index 000000000000..c37bd062e60c Binary files /dev/null and b/systemvm/debian/root/noVNC/images/exclamation.png differ diff --git a/systemvm/debian/root/noVNC/images/lock.png b/systemvm/debian/root/noVNC/images/lock.png new file mode 100644 index 000000000000..2ebc4f6f9663 Binary files /dev/null and b/systemvm/debian/root/noVNC/images/lock.png differ diff --git a/systemvm/debian/root/noVNC/images/logo.png b/systemvm/debian/root/noVNC/images/logo.png new file mode 100644 index 000000000000..2e3aae936c41 Binary files /dev/null and b/systemvm/debian/root/noVNC/images/logo.png differ diff --git a/systemvm/debian/root/noVNC/karma.conf.js b/systemvm/debian/root/noVNC/karma.conf.js new file mode 100644 index 000000000000..5cbd7a5de86a --- /dev/null +++ b/systemvm/debian/root/noVNC/karma.conf.js @@ -0,0 +1,134 @@ +// Karma configuration + +module.exports = (config) => { + const customLaunchers = {}; + let browsers = []; + let useSauce = false; + + // use Sauce when running on Travis + if (process.env.TRAVIS_JOB_NUMBER) { + useSauce = true; + } + + if (useSauce && process.env.TEST_BROWSER_NAME && process.env.TEST_BROWSER_NAME != 'PhantomJS') { + const names = process.env.TEST_BROWSER_NAME.split(','); + const platforms = process.env.TEST_BROWSER_OS.split(','); + const versions = process.env.TEST_BROWSER_VERSION + ? process.env.TEST_BROWSER_VERSION.split(',') + : [null]; + + for (let i = 0; i < names.length; i++) { + for (let j = 0; j < platforms.length; j++) { + for (let k = 0; k < versions.length; k++) { + let launcher_name = 'sl_' + platforms[j].replace(/[^a-zA-Z0-9]/g, '') + '_' + names[i]; + if (versions[k]) { + launcher_name += '_' + versions[k]; + } + + customLaunchers[launcher_name] = { + base: 'SauceLabs', + browserName: names[i], + platform: platforms[j], + }; + + if (versions[i]) { + customLaunchers[launcher_name].version = versions[k]; + } + } + } + } + + browsers = Object.keys(customLaunchers); + } else { + useSauce = false; + //browsers = ['PhantomJS']; + browsers = []; + } + + const my_conf = { + + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha', 'sinon-chai'], + + // list of files / patterns to load in the browser (loaded in order) + files: [ + { pattern: 'app/localization.js', included: false }, + { pattern: 'app/webutil.js', included: false }, + { pattern: 'core/**/*.js', included: false }, + { pattern: 'vendor/pako/**/*.js', included: false }, + { pattern: 'vendor/browser-es-module-loader/dist/*.js*', included: false }, + { pattern: 'tests/test.*.js', included: false }, + { pattern: 'tests/fake.*.js', included: false }, + { pattern: 'tests/assertions.js', included: false }, + 'vendor/promise.js', + 'tests/karma-test-main.js', + ], + + client: { + mocha: { + // replace Karma debug page with mocha display + 'reporter': 'html', + 'ui': 'bdd' + } + }, + + // list of files to exclude + exclude: [ + ], + + customLaunchers: customLaunchers, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: browsers, + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['mocha'], + + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: true, + + // Increase timeout in case connection is slow/we run more browsers than possible + // (we currently get 3 for free, and we try to run 7, so it can take a while) + captureTimeout: 240000, + + // similarly to above + browserNoActivityTimeout: 100000, + }; + + if (useSauce) { + my_conf.reporters.push('saucelabs'); + my_conf.captureTimeout = 0; // use SL timeout + my_conf.sauceLabs = { + testName: 'noVNC Tests (all)', + startConnect: false, + tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER + }; + } + + config.set(my_conf); +}; diff --git a/systemvm/debian/root/noVNC/package.json b/systemvm/debian/root/noVNC/package.json new file mode 100644 index 000000000000..2d84a5f38e5f --- /dev/null +++ b/systemvm/debian/root/noVNC/package.json @@ -0,0 +1,81 @@ +{ + "name": "@novnc/novnc", + "version": "1.1.0", + "description": "An HTML5 VNC client", + "browser": "lib/rfb", + "directories": { + "lib": "lib", + "doc": "docs", + "test": "tests" + }, + "files": [ + "lib", + "AUTHORS", + "VERSION", + "docs/API.md", + "docs/LIBRARY.md", + "docs/LICENSE*", + "core", + "vendor/pako" + ], + "scripts": { + "lint": "eslint app core po tests utils", + "test": "karma start karma.conf.js", + "prepublish": "node ./utils/use_require.js --as commonjs --clean" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/novnc/noVNC.git" + }, + "author": "Joel Martin (https://github.com/kanaka)", + "contributors": [ + "Solly Ross (https://github.com/directxman12)", + "Peter Åstrand (https://github.com/astrand)", + "Samuel Mannehed (https://github.com/samhed)", + "Pierre Ossman (https://github.com/CendioOssman)" + ], + "license": "MPL-2.0", + "bugs": { + "url": "https://github.com/novnc/noVNC/issues" + }, + "homepage": "https://github.com/novnc/noVNC", + "devDependencies": { + "babel-core": "^6.22.1", + "babel-plugin-add-module-exports": "^0.2.1", + "babel-plugin-import-redirect": "*", + "babel-plugin-syntax-dynamic-import": "^6.18.0", + "babel-plugin-transform-es2015-modules-amd": "^6.22.0", + "babel-plugin-transform-es2015-modules-commonjs": "^6.18.0", + "babel-plugin-transform-es2015-modules-systemjs": "^6.22.0", + "babel-plugin-transform-es2015-modules-umd": "^6.22.0", + "babel-preset-es2015": "^6.24.1", + "babelify": "^7.3.0", + "browserify": "^13.1.0", + "chai": "^3.5.0", + "commander": "^2.9.0", + "es-module-loader": "^2.1.0", + "eslint": "^4.16.0", + "fs-extra": "^1.0.0", + "jsdom": "*", + "karma": "^1.3.0", + "karma-mocha": "^1.3.0", + "karma-mocha-reporter": "^2.2.0", + "karma-sauce-launcher": "^1.0.0", + "karma-sinon-chai": "^2.0.0", + "mocha": "^3.1.2", + "node-getopt": "*", + "po2json": "*", + "requirejs": "^2.3.2", + "rollup": "^0.41.4", + "rollup-plugin-node-resolve": "^2.0.0", + "sinon": "^4.0.0", + "sinon-chai": "^2.8.0" + }, + "dependencies": {}, + "keywords": [ + "vnc", + "rfb", + "novnc", + "websockify" + ] +} diff --git a/systemvm/debian/root/noVNC/po/Makefile b/systemvm/debian/root/noVNC/po/Makefile new file mode 100644 index 000000000000..6dbd83043f7c --- /dev/null +++ b/systemvm/debian/root/noVNC/po/Makefile @@ -0,0 +1,35 @@ +all: +.PHONY: update-po update-js update-pot + +LINGUAS := cs de el es ko nl pl ru sv tr zh_CN zh_TW + +VERSION := $(shell grep '"version"' ../package.json | cut -d '"' -f 4) + +POFILES := $(addsuffix .po,$(LINGUAS)) +JSONFILES := $(addprefix ../app/locale/,$(addsuffix .json,$(LINGUAS))) + +update-po: $(POFILES) +update-js: $(JSONFILES) + +%.po: noVNC.pot + msgmerge --update --lang=$* $@ $< +../app/locale/%.json: %.po + ./po2js $< $@ + +update-pot: + xgettext --output=noVNC.js.pot \ + --copyright-holder="The noVNC Authors" \ + --package-name="noVNC" \ + --package-version="$(VERSION)" \ + --msgid-bugs-address="novnc@googlegroups.com" \ + --add-comments=TRANSLATORS: \ + --from-code=UTF-8 \ + --sort-by-file \ + ../app/*.js \ + ../core/*.js \ + ../core/input/*.js + ./xgettext-html --output=noVNC.html.pot \ + ../vnc.html + msgcat --output-file=noVNC.pot \ + --sort-by-file noVNC.js.pot noVNC.html.pot + rm -f noVNC.js.pot noVNC.html.pot diff --git a/systemvm/debian/root/noVNC/po/cs.po b/systemvm/debian/root/noVNC/po/cs.po new file mode 100644 index 000000000000..2b1efd8d9183 --- /dev/null +++ b/systemvm/debian/root/noVNC/po/cs.po @@ -0,0 +1,294 @@ +# Czech translations for noVNC package. +# Copyright (C) 2018 The noVNC Authors +# This file is distributed under the same license as the noVNC package. +# Petr , 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: noVNC 1.0.0-testing.2\n" +"Report-Msgid-Bugs-To: novnc@googlegroups.com\n" +"POT-Creation-Date: 2018-10-19 12:00+0200\n" +"PO-Revision-Date: 2018-10-19 12:00+0200\n" +"Last-Translator: Petr \n" +"Language-Team: Czech\n" +"Language: cs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" + +#: ../app/ui.js:389 +msgid "Connecting..." +msgstr "Připojení..." + +#: ../app/ui.js:396 +msgid "Disconnecting..." +msgstr "Odpojení..." + +#: ../app/ui.js:402 +msgid "Reconnecting..." +msgstr "Obnova připojení..." + +#: ../app/ui.js:407 +msgid "Internal error" +msgstr "Vnitřní chyba" + +#: ../app/ui.js:997 +msgid "Must set host" +msgstr "Hostitel musí být nastavení" + +#: ../app/ui.js:1079 +msgid "Connected (encrypted) to " +msgstr "Připojení (šifrované) k " + +#: ../app/ui.js:1081 +msgid "Connected (unencrypted) to " +msgstr "Připojení (nešifrované) k " + +#: ../app/ui.js:1104 +msgid "Something went wrong, connection is closed" +msgstr "Něco se pokazilo, odpojeno" + +#: ../app/ui.js:1107 +msgid "Failed to connect to server" +msgstr "Chyba připojení k serveru" + +#: ../app/ui.js:1117 +msgid "Disconnected" +msgstr "Odpojeno" + +#: ../app/ui.js:1130 +msgid "New connection has been rejected with reason: " +msgstr "Nové připojení bylo odmítnuto s odůvodněním: " + +#: ../app/ui.js:1133 +msgid "New connection has been rejected" +msgstr "Nové připojení bylo odmítnuto" + +#: ../app/ui.js:1153 +msgid "Password is required" +msgstr "Je vyžadováno heslo" + +#: ../vnc.html:84 +msgid "noVNC encountered an error:" +msgstr "noVNC narazilo na chybu:" + +#: ../vnc.html:94 +msgid "Hide/Show the control bar" +msgstr "Skrýt/zobrazit ovládací panel" + +#: ../vnc.html:101 +msgid "Move/Drag Viewport" +msgstr "Přesunout/přetáhnout výřez" + +#: ../vnc.html:101 +msgid "viewport drag" +msgstr "přesun výřezu" + +#: ../vnc.html:107 ../vnc.html:110 ../vnc.html:113 ../vnc.html:116 +msgid "Active Mouse Button" +msgstr "Aktivní tlačítka myši" + +#: ../vnc.html:107 +msgid "No mousebutton" +msgstr "Žádné" + +#: ../vnc.html:110 +msgid "Left mousebutton" +msgstr "Levé tlačítko myši" + +#: ../vnc.html:113 +msgid "Middle mousebutton" +msgstr "Prostřední tlačítko myši" + +#: ../vnc.html:116 +msgid "Right mousebutton" +msgstr "Pravé tlačítko myši" + +#: ../vnc.html:119 +msgid "Keyboard" +msgstr "Klávesnice" + +#: ../vnc.html:119 +msgid "Show Keyboard" +msgstr "Zobrazit klávesnici" + +#: ../vnc.html:126 +msgid "Extra keys" +msgstr "Extra klávesy" + +#: ../vnc.html:126 +msgid "Show Extra Keys" +msgstr "Zobrazit extra klávesy" + +#: ../vnc.html:131 +msgid "Ctrl" +msgstr "Ctrl" + +#: ../vnc.html:131 +msgid "Toggle Ctrl" +msgstr "Přepnout Ctrl" + +#: ../vnc.html:134 +msgid "Alt" +msgstr "Alt" + +#: ../vnc.html:134 +msgid "Toggle Alt" +msgstr "Přepnout Alt" + +#: ../vnc.html:137 +msgid "Send Tab" +msgstr "Odeslat tabulátor" + +#: ../vnc.html:137 +msgid "Tab" +msgstr "Tab" + +#: ../vnc.html:140 +msgid "Esc" +msgstr "Esc" + +#: ../vnc.html:140 +msgid "Send Escape" +msgstr "Odeslat Esc" + +#: ../vnc.html:143 +msgid "Ctrl+Alt+Del" +msgstr "Ctrl+Alt+Del" + +#: ../vnc.html:143 +msgid "Send Ctrl-Alt-Del" +msgstr "Poslat Ctrl-Alt-Del" + +#: ../vnc.html:151 +msgid "Shutdown/Reboot" +msgstr "Vypnutí/Restart" + +#: ../vnc.html:151 +msgid "Shutdown/Reboot..." +msgstr "Vypnutí/Restart..." + +#: ../vnc.html:157 +msgid "Power" +msgstr "Napájení" + +#: ../vnc.html:159 +msgid "Shutdown" +msgstr "Vypnout" + +#: ../vnc.html:160 +msgid "Reboot" +msgstr "Restart" + +#: ../vnc.html:161 +msgid "Reset" +msgstr "Reset" + +#: ../vnc.html:166 ../vnc.html:172 +msgid "Clipboard" +msgstr "Schránka" + +#: ../vnc.html:176 +msgid "Clear" +msgstr "Vymazat" + +#: ../vnc.html:182 +msgid "Fullscreen" +msgstr "Celá obrazovka" + +#: ../vnc.html:187 ../vnc.html:194 +msgid "Settings" +msgstr "Nastavení" + +#: ../vnc.html:197 +msgid "Shared Mode" +msgstr "Sdílený režim" + +#: ../vnc.html:200 +msgid "View Only" +msgstr "Pouze prohlížení" + +#: ../vnc.html:204 +msgid "Clip to Window" +msgstr "Přizpůsobit oknu" + +#: ../vnc.html:207 +msgid "Scaling Mode:" +msgstr "Přizpůsobení velikosti" + +#: ../vnc.html:209 +msgid "None" +msgstr "Žádné" + +#: ../vnc.html:210 +msgid "Local Scaling" +msgstr "Místní" + +#: ../vnc.html:211 +msgid "Remote Resizing" +msgstr "Vzdálené" + +#: ../vnc.html:216 +msgid "Advanced" +msgstr "Pokročilé" + +#: ../vnc.html:219 +msgid "Repeater ID:" +msgstr "ID opakovače" + +#: ../vnc.html:223 +msgid "WebSocket" +msgstr "WebSocket" + +#: ../vnc.html:226 +msgid "Encrypt" +msgstr "Šifrování:" + +#: ../vnc.html:229 +msgid "Host:" +msgstr "Hostitel:" + +#: ../vnc.html:233 +msgid "Port:" +msgstr "Port:" + +#: ../vnc.html:237 +msgid "Path:" +msgstr "Cesta" + +#: ../vnc.html:244 +msgid "Automatic Reconnect" +msgstr "Automatická obnova připojení" + +#: ../vnc.html:247 +msgid "Reconnect Delay (ms):" +msgstr "Zpoždění připojení (ms)" + +#: ../vnc.html:252 +msgid "Show Dot when No Cursor" +msgstr "Tečka místo chybějícího kurzoru myši" + +#: ../vnc.html:257 +msgid "Logging:" +msgstr "Logování:" + +#: ../vnc.html:269 +msgid "Disconnect" +msgstr "Odpojit" + +#: ../vnc.html:288 +msgid "Connect" +msgstr "Připojit" + +#: ../vnc.html:298 +msgid "Password:" +msgstr "Heslo" + +#: ../vnc.html:302 +msgid "Send Password" +msgstr "Odeslat heslo" + +#: ../vnc.html:312 +msgid "Cancel" +msgstr "Zrušit" diff --git a/systemvm/debian/root/noVNC/po/de.po b/systemvm/debian/root/noVNC/po/de.po new file mode 100644 index 000000000000..0c3fa0d482a1 --- /dev/null +++ b/systemvm/debian/root/noVNC/po/de.po @@ -0,0 +1,303 @@ +# German translations for noVNC package +# German translation for noVNC. +# Copyright (C) 2018 The noVNC Authors +# This file is distributed under the same license as the noVNC package. +# Loek Janssen , 2016. +# +msgid "" +msgstr "" +"Project-Id-Version: noVNC 0.6.1\n" +"Report-Msgid-Bugs-To: novnc@googlegroups.com\n" +"POT-Creation-Date: 2017-11-24 07:16+0000\n" +"PO-Revision-Date: 2017-11-24 08:20+0100\n" +"Last-Translator: Dominik Csapak \n" +"Language-Team: none\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 1.8.11\n" + +#: ../app/ui.js:404 +msgid "Connecting..." +msgstr "Verbinden..." + +#: ../app/ui.js:411 +msgid "Disconnecting..." +msgstr "Verbindung trennen..." + +#: ../app/ui.js:417 +msgid "Reconnecting..." +msgstr "Verbindung wiederherstellen..." + +#: ../app/ui.js:422 +msgid "Internal error" +msgstr "Interner Fehler" + +#: ../app/ui.js:1019 +msgid "Must set host" +msgstr "Richten Sie den Server ein" + +#: ../app/ui.js:1099 +msgid "Connected (encrypted) to " +msgstr "Verbunden mit (verschlüsselt) " + +#: ../app/ui.js:1101 +msgid "Connected (unencrypted) to " +msgstr "Verbunden mit (unverschlüsselt) " + +#: ../app/ui.js:1119 +msgid "Something went wrong, connection is closed" +msgstr "Etwas lief schief, Verbindung wurde getrennt" + +#: ../app/ui.js:1129 +msgid "Disconnected" +msgstr "Verbindung zum Server getrennt" + +#: ../app/ui.js:1142 +msgid "New connection has been rejected with reason: " +msgstr "Verbindung wurde aus folgendem Grund abgelehnt: " + +#: ../app/ui.js:1145 +msgid "New connection has been rejected" +msgstr "Verbindung wurde abgelehnt" + +#: ../app/ui.js:1166 +msgid "Password is required" +msgstr "Passwort ist erforderlich" + +#: ../vnc.html:89 +msgid "noVNC encountered an error:" +msgstr "Ein Fehler ist aufgetreten:" + +#: ../vnc.html:99 +msgid "Hide/Show the control bar" +msgstr "Kontrollleiste verstecken/anzeigen" + +#: ../vnc.html:106 +msgid "Move/Drag Viewport" +msgstr "Ansichtsfenster verschieben/ziehen" + +#: ../vnc.html:106 +msgid "viewport drag" +msgstr "Ansichtsfenster ziehen" + +#: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121 +msgid "Active Mouse Button" +msgstr "Aktive Maustaste" + +#: ../vnc.html:112 +msgid "No mousebutton" +msgstr "Keine Maustaste" + +#: ../vnc.html:115 +msgid "Left mousebutton" +msgstr "Linke Maustaste" + +#: ../vnc.html:118 +msgid "Middle mousebutton" +msgstr "Mittlere Maustaste" + +#: ../vnc.html:121 +msgid "Right mousebutton" +msgstr "Rechte Maustaste" + +#: ../vnc.html:124 +msgid "Keyboard" +msgstr "Tastatur" + +#: ../vnc.html:124 +msgid "Show Keyboard" +msgstr "Tastatur anzeigen" + +#: ../vnc.html:131 +msgid "Extra keys" +msgstr "Zusatztasten" + +#: ../vnc.html:131 +msgid "Show Extra Keys" +msgstr "Zusatztasten anzeigen" + +#: ../vnc.html:136 +msgid "Ctrl" +msgstr "Strg" + +#: ../vnc.html:136 +msgid "Toggle Ctrl" +msgstr "Strg umschalten" + +#: ../vnc.html:139 +msgid "Alt" +msgstr "Alt" + +#: ../vnc.html:139 +msgid "Toggle Alt" +msgstr "Alt umschalten" + +#: ../vnc.html:142 +msgid "Send Tab" +msgstr "Tab senden" + +#: ../vnc.html:142 +msgid "Tab" +msgstr "Tab" + +#: ../vnc.html:145 +msgid "Esc" +msgstr "Esc" + +#: ../vnc.html:145 +msgid "Send Escape" +msgstr "Escape senden" + +#: ../vnc.html:148 +msgid "Ctrl+Alt+Del" +msgstr "Strg+Alt+Entf" + +#: ../vnc.html:148 +msgid "Send Ctrl-Alt-Del" +msgstr "Strg+Alt+Entf senden" + +#: ../vnc.html:156 +msgid "Shutdown/Reboot" +msgstr "Herunterfahren/Neustarten" + +#: ../vnc.html:156 +msgid "Shutdown/Reboot..." +msgstr "Herunterfahren/Neustarten..." + +#: ../vnc.html:162 +msgid "Power" +msgstr "Energie" + +#: ../vnc.html:164 +msgid "Shutdown" +msgstr "Herunterfahren" + +#: ../vnc.html:165 +msgid "Reboot" +msgstr "Neustarten" + +#: ../vnc.html:166 +msgid "Reset" +msgstr "Zurücksetzen" + +#: ../vnc.html:171 ../vnc.html:177 +msgid "Clipboard" +msgstr "Zwischenablage" + +#: ../vnc.html:181 +msgid "Clear" +msgstr "Löschen" + +#: ../vnc.html:187 +msgid "Fullscreen" +msgstr "Vollbild" + +#: ../vnc.html:192 ../vnc.html:199 +msgid "Settings" +msgstr "Einstellungen" + +#: ../vnc.html:202 +msgid "Shared Mode" +msgstr "Geteilter Modus" + +#: ../vnc.html:205 +msgid "View Only" +msgstr "Nur betrachten" + +#: ../vnc.html:209 +msgid "Clip to Window" +msgstr "Auf Fenster begrenzen" + +#: ../vnc.html:212 +msgid "Scaling Mode:" +msgstr "Skalierungsmodus:" + +#: ../vnc.html:214 +msgid "None" +msgstr "Keiner" + +#: ../vnc.html:215 +msgid "Local Scaling" +msgstr "Lokales skalieren" + +#: ../vnc.html:216 +msgid "Remote Resizing" +msgstr "Serverseitiges skalieren" + +#: ../vnc.html:221 +msgid "Advanced" +msgstr "Erweitert" + +#: ../vnc.html:224 +msgid "Repeater ID:" +msgstr "Repeater ID:" + +#: ../vnc.html:228 +msgid "WebSocket" +msgstr "WebSocket" + +#: ../vnc.html:231 +msgid "Encrypt" +msgstr "Verschlüsselt" + +#: ../vnc.html:234 +msgid "Host:" +msgstr "Server:" + +#: ../vnc.html:238 +msgid "Port:" +msgstr "Port:" + +#: ../vnc.html:242 +msgid "Path:" +msgstr "Pfad:" + +#: ../vnc.html:249 +msgid "Automatic Reconnect" +msgstr "Automatisch wiederverbinden" + +#: ../vnc.html:252 +msgid "Reconnect Delay (ms):" +msgstr "Wiederverbindungsverzögerung (ms):" + +#: ../vnc.html:258 +msgid "Logging:" +msgstr "Protokollierung:" + +#: ../vnc.html:270 +msgid "Disconnect" +msgstr "Verbindung trennen" + +#: ../vnc.html:289 +msgid "Connect" +msgstr "Verbinden" + +#: ../vnc.html:299 +msgid "Password:" +msgstr "Passwort:" + +#: ../vnc.html:313 +msgid "Cancel" +msgstr "Abbrechen" + +#: ../vnc.html:329 +msgid "Canvas not supported." +msgstr "Canvas nicht unterstützt." + +#~ msgid "Disconnect timeout" +#~ msgstr "Zeitüberschreitung beim Trennen" + +#~ msgid "Local Downscaling" +#~ msgstr "Lokales herunterskalieren" + +#~ msgid "Local Cursor" +#~ msgstr "Lokaler Mauszeiger" + +#~ msgid "Forcing clipping mode since scrollbars aren't supported by IE in fullscreen" +#~ msgstr "'Clipping-Modus' aktiviert, Scrollbalken in 'IE-Vollbildmodus' werden nicht unterstützt" + +#~ msgid "True Color" +#~ msgstr "True Color" diff --git a/systemvm/debian/root/noVNC/po/el.po b/systemvm/debian/root/noVNC/po/el.po new file mode 100644 index 000000000000..5213ae5423ab --- /dev/null +++ b/systemvm/debian/root/noVNC/po/el.po @@ -0,0 +1,323 @@ +# Greek translations for noVNC package. +# Copyright (C) 2018 The noVNC Authors +# This file is distributed under the same license as the noVNC package. +# Giannis Kosmas , 2016. +# +msgid "" +msgstr "" +"Project-Id-Version: noVNC 0.6.1\n" +"Report-Msgid-Bugs-To: novnc@googlegroups.com\n" +"POT-Creation-Date: 2017-11-17 21:40+0200\n" +"PO-Revision-Date: 2017-10-11 16:16+0200\n" +"Last-Translator: Giannis Kosmas \n" +"Language-Team: none\n" +"Language: el\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: ../app/ui.js:404 +msgid "Connecting..." +msgstr "Συνδέεται..." + +#: ../app/ui.js:411 +msgid "Disconnecting..." +msgstr "Aποσυνδέεται..." + +#: ../app/ui.js:417 +msgid "Reconnecting..." +msgstr "Επανασυνδέεται..." + +#: ../app/ui.js:422 +msgid "Internal error" +msgstr "Εσωτερικό σφάλμα" + +#: ../app/ui.js:1019 +msgid "Must set host" +msgstr "Πρέπει να οριστεί ο διακομιστής" + +#: ../app/ui.js:1099 +msgid "Connected (encrypted) to " +msgstr "Συνδέθηκε (κρυπτογραφημένα) με το " + +#: ../app/ui.js:1101 +msgid "Connected (unencrypted) to " +msgstr "Συνδέθηκε (μη κρυπτογραφημένα) με το " + +#: ../app/ui.js:1119 +msgid "Something went wrong, connection is closed" +msgstr "Κάτι πήγε στραβά, η σύνδεση διακόπηκε" + +#: ../app/ui.js:1129 +msgid "Disconnected" +msgstr "Αποσυνδέθηκε" + +#: ../app/ui.js:1142 +msgid "New connection has been rejected with reason: " +msgstr "Η νέα σύνδεση απορρίφθηκε διότι: " + +#: ../app/ui.js:1145 +msgid "New connection has been rejected" +msgstr "Η νέα σύνδεση απορρίφθηκε " + +#: ../app/ui.js:1166 +msgid "Password is required" +msgstr "Απαιτείται ο κωδικός πρόσβασης" + +#: ../vnc.html:89 +msgid "noVNC encountered an error:" +msgstr "το noVNC αντιμετώπισε ένα σφάλμα:" + +#: ../vnc.html:99 +msgid "Hide/Show the control bar" +msgstr "Απόκρυψη/Εμφάνιση γραμμής ελέγχου" + +#: ../vnc.html:106 +msgid "Move/Drag Viewport" +msgstr "Μετακίνηση/Σύρσιμο Θεατού πεδίου" + +#: ../vnc.html:106 +msgid "viewport drag" +msgstr "σύρσιμο θεατού πεδίου" + +#: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121 +msgid "Active Mouse Button" +msgstr "Ενεργό Πλήκτρο Ποντικιού" + +#: ../vnc.html:112 +msgid "No mousebutton" +msgstr "Χωρίς Πλήκτρο Ποντικιού" + +#: ../vnc.html:115 +msgid "Left mousebutton" +msgstr "Αριστερό Πλήκτρο Ποντικιού" + +#: ../vnc.html:118 +msgid "Middle mousebutton" +msgstr "Μεσαίο Πλήκτρο Ποντικιού" + +#: ../vnc.html:121 +msgid "Right mousebutton" +msgstr "Δεξί Πλήκτρο Ποντικιού" + +#: ../vnc.html:124 +msgid "Keyboard" +msgstr "Πληκτρολόγιο" + +#: ../vnc.html:124 +msgid "Show Keyboard" +msgstr "Εμφάνιση Πληκτρολογίου" + +#: ../vnc.html:131 +msgid "Extra keys" +msgstr "Επιπλέον πλήκτρα" + +#: ../vnc.html:131 +msgid "Show Extra Keys" +msgstr "Εμφάνιση Επιπλέον Πλήκτρων" + +#: ../vnc.html:136 +msgid "Ctrl" +msgstr "Ctrl" + +#: ../vnc.html:136 +msgid "Toggle Ctrl" +msgstr "Εναλλαγή Ctrl" + +#: ../vnc.html:139 +msgid "Alt" +msgstr "Alt" + +#: ../vnc.html:139 +msgid "Toggle Alt" +msgstr "Εναλλαγή Alt" + +#: ../vnc.html:142 +msgid "Send Tab" +msgstr "Αποστολή Tab" + +#: ../vnc.html:142 +msgid "Tab" +msgstr "Tab" + +#: ../vnc.html:145 +msgid "Esc" +msgstr "Esc" + +#: ../vnc.html:145 +msgid "Send Escape" +msgstr "Αποστολή Escape" + +#: ../vnc.html:148 +msgid "Ctrl+Alt+Del" +msgstr "Ctrl+Alt+Del" + +#: ../vnc.html:148 +msgid "Send Ctrl-Alt-Del" +msgstr "Αποστολή Ctrl-Alt-Del" + +#: ../vnc.html:156 +msgid "Shutdown/Reboot" +msgstr "Κλείσιμο/Επανεκκίνηση" + +#: ../vnc.html:156 +msgid "Shutdown/Reboot..." +msgstr "Κλείσιμο/Επανεκκίνηση..." + +#: ../vnc.html:162 +msgid "Power" +msgstr "Απενεργοποίηση" + +#: ../vnc.html:164 +msgid "Shutdown" +msgstr "Κλείσιμο" + +#: ../vnc.html:165 +msgid "Reboot" +msgstr "Επανεκκίνηση" + +#: ../vnc.html:166 +msgid "Reset" +msgstr "Επαναφορά" + +#: ../vnc.html:171 ../vnc.html:177 +msgid "Clipboard" +msgstr "Πρόχειρο" + +#: ../vnc.html:181 +msgid "Clear" +msgstr "Καθάρισμα" + +#: ../vnc.html:187 +msgid "Fullscreen" +msgstr "Πλήρης Οθόνη" + +#: ../vnc.html:192 ../vnc.html:199 +msgid "Settings" +msgstr "Ρυθμίσεις" + +#: ../vnc.html:202 +msgid "Shared Mode" +msgstr "Κοινόχρηστη Λειτουργία" + +#: ../vnc.html:205 +msgid "View Only" +msgstr "Μόνο Θέαση" + +#: ../vnc.html:209 +msgid "Clip to Window" +msgstr "Αποκοπή στο όριο του Παράθυρου" + +#: ../vnc.html:212 +msgid "Scaling Mode:" +msgstr "Λειτουργία Κλιμάκωσης:" + +#: ../vnc.html:214 +msgid "None" +msgstr "Καμία" + +#: ../vnc.html:215 +msgid "Local Scaling" +msgstr "Τοπική Κλιμάκωση" + +#: ../vnc.html:216 +msgid "Remote Resizing" +msgstr "Απομακρυσμένη Αλλαγή μεγέθους" + +#: ../vnc.html:221 +msgid "Advanced" +msgstr "Για προχωρημένους" + +#: ../vnc.html:224 +msgid "Repeater ID:" +msgstr "Repeater ID:" + +#: ../vnc.html:228 +msgid "WebSocket" +msgstr "WebSocket" + +#: ../vnc.html:231 +msgid "Encrypt" +msgstr "Κρυπτογράφηση" + +#: ../vnc.html:234 +msgid "Host:" +msgstr "Όνομα διακομιστή:" + +#: ../vnc.html:238 +msgid "Port:" +msgstr "Πόρτα διακομιστή:" + +#: ../vnc.html:242 +msgid "Path:" +msgstr "Διαδρομή:" + +#: ../vnc.html:249 +msgid "Automatic Reconnect" +msgstr "Αυτόματη επανασύνδεση" + +#: ../vnc.html:252 +msgid "Reconnect Delay (ms):" +msgstr "Καθυστέρηση επανασύνδεσης (ms):" + +#: ../vnc.html:258 +msgid "Logging:" +msgstr "Καταγραφή:" + +#: ../vnc.html:270 +msgid "Disconnect" +msgstr "Αποσύνδεση" + +#: ../vnc.html:289 +msgid "Connect" +msgstr "Σύνδεση" + +#: ../vnc.html:299 +msgid "Password:" +msgstr "Κωδικός Πρόσβασης:" + +#: ../vnc.html:313 +msgid "Cancel" +msgstr "Ακύρωση" + +#: ../vnc.html:329 +msgid "Canvas not supported." +msgstr "Δεν υποστηρίζεται το στοιχείο Canvas" + +#~ msgid "Disconnect timeout" +#~ msgstr "Παρέλευση χρονικού ορίου αποσύνδεσης" + +#~ msgid "Local Downscaling" +#~ msgstr "Τοπική Συρρίκνωση" + +#~ msgid "Local Cursor" +#~ msgstr "Τοπικός Δρομέας" + +#~ msgid "" +#~ "Forcing clipping mode since scrollbars aren't supported by IE in " +#~ "fullscreen" +#~ msgstr "" +#~ "Εφαρμογή λειτουργίας αποκοπής αφού δεν υποστηρίζονται οι λωρίδες κύλισης " +#~ "σε πλήρη οθόνη στον IE" + +#~ msgid "True Color" +#~ msgstr "Πραγματικά Χρώματα" + +#~ msgid "Style:" +#~ msgstr "Στυλ:" + +#~ msgid "default" +#~ msgstr "προεπιλεγμένο" + +#~ msgid "Apply" +#~ msgstr "Εφαρμογή" + +#~ msgid "Connection" +#~ msgstr "Σύνδεση" + +#~ msgid "Token:" +#~ msgstr "Διακριτικό:" + +#~ msgid "Send Password" +#~ msgstr "Αποστολή Κωδικού Πρόσβασης" diff --git a/systemvm/debian/root/noVNC/po/es.po b/systemvm/debian/root/noVNC/po/es.po new file mode 100644 index 000000000000..e15655fbfc9e --- /dev/null +++ b/systemvm/debian/root/noVNC/po/es.po @@ -0,0 +1,283 @@ +# Spanish translations for noVNC package +# Traducciones al español para el paquete noVNC. +# Copyright (C) 2018 The noVNC Authors +# This file is distributed under the same license as the noVNC package. +# Juanjo Diaz , 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: noVNC 1.0.0-testing.2\n" +"Report-Msgid-Bugs-To: novnc@googlegroups.com\n" +"POT-Creation-Date: 2017-10-06 10:07+0200\n" +"PO-Revision-Date: 2018-01-30 19:14-0800\n" +"Last-Translator: Juanjo Diaz \n" +"Language-Team: Spanish\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: ../app/ui.js:430 +msgid "Connecting..." +msgstr "Conectando..." + +#: ../app/ui.js:438 +msgid "Connected (encrypted) to " +msgstr "Conectado (con encriptación) a" + +#: ../app/ui.js:440 +msgid "Connected (unencrypted) to " +msgstr "Conectado (sin encriptación) a" + +#: ../app/ui.js:446 +msgid "Disconnecting..." +msgstr "Desconectando..." + +#: ../app/ui.js:450 +msgid "Disconnected" +msgstr "Desconectado" + +#: ../app/ui.js:1052 ../core/rfb.js:248 +msgid "Must set host" +msgstr "Debes configurar el host" + +#: ../app/ui.js:1101 +msgid "Reconnecting..." +msgstr "Reconectando..." + +#: ../app/ui.js:1140 +msgid "Password is required" +msgstr "Contraseña es obligatoria" + +#: ../core/rfb.js:548 +msgid "Disconnect timeout" +msgstr "Tiempo de desconexión agotado" + +#: ../vnc.html:89 +msgid "noVNC encountered an error:" +msgstr "noVNC ha encontrado un error:" + +#: ../vnc.html:99 +msgid "Hide/Show the control bar" +msgstr "Ocultar/Mostrar la barra de control" + +#: ../vnc.html:106 +msgid "Move/Drag Viewport" +msgstr "Mover/Arrastrar la ventana" + +#: ../vnc.html:106 +msgid "viewport drag" +msgstr "Arrastrar la ventana" + +#: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121 +msgid "Active Mouse Button" +msgstr "Botón activo del ratón" + +#: ../vnc.html:112 +msgid "No mousebutton" +msgstr "Ningún botón del ratón" + +#: ../vnc.html:115 +msgid "Left mousebutton" +msgstr "Botón izquierdo del ratón" + +#: ../vnc.html:118 +msgid "Middle mousebutton" +msgstr "Botón central del ratón" + +#: ../vnc.html:121 +msgid "Right mousebutton" +msgstr "Botón derecho del ratón" + +#: ../vnc.html:124 +msgid "Keyboard" +msgstr "Teclado" + +#: ../vnc.html:124 +msgid "Show Keyboard" +msgstr "Mostrar teclado" + +#: ../vnc.html:131 +msgid "Extra keys" +msgstr "Teclas adicionales" + +#: ../vnc.html:131 +msgid "Show Extra Keys" +msgstr "Mostrar Teclas Adicionales" + +#: ../vnc.html:136 +msgid "Ctrl" +msgstr "Ctrl" + +#: ../vnc.html:136 +msgid "Toggle Ctrl" +msgstr "Pulsar/Soltar Ctrl" + +#: ../vnc.html:139 +msgid "Alt" +msgstr "Alt" + +#: ../vnc.html:139 +msgid "Toggle Alt" +msgstr "Pulsar/Soltar Alt" + +#: ../vnc.html:142 +msgid "Send Tab" +msgstr "Enviar Tabulación" + +#: ../vnc.html:142 +msgid "Tab" +msgstr "Tabulación" + +#: ../vnc.html:145 +msgid "Esc" +msgstr "Esc" + +#: ../vnc.html:145 +msgid "Send Escape" +msgstr "Enviar Escape" + +#: ../vnc.html:148 +msgid "Ctrl+Alt+Del" +msgstr "Ctrl+Alt+Del" + +#: ../vnc.html:148 +msgid "Send Ctrl-Alt-Del" +msgstr "Enviar Ctrl+Alt+Del" + +#: ../vnc.html:156 +msgid "Shutdown/Reboot" +msgstr "Apagar/Reiniciar" + +#: ../vnc.html:156 +msgid "Shutdown/Reboot..." +msgstr "Apagar/Reiniciar..." + +#: ../vnc.html:162 +msgid "Power" +msgstr "Encender" + +#: ../vnc.html:164 +msgid "Shutdown" +msgstr "Apagar" + +#: ../vnc.html:165 +msgid "Reboot" +msgstr "Reiniciar" + +#: ../vnc.html:166 +msgid "Reset" +msgstr "Restablecer" + +#: ../vnc.html:171 ../vnc.html:177 +msgid "Clipboard" +msgstr "Portapapeles" + +#: ../vnc.html:181 +msgid "Clear" +msgstr "Vaciar" + +#: ../vnc.html:187 +msgid "Fullscreen" +msgstr "Pantalla Completa" + +#: ../vnc.html:192 ../vnc.html:199 +msgid "Settings" +msgstr "Configuraciones" + +#: ../vnc.html:202 +msgid "Shared Mode" +msgstr "Modo Compartido" + +#: ../vnc.html:205 +msgid "View Only" +msgstr "Solo visualización" + +#: ../vnc.html:209 +msgid "Clip to Window" +msgstr "Recortar al tamaño de la ventana" + +#: ../vnc.html:212 +msgid "Scaling Mode:" +msgstr "Modo de escalado:" + +#: ../vnc.html:214 +msgid "None" +msgstr "Ninguno" + +#: ../vnc.html:215 +msgid "Local Scaling" +msgstr "Escalado Local" + +#: ../vnc.html:216 +msgid "Local Downscaling" +msgstr "Reducción de escala local" + +#: ../vnc.html:217 +msgid "Remote Resizing" +msgstr "Cambio de tamaño remoto" + +#: ../vnc.html:222 +msgid "Advanced" +msgstr "Avanzado" + +#: ../vnc.html:225 +msgid "Local Cursor" +msgstr "Cursor Local" + +#: ../vnc.html:229 +msgid "Repeater ID:" +msgstr "ID del Repetidor" + +#: ../vnc.html:233 +msgid "WebSocket" +msgstr "WebSocket" + +#: ../vnc.html:236 +msgid "Encrypt" +msgstr "" + +#: ../vnc.html:239 +msgid "Host:" +msgstr "Host" + +#: ../vnc.html:243 +msgid "Port:" +msgstr "Puesto" + +#: ../vnc.html:247 +msgid "Path:" +msgstr "Ruta" + +#: ../vnc.html:254 +msgid "Automatic Reconnect" +msgstr "Reconexión automática" + +#: ../vnc.html:257 +msgid "Reconnect Delay (ms):" +msgstr "Retraso en la reconexión (ms)" + +#: ../vnc.html:263 +msgid "Logging:" +msgstr "Logging" + +#: ../vnc.html:275 +msgid "Disconnect" +msgstr "Desconectar" + +#: ../vnc.html:294 +msgid "Connect" +msgstr "Conectar" + +#: ../vnc.html:304 +msgid "Password:" +msgstr "Contraseña" + +#: ../vnc.html:318 +msgid "Cancel" +msgstr "Cancelar" + +#: ../vnc.html:334 +msgid "Canvas not supported." +msgstr "Canvas no está soportado" diff --git a/systemvm/debian/root/noVNC/po/ko.po b/systemvm/debian/root/noVNC/po/ko.po new file mode 100644 index 000000000000..87ae1069741d --- /dev/null +++ b/systemvm/debian/root/noVNC/po/ko.po @@ -0,0 +1,290 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2018 The noVNC Authors +# This file is distributed under the same license as the noVNC package. +# Baw Appie , 2018. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: noVNC 1.0.0-testing.2\n" +"Report-Msgid-Bugs-To: novnc@googlegroups.com\n" +"POT-Creation-Date: 2018-01-31 16:29+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Baw Appie \n" +"Language-Team: Korean\n" +"Language: ko\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: ../app/ui.js:395 +msgid "Connecting..." +msgstr "연결중..." + +#: ../app/ui.js:402 +msgid "Disconnecting..." +msgstr "연결 해제중..." + +#: ../app/ui.js:408 +msgid "Reconnecting..." +msgstr "재연결중..." + +#: ../app/ui.js:413 +msgid "Internal error" +msgstr "내부 오류" + +#: ../app/ui.js:1002 +msgid "Must set host" +msgstr "호스트는 설정되어야 합니다." + +#: ../app/ui.js:1083 +msgid "Connected (encrypted) to " +msgstr "다음과 (암호화되어) 연결되었습니다:" + +#: ../app/ui.js:1085 +msgid "Connected (unencrypted) to " +msgstr "다음과 (암호화 없이) 연결되었습니다:" + +#: ../app/ui.js:1108 +msgid "Something went wrong, connection is closed" +msgstr "무언가 잘못되었습니다, 연결이 닫혔습니다." + +#: ../app/ui.js:1111 +msgid "Failed to connect to server" +msgstr "서버에 연결하지 못했습니다." + +#: ../app/ui.js:1121 +msgid "Disconnected" +msgstr "연결이 해제되었습니다." + +#: ../app/ui.js:1134 +msgid "New connection has been rejected with reason: " +msgstr "새 연결이 다음 이유로 거부되었습니다:" + +#: ../app/ui.js:1137 +msgid "New connection has been rejected" +msgstr "새 연결이 거부되었습니다." + +#: ../app/ui.js:1158 +msgid "Password is required" +msgstr "비밀번호가 필요합니다." + +#: ../vnc.html:91 +msgid "noVNC encountered an error:" +msgstr "noVNC에 오류가 발생했습니다:" + +#: ../vnc.html:101 +msgid "Hide/Show the control bar" +msgstr "컨트롤 바 숨기기/보이기" + +#: ../vnc.html:108 +msgid "Move/Drag Viewport" +msgstr "움직이기/드래그 뷰포트" + +#: ../vnc.html:108 +msgid "viewport drag" +msgstr "뷰포트 드래그" + +#: ../vnc.html:114 ../vnc.html:117 ../vnc.html:120 ../vnc.html:123 +msgid "Active Mouse Button" +msgstr "마우스 버튼 활성화" + +#: ../vnc.html:114 +msgid "No mousebutton" +msgstr "마우스 버튼 없음" + +#: ../vnc.html:117 +msgid "Left mousebutton" +msgstr "왼쪽 마우스 버튼" + +#: ../vnc.html:120 +msgid "Middle mousebutton" +msgstr "중간 마우스 버튼" + +#: ../vnc.html:123 +msgid "Right mousebutton" +msgstr "오른쪽 마우스 버튼" + +#: ../vnc.html:126 +msgid "Keyboard" +msgstr "키보드" + +#: ../vnc.html:126 +msgid "Show Keyboard" +msgstr "키보드 보이기" + +#: ../vnc.html:133 +msgid "Extra keys" +msgstr "기타 키들" + +#: ../vnc.html:133 +msgid "Show Extra Keys" +msgstr "기타 키들 보이기" + +#: ../vnc.html:138 +msgid "Ctrl" +msgstr "Ctrl" + +#: ../vnc.html:138 +msgid "Toggle Ctrl" +msgstr "Ctrl 켜기/끄기" + +#: ../vnc.html:141 +msgid "Alt" +msgstr "Alt" + +#: ../vnc.html:141 +msgid "Toggle Alt" +msgstr "Alt 켜기/끄기" + +#: ../vnc.html:144 +msgid "Send Tab" +msgstr "Tab 보내기" + +#: ../vnc.html:144 +msgid "Tab" +msgstr "Tab" + +#: ../vnc.html:147 +msgid "Esc" +msgstr "Esc" + +#: ../vnc.html:147 +msgid "Send Escape" +msgstr "Esc 보내기" + +#: ../vnc.html:150 +msgid "Ctrl+Alt+Del" +msgstr "Ctrl+Alt+Del" + +#: ../vnc.html:150 +msgid "Send Ctrl-Alt-Del" +msgstr "Ctrl+Alt+Del 보내기" + +#: ../vnc.html:158 +msgid "Shutdown/Reboot" +msgstr "셧다운/리붓" + +#: ../vnc.html:158 +msgid "Shutdown/Reboot..." +msgstr "셧다운/리붓..." + +#: ../vnc.html:164 +msgid "Power" +msgstr "전원" + +#: ../vnc.html:166 +msgid "Shutdown" +msgstr "셧다운" + +#: ../vnc.html:167 +msgid "Reboot" +msgstr "리붓" + +#: ../vnc.html:168 +msgid "Reset" +msgstr "리셋" + +#: ../vnc.html:173 ../vnc.html:179 +msgid "Clipboard" +msgstr "클립보드" + +#: ../vnc.html:183 +msgid "Clear" +msgstr "지우기" + +#: ../vnc.html:189 +msgid "Fullscreen" +msgstr "전체화면" + +#: ../vnc.html:194 ../vnc.html:201 +msgid "Settings" +msgstr "설정" + +#: ../vnc.html:204 +msgid "Shared Mode" +msgstr "공유 모드" + +#: ../vnc.html:207 +msgid "View Only" +msgstr "보기 전용" + +#: ../vnc.html:211 +msgid "Clip to Window" +msgstr "창에 클립" + +#: ../vnc.html:214 +msgid "Scaling Mode:" +msgstr "스케일링 모드:" + +#: ../vnc.html:216 +msgid "None" +msgstr "없음" + +#: ../vnc.html:217 +msgid "Local Scaling" +msgstr "로컬 스케일링" + +#: ../vnc.html:218 +msgid "Remote Resizing" +msgstr "원격 크기 조절" + +#: ../vnc.html:223 +msgid "Advanced" +msgstr "고급" + +#: ../vnc.html:226 +msgid "Repeater ID:" +msgstr "중계 ID" + +#: ../vnc.html:230 +msgid "WebSocket" +msgstr "웹소켓" + +#: ../vnc.html:233 +msgid "Encrypt" +msgstr "암호화" + +#: ../vnc.html:236 +msgid "Host:" +msgstr "호스트:" + +#: ../vnc.html:240 +msgid "Port:" +msgstr "포트:" + +#: ../vnc.html:244 +msgid "Path:" +msgstr "위치:" + +#: ../vnc.html:251 +msgid "Automatic Reconnect" +msgstr "자동 재연결" + +#: ../vnc.html:254 +msgid "Reconnect Delay (ms):" +msgstr "재연결 지연 시간 (ms)" + +#: ../vnc.html:260 +msgid "Logging:" +msgstr "로깅" + +#: ../vnc.html:272 +msgid "Disconnect" +msgstr "연결 해제" + +#: ../vnc.html:291 +msgid "Connect" +msgstr "연결" + +#: ../vnc.html:301 +msgid "Password:" +msgstr "비밀번호:" + +#: ../vnc.html:305 +msgid "Send Password" +msgstr "비밀번호 전송" + +#: ../vnc.html:315 +msgid "Cancel" +msgstr "취소" diff --git a/systemvm/debian/root/noVNC/po/nl.po b/systemvm/debian/root/noVNC/po/nl.po new file mode 100644 index 000000000000..343204a9fd2c --- /dev/null +++ b/systemvm/debian/root/noVNC/po/nl.po @@ -0,0 +1,322 @@ +# Dutch translations for noVNC package +# Nederlandse vertalingen voor het pakket noVNC. +# Copyright (C) 2018 The noVNC Authors +# This file is distributed under the same license as the noVNC package. +# Loek Janssen , 2016. +# +msgid "" +msgstr "" +"Project-Id-Version: noVNC 1.1.0\n" +"Report-Msgid-Bugs-To: novnc@googlegroups.com\n" +"POT-Creation-Date: 2019-04-09 11:06+0100\n" +"PO-Revision-Date: 2019-04-09 17:17+0100\n" +"Last-Translator: Arend Lapere \n" +"Language-Team: none\n" +"Language: nl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: ../app/ui.js:383 +msgid "Connecting..." +msgstr "Verbinden..." + +#: ../app/ui.js:390 +msgid "Disconnecting..." +msgstr "Verbinding verbreken..." + +#: ../app/ui.js:396 +msgid "Reconnecting..." +msgstr "Opnieuw verbinding maken..." + +#: ../app/ui.js:401 +msgid "Internal error" +msgstr "Interne fout" + +#: ../app/ui.js:991 +msgid "Must set host" +msgstr "Host moeten worden ingesteld" + +#: ../app/ui.js:1073 +msgid "Connected (encrypted) to " +msgstr "Verbonden (versleuteld) met " + +#: ../app/ui.js:1075 +msgid "Connected (unencrypted) to " +msgstr "Verbonden (onversleuteld) met " + +#: ../app/ui.js:1098 +msgid "Something went wrong, connection is closed" +msgstr "Er iets fout gelopen, verbinding werd verbroken" + +#: ../app/ui.js:1101 +msgid "Failed to connect to server" +msgstr "Verbinding maken met server is mislukt" + +#: ../app/ui.js:1111 +msgid "Disconnected" +msgstr "Verbinding verbroken" + +#: ../app/ui.js:1124 +msgid "New connection has been rejected with reason: " +msgstr "Nieuwe verbinding is geweigerd omwille van de volgende reden: " + +#: ../app/ui.js:1127 +msgid "New connection has been rejected" +msgstr "Nieuwe verbinding is geweigerd" + +#: ../app/ui.js:1147 +msgid "Password is required" +msgstr "Wachtwoord is vereist" + +#: ../vnc.html:80 +msgid "noVNC encountered an error:" +msgstr "noVNC heeft een fout bemerkt:" + +#: ../vnc.html:90 +msgid "Hide/Show the control bar" +msgstr "Verberg/Toon de bedieningsbalk" + +#: ../vnc.html:97 +msgid "Move/Drag Viewport" +msgstr "Verplaats/Versleep Kijkvenster" + +#: ../vnc.html:97 +msgid "viewport drag" +msgstr "kijkvenster slepen" + +#: ../vnc.html:103 ../vnc.html:106 ../vnc.html:109 ../vnc.html:112 +msgid "Active Mouse Button" +msgstr "Actieve Muisknop" + +#: ../vnc.html:103 +msgid "No mousebutton" +msgstr "Geen muisknop" + +#: ../vnc.html:106 +msgid "Left mousebutton" +msgstr "Linker muisknop" + +#: ../vnc.html:109 +msgid "Middle mousebutton" +msgstr "Middelste muisknop" + +#: ../vnc.html:112 +msgid "Right mousebutton" +msgstr "Rechter muisknop" + +#: ../vnc.html:115 +msgid "Keyboard" +msgstr "Toetsenbord" + +#: ../vnc.html:115 +msgid "Show Keyboard" +msgstr "Toon Toetsenbord" + +#: ../vnc.html:121 +msgid "Extra keys" +msgstr "Extra toetsen" + +#: ../vnc.html:121 +msgid "Show Extra Keys" +msgstr "Toon Extra Toetsen" + +#: ../vnc.html:126 +msgid "Ctrl" +msgstr "Ctrl" + +#: ../vnc.html:126 +msgid "Toggle Ctrl" +msgstr "Ctrl omschakelen" + +#: ../vnc.html:129 +msgid "Alt" +msgstr "Alt" + +#: ../vnc.html:129 +msgid "Toggle Alt" +msgstr "Alt omschakelen" + +#: ../vnc.html:132 +msgid "Toggle Windows" +msgstr "Windows omschakelen" + +#: ../vnc.html:132 +msgid "Windows" +msgstr "Windows" + +#: ../vnc.html:135 +msgid "Send Tab" +msgstr "Tab Sturen" + +#: ../vnc.html:135 +msgid "Tab" +msgstr "Tab" + +#: ../vnc.html:138 +msgid "Esc" +msgstr "Esc" + +#: ../vnc.html:138 +msgid "Send Escape" +msgstr "Escape Sturen" + +#: ../vnc.html:141 +msgid "Ctrl+Alt+Del" +msgstr "Ctrl-Alt-Del" + +#: ../vnc.html:141 +msgid "Send Ctrl-Alt-Del" +msgstr "Ctrl-Alt-Del Sturen" + +#: ../vnc.html:149 +msgid "Shutdown/Reboot" +msgstr "Uitschakelen/Herstarten" + +#: ../vnc.html:149 +msgid "Shutdown/Reboot..." +msgstr "Uitschakelen/Herstarten..." + +#: ../vnc.html:155 +msgid "Power" +msgstr "Systeem" + +#: ../vnc.html:157 +msgid "Shutdown" +msgstr "Uitschakelen" + +#: ../vnc.html:158 +msgid "Reboot" +msgstr "Herstarten" + +#: ../vnc.html:159 +msgid "Reset" +msgstr "Resetten" + +#: ../vnc.html:164 ../vnc.html:170 +msgid "Clipboard" +msgstr "Klembord" + +#: ../vnc.html:174 +msgid "Clear" +msgstr "Wissen" + +#: ../vnc.html:180 +msgid "Fullscreen" +msgstr "Volledig Scherm" + +#: ../vnc.html:185 ../vnc.html:192 +msgid "Settings" +msgstr "Instellingen" + +#: ../vnc.html:195 +msgid "Shared Mode" +msgstr "Gedeelde Modus" + +#: ../vnc.html:198 +msgid "View Only" +msgstr "Alleen Kijken" + +#: ../vnc.html:202 +msgid "Clip to Window" +msgstr "Randen buiten venster afsnijden" + +#: ../vnc.html:205 +msgid "Scaling Mode:" +msgstr "Schaalmodus:" + +#: ../vnc.html:207 +msgid "None" +msgstr "Geen" + +#: ../vnc.html:208 +msgid "Local Scaling" +msgstr "Lokaal Schalen" + +#: ../vnc.html:209 +msgid "Remote Resizing" +msgstr "Op Afstand Formaat Wijzigen" + +#: ../vnc.html:214 +msgid "Advanced" +msgstr "Geavanceerd" + +#: ../vnc.html:217 +msgid "Repeater ID:" +msgstr "Repeater ID:" + +#: ../vnc.html:221 +msgid "WebSocket" +msgstr "WebSocket" + +#: ../vnc.html:224 +msgid "Encrypt" +msgstr "Versleutelen" + +#: ../vnc.html:227 +msgid "Host:" +msgstr "Host:" + +#: ../vnc.html:231 +msgid "Port:" +msgstr "Poort:" + +#: ../vnc.html:235 +msgid "Path:" +msgstr "Pad:" + +#: ../vnc.html:242 +msgid "Automatic Reconnect" +msgstr "Automatisch Opnieuw Verbinden" + +#: ../vnc.html:245 +msgid "Reconnect Delay (ms):" +msgstr "Vertraging voor Opnieuw Verbinden (ms):" + +#: ../vnc.html:250 +msgid "Show Dot when No Cursor" +msgstr "Geef stip weer indien geen cursor" + +#: ../vnc.html:255 +msgid "Logging:" +msgstr "Logmeldingen:" + +#: ../vnc.html:267 +msgid "Disconnect" +msgstr "Verbinding verbreken" + +#: ../vnc.html:286 +msgid "Connect" +msgstr "Verbinden" + +#: ../vnc.html:296 +msgid "Password:" +msgstr "Wachtwoord:" + +#: ../vnc.html:300 +msgid "Send Password" +msgstr "Verzend Wachtwoord:" + +#: ../vnc.html:310 +msgid "Cancel" +msgstr "Annuleren" + +#~ msgid "Disconnect timeout" +#~ msgstr "Timeout tijdens verbreken van verbinding" + +#~ msgid "Local Downscaling" +#~ msgstr "Lokaal Neerschalen" + +#~ msgid "Local Cursor" +#~ msgstr "Lokale Cursor" + +#~ msgid "Canvas not supported." +#~ msgstr "Canvas wordt niet ondersteund." + +#~ msgid "" +#~ "Forcing clipping mode since scrollbars aren't supported by IE in " +#~ "fullscreen" +#~ msgstr "" +#~ "''Clipping mode' ingeschakeld, omdat schuifbalken in volledige-scherm-" +#~ "modus in IE niet worden ondersteund" diff --git a/systemvm/debian/root/noVNC/po/noVNC.pot b/systemvm/debian/root/noVNC/po/noVNC.pot new file mode 100644 index 000000000000..200be01de6e8 --- /dev/null +++ b/systemvm/debian/root/noVNC/po/noVNC.pot @@ -0,0 +1,302 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR The noVNC Authors +# This file is distributed under the same license as the noVNC package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: noVNC 1.1.0\n" +"Report-Msgid-Bugs-To: novnc@googlegroups.com\n" +"POT-Creation-Date: 2019-01-16 11:06+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: ../app/ui.js:387 +msgid "Connecting..." +msgstr "" + +#: ../app/ui.js:394 +msgid "Disconnecting..." +msgstr "" + +#: ../app/ui.js:400 +msgid "Reconnecting..." +msgstr "" + +#: ../app/ui.js:405 +msgid "Internal error" +msgstr "" + +#: ../app/ui.js:995 +msgid "Must set host" +msgstr "" + +#: ../app/ui.js:1077 +msgid "Connected (encrypted) to " +msgstr "" + +#: ../app/ui.js:1079 +msgid "Connected (unencrypted) to " +msgstr "" + +#: ../app/ui.js:1102 +msgid "Something went wrong, connection is closed" +msgstr "" + +#: ../app/ui.js:1105 +msgid "Failed to connect to server" +msgstr "" + +#: ../app/ui.js:1115 +msgid "Disconnected" +msgstr "" + +#: ../app/ui.js:1128 +msgid "New connection has been rejected with reason: " +msgstr "" + +#: ../app/ui.js:1131 +msgid "New connection has been rejected" +msgstr "" + +#: ../app/ui.js:1151 +msgid "Password is required" +msgstr "" + +#: ../vnc.html:84 +msgid "noVNC encountered an error:" +msgstr "" + +#: ../vnc.html:94 +msgid "Hide/Show the control bar" +msgstr "" + +#: ../vnc.html:101 +msgid "Move/Drag Viewport" +msgstr "" + +#: ../vnc.html:101 +msgid "viewport drag" +msgstr "" + +#: ../vnc.html:107 ../vnc.html:110 ../vnc.html:113 ../vnc.html:116 +msgid "Active Mouse Button" +msgstr "" + +#: ../vnc.html:107 +msgid "No mousebutton" +msgstr "" + +#: ../vnc.html:110 +msgid "Left mousebutton" +msgstr "" + +#: ../vnc.html:113 +msgid "Middle mousebutton" +msgstr "" + +#: ../vnc.html:116 +msgid "Right mousebutton" +msgstr "" + +#: ../vnc.html:119 +msgid "Keyboard" +msgstr "" + +#: ../vnc.html:119 +msgid "Show Keyboard" +msgstr "" + +#: ../vnc.html:126 +msgid "Extra keys" +msgstr "" + +#: ../vnc.html:126 +msgid "Show Extra Keys" +msgstr "" + +#: ../vnc.html:131 +msgid "Ctrl" +msgstr "" + +#: ../vnc.html:131 +msgid "Toggle Ctrl" +msgstr "" + +#: ../vnc.html:134 +msgid "Alt" +msgstr "" + +#: ../vnc.html:134 +msgid "Toggle Alt" +msgstr "" + +#: ../vnc.html:137 +msgid "Toggle Windows" +msgstr "" + +#: ../vnc.html:137 +msgid "Windows" +msgstr "" + +#: ../vnc.html:140 +msgid "Send Tab" +msgstr "" + +#: ../vnc.html:140 +msgid "Tab" +msgstr "" + +#: ../vnc.html:143 +msgid "Esc" +msgstr "" + +#: ../vnc.html:143 +msgid "Send Escape" +msgstr "" + +#: ../vnc.html:146 +msgid "Ctrl+Alt+Del" +msgstr "" + +#: ../vnc.html:146 +msgid "Send Ctrl-Alt-Del" +msgstr "" + +#: ../vnc.html:154 +msgid "Shutdown/Reboot" +msgstr "" + +#: ../vnc.html:154 +msgid "Shutdown/Reboot..." +msgstr "" + +#: ../vnc.html:160 +msgid "Power" +msgstr "" + +#: ../vnc.html:162 +msgid "Shutdown" +msgstr "" + +#: ../vnc.html:163 +msgid "Reboot" +msgstr "" + +#: ../vnc.html:164 +msgid "Reset" +msgstr "" + +#: ../vnc.html:169 ../vnc.html:175 +msgid "Clipboard" +msgstr "" + +#: ../vnc.html:179 +msgid "Clear" +msgstr "" + +#: ../vnc.html:185 +msgid "Fullscreen" +msgstr "" + +#: ../vnc.html:190 ../vnc.html:197 +msgid "Settings" +msgstr "" + +#: ../vnc.html:200 +msgid "Shared Mode" +msgstr "" + +#: ../vnc.html:203 +msgid "View Only" +msgstr "" + +#: ../vnc.html:207 +msgid "Clip to Window" +msgstr "" + +#: ../vnc.html:210 +msgid "Scaling Mode:" +msgstr "" + +#: ../vnc.html:212 +msgid "None" +msgstr "" + +#: ../vnc.html:213 +msgid "Local Scaling" +msgstr "" + +#: ../vnc.html:214 +msgid "Remote Resizing" +msgstr "" + +#: ../vnc.html:219 +msgid "Advanced" +msgstr "" + +#: ../vnc.html:222 +msgid "Repeater ID:" +msgstr "" + +#: ../vnc.html:226 +msgid "WebSocket" +msgstr "" + +#: ../vnc.html:229 +msgid "Encrypt" +msgstr "" + +#: ../vnc.html:232 +msgid "Host:" +msgstr "" + +#: ../vnc.html:236 +msgid "Port:" +msgstr "" + +#: ../vnc.html:240 +msgid "Path:" +msgstr "" + +#: ../vnc.html:247 +msgid "Automatic Reconnect" +msgstr "" + +#: ../vnc.html:250 +msgid "Reconnect Delay (ms):" +msgstr "" + +#: ../vnc.html:255 +msgid "Show Dot when No Cursor" +msgstr "" + +#: ../vnc.html:260 +msgid "Logging:" +msgstr "" + +#: ../vnc.html:272 +msgid "Disconnect" +msgstr "" + +#: ../vnc.html:291 +msgid "Connect" +msgstr "" + +#: ../vnc.html:301 +msgid "Password:" +msgstr "" + +#: ../vnc.html:305 +msgid "Send Password" +msgstr "" + +#: ../vnc.html:315 +msgid "Cancel" +msgstr "" diff --git a/systemvm/debian/root/noVNC/po/pl.po b/systemvm/debian/root/noVNC/po/pl.po new file mode 100644 index 000000000000..5acfdc4f4b83 --- /dev/null +++ b/systemvm/debian/root/noVNC/po/pl.po @@ -0,0 +1,325 @@ +# Polish translations for noVNC package. +# Copyright (C) 2018 The noVNC Authors +# This file is distributed under the same license as the noVNC package. +# Mariusz Jamro , 2017. +# +msgid "" +msgstr "" +"Project-Id-Version: noVNC 0.6.1\n" +"Report-Msgid-Bugs-To: novnc@googlegroups.com\n" +"POT-Creation-Date: 2017-11-21 19:53+0100\n" +"PO-Revision-Date: 2017-11-21 19:54+0100\n" +"Last-Translator: Mariusz Jamro \n" +"Language-Team: Polish\n" +"Language: pl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2);\n" +"X-Generator: Poedit 2.0.1\n" + +#: ../app/ui.js:404 +msgid "Connecting..." +msgstr "Łączenie..." + +#: ../app/ui.js:411 +msgid "Disconnecting..." +msgstr "Rozłączanie..." + +#: ../app/ui.js:417 +msgid "Reconnecting..." +msgstr "Łączenie..." + +#: ../app/ui.js:422 +msgid "Internal error" +msgstr "Błąd wewnętrzny" + +#: ../app/ui.js:1019 +msgid "Must set host" +msgstr "Host i port są wymagane" + +#: ../app/ui.js:1099 +msgid "Connected (encrypted) to " +msgstr "Połączenie (szyfrowane) z " + +#: ../app/ui.js:1101 +msgid "Connected (unencrypted) to " +msgstr "Połączenie (nieszyfrowane) z " + +#: ../app/ui.js:1119 +msgid "Something went wrong, connection is closed" +msgstr "Coś poszło źle, połączenie zostało zamknięte" + +#: ../app/ui.js:1129 +msgid "Disconnected" +msgstr "Rozłączony" + +#: ../app/ui.js:1142 +msgid "New connection has been rejected with reason: " +msgstr "Nowe połączenie zostało odrzucone z powodu: " + +#: ../app/ui.js:1145 +msgid "New connection has been rejected" +msgstr "Nowe połączenie zostało odrzucone" + +#: ../app/ui.js:1166 +msgid "Password is required" +msgstr "Hasło jest wymagane" + +#: ../vnc.html:89 +msgid "noVNC encountered an error:" +msgstr "noVNC napotkało błąd:" + +#: ../vnc.html:99 +msgid "Hide/Show the control bar" +msgstr "Pokaż/Ukryj pasek ustawień" + +#: ../vnc.html:106 +msgid "Move/Drag Viewport" +msgstr "Ruszaj/Przeciągaj Viewport" + +#: ../vnc.html:106 +msgid "viewport drag" +msgstr "przeciągnij viewport" + +#: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121 +msgid "Active Mouse Button" +msgstr "Aktywny Przycisk Myszy" + +#: ../vnc.html:112 +msgid "No mousebutton" +msgstr "Brak przycisku myszy" + +#: ../vnc.html:115 +msgid "Left mousebutton" +msgstr "Lewy przycisk myszy" + +#: ../vnc.html:118 +msgid "Middle mousebutton" +msgstr "Środkowy przycisk myszy" + +#: ../vnc.html:121 +msgid "Right mousebutton" +msgstr "Prawy przycisk myszy" + +#: ../vnc.html:124 +msgid "Keyboard" +msgstr "Klawiatura" + +#: ../vnc.html:124 +msgid "Show Keyboard" +msgstr "Pokaż klawiaturę" + +#: ../vnc.html:131 +msgid "Extra keys" +msgstr "Przyciski dodatkowe" + +#: ../vnc.html:131 +msgid "Show Extra Keys" +msgstr "Pokaż przyciski dodatkowe" + +#: ../vnc.html:136 +msgid "Ctrl" +msgstr "Ctrl" + +#: ../vnc.html:136 +msgid "Toggle Ctrl" +msgstr "Przełącz Ctrl" + +#: ../vnc.html:139 +msgid "Alt" +msgstr "Alt" + +#: ../vnc.html:139 +msgid "Toggle Alt" +msgstr "Przełącz Alt" + +#: ../vnc.html:142 +msgid "Send Tab" +msgstr "Wyślij Tab" + +#: ../vnc.html:142 +msgid "Tab" +msgstr "Tab" + +#: ../vnc.html:145 +msgid "Esc" +msgstr "Esc" + +#: ../vnc.html:145 +msgid "Send Escape" +msgstr "Wyślij Escape" + +#: ../vnc.html:148 +msgid "Ctrl+Alt+Del" +msgstr "Ctrl+Alt+Del" + +#: ../vnc.html:148 +msgid "Send Ctrl-Alt-Del" +msgstr "Wyślij Ctrl-Alt-Del" + +#: ../vnc.html:156 +msgid "Shutdown/Reboot" +msgstr "Wyłącz/Uruchom ponownie" + +#: ../vnc.html:156 +msgid "Shutdown/Reboot..." +msgstr "Wyłącz/Uruchom ponownie..." + +#: ../vnc.html:162 +msgid "Power" +msgstr "Włączony" + +#: ../vnc.html:164 +msgid "Shutdown" +msgstr "Wyłącz" + +#: ../vnc.html:165 +msgid "Reboot" +msgstr "Uruchom ponownie" + +#: ../vnc.html:166 +msgid "Reset" +msgstr "Resetuj" + +#: ../vnc.html:171 ../vnc.html:177 +msgid "Clipboard" +msgstr "Schowek" + +#: ../vnc.html:181 +msgid "Clear" +msgstr "Wyczyść" + +#: ../vnc.html:187 +msgid "Fullscreen" +msgstr "Pełny ekran" + +#: ../vnc.html:192 ../vnc.html:199 +msgid "Settings" +msgstr "Ustawienia" + +#: ../vnc.html:202 +msgid "Shared Mode" +msgstr "Tryb Współdzielenia" + +#: ../vnc.html:205 +msgid "View Only" +msgstr "Tylko Podgląd" + +#: ../vnc.html:209 +msgid "Clip to Window" +msgstr "Przytnij do Okna" + +#: ../vnc.html:212 +msgid "Scaling Mode:" +msgstr "Tryb Skalowania:" + +#: ../vnc.html:214 +msgid "None" +msgstr "Brak" + +#: ../vnc.html:215 +msgid "Local Scaling" +msgstr "Skalowanie lokalne" + +#: ../vnc.html:216 +msgid "Remote Resizing" +msgstr "Skalowanie zdalne" + +#: ../vnc.html:221 +msgid "Advanced" +msgstr "Zaawansowane" + +#: ../vnc.html:224 +msgid "Repeater ID:" +msgstr "ID Repeatera:" + +#: ../vnc.html:228 +msgid "WebSocket" +msgstr "WebSocket" + +#: ../vnc.html:231 +msgid "Encrypt" +msgstr "Szyfrowanie" + +#: ../vnc.html:234 +msgid "Host:" +msgstr "Host:" + +#: ../vnc.html:238 +msgid "Port:" +msgstr "Port:" + +#: ../vnc.html:242 +msgid "Path:" +msgstr "Ścieżka:" + +#: ../vnc.html:249 +msgid "Automatic Reconnect" +msgstr "Automatycznie wznawiaj połączenie" + +#: ../vnc.html:252 +msgid "Reconnect Delay (ms):" +msgstr "Opóźnienie wznawiania (ms):" + +#: ../vnc.html:258 +msgid "Logging:" +msgstr "Poziom logowania:" + +#: ../vnc.html:270 +msgid "Disconnect" +msgstr "Rozłącz" + +#: ../vnc.html:289 +msgid "Connect" +msgstr "Połącz" + +#: ../vnc.html:299 +msgid "Password:" +msgstr "Hasło:" + +#: ../vnc.html:313 +msgid "Cancel" +msgstr "Anuluj" + +#: ../vnc.html:329 +msgid "Canvas not supported." +msgstr "Element Canvas nie jest wspierany." + +#~ msgid "Disconnect timeout" +#~ msgstr "Timeout rozłączenia" + +#~ msgid "Local Downscaling" +#~ msgstr "Downscaling lokalny" + +#~ msgid "Local Cursor" +#~ msgstr "Lokalny kursor" + +#~ msgid "" +#~ "Forcing clipping mode since scrollbars aren't supported by IE in " +#~ "fullscreen" +#~ msgstr "" +#~ "Wymuszam clipping mode ponieważ paski przewijania nie są wspierane przez " +#~ "IE w trybie pełnoekranowym" + +#~ msgid "True Color" +#~ msgstr "True Color" + +#~ msgid "Style:" +#~ msgstr "Styl:" + +#~ msgid "default" +#~ msgstr "domyślny" + +#~ msgid "Apply" +#~ msgstr "Zapisz" + +#~ msgid "Connection" +#~ msgstr "Połączenie" + +#~ msgid "Token:" +#~ msgstr "Token:" + +#~ msgid "Send Password" +#~ msgstr "Wyślij Hasło" diff --git a/systemvm/debian/root/noVNC/po/po2js b/systemvm/debian/root/noVNC/po/po2js new file mode 100755 index 000000000000..03c14900fff9 --- /dev/null +++ b/systemvm/debian/root/noVNC/po/po2js @@ -0,0 +1,43 @@ +#!/usr/bin/env node +/* + * ps2js: gettext .po to noVNC .js converter + * Copyright (C) 2018 The noVNC Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +const getopt = require('node-getopt'); +const fs = require('fs'); +const po2json = require("po2json"); + +const opt = getopt.create([ + ['h' , 'help' , 'display this help'], +]).bindHelp().parseSystem(); + +if (opt.argv.length != 2) { + console.error("Incorrect number of arguments given"); + process.exit(1); +} + +const data = po2json.parseFileSync(opt.argv[0]); + +const bodyPart = Object.keys(data).filter((msgid) => msgid !== "").map((msgid) => { + if (msgid === "") return; + const msgstr = data[msgid][1]; + return " " + JSON.stringify(msgid) + ": " + JSON.stringify(msgstr); +}).join(",\n"); + +const output = "{\n" + bodyPart + "\n}"; + +fs.writeFileSync(opt.argv[1], output); diff --git a/systemvm/debian/root/noVNC/po/ru.po b/systemvm/debian/root/noVNC/po/ru.po new file mode 100644 index 000000000000..fb5d0875ef89 --- /dev/null +++ b/systemvm/debian/root/noVNC/po/ru.po @@ -0,0 +1,306 @@ +# Russian translations for noVNC package +# Русский перевод для пакета noVNC. +# Copyright (C) 2019 Dmitriy Shweew +# This file is distributed under the same license as the noVNC package. +# Dmitriy Shweew , 2019. +# +msgid "" +msgstr "" +"Project-Id-Version: noVNC 1.1.0\n" +"Report-Msgid-Bugs-To: novnc@googlegroups.com\n" +"POT-Creation-Date: 2019-02-26 14:53+0400\n" +"PO-Revision-Date: 2019-02-17 17:29+0400\n" +"Last-Translator: Dmitriy Shweew \n" +"Language-Team: Russian\n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Poedit 2.2.1\n" +"X-Poedit-Flags-xgettext: --add-comments\n" + +#: ../app/ui.js:387 +msgid "Connecting..." +msgstr "Подключение..." + +#: ../app/ui.js:394 +msgid "Disconnecting..." +msgstr "Отключение..." + +#: ../app/ui.js:400 +msgid "Reconnecting..." +msgstr "Переподключение..." + +#: ../app/ui.js:405 +msgid "Internal error" +msgstr "Внутренняя ошибка" + +#: ../app/ui.js:995 +msgid "Must set host" +msgstr "Задайте имя сервера или IP" + +#: ../app/ui.js:1077 +msgid "Connected (encrypted) to " +msgstr "Подключено (с шифрованием) к " + +#: ../app/ui.js:1079 +msgid "Connected (unencrypted) to " +msgstr "Подключено (без шифрования) к " + +#: ../app/ui.js:1102 +msgid "Something went wrong, connection is closed" +msgstr "Что-то пошло не так, подключение разорвано" + +#: ../app/ui.js:1105 +msgid "Failed to connect to server" +msgstr "Ошибка подключения к серверу" + +#: ../app/ui.js:1115 +msgid "Disconnected" +msgstr "Отключено" + +#: ../app/ui.js:1128 +msgid "New connection has been rejected with reason: " +msgstr "Подключиться не удалось: " + +#: ../app/ui.js:1131 +msgid "New connection has been rejected" +msgstr "Подключиться не удалось" + +#: ../app/ui.js:1151 +msgid "Password is required" +msgstr "Требуется пароль" + +#: ../vnc.html:84 +msgid "noVNC encountered an error:" +msgstr "Ошибка noVNC: " + +#: ../vnc.html:94 +msgid "Hide/Show the control bar" +msgstr "Скрыть/Показать контрольную панель" + +#: ../vnc.html:101 +msgid "Move/Drag Viewport" +msgstr "Переместить окно" + +#: ../vnc.html:101 +msgid "viewport drag" +msgstr "Переместить окно" + +#: ../vnc.html:107 ../vnc.html:110 ../vnc.html:113 ../vnc.html:116 +msgid "Active Mouse Button" +msgstr "Активировать кнопки мыши" + +#: ../vnc.html:107 +msgid "No mousebutton" +msgstr "Отключить кнопки мыши" + +#: ../vnc.html:110 +msgid "Left mousebutton" +msgstr "Левая кнопка мыши" + +#: ../vnc.html:113 +msgid "Middle mousebutton" +msgstr "Средняя кнопка мыши" + +#: ../vnc.html:116 +msgid "Right mousebutton" +msgstr "Правая кнопка мыши" + +#: ../vnc.html:119 +msgid "Keyboard" +msgstr "Клавиатура" + +#: ../vnc.html:119 +msgid "Show Keyboard" +msgstr "Показать клавиатуру" + +#: ../vnc.html:126 +msgid "Extra keys" +msgstr "Доп. кнопки" + +#: ../vnc.html:126 +msgid "Show Extra Keys" +msgstr "Показать дополнительные кнопки" + +#: ../vnc.html:131 +msgid "Ctrl" +msgstr "Ctrl" + +#: ../vnc.html:131 +msgid "Toggle Ctrl" +msgstr "Передать нажатие Ctrl" + +#: ../vnc.html:134 +msgid "Alt" +msgstr "Alt" + +#: ../vnc.html:134 +msgid "Toggle Alt" +msgstr "Передать нажатие Alt" + +#: ../vnc.html:137 +msgid "Toggle Windows" +msgstr "Переключение вкладок" + +#: ../vnc.html:137 +msgid "Windows" +msgstr "Вкладка" + +#: ../vnc.html:140 +msgid "Send Tab" +msgstr "Передать нажатие Tab" + +#: ../vnc.html:140 +msgid "Tab" +msgstr "Tab" + +#: ../vnc.html:143 +msgid "Esc" +msgstr "Esc" + +#: ../vnc.html:143 +msgid "Send Escape" +msgstr "Передать нажатие Escape" + +#: ../vnc.html:146 +msgid "Ctrl+Alt+Del" +msgstr "Ctrl+Alt+Del" + +#: ../vnc.html:146 +msgid "Send Ctrl-Alt-Del" +msgstr "Передать нажатие Ctrl-Alt-Del" + +#: ../vnc.html:154 +msgid "Shutdown/Reboot" +msgstr "Выключить/Перезагрузить" + +#: ../vnc.html:154 +msgid "Shutdown/Reboot..." +msgstr "Выключить/Перезагрузить..." + +#: ../vnc.html:160 +msgid "Power" +msgstr "Питание" + +#: ../vnc.html:162 +msgid "Shutdown" +msgstr "Выключить" + +#: ../vnc.html:163 +msgid "Reboot" +msgstr "Перезагрузить" + +#: ../vnc.html:164 +msgid "Reset" +msgstr "Сброс" + +#: ../vnc.html:169 ../vnc.html:175 +msgid "Clipboard" +msgstr "Буфер обмена" + +#: ../vnc.html:179 +msgid "Clear" +msgstr "Очистить" + +#: ../vnc.html:185 +msgid "Fullscreen" +msgstr "Во весь экран" + +#: ../vnc.html:190 ../vnc.html:197 +msgid "Settings" +msgstr "Настройки" + +#: ../vnc.html:200 +msgid "Shared Mode" +msgstr "Общий режим" + +#: ../vnc.html:203 +msgid "View Only" +msgstr "Просмотр" + +#: ../vnc.html:207 +msgid "Clip to Window" +msgstr "В окно" + +#: ../vnc.html:210 +msgid "Scaling Mode:" +msgstr "Масштаб:" + +#: ../vnc.html:212 +msgid "None" +msgstr "Нет" + +#: ../vnc.html:213 +msgid "Local Scaling" +msgstr "Локльный масштаб" + +#: ../vnc.html:214 +msgid "Remote Resizing" +msgstr "Удаленный масштаб" + +#: ../vnc.html:219 +msgid "Advanced" +msgstr "Дополнительно" + +#: ../vnc.html:222 +msgid "Repeater ID:" +msgstr "Идентификатор ID:" + +#: ../vnc.html:226 +msgid "WebSocket" +msgstr "WebSocket" + +#: ../vnc.html:229 +msgid "Encrypt" +msgstr "Шифрование" + +#: ../vnc.html:232 +msgid "Host:" +msgstr "Сервер:" + +#: ../vnc.html:236 +msgid "Port:" +msgstr "Порт:" + +#: ../vnc.html:240 +msgid "Path:" +msgstr "Путь:" + +#: ../vnc.html:247 +msgid "Automatic Reconnect" +msgstr "Автоматическое переподключение" + +#: ../vnc.html:250 +msgid "Reconnect Delay (ms):" +msgstr "Задержка переподключения (мс):" + +#: ../vnc.html:255 +msgid "Show Dot when No Cursor" +msgstr "Показать точку вместо курсора" + +#: ../vnc.html:260 +msgid "Logging:" +msgstr "Лог:" + +#: ../vnc.html:272 +msgid "Disconnect" +msgstr "Отключение" + +#: ../vnc.html:291 +msgid "Connect" +msgstr "Подключение" + +#: ../vnc.html:301 +msgid "Password:" +msgstr "Пароль:" + +#: ../vnc.html:305 +msgid "Send Password" +msgstr "Пароль: " + +#: ../vnc.html:315 +msgid "Cancel" +msgstr "Выход" diff --git a/systemvm/debian/root/noVNC/po/sv.po b/systemvm/debian/root/noVNC/po/sv.po new file mode 100644 index 000000000000..f79556629572 --- /dev/null +++ b/systemvm/debian/root/noVNC/po/sv.po @@ -0,0 +1,316 @@ +# Swedish translations for noVNC package +# Svenska översättningar för paket noVNC. +# Copyright (C) 2018 The noVNC Authors +# This file is distributed under the same license as the noVNC package. +# Samuel Mannehed , 2019. +# +msgid "" +msgstr "" +"Project-Id-Version: noVNC 1.1.0\n" +"Report-Msgid-Bugs-To: novnc@googlegroups.com\n" +"POT-Creation-Date: 2019-01-16 11:06+0100\n" +"PO-Revision-Date: 2019-04-08 10:18+0200\n" +"Last-Translator: Samuel Mannehed \n" +"Language-Team: none\n" +"Language: sv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 2.0.3\n" + +#: ../app/ui.js:387 +msgid "Connecting..." +msgstr "Ansluter..." + +#: ../app/ui.js:394 +msgid "Disconnecting..." +msgstr "Kopplar ner..." + +#: ../app/ui.js:400 +msgid "Reconnecting..." +msgstr "Återansluter..." + +#: ../app/ui.js:405 +msgid "Internal error" +msgstr "Internt fel" + +#: ../app/ui.js:995 +msgid "Must set host" +msgstr "Du måste specifiera en värd" + +#: ../app/ui.js:1077 +msgid "Connected (encrypted) to " +msgstr "Ansluten (krypterat) till " + +#: ../app/ui.js:1079 +msgid "Connected (unencrypted) to " +msgstr "Ansluten (okrypterat) till " + +#: ../app/ui.js:1102 +msgid "Something went wrong, connection is closed" +msgstr "Något gick fel, anslutningen avslutades" + +#: ../app/ui.js:1105 +msgid "Failed to connect to server" +msgstr "Misslyckades att ansluta till servern" + +#: ../app/ui.js:1115 +msgid "Disconnected" +msgstr "Frånkopplad" + +#: ../app/ui.js:1128 +msgid "New connection has been rejected with reason: " +msgstr "Ny anslutning har blivit nekad med följande skäl: " + +#: ../app/ui.js:1131 +msgid "New connection has been rejected" +msgstr "Ny anslutning har blivit nekad" + +#: ../app/ui.js:1151 +msgid "Password is required" +msgstr "Lösenord krävs" + +#: ../vnc.html:84 +msgid "noVNC encountered an error:" +msgstr "noVNC stötte på ett problem:" + +#: ../vnc.html:94 +msgid "Hide/Show the control bar" +msgstr "Göm/Visa kontrollbaren" + +#: ../vnc.html:101 +msgid "Move/Drag Viewport" +msgstr "Flytta/Dra Vyn" + +#: ../vnc.html:101 +msgid "viewport drag" +msgstr "dra vy" + +#: ../vnc.html:107 ../vnc.html:110 ../vnc.html:113 ../vnc.html:116 +msgid "Active Mouse Button" +msgstr "Aktiv musknapp" + +#: ../vnc.html:107 +msgid "No mousebutton" +msgstr "Ingen musknapp" + +#: ../vnc.html:110 +msgid "Left mousebutton" +msgstr "Vänster musknapp" + +#: ../vnc.html:113 +msgid "Middle mousebutton" +msgstr "Mitten-musknapp" + +#: ../vnc.html:116 +msgid "Right mousebutton" +msgstr "Höger musknapp" + +#: ../vnc.html:119 +msgid "Keyboard" +msgstr "Tangentbord" + +#: ../vnc.html:119 +msgid "Show Keyboard" +msgstr "Visa Tangentbord" + +#: ../vnc.html:126 +msgid "Extra keys" +msgstr "Extraknappar" + +#: ../vnc.html:126 +msgid "Show Extra Keys" +msgstr "Visa Extraknappar" + +#: ../vnc.html:131 +msgid "Ctrl" +msgstr "Ctrl" + +#: ../vnc.html:131 +msgid "Toggle Ctrl" +msgstr "Växla Ctrl" + +#: ../vnc.html:134 +msgid "Alt" +msgstr "Alt" + +#: ../vnc.html:134 +msgid "Toggle Alt" +msgstr "Växla Alt" + +#: ../vnc.html:137 +msgid "Toggle Windows" +msgstr "Växla Windows" + +#: ../vnc.html:137 +msgid "Windows" +msgstr "Windows" + +#: ../vnc.html:140 +msgid "Send Tab" +msgstr "Skicka Tab" + +#: ../vnc.html:140 +msgid "Tab" +msgstr "Tab" + +#: ../vnc.html:143 +msgid "Esc" +msgstr "Esc" + +#: ../vnc.html:143 +msgid "Send Escape" +msgstr "Skicka Escape" + +#: ../vnc.html:146 +msgid "Ctrl+Alt+Del" +msgstr "Ctrl+Alt+Del" + +#: ../vnc.html:146 +msgid "Send Ctrl-Alt-Del" +msgstr "Skicka Ctrl-Alt-Del" + +#: ../vnc.html:154 +msgid "Shutdown/Reboot" +msgstr "Stäng av/Boota om" + +#: ../vnc.html:154 +msgid "Shutdown/Reboot..." +msgstr "Stäng av/Boota om..." + +#: ../vnc.html:160 +msgid "Power" +msgstr "Ström" + +#: ../vnc.html:162 +msgid "Shutdown" +msgstr "Stäng av" + +#: ../vnc.html:163 +msgid "Reboot" +msgstr "Boota om" + +#: ../vnc.html:164 +msgid "Reset" +msgstr "Återställ" + +#: ../vnc.html:169 ../vnc.html:175 +msgid "Clipboard" +msgstr "Urklipp" + +#: ../vnc.html:179 +msgid "Clear" +msgstr "Rensa" + +#: ../vnc.html:185 +msgid "Fullscreen" +msgstr "Fullskärm" + +#: ../vnc.html:190 ../vnc.html:197 +msgid "Settings" +msgstr "Inställningar" + +#: ../vnc.html:200 +msgid "Shared Mode" +msgstr "Delat Läge" + +#: ../vnc.html:203 +msgid "View Only" +msgstr "Endast Visning" + +#: ../vnc.html:207 +msgid "Clip to Window" +msgstr "Begränsa till Fönster" + +#: ../vnc.html:210 +msgid "Scaling Mode:" +msgstr "Skalningsläge:" + +#: ../vnc.html:212 +msgid "None" +msgstr "Ingen" + +#: ../vnc.html:213 +msgid "Local Scaling" +msgstr "Lokal Skalning" + +#: ../vnc.html:214 +msgid "Remote Resizing" +msgstr "Ändra Storlek" + +#: ../vnc.html:219 +msgid "Advanced" +msgstr "Avancerat" + +#: ../vnc.html:222 +msgid "Repeater ID:" +msgstr "Repeater-ID:" + +#: ../vnc.html:226 +msgid "WebSocket" +msgstr "WebSocket" + +#: ../vnc.html:229 +msgid "Encrypt" +msgstr "Kryptera" + +#: ../vnc.html:232 +msgid "Host:" +msgstr "Värd:" + +#: ../vnc.html:236 +msgid "Port:" +msgstr "Port:" + +#: ../vnc.html:240 +msgid "Path:" +msgstr "Sökväg:" + +#: ../vnc.html:247 +msgid "Automatic Reconnect" +msgstr "Automatisk Återanslutning" + +#: ../vnc.html:250 +msgid "Reconnect Delay (ms):" +msgstr "Fördröjning (ms):" + +#: ../vnc.html:255 +msgid "Show Dot when No Cursor" +msgstr "Visa prick när ingen muspekare finns" + +#: ../vnc.html:260 +msgid "Logging:" +msgstr "Loggning:" + +#: ../vnc.html:272 +msgid "Disconnect" +msgstr "Koppla från" + +#: ../vnc.html:291 +msgid "Connect" +msgstr "Anslut" + +#: ../vnc.html:301 +msgid "Password:" +msgstr "Lösenord:" + +#: ../vnc.html:305 +msgid "Send Password" +msgstr "Skicka lösenord" + +#: ../vnc.html:315 +msgid "Cancel" +msgstr "Avbryt" + +#~ msgid "Disconnect timeout" +#~ msgstr "Det tog för lång tid att koppla ner" + +#~ msgid "Local Downscaling" +#~ msgstr "Lokal Nedskalning" + +#~ msgid "Local Cursor" +#~ msgstr "Lokal Muspekare" + +#~ msgid "Canvas not supported." +#~ msgstr "Canvas stöds ej" diff --git a/systemvm/debian/root/noVNC/po/tr.po b/systemvm/debian/root/noVNC/po/tr.po new file mode 100644 index 000000000000..8b5c1813455e --- /dev/null +++ b/systemvm/debian/root/noVNC/po/tr.po @@ -0,0 +1,288 @@ +# Turkish translations for noVNC package +# Turkish translation for noVNC. +# Copyright (C) 2018 The noVNC Authors +# This file is distributed under the same license as the noVNC package. +# Ömer ÇAKMAK , 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: noVNC 0.6.1\n" +"Report-Msgid-Bugs-To: novnc@googlegroups.com\n" +"POT-Creation-Date: 2017-11-24 07:16+0000\n" +"PO-Revision-Date: 2018-01-05 19:07+0300\n" +"Last-Translator: Ömer ÇAKMAK \n" +"Language-Team: Türkçe \n" +"Language: tr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Gtranslator 2.91.7\n" + +#: ../app/ui.js:404 +msgid "Connecting..." +msgstr "Bağlanıyor..." + +#: ../app/ui.js:411 +msgid "Disconnecting..." +msgstr "Bağlantı kesiliyor..." + +#: ../app/ui.js:417 +msgid "Reconnecting..." +msgstr "Yeniden bağlantı kuruluyor..." + +#: ../app/ui.js:422 +msgid "Internal error" +msgstr "İç hata" + +#: ../app/ui.js:1019 +msgid "Must set host" +msgstr "Sunucuyu kur" + +#: ../app/ui.js:1099 +msgid "Connected (encrypted) to " +msgstr "Bağlı (şifrelenmiş)" + +#: ../app/ui.js:1101 +msgid "Connected (unencrypted) to " +msgstr "Bağlandı (şifrelenmemiş)" + +#: ../app/ui.js:1119 +msgid "Something went wrong, connection is closed" +msgstr "Bir şeyler ters gitti, bağlantı kesildi" + +#: ../app/ui.js:1129 +msgid "Disconnected" +msgstr "Bağlantı kesildi" + +#: ../app/ui.js:1142 +msgid "New connection has been rejected with reason: " +msgstr "Bağlantı aşağıdaki nedenlerden dolayı reddedildi: " + +#: ../app/ui.js:1145 +msgid "New connection has been rejected" +msgstr "Bağlantı reddedildi" + +#: ../app/ui.js:1166 +msgid "Password is required" +msgstr "Şifre gerekli" + +#: ../vnc.html:89 +msgid "noVNC encountered an error:" +msgstr "Bir hata oluştu:" + +#: ../vnc.html:99 +msgid "Hide/Show the control bar" +msgstr "Denetim masasını Gizle/Göster" + +#: ../vnc.html:106 +msgid "Move/Drag Viewport" +msgstr "Görünümü Taşı/Sürükle" + +#: ../vnc.html:106 +msgid "viewport drag" +msgstr "Görüntü penceresini sürükle" + +#: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121 +msgid "Active Mouse Button" +msgstr "Aktif Fare Düğmesi" + +#: ../vnc.html:112 +msgid "No mousebutton" +msgstr "Fare düğmesi yok" + +#: ../vnc.html:115 +msgid "Left mousebutton" +msgstr "Farenin sol düğmesi" + +#: ../vnc.html:118 +msgid "Middle mousebutton" +msgstr "Farenin orta düğmesi" + +#: ../vnc.html:121 +msgid "Right mousebutton" +msgstr "Farenin sağ düğmesi" + +#: ../vnc.html:124 +msgid "Keyboard" +msgstr "Klavye" + +#: ../vnc.html:124 +msgid "Show Keyboard" +msgstr "Klavye Düzenini Göster" + +#: ../vnc.html:131 +msgid "Extra keys" +msgstr "Ekstra tuşlar" + +#: ../vnc.html:131 +msgid "Show Extra Keys" +msgstr "Ekstra tuşları göster" + +#: ../vnc.html:136 +msgid "Ctrl" +msgstr "Ctrl" + +#: ../vnc.html:136 +msgid "Toggle Ctrl" +msgstr "Ctrl Değiştir " + +#: ../vnc.html:139 +msgid "Alt" +msgstr "Alt" + +#: ../vnc.html:139 +msgid "Toggle Alt" +msgstr "Alt Değiştir" + +#: ../vnc.html:142 +msgid "Send Tab" +msgstr "Sekme Gönder" + +#: ../vnc.html:142 +msgid "Tab" +msgstr "Sekme" + +#: ../vnc.html:145 +msgid "Esc" +msgstr "Esc" + +#: ../vnc.html:145 +msgid "Send Escape" +msgstr "Boşluk Gönder" + +#: ../vnc.html:148 +msgid "Ctrl+Alt+Del" +msgstr "Ctrl + Alt + Del" + +#: ../vnc.html:148 +msgid "Send Ctrl-Alt-Del" +msgstr "Ctrl-Alt-Del Gönder" + +#: ../vnc.html:156 +msgid "Shutdown/Reboot" +msgstr "Kapat/Yeniden Başlat" + +#: ../vnc.html:156 +msgid "Shutdown/Reboot..." +msgstr "Kapat/Yeniden Başlat..." + +#: ../vnc.html:162 +msgid "Power" +msgstr "Güç" + +#: ../vnc.html:164 +msgid "Shutdown" +msgstr "Kapat" + +#: ../vnc.html:165 +msgid "Reboot" +msgstr "Yeniden Başlat" + +#: ../vnc.html:166 +msgid "Reset" +msgstr "Sıfırla" + +#: ../vnc.html:171 ../vnc.html:177 +msgid "Clipboard" +msgstr "Pano" + +#: ../vnc.html:181 +msgid "Clear" +msgstr "Temizle" + +#: ../vnc.html:187 +msgid "Fullscreen" +msgstr "Tam Ekran" + +#: ../vnc.html:192 ../vnc.html:199 +msgid "Settings" +msgstr "Ayarlar" + +#: ../vnc.html:202 +msgid "Shared Mode" +msgstr "Paylaşım Modu" + +#: ../vnc.html:205 +msgid "View Only" +msgstr "Sadece Görüntüle" + +#: ../vnc.html:209 +msgid "Clip to Window" +msgstr "Pencereye Tıkla" + +#: ../vnc.html:212 +msgid "Scaling Mode:" +msgstr "Ölçekleme Modu:" + +#: ../vnc.html:214 +msgid "None" +msgstr "Bilinmeyen" + +#: ../vnc.html:215 +msgid "Local Scaling" +msgstr "Yerel Ölçeklendirme" + +#: ../vnc.html:216 +msgid "Remote Resizing" +msgstr "Uzaktan Yeniden Boyutlandırma" + +#: ../vnc.html:221 +msgid "Advanced" +msgstr "Gelişmiş" + +#: ../vnc.html:224 +msgid "Repeater ID:" +msgstr "Tekralayıcı ID:" + +#: ../vnc.html:228 +msgid "WebSocket" +msgstr "WebSocket" + +#: ../vnc.html:231 +msgid "Encrypt" +msgstr "Şifrele" + +#: ../vnc.html:234 +msgid "Host:" +msgstr "Ana makine:" + +#: ../vnc.html:238 +msgid "Port:" +msgstr "Port:" + +#: ../vnc.html:242 +msgid "Path:" +msgstr "Yol:" + +#: ../vnc.html:249 +msgid "Automatic Reconnect" +msgstr "Otomatik Yeniden Bağlan" + +#: ../vnc.html:252 +msgid "Reconnect Delay (ms):" +msgstr "Yeniden Bağlanma Süreci (ms):" + +#: ../vnc.html:258 +msgid "Logging:" +msgstr "Giriş yapılıyor:" + +#: ../vnc.html:270 +msgid "Disconnect" +msgstr "Bağlantıyı Kes" + +#: ../vnc.html:289 +msgid "Connect" +msgstr "Bağlan" + +#: ../vnc.html:299 +msgid "Password:" +msgstr "Parola:" + +#: ../vnc.html:313 +msgid "Cancel" +msgstr "Vazgeç" + +#: ../vnc.html:329 +msgid "Canvas not supported." +msgstr "Tuval desteklenmiyor." diff --git a/systemvm/debian/root/noVNC/po/xgettext-html b/systemvm/debian/root/noVNC/po/xgettext-html new file mode 100755 index 000000000000..547f5687698c --- /dev/null +++ b/systemvm/debian/root/noVNC/po/xgettext-html @@ -0,0 +1,115 @@ +#!/usr/bin/env node +/* + * xgettext-html: HTML gettext parser + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + */ + +const getopt = require('node-getopt'); +const jsdom = require("jsdom"); +const fs = require("fs"); + +const opt = getopt.create([ + ['o' , 'output=FILE' , 'write output to specified file'], + ['h' , 'help' , 'display this help'], +]).bindHelp().parseSystem(); + +const strings = {}; + +function addString(str, location) { + if (str.length == 0) { + return; + } + + if (strings[str] === undefined) { + strings[str] = {} + } + strings[str][location] = null; +} + +// See https://html.spec.whatwg.org/multipage/dom.html#attr-translate +function process(elem, locator, enabled) { + function isAnyOf(searchElement, items) { + return items.indexOf(searchElement) !== -1; + } + + if (elem.hasAttribute("translate")) { + if (isAnyOf(elem.getAttribute("translate"), ["", "yes"])) { + enabled = true; + } else if (isAnyOf(elem.getAttribute("translate"), ["no"])) { + enabled = false; + } + } + + if (enabled) { + if (elem.hasAttribute("abbr") && + elem.tagName === "TH") { + addString(elem.getAttribute("abbr"), locator(elem)); + } + if (elem.hasAttribute("alt") && + isAnyOf(elem.tagName, ["AREA", "IMG", "INPUT"])) { + addString(elem.getAttribute("alt"), locator(elem)); + } + if (elem.hasAttribute("download") && + isAnyOf(elem.tagName, ["A", "AREA"])) { + addString(elem.getAttribute("download"), locator(elem)); + } + if (elem.hasAttribute("label") && + isAnyOf(elem.tagName, ["MENUITEM", "MENU", "OPTGROUP", + "OPTION", "TRACK"])) { + addString(elem.getAttribute("label"), locator(elem)); + } + if (elem.hasAttribute("placeholder") && + isAnyOf(elem.tagName in ["INPUT", "TEXTAREA"])) { + addString(elem.getAttribute("placeholder"), locator(elem)); + } + if (elem.hasAttribute("title")) { + addString(elem.getAttribute("title"), locator(elem)); + } + if (elem.hasAttribute("value") && + elem.tagName === "INPUT" && + isAnyOf(elem.getAttribute("type"), ["reset", "button", "submit"])) { + addString(elem.getAttribute("value"), locator(elem)); + } + } + + for (let i = 0; i < elem.childNodes.length; i++) { + node = elem.childNodes[i]; + if (node.nodeType === node.ELEMENT_NODE) { + process(node, locator, enabled); + } else if (node.nodeType === node.TEXT_NODE && enabled) { + addString(node.data.trim(), locator(node)); + } + } +} + +for (let i = 0; i < opt.argv.length; i++) { + const fn = opt.argv[i]; + const file = fs.readFileSync(fn, "utf8"); + const dom = new jsdom.JSDOM(file, { includeNodeLocations: true }); + const body = dom.window.document.body; + + function locator(elem) { + const offset = dom.nodeLocation(elem).startOffset; + const line = file.slice(0, offset).split("\n").length; + return fn + ":" + line; + } + + process(body, locator, true); +} + +let output = ""; + +for (str in strings) { + output += "#:"; + for (location in strings[str]) { + output += " " + location; + } + output += "\n"; + + output += "msgid " + JSON.stringify(str) + "\n"; + output += "msgstr \"\"\n"; + output += "\n"; +} + +fs.writeFileSync(opt.options.output, output); diff --git a/systemvm/debian/root/noVNC/po/zh_CN.po b/systemvm/debian/root/noVNC/po/zh_CN.po new file mode 100644 index 000000000000..78bfb958d5d1 --- /dev/null +++ b/systemvm/debian/root/noVNC/po/zh_CN.po @@ -0,0 +1,284 @@ +# Simplified Chinese translations for noVNC package. +# Copyright (C) 2018 The noVNC Authors +# This file is distributed under the same license as the noVNC package. +# Peter Dave Hello , 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: noVNC 1.0.0-testing.2\n" +"Report-Msgid-Bugs-To: novnc@googlegroups.com\n" +"POT-Creation-Date: 2018-01-10 00:53+0800\n" +"PO-Revision-Date: 2018-04-06 21:33+0800\n" +"Last-Translator: CUI Wei \n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: ../app/ui.js:395 +msgid "Connecting..." +msgstr "链接中..." + +#: ../app/ui.js:402 +msgid "Disconnecting..." +msgstr "正在中断连接..." + +#: ../app/ui.js:408 +msgid "Reconnecting..." +msgstr "重新链接中..." + +#: ../app/ui.js:413 +msgid "Internal error" +msgstr "内部错误" + +#: ../app/ui.js:1015 +msgid "Must set host" +msgstr "请提供主机名" + +#: ../app/ui.js:1097 +msgid "Connected (encrypted) to " +msgstr "已加密链接到" + +#: ../app/ui.js:1099 +msgid "Connected (unencrypted) to " +msgstr "未加密链接到" + +#: ../app/ui.js:1120 +msgid "Something went wrong, connection is closed" +msgstr "发生错误,链接已关闭" + +#: ../app/ui.js:1123 +msgid "Failed to connect to server" +msgstr "无法链接到服务器" + +#: ../app/ui.js:1133 +msgid "Disconnected" +msgstr "链接已中断" + +#: ../app/ui.js:1146 +msgid "New connection has been rejected with reason: " +msgstr "链接被拒绝,原因:" + +#: ../app/ui.js:1149 +msgid "New connection has been rejected" +msgstr "链接被拒绝" + +#: ../app/ui.js:1170 +msgid "Password is required" +msgstr "请提供密码" + +#: ../vnc.html:89 +msgid "noVNC encountered an error:" +msgstr "noVNC 遇到一个错误:" + +#: ../vnc.html:99 +msgid "Hide/Show the control bar" +msgstr "显示/隐藏控制列" + +#: ../vnc.html:106 +msgid "Move/Drag Viewport" +msgstr "拖放显示范围" + +#: ../vnc.html:106 +msgid "viewport drag" +msgstr "显示范围拖放" + +#: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121 +msgid "Active Mouse Button" +msgstr "启动鼠标按鍵" + +#: ../vnc.html:112 +msgid "No mousebutton" +msgstr "禁用鼠标按鍵" + +#: ../vnc.html:115 +msgid "Left mousebutton" +msgstr "鼠标左鍵" + +#: ../vnc.html:118 +msgid "Middle mousebutton" +msgstr "鼠标中鍵" + +#: ../vnc.html:121 +msgid "Right mousebutton" +msgstr "鼠标右鍵" + +#: ../vnc.html:124 +msgid "Keyboard" +msgstr "键盘" + +#: ../vnc.html:124 +msgid "Show Keyboard" +msgstr "显示键盘" + +#: ../vnc.html:131 +msgid "Extra keys" +msgstr "额外按键" + +#: ../vnc.html:131 +msgid "Show Extra Keys" +msgstr "显示额外按键" + +#: ../vnc.html:136 +msgid "Ctrl" +msgstr "Ctrl" + +#: ../vnc.html:136 +msgid "Toggle Ctrl" +msgstr "切换 Ctrl" + +#: ../vnc.html:139 +msgid "Alt" +msgstr "Alt" + +#: ../vnc.html:139 +msgid "Toggle Alt" +msgstr "切换 Alt" + +#: ../vnc.html:142 +msgid "Send Tab" +msgstr "发送 Tab 键" + +#: ../vnc.html:142 +msgid "Tab" +msgstr "Tab" + +#: ../vnc.html:145 +msgid "Esc" +msgstr "Esc" + +#: ../vnc.html:145 +msgid "Send Escape" +msgstr "发送 Escape 键" + +#: ../vnc.html:148 +msgid "Ctrl+Alt+Del" +msgstr "Ctrl-Alt-Del" + +#: ../vnc.html:148 +msgid "Send Ctrl-Alt-Del" +msgstr "发送 Ctrl-Alt-Del 键" + +#: ../vnc.html:156 +msgid "Shutdown/Reboot" +msgstr "关机/重新启动" + +#: ../vnc.html:156 +msgid "Shutdown/Reboot..." +msgstr "关机/重新启动..." + +#: ../vnc.html:162 +msgid "Power" +msgstr "电源" + +#: ../vnc.html:164 +msgid "Shutdown" +msgstr "关机" + +#: ../vnc.html:165 +msgid "Reboot" +msgstr "重新启动" + +#: ../vnc.html:166 +msgid "Reset" +msgstr "重置" + +#: ../vnc.html:171 ../vnc.html:177 +msgid "Clipboard" +msgstr "剪贴板" + +#: ../vnc.html:181 +msgid "Clear" +msgstr "清除" + +#: ../vnc.html:187 +msgid "Fullscreen" +msgstr "全屏幕" + +#: ../vnc.html:192 ../vnc.html:199 +msgid "Settings" +msgstr "设置" + +#: ../vnc.html:202 +msgid "Shared Mode" +msgstr "分享模式" + +#: ../vnc.html:205 +msgid "View Only" +msgstr "仅检视" + +#: ../vnc.html:209 +msgid "Clip to Window" +msgstr "限制/裁切窗口大小" + +#: ../vnc.html:212 +msgid "Scaling Mode:" +msgstr "缩放模式:" + +#: ../vnc.html:214 +msgid "None" +msgstr "无" + +#: ../vnc.html:215 +msgid "Local Scaling" +msgstr "本地缩放" + +#: ../vnc.html:216 +msgid "Remote Resizing" +msgstr "远程调整大小" + +#: ../vnc.html:221 +msgid "Advanced" +msgstr "高级" + +#: ../vnc.html:224 +msgid "Repeater ID:" +msgstr "中继站 ID" + +#: ../vnc.html:228 +msgid "WebSocket" +msgstr "WebSocket" + +#: ../vnc.html:231 +msgid "Encrypt" +msgstr "加密" + +#: ../vnc.html:234 +msgid "Host:" +msgstr "主机:" + +#: ../vnc.html:238 +msgid "Port:" +msgstr "端口:" + +#: ../vnc.html:242 +msgid "Path:" +msgstr "路径:" + +#: ../vnc.html:249 +msgid "Automatic Reconnect" +msgstr "自动重新链接" + +#: ../vnc.html:252 +msgid "Reconnect Delay (ms):" +msgstr "重新链接间隔 (ms):" + +#: ../vnc.html:258 +msgid "Logging:" +msgstr "日志级别:" + +#: ../vnc.html:270 +msgid "Disconnect" +msgstr "终端链接" + +#: ../vnc.html:289 +msgid "Connect" +msgstr "链接" + +#: ../vnc.html:299 +msgid "Password:" +msgstr "密码:" + +#: ../vnc.html:313 +msgid "Cancel" +msgstr "取消" diff --git a/systemvm/debian/root/noVNC/po/zh_TW.po b/systemvm/debian/root/noVNC/po/zh_TW.po new file mode 100644 index 000000000000..9ddf550c1d2d --- /dev/null +++ b/systemvm/debian/root/noVNC/po/zh_TW.po @@ -0,0 +1,285 @@ +# Traditional Chinese translations for noVNC package. +# Copyright (C) 2018 The noVNC Authors +# This file is distributed under the same license as the noVNC package. +# Peter Dave Hello , 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: noVNC 1.0.0-testing.2\n" +"Report-Msgid-Bugs-To: novnc@googlegroups.com\n" +"POT-Creation-Date: 2018-01-10 00:53+0800\n" +"PO-Revision-Date: 2018-01-10 01:33+0800\n" +"Last-Translator: Peter Dave Hello \n" +"Language-Team: Peter Dave Hello \n" +"Language: zh\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: ../app/ui.js:395 +msgid "Connecting..." +msgstr "連線中..." + +#: ../app/ui.js:402 +msgid "Disconnecting..." +msgstr "正在中斷連線..." + +#: ../app/ui.js:408 +msgid "Reconnecting..." +msgstr "重新連線中..." + +#: ../app/ui.js:413 +msgid "Internal error" +msgstr "內部錯誤" + +#: ../app/ui.js:1015 +msgid "Must set host" +msgstr "請提供主機資訊" + +#: ../app/ui.js:1097 +msgid "Connected (encrypted) to " +msgstr "已加密連線到" + +#: ../app/ui.js:1099 +msgid "Connected (unencrypted) to " +msgstr "未加密連線到" + +#: ../app/ui.js:1120 +msgid "Something went wrong, connection is closed" +msgstr "發生錯誤,連線已關閉" + +#: ../app/ui.js:1123 +msgid "Failed to connect to server" +msgstr "無法連線到伺服器" + +#: ../app/ui.js:1133 +msgid "Disconnected" +msgstr "連線已中斷" + +#: ../app/ui.js:1146 +msgid "New connection has been rejected with reason: " +msgstr "連線被拒絕,原因:" + +#: ../app/ui.js:1149 +msgid "New connection has been rejected" +msgstr "連線被拒絕" + +#: ../app/ui.js:1170 +msgid "Password is required" +msgstr "請提供密碼" + +#: ../vnc.html:89 +msgid "noVNC encountered an error:" +msgstr "noVNC 遇到一個錯誤:" + +#: ../vnc.html:99 +msgid "Hide/Show the control bar" +msgstr "顯示/隱藏控制列" + +#: ../vnc.html:106 +msgid "Move/Drag Viewport" +msgstr "拖放顯示範圍" + +#: ../vnc.html:106 +msgid "viewport drag" +msgstr "顯示範圍拖放" + +#: ../vnc.html:112 ../vnc.html:115 ../vnc.html:118 ../vnc.html:121 +msgid "Active Mouse Button" +msgstr "啟用滑鼠按鍵" + +#: ../vnc.html:112 +msgid "No mousebutton" +msgstr "無滑鼠按鍵" + +#: ../vnc.html:115 +msgid "Left mousebutton" +msgstr "滑鼠左鍵" + +#: ../vnc.html:118 +msgid "Middle mousebutton" +msgstr "滑鼠中鍵" + +#: ../vnc.html:121 +msgid "Right mousebutton" +msgstr "滑鼠右鍵" + +#: ../vnc.html:124 +msgid "Keyboard" +msgstr "鍵盤" + +#: ../vnc.html:124 +msgid "Show Keyboard" +msgstr "顯示鍵盤" + +#: ../vnc.html:131 +msgid "Extra keys" +msgstr "額外按鍵" + +#: ../vnc.html:131 +msgid "Show Extra Keys" +msgstr "顯示額外按鍵" + +#: ../vnc.html:136 +msgid "Ctrl" +msgstr "Ctrl" + +#: ../vnc.html:136 +msgid "Toggle Ctrl" +msgstr "切換 Ctrl" + +#: ../vnc.html:139 +msgid "Alt" +msgstr "Alt" + +#: ../vnc.html:139 +msgid "Toggle Alt" +msgstr "切換 Alt" + +#: ../vnc.html:142 +msgid "Send Tab" +msgstr "送出 Tab 鍵" + +#: ../vnc.html:142 +msgid "Tab" +msgstr "Tab" + +#: ../vnc.html:145 +msgid "Esc" +msgstr "Esc" + +#: ../vnc.html:145 +msgid "Send Escape" +msgstr "送出 Escape 鍵" + +#: ../vnc.html:148 +msgid "Ctrl+Alt+Del" +msgstr "Ctrl-Alt-Del" + +#: ../vnc.html:148 +msgid "Send Ctrl-Alt-Del" +msgstr "送出 Ctrl-Alt-Del 快捷鍵" + +#: ../vnc.html:156 +msgid "Shutdown/Reboot" +msgstr "關機/重新啟動" + +#: ../vnc.html:156 +msgid "Shutdown/Reboot..." +msgstr "關機/重新啟動..." + +#: ../vnc.html:162 +msgid "Power" +msgstr "電源" + +#: ../vnc.html:164 +msgid "Shutdown" +msgstr "關機" + +#: ../vnc.html:165 +msgid "Reboot" +msgstr "重新啟動" + +#: ../vnc.html:166 +msgid "Reset" +msgstr "重設" + +#: ../vnc.html:171 ../vnc.html:177 +msgid "Clipboard" +msgstr "剪貼簿" + +#: ../vnc.html:181 +msgid "Clear" +msgstr "清除" + +#: ../vnc.html:187 +msgid "Fullscreen" +msgstr "全螢幕" + +#: ../vnc.html:192 ../vnc.html:199 +msgid "Settings" +msgstr "設定" + +#: ../vnc.html:202 +msgid "Shared Mode" +msgstr "分享模式" + +#: ../vnc.html:205 +msgid "View Only" +msgstr "僅檢視" + +#: ../vnc.html:209 +msgid "Clip to Window" +msgstr "限制/裁切視窗大小" + +#: ../vnc.html:212 +msgid "Scaling Mode:" +msgstr "縮放模式:" + +#: ../vnc.html:214 +msgid "None" +msgstr "無" + +#: ../vnc.html:215 +msgid "Local Scaling" +msgstr "本機縮放" + +#: ../vnc.html:216 +msgid "Remote Resizing" +msgstr "遠端調整大小" + +#: ../vnc.html:221 +msgid "Advanced" +msgstr "進階" + +#: ../vnc.html:224 +msgid "Repeater ID:" +msgstr "中繼站 ID" + +#: ../vnc.html:228 +msgid "WebSocket" +msgstr "WebSocket" + +#: ../vnc.html:231 +msgid "Encrypt" +msgstr "加密" + +#: ../vnc.html:234 +msgid "Host:" +msgstr "主機:" + +#: ../vnc.html:238 +msgid "Port:" +msgstr "連接埠:" + +#: ../vnc.html:242 +msgid "Path:" +msgstr "路徑:" + +#: ../vnc.html:249 +msgid "Automatic Reconnect" +msgstr "自動重新連線" + +#: ../vnc.html:252 +msgid "Reconnect Delay (ms):" +msgstr "重新連線間隔 (ms):" + +#: ../vnc.html:258 +msgid "Logging:" +msgstr "日誌級別:" + +#: ../vnc.html:270 +msgid "Disconnect" +msgstr "中斷連線" + +#: ../vnc.html:289 +msgid "Connect" +msgstr "連線" + +#: ../vnc.html:299 +msgid "Password:" +msgstr "密碼:" + +#: ../vnc.html:313 +msgid "Cancel" +msgstr "取消" diff --git a/systemvm/debian/root/noVNC/tests/.eslintrc b/systemvm/debian/root/noVNC/tests/.eslintrc new file mode 100644 index 000000000000..545fa2ed25ed --- /dev/null +++ b/systemvm/debian/root/noVNC/tests/.eslintrc @@ -0,0 +1,15 @@ +{ + "env": { + "node": true, + "mocha": true + }, + "globals": { + "chai": false, + "sinon": false + }, + "rules": { + "prefer-arrow-callback": 0, + // Too many anonymous callbacks + "func-names": "off", + } +} diff --git a/systemvm/debian/root/noVNC/tests/assertions.js b/systemvm/debian/root/noVNC/tests/assertions.js new file mode 100644 index 000000000000..07a5c2977686 --- /dev/null +++ b/systemvm/debian/root/noVNC/tests/assertions.js @@ -0,0 +1,101 @@ +// noVNC specific assertions +chai.use(function (_chai, utils) { + _chai.Assertion.addMethod('displayed', function (target_data) { + const obj = this._obj; + const ctx = obj._target.getContext('2d'); + const data_cl = ctx.getImageData(0, 0, obj._target.width, obj._target.height).data; + // NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray, so work around that + const data = new Uint8Array(data_cl); + const len = data_cl.length; + new chai.Assertion(len).to.be.equal(target_data.length, "unexpected display size"); + let same = true; + for (let i = 0; i < len; i++) { + if (data[i] != target_data[i]) { + same = false; + break; + } + } + if (!same) { + // eslint-disable-next-line no-console + console.log("expected data: %o, actual data: %o", target_data, data); + } + this.assert(same, + "expected #{this} to have displayed the image #{exp}, but instead it displayed #{act}", + "expected #{this} not to have displayed the image #{act}", + target_data, + data); + }); + + _chai.Assertion.addMethod('sent', function (target_data) { + const obj = this._obj; + obj.inspect = () => { + const res = { _websocket: obj._websocket, rQi: obj._rQi, _rQ: new Uint8Array(obj._rQ.buffer, 0, obj._rQlen), + _sQ: new Uint8Array(obj._sQ.buffer, 0, obj._sQlen) }; + res.prototype = obj; + return res; + }; + const data = obj._websocket._get_sent_data(); + let same = true; + if (data.length != target_data.length) { + same = false; + } else { + for (let i = 0; i < data.length; i++) { + if (data[i] != target_data[i]) { + same = false; + break; + } + } + } + if (!same) { + // eslint-disable-next-line no-console + console.log("expected data: %o, actual data: %o", target_data, data); + } + this.assert(same, + "expected #{this} to have sent the data #{exp}, but it actually sent #{act}", + "expected #{this} not to have sent the data #{act}", + Array.prototype.slice.call(target_data), + Array.prototype.slice.call(data)); + }); + + _chai.Assertion.addProperty('array', function () { + utils.flag(this, 'array', true); + }); + + _chai.Assertion.overwriteMethod('equal', function (_super) { + return function assertArrayEqual(target) { + if (utils.flag(this, 'array')) { + const obj = this._obj; + + let same = true; + + if (utils.flag(this, 'deep')) { + for (let i = 0; i < obj.length; i++) { + if (!utils.eql(obj[i], target[i])) { + same = false; + break; + } + } + + this.assert(same, + "expected #{this} to have elements deeply equal to #{exp}", + "expected #{this} not to have elements deeply equal to #{exp}", + Array.prototype.slice.call(target)); + } else { + for (let i = 0; i < obj.length; i++) { + if (obj[i] != target[i]) { + same = false; + break; + } + } + + this.assert(same, + "expected #{this} to have elements equal to #{exp}", + "expected #{this} not to have elements equal to #{exp}", + Array.prototype.slice.call(target)); + } + } else { + _super.apply(this, arguments); + } + }; + }); +}); diff --git a/systemvm/debian/root/noVNC/tests/fake.websocket.js b/systemvm/debian/root/noVNC/tests/fake.websocket.js new file mode 100644 index 000000000000..68ab3f8487d4 --- /dev/null +++ b/systemvm/debian/root/noVNC/tests/fake.websocket.js @@ -0,0 +1,96 @@ +import Base64 from '../core/base64.js'; + +// PhantomJS can't create Event objects directly, so we need to use this +function make_event(name, props) { + const evt = document.createEvent('Event'); + evt.initEvent(name, true, true); + if (props) { + for (let prop in props) { + evt[prop] = props[prop]; + } + } + return evt; +} + +export default class FakeWebSocket { + constructor(uri, protocols) { + this.url = uri; + this.binaryType = "arraybuffer"; + this.extensions = ""; + + if (!protocols || typeof protocols === 'string') { + this.protocol = protocols; + } else { + this.protocol = protocols[0]; + } + + this._send_queue = new Uint8Array(20000); + + this.readyState = FakeWebSocket.CONNECTING; + this.bufferedAmount = 0; + + this.__is_fake = true; + } + + close(code, reason) { + this.readyState = FakeWebSocket.CLOSED; + if (this.onclose) { + this.onclose(make_event("close", { 'code': code, 'reason': reason, 'wasClean': true })); + } + } + + send(data) { + if (this.protocol == 'base64') { + data = Base64.decode(data); + } else { + data = new Uint8Array(data); + } + this._send_queue.set(data, this.bufferedAmount); + this.bufferedAmount += data.length; + } + + _get_sent_data() { + const res = new Uint8Array(this._send_queue.buffer, 0, this.bufferedAmount); + this.bufferedAmount = 0; + return res; + } + + _open() { + this.readyState = FakeWebSocket.OPEN; + if (this.onopen) { + this.onopen(make_event('open')); + } + } + + _receive_data(data) { + // Break apart the data to expose bugs where we assume data is + // neatly packaged + for (let i = 0;i < data.length;i++) { + let buf = data.subarray(i, i+1); + this.onmessage(make_event("message", { 'data': buf })); + } + } +} + +FakeWebSocket.OPEN = WebSocket.OPEN; +FakeWebSocket.CONNECTING = WebSocket.CONNECTING; +FakeWebSocket.CLOSING = WebSocket.CLOSING; +FakeWebSocket.CLOSED = WebSocket.CLOSED; + +FakeWebSocket.__is_fake = true; + +FakeWebSocket.replace = () => { + if (!WebSocket.__is_fake) { + const real_version = WebSocket; + // eslint-disable-next-line no-global-assign + WebSocket = FakeWebSocket; + FakeWebSocket.__real_version = real_version; + } +}; + +FakeWebSocket.restore = () => { + if (WebSocket.__is_fake) { + // eslint-disable-next-line no-global-assign + WebSocket = WebSocket.__real_version; + } +}; diff --git a/systemvm/debian/root/noVNC/tests/karma-test-main.js b/systemvm/debian/root/noVNC/tests/karma-test-main.js new file mode 100644 index 000000000000..28436667e6d1 --- /dev/null +++ b/systemvm/debian/root/noVNC/tests/karma-test-main.js @@ -0,0 +1,48 @@ +const TEST_REGEXP = /test\..*\.js/; +const allTestFiles = []; +const extraFiles = ['/base/tests/assertions.js']; + +Object.keys(window.__karma__.files).forEach(function (file) { + if (TEST_REGEXP.test(file)) { + // TODO: normalize? + allTestFiles.push(file); + } +}); + +// Stub out mocha's start function so we can run it once we're done loading +mocha.origRun = mocha.run; +mocha.run = function () {}; + +let script; + +// Script to import all our tests +script = document.createElement("script"); +script.type = "module"; +script.text = ""; +let allModules = allTestFiles.concat(extraFiles); +allModules.forEach(function (file) { + script.text += "import \"" + file + "\";\n"; +}); +script.text += "\nmocha.origRun();\n"; +document.body.appendChild(script); + +// Fallback code for browsers that don't support modules (IE) +script = document.createElement("script"); +script.type = "module"; +script.text = "window._noVNC_has_module_support = true;\n"; +document.body.appendChild(script); + +function fallback() { + if (!window._noVNC_has_module_support) { + /* eslint-disable no-console */ + if (console) { + console.log("No module support detected. Loading fallback..."); + } + /* eslint-enable no-console */ + let loader = document.createElement("script"); + loader.src = "base/vendor/browser-es-module-loader/dist/browser-es-module-loader.js"; + document.body.appendChild(loader); + } +} + +setTimeout(fallback, 500); diff --git a/systemvm/debian/root/noVNC/tests/playback-ui.js b/systemvm/debian/root/noVNC/tests/playback-ui.js new file mode 100644 index 000000000000..65c715a9fe55 --- /dev/null +++ b/systemvm/debian/root/noVNC/tests/playback-ui.js @@ -0,0 +1,210 @@ +/* global VNC_frame_data, VNC_frame_encoding */ + +import * as WebUtil from '../app/webutil.js'; +import RecordingPlayer from './playback.js'; +import Base64 from '../core/base64.js'; + +let frames = null; + +function message(str) { + const cell = document.getElementById('messages'); + cell.textContent += str + "\n"; + cell.scrollTop = cell.scrollHeight; +} + +function loadFile() { + const fname = WebUtil.getQueryVar('data', null); + + if (!fname) { + return Promise.reject("Must specify data=FOO in query string."); + } + + message("Loading " + fname + "..."); + + return new Promise((resolve, reject) => { + const script = document.createElement("script"); + script.onload = resolve; + script.onerror = reject; + document.body.appendChild(script); + script.src = "../recordings/" + fname; + }); +} + +function enableUI() { + const iterations = WebUtil.getQueryVar('iterations', 3); + document.getElementById('iterations').value = iterations; + + const mode = WebUtil.getQueryVar('mode', 3); + if (mode === 'realtime') { + document.getElementById('mode2').checked = true; + } else { + document.getElementById('mode1').checked = true; + } + + message("Loaded " + VNC_frame_data.length + " frames"); + + const startButton = document.getElementById('startButton'); + startButton.disabled = false; + startButton.addEventListener('click', start); + + message("Converting..."); + + frames = VNC_frame_data; + + let encoding; + // Only present in older recordings + if (window.VNC_frame_encoding) { + encoding = VNC_frame_encoding; + } else { + let frame = frames[0]; + let start = frame.indexOf('{', 1) + 1; + if (frame.slice(start, start+4) === 'UkZC') { + encoding = 'base64'; + } else { + encoding = 'binary'; + } + } + + for (let i = 0;i < frames.length;i++) { + let frame = frames[i]; + + if (frame === "EOF") { + frames.splice(i); + break; + } + + let dataIdx = frame.indexOf('{', 1) + 1; + + let time = parseInt(frame.slice(1, dataIdx - 1)); + + let u8; + if (encoding === 'base64') { + u8 = Base64.decode(frame.slice(dataIdx)); + } else { + u8 = new Uint8Array(frame.length - dataIdx); + for (let j = 0; j < frame.length - dataIdx; j++) { + u8[j] = frame.charCodeAt(dataIdx + j); + } + } + + frames[i] = { fromClient: frame[0] === '}', + timestamp: time, + data: u8 }; + } + + message("Ready"); +} + +class IterationPlayer { + constructor(iterations, frames) { + this._iterations = iterations; + + this._iteration = undefined; + this._player = undefined; + + this._start_time = undefined; + + this._frames = frames; + + this._state = 'running'; + + this.onfinish = () => {}; + this.oniterationfinish = () => {}; + this.rfbdisconnected = () => {}; + } + + start(realtime) { + this._iteration = 0; + this._start_time = (new Date()).getTime(); + + this._realtime = realtime; + + this._nextIteration(); + } + + _nextIteration() { + const player = new RecordingPlayer(this._frames, this._disconnected.bind(this)); + player.onfinish = this._iterationFinish.bind(this); + + if (this._state !== 'running') { return; } + + this._iteration++; + if (this._iteration > this._iterations) { + this._finish(); + return; + } + + player.run(this._realtime, false); + } + + _finish() { + const endTime = (new Date()).getTime(); + const totalDuration = endTime - this._start_time; + + const evt = new CustomEvent('finish', + { detail: + { duration: totalDuration, + iterations: this._iterations } } ); + this.onfinish(evt); + } + + _iterationFinish(duration) { + const evt = new CustomEvent('iterationfinish', + { detail: + { duration: duration, + number: this._iteration } } ); + this.oniterationfinish(evt); + + this._nextIteration(); + } + + _disconnected(clean, frame) { + if (!clean) { + this._state = 'failed'; + } + + const evt = new CustomEvent('rfbdisconnected', + { detail: + { clean: clean, + frame: frame, + iteration: this._iteration } } ); + this.onrfbdisconnected(evt); + } +} + +function start() { + document.getElementById('startButton').value = "Running"; + document.getElementById('startButton').disabled = true; + + const iterations = document.getElementById('iterations').value; + + let realtime; + + if (document.getElementById('mode1').checked) { + message(`Starting performance playback (fullspeed) [${iterations} iteration(s)]`); + realtime = false; + } else { + message(`Starting realtime playback [${iterations} iteration(s)]`); + realtime = true; + } + + const player = new IterationPlayer(iterations, frames); + player.oniterationfinish = (evt) => { + message(`Iteration ${evt.detail.number} took ${evt.detail.duration}ms`); + }; + player.onrfbdisconnected = (evt) => { + if (!evt.detail.clean) { + message(`noVNC sent disconnected during iteration ${evt.detail.iteration} frame ${evt.detail.frame}`); + } + }; + player.onfinish = (evt) => { + const iterTime = parseInt(evt.detail.duration / evt.detail.iterations, 10); + message(`${evt.detail.iterations} iterations took ${evt.detail.duration}ms (average ${iterTime}ms / iteration)`); + + document.getElementById('startButton').disabled = false; + document.getElementById('startButton').value = "Start"; + }; + player.start(realtime); +} + +loadFile().then(enableUI).catch(e => message("Error loading recording: " + e)); diff --git a/systemvm/debian/root/noVNC/tests/playback.js b/systemvm/debian/root/noVNC/tests/playback.js new file mode 100644 index 000000000000..5bd8103a840e --- /dev/null +++ b/systemvm/debian/root/noVNC/tests/playback.js @@ -0,0 +1,172 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + */ + +import RFB from '../core/rfb.js'; +import * as Log from '../core/util/logging.js'; + +// Immediate polyfill +if (window.setImmediate === undefined) { + let _immediateIdCounter = 1; + const _immediateFuncs = {}; + + window.setImmediate = (func) => { + const index = _immediateIdCounter++; + _immediateFuncs[index] = func; + window.postMessage("noVNC immediate trigger:" + index, "*"); + return index; + }; + + window.clearImmediate = (id) => { + _immediateFuncs[id]; + }; + + window.addEventListener("message", (event) => { + if ((typeof event.data !== "string") || + (event.data.indexOf("noVNC immediate trigger:") !== 0)) { + return; + } + + const index = event.data.slice("noVNC immediate trigger:".length); + + const callback = _immediateFuncs[index]; + if (callback === undefined) { + return; + } + + delete _immediateFuncs[index]; + + callback(); + }); +} + +export default class RecordingPlayer { + constructor(frames, disconnected) { + this._frames = frames; + + this._disconnected = disconnected; + + this._rfb = undefined; + this._frame_length = this._frames.length; + + this._frame_index = 0; + this._start_time = undefined; + this._realtime = true; + this._trafficManagement = true; + + this._running = false; + + this.onfinish = () => {}; + } + + run(realtime, trafficManagement) { + // initialize a new RFB + this._rfb = new RFB(document.getElementById('VNC_screen'), 'wss://test'); + this._rfb.viewOnly = true; + this._rfb.addEventListener("disconnect", + this._handleDisconnect.bind(this)); + this._rfb.addEventListener("credentialsrequired", + this._handleCredentials.bind(this)); + this._enablePlaybackMode(); + + // reset the frame index and timer + this._frame_index = 0; + this._start_time = (new Date()).getTime(); + + this._realtime = realtime; + this._trafficManagement = (trafficManagement === undefined) ? !realtime : trafficManagement; + + this._running = true; + } + + // _enablePlaybackMode mocks out things not required for running playback + _enablePlaybackMode() { + const self = this; + this._rfb._sock.send = () => {}; + this._rfb._sock.close = () => {}; + this._rfb._sock.flush = () => {}; + this._rfb._sock.open = function () { + this.init(); + this._eventHandlers.open(); + self._queueNextPacket(); + }; + } + + _queueNextPacket() { + if (!this._running) { return; } + + let frame = this._frames[this._frame_index]; + + // skip send frames + while (this._frame_index < this._frame_length && frame.fromClient) { + this._frame_index++; + frame = this._frames[this._frame_index]; + } + + if (this._frame_index >= this._frame_length) { + Log.Debug('Finished, no more frames'); + this._finish(); + return; + } + + if (this._realtime) { + const toffset = (new Date()).getTime() - this._start_time; + let delay = frame.timestamp - toffset; + if (delay < 1) delay = 1; + + setTimeout(this._doPacket.bind(this), delay); + } else { + setImmediate(this._doPacket.bind(this)); + } + } + + _doPacket() { + // Avoid having excessive queue buildup in non-realtime mode + if (this._trafficManagement && this._rfb._flushing) { + const orig = this._rfb._display.onflush; + this._rfb._display.onflush = () => { + this._rfb._display.onflush = orig; + this._rfb._onFlush(); + this._doPacket(); + }; + return; + } + + const frame = this._frames[this._frame_index]; + + this._rfb._sock._recv_message({'data': frame.data}); + this._frame_index++; + + this._queueNextPacket(); + } + + _finish() { + if (this._rfb._display.pending()) { + this._rfb._display.onflush = () => { + if (this._rfb._flushing) { + this._rfb._onFlush(); + } + this._finish(); + }; + this._rfb._display.flush(); + } else { + this._running = false; + this._rfb._sock._eventHandlers.close({code: 1000, reason: ""}); + delete this._rfb; + this.onfinish((new Date()).getTime() - this._start_time); + } + } + + _handleDisconnect(evt) { + this._running = false; + this._disconnected(evt.detail.clean, this._frame_index); + } + + _handleCredentials(evt) { + this._rfb.sendCredentials({"username": "Foo", + "password": "Bar", + "target": "Baz"}); + } +} diff --git a/systemvm/debian/root/noVNC/tests/test.base64.js b/systemvm/debian/root/noVNC/tests/test.base64.js new file mode 100644 index 000000000000..04bd207b7cf8 --- /dev/null +++ b/systemvm/debian/root/noVNC/tests/test.base64.js @@ -0,0 +1,33 @@ +const expect = chai.expect; + +import Base64 from '../core/base64.js'; + +describe('Base64 Tools', function () { + "use strict"; + + const BIN_ARR = new Array(256); + for (let i = 0; i < 256; i++) { + BIN_ARR[i] = i; + } + + const B64_STR = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w=="; + + + describe('encode', function () { + it('should encode a binary string into Base64', function () { + const encoded = Base64.encode(BIN_ARR); + expect(encoded).to.equal(B64_STR); + }); + }); + + describe('decode', function () { + it('should decode a Base64 string into a normal string', function () { + const decoded = Base64.decode(B64_STR); + expect(decoded).to.deep.equal(BIN_ARR); + }); + + it('should throw an error if we have extra characters at the end of the string', function () { + expect(() => Base64.decode(B64_STR+'abcdef')).to.throw(Error); + }); + }); +}); diff --git a/systemvm/debian/root/noVNC/tests/test.display.js b/systemvm/debian/root/noVNC/tests/test.display.js new file mode 100644 index 000000000000..b359550326de --- /dev/null +++ b/systemvm/debian/root/noVNC/tests/test.display.js @@ -0,0 +1,486 @@ +const expect = chai.expect; + +import Base64 from '../core/base64.js'; +import Display from '../core/display.js'; + +describe('Display/Canvas Helper', function () { + const checked_data = new Uint8Array([ + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 + ]); + + const basic_data = new Uint8Array([0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0xff, 0xff, 0xff, 255]); + + function make_image_canvas(input_data) { + const canvas = document.createElement('canvas'); + canvas.width = 4; + canvas.height = 4; + const ctx = canvas.getContext('2d'); + const data = ctx.createImageData(4, 4); + for (let i = 0; i < checked_data.length; i++) { data.data[i] = input_data[i]; } + ctx.putImageData(data, 0, 0); + return canvas; + } + + function make_image_png(input_data) { + const canvas = make_image_canvas(input_data); + const url = canvas.toDataURL(); + const data = url.split(",")[1]; + return Base64.decode(data); + } + + describe('viewport handling', function () { + let display; + beforeEach(function () { + display = new Display(document.createElement('canvas')); + display.clipViewport = true; + display.resize(5, 5); + display.viewportChangeSize(3, 3); + display.viewportChangePos(1, 1); + }); + + it('should take viewport location into consideration when drawing images', function () { + display.resize(4, 4); + display.viewportChangeSize(2, 2); + display.drawImage(make_image_canvas(basic_data), 1, 1); + display.flip(); + + const expected = new Uint8Array(16); + for (let i = 0; i < 8; i++) { expected[i] = basic_data[i]; } + for (let i = 8; i < 16; i++) { expected[i] = 0; } + expect(display).to.have.displayed(expected); + }); + + it('should resize the target canvas when resizing the viewport', function () { + display.viewportChangeSize(2, 2); + expect(display._target.width).to.equal(2); + expect(display._target.height).to.equal(2); + }); + + it('should move the viewport if necessary', function () { + display.viewportChangeSize(5, 5); + expect(display.absX(0)).to.equal(0); + expect(display.absY(0)).to.equal(0); + expect(display._target.width).to.equal(5); + expect(display._target.height).to.equal(5); + }); + + it('should limit the viewport to the framebuffer size', function () { + display.viewportChangeSize(6, 6); + expect(display._target.width).to.equal(5); + expect(display._target.height).to.equal(5); + }); + + it('should redraw when moving the viewport', function () { + display.flip = sinon.spy(); + display.viewportChangePos(-1, 1); + expect(display.flip).to.have.been.calledOnce; + }); + + it('should redraw when resizing the viewport', function () { + display.flip = sinon.spy(); + display.viewportChangeSize(2, 2); + expect(display.flip).to.have.been.calledOnce; + }); + + it('should show the entire framebuffer when disabling the viewport', function () { + display.clipViewport = false; + expect(display.absX(0)).to.equal(0); + expect(display.absY(0)).to.equal(0); + expect(display._target.width).to.equal(5); + expect(display._target.height).to.equal(5); + }); + + it('should ignore viewport changes when the viewport is disabled', function () { + display.clipViewport = false; + display.viewportChangeSize(2, 2); + display.viewportChangePos(1, 1); + expect(display.absX(0)).to.equal(0); + expect(display.absY(0)).to.equal(0); + expect(display._target.width).to.equal(5); + expect(display._target.height).to.equal(5); + }); + + it('should show the entire framebuffer just after enabling the viewport', function () { + display.clipViewport = false; + display.clipViewport = true; + expect(display.absX(0)).to.equal(0); + expect(display.absY(0)).to.equal(0); + expect(display._target.width).to.equal(5); + expect(display._target.height).to.equal(5); + }); + }); + + describe('resizing', function () { + let display; + beforeEach(function () { + display = new Display(document.createElement('canvas')); + display.clipViewport = false; + display.resize(4, 4); + }); + + it('should change the size of the logical canvas', function () { + display.resize(5, 7); + expect(display._fb_width).to.equal(5); + expect(display._fb_height).to.equal(7); + }); + + it('should keep the framebuffer data', function () { + display.fillRect(0, 0, 4, 4, [0, 0, 0xff]); + display.resize(2, 2); + display.flip(); + const expected = []; + for (let i = 0; i < 4 * 2*2; i += 4) { + expected[i] = 0xff; + expected[i+1] = expected[i+2] = 0; + expected[i+3] = 0xff; + } + expect(display).to.have.displayed(new Uint8Array(expected)); + }); + + describe('viewport', function () { + beforeEach(function () { + display.clipViewport = true; + display.viewportChangeSize(3, 3); + display.viewportChangePos(1, 1); + }); + + it('should keep the viewport position and size if possible', function () { + display.resize(6, 6); + expect(display.absX(0)).to.equal(1); + expect(display.absY(0)).to.equal(1); + expect(display._target.width).to.equal(3); + expect(display._target.height).to.equal(3); + }); + + it('should move the viewport if necessary', function () { + display.resize(3, 3); + expect(display.absX(0)).to.equal(0); + expect(display.absY(0)).to.equal(0); + expect(display._target.width).to.equal(3); + expect(display._target.height).to.equal(3); + }); + + it('should shrink the viewport if necessary', function () { + display.resize(2, 2); + expect(display.absX(0)).to.equal(0); + expect(display.absY(0)).to.equal(0); + expect(display._target.width).to.equal(2); + expect(display._target.height).to.equal(2); + }); + }); + }); + + describe('rescaling', function () { + let display; + let canvas; + + beforeEach(function () { + canvas = document.createElement('canvas'); + display = new Display(canvas); + display.clipViewport = true; + display.resize(4, 4); + display.viewportChangeSize(3, 3); + display.viewportChangePos(1, 1); + document.body.appendChild(canvas); + }); + + afterEach(function () { + document.body.removeChild(canvas); + }); + + it('should not change the bitmap size of the canvas', function () { + display.scale = 2.0; + expect(canvas.width).to.equal(3); + expect(canvas.height).to.equal(3); + }); + + it('should change the effective rendered size of the canvas', function () { + display.scale = 2.0; + expect(canvas.clientWidth).to.equal(6); + expect(canvas.clientHeight).to.equal(6); + }); + + it('should not change when resizing', function () { + display.scale = 2.0; + display.resize(5, 5); + expect(display.scale).to.equal(2.0); + expect(canvas.width).to.equal(3); + expect(canvas.height).to.equal(3); + expect(canvas.clientWidth).to.equal(6); + expect(canvas.clientHeight).to.equal(6); + }); + }); + + describe('autoscaling', function () { + let display; + let canvas; + + beforeEach(function () { + canvas = document.createElement('canvas'); + display = new Display(canvas); + display.clipViewport = true; + display.resize(4, 3); + document.body.appendChild(canvas); + }); + + afterEach(function () { + document.body.removeChild(canvas); + }); + + it('should preserve aspect ratio while autoscaling', function () { + display.autoscale(16, 9); + expect(canvas.clientWidth / canvas.clientHeight).to.equal(4 / 3); + }); + + it('should use width to determine scale when the current aspect ratio is wider than the target', function () { + display.autoscale(9, 16); + expect(display.absX(9)).to.equal(4); + expect(display.absY(18)).to.equal(8); + expect(canvas.clientWidth).to.equal(9); + expect(canvas.clientHeight).to.equal(7); // round 9 / (4 / 3) + }); + + it('should use height to determine scale when the current aspect ratio is taller than the target', function () { + display.autoscale(16, 9); + expect(display.absX(9)).to.equal(3); + expect(display.absY(18)).to.equal(6); + expect(canvas.clientWidth).to.equal(12); // 16 * (4 / 3) + expect(canvas.clientHeight).to.equal(9); + + }); + + it('should not change the bitmap size of the canvas', function () { + display.autoscale(16, 9); + expect(canvas.width).to.equal(4); + expect(canvas.height).to.equal(3); + }); + }); + + describe('drawing', function () { + + // TODO(directxman12): improve the tests for each of the drawing functions to cover more than just the + // basic cases + let display; + beforeEach(function () { + display = new Display(document.createElement('canvas')); + display.resize(4, 4); + }); + + it('should clear the screen on #clear without a logo set', function () { + display.fillRect(0, 0, 4, 4, [0x00, 0x00, 0xff]); + display._logo = null; + display.clear(); + display.resize(4, 4); + const empty = []; + for (let i = 0; i < 4 * display._fb_width * display._fb_height; i++) { empty[i] = 0; } + expect(display).to.have.displayed(new Uint8Array(empty)); + }); + + it('should draw the logo on #clear with a logo set', function (done) { + display._logo = { width: 4, height: 4, type: "image/png", data: make_image_png(checked_data) }; + display.clear(); + display.onflush = () => { + expect(display).to.have.displayed(checked_data); + expect(display._fb_width).to.equal(4); + expect(display._fb_height).to.equal(4); + done(); + }; + display.flush(); + }); + + it('should not draw directly on the target canvas', function () { + display.fillRect(0, 0, 4, 4, [0, 0, 0xff]); + display.flip(); + display.fillRect(0, 0, 4, 4, [0, 0xff, 0]); + const expected = []; + for (let i = 0; i < 4 * display._fb_width * display._fb_height; i += 4) { + expected[i] = 0xff; + expected[i+1] = expected[i+2] = 0; + expected[i+3] = 0xff; + } + expect(display).to.have.displayed(new Uint8Array(expected)); + }); + + it('should support filling a rectangle with particular color via #fillRect', function () { + display.fillRect(0, 0, 4, 4, [0, 0xff, 0]); + display.fillRect(0, 0, 2, 2, [0xff, 0, 0]); + display.fillRect(2, 2, 2, 2, [0xff, 0, 0]); + display.flip(); + expect(display).to.have.displayed(checked_data); + }); + + it('should support copying an portion of the canvas via #copyImage', function () { + display.fillRect(0, 0, 4, 4, [0, 0xff, 0]); + display.fillRect(0, 0, 2, 2, [0xff, 0, 0x00]); + display.copyImage(0, 0, 2, 2, 2, 2); + display.flip(); + expect(display).to.have.displayed(checked_data); + }); + + it('should support drawing images via #imageRect', function (done) { + display.imageRect(0, 0, "image/png", make_image_png(checked_data)); + display.flip(); + display.onflush = () => { + expect(display).to.have.displayed(checked_data); + done(); + }; + display.flush(); + }); + + it('should support drawing tile data with a background color and sub tiles', function () { + display.startTile(0, 0, 4, 4, [0, 0xff, 0]); + display.subTile(0, 0, 2, 2, [0xff, 0, 0]); + display.subTile(2, 2, 2, 2, [0xff, 0, 0]); + display.finishTile(); + display.flip(); + expect(display).to.have.displayed(checked_data); + }); + + // We have a special cache for 16x16 tiles that we need to test + it('should support drawing a 16x16 tile', function () { + const large_checked_data = new Uint8Array(16*16*4); + display.resize(16, 16); + + for (let y = 0;y < 16;y++) { + for (let x = 0;x < 16;x++) { + let pixel; + if ((x < 4) && (y < 4)) { + // NB: of course IE11 doesn't support #slice on ArrayBufferViews... + pixel = Array.prototype.slice.call(checked_data, (y*4+x)*4, (y*4+x+1)*4); + } else { + pixel = [0, 0xff, 0, 255]; + } + large_checked_data.set(pixel, (y*16+x)*4); + } + } + + display.startTile(0, 0, 16, 16, [0, 0xff, 0]); + display.subTile(0, 0, 2, 2, [0xff, 0, 0]); + display.subTile(2, 2, 2, 2, [0xff, 0, 0]); + display.finishTile(); + display.flip(); + expect(display).to.have.displayed(large_checked_data); + }); + + it('should support drawing BGRX blit images with true color via #blitImage', function () { + const data = []; + for (let i = 0; i < 16; i++) { + data[i * 4] = checked_data[i * 4 + 2]; + data[i * 4 + 1] = checked_data[i * 4 + 1]; + data[i * 4 + 2] = checked_data[i * 4]; + data[i * 4 + 3] = checked_data[i * 4 + 3]; + } + display.blitImage(0, 0, 4, 4, data, 0); + display.flip(); + expect(display).to.have.displayed(checked_data); + }); + + it('should support drawing RGB blit images with true color via #blitRgbImage', function () { + const data = []; + for (let i = 0; i < 16; i++) { + data[i * 3] = checked_data[i * 4]; + data[i * 3 + 1] = checked_data[i * 4 + 1]; + data[i * 3 + 2] = checked_data[i * 4 + 2]; + } + display.blitRgbImage(0, 0, 4, 4, data, 0); + display.flip(); + expect(display).to.have.displayed(checked_data); + }); + + it('should support drawing an image object via #drawImage', function () { + const img = make_image_canvas(checked_data); + display.drawImage(img, 0, 0); + display.flip(); + expect(display).to.have.displayed(checked_data); + }); + }); + + describe('the render queue processor', function () { + let display; + beforeEach(function () { + display = new Display(document.createElement('canvas')); + display.resize(4, 4); + sinon.spy(display, '_scan_renderQ'); + }); + + afterEach(function () { + window.requestAnimationFrame = this.old_requestAnimationFrame; + }); + + it('should try to process an item when it is pushed on, if nothing else is on the queue', function () { + display._renderQ_push({ type: 'noop' }); // does nothing + expect(display._scan_renderQ).to.have.been.calledOnce; + }); + + it('should not try to process an item when it is pushed on if we are waiting for other items', function () { + display._renderQ.length = 2; + display._renderQ_push({ type: 'noop' }); + expect(display._scan_renderQ).to.not.have.been.called; + }); + + it('should wait until an image is loaded to attempt to draw it and the rest of the queue', function () { + const img = { complete: false, addEventListener: sinon.spy() }; + display._renderQ = [{ type: 'img', x: 3, y: 4, img: img }, + { type: 'fill', x: 1, y: 2, width: 3, height: 4, color: 5 }]; + display.drawImage = sinon.spy(); + display.fillRect = sinon.spy(); + + display._scan_renderQ(); + expect(display.drawImage).to.not.have.been.called; + expect(display.fillRect).to.not.have.been.called; + expect(img.addEventListener).to.have.been.calledOnce; + + display._renderQ[0].img.complete = true; + display._scan_renderQ(); + expect(display.drawImage).to.have.been.calledOnce; + expect(display.fillRect).to.have.been.calledOnce; + expect(img.addEventListener).to.have.been.calledOnce; + }); + + it('should call callback when queue is flushed', function () { + display.onflush = sinon.spy(); + display.fillRect(0, 0, 4, 4, [0, 0xff, 0]); + expect(display.onflush).to.not.have.been.called; + display.flush(); + expect(display.onflush).to.have.been.calledOnce; + }); + + it('should draw a blit image on type "blit"', function () { + display.blitImage = sinon.spy(); + display._renderQ_push({ type: 'blit', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] }); + expect(display.blitImage).to.have.been.calledOnce; + expect(display.blitImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0); + }); + + it('should draw a blit RGB image on type "blitRgb"', function () { + display.blitRgbImage = sinon.spy(); + display._renderQ_push({ type: 'blitRgb', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] }); + expect(display.blitRgbImage).to.have.been.calledOnce; + expect(display.blitRgbImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0); + }); + + it('should copy a region on type "copy"', function () { + display.copyImage = sinon.spy(); + display._renderQ_push({ type: 'copy', x: 3, y: 4, width: 5, height: 6, old_x: 7, old_y: 8 }); + expect(display.copyImage).to.have.been.calledOnce; + expect(display.copyImage).to.have.been.calledWith(7, 8, 3, 4, 5, 6); + }); + + it('should fill a rect with a given color on type "fill"', function () { + display.fillRect = sinon.spy(); + display._renderQ_push({ type: 'fill', x: 3, y: 4, width: 5, height: 6, color: [7, 8, 9]}); + expect(display.fillRect).to.have.been.calledOnce; + expect(display.fillRect).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9]); + }); + + it('should draw an image from an image object on type "img" (if complete)', function () { + display.drawImage = sinon.spy(); + display._renderQ_push({ type: 'img', x: 3, y: 4, img: { complete: true } }); + expect(display.drawImage).to.have.been.calledOnce; + expect(display.drawImage).to.have.been.calledWith({ complete: true }, 3, 4); + }); + }); +}); diff --git a/systemvm/debian/root/noVNC/tests/test.helper.js b/systemvm/debian/root/noVNC/tests/test.helper.js new file mode 100644 index 000000000000..d44bab0fe2dc --- /dev/null +++ b/systemvm/debian/root/noVNC/tests/test.helper.js @@ -0,0 +1,223 @@ +const expect = chai.expect; + +import keysyms from '../core/input/keysymdef.js'; +import * as KeyboardUtil from "../core/input/util.js"; +import * as browser from '../core/util/browser.js'; + +describe('Helpers', function () { + "use strict"; + + describe('keysyms.lookup', function () { + it('should map ASCII characters to keysyms', function () { + expect(keysyms.lookup('a'.charCodeAt())).to.be.equal(0x61); + expect(keysyms.lookup('A'.charCodeAt())).to.be.equal(0x41); + }); + it('should map Latin-1 characters to keysyms', function () { + expect(keysyms.lookup('ø'.charCodeAt())).to.be.equal(0xf8); + + expect(keysyms.lookup('é'.charCodeAt())).to.be.equal(0xe9); + }); + it('should map characters that are in Windows-1252 but not in Latin-1 to keysyms', function () { + expect(keysyms.lookup('Š'.charCodeAt())).to.be.equal(0x01a9); + }); + it('should map characters which aren\'t in Latin1 *or* Windows-1252 to keysyms', function () { + expect(keysyms.lookup('ũ'.charCodeAt())).to.be.equal(0x03fd); + }); + it('should map unknown codepoints to the Unicode range', function () { + expect(keysyms.lookup('\n'.charCodeAt())).to.be.equal(0x100000a); + expect(keysyms.lookup('\u262D'.charCodeAt())).to.be.equal(0x100262d); + }); + // This requires very recent versions of most browsers... skipping for now + it.skip('should map UCS-4 codepoints to the Unicode range', function () { + //expect(keysyms.lookup('\u{1F686}'.codePointAt())).to.be.equal(0x101f686); + }); + }); + + describe('getKeycode', function () { + it('should pass through proper code', function () { + expect(KeyboardUtil.getKeycode({code: 'Semicolon'})).to.be.equal('Semicolon'); + }); + it('should map legacy values', function () { + expect(KeyboardUtil.getKeycode({code: ''})).to.be.equal('Unidentified'); + expect(KeyboardUtil.getKeycode({code: 'OSLeft'})).to.be.equal('MetaLeft'); + }); + it('should map keyCode to code when possible', function () { + expect(KeyboardUtil.getKeycode({keyCode: 0x14})).to.be.equal('CapsLock'); + expect(KeyboardUtil.getKeycode({keyCode: 0x5b})).to.be.equal('MetaLeft'); + expect(KeyboardUtil.getKeycode({keyCode: 0x35})).to.be.equal('Digit5'); + expect(KeyboardUtil.getKeycode({keyCode: 0x65})).to.be.equal('Numpad5'); + }); + it('should map keyCode left/right side', function () { + expect(KeyboardUtil.getKeycode({keyCode: 0x10, location: 1})).to.be.equal('ShiftLeft'); + expect(KeyboardUtil.getKeycode({keyCode: 0x10, location: 2})).to.be.equal('ShiftRight'); + expect(KeyboardUtil.getKeycode({keyCode: 0x11, location: 1})).to.be.equal('ControlLeft'); + expect(KeyboardUtil.getKeycode({keyCode: 0x11, location: 2})).to.be.equal('ControlRight'); + }); + it('should map keyCode on numpad', function () { + expect(KeyboardUtil.getKeycode({keyCode: 0x0d, location: 0})).to.be.equal('Enter'); + expect(KeyboardUtil.getKeycode({keyCode: 0x0d, location: 3})).to.be.equal('NumpadEnter'); + expect(KeyboardUtil.getKeycode({keyCode: 0x23, location: 0})).to.be.equal('End'); + expect(KeyboardUtil.getKeycode({keyCode: 0x23, location: 3})).to.be.equal('Numpad1'); + }); + it('should return Unidentified when it cannot map the keyCode', function () { + expect(KeyboardUtil.getKeycode({keycode: 0x42})).to.be.equal('Unidentified'); + }); + + describe('Fix Meta on macOS', function () { + let origNavigator; + beforeEach(function () { + // window.navigator is a protected read-only property in many + // environments, so we need to redefine it whilst running these + // tests. + origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); + if (origNavigator === undefined) { + // Object.getOwnPropertyDescriptor() doesn't work + // properly in any version of IE + this.skip(); + } + + Object.defineProperty(window, "navigator", {value: {}}); + if (window.navigator.platform !== undefined) { + // Object.defineProperty() doesn't work properly in old + // versions of Chrome + this.skip(); + } + + window.navigator.platform = "Mac x86_64"; + }); + afterEach(function () { + Object.defineProperty(window, "navigator", origNavigator); + }); + + it('should respect ContextMenu on modern browser', function () { + expect(KeyboardUtil.getKeycode({code: 'ContextMenu', keyCode: 0x5d})).to.be.equal('ContextMenu'); + }); + it('should translate legacy ContextMenu to MetaRight', function () { + expect(KeyboardUtil.getKeycode({keyCode: 0x5d})).to.be.equal('MetaRight'); + }); + }); + }); + + describe('getKey', function () { + it('should prefer key', function () { + if (browser.isIE() || browser.isEdge()) this.skip(); + expect(KeyboardUtil.getKey({key: 'a', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.be.equal('a'); + }); + it('should map legacy values', function () { + expect(KeyboardUtil.getKey({key: 'Spacebar'})).to.be.equal(' '); + expect(KeyboardUtil.getKey({key: 'Left'})).to.be.equal('ArrowLeft'); + expect(KeyboardUtil.getKey({key: 'OS'})).to.be.equal('Meta'); + expect(KeyboardUtil.getKey({key: 'Win'})).to.be.equal('Meta'); + expect(KeyboardUtil.getKey({key: 'UIKeyInputLeftArrow'})).to.be.equal('ArrowLeft'); + }); + it('should use code if no key', function () { + expect(KeyboardUtil.getKey({code: 'NumpadBackspace'})).to.be.equal('Backspace'); + }); + it('should not use code fallback for character keys', function () { + expect(KeyboardUtil.getKey({code: 'KeyA'})).to.be.equal('Unidentified'); + expect(KeyboardUtil.getKey({code: 'Digit1'})).to.be.equal('Unidentified'); + expect(KeyboardUtil.getKey({code: 'Period'})).to.be.equal('Unidentified'); + expect(KeyboardUtil.getKey({code: 'Numpad1'})).to.be.equal('Unidentified'); + }); + it('should use charCode if no key', function () { + expect(KeyboardUtil.getKey({charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.be.equal('Š'); + }); + it('should return Unidentified when it cannot map the key', function () { + expect(KeyboardUtil.getKey({keycode: 0x42})).to.be.equal('Unidentified'); + }); + + describe('Broken key AltGraph on IE/Edge', function () { + let origNavigator; + beforeEach(function () { + // window.navigator is a protected read-only property in many + // environments, so we need to redefine it whilst running these + // tests. + origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); + if (origNavigator === undefined) { + // Object.getOwnPropertyDescriptor() doesn't work + // properly in any version of IE + this.skip(); + } + + Object.defineProperty(window, "navigator", {value: {}}); + if (window.navigator.platform !== undefined) { + // Object.defineProperty() doesn't work properly in old + // versions of Chrome + this.skip(); + } + }); + afterEach(function () { + Object.defineProperty(window, "navigator", origNavigator); + }); + + it('should ignore printable character key on IE', function () { + window.navigator.userAgent = "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko"; + expect(KeyboardUtil.getKey({key: 'a'})).to.be.equal('Unidentified'); + }); + it('should ignore printable character key on Edge', function () { + window.navigator.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393"; + expect(KeyboardUtil.getKey({key: 'a'})).to.be.equal('Unidentified'); + }); + it('should allow non-printable character key on IE', function () { + window.navigator.userAgent = "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko"; + expect(KeyboardUtil.getKey({key: 'Shift'})).to.be.equal('Shift'); + }); + it('should allow non-printable character key on Edge', function () { + window.navigator.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393"; + expect(KeyboardUtil.getKey({key: 'Shift'})).to.be.equal('Shift'); + }); + }); + }); + + describe('getKeysym', function () { + describe('Non-character keys', function () { + it('should recognize the right keys', function () { + expect(KeyboardUtil.getKeysym({key: 'Enter'})).to.be.equal(0xFF0D); + expect(KeyboardUtil.getKeysym({key: 'Backspace'})).to.be.equal(0xFF08); + expect(KeyboardUtil.getKeysym({key: 'Tab'})).to.be.equal(0xFF09); + expect(KeyboardUtil.getKeysym({key: 'Shift'})).to.be.equal(0xFFE1); + expect(KeyboardUtil.getKeysym({key: 'Control'})).to.be.equal(0xFFE3); + expect(KeyboardUtil.getKeysym({key: 'Alt'})).to.be.equal(0xFFE9); + expect(KeyboardUtil.getKeysym({key: 'Meta'})).to.be.equal(0xFFEB); + expect(KeyboardUtil.getKeysym({key: 'Escape'})).to.be.equal(0xFF1B); + expect(KeyboardUtil.getKeysym({key: 'ArrowUp'})).to.be.equal(0xFF52); + }); + it('should map left/right side', function () { + expect(KeyboardUtil.getKeysym({key: 'Shift', location: 1})).to.be.equal(0xFFE1); + expect(KeyboardUtil.getKeysym({key: 'Shift', location: 2})).to.be.equal(0xFFE2); + expect(KeyboardUtil.getKeysym({key: 'Control', location: 1})).to.be.equal(0xFFE3); + expect(KeyboardUtil.getKeysym({key: 'Control', location: 2})).to.be.equal(0xFFE4); + }); + it('should handle AltGraph', function () { + expect(KeyboardUtil.getKeysym({code: 'AltRight', key: 'Alt', location: 2})).to.be.equal(0xFFEA); + expect(KeyboardUtil.getKeysym({code: 'AltRight', key: 'AltGraph', location: 2})).to.be.equal(0xFE03); + }); + it('should return null for unknown keys', function () { + expect(KeyboardUtil.getKeysym({key: 'Semicolon'})).to.be.null; + expect(KeyboardUtil.getKeysym({key: 'BracketRight'})).to.be.null; + }); + it('should handle remappings', function () { + expect(KeyboardUtil.getKeysym({code: 'ControlLeft', key: 'Tab'})).to.be.equal(0xFF09); + }); + }); + + describe('Numpad', function () { + it('should handle Numpad numbers', function () { + if (browser.isIE() || browser.isEdge()) this.skip(); + expect(KeyboardUtil.getKeysym({code: 'Digit5', key: '5', location: 0})).to.be.equal(0x0035); + expect(KeyboardUtil.getKeysym({code: 'Numpad5', key: '5', location: 3})).to.be.equal(0xFFB5); + }); + it('should handle Numpad non-character keys', function () { + expect(KeyboardUtil.getKeysym({code: 'Home', key: 'Home', location: 0})).to.be.equal(0xFF50); + expect(KeyboardUtil.getKeysym({code: 'Numpad5', key: 'Home', location: 3})).to.be.equal(0xFF95); + expect(KeyboardUtil.getKeysym({code: 'Delete', key: 'Delete', location: 0})).to.be.equal(0xFFFF); + expect(KeyboardUtil.getKeysym({code: 'NumpadDecimal', key: 'Delete', location: 3})).to.be.equal(0xFF9F); + }); + it('should handle Numpad Decimal key', function () { + if (browser.isIE() || browser.isEdge()) this.skip(); + expect(KeyboardUtil.getKeysym({code: 'NumpadDecimal', key: '.', location: 3})).to.be.equal(0xFFAE); + expect(KeyboardUtil.getKeysym({code: 'NumpadDecimal', key: ',', location: 3})).to.be.equal(0xFFAC); + }); + }); + }); +}); diff --git a/systemvm/debian/root/noVNC/tests/test.keyboard.js b/systemvm/debian/root/noVNC/tests/test.keyboard.js new file mode 100644 index 000000000000..77fe3f6f968f --- /dev/null +++ b/systemvm/debian/root/noVNC/tests/test.keyboard.js @@ -0,0 +1,510 @@ +const expect = chai.expect; + +import Keyboard from '../core/input/keyboard.js'; +import * as browser from '../core/util/browser.js'; + +describe('Key Event Handling', function () { + "use strict"; + + // The real KeyboardEvent constructor might not work everywhere we + // want to run these tests + function keyevent(typeArg, KeyboardEventInit) { + const e = { type: typeArg }; + for (let key in KeyboardEventInit) { + e[key] = KeyboardEventInit[key]; + } + e.stopPropagation = sinon.spy(); + e.preventDefault = sinon.spy(); + return e; + } + + describe('Decode Keyboard Events', function () { + it('should decode keydown events', function (done) { + if (browser.isIE() || browser.isEdge()) this.skip(); + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + expect(down).to.be.equal(true); + done(); + }; + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); + }); + it('should decode keyup events', function (done) { + if (browser.isIE() || browser.isEdge()) this.skip(); + let calls = 0; + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + if (calls++ === 1) { + expect(down).to.be.equal(false); + done(); + } + }; + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); + kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'})); + }); + + describe('Legacy keypress Events', function () { + it('should wait for keypress when needed', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41})); + expect(kbd.onkeyevent).to.not.have.been.called; + }); + it('should decode keypress events', function (done) { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + expect(down).to.be.equal(true); + done(); + }; + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41})); + kbd._handleKeyPress(keyevent('keypress', {code: 'KeyA', charCode: 0x61})); + }); + it('should ignore keypress with different code', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41})); + kbd._handleKeyPress(keyevent('keypress', {code: 'KeyB', charCode: 0x61})); + expect(kbd.onkeyevent).to.not.have.been.called; + }); + it('should handle keypress with missing code', function (done) { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + expect(down).to.be.equal(true); + done(); + }; + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41})); + kbd._handleKeyPress(keyevent('keypress', {charCode: 0x61})); + }); + it('should guess key if no keypress and numeric key', function (done) { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x32); + expect(code).to.be.equal('Digit2'); + expect(down).to.be.equal(true); + done(); + }; + kbd._handleKeyDown(keyevent('keydown', {code: 'Digit2', keyCode: 0x32})); + }); + it('should guess key if no keypress and alpha key', function (done) { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + expect(down).to.be.equal(true); + done(); + }; + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41, shiftKey: false})); + }); + it('should guess key if no keypress and alpha key (with shift)', function (done) { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x41); + expect(code).to.be.equal('KeyA'); + expect(down).to.be.equal(true); + done(); + }; + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41, shiftKey: true})); + }); + it('should not guess key if no keypress and unknown key', function (done) { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0); + expect(code).to.be.equal('KeyA'); + expect(down).to.be.equal(true); + done(); + }; + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x09})); + }); + }); + + describe('suppress the right events at the right time', function () { + beforeEach(function () { + if (browser.isIE() || browser.isEdge()) this.skip(); + }); + it('should suppress anything with a valid key', function () { + const kbd = new Keyboard(document, {}); + const evt1 = keyevent('keydown', {code: 'KeyA', key: 'a'}); + kbd._handleKeyDown(evt1); + expect(evt1.preventDefault).to.have.been.called; + const evt2 = keyevent('keyup', {code: 'KeyA', key: 'a'}); + kbd._handleKeyUp(evt2); + expect(evt2.preventDefault).to.have.been.called; + }); + it('should not suppress keys without key', function () { + const kbd = new Keyboard(document, {}); + const evt = keyevent('keydown', {code: 'KeyA', keyCode: 0x41}); + kbd._handleKeyDown(evt); + expect(evt.preventDefault).to.not.have.been.called; + }); + it('should suppress the following keypress event', function () { + const kbd = new Keyboard(document, {}); + const evt1 = keyevent('keydown', {code: 'KeyA', keyCode: 0x41}); + kbd._handleKeyDown(evt1); + const evt2 = keyevent('keypress', {code: 'KeyA', charCode: 0x41}); + kbd._handleKeyPress(evt2); + expect(evt2.preventDefault).to.have.been.called; + }); + }); + }); + + describe('Fake keyup', function () { + it('should fake keyup events for virtual keyboards', function (done) { + if (browser.isIE() || browser.isEdge()) this.skip(); + let count = 0; + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + switch (count++) { + case 0: + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('Unidentified'); + expect(down).to.be.equal(true); + break; + case 1: + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('Unidentified'); + expect(down).to.be.equal(false); + done(); + } + }; + kbd._handleKeyDown(keyevent('keydown', {code: 'Unidentified', key: 'a'})); + }); + + describe('iOS', function () { + let origNavigator; + beforeEach(function () { + // window.navigator is a protected read-only property in many + // environments, so we need to redefine it whilst running these + // tests. + origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); + if (origNavigator === undefined) { + // Object.getOwnPropertyDescriptor() doesn't work + // properly in any version of IE + this.skip(); + } + + Object.defineProperty(window, "navigator", {value: {}}); + if (window.navigator.platform !== undefined) { + // Object.defineProperty() doesn't work properly in old + // versions of Chrome + this.skip(); + } + + window.navigator.platform = "iPhone 9.0"; + }); + afterEach(function () { + Object.defineProperty(window, "navigator", origNavigator); + }); + + it('should fake keyup events on iOS', function (done) { + if (browser.isIE() || browser.isEdge()) this.skip(); + let count = 0; + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + switch (count++) { + case 0: + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + expect(down).to.be.equal(true); + break; + case 1: + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + expect(down).to.be.equal(false); + done(); + } + }; + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); + }); + }); + }); + + describe('Track Key State', function () { + beforeEach(function () { + if (browser.isIE() || browser.isEdge()) this.skip(); + }); + it('should send release using the same keysym as the press', function (done) { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + if (!down) { + done(); + } + }; + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); + kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'b'})); + }); + it('should send the same keysym for multiple presses', function () { + let count = 0; + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + expect(down).to.be.equal(true); + count++; + }; + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'b'})); + expect(count).to.be.equal(2); + }); + it('should do nothing on keyup events if no keys are down', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'})); + expect(kbd.onkeyevent).to.not.have.been.called; + }); + + describe('Legacy Events', function () { + it('should track keys using keyCode if no code', function (done) { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('Platform65'); + if (!down) { + done(); + } + }; + kbd._handleKeyDown(keyevent('keydown', {keyCode: 65, key: 'a'})); + kbd._handleKeyUp(keyevent('keyup', {keyCode: 65, key: 'b'})); + }); + it('should ignore compositing code', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('Unidentified'); + }; + kbd._handleKeyDown(keyevent('keydown', {keyCode: 229, key: 'a'})); + }); + it('should track keys using keyIdentifier if no code', function (done) { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('Platform65'); + if (!down) { + done(); + } + }; + kbd._handleKeyDown(keyevent('keydown', {keyIdentifier: 'U+0041', key: 'a'})); + kbd._handleKeyUp(keyevent('keyup', {keyIdentifier: 'U+0041', key: 'b'})); + }); + }); + }); + + describe('Shuffle modifiers on macOS', function () { + let origNavigator; + beforeEach(function () { + // window.navigator is a protected read-only property in many + // environments, so we need to redefine it whilst running these + // tests. + origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); + if (origNavigator === undefined) { + // Object.getOwnPropertyDescriptor() doesn't work + // properly in any version of IE + this.skip(); + } + + Object.defineProperty(window, "navigator", {value: {}}); + if (window.navigator.platform !== undefined) { + // Object.defineProperty() doesn't work properly in old + // versions of Chrome + this.skip(); + } + + window.navigator.platform = "Mac x86_64"; + }); + afterEach(function () { + Object.defineProperty(window, "navigator", origNavigator); + }); + + it('should change Alt to AltGraph', function () { + let count = 0; + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + switch (count++) { + case 0: + expect(keysym).to.be.equal(0xFF7E); + expect(code).to.be.equal('AltLeft'); + break; + case 1: + expect(keysym).to.be.equal(0xFE03); + expect(code).to.be.equal('AltRight'); + break; + } + }; + kbd._handleKeyDown(keyevent('keydown', {code: 'AltLeft', key: 'Alt', location: 1})); + kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2})); + expect(count).to.be.equal(2); + }); + it('should change left Super to Alt', function (done) { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0xFFE9); + expect(code).to.be.equal('MetaLeft'); + done(); + }; + kbd._handleKeyDown(keyevent('keydown', {code: 'MetaLeft', key: 'Meta', location: 1})); + }); + it('should change right Super to left Super', function (done) { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0xFFEB); + expect(code).to.be.equal('MetaRight'); + done(); + }; + kbd._handleKeyDown(keyevent('keydown', {code: 'MetaRight', key: 'Meta', location: 2})); + }); + }); + + describe('Escape AltGraph on Windows', function () { + let origNavigator; + beforeEach(function () { + // window.navigator is a protected read-only property in many + // environments, so we need to redefine it whilst running these + // tests. + origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); + if (origNavigator === undefined) { + // Object.getOwnPropertyDescriptor() doesn't work + // properly in any version of IE + this.skip(); + } + + Object.defineProperty(window, "navigator", {value: {}}); + if (window.navigator.platform !== undefined) { + // Object.defineProperty() doesn't work properly in old + // versions of Chrome + this.skip(); + } + + window.navigator.platform = "Windows x86_64"; + + this.clock = sinon.useFakeTimers(); + }); + afterEach(function () { + Object.defineProperty(window, "navigator", origNavigator); + this.clock.restore(); + }); + + it('should supress ControlLeft until it knows if it is AltGr', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); + expect(kbd.onkeyevent).to.not.have.been.called; + }); + + it('should not trigger on repeating ControlLeft', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); + expect(kbd.onkeyevent).to.have.been.calledTwice; + expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xffe3, "ControlLeft", true); + expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xffe3, "ControlLeft", true); + }); + + it('should not supress ControlRight', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlRight', key: 'Control', location: 2})); + expect(kbd.onkeyevent).to.have.been.calledOnce; + expect(kbd.onkeyevent).to.have.been.calledWith(0xffe4, "ControlRight", true); + }); + + it('should release ControlLeft after 100 ms', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); + expect(kbd.onkeyevent).to.not.have.been.called; + this.clock.tick(100); + expect(kbd.onkeyevent).to.have.been.calledOnce; + expect(kbd.onkeyevent).to.have.been.calledWith(0xffe3, "ControlLeft", true); + }); + + it('should release ControlLeft on other key press', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); + expect(kbd.onkeyevent).to.not.have.been.called; + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); + expect(kbd.onkeyevent).to.have.been.calledTwice; + expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xffe3, "ControlLeft", true); + expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0x61, "KeyA", true); + + // Check that the timer is properly dead + kbd.onkeyevent.reset(); + this.clock.tick(100); + expect(kbd.onkeyevent).to.not.have.been.called; + }); + + it('should release ControlLeft on other key release', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); + expect(kbd.onkeyevent).to.have.been.calledOnce; + expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0x61, "KeyA", true); + kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'})); + expect(kbd.onkeyevent).to.have.been.calledThrice; + expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xffe3, "ControlLeft", true); + expect(kbd.onkeyevent.thirdCall).to.have.been.calledWith(0x61, "KeyA", false); + + // Check that the timer is properly dead + kbd.onkeyevent.reset(); + this.clock.tick(100); + expect(kbd.onkeyevent).to.not.have.been.called; + }); + + it('should generate AltGraph for quick Ctrl+Alt sequence', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1, timeStamp: Date.now()})); + this.clock.tick(20); + kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2, timeStamp: Date.now()})); + expect(kbd.onkeyevent).to.have.been.calledOnce; + expect(kbd.onkeyevent).to.have.been.calledWith(0xfe03, 'AltRight', true); + + // Check that the timer is properly dead + kbd.onkeyevent.reset(); + this.clock.tick(100); + expect(kbd.onkeyevent).to.not.have.been.called; + }); + + it('should generate Ctrl, Alt for slow Ctrl+Alt sequence', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1, timeStamp: Date.now()})); + this.clock.tick(60); + kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2, timeStamp: Date.now()})); + expect(kbd.onkeyevent).to.have.been.calledTwice; + expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xffe3, "ControlLeft", true); + expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xffea, "AltRight", true); + + // Check that the timer is properly dead + kbd.onkeyevent.reset(); + this.clock.tick(100); + expect(kbd.onkeyevent).to.not.have.been.called; + }); + + it('should pass through single Alt', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2})); + expect(kbd.onkeyevent).to.have.been.calledOnce; + expect(kbd.onkeyevent).to.have.been.calledWith(0xffea, 'AltRight', true); + }); + + it('should pass through single AltGr', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'AltGraph', location: 2})); + expect(kbd.onkeyevent).to.have.been.calledOnce; + expect(kbd.onkeyevent).to.have.been.calledWith(0xfe03, 'AltRight', true); + }); + }); +}); diff --git a/systemvm/debian/root/noVNC/tests/test.localization.js b/systemvm/debian/root/noVNC/tests/test.localization.js new file mode 100644 index 000000000000..9570c1798a9d --- /dev/null +++ b/systemvm/debian/root/noVNC/tests/test.localization.js @@ -0,0 +1,72 @@ +const expect = chai.expect; +import { l10n } from '../app/localization.js'; + +describe('Localization', function () { + "use strict"; + + describe('language selection', function () { + let origNavigator; + beforeEach(function () { + // window.navigator is a protected read-only property in many + // environments, so we need to redefine it whilst running these + // tests. + origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); + if (origNavigator === undefined) { + // Object.getOwnPropertyDescriptor() doesn't work + // properly in any version of IE + this.skip(); + } + + Object.defineProperty(window, "navigator", {value: {}}); + if (window.navigator.languages !== undefined) { + // Object.defineProperty() doesn't work properly in old + // versions of Chrome + this.skip(); + } + + window.navigator.languages = []; + }); + afterEach(function () { + Object.defineProperty(window, "navigator", origNavigator); + }); + + it('should use English by default', function () { + expect(l10n.language).to.equal('en'); + }); + it('should use English if no user language matches', function () { + window.navigator.languages = ["nl", "de"]; + l10n.setup(["es", "fr"]); + expect(l10n.language).to.equal('en'); + }); + it('should use the most preferred user language', function () { + window.navigator.languages = ["nl", "de", "fr"]; + l10n.setup(["es", "fr", "de"]); + expect(l10n.language).to.equal('de'); + }); + it('should prefer sub-languages languages', function () { + window.navigator.languages = ["pt-BR"]; + l10n.setup(["pt", "pt-BR"]); + expect(l10n.language).to.equal('pt-BR'); + }); + it('should fall back to language "parents"', function () { + window.navigator.languages = ["pt-BR"]; + l10n.setup(["fr", "pt", "de"]); + expect(l10n.language).to.equal('pt'); + }); + it('should not use specific language when user asks for a generic language', function () { + window.navigator.languages = ["pt", "de"]; + l10n.setup(["fr", "pt-BR", "de"]); + expect(l10n.language).to.equal('de'); + }); + it('should handle underscore as a separator', function () { + window.navigator.languages = ["pt-BR"]; + l10n.setup(["pt_BR"]); + expect(l10n.language).to.equal('pt_BR'); + }); + it('should handle difference in case', function () { + window.navigator.languages = ["pt-br"]; + l10n.setup(["pt-BR"]); + expect(l10n.language).to.equal('pt-BR'); + }); + }); +}); diff --git a/systemvm/debian/root/noVNC/tests/test.mouse.js b/systemvm/debian/root/noVNC/tests/test.mouse.js new file mode 100644 index 000000000000..78c74f157244 --- /dev/null +++ b/systemvm/debian/root/noVNC/tests/test.mouse.js @@ -0,0 +1,304 @@ +const expect = chai.expect; + +import Mouse from '../core/input/mouse.js'; + +describe('Mouse Event Handling', function () { + "use strict"; + + let target; + + beforeEach(function () { + // For these tests we can assume that the canvas is 100x100 + // located at coordinates 10x10 + target = document.createElement('canvas'); + target.style.position = "absolute"; + target.style.top = "10px"; + target.style.left = "10px"; + target.style.width = "100px"; + target.style.height = "100px"; + document.body.appendChild(target); + }); + afterEach(function () { + document.body.removeChild(target); + target = null; + }); + + // The real constructors might not work everywhere we + // want to run these tests + const mouseevent = (typeArg, MouseEventInit) => { + const e = { type: typeArg }; + for (let key in MouseEventInit) { + e[key] = MouseEventInit[key]; + } + e.stopPropagation = sinon.spy(); + e.preventDefault = sinon.spy(); + return e; + }; + const touchevent = mouseevent; + + describe('Decode Mouse Events', function () { + it('should decode mousedown events', function (done) { + const mouse = new Mouse(target); + mouse.onmousebutton = (x, y, down, bmask) => { + expect(bmask).to.be.equal(0x01); + expect(down).to.be.equal(1); + done(); + }; + mouse._handleMouseDown(mouseevent('mousedown', { button: '0x01' })); + }); + it('should decode mouseup events', function (done) { + let calls = 0; + const mouse = new Mouse(target); + mouse.onmousebutton = (x, y, down, bmask) => { + expect(bmask).to.be.equal(0x01); + if (calls++ === 1) { + expect(down).to.not.be.equal(1); + done(); + } + }; + mouse._handleMouseDown(mouseevent('mousedown', { button: '0x01' })); + mouse._handleMouseUp(mouseevent('mouseup', { button: '0x01' })); + }); + it('should decode mousemove events', function (done) { + const mouse = new Mouse(target); + mouse.onmousemove = (x, y) => { + // Note that target relative coordinates are sent + expect(x).to.be.equal(40); + expect(y).to.be.equal(10); + done(); + }; + mouse._handleMouseMove(mouseevent('mousemove', + { clientX: 50, clientY: 20 })); + }); + it('should decode mousewheel events', function (done) { + let calls = 0; + const mouse = new Mouse(target); + mouse.onmousebutton = (x, y, down, bmask) => { + calls++; + expect(bmask).to.be.equal(1<<6); + if (calls === 1) { + expect(down).to.be.equal(1); + } else if (calls === 2) { + expect(down).to.not.be.equal(1); + done(); + } + }; + mouse._handleMouseWheel(mouseevent('mousewheel', + { deltaX: 50, deltaY: 0, + deltaMode: 0})); + }); + }); + + describe('Double-click for Touch', function () { + + beforeEach(function () { this.clock = sinon.useFakeTimers(); }); + afterEach(function () { this.clock.restore(); }); + + it('should use same pos for 2nd tap if close enough', function (done) { + let calls = 0; + const mouse = new Mouse(target); + mouse.onmousebutton = (x, y, down, bmask) => { + calls++; + if (calls === 1) { + expect(down).to.be.equal(1); + expect(x).to.be.equal(68); + expect(y).to.be.equal(36); + } else if (calls === 3) { + expect(down).to.be.equal(1); + expect(x).to.be.equal(68); + expect(y).to.be.equal(36); + done(); + } + }; + // touch events are sent in an array of events + // with one item for each touch point + mouse._handleMouseDown(touchevent( + 'touchstart', { touches: [{ clientX: 78, clientY: 46 }]})); + this.clock.tick(10); + mouse._handleMouseUp(touchevent( + 'touchend', { touches: [{ clientX: 79, clientY: 45 }]})); + this.clock.tick(200); + mouse._handleMouseDown(touchevent( + 'touchstart', { touches: [{ clientX: 67, clientY: 35 }]})); + this.clock.tick(10); + mouse._handleMouseUp(touchevent( + 'touchend', { touches: [{ clientX: 66, clientY: 36 }]})); + }); + + it('should not modify 2nd tap pos if far apart', function (done) { + let calls = 0; + const mouse = new Mouse(target); + mouse.onmousebutton = (x, y, down, bmask) => { + calls++; + if (calls === 1) { + expect(down).to.be.equal(1); + expect(x).to.be.equal(68); + expect(y).to.be.equal(36); + } else if (calls === 3) { + expect(down).to.be.equal(1); + expect(x).to.not.be.equal(68); + expect(y).to.not.be.equal(36); + done(); + } + }; + mouse._handleMouseDown(touchevent( + 'touchstart', { touches: [{ clientX: 78, clientY: 46 }]})); + this.clock.tick(10); + mouse._handleMouseUp(touchevent( + 'touchend', { touches: [{ clientX: 79, clientY: 45 }]})); + this.clock.tick(200); + mouse._handleMouseDown(touchevent( + 'touchstart', { touches: [{ clientX: 57, clientY: 35 }]})); + this.clock.tick(10); + mouse._handleMouseUp(touchevent( + 'touchend', { touches: [{ clientX: 56, clientY: 36 }]})); + }); + + it('should not modify 2nd tap pos if not soon enough', function (done) { + let calls = 0; + const mouse = new Mouse(target); + mouse.onmousebutton = (x, y, down, bmask) => { + calls++; + if (calls === 1) { + expect(down).to.be.equal(1); + expect(x).to.be.equal(68); + expect(y).to.be.equal(36); + } else if (calls === 3) { + expect(down).to.be.equal(1); + expect(x).to.not.be.equal(68); + expect(y).to.not.be.equal(36); + done(); + } + }; + mouse._handleMouseDown(touchevent( + 'touchstart', { touches: [{ clientX: 78, clientY: 46 }]})); + this.clock.tick(10); + mouse._handleMouseUp(touchevent( + 'touchend', { touches: [{ clientX: 79, clientY: 45 }]})); + this.clock.tick(500); + mouse._handleMouseDown(touchevent( + 'touchstart', { touches: [{ clientX: 67, clientY: 35 }]})); + this.clock.tick(10); + mouse._handleMouseUp(touchevent( + 'touchend', { touches: [{ clientX: 66, clientY: 36 }]})); + }); + + it('should not modify 2nd tap pos if not touch', function (done) { + let calls = 0; + const mouse = new Mouse(target); + mouse.onmousebutton = (x, y, down, bmask) => { + calls++; + if (calls === 1) { + expect(down).to.be.equal(1); + expect(x).to.be.equal(68); + expect(y).to.be.equal(36); + } else if (calls === 3) { + expect(down).to.be.equal(1); + expect(x).to.not.be.equal(68); + expect(y).to.not.be.equal(36); + done(); + } + }; + mouse._handleMouseDown(mouseevent( + 'mousedown', { button: '0x01', clientX: 78, clientY: 46 })); + this.clock.tick(10); + mouse._handleMouseUp(mouseevent( + 'mouseup', { button: '0x01', clientX: 79, clientY: 45 })); + this.clock.tick(200); + mouse._handleMouseDown(mouseevent( + 'mousedown', { button: '0x01', clientX: 67, clientY: 35 })); + this.clock.tick(10); + mouse._handleMouseUp(mouseevent( + 'mouseup', { button: '0x01', clientX: 66, clientY: 36 })); + }); + + }); + + describe('Accumulate mouse wheel events with small delta', function () { + + beforeEach(function () { this.clock = sinon.useFakeTimers(); }); + afterEach(function () { this.clock.restore(); }); + + it('should accumulate wheel events if small enough', function () { + const mouse = new Mouse(target); + mouse.onmousebutton = sinon.spy(); + + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { clientX: 18, clientY: 40, + deltaX: 4, deltaY: 0, deltaMode: 0 })); + this.clock.tick(10); + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { clientX: 18, clientY: 40, + deltaX: 4, deltaY: 0, deltaMode: 0 })); + + // threshold is 10 + expect(mouse._accumulatedWheelDeltaX).to.be.equal(8); + + this.clock.tick(10); + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { clientX: 18, clientY: 40, + deltaX: 4, deltaY: 0, deltaMode: 0 })); + + expect(mouse.onmousebutton).to.have.callCount(2); // mouse down and up + + this.clock.tick(10); + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { clientX: 18, clientY: 40, + deltaX: 4, deltaY: 9, deltaMode: 0 })); + + expect(mouse._accumulatedWheelDeltaX).to.be.equal(4); + expect(mouse._accumulatedWheelDeltaY).to.be.equal(9); + + expect(mouse.onmousebutton).to.have.callCount(2); // still + }); + + it('should not accumulate large wheel events', function () { + const mouse = new Mouse(target); + mouse.onmousebutton = sinon.spy(); + + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { clientX: 18, clientY: 40, + deltaX: 11, deltaY: 0, deltaMode: 0 })); + this.clock.tick(10); + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { clientX: 18, clientY: 40, + deltaX: 0, deltaY: 70, deltaMode: 0 })); + this.clock.tick(10); + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { clientX: 18, clientY: 40, + deltaX: 400, deltaY: 400, deltaMode: 0 })); + + expect(mouse.onmousebutton).to.have.callCount(8); // mouse down and up + }); + + it('should send even small wheel events after a timeout', function () { + const mouse = new Mouse(target); + mouse.onmousebutton = sinon.spy(); + + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { clientX: 18, clientY: 40, + deltaX: 1, deltaY: 0, deltaMode: 0 })); + this.clock.tick(51); // timeout on 50 ms + + expect(mouse.onmousebutton).to.have.callCount(2); // mouse down and up + }); + + it('should account for non-zero deltaMode', function () { + const mouse = new Mouse(target); + mouse.onmousebutton = sinon.spy(); + + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { clientX: 18, clientY: 40, + deltaX: 0, deltaY: 2, deltaMode: 1 })); + + this.clock.tick(10); + + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { clientX: 18, clientY: 40, + deltaX: 1, deltaY: 0, deltaMode: 2 })); + + expect(mouse.onmousebutton).to.have.callCount(4); // mouse down and up + }); + }); + +}); diff --git a/systemvm/debian/root/noVNC/tests/test.rfb.js b/systemvm/debian/root/noVNC/tests/test.rfb.js new file mode 100644 index 000000000000..99c9c90c8ff0 --- /dev/null +++ b/systemvm/debian/root/noVNC/tests/test.rfb.js @@ -0,0 +1,2389 @@ +const expect = chai.expect; + +import RFB from '../core/rfb.js'; +import Websock from '../core/websock.js'; +import { encodings } from '../core/encodings.js'; + +import FakeWebSocket from './fake.websocket.js'; + +/* UIEvent constructor polyfill for IE */ +(() => { + if (typeof window.UIEvent === "function") return; + + function UIEvent( event, params ) { + params = params || { bubbles: false, cancelable: false, view: window, detail: undefined }; + const evt = document.createEvent( 'UIEvent' ); + evt.initUIEvent( event, params.bubbles, params.cancelable, params.view, params.detail ); + return evt; + } + + UIEvent.prototype = window.UIEvent.prototype; + + window.UIEvent = UIEvent; +})(); + +function push8(arr, num) { + "use strict"; + arr.push(num & 0xFF); +} + +function push16(arr, num) { + "use strict"; + arr.push((num >> 8) & 0xFF, + num & 0xFF); +} + +function push32(arr, num) { + "use strict"; + arr.push((num >> 24) & 0xFF, + (num >> 16) & 0xFF, + (num >> 8) & 0xFF, + num & 0xFF); +} + +describe('Remote Frame Buffer Protocol Client', function () { + let clock; + let raf; + + before(FakeWebSocket.replace); + after(FakeWebSocket.restore); + + before(function () { + this.clock = clock = sinon.useFakeTimers(); + // sinon doesn't support this yet + raf = window.requestAnimationFrame; + window.requestAnimationFrame = setTimeout; + // Use a single set of buffers instead of reallocating to + // speed up tests + const sock = new Websock(); + const _sQ = new Uint8Array(sock._sQbufferSize); + const rQ = new Uint8Array(sock._rQbufferSize); + + Websock.prototype._old_allocate_buffers = Websock.prototype._allocate_buffers; + Websock.prototype._allocate_buffers = function () { + this._sQ = _sQ; + this._rQ = rQ; + }; + + }); + + after(function () { + Websock.prototype._allocate_buffers = Websock.prototype._old_allocate_buffers; + this.clock.restore(); + window.requestAnimationFrame = raf; + }); + + let container; + let rfbs; + + beforeEach(function () { + // Create a container element for all RFB objects to attach to + container = document.createElement('div'); + container.style.width = "100%"; + container.style.height = "100%"; + document.body.appendChild(container); + + // And track all created RFB objects + rfbs = []; + }); + afterEach(function () { + // Make sure every created RFB object is properly cleaned up + // or they might affect subsequent tests + rfbs.forEach(function (rfb) { + rfb.disconnect(); + expect(rfb._disconnect).to.have.been.called; + }); + rfbs = []; + + document.body.removeChild(container); + container = null; + }); + + function make_rfb(url, options) { + url = url || 'wss://host:8675'; + const rfb = new RFB(container, url, options); + clock.tick(); + rfb._sock._websocket._open(); + rfb._rfb_connection_state = 'connected'; + sinon.spy(rfb, "_disconnect"); + rfbs.push(rfb); + return rfb; + } + + describe('Connecting/Disconnecting', function () { + describe('#RFB', function () { + it('should set the current state to "connecting"', function () { + const client = new RFB(document.createElement('div'), 'wss://host:8675'); + client._rfb_connection_state = ''; + this.clock.tick(); + expect(client._rfb_connection_state).to.equal('connecting'); + }); + + it('should actually connect to the websocket', function () { + const client = new RFB(document.createElement('div'), 'ws://HOST:8675/PATH'); + sinon.spy(client._sock, 'open'); + this.clock.tick(); + expect(client._sock.open).to.have.been.calledOnce; + expect(client._sock.open).to.have.been.calledWith('ws://HOST:8675/PATH'); + }); + }); + + describe('#disconnect', function () { + let client; + beforeEach(function () { + client = make_rfb(); + }); + + it('should go to state "disconnecting" before "disconnected"', function () { + sinon.spy(client, '_updateConnectionState'); + client.disconnect(); + expect(client._updateConnectionState).to.have.been.calledTwice; + expect(client._updateConnectionState.getCall(0).args[0]) + .to.equal('disconnecting'); + expect(client._updateConnectionState.getCall(1).args[0]) + .to.equal('disconnected'); + expect(client._rfb_connection_state).to.equal('disconnected'); + }); + + it('should unregister error event handler', function () { + sinon.spy(client._sock, 'off'); + client.disconnect(); + expect(client._sock.off).to.have.been.calledWith('error'); + }); + + it('should unregister message event handler', function () { + sinon.spy(client._sock, 'off'); + client.disconnect(); + expect(client._sock.off).to.have.been.calledWith('message'); + }); + + it('should unregister open event handler', function () { + sinon.spy(client._sock, 'off'); + client.disconnect(); + expect(client._sock.off).to.have.been.calledWith('open'); + }); + }); + + describe('#sendCredentials', function () { + let client; + beforeEach(function () { + client = make_rfb(); + client._rfb_connection_state = 'connecting'; + }); + + it('should set the rfb credentials properly"', function () { + client.sendCredentials({ password: 'pass' }); + expect(client._rfb_credentials).to.deep.equal({ password: 'pass' }); + }); + + it('should call init_msg "soon"', function () { + client._init_msg = sinon.spy(); + client.sendCredentials({ password: 'pass' }); + this.clock.tick(5); + expect(client._init_msg).to.have.been.calledOnce; + }); + }); + }); + + describe('Public API Basic Behavior', function () { + let client; + beforeEach(function () { + client = make_rfb(); + }); + + describe('#sendCtrlAlDel', function () { + it('should sent ctrl[down]-alt[down]-del[down] then del[up]-alt[up]-ctrl[up]', function () { + const expected = {_sQ: new Uint8Array(48), _sQlen: 0, flush: () => {}}; + RFB.messages.keyEvent(expected, 0xFFE3, 1); + RFB.messages.keyEvent(expected, 0xFFE9, 1); + RFB.messages.keyEvent(expected, 0xFFFF, 1); + RFB.messages.keyEvent(expected, 0xFFFF, 0); + RFB.messages.keyEvent(expected, 0xFFE9, 0); + RFB.messages.keyEvent(expected, 0xFFE3, 0); + + client.sendCtrlAltDel(); + expect(client._sock).to.have.sent(expected._sQ); + }); + + it('should not send the keys if we are not in a normal state', function () { + sinon.spy(client._sock, 'flush'); + client._rfb_connection_state = "connecting"; + client.sendCtrlAltDel(); + expect(client._sock.flush).to.not.have.been.called; + }); + + it('should not send the keys if we are set as view_only', function () { + sinon.spy(client._sock, 'flush'); + client._viewOnly = true; + client.sendCtrlAltDel(); + expect(client._sock.flush).to.not.have.been.called; + }); + }); + + describe('#sendKey', function () { + it('should send a single key with the given code and state (down = true)', function () { + const expected = {_sQ: new Uint8Array(8), _sQlen: 0, flush: () => {}}; + RFB.messages.keyEvent(expected, 123, 1); + client.sendKey(123, 'Key123', true); + expect(client._sock).to.have.sent(expected._sQ); + }); + + it('should send both a down and up event if the state is not specified', function () { + const expected = {_sQ: new Uint8Array(16), _sQlen: 0, flush: () => {}}; + RFB.messages.keyEvent(expected, 123, 1); + RFB.messages.keyEvent(expected, 123, 0); + client.sendKey(123, 'Key123'); + expect(client._sock).to.have.sent(expected._sQ); + }); + + it('should not send the key if we are not in a normal state', function () { + sinon.spy(client._sock, 'flush'); + client._rfb_connection_state = "connecting"; + client.sendKey(123, 'Key123'); + expect(client._sock.flush).to.not.have.been.called; + }); + + it('should not send the key if we are set as view_only', function () { + sinon.spy(client._sock, 'flush'); + client._viewOnly = true; + client.sendKey(123, 'Key123'); + expect(client._sock.flush).to.not.have.been.called; + }); + + it('should send QEMU extended events if supported', function () { + client._qemuExtKeyEventSupported = true; + const expected = {_sQ: new Uint8Array(12), _sQlen: 0, flush: () => {}}; + RFB.messages.QEMUExtendedKeyEvent(expected, 0x20, true, 0x0039); + client.sendKey(0x20, 'Space', true); + expect(client._sock).to.have.sent(expected._sQ); + }); + + it('should not send QEMU extended events if unknown key code', function () { + client._qemuExtKeyEventSupported = true; + const expected = {_sQ: new Uint8Array(8), _sQlen: 0, flush: () => {}}; + RFB.messages.keyEvent(expected, 123, 1); + client.sendKey(123, 'FooBar', true); + expect(client._sock).to.have.sent(expected._sQ); + }); + }); + + describe('#focus', function () { + it('should move focus to canvas object', function () { + client._canvas.focus = sinon.spy(); + client.focus(); + expect(client._canvas.focus).to.have.been.called.once; + }); + }); + + describe('#blur', function () { + it('should remove focus from canvas object', function () { + client._canvas.blur = sinon.spy(); + client.blur(); + expect(client._canvas.blur).to.have.been.called.once; + }); + }); + + describe('#clipboardPasteFrom', function () { + it('should send the given text in a paste event', function () { + const expected = {_sQ: new Uint8Array(11), _sQlen: 0, + _sQbufferSize: 11, flush: () => {}}; + RFB.messages.clientCutText(expected, 'abc'); + client.clipboardPasteFrom('abc'); + expect(client._sock).to.have.sent(expected._sQ); + }); + + it('should flush multiple times for large clipboards', function () { + sinon.spy(client._sock, 'flush'); + let long_text = ""; + for (let i = 0; i < client._sock._sQbufferSize + 100; i++) { + long_text += 'a'; + } + client.clipboardPasteFrom(long_text); + expect(client._sock.flush).to.have.been.calledTwice; + }); + + it('should not send the text if we are not in a normal state', function () { + sinon.spy(client._sock, 'flush'); + client._rfb_connection_state = "connecting"; + client.clipboardPasteFrom('abc'); + expect(client._sock.flush).to.not.have.been.called; + }); + }); + + describe("XVP operations", function () { + beforeEach(function () { + client._rfb_xvp_ver = 1; + }); + + it('should send the shutdown signal on #machineShutdown', function () { + client.machineShutdown(); + expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x02])); + }); + + it('should send the reboot signal on #machineReboot', function () { + client.machineReboot(); + expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x03])); + }); + + it('should send the reset signal on #machineReset', function () { + client.machineReset(); + expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x04])); + }); + + it('should not send XVP operations with higher versions than we support', function () { + sinon.spy(client._sock, 'flush'); + client._xvpOp(2, 7); + expect(client._sock.flush).to.not.have.been.called; + }); + }); + }); + + describe('Clipping', function () { + let client; + beforeEach(function () { + client = make_rfb(); + container.style.width = '70px'; + container.style.height = '80px'; + client.clipViewport = true; + }); + + it('should update display clip state when changing the property', function () { + const spy = sinon.spy(client._display, "clipViewport", ["set"]); + + client.clipViewport = false; + expect(spy.set).to.have.been.calledOnce; + expect(spy.set).to.have.been.calledWith(false); + spy.set.reset(); + + client.clipViewport = true; + expect(spy.set).to.have.been.calledOnce; + expect(spy.set).to.have.been.calledWith(true); + }); + + it('should update the viewport when the container size changes', function () { + sinon.spy(client._display, "viewportChangeSize"); + + container.style.width = '40px'; + container.style.height = '50px'; + const event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(); + + expect(client._display.viewportChangeSize).to.have.been.calledOnce; + expect(client._display.viewportChangeSize).to.have.been.calledWith(40, 50); + }); + + it('should update the viewport when the remote session resizes', function () { + // Simple ExtendedDesktopSize FBU message + const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0xfe, 0xcc, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, + 0x00, 0x00, 0x00, 0x00 ]; + + sinon.spy(client._display, "viewportChangeSize"); + + client._sock._websocket._receive_data(new Uint8Array(incoming)); + + // FIXME: Display implicitly calls viewportChangeSize() when + // resizing the framebuffer, hence calledTwice. + expect(client._display.viewportChangeSize).to.have.been.calledTwice; + expect(client._display.viewportChangeSize).to.have.been.calledWith(70, 80); + }); + + it('should not update the viewport if not clipping', function () { + client.clipViewport = false; + sinon.spy(client._display, "viewportChangeSize"); + + container.style.width = '40px'; + container.style.height = '50px'; + const event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(); + + expect(client._display.viewportChangeSize).to.not.have.been.called; + }); + + it('should not update the viewport if scaling', function () { + client.scaleViewport = true; + sinon.spy(client._display, "viewportChangeSize"); + + container.style.width = '40px'; + container.style.height = '50px'; + const event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(); + + expect(client._display.viewportChangeSize).to.not.have.been.called; + }); + + describe('Dragging', function () { + beforeEach(function () { + client.dragViewport = true; + sinon.spy(RFB.messages, "pointerEvent"); + }); + + afterEach(function () { + RFB.messages.pointerEvent.restore(); + }); + + it('should not send button messages when initiating viewport dragging', function () { + client._handleMouseButton(13, 9, 0x001); + expect(RFB.messages.pointerEvent).to.not.have.been.called; + }); + + it('should send button messages when release without movement', function () { + // Just up and down + client._handleMouseButton(13, 9, 0x001); + client._handleMouseButton(13, 9, 0x000); + expect(RFB.messages.pointerEvent).to.have.been.calledTwice; + + RFB.messages.pointerEvent.reset(); + + // Small movement + client._handleMouseButton(13, 9, 0x001); + client._handleMouseMove(15, 14); + client._handleMouseButton(15, 14, 0x000); + expect(RFB.messages.pointerEvent).to.have.been.calledTwice; + }); + + it('should send button message directly when drag is disabled', function () { + client.dragViewport = false; + client._handleMouseButton(13, 9, 0x001); + expect(RFB.messages.pointerEvent).to.have.been.calledOnce; + }); + + it('should be initiate viewport dragging on sufficient movement', function () { + sinon.spy(client._display, "viewportChangePos"); + + // Too small movement + + client._handleMouseButton(13, 9, 0x001); + client._handleMouseMove(18, 9); + + expect(RFB.messages.pointerEvent).to.not.have.been.called; + expect(client._display.viewportChangePos).to.not.have.been.called; + + // Sufficient movement + + client._handleMouseMove(43, 9); + + expect(RFB.messages.pointerEvent).to.not.have.been.called; + expect(client._display.viewportChangePos).to.have.been.calledOnce; + expect(client._display.viewportChangePos).to.have.been.calledWith(-30, 0); + + client._display.viewportChangePos.reset(); + + // Now a small movement should move right away + + client._handleMouseMove(43, 14); + + expect(RFB.messages.pointerEvent).to.not.have.been.called; + expect(client._display.viewportChangePos).to.have.been.calledOnce; + expect(client._display.viewportChangePos).to.have.been.calledWith(0, -5); + }); + + it('should not send button messages when dragging ends', function () { + // First the movement + + client._handleMouseButton(13, 9, 0x001); + client._handleMouseMove(43, 9); + client._handleMouseButton(43, 9, 0x000); + + expect(RFB.messages.pointerEvent).to.not.have.been.called; + }); + + it('should terminate viewport dragging on a button up event', function () { + // First the dragging movement + + client._handleMouseButton(13, 9, 0x001); + client._handleMouseMove(43, 9); + client._handleMouseButton(43, 9, 0x000); + + // Another movement now should not move the viewport + + sinon.spy(client._display, "viewportChangePos"); + + client._handleMouseMove(43, 59); + + expect(client._display.viewportChangePos).to.not.have.been.called; + }); + }); + }); + + describe('Scaling', function () { + let client; + beforeEach(function () { + client = make_rfb(); + container.style.width = '70px'; + container.style.height = '80px'; + client.scaleViewport = true; + }); + + it('should update display scale factor when changing the property', function () { + const spy = sinon.spy(client._display, "scale", ["set"]); + sinon.spy(client._display, "autoscale"); + + client.scaleViewport = false; + expect(spy.set).to.have.been.calledOnce; + expect(spy.set).to.have.been.calledWith(1.0); + expect(client._display.autoscale).to.not.have.been.called; + + client.scaleViewport = true; + expect(client._display.autoscale).to.have.been.calledOnce; + expect(client._display.autoscale).to.have.been.calledWith(70, 80); + }); + + it('should update the clipping setting when changing the property', function () { + client.clipViewport = true; + + const spy = sinon.spy(client._display, "clipViewport", ["set"]); + + client.scaleViewport = false; + expect(spy.set).to.have.been.calledOnce; + expect(spy.set).to.have.been.calledWith(true); + + spy.set.reset(); + + client.scaleViewport = true; + expect(spy.set).to.have.been.calledOnce; + expect(spy.set).to.have.been.calledWith(false); + }); + + it('should update the scaling when the container size changes', function () { + sinon.spy(client._display, "autoscale"); + + container.style.width = '40px'; + container.style.height = '50px'; + const event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(); + + expect(client._display.autoscale).to.have.been.calledOnce; + expect(client._display.autoscale).to.have.been.calledWith(40, 50); + }); + + it('should update the scaling when the remote session resizes', function () { + // Simple ExtendedDesktopSize FBU message + const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0xfe, 0xcc, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, + 0x00, 0x00, 0x00, 0x00 ]; + + sinon.spy(client._display, "autoscale"); + + client._sock._websocket._receive_data(new Uint8Array(incoming)); + + expect(client._display.autoscale).to.have.been.calledOnce; + expect(client._display.autoscale).to.have.been.calledWith(70, 80); + }); + + it('should not update the display scale factor if not scaling', function () { + client.scaleViewport = false; + + sinon.spy(client._display, "autoscale"); + + container.style.width = '40px'; + container.style.height = '50px'; + const event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(); + + expect(client._display.autoscale).to.not.have.been.called; + }); + }); + + describe('Remote resize', function () { + let client; + beforeEach(function () { + client = make_rfb(); + client._supportsSetDesktopSize = true; + client.resizeSession = true; + container.style.width = '70px'; + container.style.height = '80px'; + sinon.spy(RFB.messages, "setDesktopSize"); + }); + + afterEach(function () { + RFB.messages.setDesktopSize.restore(); + }); + + it('should only request a resize when turned on', function () { + client.resizeSession = false; + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + client.resizeSession = true; + expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; + }); + + it('should request a resize when initially connecting', function () { + // Simple ExtendedDesktopSize FBU message + const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x04, 0xff, 0xff, 0xfe, 0xcc, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00 ]; + + // First message should trigger a resize + + client._supportsSetDesktopSize = false; + + client._sock._websocket._receive_data(new Uint8Array(incoming)); + + expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; + expect(RFB.messages.setDesktopSize).to.have.been.calledWith(sinon.match.object, 70, 80, 0, 0); + + RFB.messages.setDesktopSize.reset(); + + // Second message should not trigger a resize + + client._sock._websocket._receive_data(new Uint8Array(incoming)); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + }); + + it('should request a resize when the container resizes', function () { + container.style.width = '40px'; + container.style.height = '50px'; + const event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(1000); + + expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; + expect(RFB.messages.setDesktopSize).to.have.been.calledWith(sinon.match.object, 40, 50, 0, 0); + }); + + it('should not resize until the container size is stable', function () { + container.style.width = '20px'; + container.style.height = '30px'; + const event1 = new UIEvent('resize'); + window.dispatchEvent(event1); + clock.tick(400); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + + container.style.width = '40px'; + container.style.height = '50px'; + const event2 = new UIEvent('resize'); + window.dispatchEvent(event2); + clock.tick(400); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + + clock.tick(200); + + expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; + expect(RFB.messages.setDesktopSize).to.have.been.calledWith(sinon.match.object, 40, 50, 0, 0); + }); + + it('should not resize when resize is disabled', function () { + client._resizeSession = false; + + container.style.width = '40px'; + container.style.height = '50px'; + const event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(1000); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + }); + + it('should not resize when resize is not supported', function () { + client._supportsSetDesktopSize = false; + + container.style.width = '40px'; + container.style.height = '50px'; + const event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(1000); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + }); + + it('should not resize when in view only mode', function () { + client._viewOnly = true; + + container.style.width = '40px'; + container.style.height = '50px'; + const event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(1000); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + }); + + it('should not try to override a server resize', function () { + // Simple ExtendedDesktopSize FBU message + const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x04, 0xff, 0xff, 0xfe, 0xcc, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00 ]; + + client._sock._websocket._receive_data(new Uint8Array(incoming)); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + }); + }); + + describe('Misc Internals', function () { + describe('#_updateConnectionState', function () { + let client; + beforeEach(function () { + client = make_rfb(); + }); + + it('should clear the disconnect timer if the state is not "disconnecting"', function () { + const spy = sinon.spy(); + client._disconnTimer = setTimeout(spy, 50); + client._rfb_connection_state = 'connecting'; + client._updateConnectionState('connected'); + this.clock.tick(51); + expect(spy).to.not.have.been.called; + expect(client._disconnTimer).to.be.null; + }); + + it('should set the rfb_connection_state', function () { + client._rfb_connection_state = 'connecting'; + client._updateConnectionState('connected'); + expect(client._rfb_connection_state).to.equal('connected'); + }); + + it('should not change the state when we are disconnected', function () { + client.disconnect(); + expect(client._rfb_connection_state).to.equal('disconnected'); + client._updateConnectionState('connecting'); + expect(client._rfb_connection_state).to.not.equal('connecting'); + }); + + it('should ignore state changes to the same state', function () { + const connectSpy = sinon.spy(); + client.addEventListener("connect", connectSpy); + + expect(client._rfb_connection_state).to.equal('connected'); + client._updateConnectionState('connected'); + expect(connectSpy).to.not.have.been.called; + + client.disconnect(); + + const disconnectSpy = sinon.spy(); + client.addEventListener("disconnect", disconnectSpy); + + expect(client._rfb_connection_state).to.equal('disconnected'); + client._updateConnectionState('disconnected'); + expect(disconnectSpy).to.not.have.been.called; + }); + + it('should ignore illegal state changes', function () { + const spy = sinon.spy(); + client.addEventListener("disconnect", spy); + client._updateConnectionState('disconnected'); + expect(client._rfb_connection_state).to.not.equal('disconnected'); + expect(spy).to.not.have.been.called; + }); + }); + + describe('#_fail', function () { + let client; + beforeEach(function () { + client = make_rfb(); + }); + + it('should close the WebSocket connection', function () { + sinon.spy(client._sock, 'close'); + client._fail(); + expect(client._sock.close).to.have.been.calledOnce; + }); + + it('should transition to disconnected', function () { + sinon.spy(client, '_updateConnectionState'); + client._fail(); + this.clock.tick(2000); + expect(client._updateConnectionState).to.have.been.called; + expect(client._rfb_connection_state).to.equal('disconnected'); + }); + + it('should set clean_disconnect variable', function () { + client._rfb_clean_disconnect = true; + client._rfb_connection_state = 'connected'; + client._fail(); + expect(client._rfb_clean_disconnect).to.be.false; + }); + + it('should result in disconnect event with clean set to false', function () { + client._rfb_connection_state = 'connected'; + const spy = sinon.spy(); + client.addEventListener("disconnect", spy); + client._fail(); + this.clock.tick(2000); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][0].detail.clean).to.be.false; + }); + + }); + }); + + describe('Connection States', function () { + describe('connecting', function () { + it('should open the websocket connection', function () { + const client = new RFB(document.createElement('div'), + 'ws://HOST:8675/PATH'); + sinon.spy(client._sock, 'open'); + this.clock.tick(); + expect(client._sock.open).to.have.been.calledOnce; + }); + }); + + describe('connected', function () { + let client; + beforeEach(function () { + client = make_rfb(); + }); + + it('should result in a connect event if state becomes connected', function () { + const spy = sinon.spy(); + client.addEventListener("connect", spy); + client._rfb_connection_state = 'connecting'; + client._updateConnectionState('connected'); + expect(spy).to.have.been.calledOnce; + }); + + it('should not result in a connect event if the state is not "connected"', function () { + const spy = sinon.spy(); + client.addEventListener("connect", spy); + client._sock._websocket.open = () => {}; // explicitly don't call onopen + client._updateConnectionState('connecting'); + expect(spy).to.not.have.been.called; + }); + }); + + describe('disconnecting', function () { + let client; + beforeEach(function () { + client = make_rfb(); + }); + + it('should force disconnect if we do not call Websock.onclose within the disconnection timeout', function () { + sinon.spy(client, '_updateConnectionState'); + client._sock._websocket.close = () => {}; // explicitly don't call onclose + client._updateConnectionState('disconnecting'); + this.clock.tick(3 * 1000); + expect(client._updateConnectionState).to.have.been.calledTwice; + expect(client._rfb_disconnect_reason).to.not.equal(""); + expect(client._rfb_connection_state).to.equal("disconnected"); + }); + + it('should not fail if Websock.onclose gets called within the disconnection timeout', function () { + client._updateConnectionState('disconnecting'); + this.clock.tick(3 * 1000 / 2); + client._sock._websocket.close(); + this.clock.tick(3 * 1000 / 2 + 1); + expect(client._rfb_connection_state).to.equal('disconnected'); + }); + + it('should close the WebSocket connection', function () { + sinon.spy(client._sock, 'close'); + client._updateConnectionState('disconnecting'); + expect(client._sock.close).to.have.been.calledOnce; + }); + + it('should not result in a disconnect event', function () { + const spy = sinon.spy(); + client.addEventListener("disconnect", spy); + client._sock._websocket.close = () => {}; // explicitly don't call onclose + client._updateConnectionState('disconnecting'); + expect(spy).to.not.have.been.called; + }); + }); + + describe('disconnected', function () { + let client; + beforeEach(function () { + client = new RFB(document.createElement('div'), 'ws://HOST:8675/PATH'); + }); + + it('should result in a disconnect event if state becomes "disconnected"', function () { + const spy = sinon.spy(); + client.addEventListener("disconnect", spy); + client._rfb_connection_state = 'disconnecting'; + client._updateConnectionState('disconnected'); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][0].detail.clean).to.be.true; + }); + + it('should result in a disconnect event without msg when no reason given', function () { + const spy = sinon.spy(); + client.addEventListener("disconnect", spy); + client._rfb_connection_state = 'disconnecting'; + client._rfb_disconnect_reason = ""; + client._updateConnectionState('disconnected'); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0].length).to.equal(1); + }); + }); + }); + + describe('Protocol Initialization States', function () { + let client; + beforeEach(function () { + client = make_rfb(); + client._rfb_connection_state = 'connecting'; + }); + + describe('ProtocolVersion', function () { + function send_ver(ver, client) { + const arr = new Uint8Array(12); + for (let i = 0; i < ver.length; i++) { + arr[i+4] = ver.charCodeAt(i); + } + arr[0] = 'R'; arr[1] = 'F'; arr[2] = 'B'; arr[3] = ' '; + arr[11] = '\n'; + client._sock._websocket._receive_data(arr); + } + + describe('version parsing', function () { + it('should interpret version 003.003 as version 3.3', function () { + send_ver('003.003', client); + expect(client._rfb_version).to.equal(3.3); + }); + + it('should interpret version 003.006 as version 3.3', function () { + send_ver('003.006', client); + expect(client._rfb_version).to.equal(3.3); + }); + + it('should interpret version 003.889 as version 3.3', function () { + send_ver('003.889', client); + expect(client._rfb_version).to.equal(3.3); + }); + + it('should interpret version 003.007 as version 3.7', function () { + send_ver('003.007', client); + expect(client._rfb_version).to.equal(3.7); + }); + + it('should interpret version 003.008 as version 3.8', function () { + send_ver('003.008', client); + expect(client._rfb_version).to.equal(3.8); + }); + + it('should interpret version 004.000 as version 3.8', function () { + send_ver('004.000', client); + expect(client._rfb_version).to.equal(3.8); + }); + + it('should interpret version 004.001 as version 3.8', function () { + send_ver('004.001', client); + expect(client._rfb_version).to.equal(3.8); + }); + + it('should interpret version 005.000 as version 3.8', function () { + send_ver('005.000', client); + expect(client._rfb_version).to.equal(3.8); + }); + + it('should fail on an invalid version', function () { + sinon.spy(client, "_fail"); + send_ver('002.000', client); + expect(client._fail).to.have.been.calledOnce; + }); + }); + + it('should send back the interpreted version', function () { + send_ver('004.000', client); + + const expected_str = 'RFB 003.008\n'; + const expected = []; + for (let i = 0; i < expected_str.length; i++) { + expected[i] = expected_str.charCodeAt(i); + } + + expect(client._sock).to.have.sent(new Uint8Array(expected)); + }); + + it('should transition to the Security state on successful negotiation', function () { + send_ver('003.008', client); + expect(client._rfb_init_state).to.equal('Security'); + }); + + describe('Repeater', function () { + beforeEach(function () { + client = make_rfb('wss://host:8675', { repeaterID: "12345" }); + client._rfb_connection_state = 'connecting'; + }); + + it('should interpret version 000.000 as a repeater', function () { + send_ver('000.000', client); + expect(client._rfb_version).to.equal(0); + + const sent_data = client._sock._websocket._get_sent_data(); + expect(new Uint8Array(sent_data.buffer, 0, 9)).to.array.equal(new Uint8Array([73, 68, 58, 49, 50, 51, 52, 53, 0])); + expect(sent_data).to.have.length(250); + }); + + it('should handle two step repeater negotiation', function () { + send_ver('000.000', client); + send_ver('003.008', client); + expect(client._rfb_version).to.equal(3.8); + }); + }); + }); + + describe('Security', function () { + beforeEach(function () { + client._rfb_init_state = 'Security'; + }); + + it('should simply receive the auth scheme when for versions < 3.7', function () { + client._rfb_version = 3.6; + const auth_scheme_raw = [1, 2, 3, 4]; + const auth_scheme = (auth_scheme_raw[0] << 24) + (auth_scheme_raw[1] << 16) + + (auth_scheme_raw[2] << 8) + auth_scheme_raw[3]; + client._sock._websocket._receive_data(new Uint8Array(auth_scheme_raw)); + expect(client._rfb_auth_scheme).to.equal(auth_scheme); + }); + + it('should prefer no authentication is possible', function () { + client._rfb_version = 3.7; + const auth_schemes = [2, 1, 3]; + client._sock._websocket._receive_data(new Uint8Array(auth_schemes)); + expect(client._rfb_auth_scheme).to.equal(1); + expect(client._sock).to.have.sent(new Uint8Array([1, 1])); + }); + + it('should choose for the most prefered scheme possible for versions >= 3.7', function () { + client._rfb_version = 3.7; + const auth_schemes = [2, 22, 16]; + client._sock._websocket._receive_data(new Uint8Array(auth_schemes)); + expect(client._rfb_auth_scheme).to.equal(22); + expect(client._sock).to.have.sent(new Uint8Array([22])); + }); + + it('should fail if there are no supported schemes for versions >= 3.7', function () { + sinon.spy(client, "_fail"); + client._rfb_version = 3.7; + const auth_schemes = [1, 32]; + client._sock._websocket._receive_data(new Uint8Array(auth_schemes)); + expect(client._fail).to.have.been.calledOnce; + }); + + it('should fail with the appropriate message if no types are sent for versions >= 3.7', function () { + client._rfb_version = 3.7; + const failure_data = [0, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115]; + sinon.spy(client, '_fail'); + client._sock._websocket._receive_data(new Uint8Array(failure_data)); + + expect(client._fail).to.have.been.calledOnce; + expect(client._fail).to.have.been.calledWith( + 'Security negotiation failed on no security types (reason: whoops)'); + }); + + it('should transition to the Authentication state and continue on successful negotiation', function () { + client._rfb_version = 3.7; + const auth_schemes = [1, 1]; + client._negotiate_authentication = sinon.spy(); + client._sock._websocket._receive_data(new Uint8Array(auth_schemes)); + expect(client._rfb_init_state).to.equal('Authentication'); + expect(client._negotiate_authentication).to.have.been.calledOnce; + }); + }); + + describe('Authentication', function () { + beforeEach(function () { + client._rfb_init_state = 'Security'; + }); + + function send_security(type, cl) { + cl._sock._websocket._receive_data(new Uint8Array([1, type])); + } + + it('should fail on auth scheme 0 (pre 3.7) with the given message', function () { + client._rfb_version = 3.6; + const err_msg = "Whoopsies"; + const data = [0, 0, 0, 0]; + const err_len = err_msg.length; + push32(data, err_len); + for (let i = 0; i < err_len; i++) { + data.push(err_msg.charCodeAt(i)); + } + + sinon.spy(client, '_fail'); + client._sock._websocket._receive_data(new Uint8Array(data)); + expect(client._fail).to.have.been.calledWith( + 'Security negotiation failed on authentication scheme (reason: Whoopsies)'); + }); + + it('should transition straight to SecurityResult on "no auth" (1) for versions >= 3.8', function () { + client._rfb_version = 3.8; + send_security(1, client); + expect(client._rfb_init_state).to.equal('SecurityResult'); + }); + + it('should transition straight to ServerInitialisation on "no auth" for versions < 3.8', function () { + client._rfb_version = 3.7; + send_security(1, client); + expect(client._rfb_init_state).to.equal('ServerInitialisation'); + }); + + it('should fail on an unknown auth scheme', function () { + sinon.spy(client, "_fail"); + client._rfb_version = 3.8; + send_security(57, client); + expect(client._fail).to.have.been.calledOnce; + }); + + describe('VNC Authentication (type 2) Handler', function () { + beforeEach(function () { + client._rfb_init_state = 'Security'; + client._rfb_version = 3.8; + }); + + it('should fire the credentialsrequired event if missing a password', function () { + const spy = sinon.spy(); + client.addEventListener("credentialsrequired", spy); + send_security(2, client); + + const challenge = []; + for (let i = 0; i < 16; i++) { challenge[i] = i; } + client._sock._websocket._receive_data(new Uint8Array(challenge)); + + expect(client._rfb_credentials).to.be.empty; + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][0].detail.types).to.have.members(["password"]); + }); + + it('should encrypt the password with DES and then send it back', function () { + client._rfb_credentials = { password: 'passwd' }; + send_security(2, client); + client._sock._websocket._get_sent_data(); // skip the choice of auth reply + + const challenge = []; + for (let i = 0; i < 16; i++) { challenge[i] = i; } + client._sock._websocket._receive_data(new Uint8Array(challenge)); + + const des_pass = RFB.genDES('passwd', challenge); + expect(client._sock).to.have.sent(new Uint8Array(des_pass)); + }); + + it('should transition to SecurityResult immediately after sending the password', function () { + client._rfb_credentials = { password: 'passwd' }; + send_security(2, client); + + const challenge = []; + for (let i = 0; i < 16; i++) { challenge[i] = i; } + client._sock._websocket._receive_data(new Uint8Array(challenge)); + + expect(client._rfb_init_state).to.equal('SecurityResult'); + }); + }); + + describe('XVP Authentication (type 22) Handler', function () { + beforeEach(function () { + client._rfb_init_state = 'Security'; + client._rfb_version = 3.8; + }); + + it('should fall through to standard VNC authentication upon completion', function () { + client._rfb_credentials = { username: 'user', + target: 'target', + password: 'password' }; + client._negotiate_std_vnc_auth = sinon.spy(); + send_security(22, client); + expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce; + }); + + it('should fire the credentialsrequired event if all credentials are missing', function () { + const spy = sinon.spy(); + client.addEventListener("credentialsrequired", spy); + client._rfb_credentials = {}; + send_security(22, client); + + expect(client._rfb_credentials).to.be.empty; + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][0].detail.types).to.have.members(["username", "password", "target"]); + }); + + it('should fire the credentialsrequired event if some credentials are missing', function () { + const spy = sinon.spy(); + client.addEventListener("credentialsrequired", spy); + client._rfb_credentials = { username: 'user', + target: 'target' }; + send_security(22, client); + + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][0].detail.types).to.have.members(["username", "password", "target"]); + }); + + it('should send user and target separately', function () { + client._rfb_credentials = { username: 'user', + target: 'target', + password: 'password' }; + client._negotiate_std_vnc_auth = sinon.spy(); + + send_security(22, client); + + const expected = [22, 4, 6]; // auth selection, len user, len target + for (let i = 0; i < 10; i++) { expected[i+3] = 'usertarget'.charCodeAt(i); } + + expect(client._sock).to.have.sent(new Uint8Array(expected)); + }); + }); + + describe('TightVNC Authentication (type 16) Handler', function () { + beforeEach(function () { + client._rfb_init_state = 'Security'; + client._rfb_version = 3.8; + send_security(16, client); + client._sock._websocket._get_sent_data(); // skip the security reply + }); + + function send_num_str_pairs(pairs, client) { + const data = []; + push32(data, pairs.length); + + for (let i = 0; i < pairs.length; i++) { + push32(data, pairs[i][0]); + for (let j = 0; j < 4; j++) { + data.push(pairs[i][1].charCodeAt(j)); + } + for (let j = 0; j < 8; j++) { + data.push(pairs[i][2].charCodeAt(j)); + } + } + + client._sock._websocket._receive_data(new Uint8Array(data)); + } + + it('should skip tunnel negotiation if no tunnels are requested', function () { + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); + expect(client._rfb_tightvnc).to.be.true; + }); + + it('should fail if no supported tunnels are listed', function () { + sinon.spy(client, "_fail"); + send_num_str_pairs([[123, 'OTHR', 'SOMETHNG']], client); + expect(client._fail).to.have.been.calledOnce; + }); + + it('should choose the notunnel tunnel type', function () { + send_num_str_pairs([[0, 'TGHT', 'NOTUNNEL'], [123, 'OTHR', 'SOMETHNG']], client); + expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 0])); + }); + + it('should choose the notunnel tunnel type for Siemens devices', function () { + send_num_str_pairs([[1, 'SICR', 'SCHANNEL'], [2, 'SICR', 'SCHANLPW']], client); + expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 0])); + }); + + it('should continue to sub-auth negotiation after tunnel negotiation', function () { + send_num_str_pairs([[0, 'TGHT', 'NOTUNNEL']], client); + client._sock._websocket._get_sent_data(); // skip the tunnel choice here + send_num_str_pairs([[1, 'STDV', 'NOAUTH__']], client); + expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 1])); + expect(client._rfb_init_state).to.equal('SecurityResult'); + }); + + /*it('should attempt to use VNC auth over no auth when possible', function () { + client._rfb_tightvnc = true; + client._negotiate_std_vnc_auth = sinon.spy(); + send_num_str_pairs([[1, 'STDV', 'NOAUTH__'], [2, 'STDV', 'VNCAUTH_']], client); + expect(client._sock).to.have.sent([0, 0, 0, 1]); + expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce; + expect(client._rfb_auth_scheme).to.equal(2); + });*/ // while this would make sense, the original code doesn't actually do this + + it('should accept the "no auth" auth type and transition to SecurityResult', function () { + client._rfb_tightvnc = true; + send_num_str_pairs([[1, 'STDV', 'NOAUTH__']], client); + expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 1])); + expect(client._rfb_init_state).to.equal('SecurityResult'); + }); + + it('should accept VNC authentication and transition to that', function () { + client._rfb_tightvnc = true; + client._negotiate_std_vnc_auth = sinon.spy(); + send_num_str_pairs([[2, 'STDV', 'VNCAUTH__']], client); + expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 2])); + expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce; + expect(client._rfb_auth_scheme).to.equal(2); + }); + + it('should fail if there are no supported auth types', function () { + sinon.spy(client, "_fail"); + client._rfb_tightvnc = true; + send_num_str_pairs([[23, 'stdv', 'badval__']], client); + expect(client._fail).to.have.been.calledOnce; + }); + }); + }); + + describe('SecurityResult', function () { + beforeEach(function () { + client._rfb_init_state = 'SecurityResult'; + }); + + it('should fall through to ServerInitialisation on a response code of 0', function () { + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); + expect(client._rfb_init_state).to.equal('ServerInitialisation'); + }); + + it('should fail on an error code of 1 with the given message for versions >= 3.8', function () { + client._rfb_version = 3.8; + sinon.spy(client, '_fail'); + const failure_data = [0, 0, 0, 1, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115]; + client._sock._websocket._receive_data(new Uint8Array(failure_data)); + expect(client._fail).to.have.been.calledWith( + 'Security negotiation failed on security result (reason: whoops)'); + }); + + it('should fail on an error code of 1 with a standard message for version < 3.8', function () { + sinon.spy(client, '_fail'); + client._rfb_version = 3.7; + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 1])); + expect(client._fail).to.have.been.calledWith( + 'Security handshake failed'); + }); + + it('should result in securityfailure event when receiving a non zero status', function () { + const spy = sinon.spy(); + client.addEventListener("securityfailure", spy); + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 2])); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][0].detail.status).to.equal(2); + }); + + it('should include reason when provided in securityfailure event', function () { + client._rfb_version = 3.8; + const spy = sinon.spy(); + client.addEventListener("securityfailure", spy); + const failure_data = [0, 0, 0, 1, 0, 0, 0, 12, 115, 117, 99, 104, + 32, 102, 97, 105, 108, 117, 114, 101]; + client._sock._websocket._receive_data(new Uint8Array(failure_data)); + expect(spy.args[0][0].detail.status).to.equal(1); + expect(spy.args[0][0].detail.reason).to.equal('such failure'); + }); + + it('should not include reason when length is zero in securityfailure event', function () { + client._rfb_version = 3.9; + const spy = sinon.spy(); + client.addEventListener("securityfailure", spy); + const failure_data = [0, 0, 0, 1, 0, 0, 0, 0]; + client._sock._websocket._receive_data(new Uint8Array(failure_data)); + expect(spy.args[0][0].detail.status).to.equal(1); + expect('reason' in spy.args[0][0].detail).to.be.false; + }); + + it('should not include reason in securityfailure event for version < 3.8', function () { + client._rfb_version = 3.6; + const spy = sinon.spy(); + client.addEventListener("securityfailure", spy); + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 2])); + expect(spy.args[0][0].detail.status).to.equal(2); + expect('reason' in spy.args[0][0].detail).to.be.false; + }); + }); + + describe('ClientInitialisation', function () { + it('should transition to the ServerInitialisation state', function () { + const client = make_rfb(); + client._rfb_connection_state = 'connecting'; + client._rfb_init_state = 'SecurityResult'; + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); + expect(client._rfb_init_state).to.equal('ServerInitialisation'); + }); + + it('should send 1 if we are in shared mode', function () { + const client = make_rfb('wss://host:8675', { shared: true }); + client._rfb_connection_state = 'connecting'; + client._rfb_init_state = 'SecurityResult'; + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); + expect(client._sock).to.have.sent(new Uint8Array([1])); + }); + + it('should send 0 if we are not in shared mode', function () { + const client = make_rfb('wss://host:8675', { shared: false }); + client._rfb_connection_state = 'connecting'; + client._rfb_init_state = 'SecurityResult'; + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); + expect(client._sock).to.have.sent(new Uint8Array([0])); + }); + }); + + describe('ServerInitialisation', function () { + beforeEach(function () { + client._rfb_init_state = 'ServerInitialisation'; + }); + + function send_server_init(opts, client) { + const full_opts = { width: 10, height: 12, bpp: 24, depth: 24, big_endian: 0, + true_color: 1, red_max: 255, green_max: 255, blue_max: 255, + red_shift: 16, green_shift: 8, blue_shift: 0, name: 'a name' }; + for (let opt in opts) { + full_opts[opt] = opts[opt]; + } + const data = []; + + push16(data, full_opts.width); + push16(data, full_opts.height); + + data.push(full_opts.bpp); + data.push(full_opts.depth); + data.push(full_opts.big_endian); + data.push(full_opts.true_color); + + push16(data, full_opts.red_max); + push16(data, full_opts.green_max); + push16(data, full_opts.blue_max); + push8(data, full_opts.red_shift); + push8(data, full_opts.green_shift); + push8(data, full_opts.blue_shift); + + // padding + push8(data, 0); + push8(data, 0); + push8(data, 0); + + client._sock._websocket._receive_data(new Uint8Array(data)); + + const name_data = []; + push32(name_data, full_opts.name.length); + for (let i = 0; i < full_opts.name.length; i++) { + name_data.push(full_opts.name.charCodeAt(i)); + } + client._sock._websocket._receive_data(new Uint8Array(name_data)); + } + + it('should set the framebuffer width and height', function () { + send_server_init({ width: 32, height: 84 }, client); + expect(client._fb_width).to.equal(32); + expect(client._fb_height).to.equal(84); + }); + + // NB(sross): we just warn, not fail, for endian-ness and shifts, so we don't test them + + it('should set the framebuffer name and call the callback', function () { + const spy = sinon.spy(); + client.addEventListener("desktopname", spy); + send_server_init({ name: 'some name' }, client); + + expect(client._fb_name).to.equal('some name'); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][0].detail.name).to.equal('some name'); + }); + + it('should handle the extended init message of the tight encoding', function () { + // NB(sross): we don't actually do anything with it, so just test that we can + // read it w/o throwing an error + client._rfb_tightvnc = true; + send_server_init({}, client); + + const tight_data = []; + push16(tight_data, 1); + push16(tight_data, 2); + push16(tight_data, 3); + push16(tight_data, 0); + for (let i = 0; i < 16 + 32 + 48; i++) { + tight_data.push(i); + } + client._sock._websocket._receive_data(new Uint8Array(tight_data)); + + expect(client._rfb_connection_state).to.equal('connected'); + }); + + it('should resize the display', function () { + sinon.spy(client._display, 'resize'); + send_server_init({ width: 27, height: 32 }, client); + + expect(client._display.resize).to.have.been.calledOnce; + expect(client._display.resize).to.have.been.calledWith(27, 32); + }); + + it('should grab the mouse and keyboard', function () { + sinon.spy(client._keyboard, 'grab'); + sinon.spy(client._mouse, 'grab'); + send_server_init({}, client); + expect(client._keyboard.grab).to.have.been.calledOnce; + expect(client._mouse.grab).to.have.been.calledOnce; + }); + + describe('Initial Update Request', function () { + beforeEach(function () { + sinon.spy(RFB.messages, "pixelFormat"); + sinon.spy(RFB.messages, "clientEncodings"); + sinon.spy(RFB.messages, "fbUpdateRequest"); + }); + + afterEach(function () { + RFB.messages.pixelFormat.restore(); + RFB.messages.clientEncodings.restore(); + RFB.messages.fbUpdateRequest.restore(); + }); + + // TODO(directxman12): test the various options in this configuration matrix + it('should reply with the pixel format, client encodings, and initial update request', function () { + send_server_init({ width: 27, height: 32 }, client); + + expect(RFB.messages.pixelFormat).to.have.been.calledOnce; + expect(RFB.messages.pixelFormat).to.have.been.calledWith(client._sock, 24, true); + expect(RFB.messages.pixelFormat).to.have.been.calledBefore(RFB.messages.clientEncodings); + expect(RFB.messages.clientEncodings).to.have.been.calledOnce; + expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.include(encodings.encodingTight); + expect(RFB.messages.clientEncodings).to.have.been.calledBefore(RFB.messages.fbUpdateRequest); + expect(RFB.messages.fbUpdateRequest).to.have.been.calledOnce; + expect(RFB.messages.fbUpdateRequest).to.have.been.calledWith(client._sock, false, 0, 0, 27, 32); + }); + + it('should reply with restricted settings for Intel AMT servers', function () { + send_server_init({ width: 27, height: 32, name: "Intel(r) AMT KVM"}, client); + + expect(RFB.messages.pixelFormat).to.have.been.calledOnce; + expect(RFB.messages.pixelFormat).to.have.been.calledWith(client._sock, 8, true); + expect(RFB.messages.pixelFormat).to.have.been.calledBefore(RFB.messages.clientEncodings); + expect(RFB.messages.clientEncodings).to.have.been.calledOnce; + expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.not.include(encodings.encodingTight); + expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.not.include(encodings.encodingHextile); + expect(RFB.messages.clientEncodings).to.have.been.calledBefore(RFB.messages.fbUpdateRequest); + expect(RFB.messages.fbUpdateRequest).to.have.been.calledOnce; + expect(RFB.messages.fbUpdateRequest).to.have.been.calledWith(client._sock, false, 0, 0, 27, 32); + }); + }); + + it('should transition to the "connected" state', function () { + send_server_init({}, client); + expect(client._rfb_connection_state).to.equal('connected'); + }); + }); + }); + + describe('Protocol Message Processing After Completing Initialization', function () { + let client; + + beforeEach(function () { + client = make_rfb(); + client._fb_name = 'some device'; + client._fb_width = 640; + client._fb_height = 20; + }); + + describe('Framebuffer Update Handling', function () { + const target_data_arr = [ + 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255, + 0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255 + ]; + let target_data; + + const target_data_check_arr = [ + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 + ]; + let target_data_check; + + before(function () { + // NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray + target_data = new Uint8Array(target_data_arr); + target_data_check = new Uint8Array(target_data_check_arr); + }); + + function send_fbu_msg(rect_info, rect_data, client, rect_cnt) { + let data = []; + + if (!rect_cnt || rect_cnt > -1) { + // header + data.push(0); // msg type + data.push(0); // padding + push16(data, rect_cnt || rect_data.length); + } + + for (let i = 0; i < rect_data.length; i++) { + if (rect_info[i]) { + push16(data, rect_info[i].x); + push16(data, rect_info[i].y); + push16(data, rect_info[i].width); + push16(data, rect_info[i].height); + push32(data, rect_info[i].encoding); + } + data = data.concat(rect_data[i]); + } + + client._sock._websocket._receive_data(new Uint8Array(data)); + } + + it('should send an update request if there is sufficient data', function () { + const expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: () => {}}; + RFB.messages.fbUpdateRequest(expected_msg, true, 0, 0, 640, 20); + + client._framebufferUpdate = () => true; + client._sock._websocket._receive_data(new Uint8Array([0])); + + expect(client._sock).to.have.sent(expected_msg._sQ); + }); + + it('should not send an update request if we need more data', function () { + client._sock._websocket._receive_data(new Uint8Array([0])); + expect(client._sock._websocket._get_sent_data()).to.have.length(0); + }); + + it('should resume receiving an update if we previously did not have enough data', function () { + const expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: () => {}}; + RFB.messages.fbUpdateRequest(expected_msg, true, 0, 0, 640, 20); + + // just enough to set FBU.rects + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 3])); + expect(client._sock._websocket._get_sent_data()).to.have.length(0); + + client._framebufferUpdate = function () { this._sock.rQskipBytes(1); return true; }; // we magically have enough data + // 247 should *not* be used as the message type here + client._sock._websocket._receive_data(new Uint8Array([247])); + expect(client._sock).to.have.sent(expected_msg._sQ); + }); + + it('should not send a request in continuous updates mode', function () { + client._enabledContinuousUpdates = true; + client._framebufferUpdate = () => true; + client._sock._websocket._receive_data(new Uint8Array([0])); + + expect(client._sock._websocket._get_sent_data()).to.have.length(0); + }); + + it('should fail on an unsupported encoding', function () { + sinon.spy(client, "_fail"); + const rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 234 }; + send_fbu_msg([rect_info], [[]], client); + expect(client._fail).to.have.been.calledOnce; + }); + + it('should be able to pause and resume receiving rects if not enought data', function () { + // seed some initial data to copy + client._fb_width = 4; + client._fb_height = 4; + client._display.resize(4, 4); + client._display.blitRgbxImage(0, 0, 4, 2, new Uint8Array(target_data_check_arr.slice(0, 32)), 0); + + const info = [{ x: 0, y: 2, width: 2, height: 2, encoding: 0x01}, + { x: 2, y: 2, width: 2, height: 2, encoding: 0x01}]; + // data says [{ old_x: 2, old_y: 0 }, { old_x: 0, old_y: 0 }] + const rects = [[0, 2, 0, 0], [0, 0, 0, 0]]; + send_fbu_msg([info[0]], [rects[0]], client, 2); + send_fbu_msg([info[1]], [rects[1]], client, -1); + expect(client._display).to.have.displayed(target_data_check); + }); + + describe('Message Encoding Handlers', function () { + beforeEach(function () { + // a really small frame + client._fb_width = 4; + client._fb_height = 4; + client._fb_depth = 24; + client._display.resize(4, 4); + }); + + it('should handle the RAW encoding', function () { + const info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 2, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 0, y: 2, width: 4, height: 1, encoding: 0x00 }, + { x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }]; + // data is in bgrx + const rects = [ + [0x00, 0x00, 0xff, 0, 0x00, 0xff, 0x00, 0, 0x00, 0xff, 0x00, 0, 0x00, 0x00, 0xff, 0], + [0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0], + [0xff, 0x00, 0xee, 0, 0xff, 0xee, 0x00, 0, 0xff, 0xee, 0xaa, 0, 0xff, 0xee, 0xab, 0], + [0xff, 0x00, 0xee, 0, 0xff, 0xee, 0x00, 0, 0xff, 0xee, 0xaa, 0, 0xff, 0xee, 0xab, 0]]; + send_fbu_msg(info, rects, client); + expect(client._display).to.have.displayed(target_data); + }); + + it('should handle the RAW encoding in low colour mode', function () { + const info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 2, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 0, y: 2, width: 4, height: 1, encoding: 0x00 }, + { x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }]; + const rects = [ + [0x03, 0x03, 0x03, 0x03], + [0x0c, 0x0c, 0x0c, 0x0c], + [0x0c, 0x0c, 0x03, 0x03], + [0x0c, 0x0c, 0x03, 0x03]]; + client._fb_depth = 8; + send_fbu_msg(info, rects, client); + expect(client._display).to.have.displayed(target_data_check); + }); + + it('should handle the COPYRECT encoding', function () { + // seed some initial data to copy + client._display.blitRgbxImage(0, 0, 4, 2, new Uint8Array(target_data_check_arr.slice(0, 32)), 0); + + const info = [{ x: 0, y: 2, width: 2, height: 2, encoding: 0x01}, + { x: 2, y: 2, width: 2, height: 2, encoding: 0x01}]; + // data says [{ old_x: 0, old_y: 0 }, { old_x: 0, old_y: 0 }] + const rects = [[0, 2, 0, 0], [0, 0, 0, 0]]; + send_fbu_msg(info, rects, client); + expect(client._display).to.have.displayed(target_data_check); + }); + + // TODO(directxman12): for encodings with subrects, test resuming on partial send? + // TODO(directxman12): test rre_chunk_sz (related to above about subrects)? + + it('should handle the RRE encoding', function () { + const info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x02 }]; + const rect = []; + push32(rect, 2); // 2 subrects + push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + rect.push(0xff); // becomes ff0000ff --> #0000FF color + rect.push(0x00); + rect.push(0x00); + rect.push(0xff); + push16(rect, 0); // x: 0 + push16(rect, 0); // y: 0 + push16(rect, 2); // width: 2 + push16(rect, 2); // height: 2 + rect.push(0xff); // becomes ff0000ff --> #0000FF color + rect.push(0x00); + rect.push(0x00); + rect.push(0xff); + push16(rect, 2); // x: 2 + push16(rect, 2); // y: 2 + push16(rect, 2); // width: 2 + push16(rect, 2); // height: 2 + + send_fbu_msg(info, [rect], client); + expect(client._display).to.have.displayed(target_data_check); + }); + + describe('the HEXTILE encoding handler', function () { + it('should handle a tile with fg, bg specified, normal subrects', function () { + const info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; + const rect = []; + rect.push(0x02 | 0x04 | 0x08); // bg spec, fg spec, anysubrects + push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + rect.push(0xff); // becomes ff0000ff --> #0000FF fg color + rect.push(0x00); + rect.push(0x00); + rect.push(0xff); + rect.push(2); // 2 subrects + rect.push(0); // x: 0, y: 0 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + rect.push(2 | (2 << 4)); // x: 2, y: 2 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + send_fbu_msg(info, [rect], client); + expect(client._display).to.have.displayed(target_data_check); + }); + + it('should handle a raw tile', function () { + const info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; + const rect = []; + rect.push(0x01); // raw + for (let i = 0; i < target_data.length; i += 4) { + rect.push(target_data[i + 2]); + rect.push(target_data[i + 1]); + rect.push(target_data[i]); + rect.push(target_data[i + 3]); + } + send_fbu_msg(info, [rect], client); + expect(client._display).to.have.displayed(target_data); + }); + + it('should handle a tile with only bg specified (solid bg)', function () { + const info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; + const rect = []; + rect.push(0x02); + push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + send_fbu_msg(info, [rect], client); + + const expected = []; + for (let i = 0; i < 16; i++) { push32(expected, 0xff00ff); } + expect(client._display).to.have.displayed(new Uint8Array(expected)); + }); + + it('should handle a tile with only bg specified and an empty frame afterwards', function () { + // set the width so we can have two tiles + client._fb_width = 8; + client._display.resize(8, 4); + + const info = [{ x: 0, y: 0, width: 32, height: 4, encoding: 0x05 }]; + + const rect = []; + + // send a bg frame + rect.push(0x02); + push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + + // send an empty frame + rect.push(0x00); + + send_fbu_msg(info, [rect], client); + + const expected = []; + for (let i = 0; i < 16; i++) { push32(expected, 0xff00ff); } // rect 1: solid + for (let i = 0; i < 16; i++) { push32(expected, 0xff00ff); } // rect 2: same bkground color + expect(client._display).to.have.displayed(new Uint8Array(expected)); + }); + + it('should handle a tile with bg and coloured subrects', function () { + const info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; + const rect = []; + rect.push(0x02 | 0x08 | 0x10); // bg spec, anysubrects, colouredsubrects + push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + rect.push(2); // 2 subrects + rect.push(0xff); // becomes ff0000ff --> #0000FF fg color + rect.push(0x00); + rect.push(0x00); + rect.push(0xff); + rect.push(0); // x: 0, y: 0 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + rect.push(0xff); // becomes ff0000ff --> #0000FF fg color + rect.push(0x00); + rect.push(0x00); + rect.push(0xff); + rect.push(2 | (2 << 4)); // x: 2, y: 2 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + send_fbu_msg(info, [rect], client); + expect(client._display).to.have.displayed(target_data_check); + }); + + it('should carry over fg and bg colors from the previous tile if not specified', function () { + client._fb_width = 4; + client._fb_height = 17; + client._display.resize(4, 17); + + const info = [{ x: 0, y: 0, width: 4, height: 17, encoding: 0x05}]; + const rect = []; + rect.push(0x02 | 0x04 | 0x08); // bg spec, fg spec, anysubrects + push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + rect.push(0xff); // becomes ff0000ff --> #0000FF fg color + rect.push(0x00); + rect.push(0x00); + rect.push(0xff); + rect.push(8); // 8 subrects + for (let i = 0; i < 4; i++) { + rect.push((0 << 4) | (i * 4)); // x: 0, y: i*4 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + rect.push((2 << 4) | (i * 4 + 2)); // x: 2, y: i * 4 + 2 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + } + rect.push(0x08); // anysubrects + rect.push(1); // 1 subrect + rect.push(0); // x: 0, y: 0 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + send_fbu_msg(info, [rect], client); + + let expected = []; + for (let i = 0; i < 4; i++) { expected = expected.concat(target_data_check_arr); } + expected = expected.concat(target_data_check_arr.slice(0, 16)); + expect(client._display).to.have.displayed(new Uint8Array(expected)); + }); + + it('should fail on an invalid subencoding', function () { + sinon.spy(client, "_fail"); + const info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; + const rects = [[45]]; // an invalid subencoding + send_fbu_msg(info, rects, client); + expect(client._fail).to.have.been.calledOnce; + }); + }); + + it.skip('should handle the TIGHT encoding', function () { + // TODO(directxman12): test this + }); + + it.skip('should handle the TIGHT_PNG encoding', function () { + // TODO(directxman12): test this + }); + + it('should handle the DesktopSize pseduo-encoding', function () { + sinon.spy(client._display, 'resize'); + send_fbu_msg([{ x: 0, y: 0, width: 20, height: 50, encoding: -223 }], [[]], client); + + expect(client._fb_width).to.equal(20); + expect(client._fb_height).to.equal(50); + + expect(client._display.resize).to.have.been.calledOnce; + expect(client._display.resize).to.have.been.calledWith(20, 50); + }); + + describe('the ExtendedDesktopSize pseudo-encoding handler', function () { + beforeEach(function () { + // a really small frame + client._fb_width = 4; + client._fb_height = 4; + client._display.resize(4, 4); + sinon.spy(client._display, 'resize'); + }); + + function make_screen_data(nr_of_screens) { + const data = []; + push8(data, nr_of_screens); // number-of-screens + push8(data, 0); // padding + push16(data, 0); // padding + for (let i=0; i {}}; + const incoming_msg = {_sQ: new Uint8Array(16), _sQlen: 0, flush: () => {}}; + + const payload = "foo\x00ab9"; + + // ClientFence and ServerFence are identical in structure + RFB.messages.clientFence(expected_msg, (1<<0) | (1<<1), payload); + RFB.messages.clientFence(incoming_msg, 0xffffffff, payload); + + client._sock._websocket._receive_data(incoming_msg._sQ); + + expect(client._sock).to.have.sent(expected_msg._sQ); + + expected_msg._sQlen = 0; + incoming_msg._sQlen = 0; + + RFB.messages.clientFence(expected_msg, (1<<0), payload); + RFB.messages.clientFence(incoming_msg, (1<<0) | (1<<31), payload); + + client._sock._websocket._receive_data(incoming_msg._sQ); + + expect(client._sock).to.have.sent(expected_msg._sQ); + }); + + it('should enable continuous updates on first EndOfContinousUpdates', function () { + const expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: () => {}}; + + RFB.messages.enableContinuousUpdates(expected_msg, true, 0, 0, 640, 20); + + expect(client._enabledContinuousUpdates).to.be.false; + + client._sock._websocket._receive_data(new Uint8Array([150])); + + expect(client._enabledContinuousUpdates).to.be.true; + expect(client._sock).to.have.sent(expected_msg._sQ); + }); + + it('should disable continuous updates on subsequent EndOfContinousUpdates', function () { + client._enabledContinuousUpdates = true; + client._supportsContinuousUpdates = true; + + client._sock._websocket._receive_data(new Uint8Array([150])); + + expect(client._enabledContinuousUpdates).to.be.false; + }); + + it('should update continuous updates on resize', function () { + const expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: () => {}}; + RFB.messages.enableContinuousUpdates(expected_msg, true, 0, 0, 90, 700); + + client._resize(450, 160); + + expect(client._sock._websocket._get_sent_data()).to.have.length(0); + + client._enabledContinuousUpdates = true; + + client._resize(90, 700); + + expect(client._sock).to.have.sent(expected_msg._sQ); + }); + + it('should fail on an unknown message type', function () { + sinon.spy(client, "_fail"); + client._sock._websocket._receive_data(new Uint8Array([87])); + expect(client._fail).to.have.been.calledOnce; + }); + }); + + describe('Asynchronous Events', function () { + let client; + beforeEach(function () { + client = make_rfb(); + }); + + describe('Mouse event handlers', function () { + it('should not send button messages in view-only mode', function () { + client._viewOnly = true; + sinon.spy(client._sock, 'flush'); + client._handleMouseButton(0, 0, 1, 0x001); + expect(client._sock.flush).to.not.have.been.called; + }); + + it('should not send movement messages in view-only mode', function () { + client._viewOnly = true; + sinon.spy(client._sock, 'flush'); + client._handleMouseMove(0, 0); + expect(client._sock.flush).to.not.have.been.called; + }); + + it('should send a pointer event on mouse button presses', function () { + client._handleMouseButton(10, 12, 1, 0x001); + const pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0, flush: () => {}}; + RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x001); + expect(client._sock).to.have.sent(pointer_msg._sQ); + }); + + it('should send a mask of 1 on mousedown', function () { + client._handleMouseButton(10, 12, 1, 0x001); + const pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0, flush: () => {}}; + RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x001); + expect(client._sock).to.have.sent(pointer_msg._sQ); + }); + + it('should send a mask of 0 on mouseup', function () { + client._mouse_buttonMask = 0x001; + client._handleMouseButton(10, 12, 0, 0x001); + const pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0, flush: () => {}}; + RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x000); + expect(client._sock).to.have.sent(pointer_msg._sQ); + }); + + it('should send a pointer event on mouse movement', function () { + client._handleMouseMove(10, 12); + const pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0, flush: () => {}}; + RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x000); + expect(client._sock).to.have.sent(pointer_msg._sQ); + }); + + it('should set the button mask so that future mouse movements use it', function () { + client._handleMouseButton(10, 12, 1, 0x010); + client._handleMouseMove(13, 9); + const pointer_msg = {_sQ: new Uint8Array(12), _sQlen: 0, flush: () => {}}; + RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x010); + RFB.messages.pointerEvent(pointer_msg, 13, 9, 0x010); + expect(client._sock).to.have.sent(pointer_msg._sQ); + }); + }); + + describe('Keyboard Event Handlers', function () { + it('should send a key message on a key press', function () { + client._handleKeyEvent(0x41, 'KeyA', true); + const key_msg = {_sQ: new Uint8Array(8), _sQlen: 0, flush: () => {}}; + RFB.messages.keyEvent(key_msg, 0x41, 1); + expect(client._sock).to.have.sent(key_msg._sQ); + }); + + it('should not send messages in view-only mode', function () { + client._viewOnly = true; + sinon.spy(client._sock, 'flush'); + client._handleKeyEvent('a', 'KeyA', true); + expect(client._sock.flush).to.not.have.been.called; + }); + }); + + describe('WebSocket event handlers', function () { + // message events + it('should do nothing if we receive an empty message and have nothing in the queue', function () { + client._normal_msg = sinon.spy(); + client._sock._websocket._receive_data(new Uint8Array([])); + expect(client._normal_msg).to.not.have.been.called; + }); + + it('should handle a message in the connected state as a normal message', function () { + client._normal_msg = sinon.spy(); + client._sock._websocket._receive_data(new Uint8Array([1, 2, 3])); + expect(client._normal_msg).to.have.been.called; + }); + + it('should handle a message in any non-disconnected/failed state like an init message', function () { + client._rfb_connection_state = 'connecting'; + client._rfb_init_state = 'ProtocolVersion'; + client._init_msg = sinon.spy(); + client._sock._websocket._receive_data(new Uint8Array([1, 2, 3])); + expect(client._init_msg).to.have.been.called; + }); + + it('should process all normal messages directly', function () { + const spy = sinon.spy(); + client.addEventListener("bell", spy); + client._sock._websocket._receive_data(new Uint8Array([0x02, 0x02])); + expect(spy).to.have.been.calledTwice; + }); + + // open events + it('should update the state to ProtocolVersion on open (if the state is "connecting")', function () { + client = new RFB(document.createElement('div'), 'wss://host:8675'); + this.clock.tick(); + client._sock._websocket._open(); + expect(client._rfb_init_state).to.equal('ProtocolVersion'); + }); + + it('should fail if we are not currently ready to connect and we get an "open" event', function () { + sinon.spy(client, "_fail"); + client._rfb_connection_state = 'connected'; + client._sock._websocket._open(); + expect(client._fail).to.have.been.calledOnce; + }); + + // close events + it('should transition to "disconnected" from "disconnecting" on a close event', function () { + const real = client._sock._websocket.close; + client._sock._websocket.close = () => {}; + client.disconnect(); + expect(client._rfb_connection_state).to.equal('disconnecting'); + client._sock._websocket.close = real; + client._sock._websocket.close(); + expect(client._rfb_connection_state).to.equal('disconnected'); + }); + + it('should fail if we get a close event while connecting', function () { + sinon.spy(client, "_fail"); + client._rfb_connection_state = 'connecting'; + client._sock._websocket.close(); + expect(client._fail).to.have.been.calledOnce; + }); + + it('should unregister close event handler', function () { + sinon.spy(client._sock, 'off'); + client.disconnect(); + client._sock._websocket.close(); + expect(client._sock.off).to.have.been.calledWith('close'); + }); + + // error events do nothing + }); + }); +}); diff --git a/systemvm/debian/root/noVNC/tests/test.util.js b/systemvm/debian/root/noVNC/tests/test.util.js new file mode 100644 index 000000000000..201acc8bb0d2 --- /dev/null +++ b/systemvm/debian/root/noVNC/tests/test.util.js @@ -0,0 +1,69 @@ +/* eslint-disable no-console */ +const expect = chai.expect; + +import * as Log from '../core/util/logging.js'; + +describe('Utils', function () { + "use strict"; + + describe('logging functions', function () { + beforeEach(function () { + sinon.spy(console, 'log'); + sinon.spy(console, 'debug'); + sinon.spy(console, 'warn'); + sinon.spy(console, 'error'); + sinon.spy(console, 'info'); + }); + + afterEach(function () { + console.log.restore(); + console.debug.restore(); + console.warn.restore(); + console.error.restore(); + console.info.restore(); + Log.init_logging(); + }); + + it('should use noop for levels lower than the min level', function () { + Log.init_logging('warn'); + Log.Debug('hi'); + Log.Info('hello'); + expect(console.log).to.not.have.been.called; + }); + + it('should use console.debug for Debug', function () { + Log.init_logging('debug'); + Log.Debug('dbg'); + expect(console.debug).to.have.been.calledWith('dbg'); + }); + + it('should use console.info for Info', function () { + Log.init_logging('debug'); + Log.Info('inf'); + expect(console.info).to.have.been.calledWith('inf'); + }); + + it('should use console.warn for Warn', function () { + Log.init_logging('warn'); + Log.Warn('wrn'); + expect(console.warn).to.have.been.called; + expect(console.warn).to.have.been.calledWith('wrn'); + }); + + it('should use console.error for Error', function () { + Log.init_logging('error'); + Log.Error('err'); + expect(console.error).to.have.been.called; + expect(console.error).to.have.been.calledWith('err'); + }); + }); + + // TODO(directxman12): test the conf_default and conf_defaults methods + // TODO(directxman12): test decodeUTF8 + // TODO(directxman12): test the event methods (addEvent, removeEvent, stopEvent) + // TODO(directxman12): figure out a good way to test getPosition and getEventPosition + // TODO(directxman12): figure out how to test the browser detection functions properly + // (we can't really test them against the browsers, except for Gecko + // via PhantomJS, the default test driver) +}); +/* eslint-enable no-console */ diff --git a/systemvm/debian/root/noVNC/tests/test.websock.js b/systemvm/debian/root/noVNC/tests/test.websock.js new file mode 100644 index 000000000000..30e19e9de713 --- /dev/null +++ b/systemvm/debian/root/noVNC/tests/test.websock.js @@ -0,0 +1,441 @@ +const expect = chai.expect; + +import Websock from '../core/websock.js'; +import FakeWebSocket from './fake.websocket.js'; + +describe('Websock', function () { + "use strict"; + + describe('Queue methods', function () { + let sock; + const RQ_TEMPLATE = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]); + + beforeEach(function () { + sock = new Websock(); + // skip init + sock._allocate_buffers(); + sock._rQ.set(RQ_TEMPLATE); + sock._rQlen = RQ_TEMPLATE.length; + }); + describe('rQlen', function () { + it('should return the length of the receive queue', function () { + sock.rQi = 0; + + expect(sock.rQlen).to.equal(RQ_TEMPLATE.length); + }); + + it("should return the proper length if we read some from the receive queue", function () { + sock.rQi = 1; + + expect(sock.rQlen).to.equal(RQ_TEMPLATE.length - 1); + }); + }); + + describe('rQpeek8', function () { + it('should peek at the next byte without poping it off the queue', function () { + const bef_len = sock.rQlen; + const peek = sock.rQpeek8(); + expect(sock.rQpeek8()).to.equal(peek); + expect(sock.rQlen).to.equal(bef_len); + }); + }); + + describe('rQshift8()', function () { + it('should pop a single byte from the receive queue', function () { + const peek = sock.rQpeek8(); + const bef_len = sock.rQlen; + expect(sock.rQshift8()).to.equal(peek); + expect(sock.rQlen).to.equal(bef_len - 1); + }); + }); + + describe('rQshift16()', function () { + it('should pop two bytes from the receive queue and return a single number', function () { + const bef_len = sock.rQlen; + const expected = (RQ_TEMPLATE[0] << 8) + RQ_TEMPLATE[1]; + expect(sock.rQshift16()).to.equal(expected); + expect(sock.rQlen).to.equal(bef_len - 2); + }); + }); + + describe('rQshift32()', function () { + it('should pop four bytes from the receive queue and return a single number', function () { + const bef_len = sock.rQlen; + const expected = (RQ_TEMPLATE[0] << 24) + + (RQ_TEMPLATE[1] << 16) + + (RQ_TEMPLATE[2] << 8) + + RQ_TEMPLATE[3]; + expect(sock.rQshift32()).to.equal(expected); + expect(sock.rQlen).to.equal(bef_len - 4); + }); + }); + + describe('rQshiftStr', function () { + it('should shift the given number of bytes off of the receive queue and return a string', function () { + const bef_len = sock.rQlen; + const bef_rQi = sock.rQi; + const shifted = sock.rQshiftStr(3); + expect(shifted).to.be.a('string'); + expect(shifted).to.equal(String.fromCharCode.apply(null, Array.prototype.slice.call(new Uint8Array(RQ_TEMPLATE.buffer, bef_rQi, 3)))); + expect(sock.rQlen).to.equal(bef_len - 3); + }); + + it('should shift the entire rest of the queue off if no length is given', function () { + sock.rQshiftStr(); + expect(sock.rQlen).to.equal(0); + }); + + it('should be able to handle very large strings', function () { + const BIG_LEN = 500000; + const RQ_BIG = new Uint8Array(BIG_LEN); + let expected = ""; + let letterCode = 'a'.charCodeAt(0); + for (let i = 0; i < BIG_LEN; i++) { + RQ_BIG[i] = letterCode; + expected += String.fromCharCode(letterCode); + + if (letterCode < 'z'.charCodeAt(0)) { + letterCode++; + } else { + letterCode = 'a'.charCodeAt(0); + } + } + sock._rQ.set(RQ_BIG); + sock._rQlen = RQ_BIG.length; + + const shifted = sock.rQshiftStr(); + + expect(shifted).to.be.equal(expected); + expect(sock.rQlen).to.equal(0); + }); + }); + + describe('rQshiftBytes', function () { + it('should shift the given number of bytes of the receive queue and return an array', function () { + const bef_len = sock.rQlen; + const bef_rQi = sock.rQi; + const shifted = sock.rQshiftBytes(3); + expect(shifted).to.be.an.instanceof(Uint8Array); + expect(shifted).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, bef_rQi, 3)); + expect(sock.rQlen).to.equal(bef_len - 3); + }); + + it('should shift the entire rest of the queue off if no length is given', function () { + sock.rQshiftBytes(); + expect(sock.rQlen).to.equal(0); + }); + }); + + describe('rQslice', function () { + beforeEach(function () { + sock.rQi = 0; + }); + + it('should not modify the receive queue', function () { + const bef_len = sock.rQlen; + sock.rQslice(0, 2); + expect(sock.rQlen).to.equal(bef_len); + }); + + it('should return an array containing the given slice of the receive queue', function () { + const sl = sock.rQslice(0, 2); + expect(sl).to.be.an.instanceof(Uint8Array); + expect(sl).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 0, 2)); + }); + + it('should use the rest of the receive queue if no end is given', function () { + const sl = sock.rQslice(1); + expect(sl).to.have.length(RQ_TEMPLATE.length - 1); + expect(sl).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 1)); + }); + + it('should take the current rQi in to account', function () { + sock.rQi = 1; + expect(sock.rQslice(0, 2)).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 1, 2)); + }); + }); + + describe('rQwait', function () { + beforeEach(function () { + sock.rQi = 0; + }); + + it('should return true if there are not enough bytes in the receive queue', function () { + expect(sock.rQwait('hi', RQ_TEMPLATE.length + 1)).to.be.true; + }); + + it('should return false if there are enough bytes in the receive queue', function () { + expect(sock.rQwait('hi', RQ_TEMPLATE.length)).to.be.false; + }); + + it('should return true and reduce rQi by "goback" if there are not enough bytes', function () { + sock.rQi = 5; + expect(sock.rQwait('hi', RQ_TEMPLATE.length, 4)).to.be.true; + expect(sock.rQi).to.equal(1); + }); + + it('should raise an error if we try to go back more than possible', function () { + sock.rQi = 5; + expect(() => sock.rQwait('hi', RQ_TEMPLATE.length, 6)).to.throw(Error); + }); + + it('should not reduce rQi if there are enough bytes', function () { + sock.rQi = 5; + sock.rQwait('hi', 1, 6); + expect(sock.rQi).to.equal(5); + }); + }); + + describe('flush', function () { + beforeEach(function () { + sock._websocket = { + send: sinon.spy() + }; + }); + + it('should actually send on the websocket', function () { + sock._websocket.bufferedAmount = 8; + sock._websocket.readyState = WebSocket.OPEN; + sock._sQ = new Uint8Array([1, 2, 3]); + sock._sQlen = 3; + const encoded = sock._encode_message(); + + sock.flush(); + expect(sock._websocket.send).to.have.been.calledOnce; + expect(sock._websocket.send).to.have.been.calledWith(encoded); + }); + + it('should not call send if we do not have anything queued up', function () { + sock._sQlen = 0; + sock._websocket.bufferedAmount = 8; + + sock.flush(); + + expect(sock._websocket.send).not.to.have.been.called; + }); + }); + + describe('send', function () { + beforeEach(function () { + sock.flush = sinon.spy(); + }); + + it('should add to the send queue', function () { + sock.send([1, 2, 3]); + const sq = sock.sQ; + expect(new Uint8Array(sq.buffer, sock._sQlen - 3, 3)).to.array.equal(new Uint8Array([1, 2, 3])); + }); + + it('should call flush', function () { + sock.send([1, 2, 3]); + expect(sock.flush).to.have.been.calledOnce; + }); + }); + + describe('send_string', function () { + beforeEach(function () { + sock.send = sinon.spy(); + }); + + it('should call send after converting the string to an array', function () { + sock.send_string("\x01\x02\x03"); + expect(sock.send).to.have.been.calledWith([1, 2, 3]); + }); + }); + }); + + describe('lifecycle methods', function () { + let old_WS; + before(function () { + old_WS = WebSocket; + }); + + let sock; + beforeEach(function () { + sock = new Websock(); + // eslint-disable-next-line no-global-assign + WebSocket = sinon.spy(); + WebSocket.OPEN = old_WS.OPEN; + WebSocket.CONNECTING = old_WS.CONNECTING; + WebSocket.CLOSING = old_WS.CLOSING; + WebSocket.CLOSED = old_WS.CLOSED; + + WebSocket.prototype.binaryType = 'arraybuffer'; + }); + + describe('opening', function () { + it('should pick the correct protocols if none are given', function () { + + }); + + it('should open the actual websocket', function () { + sock.open('ws://localhost:8675', 'binary'); + expect(WebSocket).to.have.been.calledWith('ws://localhost:8675', 'binary'); + }); + + // it('should initialize the event handlers')? + }); + + describe('closing', function () { + beforeEach(function () { + sock.open('ws://'); + sock._websocket.close = sinon.spy(); + }); + + it('should close the actual websocket if it is open', function () { + sock._websocket.readyState = WebSocket.OPEN; + sock.close(); + expect(sock._websocket.close).to.have.been.calledOnce; + }); + + it('should close the actual websocket if it is connecting', function () { + sock._websocket.readyState = WebSocket.CONNECTING; + sock.close(); + expect(sock._websocket.close).to.have.been.calledOnce; + }); + + it('should not try to close the actual websocket if closing', function () { + sock._websocket.readyState = WebSocket.CLOSING; + sock.close(); + expect(sock._websocket.close).not.to.have.been.called; + }); + + it('should not try to close the actual websocket if closed', function () { + sock._websocket.readyState = WebSocket.CLOSED; + sock.close(); + expect(sock._websocket.close).not.to.have.been.called; + }); + + it('should reset onmessage to not call _recv_message', function () { + sinon.spy(sock, '_recv_message'); + sock.close(); + sock._websocket.onmessage(null); + try { + expect(sock._recv_message).not.to.have.been.called; + } finally { + sock._recv_message.restore(); + } + }); + }); + + describe('event handlers', function () { + beforeEach(function () { + sock._recv_message = sinon.spy(); + sock.on('open', sinon.spy()); + sock.on('close', sinon.spy()); + sock.on('error', sinon.spy()); + sock.open('ws://'); + }); + + it('should call _recv_message on a message', function () { + sock._websocket.onmessage(null); + expect(sock._recv_message).to.have.been.calledOnce; + }); + + it('should call the open event handler on opening', function () { + sock._websocket.onopen(); + expect(sock._eventHandlers.open).to.have.been.calledOnce; + }); + + it('should call the close event handler on closing', function () { + sock._websocket.onclose(); + expect(sock._eventHandlers.close).to.have.been.calledOnce; + }); + + it('should call the error event handler on error', function () { + sock._websocket.onerror(); + expect(sock._eventHandlers.error).to.have.been.calledOnce; + }); + }); + + after(function () { + // eslint-disable-next-line no-global-assign + WebSocket = old_WS; + }); + }); + + describe('WebSocket Receiving', function () { + let sock; + beforeEach(function () { + sock = new Websock(); + sock._allocate_buffers(); + }); + + it('should support adding binary Uint8Array data to the receive queue', function () { + const msg = { data: new Uint8Array([1, 2, 3]) }; + sock._mode = 'binary'; + sock._recv_message(msg); + expect(sock.rQshiftStr(3)).to.equal('\x01\x02\x03'); + }); + + it('should call the message event handler if present', function () { + sock._eventHandlers.message = sinon.spy(); + const msg = { data: new Uint8Array([1, 2, 3]).buffer }; + sock._mode = 'binary'; + sock._recv_message(msg); + expect(sock._eventHandlers.message).to.have.been.calledOnce; + }); + + it('should not call the message event handler if there is nothing in the receive queue', function () { + sock._eventHandlers.message = sinon.spy(); + const msg = { data: new Uint8Array([]).buffer }; + sock._mode = 'binary'; + sock._recv_message(msg); + expect(sock._eventHandlers.message).not.to.have.been.called; + }); + + it('should compact the receive queue', function () { + // NB(sross): while this is an internal implementation detail, it's important to + // test, otherwise the receive queue could become very large very quickly + sock._rQ = new Uint8Array([0, 1, 2, 3, 4, 5, 0, 0, 0, 0]); + sock._rQlen = 6; + sock.rQi = 6; + sock._rQmax = 3; + const msg = { data: new Uint8Array([1, 2, 3]).buffer }; + sock._mode = 'binary'; + sock._recv_message(msg); + expect(sock._rQlen).to.equal(3); + expect(sock.rQi).to.equal(0); + }); + + it('should automatically resize the receive queue if the incoming message is too large', function () { + sock._rQ = new Uint8Array(20); + sock._rQlen = 0; + sock.rQi = 0; + sock._rQbufferSize = 20; + sock._rQmax = 2; + const msg = { data: new Uint8Array(30).buffer }; + sock._mode = 'binary'; + sock._recv_message(msg); + expect(sock._rQlen).to.equal(30); + expect(sock.rQi).to.equal(0); + expect(sock._rQ.length).to.equal(240); // keep the invariant that rQbufferSize / 8 >= rQlen + }); + }); + + describe('Data encoding', function () { + before(function () { FakeWebSocket.replace(); }); + after(function () { FakeWebSocket.restore(); }); + + describe('as binary data', function () { + let sock; + beforeEach(function () { + sock = new Websock(); + sock.open('ws://', 'binary'); + sock._websocket._open(); + }); + + it('should only send the send queue up to the send queue length', function () { + sock._sQ = new Uint8Array([1, 2, 3, 4, 5]); + sock._sQlen = 3; + const res = sock._encode_message(); + expect(res).to.array.equal(new Uint8Array([1, 2, 3])); + }); + + it('should properly pass the encoded data off to the actual WebSocket', function () { + sock.send([1, 2, 3]); + expect(sock._websocket._get_sent_data()).to.array.equal(new Uint8Array([1, 2, 3])); + }); + }); + }); +}); diff --git a/systemvm/debian/root/noVNC/tests/test.webutil.js b/systemvm/debian/root/noVNC/tests/test.webutil.js new file mode 100644 index 000000000000..72e194210d57 --- /dev/null +++ b/systemvm/debian/root/noVNC/tests/test.webutil.js @@ -0,0 +1,184 @@ +/* jshint expr: true */ + +const expect = chai.expect; + +import * as WebUtil from '../app/webutil.js'; + +describe('WebUtil', function () { + "use strict"; + + describe('settings', function () { + + describe('localStorage', function () { + let chrome = window.chrome; + before(function () { + chrome = window.chrome; + window.chrome = null; + }); + after(function () { + window.chrome = chrome; + }); + + let origLocalStorage; + beforeEach(function () { + origLocalStorage = Object.getOwnPropertyDescriptor(window, "localStorage"); + if (origLocalStorage === undefined) { + // Object.getOwnPropertyDescriptor() doesn't work + // properly in any version of IE + this.skip(); + } + + Object.defineProperty(window, "localStorage", {value: {}}); + if (window.localStorage.setItem !== undefined) { + // Object.defineProperty() doesn't work properly in old + // versions of Chrome + this.skip(); + } + + window.localStorage.setItem = sinon.stub(); + window.localStorage.getItem = sinon.stub(); + window.localStorage.removeItem = sinon.stub(); + + return WebUtil.initSettings(); + }); + afterEach(function () { + Object.defineProperty(window, "localStorage", origLocalStorage); + }); + + describe('writeSetting', function () { + it('should save the setting value to local storage', function () { + WebUtil.writeSetting('test', 'value'); + expect(window.localStorage.setItem).to.have.been.calledWithExactly('test', 'value'); + expect(WebUtil.readSetting('test')).to.equal('value'); + }); + }); + + describe('setSetting', function () { + it('should update the setting but not save to local storage', function () { + WebUtil.setSetting('test', 'value'); + expect(window.localStorage.setItem).to.not.have.been.called; + expect(WebUtil.readSetting('test')).to.equal('value'); + }); + }); + + describe('readSetting', function () { + it('should read the setting value from local storage', function () { + localStorage.getItem.returns('value'); + expect(WebUtil.readSetting('test')).to.equal('value'); + }); + + it('should return the default value when not in local storage', function () { + expect(WebUtil.readSetting('test', 'default')).to.equal('default'); + }); + + it('should return the cached value even if local storage changed', function () { + localStorage.getItem.returns('value'); + expect(WebUtil.readSetting('test')).to.equal('value'); + localStorage.getItem.returns('something else'); + expect(WebUtil.readSetting('test')).to.equal('value'); + }); + + it('should cache the value even if it is not initially in local storage', function () { + expect(WebUtil.readSetting('test')).to.be.null; + localStorage.getItem.returns('value'); + expect(WebUtil.readSetting('test')).to.be.null; + }); + + it('should return the default value always if the first read was not in local storage', function () { + expect(WebUtil.readSetting('test', 'default')).to.equal('default'); + localStorage.getItem.returns('value'); + expect(WebUtil.readSetting('test', 'another default')).to.equal('another default'); + }); + + it('should return the last local written value', function () { + localStorage.getItem.returns('value'); + expect(WebUtil.readSetting('test')).to.equal('value'); + WebUtil.writeSetting('test', 'something else'); + expect(WebUtil.readSetting('test')).to.equal('something else'); + }); + }); + + // this doesn't appear to be used anywhere + describe('eraseSetting', function () { + it('should remove the setting from local storage', function () { + WebUtil.eraseSetting('test'); + expect(window.localStorage.removeItem).to.have.been.calledWithExactly('test'); + }); + }); + }); + + describe('chrome.storage', function () { + let chrome = window.chrome; + let settings = {}; + before(function () { + chrome = window.chrome; + window.chrome = { + storage: { + sync: { + get(cb) { cb(settings); }, + set() {}, + remove() {} + } + } + }; + }); + after(function () { + window.chrome = chrome; + }); + + const csSandbox = sinon.createSandbox(); + + beforeEach(function () { + settings = {}; + csSandbox.spy(window.chrome.storage.sync, 'set'); + csSandbox.spy(window.chrome.storage.sync, 'remove'); + return WebUtil.initSettings(); + }); + afterEach(function () { + csSandbox.restore(); + }); + + describe('writeSetting', function () { + it('should save the setting value to chrome storage', function () { + WebUtil.writeSetting('test', 'value'); + expect(window.chrome.storage.sync.set).to.have.been.calledWithExactly(sinon.match({ test: 'value' })); + expect(WebUtil.readSetting('test')).to.equal('value'); + }); + }); + + describe('setSetting', function () { + it('should update the setting but not save to chrome storage', function () { + WebUtil.setSetting('test', 'value'); + expect(window.chrome.storage.sync.set).to.not.have.been.called; + expect(WebUtil.readSetting('test')).to.equal('value'); + }); + }); + + describe('readSetting', function () { + it('should read the setting value from chrome storage', function () { + settings.test = 'value'; + expect(WebUtil.readSetting('test')).to.equal('value'); + }); + + it('should return the default value when not in chrome storage', function () { + expect(WebUtil.readSetting('test', 'default')).to.equal('default'); + }); + + it('should return the last local written value', function () { + settings.test = 'value'; + expect(WebUtil.readSetting('test')).to.equal('value'); + WebUtil.writeSetting('test', 'something else'); + expect(WebUtil.readSetting('test')).to.equal('something else'); + }); + }); + + // this doesn't appear to be used anywhere + describe('eraseSetting', function () { + it('should remove the setting from chrome storage', function () { + WebUtil.eraseSetting('test'); + expect(window.chrome.storage.sync.remove).to.have.been.calledWithExactly('test'); + }); + }); + }); + }); +}); diff --git a/systemvm/debian/root/noVNC/tests/vnc_playback.html b/systemvm/debian/root/noVNC/tests/vnc_playback.html new file mode 100644 index 000000000000..4fd746580530 --- /dev/null +++ b/systemvm/debian/root/noVNC/tests/vnc_playback.html @@ -0,0 +1,43 @@ + + + + VNC Playback + + + + + + + + + + + Iterations:   + Perftest:  + Realtime:   + +   + +

+ + Results:
+ + +

+ +
+
Loading
+
+ + + + diff --git a/systemvm/debian/root/noVNC/utils/.eslintrc b/systemvm/debian/root/noVNC/utils/.eslintrc new file mode 100644 index 000000000000..b7dc129f139e --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/.eslintrc @@ -0,0 +1,8 @@ +{ + "env": { + "node": true + }, + "rules": { + "no-console": 0 + } +} \ No newline at end of file diff --git a/systemvm/debian/root/noVNC/utils/README.md b/systemvm/debian/root/noVNC/utils/README.md new file mode 100644 index 000000000000..32582e65ea8c --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/README.md @@ -0,0 +1,14 @@ +## WebSockets Proxy/Bridge + +Websockify has been forked out into its own project. `launch.sh` wil +automatically download it here if it is not already present and not +installed as system-wide. + +For more detailed description and usage information please refer to +the [websockify README](https://github.com/novnc/websockify/blob/master/README.md). + +The other versions of websockify (C, Node.js) and the associated test +programs have been moved to +[websockify](https://github.com/novnc/websockify). Websockify was +formerly named wsproxy. + diff --git a/systemvm/debian/root/noVNC/utils/b64-to-binary.pl b/systemvm/debian/root/noVNC/utils/b64-to-binary.pl new file mode 100755 index 000000000000..280e28c93f03 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/b64-to-binary.pl @@ -0,0 +1,17 @@ +#!/usr/bin/env perl +use MIME::Base64; + +for (<>) { + unless (/^'([{}])(\d+)\1(.+?)',$/) { + print; + next; + } + + my ($dir, $amt, $b64) = ($1, $2, $3); + + my $decoded = MIME::Base64::decode($b64) or die "Could not base64-decode line `$_`"; + + my $decoded_escaped = join "", map { "\\x$_" } unpack("(H2)*", $decoded); + + print "'${dir}${amt}${dir}${decoded_escaped}',\n"; +} diff --git a/systemvm/debian/root/noVNC/utils/genkeysymdef.js b/systemvm/debian/root/noVNC/utils/genkeysymdef.js new file mode 100755 index 000000000000..d21773f9f65f --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/genkeysymdef.js @@ -0,0 +1,127 @@ +#!/usr/bin/env node +/* + * genkeysymdef: X11 keysymdef.h to JavaScript converter + * Copyright (C) 2018 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + */ + +"use strict"; + +const fs = require('fs'); + +let show_help = process.argv.length === 2; +let filename; + +for (let i = 2; i < process.argv.length; ++i) { + switch (process.argv[i]) { + case "--help": + case "-h": + show_help = true; + break; + case "--file": + case "-f": + default: + filename = process.argv[i]; + } +} + +if (!filename) { + show_help = true; + console.log("Error: No filename specified\n"); +} + +if (show_help) { + console.log("Parses a *nix keysymdef.h to generate Unicode code point mappings"); + console.log("Usage: node parse.js [options] filename:"); + console.log(" -h [ --help ] Produce this help message"); + console.log(" filename The keysymdef.h file to parse"); + process.exit(0); +} + +const buf = fs.readFileSync(filename); +const str = buf.toString('utf8'); + +const re = /^#define XK_([a-zA-Z_0-9]+)\s+0x([0-9a-fA-F]+)\s*(\/\*\s*(.*)\s*\*\/)?\s*$/m; + +const arr = str.split('\n'); + +const codepoints = {}; + +for (let i = 0; i < arr.length; ++i) { + const result = re.exec(arr[i]); + if (result) { + const keyname = result[1]; + const keysym = parseInt(result[2], 16); + const remainder = result[3]; + + const unicodeRes = /U\+([0-9a-fA-F]+)/.exec(remainder); + if (unicodeRes) { + const unicode = parseInt(unicodeRes[1], 16); + // The first entry is the preferred one + if (!codepoints[unicode]) { + codepoints[unicode] = { keysym: keysym, name: keyname }; + } + } + } +} + +let out = +"/*\n" + +" * Mapping from Unicode codepoints to X11/RFB keysyms\n" + +" *\n" + +" * This file was automatically generated from keysymdef.h\n" + +" * DO NOT EDIT!\n" + +" */\n" + +"\n" + +"/* Functions at the bottom */\n" + +"\n" + +"const codepoints = {\n"; + +function toHex(num) { + let s = num.toString(16); + if (s.length < 4) { + s = ("0000" + s).slice(-4); + } + return "0x" + s; +} + +for (let codepoint in codepoints) { + codepoint = parseInt(codepoint); + + // Latin-1? + if ((codepoint >= 0x20) && (codepoint <= 0xff)) { + continue; + } + + // Handled by the general Unicode mapping? + if ((codepoint | 0x01000000) === codepoints[codepoint].keysym) { + continue; + } + + out += " " + toHex(codepoint) + ": " + + toHex(codepoints[codepoint].keysym) + + ", // XK_" + codepoints[codepoint].name + "\n"; +} + +out += +"};\n" + +"\n" + +"export default {\n" + +" lookup(u) {\n" + +" // Latin-1 is one-to-one mapping\n" + +" if ((u >= 0x20) && (u <= 0xff)) {\n" + +" return u;\n" + +" }\n" + +"\n" + +" // Lookup table (fairly random)\n" + +" const keysym = codepoints[u];\n" + +" if (keysym !== undefined) {\n" + +" return keysym;\n" + +" }\n" + +"\n" + +" // General mapping as final fallback\n" + +" return 0x01000000 | u;\n" + +" },\n" + +"};"; + +console.log(out); diff --git a/systemvm/debian/root/noVNC/utils/img2js.py b/systemvm/debian/root/noVNC/utils/img2js.py new file mode 100755 index 000000000000..ceab6bf75431 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/img2js.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python + +# +# Convert image to Javascript compatible base64 Data URI +# Copyright (C) 2018 The noVNC Authors +# Licensed under MPL 2.0 (see docs/LICENSE.MPL-2.0) +# + +import sys, base64 + +try: + from PIL import Image +except: + print "python PIL module required (python-imaging package)" + sys.exit(1) + + +if len(sys.argv) < 3: + print "Usage: %s IMAGE JS_VARIABLE" % sys.argv[0] + sys.exit(1) + +fname = sys.argv[1] +var = sys.argv[2] + +ext = fname.lower().split('.')[-1] +if ext == "png": mime = "image/png" +elif ext in ["jpg", "jpeg"]: mime = "image/jpeg" +elif ext == "gif": mime = "image/gif" +else: + print "Only PNG, JPEG and GIF images are supported" + sys.exit(1) +uri = "data:%s;base64," % mime + +im = Image.open(fname) +w, h = im.size + +raw = open(fname).read() + +print '%s = {"width": %s, "height": %s, "data": "%s%s"};' % ( + var, w, h, uri, base64.b64encode(raw)) diff --git a/systemvm/debian/root/noVNC/utils/json2graph.py b/systemvm/debian/root/noVNC/utils/json2graph.py new file mode 100755 index 000000000000..bdaeeccaf212 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/json2graph.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python + +''' +Use matplotlib to generate performance charts +Copyright (C) 2018 The noVNC Authors +Licensed under MPL-2.0 (see docs/LICENSE.MPL-2.0) +''' + +# a bar plot with errorbars +import sys, json +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.font_manager import FontProperties + +def usage(): + print "%s json_file level1 level2 level3 [legend_height]\n\n" % sys.argv[0] + print "Description:\n" + print "level1, level2, and level3 are one each of the following:\n"; + print " select=ITEM - select only ITEM at this level"; + print " bar - each item on this level becomes a graph bar"; + print " group - items on this level become groups of bars"; + print "\n"; + print "json_file is a file containing json data in the following format:\n" + print ' {'; + print ' "conf": {'; + print ' "order_l1": ['; + print ' "level1_label1",'; + print ' "level1_label2",'; + print ' ...'; + print ' ],'; + print ' "order_l2": ['; + print ' "level2_label1",'; + print ' "level2_label2",'; + print ' ...'; + print ' ],'; + print ' "order_l3": ['; + print ' "level3_label1",'; + print ' "level3_label2",'; + print ' ...'; + print ' ]'; + print ' },'; + print ' "stats": {'; + print ' "level1_label1": {'; + print ' "level2_label1": {'; + print ' "level3_label1": [val1, val2, val3],'; + print ' "level3_label2": [val1, val2, val3],'; + print ' ...'; + print ' },'; + print ' "level2_label2": {'; + print ' ...'; + print ' },'; + print ' },'; + print ' "level1_label2": {'; + print ' ...'; + print ' },'; + print ' ...'; + print ' },'; + print ' }'; + sys.exit(2) + +def error(msg): + print msg + sys.exit(1) + + +#colors = ['#ff0000', '#0863e9', '#00f200', '#ffa100', +# '#800000', '#805100', '#013075', '#007900'] +colors = ['#ff0000', '#00ff00', '#0000ff', + '#dddd00', '#dd00dd', '#00dddd', + '#dd6622', '#dd2266', '#66dd22', + '#8844dd', '#44dd88', '#4488dd'] + +if len(sys.argv) < 5: + usage() + +filename = sys.argv[1] +L1 = sys.argv[2] +L2 = sys.argv[3] +L3 = sys.argv[4] +if len(sys.argv) > 5: + legendHeight = float(sys.argv[5]) +else: + legendHeight = 0.75 + +# Load the JSON data from the file +data = json.loads(file(filename).read()) +conf = data['conf'] +stats = data['stats'] + +# Sanity check data hierarchy +if len(conf['order_l1']) != len(stats.keys()): + error("conf.order_l1 does not match stats level 1") +for l1 in stats.keys(): + if len(conf['order_l2']) != len(stats[l1].keys()): + error("conf.order_l2 does not match stats level 2 for %s" % l1) + if conf['order_l1'].count(l1) < 1: + error("%s not found in conf.order_l1" % l1) + for l2 in stats[l1].keys(): + if len(conf['order_l3']) != len(stats[l1][l2].keys()): + error("conf.order_l3 does not match stats level 3") + if conf['order_l2'].count(l2) < 1: + error("%s not found in conf.order_l2" % l2) + for l3 in stats[l1][l2].keys(): + if conf['order_l3'].count(l3) < 1: + error("%s not found in conf.order_l3" % l3) + +# +# Generate the data based on the level specifications +# +bar_labels = None +group_labels = None +bar_vals = [] +bar_sdvs = [] +if L3.startswith("select="): + select_label = l3 = L3.split("=")[1] + bar_labels = conf['order_l1'] + group_labels = conf['order_l2'] + bar_vals = [[0]*len(group_labels) for i in bar_labels] + bar_sdvs = [[0]*len(group_labels) for i in bar_labels] + for b in range(len(bar_labels)): + l1 = bar_labels[b] + for g in range(len(group_labels)): + l2 = group_labels[g] + bar_vals[b][g] = np.mean(stats[l1][l2][l3]) + bar_sdvs[b][g] = np.std(stats[l1][l2][l3]) +elif L2.startswith("select="): + select_label = l2 = L2.split("=")[1] + bar_labels = conf['order_l1'] + group_labels = conf['order_l3'] + bar_vals = [[0]*len(group_labels) for i in bar_labels] + bar_sdvs = [[0]*len(group_labels) for i in bar_labels] + for b in range(len(bar_labels)): + l1 = bar_labels[b] + for g in range(len(group_labels)): + l3 = group_labels[g] + bar_vals[b][g] = np.mean(stats[l1][l2][l3]) + bar_sdvs[b][g] = np.std(stats[l1][l2][l3]) +elif L1.startswith("select="): + select_label = l1 = L1.split("=")[1] + bar_labels = conf['order_l2'] + group_labels = conf['order_l3'] + bar_vals = [[0]*len(group_labels) for i in bar_labels] + bar_sdvs = [[0]*len(group_labels) for i in bar_labels] + for b in range(len(bar_labels)): + l2 = bar_labels[b] + for g in range(len(group_labels)): + l3 = group_labels[g] + bar_vals[b][g] = np.mean(stats[l1][l2][l3]) + bar_sdvs[b][g] = np.std(stats[l1][l2][l3]) +else: + usage() + +# If group is before bar then flip (zip) the data +if [L1, L2, L3].index("group") < [L1, L2, L3].index("bar"): + bar_labels, group_labels = group_labels, bar_labels + bar_vals = zip(*bar_vals) + bar_sdvs = zip(*bar_sdvs) + +print "bar_vals:", bar_vals + +# +# Now render the bar graph +# +ind = np.arange(len(group_labels)) # the x locations for the groups +width = 0.8 * (1.0/len(bar_labels)) # the width of the bars + +fig = plt.figure(figsize=(10,6), dpi=80) +plot = fig.add_subplot(1, 1, 1) + +rects = [] +for i in range(len(bar_vals)): + rects.append(plot.bar(ind+width*i, bar_vals[i], width, color=colors[i], + yerr=bar_sdvs[i], align='center')) + +# add some +plot.set_ylabel('Milliseconds (less is better)') +plot.set_title("Javascript array test: %s" % select_label) +plot.set_xticks(ind+width) +plot.set_xticklabels( group_labels ) + +fontP = FontProperties() +fontP.set_size('small') +plot.legend( [r[0] for r in rects], bar_labels, prop=fontP, + loc = 'center right', bbox_to_anchor = (1.0, legendHeight)) + +def autolabel(rects): + # attach some text labels + for rect in rects: + height = rect.get_height() + if np.isnan(height): + height = 0.0 + plot.text(rect.get_x()+rect.get_width()/2., height+20, '%d'%int(height), + ha='center', va='bottom', size='7') + +for rect in rects: + autolabel(rect) + +# Adjust axis sizes +axis = list(plot.axis()) +axis[0] = -width # Make sure left side has enough for bar +#axis[1] = axis[1] * 1.20 # Add 20% to the right to make sure it fits +axis[2] = 0 # Make y-axis start at 0 +axis[3] = axis[3] * 1.10 # Add 10% to the top +plot.axis(axis) + +plt.show() diff --git a/systemvm/debian/root/noVNC/utils/launch.sh b/systemvm/debian/root/noVNC/utils/launch.sh new file mode 100755 index 000000000000..162607eb05c6 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/launch.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash + +# Copyright (C) 2018 The noVNC Authors +# Licensed under MPL 2.0 or any later version (see LICENSE.txt) + +usage() { + if [ "$*" ]; then + echo "$*" + echo + fi + echo "Usage: ${NAME} [--listen PORT] [--vnc VNC_HOST:PORT] [--cert CERT] [--ssl-only]" + echo + echo "Starts the WebSockets proxy and a mini-webserver and " + echo "provides a cut-and-paste URL to go to." + echo + echo " --listen PORT Port for proxy/webserver to listen on" + echo " Default: 6080" + echo " --vnc VNC_HOST:PORT VNC server host:port proxy target" + echo " Default: localhost:5900" + echo " --cert CERT Path to combined cert/key file" + echo " Default: self.pem" + echo " --web WEB Path to web files (e.g. vnc.html)" + echo " Default: ./" + echo " --ssl-only Disable non-https connections." + echo " " + echo " --record FILE Record traffic to FILE.session.js" + echo " " + exit 2 +} + +NAME="$(basename $0)" +REAL_NAME="$(readlink -f $0)" +HERE="$(cd "$(dirname "$REAL_NAME")" && pwd)" +PORT="6080" +VNC_DEST="localhost:5900" +CERT="" +WEB="" +proxy_pid="" +SSLONLY="" +RECORD_ARG="" + +die() { + echo "$*" + exit 1 +} + +cleanup() { + trap - TERM QUIT INT EXIT + trap "true" CHLD # Ignore cleanup messages + echo + if [ -n "${proxy_pid}" ]; then + echo "Terminating WebSockets proxy (${proxy_pid})" + kill ${proxy_pid} + fi +} + +# Process Arguments + +# Arguments that only apply to chrooter itself +while [ "$*" ]; do + param=$1; shift; OPTARG=$1 + case $param in + --listen) PORT="${OPTARG}"; shift ;; + --vnc) VNC_DEST="${OPTARG}"; shift ;; + --cert) CERT="${OPTARG}"; shift ;; + --web) WEB="${OPTARG}"; shift ;; + --ssl-only) SSLONLY="--ssl-only" ;; + --record) RECORD_ARG="--record ${OPTARG}"; shift ;; + -h|--help) usage ;; + -*) usage "Unknown chrooter option: ${param}" ;; + *) break ;; + esac +done + +# Sanity checks +if bash -c "exec 7<>/dev/tcp/localhost/${PORT}" &> /dev/null; then + exec 7<&- + exec 7>&- + die "Port ${PORT} in use. Try --listen PORT" +else + exec 7<&- + exec 7>&- +fi + +trap "cleanup" TERM QUIT INT EXIT + +# Find vnc.html +if [ -n "${WEB}" ]; then + if [ ! -e "${WEB}/vnc.html" ]; then + die "Could not find ${WEB}/vnc.html" + fi +elif [ -e "$(pwd)/vnc.html" ]; then + WEB=$(pwd) +elif [ -e "${HERE}/../vnc.html" ]; then + WEB=${HERE}/../ +elif [ -e "${HERE}/vnc.html" ]; then + WEB=${HERE} +elif [ -e "${HERE}/../share/novnc/vnc.html" ]; then + WEB=${HERE}/../share/novnc/ +else + die "Could not find vnc.html" +fi + +# Find self.pem +if [ -n "${CERT}" ]; then + if [ ! -e "${CERT}" ]; then + die "Could not find ${CERT}" + fi +elif [ -e "$(pwd)/self.pem" ]; then + CERT="$(pwd)/self.pem" +elif [ -e "${HERE}/../self.pem" ]; then + CERT="${HERE}/../self.pem" +elif [ -e "${HERE}/self.pem" ]; then + CERT="${HERE}/self.pem" +else + echo "Warning: could not find self.pem" +fi + +# try to find websockify (prefer local, try global, then download local) +if [[ -e ${HERE}/websockify ]]; then + WEBSOCKIFY=${HERE}/websockify/run + + if [[ ! -x $WEBSOCKIFY ]]; then + echo "The path ${HERE}/websockify exists, but $WEBSOCKIFY either does not exist or is not executable." + echo "If you intended to use an installed websockify package, please remove ${HERE}/websockify." + exit 1 + fi + + echo "Using local websockify at $WEBSOCKIFY" +else + WEBSOCKIFY=$(which websockify 2>/dev/null) + + if [[ $? -ne 0 ]]; then + echo "No installed websockify, attempting to clone websockify..." + WEBSOCKIFY=${HERE}/websockify/run + git clone https://github.com/novnc/websockify ${HERE}/websockify + + if [[ ! -e $WEBSOCKIFY ]]; then + echo "Unable to locate ${HERE}/websockify/run after downloading" + exit 1 + fi + + echo "Using local websockify at $WEBSOCKIFY" + else + echo "Using installed websockify at $WEBSOCKIFY" + fi +fi + +echo "Starting webserver and WebSockets proxy on port ${PORT}" +#${HERE}/websockify --web ${WEB} ${CERT:+--cert ${CERT}} ${PORT} ${VNC_DEST} & +${WEBSOCKIFY} ${SSLONLY} --web ${WEB} ${CERT:+--cert ${CERT}} ${PORT} ${VNC_DEST} ${RECORD_ARG} & +proxy_pid="$!" +sleep 1 +if ! ps -p ${proxy_pid} >/dev/null; then + proxy_pid= + echo "Failed to start WebSockets proxy" + exit 1 +fi + +echo -e "\n\nNavigate to this URL:\n" +if [ "x$SSLONLY" == "x" ]; then + echo -e " http://$(hostname):${PORT}/vnc.html?host=$(hostname)&port=${PORT}\n" +else + echo -e " https://$(hostname):${PORT}/vnc.html?host=$(hostname)&port=${PORT}\n" +fi + +echo -e "Press Ctrl-C to exit\n\n" + +wait ${proxy_pid} diff --git a/systemvm/debian/root/noVNC/utils/pyDes.py b/systemvm/debian/root/noVNC/utils/pyDes.py new file mode 100644 index 000000000000..3daab23ebfa3 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/pyDes.py @@ -0,0 +1,852 @@ +############################################################################# +# Documentation # +############################################################################# + +# Author: Todd Whiteman +# Date: 28th April, 2010 +# Version: 2.0.1 +# License: MIT +# Homepage: https://github.com/twhiteman/pyDes +# +# This is a pure python implementation of the DES encryption algorithm. +# It's pure python to avoid portability issues, since most DES +# implementations are programmed in C (for performance reasons). +# +# Triple DES class is also implemented, utilizing the DES base. Triple DES +# is either DES-EDE3 with a 24 byte key, or DES-EDE2 with a 16 byte key. +# +# See the README.txt that should come with this python module for the +# implementation methods used. +# +# Thanks to: +# * David Broadwell for ideas, comments and suggestions. +# * Mario Wolff for pointing out and debugging some triple des CBC errors. +# * Santiago Palladino for providing the PKCS5 padding technique. +# * Shaya for correcting the PAD_PKCS5 triple des CBC errors. +# +"""A pure python implementation of the DES and TRIPLE DES encryption algorithms. + +Class initialization +-------------------- +pyDes.des(key, [mode], [IV], [pad], [padmode]) +pyDes.triple_des(key, [mode], [IV], [pad], [padmode]) + +key -> Bytes containing the encryption key. 8 bytes for DES, 16 or 24 bytes + for Triple DES +mode -> Optional argument for encryption type, can be either + pyDes.ECB (Electronic Code Book) or pyDes.CBC (Cypher Block Chaining) +IV -> Optional Initial Value bytes, must be supplied if using CBC mode. + Length must be 8 bytes. +pad -> Optional argument, set the pad character (PAD_NORMAL) to use during + all encrypt/decrypt operations done with this instance. +padmode -> Optional argument, set the padding mode (PAD_NORMAL or PAD_PKCS5) + to use during all encrypt/decrypt operations done with this instance. + +I recommend to use PAD_PKCS5 padding, as then you never need to worry about any +padding issues, as the padding can be removed unambiguously upon decrypting +data that was encrypted using PAD_PKCS5 padmode. + +Common methods +-------------- +encrypt(data, [pad], [padmode]) +decrypt(data, [pad], [padmode]) + +data -> Bytes to be encrypted/decrypted +pad -> Optional argument. Only when using padmode of PAD_NORMAL. For + encryption, adds this characters to the end of the data block when + data is not a multiple of 8 bytes. For decryption, will remove the + trailing characters that match this pad character from the last 8 + bytes of the unencrypted data block. +padmode -> Optional argument, set the padding mode, must be one of PAD_NORMAL + or PAD_PKCS5). Defaults to PAD_NORMAL. + + +Example +------- +from pyDes import * + +data = "Please encrypt my data" +k = des("DESCRYPT", CBC, "\0\0\0\0\0\0\0\0", pad=None, padmode=PAD_PKCS5) +# For Python3, you'll need to use bytes, i.e.: +# data = b"Please encrypt my data" +# k = des(b"DESCRYPT", CBC, b"\0\0\0\0\0\0\0\0", pad=None, padmode=PAD_PKCS5) +d = k.encrypt(data) +print "Encrypted: %r" % d +print "Decrypted: %r" % k.decrypt(d) +assert k.decrypt(d, padmode=PAD_PKCS5) == data + + +See the module source (pyDes.py) for more examples of use. +You can also run the pyDes.py file without and arguments to see a simple test. + +Note: This code was not written for high-end systems needing a fast + implementation, but rather a handy portable solution with small usage. + +""" + +import sys + +# _pythonMajorVersion is used to handle Python2 and Python3 differences. +_pythonMajorVersion = sys.version_info[0] + +# Modes of crypting / cyphering +ECB = 0 +CBC = 1 + +# Modes of padding +PAD_NORMAL = 1 +PAD_PKCS5 = 2 + +# PAD_PKCS5: is a method that will unambiguously remove all padding +# characters after decryption, when originally encrypted with +# this padding mode. +# For a good description of the PKCS5 padding technique, see: +# http://www.faqs.org/rfcs/rfc1423.html + +# The base class shared by des and triple des. +class _baseDes(object): + def __init__(self, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): + if IV: + IV = self._guardAgainstUnicode(IV) + if pad: + pad = self._guardAgainstUnicode(pad) + self.block_size = 8 + # Sanity checking of arguments. + if pad and padmode == PAD_PKCS5: + raise ValueError("Cannot use a pad character with PAD_PKCS5") + if IV and len(IV) != self.block_size: + raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes") + + # Set the passed in variables + self._mode = mode + self._iv = IV + self._padding = pad + self._padmode = padmode + + def getKey(self): + """getKey() -> bytes""" + return self.__key + + def setKey(self, key): + """Will set the crypting key for this object.""" + key = self._guardAgainstUnicode(key) + self.__key = key + + def getMode(self): + """getMode() -> pyDes.ECB or pyDes.CBC""" + return self._mode + + def setMode(self, mode): + """Sets the type of crypting mode, pyDes.ECB or pyDes.CBC""" + self._mode = mode + + def getPadding(self): + """getPadding() -> bytes of length 1. Padding character.""" + return self._padding + + def setPadding(self, pad): + """setPadding() -> bytes of length 1. Padding character.""" + if pad is not None: + pad = self._guardAgainstUnicode(pad) + self._padding = pad + + def getPadMode(self): + """getPadMode() -> pyDes.PAD_NORMAL or pyDes.PAD_PKCS5""" + return self._padmode + + def setPadMode(self, mode): + """Sets the type of padding mode, pyDes.PAD_NORMAL or pyDes.PAD_PKCS5""" + self._padmode = mode + + def getIV(self): + """getIV() -> bytes""" + return self._iv + + def setIV(self, IV): + """Will set the Initial Value, used in conjunction with CBC mode""" + if not IV or len(IV) != self.block_size: + raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes") + IV = self._guardAgainstUnicode(IV) + self._iv = IV + + def _padData(self, data, pad, padmode): + # Pad data depending on the mode + if padmode is None: + # Get the default padding mode. + padmode = self.getPadMode() + if pad and padmode == PAD_PKCS5: + raise ValueError("Cannot use a pad character with PAD_PKCS5") + + if padmode == PAD_NORMAL: + if len(data) % self.block_size == 0: + # No padding required. + return data + + if not pad: + # Get the default padding. + pad = self.getPadding() + if not pad: + raise ValueError("Data must be a multiple of " + str(self.block_size) + " bytes in length. Use padmode=PAD_PKCS5 or set the pad character.") + data += (self.block_size - (len(data) % self.block_size)) * pad + + elif padmode == PAD_PKCS5: + pad_len = 8 - (len(data) % self.block_size) + if _pythonMajorVersion < 3: + data += pad_len * chr(pad_len) + else: + data += bytes([pad_len] * pad_len) + + return data + + def _unpadData(self, data, pad, padmode): + # Unpad data depending on the mode. + if not data: + return data + if pad and padmode == PAD_PKCS5: + raise ValueError("Cannot use a pad character with PAD_PKCS5") + if padmode is None: + # Get the default padding mode. + padmode = self.getPadMode() + + if padmode == PAD_NORMAL: + if not pad: + # Get the default padding. + pad = self.getPadding() + if pad: + data = data[:-self.block_size] + \ + data[-self.block_size:].rstrip(pad) + + elif padmode == PAD_PKCS5: + if _pythonMajorVersion < 3: + pad_len = ord(data[-1]) + else: + pad_len = data[-1] + data = data[:-pad_len] + + return data + + def _guardAgainstUnicode(self, data): + # Only accept byte strings or ascii unicode values, otherwise + # there is no way to correctly decode the data into bytes. + if _pythonMajorVersion < 3: + if isinstance(data, unicode): + raise ValueError("pyDes can only work with bytes, not Unicode strings.") + else: + if isinstance(data, str): + # Only accept ascii unicode values. + try: + return data.encode('ascii') + except UnicodeEncodeError: + pass + raise ValueError("pyDes can only work with encoded strings, not Unicode.") + return data + +############################################################################# +# DES # +############################################################################# +class des(_baseDes): + """DES encryption/decrytpion class + + Supports ECB (Electronic Code Book) and CBC (Cypher Block Chaining) modes. + + pyDes.des(key,[mode], [IV]) + + key -> Bytes containing the encryption key, must be exactly 8 bytes + mode -> Optional argument for encryption type, can be either pyDes.ECB + (Electronic Code Book), pyDes.CBC (Cypher Block Chaining) + IV -> Optional Initial Value bytes, must be supplied if using CBC mode. + Must be 8 bytes in length. + pad -> Optional argument, set the pad character (PAD_NORMAL) to use + during all encrypt/decrypt operations done with this instance. + padmode -> Optional argument, set the padding mode (PAD_NORMAL or + PAD_PKCS5) to use during all encrypt/decrypt operations done + with this instance. + """ + + + # Permutation and translation tables for DES + __pc1 = [56, 48, 40, 32, 24, 16, 8, + 0, 57, 49, 41, 33, 25, 17, + 9, 1, 58, 50, 42, 34, 26, + 18, 10, 2, 59, 51, 43, 35, + 62, 54, 46, 38, 30, 22, 14, + 6, 61, 53, 45, 37, 29, 21, + 13, 5, 60, 52, 44, 36, 28, + 20, 12, 4, 27, 19, 11, 3 + ] + + # number left rotations of pc1 + __left_rotations = [ + 1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1 + ] + + # permuted choice key (table 2) + __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 + ] + + # initial permutation IP + __ip = [57, 49, 41, 33, 25, 17, 9, 1, + 59, 51, 43, 35, 27, 19, 11, 3, + 61, 53, 45, 37, 29, 21, 13, 5, + 63, 55, 47, 39, 31, 23, 15, 7, + 56, 48, 40, 32, 24, 16, 8, 0, + 58, 50, 42, 34, 26, 18, 10, 2, + 60, 52, 44, 36, 28, 20, 12, 4, + 62, 54, 46, 38, 30, 22, 14, 6 + ] + + # Expansion table for turning 32 bit blocks into 48 bits + __expansion_table = [ + 31, 0, 1, 2, 3, 4, + 3, 4, 5, 6, 7, 8, + 7, 8, 9, 10, 11, 12, + 11, 12, 13, 14, 15, 16, + 15, 16, 17, 18, 19, 20, + 19, 20, 21, 22, 23, 24, + 23, 24, 25, 26, 27, 28, + 27, 28, 29, 30, 31, 0 + ] + + # The (in)famous S-boxes + __sbox = [ + # S1 + [14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, + 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, + 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, + 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13], + + # S2 + [15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, + 3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5, + 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, + 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9], + + # S3 + [10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, + 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1, + 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, + 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12], + + # S4 + [7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, + 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9, + 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, + 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14], + + # S5 + [2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, + 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6, + 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, + 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3], + + # S6 + [12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, + 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8, + 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, + 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13], + + # S7 + [4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, + 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6, + 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, + 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12], + + # S8 + [13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, + 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, + 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, + 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11], + ] + + + # 32-bit permutation function P used on the output of the S-boxes + __p = [ + 15, 6, 19, 20, 28, 11, + 27, 16, 0, 14, 22, 25, + 4, 17, 30, 9, 1, 7, + 23,13, 31, 26, 2, 8, + 18, 12, 29, 5, 21, 10, + 3, 24 + ] + + # final permutation IP^-1 + __fp = [ + 39, 7, 47, 15, 55, 23, 63, 31, + 38, 6, 46, 14, 54, 22, 62, 30, + 37, 5, 45, 13, 53, 21, 61, 29, + 36, 4, 44, 12, 52, 20, 60, 28, + 35, 3, 43, 11, 51, 19, 59, 27, + 34, 2, 42, 10, 50, 18, 58, 26, + 33, 1, 41, 9, 49, 17, 57, 25, + 32, 0, 40, 8, 48, 16, 56, 24 + ] + + # Type of crypting being done + ENCRYPT = 0x00 + DECRYPT = 0x01 + + # Initialisation + def __init__(self, key, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): + # Sanity checking of arguments. + if len(key) != 8: + raise ValueError("Invalid DES key size. Key must be exactly 8 bytes long.") + _baseDes.__init__(self, mode, IV, pad, padmode) + self.key_size = 8 + + self.L = [] + self.R = [] + self.Kn = [ [0] * 48 ] * 16 # 16 48-bit keys (K1 - K16) + self.final = [] + + self.setKey(key) + + def setKey(self, key): + """Will set the crypting key for this object. Must be 8 bytes.""" + _baseDes.setKey(self, key) + self.__create_sub_keys() + + def __String_to_BitList(self, data): + """Turn the string data, into a list of bits (1, 0)'s""" + if _pythonMajorVersion < 3: + # Turn the strings into integers. Python 3 uses a bytes + # class, which already has this behaviour. + data = [ord(c) for c in data] + l = len(data) * 8 + result = [0] * l + pos = 0 + for ch in data: + i = 7 + while i >= 0: + if ch & (1 << i) != 0: + result[pos] = 1 + else: + result[pos] = 0 + pos += 1 + i -= 1 + + return result + + def __BitList_to_String(self, data): + """Turn the list of bits -> data, into a string""" + result = [] + pos = 0 + c = 0 + while pos < len(data): + c += data[pos] << (7 - (pos % 8)) + if (pos % 8) == 7: + result.append(c) + c = 0 + pos += 1 + + if _pythonMajorVersion < 3: + return ''.join([ chr(c) for c in result ]) + else: + return bytes(result) + + def __permutate(self, table, block): + """Permutate this block with the specified table""" + return list(map(lambda x: block[x], table)) + + # Transform the secret key, so that it is ready for data processing + # Create the 16 subkeys, K[1] - K[16] + def __create_sub_keys(self): + """Create the 16 subkeys K[1] to K[16] from the given key""" + key = self.__permutate(des.__pc1, self.__String_to_BitList(self.getKey())) + i = 0 + # Split into Left and Right sections + self.L = key[:28] + self.R = key[28:] + while i < 16: + j = 0 + # Perform circular left shifts + while j < des.__left_rotations[i]: + self.L.append(self.L[0]) + del self.L[0] + + self.R.append(self.R[0]) + del self.R[0] + + j += 1 + + # Create one of the 16 subkeys through pc2 permutation + self.Kn[i] = self.__permutate(des.__pc2, self.L + self.R) + + i += 1 + + # Main part of the encryption algorithm, the number cruncher :) + def __des_crypt(self, block, crypt_type): + """Crypt the block of data through DES bit-manipulation""" + block = self.__permutate(des.__ip, block) + self.L = block[:32] + self.R = block[32:] + + # Encryption starts from Kn[1] through to Kn[16] + if crypt_type == des.ENCRYPT: + iteration = 0 + iteration_adjustment = 1 + # Decryption starts from Kn[16] down to Kn[1] + else: + iteration = 15 + iteration_adjustment = -1 + + i = 0 + while i < 16: + # Make a copy of R[i-1], this will later become L[i] + tempR = self.R[:] + + # Permutate R[i - 1] to start creating R[i] + self.R = self.__permutate(des.__expansion_table, self.R) + + # Exclusive or R[i - 1] with K[i], create B[1] to B[8] whilst here + self.R = list(map(lambda x, y: x ^ y, self.R, self.Kn[iteration])) + B = [self.R[:6], self.R[6:12], self.R[12:18], self.R[18:24], self.R[24:30], self.R[30:36], self.R[36:42], self.R[42:]] + # Optimization: Replaced below commented code with above + #j = 0 + #B = [] + #while j < len(self.R): + # self.R[j] = self.R[j] ^ self.Kn[iteration][j] + # j += 1 + # if j % 6 == 0: + # B.append(self.R[j-6:j]) + + # Permutate B[1] to B[8] using the S-Boxes + j = 0 + Bn = [0] * 32 + pos = 0 + while j < 8: + # Work out the offsets + m = (B[j][0] << 1) + B[j][5] + n = (B[j][1] << 3) + (B[j][2] << 2) + (B[j][3] << 1) + B[j][4] + + # Find the permutation value + v = des.__sbox[j][(m << 4) + n] + + # Turn value into bits, add it to result: Bn + Bn[pos] = (v & 8) >> 3 + Bn[pos + 1] = (v & 4) >> 2 + Bn[pos + 2] = (v & 2) >> 1 + Bn[pos + 3] = v & 1 + + pos += 4 + j += 1 + + # Permutate the concatination of B[1] to B[8] (Bn) + self.R = self.__permutate(des.__p, Bn) + + # Xor with L[i - 1] + self.R = list(map(lambda x, y: x ^ y, self.R, self.L)) + # Optimization: This now replaces the below commented code + #j = 0 + #while j < len(self.R): + # self.R[j] = self.R[j] ^ self.L[j] + # j += 1 + + # L[i] becomes R[i - 1] + self.L = tempR + + i += 1 + iteration += iteration_adjustment + + # Final permutation of R[16]L[16] + self.final = self.__permutate(des.__fp, self.R + self.L) + return self.final + + + # Data to be encrypted/decrypted + def crypt(self, data, crypt_type): + """Crypt the data in blocks, running it through des_crypt()""" + + # Error check the data + if not data: + return '' + if len(data) % self.block_size != 0: + if crypt_type == des.DECRYPT: # Decryption must work on 8 byte blocks + raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n.") + if not self.getPadding(): + raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n. Try setting the optional padding character") + else: + data += (self.block_size - (len(data) % self.block_size)) * self.getPadding() + # print "Len of data: %f" % (len(data) / self.block_size) + + if self.getMode() == CBC: + if self.getIV(): + iv = self.__String_to_BitList(self.getIV()) + else: + raise ValueError("For CBC mode, you must supply the Initial Value (IV) for ciphering") + + # Split the data into blocks, crypting each one seperately + i = 0 + dict = {} + result = [] + #cached = 0 + #lines = 0 + while i < len(data): + # Test code for caching encryption results + #lines += 1 + #if dict.has_key(data[i:i+8]): + #print "Cached result for: %s" % data[i:i+8] + # cached += 1 + # result.append(dict[data[i:i+8]]) + # i += 8 + # continue + + block = self.__String_to_BitList(data[i:i+8]) + + # Xor with IV if using CBC mode + if self.getMode() == CBC: + if crypt_type == des.ENCRYPT: + block = list(map(lambda x, y: x ^ y, block, iv)) + #j = 0 + #while j < len(block): + # block[j] = block[j] ^ iv[j] + # j += 1 + + processed_block = self.__des_crypt(block, crypt_type) + + if crypt_type == des.DECRYPT: + processed_block = list(map(lambda x, y: x ^ y, processed_block, iv)) + #j = 0 + #while j < len(processed_block): + # processed_block[j] = processed_block[j] ^ iv[j] + # j += 1 + iv = block + else: + iv = processed_block + else: + processed_block = self.__des_crypt(block, crypt_type) + + + # Add the resulting crypted block to our list + #d = self.__BitList_to_String(processed_block) + #result.append(d) + result.append(self.__BitList_to_String(processed_block)) + #dict[data[i:i+8]] = d + i += 8 + + # print "Lines: %d, cached: %d" % (lines, cached) + + # Return the full crypted string + if _pythonMajorVersion < 3: + return ''.join(result) + else: + return bytes.fromhex('').join(result) + + def encrypt(self, data, pad=None, padmode=None): + """encrypt(data, [pad], [padmode]) -> bytes + + data : Bytes to be encrypted + pad : Optional argument for encryption padding. Must only be one byte + padmode : Optional argument for overriding the padding mode. + + The data must be a multiple of 8 bytes and will be encrypted + with the already specified key. Data does not have to be a + multiple of 8 bytes if the padding character is supplied, or + the padmode is set to PAD_PKCS5, as bytes will then added to + ensure the be padded data is a multiple of 8 bytes. + """ + data = self._guardAgainstUnicode(data) + if pad is not None: + pad = self._guardAgainstUnicode(pad) + data = self._padData(data, pad, padmode) + return self.crypt(data, des.ENCRYPT) + + def decrypt(self, data, pad=None, padmode=None): + """decrypt(data, [pad], [padmode]) -> bytes + + data : Bytes to be decrypted + pad : Optional argument for decryption padding. Must only be one byte + padmode : Optional argument for overriding the padding mode. + + The data must be a multiple of 8 bytes and will be decrypted + with the already specified key. In PAD_NORMAL mode, if the + optional padding character is supplied, then the un-encrypted + data will have the padding characters removed from the end of + the bytes. This pad removal only occurs on the last 8 bytes of + the data (last data block). In PAD_PKCS5 mode, the special + padding end markers will be removed from the data after decrypting. + """ + data = self._guardAgainstUnicode(data) + if pad is not None: + pad = self._guardAgainstUnicode(pad) + data = self.crypt(data, des.DECRYPT) + return self._unpadData(data, pad, padmode) + + + +############################################################################# +# Triple DES # +############################################################################# +class triple_des(_baseDes): + """Triple DES encryption/decrytpion class + + This algorithm uses the DES-EDE3 (when a 24 byte key is supplied) or + the DES-EDE2 (when a 16 byte key is supplied) encryption methods. + Supports ECB (Electronic Code Book) and CBC (Cypher Block Chaining) modes. + + pyDes.des(key, [mode], [IV]) + + key -> Bytes containing the encryption key, must be either 16 or + 24 bytes long + mode -> Optional argument for encryption type, can be either pyDes.ECB + (Electronic Code Book), pyDes.CBC (Cypher Block Chaining) + IV -> Optional Initial Value bytes, must be supplied if using CBC mode. + Must be 8 bytes in length. + pad -> Optional argument, set the pad character (PAD_NORMAL) to use + during all encrypt/decrypt operations done with this instance. + padmode -> Optional argument, set the padding mode (PAD_NORMAL or + PAD_PKCS5) to use during all encrypt/decrypt operations done + with this instance. + """ + def __init__(self, key, mode=ECB, IV=None, pad=None, padmode=PAD_NORMAL): + _baseDes.__init__(self, mode, IV, pad, padmode) + self.setKey(key) + + def setKey(self, key): + """Will set the crypting key for this object. Either 16 or 24 bytes long.""" + self.key_size = 24 # Use DES-EDE3 mode + if len(key) != self.key_size: + if len(key) == 16: # Use DES-EDE2 mode + self.key_size = 16 + else: + raise ValueError("Invalid triple DES key size. Key must be either 16 or 24 bytes long") + if self.getMode() == CBC: + if not self.getIV(): + # Use the first 8 bytes of the key + self._iv = key[:self.block_size] + if len(self.getIV()) != self.block_size: + raise ValueError("Invalid IV, must be 8 bytes in length") + self.__key1 = des(key[:8], self._mode, self._iv, + self._padding, self._padmode) + self.__key2 = des(key[8:16], self._mode, self._iv, + self._padding, self._padmode) + if self.key_size == 16: + self.__key3 = self.__key1 + else: + self.__key3 = des(key[16:], self._mode, self._iv, + self._padding, self._padmode) + _baseDes.setKey(self, key) + + # Override setter methods to work on all 3 keys. + + def setMode(self, mode): + """Sets the type of crypting mode, pyDes.ECB or pyDes.CBC""" + _baseDes.setMode(self, mode) + for key in (self.__key1, self.__key2, self.__key3): + key.setMode(mode) + + def setPadding(self, pad): + """setPadding() -> bytes of length 1. Padding character.""" + _baseDes.setPadding(self, pad) + for key in (self.__key1, self.__key2, self.__key3): + key.setPadding(pad) + + def setPadMode(self, mode): + """Sets the type of padding mode, pyDes.PAD_NORMAL or pyDes.PAD_PKCS5""" + _baseDes.setPadMode(self, mode) + for key in (self.__key1, self.__key2, self.__key3): + key.setPadMode(mode) + + def setIV(self, IV): + """Will set the Initial Value, used in conjunction with CBC mode""" + _baseDes.setIV(self, IV) + for key in (self.__key1, self.__key2, self.__key3): + key.setIV(IV) + + def encrypt(self, data, pad=None, padmode=None): + """encrypt(data, [pad], [padmode]) -> bytes + + data : bytes to be encrypted + pad : Optional argument for encryption padding. Must only be one byte + padmode : Optional argument for overriding the padding mode. + + The data must be a multiple of 8 bytes and will be encrypted + with the already specified key. Data does not have to be a + multiple of 8 bytes if the padding character is supplied, or + the padmode is set to PAD_PKCS5, as bytes will then added to + ensure the be padded data is a multiple of 8 bytes. + """ + ENCRYPT = des.ENCRYPT + DECRYPT = des.DECRYPT + data = self._guardAgainstUnicode(data) + if pad is not None: + pad = self._guardAgainstUnicode(pad) + # Pad the data accordingly. + data = self._padData(data, pad, padmode) + if self.getMode() == CBC: + self.__key1.setIV(self.getIV()) + self.__key2.setIV(self.getIV()) + self.__key3.setIV(self.getIV()) + i = 0 + result = [] + while i < len(data): + block = self.__key1.crypt(data[i:i+8], ENCRYPT) + block = self.__key2.crypt(block, DECRYPT) + block = self.__key3.crypt(block, ENCRYPT) + self.__key1.setIV(block) + self.__key2.setIV(block) + self.__key3.setIV(block) + result.append(block) + i += 8 + if _pythonMajorVersion < 3: + return ''.join(result) + else: + return bytes.fromhex('').join(result) + else: + data = self.__key1.crypt(data, ENCRYPT) + data = self.__key2.crypt(data, DECRYPT) + return self.__key3.crypt(data, ENCRYPT) + + def decrypt(self, data, pad=None, padmode=None): + """decrypt(data, [pad], [padmode]) -> bytes + + data : bytes to be encrypted + pad : Optional argument for decryption padding. Must only be one byte + padmode : Optional argument for overriding the padding mode. + + The data must be a multiple of 8 bytes and will be decrypted + with the already specified key. In PAD_NORMAL mode, if the + optional padding character is supplied, then the un-encrypted + data will have the padding characters removed from the end of + the bytes. This pad removal only occurs on the last 8 bytes of + the data (last data block). In PAD_PKCS5 mode, the special + padding end markers will be removed from the data after + decrypting, no pad character is required for PAD_PKCS5. + """ + ENCRYPT = des.ENCRYPT + DECRYPT = des.DECRYPT + data = self._guardAgainstUnicode(data) + if pad is not None: + pad = self._guardAgainstUnicode(pad) + if self.getMode() == CBC: + self.__key1.setIV(self.getIV()) + self.__key2.setIV(self.getIV()) + self.__key3.setIV(self.getIV()) + i = 0 + result = [] + while i < len(data): + iv = data[i:i+8] + block = self.__key3.crypt(iv, DECRYPT) + block = self.__key2.crypt(block, ENCRYPT) + block = self.__key1.crypt(block, DECRYPT) + self.__key1.setIV(iv) + self.__key2.setIV(iv) + self.__key3.setIV(iv) + result.append(block) + i += 8 + if _pythonMajorVersion < 3: + data = ''.join(result) + else: + data = bytes.fromhex('').join(result) + else: + data = self.__key3.crypt(data, DECRYPT) + data = self.__key2.crypt(data, ENCRYPT) + data = self.__key1.crypt(data, DECRYPT) + return self._unpadData(data, pad, padmode) diff --git a/systemvm/debian/root/noVNC/utils/rijndael.py b/systemvm/debian/root/noVNC/utils/rijndael.py new file mode 100644 index 000000000000..8815a3c955ba --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/rijndael.py @@ -0,0 +1,376 @@ +""" +A pure python (slow) implementation of rijndael with a decent interface + +To include - + +from rijndael import rijndael + +To do a key setup - + +r = rijndael(key, block_size = 16) + +key must be a string of length 16, 24, or 32 +blocksize must be 16, 24, or 32. Default is 16 + +To use - + +ciphertext = r.encrypt(plaintext) +plaintext = r.decrypt(ciphertext) + +If any strings are of the wrong length a ValueError is thrown +""" + +# ported from the Java reference code by Bram Cohen, April 2001 +# this code is public domain, unless someone makes +# an intellectual property claim against the reference +# code, in which case it can be made public domain by +# deleting all the comments and renaming all the variables + +import copy +import string + +shifts = [[[0, 0], [1, 3], [2, 2], [3, 1]], + [[0, 0], [1, 5], [2, 4], [3, 3]], + [[0, 0], [1, 7], [3, 5], [4, 4]]] + +# [keysize][block_size] +num_rounds = {16: {16: 10, 24: 12, 32: 14}, 24: {16: 12, 24: 12, 32: 14}, 32: {16: 14, 24: 14, 32: 14}} + +A = [[1, 1, 1, 1, 1, 0, 0, 0], + [0, 1, 1, 1, 1, 1, 0, 0], + [0, 0, 1, 1, 1, 1, 1, 0], + [0, 0, 0, 1, 1, 1, 1, 1], + [1, 0, 0, 0, 1, 1, 1, 1], + [1, 1, 0, 0, 0, 1, 1, 1], + [1, 1, 1, 0, 0, 0, 1, 1], + [1, 1, 1, 1, 0, 0, 0, 1]] + +# produce log and alog tables, needed for multiplying in the +# field GF(2^m) (generator = 3) +alog = [1] +for i in range(255): + j = (alog[-1] << 1) ^ alog[-1] + if j & 0x100 != 0: + j ^= 0x11B + alog.append(j) + +log = [0] * 256 +for i in range(1, 255): + log[alog[i]] = i + +# multiply two elements of GF(2^m) +def mul(a, b): + if a == 0 or b == 0: + return 0 + return alog[(log[a & 0xFF] + log[b & 0xFF]) % 255] + +# substitution box based on F^{-1}(x) +box = [[0] * 8 for i in range(256)] +box[1][7] = 1 +for i in range(2, 256): + j = alog[255 - log[i]] + for t in range(8): + box[i][t] = (j >> (7 - t)) & 0x01 + +B = [0, 1, 1, 0, 0, 0, 1, 1] + +# affine transform: box[i] <- B + A*box[i] +cox = [[0] * 8 for i in range(256)] +for i in range(256): + for t in range(8): + cox[i][t] = B[t] + for j in range(8): + cox[i][t] ^= A[t][j] * box[i][j] + +# S-boxes and inverse S-boxes +S = [0] * 256 +Si = [0] * 256 +for i in range(256): + S[i] = cox[i][0] << 7 + for t in range(1, 8): + S[i] ^= cox[i][t] << (7-t) + Si[S[i] & 0xFF] = i + +# T-boxes +G = [[2, 1, 1, 3], + [3, 2, 1, 1], + [1, 3, 2, 1], + [1, 1, 3, 2]] + +AA = [[0] * 8 for i in range(4)] + +for i in range(4): + for j in range(4): + AA[i][j] = G[i][j] + AA[i][i+4] = 1 + +for i in range(4): + pivot = AA[i][i] + if pivot == 0: + t = i + 1 + while AA[t][i] == 0 and t < 4: + t += 1 + assert t != 4, 'G matrix must be invertible' + for j in range(8): + AA[i][j], AA[t][j] = AA[t][j], AA[i][j] + pivot = AA[i][i] + for j in range(8): + if AA[i][j] != 0: + AA[i][j] = alog[(255 + log[AA[i][j] & 0xFF] - log[pivot & 0xFF]) % 255] + for t in range(4): + if i != t: + for j in range(i+1, 8): + AA[t][j] ^= mul(AA[i][j], AA[t][i]) + AA[t][i] = 0 + +iG = [[0] * 4 for i in range(4)] + +for i in range(4): + for j in range(4): + iG[i][j] = AA[i][j + 4] + +def mul4(a, bs): + if a == 0: + return 0 + r = 0 + for b in bs: + r <<= 8 + if b != 0: + r = r | mul(a, b) + return r + +T1 = [] +T2 = [] +T3 = [] +T4 = [] +T5 = [] +T6 = [] +T7 = [] +T8 = [] +U1 = [] +U2 = [] +U3 = [] +U4 = [] + +for t in range(256): + s = S[t] + T1.append(mul4(s, G[0])) + T2.append(mul4(s, G[1])) + T3.append(mul4(s, G[2])) + T4.append(mul4(s, G[3])) + + s = Si[t] + T5.append(mul4(s, iG[0])) + T6.append(mul4(s, iG[1])) + T7.append(mul4(s, iG[2])) + T8.append(mul4(s, iG[3])) + + U1.append(mul4(t, iG[0])) + U2.append(mul4(t, iG[1])) + U3.append(mul4(t, iG[2])) + U4.append(mul4(t, iG[3])) + +# round constants +rcon = [1] +r = 1 +for t in range(1, 30): + r = mul(2, r) + rcon.append(r) + +del A +del AA +del pivot +del B +del G +del box +del log +del alog +del i +del j +del r +del s +del t +del mul +del mul4 +del cox +del iG + +class rijndael: + def __init__(self, key, block_size = 16): + if block_size != 16 and block_size != 24 and block_size != 32: + raise ValueError('Invalid block size: ' + str(block_size)) + if len(key) != 16 and len(key) != 24 and len(key) != 32: + raise ValueError('Invalid key size: ' + str(len(key))) + self.block_size = block_size + + ROUNDS = num_rounds[len(key)][block_size] + BC = block_size // 4 + # encryption round keys + Ke = [[0] * BC for i in range(ROUNDS + 1)] + # decryption round keys + Kd = [[0] * BC for i in range(ROUNDS + 1)] + ROUND_KEY_COUNT = (ROUNDS + 1) * BC + KC = len(key) // 4 + + # copy user material bytes into temporary ints + tk = [] + for i in range(0, KC): + tk.append((ord(key[i * 4]) << 24) | (ord(key[i * 4 + 1]) << 16) | + (ord(key[i * 4 + 2]) << 8) | ord(key[i * 4 + 3])) + + # copy values into round key arrays + t = 0 + j = 0 + while j < KC and t < ROUND_KEY_COUNT: + Ke[t // BC][t % BC] = tk[j] + Kd[ROUNDS - (t // BC)][t % BC] = tk[j] + j += 1 + t += 1 + tt = 0 + rconpointer = 0 + while t < ROUND_KEY_COUNT: + # extrapolate using phi (the round key evolution function) + tt = tk[KC - 1] + tk[0] ^= (S[(tt >> 16) & 0xFF] & 0xFF) << 24 ^ \ + (S[(tt >> 8) & 0xFF] & 0xFF) << 16 ^ \ + (S[ tt & 0xFF] & 0xFF) << 8 ^ \ + (S[(tt >> 24) & 0xFF] & 0xFF) ^ \ + (rcon[rconpointer] & 0xFF) << 24 + rconpointer += 1 + if KC != 8: + for i in range(1, KC): + tk[i] ^= tk[i-1] + else: + for i in range(1, KC // 2): + tk[i] ^= tk[i-1] + tt = tk[KC // 2 - 1] + tk[KC // 2] ^= (S[ tt & 0xFF] & 0xFF) ^ \ + (S[(tt >> 8) & 0xFF] & 0xFF) << 8 ^ \ + (S[(tt >> 16) & 0xFF] & 0xFF) << 16 ^ \ + (S[(tt >> 24) & 0xFF] & 0xFF) << 24 + for i in range(KC // 2 + 1, KC): + tk[i] ^= tk[i-1] + # copy values into round key arrays + j = 0 + while j < KC and t < ROUND_KEY_COUNT: + Ke[t // BC][t % BC] = tk[j] + Kd[ROUNDS - (t // BC)][t % BC] = tk[j] + j += 1 + t += 1 + # inverse MixColumn where needed + for r in range(1, ROUNDS): + for j in range(BC): + tt = Kd[r][j] + Kd[r][j] = U1[(tt >> 24) & 0xFF] ^ \ + U2[(tt >> 16) & 0xFF] ^ \ + U3[(tt >> 8) & 0xFF] ^ \ + U4[ tt & 0xFF] + self.Ke = Ke + self.Kd = Kd + + def encrypt(self, plaintext): + if len(plaintext) != self.block_size: + raise ValueError('wrong block length, expected ' + str(self.block_size) + ' got ' + str(len(plaintext))) + Ke = self.Ke + + BC = self.block_size // 4 + ROUNDS = len(Ke) - 1 + if BC == 4: + SC = 0 + elif BC == 6: + SC = 1 + else: + SC = 2 + s1 = shifts[SC][1][0] + s2 = shifts[SC][2][0] + s3 = shifts[SC][3][0] + a = [0] * BC + # temporary work array + t = [] + # plaintext to ints + key + for i in range(BC): + t.append((ord(plaintext[i * 4 ]) << 24 | + ord(plaintext[i * 4 + 1]) << 16 | + ord(plaintext[i * 4 + 2]) << 8 | + ord(plaintext[i * 4 + 3]) ) ^ Ke[0][i]) + # apply round transforms + for r in range(1, ROUNDS): + for i in range(BC): + a[i] = (T1[(t[ i ] >> 24) & 0xFF] ^ + T2[(t[(i + s1) % BC] >> 16) & 0xFF] ^ + T3[(t[(i + s2) % BC] >> 8) & 0xFF] ^ + T4[ t[(i + s3) % BC] & 0xFF] ) ^ Ke[r][i] + t = copy.copy(a) + # last round is special + result = [] + for i in range(BC): + tt = Ke[ROUNDS][i] + result.append((S[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((S[(t[(i + s1) % BC] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((S[(t[(i + s2) % BC] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((S[ t[(i + s3) % BC] & 0xFF] ^ tt ) & 0xFF) + return ''.join(map(chr, result)) + + def decrypt(self, ciphertext): + if len(ciphertext) != self.block_size: + raise ValueError('wrong block length, expected ' + str(self.block_size) + ' got ' + str(len(ciphertext))) + Kd = self.Kd + + BC = self.block_size // 4 + ROUNDS = len(Kd) - 1 + if BC == 4: + SC = 0 + elif BC == 6: + SC = 1 + else: + SC = 2 + s1 = shifts[SC][1][1] + s2 = shifts[SC][2][1] + s3 = shifts[SC][3][1] + a = [0] * BC + # temporary work array + t = [0] * BC + # ciphertext to ints + key + for i in range(BC): + t[i] = (ord(ciphertext[i * 4 ]) << 24 | + ord(ciphertext[i * 4 + 1]) << 16 | + ord(ciphertext[i * 4 + 2]) << 8 | + ord(ciphertext[i * 4 + 3]) ) ^ Kd[0][i] + # apply round transforms + for r in range(1, ROUNDS): + for i in range(BC): + a[i] = (T5[(t[ i ] >> 24) & 0xFF] ^ + T6[(t[(i + s1) % BC] >> 16) & 0xFF] ^ + T7[(t[(i + s2) % BC] >> 8) & 0xFF] ^ + T8[ t[(i + s3) % BC] & 0xFF] ) ^ Kd[r][i] + t = copy.copy(a) + # last round is special + result = [] + for i in range(BC): + tt = Kd[ROUNDS][i] + result.append((Si[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((Si[(t[(i + s1) % BC] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((Si[(t[(i + s2) % BC] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((Si[ t[(i + s3) % BC] & 0xFF] ^ tt ) & 0xFF) + return ''.join(map(chr, result)) + +def encrypt(key, block): + return rijndael(key, len(block)).encrypt(block) + +def decrypt(key, block): + return rijndael(key, len(block)).decrypt(block) + +def test(): + def t(kl, bl): + b = 'b' * bl + r = rijndael('a' * kl, bl) + assert r.decrypt(r.encrypt(b)) == b + t(16, 16) + t(16, 24) + t(16, 32) + t(24, 16) + t(24, 24) + t(24, 32) + t(32, 16) + t(32, 24) + t(32, 32) diff --git a/systemvm/debian/root/noVNC/utils/rijndael_cs.py b/systemvm/debian/root/noVNC/utils/rijndael_cs.py new file mode 100644 index 000000000000..f39da1f8e495 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/rijndael_cs.py @@ -0,0 +1,41 @@ +import rijndael +import base64 + +KEY_SIZE = 16 +BLOCK_SIZE = 32 + +def encrypt(key, plaintext): + padded_key = key.ljust(KEY_SIZE, '\0') + padded_text = plaintext + (BLOCK_SIZE - len(plaintext) % BLOCK_SIZE) * '\0' + + # could also be one of + #if len(plaintext) % BLOCK_SIZE != 0: + # padded_text = plaintext.ljust((len(plaintext) / BLOCK_SIZE) + 1 * BLOCKSIZE), '\0') + # -OR- + #padded_text = plaintext.ljust((len(plaintext) + (BLOCK_SIZE - len(plaintext) % BLOCK_SIZE)), '\0') + + r = rijndael.rijndael(padded_key, BLOCK_SIZE) + + ciphertext = '' + for start in range(0, len(padded_text), BLOCK_SIZE): + ciphertext += r.encrypt(padded_text[start:start+BLOCK_SIZE]) + + encoded = ciphertext.encode('hex_codec') + + return encoded + + +def decrypt(key, encoded): + padded_key = key.ljust(KEY_SIZE, '\0') + + ciphertext = encoded.decode('hex_codec') + + r = rijndael.rijndael(padded_key, BLOCK_SIZE) + + padded_text = '' + for start in range(0, len(ciphertext), BLOCK_SIZE): + padded_text += r.decrypt(ciphertext[start:start+BLOCK_SIZE]) + + plaintext = padded_text.split('\x00', 1)[0] + + return plaintext diff --git a/systemvm/debian/root/noVNC/utils/u2x11 b/systemvm/debian/root/noVNC/utils/u2x11 new file mode 100755 index 000000000000..fd3e4ba88a5f --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/u2x11 @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# +# Convert "U+..." commented entries in /usr/include/X11/keysymdef.h +# into JavaScript for use by noVNC. Note this is likely to produce +# a few duplicate properties with clashing values, that will need +# resolving manually. +# +# Colin Dean +# + +regex="^#define[ \t]+XK_[A-Za-z0-9_]+[ \t]+0x([0-9a-fA-F]+)[ \t]+\/\*[ \t]+U\+([0-9a-fA-F]+)[ \t]+[^*]+.[ \t]+\*\/[ \t]*$" +echo "unicodeTable = {" +while read line; do + if echo "${line}" | egrep -qs "${regex}"; then + + x11=$(echo "${line}" | sed -r "s/${regex}/\1/") + vnc=$(echo "${line}" | sed -r "s/${regex}/\2/") + + if echo "${vnc}" | egrep -qs "^00[2-9A-F][0-9A-F]$"; then + : # skip ISO Latin-1 (U+0020 to U+00FF) as 1-to-1 mapping + else + # note 1-to-1 is possible (e.g. for Euro symbol, U+20AC) + echo " 0x${vnc} : 0x${x11}," + fi + fi +done < /usr/include/X11/keysymdef.h | uniq +echo "};" + diff --git a/systemvm/debian/root/noVNC/utils/use_require.js b/systemvm/debian/root/noVNC/utils/use_require.js new file mode 100755 index 000000000000..248792718c92 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/use_require.js @@ -0,0 +1,313 @@ +#!/usr/bin/env node + +const path = require('path'); +const program = require('commander'); +const fs = require('fs'); +const fse = require('fs-extra'); +const babel = require('babel-core'); + +const SUPPORTED_FORMATS = new Set(['amd', 'commonjs', 'systemjs', 'umd']); + +program + .option('--as [format]', `output files using various import formats instead of ES6 import and export. Supports ${Array.from(SUPPORTED_FORMATS)}.`) + .option('-m, --with-source-maps [type]', 'output source maps when not generating a bundled app (type may be empty for external source maps, inline for inline source maps, or both) ') + .option('--with-app', 'process app files as well as core files') + .option('--only-legacy', 'only output legacy files (no ES6 modules) for the app') + .option('--clean', 'clear the lib folder before building') + .parse(process.argv); + +// the various important paths +const paths = { + main: path.resolve(__dirname, '..'), + core: path.resolve(__dirname, '..', 'core'), + app: path.resolve(__dirname, '..', 'app'), + vendor: path.resolve(__dirname, '..', 'vendor'), + out_dir_base: path.resolve(__dirname, '..', 'build'), + lib_dir_base: path.resolve(__dirname, '..', 'lib'), +}; + +const no_copy_files = new Set([ + // skip these -- they don't belong in the processed application + path.join(paths.vendor, 'sinon.js'), + path.join(paths.vendor, 'browser-es-module-loader'), + path.join(paths.vendor, 'promise.js'), + path.join(paths.app, 'images', 'icons', 'Makefile'), +]); + +const no_transform_files = new Set([ + // don't transform this -- we want it imported as-is to properly catch loading errors + path.join(paths.app, 'error-handler.js'), +]); + +no_copy_files.forEach(file => no_transform_files.add(file)); + +// util.promisify requires Node.js 8.x, so we have our own +function promisify(original) { + return function promise_wrap() { + const args = Array.prototype.slice.call(arguments); + return new Promise((resolve, reject) => { + original.apply(this, args.concat((err, value) => { + if (err) return reject(err); + resolve(value); + })); + }); + }; +} + +const readFile = promisify(fs.readFile); +const writeFile = promisify(fs.writeFile); + +const readdir = promisify(fs.readdir); +const lstat = promisify(fs.lstat); + +const copy = promisify(fse.copy); +const unlink = promisify(fse.unlink); +const ensureDir = promisify(fse.ensureDir); +const rmdir = promisify(fse.rmdir); + +const babelTransformFile = promisify(babel.transformFile); + +// walkDir *recursively* walks directories trees, +// calling the callback for all normal files found. +function walkDir(base_path, cb, filter) { + return readdir(base_path) + .then((files) => { + const paths = files.map(filename => path.join(base_path, filename)); + return Promise.all(paths.map(filepath => lstat(filepath) + .then((stats) => { + if (filter !== undefined && !filter(filepath, stats)) return; + + if (stats.isSymbolicLink()) return; + if (stats.isFile()) return cb(filepath); + if (stats.isDirectory()) return walkDir(filepath, cb, filter); + }))); + }); +} + +function transform_html(legacy_scripts, only_legacy) { + // write out the modified vnc.html file that works with the bundle + const src_html_path = path.resolve(__dirname, '..', 'vnc.html'); + const out_html_path = path.resolve(paths.out_dir_base, 'vnc.html'); + return readFile(src_html_path) + .then((contents_raw) => { + let contents = contents_raw.toString(); + + const start_marker = '\n'; + const end_marker = ''; + const start_ind = contents.indexOf(start_marker) + start_marker.length; + const end_ind = contents.indexOf(end_marker, start_ind); + + let new_script = ''; + + if (only_legacy) { + // Only legacy version, so include things directly + for (let i = 0;i < legacy_scripts.length;i++) { + new_script += ` \n`; + } + } else { + // Otherwise detect if it's a modern browser and select + // variant accordingly + new_script += `\ + \n\ + \n`; + + // Original, ES6 modules + new_script += ' \n'; + } + + contents = contents.slice(0, start_ind) + `${new_script}\n` + contents.slice(end_ind); + + return contents; + }) + .then((contents) => { + console.log(`Writing ${out_html_path}`); + return writeFile(out_html_path, contents); + }); +} + +function make_lib_files(import_format, source_maps, with_app_dir, only_legacy) { + if (!import_format) { + throw new Error("you must specify an import format to generate compiled noVNC libraries"); + } else if (!SUPPORTED_FORMATS.has(import_format)) { + throw new Error(`unsupported output format "${import_format}" for import/export -- only ${Array.from(SUPPORTED_FORMATS)} are supported`); + } + + // NB: we need to make a copy of babel_opts, since babel sets some defaults on it + const babel_opts = () => ({ + plugins: [`transform-es2015-modules-${import_format}`], + presets: ['es2015'], + ast: false, + sourceMaps: source_maps, + }); + + // No point in duplicate files without the app, so force only converted files + if (!with_app_dir) { + only_legacy = true; + } + + let in_path; + let out_path_base; + if (with_app_dir) { + out_path_base = paths.out_dir_base; + in_path = paths.main; + } else { + out_path_base = paths.lib_dir_base; + } + const legacy_path_base = only_legacy ? out_path_base : path.join(out_path_base, 'legacy'); + + fse.ensureDirSync(out_path_base); + + const helpers = require('./use_require_helpers'); + const helper = helpers[import_format]; + + const outFiles = []; + + const handleDir = (js_only, vendor_rewrite, in_path_base, filename) => Promise.resolve() + .then(() => { + if (no_copy_files.has(filename)) return; + + const out_path = path.join(out_path_base, path.relative(in_path_base, filename)); + const legacy_path = path.join(legacy_path_base, path.relative(in_path_base, filename)); + + if (path.extname(filename) !== '.js') { + if (!js_only) { + console.log(`Writing ${out_path}`); + return copy(filename, out_path); + } + return; // skip non-javascript files + } + + return Promise.resolve() + .then(() => { + if (only_legacy && !no_transform_files.has(filename)) { + return; + } + return ensureDir(path.dirname(out_path)) + .then(() => { + console.log(`Writing ${out_path}`); + return copy(filename, out_path); + }); + }) + .then(() => ensureDir(path.dirname(legacy_path))) + .then(() => { + if (no_transform_files.has(filename)) { + return; + } + + const opts = babel_opts(); + if (helper && helpers.optionsOverride) { + helper.optionsOverride(opts); + } + // Adjust for the fact that we move the core files relative + // to the vendor directory + if (vendor_rewrite) { + opts.plugins.push(["import-redirect", + {"root": legacy_path_base, + "redirect": { "vendor/(.+)": "./vendor/$1"}}]); + } + + return babelTransformFile(filename, opts) + .then((res) => { + console.log(`Writing ${legacy_path}`); + const {map} = res; + let {code} = res; + if (source_maps === true) { + // append URL for external source map + code += `\n//# sourceMappingURL=${path.basename(legacy_path)}.map\n`; + } + outFiles.push(`${legacy_path}`); + return writeFile(legacy_path, code) + .then(() => { + if (source_maps === true || source_maps === 'both') { + console.log(` and ${legacy_path}.map`); + outFiles.push(`${legacy_path}.map`); + return writeFile(`${legacy_path}.map`, JSON.stringify(map)); + } + }); + }); + }); + }); + + if (with_app_dir && helper && helper.noCopyOverride) { + helper.noCopyOverride(paths, no_copy_files); + } + + Promise.resolve() + .then(() => { + const handler = handleDir.bind(null, true, false, in_path || paths.main); + const filter = (filename, stats) => !no_copy_files.has(filename); + return walkDir(paths.vendor, handler, filter); + }) + .then(() => { + const handler = handleDir.bind(null, true, !in_path, in_path || paths.core); + const filter = (filename, stats) => !no_copy_files.has(filename); + return walkDir(paths.core, handler, filter); + }) + .then(() => { + if (!with_app_dir) return; + const handler = handleDir.bind(null, false, false, in_path); + const filter = (filename, stats) => !no_copy_files.has(filename); + return walkDir(paths.app, handler, filter); + }) + .then(() => { + if (!with_app_dir) return; + + if (!helper || !helper.appWriter) { + throw new Error(`Unable to generate app for the ${import_format} format!`); + } + + const out_app_path = path.join(legacy_path_base, 'app.js'); + console.log(`Writing ${out_app_path}`); + return helper.appWriter(out_path_base, legacy_path_base, out_app_path) + .then((extra_scripts) => { + const rel_app_path = path.relative(out_path_base, out_app_path); + const legacy_scripts = extra_scripts.concat([rel_app_path]); + transform_html(legacy_scripts, only_legacy); + }) + .then(() => { + if (!helper.removeModules) return; + console.log(`Cleaning up temporary files...`); + return Promise.all(outFiles.map((filepath) => { + unlink(filepath) + .then(() => { + // Try to clean up any empty directories if this + // was the last file in there + const rmdir_r = dir => + rmdir(dir) + .then(() => rmdir_r(path.dirname(dir))) + .catch(() => { + // Assume the error was ENOTEMPTY and ignore it + }); + return rmdir_r(path.dirname(filepath)); + }); + })); + }); + }) + .catch((err) => { + console.error(`Failure converting modules: ${err}`); + process.exit(1); + }); +} + +if (program.clean) { + console.log(`Removing ${paths.lib_dir_base}`); + fse.removeSync(paths.lib_dir_base); + + console.log(`Removing ${paths.out_dir_base}`); + fse.removeSync(paths.out_dir_base); +} + +make_lib_files(program.as, program.withSourceMaps, program.withApp, program.onlyLegacy); diff --git a/systemvm/debian/root/noVNC/utils/use_require_helpers.js b/systemvm/debian/root/noVNC/utils/use_require_helpers.js new file mode 100644 index 000000000000..a4f99c7045ca --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/use_require_helpers.js @@ -0,0 +1,76 @@ +// writes helpers require for vnc.html (they should output app.js) +const fs = require('fs'); +const path = require('path'); + +// util.promisify requires Node.js 8.x, so we have our own +function promisify(original) { + return function promise_wrap() { + const args = Array.prototype.slice.call(arguments); + return new Promise((resolve, reject) => { + original.apply(this, args.concat((err, value) => { + if (err) return reject(err); + resolve(value); + })); + }); + }; +} + +const writeFile = promisify(fs.writeFile); + +module.exports = { + 'amd': { + appWriter: (base_out_path, script_base_path, out_path) => { + // setup for requirejs + const ui_path = path.relative(base_out_path, + path.join(script_base_path, 'app', 'ui')); + return writeFile(out_path, `requirejs(["${ui_path}"], (ui) => {});`) + .then(() => { + console.log(`Please place RequireJS in ${path.join(script_base_path, 'require.js')}`); + const require_path = path.relative(base_out_path, + path.join(script_base_path, 'require.js')); + return [ require_path ]; + }); + }, + noCopyOverride: () => {}, + }, + 'commonjs': { + optionsOverride: (opts) => { + // CommonJS supports properly shifting the default export to work as normal + opts.plugins.unshift("add-module-exports"); + }, + appWriter: (base_out_path, script_base_path, out_path) => { + const browserify = require('browserify'); + const b = browserify(path.join(script_base_path, 'app/ui.js'), {}); + return promisify(b.bundle).call(b) + .then(buf => writeFile(out_path, buf)) + .then(() => []); + }, + noCopyOverride: () => {}, + removeModules: true, + }, + 'systemjs': { + appWriter: (base_out_path, script_base_path, out_path) => { + const ui_path = path.relative(base_out_path, + path.join(script_base_path, 'app', 'ui.js')); + return writeFile(out_path, `SystemJS.import("${ui_path}");`) + .then(() => { + console.log(`Please place SystemJS in ${path.join(script_base_path, 'system-production.js')}`); + // FIXME: Should probably be in the legacy directory + const promise_path = path.relative(base_out_path, + path.join(base_out_path, 'vendor', 'promise.js')); + const systemjs_path = path.relative(base_out_path, + path.join(script_base_path, 'system-production.js')); + return [ promise_path, systemjs_path ]; + }); + }, + noCopyOverride: (paths, no_copy_files) => { + no_copy_files.delete(path.join(paths.vendor, 'promise.js')); + }, + }, + 'umd': { + optionsOverride: (opts) => { + // umd supports properly shifting the default export to work as normal + opts.plugins.unshift("add-module-exports"); + }, + }, +}; diff --git a/systemvm/debian/root/noVNC/utils/validate b/systemvm/debian/root/noVNC/utils/validate new file mode 100755 index 000000000000..a6b5507d2ac0 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/validate @@ -0,0 +1,45 @@ +#!/bin/bash + +set -e + +RET=0 + +OUT=`mktemp` + +for fn in "$@"; do + echo "Validating $fn..." + echo + + case $fn in + *.html) + type="text/html" + ;; + *.css) + type="text/css" + ;; + *) + echo "Unknown format!" + echo + RET=1 + continue + ;; + esac + + curl --silent \ + --header "Content-Type: ${type}; charset=utf-8" \ + --data-binary @${fn} \ + https://validator.w3.org/nu/?out=text > $OUT + cat $OUT + echo + + # We don't fail the check for warnings as some warnings are + # not relevant for us, and we don't currently have a way to + # ignore just those + if grep -q -s -E "^Error:" $OUT; then + RET=1 + fi +done + +rm $OUT + +exit $RET diff --git a/systemvm/debian/root/noVNC/utils/websockify/.gitignore b/systemvm/debian/root/noVNC/utils/websockify/.gitignore new file mode 100644 index 000000000000..a6a9bfdb85b2 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/.gitignore @@ -0,0 +1,9 @@ +*.pyc +*.o +*.so +.project +.pydevproject +target.cfg +target.cfg.d +.tox +*.egg-info diff --git a/systemvm/debian/root/noVNC/utils/websockify/.gitmodules b/systemvm/debian/root/noVNC/utils/websockify/.gitmodules new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/systemvm/debian/root/noVNC/utils/websockify/.travis.yml b/systemvm/debian/root/noVNC/utils/websockify/.travis.yml new file mode 100644 index 000000000000..beeecb10861d --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/.travis.yml @@ -0,0 +1,15 @@ +language: python +python: + - 2.6 + - 2.7 + - 3.3 + - 3.4 +# We need trusty for Python 2.6. We can raise this when we no longer +# care about 2.6. +dist: trusty + +install: + - pip install 'setuptools>=18.5,<=39.0.0' + - pip install -r test-requirements.txt + +script: python setup.py nosetests --verbosity=3 diff --git a/systemvm/debian/root/noVNC/utils/websockify/CHANGES.txt b/systemvm/debian/root/noVNC/utils/websockify/CHANGES.txt new file mode 100644 index 000000000000..a44057647d28 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/CHANGES.txt @@ -0,0 +1,108 @@ +Changes +======= + +0.9.0 +----- + +* Base64 support removed and binary mode is now required +* Low level WebSocket protocol handling now has its own class +* Authentication now optionally required for web server +* Server hostname can be used as the token +* JWT/JWS/JWE can be used for the token +* redis can be used for the token +* Can now log to syslog +* Improved latency by disabling Nagle for proxied connection +* Added client certificate authentication +* Support for password protected certificate key file +* TLS ciphers and options are now configurable +* Can be invoked via inetd +* Lots of minor fixes... + +0.8.0 +----- + +* Make websockify properly terminate children on SIGTERM (#226) +* Remove logging in signal handlers (this can cause Python to hang under certain conditions) (#219) +* Make it easier to log to a file (#205) +* Add support for IPv6 addresses in tokens in the TokenFile token plugins (#197) +* Improve auth plugin framework to enable better support for HTTP auth (#194, #201) +* Fix bug in JSONTokenAPI token plugin (#192) +* Fix a missing variable in the exception handler (#178) + +0.7.0 +----- + +* Python 3 support fixes (#140, #155, #159) +* Generic token-parsing plugins support (#162) +* Generic authentication plugins support (#172) +* Fixed frame corruption on big-endian systems (#161) +* Support heartbeats (via PING) and automatic responses to PONG (#169) +* Automatically reject unmasked client frames by default (strict mode) (#174) +* Automatically restart interrupted select calls (#175) +* Make 'run' respect environment settings (including virtualenv) (#176) + +0.6.1 - May 11, 2015 +-------------------- + +* **PATCH RELEASE**: Fixes a bug causing file_only to not be passed properly + +0.6.0 - Feb 18, 2014 +-------------------- + +* **NOTE** : 0.6.0 will break existing code that sub-classes WebsocketProxy +* Refactor to use standard SocketServer RequestHandler design +* Fix zombie process bug on certain systems when using multiprocessing +* Add better unit tests +* Log information via python `logging` module + +0.5.1 - Jun 27, 2013 +-------------------- + + * use upstream einaros/ws (>=0.4.27) with websockify.js + * file_only and no_parent security options for WSRequestHandler + * Update build of web-socket-js (c0855c6cae) + * add include/web-socket-js-project submodule to gimite/web-socket-js + for DSFG compliance. + * drop Hixie protocol support + +0.4.1 - Mar 12, 2013 +-------------------- + + * ***NOTE*** : 0.5.0 will drop Hixie protocol support + * add include/ directory and remove some dev files from source + distribution. + +0.4.0 - Mar 12, 2013 +-------------------- + + * ***NOTE*** : 0.5.0 will drop Hixie protocol support + * use Buffer base64 support in Node.js implementation + +0.3.0 - Jan 15, 2013 +-------------------- + + * refactor into modules: websocket, websocketproxy + * switch to web-socket-js that uses IETF 6455 + * change to MPL 2.0 license for include/*.js + * fix session recording + +0.2.1 - Oct 15, 2012 +-------------------- + + * re-released with updated version number + +0.2.0 - Sep 17, 2012 +-------------------- + + * Binary data support in websock.js + * Target config file/dir and multiple targets with token selector + * IPv6 fixes + * SSL target support + * Proxy to/from unix socket + + +0.1.0 - May 11, 2012 +-------------------- + + * Initial versioned release. + diff --git a/systemvm/debian/root/noVNC/utils/websockify/COPYING b/systemvm/debian/root/noVNC/utils/websockify/COPYING new file mode 100644 index 000000000000..65c5ca88a67c --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/COPYING @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/systemvm/debian/root/noVNC/utils/websockify/MANIFEST.in b/systemvm/debian/root/noVNC/utils/websockify/MANIFEST.in new file mode 100644 index 000000000000..43b458ee5c9b --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/MANIFEST.in @@ -0,0 +1,2 @@ +include CHANGES.txt README.md LICENSE.txt +graft include diff --git a/systemvm/debian/root/noVNC/utils/websockify/Makefile b/systemvm/debian/root/noVNC/utils/websockify/Makefile new file mode 100644 index 000000000000..7dc1bc4f0313 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/Makefile @@ -0,0 +1,11 @@ +TARGETS=rebind.so +CFLAGS += -fPIC + +all: $(TARGETS) + +rebind.so: rebind.o + $(CC) $(LDFLAGS) $^ -shared -fPIC -ldl -o $@ + +clean: + rm -f rebind.o rebind.so + diff --git a/systemvm/debian/root/noVNC/utils/websockify/README.md b/systemvm/debian/root/noVNC/utils/websockify/README.md new file mode 100644 index 000000000000..a034b996d755 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/README.md @@ -0,0 +1,170 @@ +## websockify: WebSockets support for any application/server + +websockify was formerly named wsproxy and was part of the +[noVNC](https://github.com/novnc/noVNC) project. + +At the most basic level, websockify just translates WebSockets traffic +to normal socket traffic. Websockify accepts the WebSockets handshake, +parses it, and then begins forwarding traffic between the client and +the target in both directions. + +### News/help/contact + +Notable commits, announcements and news are posted to +@noVNC + +If you are a websockify developer/integrator/user (or want to be) +please join the noVNC/websockify +discussion group + +Bugs and feature requests can be submitted via [github +issues](https://github.com/novnc/websockify/issues). + +If you want to show appreciation for websockify you could donate to a great +non-profits such as: [Compassion +International](http://www.compassion.com/), [SIL](http://www.sil.org), +[Habitat for Humanity](http://www.habitat.org), [Electronic Frontier +Foundation](https://www.eff.org/), [Against Malaria +Foundation](http://www.againstmalaria.com/), [Nothing But +Nets](http://www.nothingbutnets.net/), etc. Please tweet @noVNC if you do. + +### WebSockets binary data + +Starting with websockify 0.5.0, only the HyBi / IETF +6455 WebSocket protocol is supported. There is no support for the older +Base64 encoded data format. + + +### Encrypted WebSocket connections (wss://) + +To encrypt the traffic using the WebSocket 'wss://' URI scheme you need to +generate a certificate and key for Websockify to load. By default, Websockify +loads a certificate file name `self.pem` but the `--cert=CERT` and `--key=KEY` +options can override the file name. You can generate a self-signed certificate +using openssl. When asked for the common name, use the hostname of the server +where the proxy will be running: + +``` +openssl req -new -x509 -days 365 -nodes -out self.pem -keyout self.pem +``` + +For a self-signed certificate to work, you need to make your client/browser +understand it. You can do this by installing it as accepted certificate, or by +using that same certificate for a HTTPS connection to which you navigate first +and approve. Browsers generally don't give you the "trust certificate?" prompt +by opening a WSS socket with invalid certificate, hence you need to have it +acccept it by either of those two methods. + +If you have a commercial/valid SSL certificate with one ore more intermediate +certificates, concat them into one file, server certificate first, then the +intermediate(s) from the CA, etc. Point to this file with the `--cert` option +and then also to the key with `--key`. Finally, use `--ssl-only` as needed. + + +### Additional websockify features + +These are not necessary for the basic operation. + +* Daemonizing: When the `-D` option is specified, websockify runs + in the background as a daemon process. + +* SSL (the wss:// WebSockets URI): This is detected automatically by + websockify by sniffing the first byte sent from the client and then + wrapping the socket if the data starts with '\x16' or '\x80' + (indicating SSL). + +* Session recording: This feature that allows recording of the traffic + sent and received from the client to a file using the `--record` + option. + +* Mini-webserver: websockify can detect and respond to normal web + requests on the same port as the WebSockets proxy. This functionality + is activated with the `--web DIR` option where DIR is the root of the + web directory to serve. + +* Wrap a program: see the "Wrap a Program" section below. + +* Log files: websockify can save all logging information in a file. + This functionality is activated with the `--log-file FILE` option + where FILE is the file where the logs should be saved. + +* Authentication plugins: websockify can demand authentication for + websocket connections and, if you use `--web-auth`, also for normal + web requests. This functionality is activated with the + `--auth-plugin CLASS` and `--auth-source ARG` options, where CLASS is + usually one from auth_plugins.py and ARG is the plugin's configuration. + +* Token plugins: a single instance of websockify can connect clients to + multiple different pre-configured targets, depending on the token sent + by the client using the `token` URL parameter, or the hostname used to + reach websockify, if you use `--host-token`. This functionality is + activated with the `--token-plugin CLASS` and `--token-source ARG` + options, where CLASS is usually one from token_plugins.py and ARG is + the plugin's configuration. + +### Other implementations of websockify + +The primary implementation of websockify is in python. There are +several alternate implementations in other languages available in +our sister repositories [websockify-js](https://github.com/novnc/websockify-js) +(JavaScript/Node.js) and [websockify-other](https://github.com/novnc/websockify-other) + (C, Clojure, Ruby). + +In addition there are several other external projects that implement +the websockify "protocol". See the alternate implementation [Feature +Matrix](https://github.com/novnc/websockify/wiki/Feature_Matrix) for +more information. + + +### Wrap a Program + +In addition to proxying from a source address to a target address +(which may be on a different system), websockify has the ability to +launch a program on the local system and proxy WebSockets traffic to +a normal TCP port owned/bound by the program. + +The is accomplished with a small LD_PRELOAD library (`rebind.so`) +which intercepts bind() system calls by the program. The specified +port is moved to a new localhost/loopback free high port. websockify +then proxies WebSockets traffic directed to the original port to the +new (moved) port of the program. + +The program wrap mode is invoked by replacing the target with `--` +followed by the program command line to wrap. + + `./run 2023 -- PROGRAM ARGS` + +The `--wrap-mode` option can be used to indicate what action to take +when the wrapped program exits or daemonizes. + +Here is an example of using websockify to wrap the vncserver command +(which backgrounds itself) for use with +[noVNC](https://github.com/novnc/noVNC): + + `./run 5901 --wrap-mode=ignore -- vncserver -geometry 1024x768 :1` + +Here is an example of wrapping telnetd (from krb5-telnetd). telnetd +exits after the connection closes so the wrap mode is set to respawn +the command: + + `sudo ./run 2023 --wrap-mode=respawn -- telnetd -debug 2023` + +The `wstelnet.html` page in the [websockify-js](https://github.com/novnc/websockify-js) +project demonstrates a simple WebSockets based telnet client (use +'localhost' and '2023' for the host and port respectively). + + +### Installing websockify + +Download one of the releases or the latest development version, extract +it and run `python setup.py install` as root in the directory where you +extracted the files. Normally, this will also install numpy for better +performance, if you don't have it installed already. However, numpy is +optional. If you don't want to install numpy or if you can't compile it, +you can edit setup.py and remove the `install_requires=['numpy'],` line +before running `python setup.py install`. + +Afterwards, websockify should be available in your path. Run +`websockify --help` to confirm it's installed correctly. diff --git a/systemvm/debian/root/noVNC/utils/websockify/Windows/Windows Service Readme.txt b/systemvm/debian/root/noVNC/utils/websockify/Windows/Windows Service Readme.txt new file mode 100644 index 000000000000..2ebd11e2bd8a --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/Windows/Windows Service Readme.txt @@ -0,0 +1,39 @@ +----------------------------------- +Windows noVNC Websockify Service +----------------------------------- + +The "noVNC Websocket Service.exe" file is a windows service wrapper created with Visual Studio 2010 to create a windows service to start stop the noVNC Websocket Server. All files used to create the wrapper can be found in 'noVNC Websocket Service Project' folder. + +To download the precompiled executables please grab the zip in the downloads section of websockify project: +https://github.com/novnc/websockify + +--------------------------- +Installation +--------------------------- + +1. This service requires websockify.exe be in the same directory. Instructions on how to compile websockify python script as a windows executable can be found here: +https://github.com/novnc/websockify/wiki/Compiling-Websockify-as-Windows-Executable + +2.To add this service to a Windows PC you need to run the commandline as administrator and then run this line: + +sc create "noVNC Websocket Server" binPath= "PATH TO noVNC eg C:\noVNC\utils\Windows\Websocket Service.exe" DisplayName= "noVNC Websocket Server" + +3 .Once this is run you will be able to access the service via Control Panel > Admin Tools > Services. In here you can specify whether you want the service to run automatically and start at stop the service. + +--------------------------- +Configuration +--------------------------- +The file noVNCConfig.ini must be in the same directory as "noVNC Websocket Service.exe". + +This file contains a single line which is the websockify.exe statup arguements. An example is: +192.168.0.1:5901 192.168.0.1:5900 + +All websockify supported arguements will work if added here. + +--------------------------- +Deletion +--------------------------- + +You can delete the service at any time by running the commandline as admin and using this command: +sc delete "noVNC Websocket Server". + diff --git a/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/Program.cs b/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/Program.cs new file mode 100644 index 000000000000..2d07c330045e --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/Program.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.ServiceProcess; +using System.Text; + +namespace MELT_Command_Websocket +{ + static class Program + { + /// + /// The main entry point for the application. + /// + static void Main() + { + ServiceBase[] ServicesToRun; + ServicesToRun = new ServiceBase[] + { + new Service1() + }; + ServiceBase.Run(ServicesToRun); + } + } +} diff --git a/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/ProjectInstaller.Designer.cs b/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/ProjectInstaller.Designer.cs new file mode 100644 index 000000000000..3dab5bb7515a --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/ProjectInstaller.Designer.cs @@ -0,0 +1,61 @@ +namespace MELT_Command_Websocket +{ + partial class ProjectInstaller + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.serviceProcessInstaller1 = new System.ServiceProcess.ServiceProcessInstaller(); + this.serviceInstaller1 = new System.ServiceProcess.ServiceInstaller(); + // + // serviceProcessInstaller1 + // + this.serviceProcessInstaller1.Account = System.ServiceProcess.ServiceAccount.NetworkService; + this.serviceProcessInstaller1.Installers.AddRange(new System.Configuration.Install.Installer[] { + this.serviceInstaller1}); + this.serviceProcessInstaller1.Password = null; + this.serviceProcessInstaller1.Username = null; + // + // serviceInstaller1 + // + this.serviceInstaller1.Description = "noVNC Websocket Service"; + this.serviceInstaller1.DisplayName = "noVNC Websocket Service"; + this.serviceInstaller1.ServiceName = "noVNC Websocket Service"; + this.serviceInstaller1.StartType = System.ServiceProcess.ServiceStartMode.Automatic; + // + // ProjectInstaller + // + this.Installers.AddRange(new System.Configuration.Install.Installer[] { + this.serviceProcessInstaller1}); + + } + + #endregion + + private System.ServiceProcess.ServiceProcessInstaller serviceProcessInstaller1; + private System.ServiceProcess.ServiceInstaller serviceInstaller1; + } +} \ No newline at end of file diff --git a/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/ProjectInstaller.cs b/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/ProjectInstaller.cs new file mode 100644 index 000000000000..783bb15b0901 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/ProjectInstaller.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Configuration.Install; +using System.Linq; + + +namespace MELT_Command_Websocket +{ + [RunInstaller(true)] + public partial class ProjectInstaller : System.Configuration.Install.Installer + { + public ProjectInstaller() + { + InitializeComponent(); + } + } +} diff --git a/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/ProjectInstaller.resx b/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/ProjectInstaller.resx new file mode 100644 index 000000000000..fbda1aecc7a3 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/ProjectInstaller.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 56 + + + 196, 17 + + + False + + \ No newline at end of file diff --git a/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/Properties/AssemblyInfo.cs b/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/Properties/AssemblyInfo.cs new file mode 100644 index 000000000000..832319eed1c8 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("MELT Command Websocket")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("MELT Command Websocket")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2011")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("5ab831cb-6852-4ce1-849c-b26725b0e10b")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/Service1.Designer.cs b/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/Service1.Designer.cs new file mode 100644 index 000000000000..c569eca84724 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/Service1.Designer.cs @@ -0,0 +1,37 @@ +namespace MELT_Command_Websocket +{ + partial class Service1 + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + this.ServiceName = "Service1"; + } + + #endregion + } +} diff --git a/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/Service1.cs b/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/Service1.cs new file mode 100644 index 000000000000..ce94bf6ba0bc --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/Service1.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Diagnostics; +using System.Linq; +using System.ServiceProcess; +using System.Text; +using System.IO; + +namespace MELT_Command_Websocket +{ + public partial class Service1 : ServiceBase + { + Process websockify; + public Service1() + { + InitializeComponent(); + } + + protected override void OnStart(string[] args) + { + + string configpath = AppDomain.CurrentDomain.BaseDirectory + "\\noVNCConfig.ini"; + string sockifypath = AppDomain.CurrentDomain.BaseDirectory + "\\websockify.exe"; + //Load commandline arguements from config file. + StreamReader streamReader = new StreamReader(configpath); + string arguements = streamReader.ReadLine(); + streamReader.Close(); + + //Start websockify. + websockify = System.Diagnostics.Process.Start(sockifypath, arguements); + } + + protected override void OnStop() + { + //Service stopped. Close websockify. + websockify.Kill(); + } + } +} diff --git a/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/noVNC Websocket.csproj b/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/noVNC Websocket.csproj new file mode 100644 index 000000000000..368d18399eaa --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/noVNC Websocket.csproj @@ -0,0 +1,75 @@ + + + + Debug + x86 + 8.0.30703 + 2.0 + {6B86AE7B-6BBD-4E74-8802-5995E8B6D27D} + WinExe + Properties + noVNC_Websocket_Service + noVNC Websocket Service + v3.5 + 512 + + + x86 + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + x86 + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + Component + + + ProjectInstaller.cs + + + Component + + + Service1.cs + + + + + + + ProjectInstaller.cs + + + + + \ No newline at end of file diff --git a/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/noVNC Websocket.sln b/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/noVNC Websocket.sln new file mode 100644 index 000000000000..140de1be5e26 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/Windows/noVNC Websocket Service Project/noVNC Websocket.sln @@ -0,0 +1,20 @@ + +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "noVNC Websocket", "noVNC Websocket.csproj", "{6B86AE7B-6BBD-4E74-8802-5995E8B6D27D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x86 = Debug|x86 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6B86AE7B-6BBD-4E74-8802-5995E8B6D27D}.Debug|x86.ActiveCfg = Debug|x86 + {6B86AE7B-6BBD-4E74-8802-5995E8B6D27D}.Debug|x86.Build.0 = Debug|x86 + {6B86AE7B-6BBD-4E74-8802-5995E8B6D27D}.Release|x86.ActiveCfg = Release|x86 + {6B86AE7B-6BBD-4E74-8802-5995E8B6D27D}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/systemvm/debian/root/noVNC/utils/websockify/docs/latency_results.txt b/systemvm/debian/root/noVNC/utils/websockify/docs/latency_results.txt new file mode 100644 index 000000000000..454a590e8468 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/docs/latency_results.txt @@ -0,0 +1,114 @@ +This data is raw copy from the latency tester set to send a frame with +a little over 2000 KB of data every 10ms. + +The number of packets sent and received is just a visual counter and +is just the total when I chose to stop the test (around 3000 or so +packets). + +The latency measure are from the point the packet was sent to when it +was received back again in milliseconds. One notable data point +missing from this is how long it actually took for the client to send +3000 packets because sending large packets can put load on the browser +and it may be a lot longer than 10ms before the timer event to +send the next packet fires. So even with low latency numbers, the +actual send rate may be fairly low because sending the WebSockets +frames is impacting the performance of the browser in general. + + +------------------------------------------------------------ + +Native WebSockets implementations, 2000 byte payload, 10ms delay + +Chrome 8.0.552 - native WebSockets + Packets sent: 2998 + Packets Received: 2998 + Average Latency: 1.84 + 40 Frame Running Average Latency: 1.90 + Minimum Latency: 1.00 + Maximum Latency: 10.00 + +firefox 4.0b9 - WebSockets enabled + Packets sent: 3011 + Packets Received: 3011 + Average Latency: 6.45 + 40 Frame Running Average Latency: 6.08 + Minimum Latency: 5.00 + Maximum Latency: 119.00 + +Opera 11 - WebSockets enabled + Packets sent: 3065 + Packets Received: 3064 + Average Latency: 9.56 + 40 Frame Running Average Latency: 8.15 + Minimum Latency: 4.00 + Maximum Latency: 53.00 + +------------------------------------------------------------ + +New web-socket-js (20f837425d4), 2000 byte payload, 10ms delay + +firefox 4.0b9 - no WebSockets + Packets sent: 3088 + Packets Received: 3087 + Average Latency: 16.71 + 40 Frame Running Average Latency: 16.80 + Minimum Latency: 7.00 + Maximum Latency: 75.00 + + - First 1000 sent in 13 seconds + - Second 1000 sent in 12 seconds + - Third 1000 sent in 12 seconds + +firefox 3.6.10 - no WebSockets + Packets sent: 3100 + Packets Received: 3099 + Average Latency: 17.32 + 40 Frame Running Average Latency: 16.73 + Minimum Latency: 6.00 + Maximum Latency: 72.00 + +Opera 11 - no WebSockets + Packets sent: 3007 + Packets Received: 3007 + Average Latency: 465.91 + 40 Frame Running Average Latency: 147.95 + Minimum Latency: 12.00 + Maximum Latency: 9143.00 + + - average starts at around 28ms + - time for each 500 packets: 13s, 16s, 25s, 37s, 50s, 72s + - also start seeing sent, receive lags around 1200 packets + +--------------------------------------------------------------- + +Old web-socket-js (9e7663771), 2000 byte payload, 10ms delay + +firefox 4.0b9 - no WebSockets + Packets sent: 3024 + Packets Received: 3020 + Average Latency: 80.59 + 40 Frame Running Average Latency: 60.15 + Minimum Latency: 10.00 + Maximm Latency: 348.00 + + +firefox 3.6.10 - no WebSockets + Packets sent: 2777 + Packets Received: 2775 + Average Latency: 34.89 + 40 Frame Running Average Latency: 24.50 + Minimum Latency: 10.00 + Maximum Latency: 208.00 + + +Opera 11 - no Websockets + Packets sent: 3012 + Packets Received: 3011 + Average Latency: 380.87 + 40 Frame Running Average Latency: 341.90 + Minimum Latency: 28.00 + Maximum Latency: 2175.00 + + - average starts at around 290ms + - time for each 1000 packets: 23s, 38s, 65s + diff --git a/systemvm/debian/root/noVNC/utils/websockify/docs/notes b/systemvm/debian/root/noVNC/utils/websockify/docs/notes new file mode 100644 index 000000000000..edb6b1d79679 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/docs/notes @@ -0,0 +1,6 @@ +Building release tarball: + - not really necessary since tagged revision can be downloaded + from github as tarballs + + git archive --format=tar --prefix=websockify-${WVER}/ v${WVER} > websockify-${WVER}.tar + gzip websockify-${WVER}.tar diff --git a/systemvm/debian/root/noVNC/utils/websockify/docs/release.txt b/systemvm/debian/root/noVNC/utils/websockify/docs/release.txt new file mode 100644 index 000000000000..813af027aba1 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/docs/release.txt @@ -0,0 +1,10 @@ +- Update setup.py and CHANGES.txt and commit +- Create version tag and tarball from tag + WVER=0.1.0 + git tag v${WVER} + git push origin master + git push origin v${WVER} +- Create the source distribution + python3 setup.py sdist +- Upload the source distribution + python3 -m twine upload dist/websockify-${WVER}.tar.gz diff --git a/systemvm/debian/root/noVNC/utils/websockify/docs/websockify.1 b/systemvm/debian/root/noVNC/utils/websockify/docs/websockify.1 new file mode 100644 index 000000000000..f2aa988ccc10 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/docs/websockify.1 @@ -0,0 +1,110 @@ +.TH websockify 1 "June 7, 2012" "version 0.3" "USER COMMANDS" + +.SH NAME + +websockify - WebSockets to TCP socket bridge + +.SH SYNOPSIS + + websockify [options] [source_addr:]source_port target_addr:target_port + websockify [options] [source_addr:]source_port \-\- WRAP_COMMAND_LINE + +.SH OPTIONS + + -h, --help show this help message and exit + -v, --verbose verbose messages and per frame traffic + --record=FILE record sessions to FILE.[session_number] + -D, --daemon become a daemon (background process) + --run-once handle a single WebSocket connection and exit + --timeout=TIMEOUT after TIMEOUT seconds exit when not connected + --cert=CERT SSL certificate file + --key=KEY SSL key file (if separate from cert) + --ssl-only disallow non-encrypted connections + --web=DIR run webserver on same port. Serve files from DIR. + --wrap-mode=MODE action to take when the wrapped program exits or + daemonizes: exit (default), ignore, respawn + +.SH DESCRIPTION + +At the most basic level, websockify just translates WebSockets traffic to normal TCP socket traffic. Websockify accepts the WebSockets handshake, parses it, and then begins forwarding traffic between the client and the target in both directions. + +websockify was formerly named wsproxy and was part of the noVNC project. + +.SH NOTES + +.SS WebSockets binary data + +Websockify supports all versions of the WebSockets protocol (Hixie and HyBI). The older Hixie versions of the protocol only support UTF-8 text payloads. In order to transport binary data over UTF-8 an encoding must used to encapsulate the data within UTF-8. Websockify uses base64 to encode all traffic to and from the client. This does not affect the data between websockify and the server. + +.SS Encrypted WebSocket connections (wss://) + +To encrypt the traffic using the WebSocket 'wss://' URI scheme you need to generate a certificate for websockify to load. By default websockify loads a certificate file name self.pem but the --cert=CERT option can override the file name. You can generate a self-signed certificate using openssl. When asked for the common name, use the hostname of the server where the proxy will be running: + +openssl req -new -x509 -days 365 -nodes -out self.pem -keyout self.pem + +.SS Additional websockify features + +These are not necessary for the basic operation. + +.IP * +Daemonizing: When the -D option is specified, websockify runs in the background as a daemon process. + +.IP * +SSL (the wss:// WebSockets URI): This is detected automatically by websockify by sniffing the first byte sent from the client and then wrapping the socket if the data starts with '\\x16' or '\\x80' (indicating SSL). + +.IP * +Session recording: This feature that allows recording of the traffic sent and received from the client to a file using the --record option. + +.IP * +Mini-webserver: websockify can detect and respond to normal web requests on the same port as the WebSockets proxy. This functionality is activate with the --web DIR option where DIR is the root of the web directory to serve. + +.IP * +Wrap a program: see the "Wrap a Program" section below. + +.SS Wrap a Program + +In addition to proxying from a source address to a target address (which may be on a different system), websockify has the ability to launch a program on the local system and proxy WebSockets traffic to a normal TCP port owned/bound by the program. + +The is accomplished with a small LD_PRELOAD library (rebind.so) which intercepts bind() system calls by the program. The specified port is moved to a new localhost/loopback free high port. websockify then proxies WebSockets traffic directed to the original port to the new (moved) port of the program. + +The program wrap mode is invoked by replacing the target with -- followed by the program command line to wrap. + +`./websockify 2023 -- PROGRAM ARGS` + +The --wrap-mode option can be used to indicate what action to take when the wrapped program exits or daemonizes. + +Here is an example of using websockify to wrap the vncserver command (which backgrounds itself) for use with noVNC: + +`./websockify 5901 --wrap-mode=ignore -- vncserver -geometry 1024x768 :1` + +Here is an example of wrapping telnetd (from krb5-telnetd). telnetd exits after the connection closes so the wrap mode is set to respawn the command: + +`sudo ./websockify 2023 --wrap-mode=respawn -- telnetd -debug 2023` + +The wstelnet.html page demonstrates a simple WebSockets based telnet client. + +.SS Use client certificate verification + +This feature requires Python 2.7.9 or newer or Python 3.4 or newer. + +The --verify-client option makes the server ask the client for a SSL certificate. Presenting a valid (not expired and trusted by any supplied certificate authority) certificate is required for the client connection. With -auth-plugin=ClientCertCNAuth, the client certificate can be checked against a list of authorised certificate users. Non-encrypted connection attempts always fail during authentication. + +Here is an example of a vncsevrer with password-less, certificate-driven authentication: + +`./websockify 5901 --cert=fullchain.pem --key=privkey.pem --ssl-only --verify-client --cafile=ca-certificates.crt --auth-plugin=ClientCertCNAuth --auth-source='jane@example.com Joe User9824510' --web=noVNC/ --wrap-mode=ignore -- vncserver :1 -geometry 1024x768 -SecurityTypes=None` + +The --auth-source option takes a white-space separated list of common names. Depending on your clients certificates they can be verified email addresses, user-names or any other string used for identification. + +The --cafile option selects a file containing concatenated certificates of authorities trusted for validating clients. If this option is omitted, system default list of CAs is used. Upon connect, the client should supply the whole certificate chain. If your clients are known not to send intermediate certificates, they can be appended to the ca-file as well. + +Note: Most browsers ask the user to select a certificate only while connecting via HTTPS, not WebSockets. Connecting directly to the SSL secured WebSocket may cause the browser to abort the connection. If you want to connect via noVNC, the --web option should point to a copy of noVNC, so it is loaded from the same host. + +.SH AUTHOR +Joel Martin (github@martintribe.org) + +.SH SEE ALSO + +https://github.com/novnc/websockify/ + +https://github.com/novnc/websockify/wiki/ + diff --git a/systemvm/debian/root/noVNC/utils/websockify/rebind b/systemvm/debian/root/noVNC/utils/websockify/rebind new file mode 100755 index 000000000000..2289aaa4dd3a --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/rebind @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +usage() { + echo "Usage: $(basename $0) OLD_PORT NEW_PORT COMMAND_LINE" + echo + echo "Launch COMMAND_LINE, but intercept system calls to bind" + echo "to OLD_PORT and instead bind them to localhost:NEW_PORT" + exit 2 +} + +# Parameter defaults +mydir=$(readlink -f $(dirname ${0})) + +export REBIND_PORT_OLD="${1}"; shift +export REBIND_PORT_NEW="${1}"; shift + +LD_PRELOAD=${mydir}/rebind.so "${@}" + diff --git a/systemvm/debian/root/noVNC/utils/websockify/rebind.c b/systemvm/debian/root/noVNC/utils/websockify/rebind.c new file mode 100644 index 000000000000..811031c6042e --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/rebind.c @@ -0,0 +1,94 @@ +/* + * rebind: Intercept bind calls and bind to a different port + * Copyright 2010 Joel Martin + * Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3) + * + * Overload (LD_PRELOAD) bind system call. If REBIND_PORT_OLD and + * REBIND_PORT_NEW environment variables are set then bind on the new + * port (of localhost) instead of the old port. + * + * This allows a bridge/proxy (such as websockify) to run on the old port and + * translate traffic to/from the new port. + * + * Usage: + * LD_PRELOAD=./rebind.so \ + * REBIND_PORT_OLD=23 \ + * REBIND_PORT_NEW=2023 \ + * program + */ + +//#define DO_DEBUG 1 + +#include +#include + +#define __USE_GNU 1 // Pull in RTLD_NEXT +#include + +#include +#include + + +#if defined(DO_DEBUG) +#define DEBUG(...) \ + fprintf(stderr, "rebind: "); \ + fprintf(stderr, __VA_ARGS__); +#else +#define DEBUG(...) +#endif + + +int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) +{ + static void * (*func)(); + int do_move = 0; + struct sockaddr_in * addr_in = (struct sockaddr_in *)addr; + struct sockaddr_in addr_tmp; + socklen_t addrlen_tmp; + char * PORT_OLD, * PORT_NEW, * end1, * end2; + int ret, oldport, newport, askport = htons(addr_in->sin_port); + uint32_t askaddr = htons(addr_in->sin_addr.s_addr); + if (!func) func = (void *(*)()) dlsym(RTLD_NEXT, "bind"); + + DEBUG(">> bind(%d, _, %d), askaddr %d, askport %d\n", + sockfd, addrlen, askaddr, askport); + + /* Determine if we should move this socket */ + if (addr_in->sin_family == AF_INET) { + // TODO: support IPv6 + PORT_OLD = getenv("REBIND_OLD_PORT"); + PORT_NEW = getenv("REBIND_NEW_PORT"); + if (PORT_OLD && (*PORT_OLD != '\0') && + PORT_NEW && (*PORT_NEW != '\0')) { + oldport = strtol(PORT_OLD, &end1, 10); + newport = strtol(PORT_NEW, &end2, 10); + if (oldport && (*end1 == '\0') && + newport && (*end2 == '\0') && + (oldport == askport)) { + do_move = 1; + } + } + } + + if (! do_move) { + /* Just pass everything right through to the real bind */ + ret = (long) func(sockfd, addr, addrlen); + DEBUG("<< bind(%d, _, %d) ret %d\n", sockfd, addrlen, ret); + return ret; + } + + DEBUG("binding fd %d on localhost:%d instead of 0x%x:%d\n", + sockfd, newport, ntohl(addr_in->sin_addr.s_addr), oldport); + + /* Use a temporary location for the new address information */ + addrlen_tmp = sizeof(addr_tmp); + memcpy(&addr_tmp, addr, addrlen_tmp); + + /* Bind to other port on the loopback instead */ + addr_tmp.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr_tmp.sin_port = htons(newport); + ret = (long) func(sockfd, &addr_tmp, addrlen_tmp); + + DEBUG("<< bind(%d, _, %d) ret %d\n", sockfd, addrlen, ret); + return ret; +} diff --git a/systemvm/debian/root/noVNC/utils/websockify/run b/systemvm/debian/root/noVNC/utils/websockify/run new file mode 100755 index 000000000000..3dfa63f86a8b --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/run @@ -0,0 +1,7 @@ +#!/usr/bin/env sh + +BASE_DIR="$(cd $(dirname "$0"); pwd)" + +cd "$BASE_DIR" + +python -m websockify $@ diff --git a/systemvm/debian/root/noVNC/utils/websockify/setup.py b/systemvm/debian/root/noVNC/utils/websockify/setup.py new file mode 100644 index 000000000000..b487172be89f --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/setup.py @@ -0,0 +1,37 @@ +from setuptools import setup, find_packages + +version = '0.9.0' +name = 'websockify' +long_description = open("README.md").read() + "\n" + \ + open("CHANGES.txt").read() + "\n" + +setup(name=name, + version=version, + description="Websockify.", + long_description=long_description, + long_description_content_type="text/markdown", + classifiers=[ + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4" + ], + keywords='noVNC websockify', + license='LGPLv3', + url="https://github.com/novnc/websockify", + author="Joel Martin", + author_email="github@martintribe.org", + + packages=['websockify'], + include_package_data=True, + install_requires=['numpy'], + zip_safe=False, + entry_points={ + 'console_scripts': [ + 'websockify = websockify.websocketproxy:websockify_init', + ] + }, + ) diff --git a/systemvm/debian/root/noVNC/utils/websockify/test-requirements.txt b/systemvm/debian/root/noVNC/utils/websockify/test-requirements.txt new file mode 100644 index 000000000000..22027e677e1e --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/test-requirements.txt @@ -0,0 +1,6 @@ +mox3 +nose +jwcrypto;python_version>="2.7" +enum34;python_version=="3.3" +redis;python_version>="2.7" +simplejson;python_version>="2.7" diff --git a/systemvm/debian/root/noVNC/utils/websockify/tests/echo.html b/systemvm/debian/root/noVNC/utils/websockify/tests/echo.html new file mode 100644 index 000000000000..0e300b586fc2 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/tests/echo.html @@ -0,0 +1,139 @@ + + + + WebSockets Echo Test + + + + + Host:   + Port:   + Encrypt:   +   + + +
+ Log:
+ + + + + + + diff --git a/systemvm/debian/root/noVNC/utils/websockify/tests/echo.py b/systemvm/debian/root/noVNC/utils/websockify/tests/echo.py new file mode 100755 index 000000000000..780891cddda9 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/tests/echo.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python + +''' +A WebSocket server that echos back whatever it receives from the client. +Copyright 2010 Joel Martin +Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3) + +You can make a cert/key with openssl using: +openssl req -new -x509 -days 365 -nodes -out self.pem -keyout self.pem +as taken from http://docs.python.org/dev/library/ssl.html#certificates +''' + +import os, sys, select, optparse, logging +sys.path.insert(0,os.path.join(os.path.dirname(__file__), "..")) +from websockify.websockifyserver import WebSockifyServer, WebSockifyRequestHandler + +class WebSocketEcho(WebSockifyRequestHandler): + """ + WebSockets server that echos back whatever is received from the + client. """ + buffer_size = 8096 + + def new_websocket_client(self): + """ + Echo back whatever is received. + """ + + cqueue = [] + c_pend = 0 + cpartial = "" + rlist = [self.request] + + while True: + wlist = [] + + if cqueue or c_pend: wlist.append(self.request) + ins, outs, excepts = select.select(rlist, wlist, [], 1) + if excepts: raise Exception("Socket exception") + + if self.request in outs: + # Send queued target data to the client + c_pend = self.send_frames(cqueue) + cqueue = [] + + if self.request in ins: + # Receive client data, decode it, and send it back + frames, closed = self.recv_frames() + cqueue.extend(frames) + + if closed: + break + +if __name__ == '__main__': + parser = optparse.OptionParser(usage="%prog [options] listen_port") + parser.add_option("--verbose", "-v", action="store_true", + help="verbose messages and per frame traffic") + parser.add_option("--cert", default="self.pem", + help="SSL certificate file") + parser.add_option("--key", default=None, + help="SSL key file (if separate from cert)") + parser.add_option("--ssl-only", action="store_true", + help="disallow non-encrypted connections") + (opts, args) = parser.parse_args() + + try: + if len(args) != 1: raise ValueError + opts.listen_port = int(args[0]) + except ValueError: + parser.error("Invalid arguments") + + logging.basicConfig(level=logging.INFO) + + opts.web = "." + server = WebSockifyServer(WebSocketEcho, **opts.__dict__) + server.start_server() + diff --git a/systemvm/debian/root/noVNC/utils/websockify/tests/echo_client.py b/systemvm/debian/root/noVNC/utils/websockify/tests/echo_client.py new file mode 100755 index 000000000000..4f238f6f4e1d --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/tests/echo_client.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python + +import os +import sys +import optparse +import select + +sys.path.insert(0,os.path.join(os.path.dirname(__file__), "..")) +from websockify.websocket import WebSocket, \ + WebSocketWantReadError, WebSocketWantWriteError + +parser = optparse.OptionParser(usage="%prog URL") +(opts, args) = parser.parse_args() + +if len(args) == 1: + URL = args[0] +else: + parser.error("Invalid arguments") + +sock = WebSocket() +print("Connecting to %s..." % URL) +sock.connect(URL) +print("Connected.") + +def send(msg): + while True: + try: + sock.sendmsg(msg) + break + except WebSocketWantReadError: + msg = '' + ins, outs, excepts = select.select([sock], [], []) + if excepts: raise Exception("Socket exception") + except WebSocketWantWriteError: + msg = '' + ins, outs, excepts = select.select([], [sock], []) + if excepts: raise Exception("Socket exception") + +def read(): + while True: + try: + return sock.recvmsg() + except WebSocketWantReadError: + ins, outs, excepts = select.select([sock], [], []) + if excepts: raise Exception("Socket exception") + except WebSocketWantWriteError: + ins, outs, excepts = select.select([], [sock], []) + if excepts: raise Exception("Socket exception") + +counter = 1 +while True: + msg = "Message #%d" % counter + counter += 1 + send(msg) + print("Sent message: %r" % msg) + + while True: + ins, outs, excepts = select.select([sock], [], [], 1.0) + if excepts: raise Exception("Socket exception") + + if ins == []: + break + + while True: + msg = read() + print("Received message: %r" % msg) + + if not sock.pending(): + break diff --git a/systemvm/debian/root/noVNC/utils/websockify/tests/fixtures/private.pem b/systemvm/debian/root/noVNC/utils/websockify/tests/fixtures/private.pem new file mode 100644 index 000000000000..413cee25396e --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/tests/fixtures/private.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEArwNQal2INbSfoVT50dZ0s8lQ+yMhu45TDc91iuwtDjlFBJ50 +E4m3/M6ESBW0S7UTP1bIOGkd/M+u38h0Aruo4qkngdguu9N3BnsU2kOeicdjxc+v +tqRc7/kbkTdT4SrpG8EFP6T2U9U1gtBpLnau02gPrzjaQzyYDLGOBq+Ozt/mN0YJ +UhJ3hlwi18dNKreTzWgJ6mmXQWS0eAmHx8TIs2Nz9x3EfRo9CIMuaaeUjRogIEg5 +Tg4xC00ZtDO0/EfgpFkeHJGVQA2DgdBJsr6rq69MjhMfFxRJItxJMJzP6an2HkJ8 +onUPBtjEBmk3/fnfiaflzRyEb5zdii6r2TD8TwIDAQABAoIBAGDrzu742WQUoYqx +CqDAyWR/is9px1adHTW6vHexD8qewLAsKFBhpnjkzbE2A+EhaIVdRAipfifxxAC+ +fDC/SGouD2kDFe6Cz5nRM90kMXpP59s2hzL4l1d2d2PWZid+ohXysTtr2dbXbokB +bh6DL5J4QKdjLsypk/MDqYneU5IQ1k9ezWzcRgM8/V3M+t+1dLRFLIWsSLbNUgbF +px81efNw8E0voV/d7kZ+6RwUThPHqR0eyLm6djPwHE7/FarZIx4AImwV+9ex44CH +OkrTFOVYenF6jEtYoUuqYCouaWtG7jNVM/f1fksoR8SD6PTq2vn7F4wTLXG1b+K7 +45PKMhECgYEA22NH8mK9ICFVd7S6caeAnki+K9cpwmiOUWEIuDInmPn5FOlv7awE +uBFN86v14PqDBtF7Aagyib0NUPo7rIw5+V5SCBZv8gQatjZUkT0vZvpxPU5jmB++ +w58yfK7zgdAWCepLxIPyTA7CAT1dmiVmuosz2pJjbo4fecVG222IE10CgYEAzDg+ +RVlvMYGy04UMmUoUNeeRlW6km/W6dqQ7EtcxfDv4O7boRDTBSRBzfIsRdXHZhcHN +gCeB2Uiz8IO3s0Yt0+y/6cTI60uJ4S7Mb2JvWJvDCKWhS3pE1BL+LJJC4Hn7khJH +yHYFOLOfnuCbOs8VA7IMmbdTPHirIKWTT5j5H5sCgYEAygK/KweUUlOfWVyHGUQ9 +gIJG6iNzhlm0QmbxGnrET25N1t2kfNsadUsp1igPfhvuLocRltMDxiTYcCoabKWq +dF5PdrcCWX1CA2o/sIUAcvhE8UiPGHKSu5qJaJnIC05KHNMq9UbyAurL5UxWNiwe +TcMD+k01VYV0ojHvLvnKhNkCgYArkoh+xXE7D+A2zzl771lWkvz19DB88jYBoFLW +V0HArw7str7h5pui2ja5yPZFp6/woQQWptdGpAN4erIUNxIKGIZt+0WfJnPZruGB +lnAJaNp5GtXKQ+ExmofOvLo2KPCrHulf9QZyLakN/gBA0PQ74J5docbJrTld8tX2 +cr4cpwKBgHqr2zybmywAmjn8wY0bUjRAyhdN8eiwYaGPtOSFt6IcWxEnNbAo5Jc2 +KsywpagjFsXZsi4Obn2XsqR7VX5bNbpNXIyLaMwBOy7MixyecgPF8tu7I4zo/CWm +7gewTKBhwVPTDAOzHqIpJGrOnUgzJM3ijkCWMn3eAh4ccOjsrKq9 +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/systemvm/debian/root/noVNC/utils/websockify/tests/fixtures/public.pem b/systemvm/debian/root/noVNC/utils/websockify/tests/fixtures/public.pem new file mode 100644 index 000000000000..7b1d284335d9 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/tests/fixtures/public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwNQal2INbSfoVT50dZ0 +s8lQ+yMhu45TDc91iuwtDjlFBJ50E4m3/M6ESBW0S7UTP1bIOGkd/M+u38h0Aruo +4qkngdguu9N3BnsU2kOeicdjxc+vtqRc7/kbkTdT4SrpG8EFP6T2U9U1gtBpLnau +02gPrzjaQzyYDLGOBq+Ozt/mN0YJUhJ3hlwi18dNKreTzWgJ6mmXQWS0eAmHx8TI +s2Nz9x3EfRo9CIMuaaeUjRogIEg5Tg4xC00ZtDO0/EfgpFkeHJGVQA2DgdBJsr6r +q69MjhMfFxRJItxJMJzP6an2HkJ8onUPBtjEBmk3/fnfiaflzRyEb5zdii6r2TD8 +TwIDAQAB +-----END PUBLIC KEY----- diff --git a/systemvm/debian/root/noVNC/utils/websockify/tests/fixtures/symmetric.key b/systemvm/debian/root/noVNC/utils/websockify/tests/fixtures/symmetric.key new file mode 100644 index 000000000000..668b39c431c1 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/tests/fixtures/symmetric.key @@ -0,0 +1 @@ +secret_sauce \ No newline at end of file diff --git a/systemvm/debian/root/noVNC/utils/websockify/tests/latency.html b/systemvm/debian/root/noVNC/utils/websockify/tests/latency.html new file mode 100644 index 000000000000..9904a02f5cf7 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/tests/latency.html @@ -0,0 +1,261 @@ + + + + WebSockets Latency Test + + + + + Host:   + Port:   + Encrypt: +
+ Payload Size:   + Send Delay (ms):   +   + +

+ + + + + + + + + + + + + + + + + + + + +
Packets sent:
Packets Received:
Average Latency:
40 Frame Running Average Latency:
Minimum Latency:
Maximum Latency:
+ +
+ Messages:
+ + + + + + + diff --git a/systemvm/debian/root/noVNC/utils/websockify/tests/latency.py b/systemvm/debian/root/noVNC/utils/websockify/tests/latency.py new file mode 120000 index 000000000000..3ae4d964c729 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/tests/latency.py @@ -0,0 +1 @@ +echo.py \ No newline at end of file diff --git a/systemvm/debian/root/noVNC/utils/websockify/tests/load.html b/systemvm/debian/root/noVNC/utils/websockify/tests/load.html new file mode 100644 index 000000000000..cb550416e0f7 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/tests/load.html @@ -0,0 +1,223 @@ + + + + WebSockets Load Test + + + + + Host:   + Port:   + Encrypt:   + Send Delay (ms):   +   + +

+ + + + + + + + + + + +
Packets sent:
0
Good Packets Received:
0
Errors (Bad Packets Received:)
0
+ +
+ Errors:
+ + + + + + + diff --git a/systemvm/debian/root/noVNC/utils/websockify/tests/load.py b/systemvm/debian/root/noVNC/utils/websockify/tests/load.py new file mode 100755 index 000000000000..710b593b90cd --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/tests/load.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python + +''' +WebSocket server-side load test program. Sends and receives traffic +that has a random payload (length and content) that is checksummed and +given a sequence number. Any errors are reported and counted. +''' + +import sys, os, select, random, time, optparse, logging +sys.path.insert(0,os.path.join(os.path.dirname(__file__), "..")) +from websockify.websockifyserver import WebSockifyServer, WebSockifyRequestHandler + +class WebSocketLoadServer(WebSockifyServer): + + recv_cnt = 0 + send_cnt = 0 + + def __init__(self, *args, **kwargs): + self.delay = kwargs.pop('delay') + + WebSockifyServer.__init__(self, *args, **kwargs) + + +class WebSocketLoad(WebSockifyRequestHandler): + + max_packet_size = 10000 + + def new_websocket_client(self): + print "Prepopulating random array" + self.rand_array = [] + for i in range(0, self.max_packet_size): + self.rand_array.append(random.randint(0, 9)) + + self.errors = 0 + self.send_cnt = 0 + self.recv_cnt = 0 + + self.responder(self.request) + + print "accumulated errors:", self.errors + self.errors = 0 + + def responder(self, client): + c_pend = 0 + cqueue = [] + cpartial = "" + socks = [client] + last_send = time.time() * 1000 + + while True: + ins, outs, excepts = select.select(socks, socks, socks, 1) + if excepts: raise Exception("Socket exception") + + if client in ins: + frames, closed = self.recv_frames() + + err = self.check(frames) + if err: + self.errors = self.errors + 1 + print err + + if closed: + break + + now = time.time() * 1000 + if client in outs: + if c_pend: + last_send = now + c_pend = self.send_frames() + elif now > (last_send + self.server.delay): + last_send = now + c_pend = self.send_frames([self.generate()]) + + def generate(self): + length = random.randint(10, self.max_packet_size) + numlist = self.rand_array[self.max_packet_size-length:] + # Error in length + #numlist.append(5) + chksum = sum(numlist) + # Error in checksum + #numlist[0] = 5 + nums = "".join( [str(n) for n in numlist] ) + data = "^%d:%d:%d:%s$" % (self.send_cnt, length, chksum, nums) + self.send_cnt += 1 + + return data + + + def check(self, frames): + + err = "" + for data in frames: + if data.count('$') > 1: + raise Exception("Multiple parts within single packet") + if len(data) == 0: + self.traffic("_") + continue + + if data[0] != "^": + err += "buf did not start with '^'\n" + continue + + try: + cnt, length, chksum, nums = data[1:-1].split(':') + cnt = int(cnt) + length = int(length) + chksum = int(chksum) + except ValueError: + print "\n" + repr(data) + "" + err += "Invalid data format\n" + continue + + if self.recv_cnt != cnt: + err += "Expected count %d but got %d\n" % (self.recv_cnt, cnt) + self.recv_cnt = cnt + 1 + continue + + self.recv_cnt += 1 + + if len(nums) != length: + err += "Expected length %d but got %d\n" % (length, len(nums)) + continue + + inv = nums.translate(None, "0123456789") + if inv: + err += "Invalid characters found: %s\n" % inv + continue + + real_chksum = 0 + for num in nums: + real_chksum += int(num) + + if real_chksum != chksum: + err += "Expected checksum %d but real chksum is %d\n" % (chksum, real_chksum) + return err + + +if __name__ == '__main__': + parser = optparse.OptionParser(usage="%prog [options] listen_port") + parser.add_option("--verbose", "-v", action="store_true", + help="verbose messages and per frame traffic") + parser.add_option("--cert", default="self.pem", + help="SSL certificate file") + parser.add_option("--key", default=None, + help="SSL key file (if separate from cert)") + parser.add_option("--ssl-only", action="store_true", + help="disallow non-encrypted connections") + (opts, args) = parser.parse_args() + + try: + if len(args) != 1: raise ValueError + opts.listen_port = int(args[0]) + + if len(args) not in [1,2]: raise ValueError + opts.listen_port = int(args[0]) + if len(args) == 2: + opts.delay = int(args[1]) + else: + opts.delay = 10 + except ValueError: + parser.error("Invalid arguments") + + logging.basicConfig(level=logging.INFO) + + opts.web = "." + server = WebSocketLoadServer(WebSocketLoad, **opts.__dict__) + server.start_server() + diff --git a/systemvm/debian/root/noVNC/utils/websockify/tests/plain_echo.html b/systemvm/debian/root/noVNC/utils/websockify/tests/plain_echo.html new file mode 100644 index 000000000000..be35e54389bb --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/tests/plain_echo.html @@ -0,0 +1,145 @@ + + + + WebSockets Echo Test + + + + + Host:   + Port:   + Encrypt:   +   + + +
+ Log:
+ + + + + + + diff --git a/systemvm/debian/root/noVNC/utils/websockify/tests/simple.html b/systemvm/debian/root/noVNC/utils/websockify/tests/simple.html new file mode 100644 index 000000000000..8f6a73fb02dd --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/tests/simple.html @@ -0,0 +1,65 @@ + + + + Websock Simple Client + + + + WebSocket/websockify URI:   + +

+   +   +

+ Log:
+ + + + + + diff --git a/systemvm/debian/root/noVNC/utils/websockify/tests/test_auth_plugins.py b/systemvm/debian/root/noVNC/utils/websockify/tests/test_auth_plugins.py new file mode 100644 index 000000000000..4b3bfb5c44f5 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/tests/test_auth_plugins.py @@ -0,0 +1,28 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +""" Unit tests for Authentication plugins""" + +from websockify.auth_plugins import BasicHTTPAuth, AuthenticationError +import unittest + + +class BasicHTTPAuthTestCase(unittest.TestCase): + + def setUp(self): + self.plugin = BasicHTTPAuth('Aladdin:open sesame') + + def test_no_auth(self): + headers = {} + self.assertRaises(AuthenticationError, self.plugin.authenticate, headers, 'localhost', '1234') + + def test_invalid_password(self): + headers = {'Authorization': 'Basic QWxhZGRpbjpzZXNhbWUgc3RyZWV0'} + self.assertRaises(AuthenticationError, self.plugin.authenticate, headers, 'localhost', '1234') + + def test_valid_password(self): + headers = {'Authorization': 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=='} + self.plugin.authenticate(headers, 'localhost', '1234') + + def test_garbage_auth(self): + headers = {'Authorization': 'Basic xxxxxxxxxxxxxxxxxxxxxxxxxxxx'} + self.assertRaises(AuthenticationError, self.plugin.authenticate, headers, 'localhost', '1234') diff --git a/systemvm/debian/root/noVNC/utils/websockify/tests/test_websocket.py b/systemvm/debian/root/noVNC/utils/websockify/tests/test_websocket.py new file mode 100644 index 000000000000..8ee44f963d27 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/tests/test_websocket.py @@ -0,0 +1,212 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright(c)2013 NTT corp. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" Unit tests for websocket """ +import unittest +from websockify import websocket + +class FakeSocket: + def __init__(self): + self.data = b'' + + def send(self, buf): + self.data += buf + return len(buf) + +class AcceptTestCase(unittest.TestCase): + def test_success(self): + ws = websocket.WebSocket() + sock = FakeSocket() + ws.accept(sock, {'upgrade': 'websocket', + 'Sec-WebSocket-Version': '13', + 'Sec-WebSocket-Key': 'DKURYVK9cRFul1vOZVA56Q=='}) + self.assertEqual(sock.data[:13], b'HTTP/1.1 101 ') + self.assertTrue(b'\r\nUpgrade: websocket\r\n' in sock.data) + self.assertTrue(b'\r\nConnection: Upgrade\r\n' in sock.data) + self.assertTrue(b'\r\nSec-WebSocket-Accept: pczpYSQsvE1vBpTQYjFQPcuoj6M=\r\n' in sock.data) + + def test_bad_version(self): + ws = websocket.WebSocket() + sock = FakeSocket() + self.assertRaises(Exception, ws.accept, + sock, {'upgrade': 'websocket', + 'Sec-WebSocket-Key': 'DKURYVK9cRFul1vOZVA56Q=='}) + self.assertRaises(Exception, ws.accept, + sock, {'upgrade': 'websocket', + 'Sec-WebSocket-Version': '5', + 'Sec-WebSocket-Key': 'DKURYVK9cRFul1vOZVA56Q=='}) + self.assertRaises(Exception, ws.accept, + sock, {'upgrade': 'websocket', + 'Sec-WebSocket-Version': '20', + 'Sec-WebSocket-Key': 'DKURYVK9cRFul1vOZVA56Q=='}) + + def test_bad_upgrade(self): + ws = websocket.WebSocket() + sock = FakeSocket() + self.assertRaises(Exception, ws.accept, + sock, {'Sec-WebSocket-Version': '13', + 'Sec-WebSocket-Key': 'DKURYVK9cRFul1vOZVA56Q=='}) + self.assertRaises(Exception, ws.accept, + sock, {'upgrade': 'websocket2', + 'Sec-WebSocket-Version': '13', + 'Sec-WebSocket-Key': 'DKURYVK9cRFul1vOZVA56Q=='}) + + def test_missing_key(self): + ws = websocket.WebSocket() + sock = FakeSocket() + self.assertRaises(Exception, ws.accept, + sock, {'upgrade': 'websocket', + 'Sec-WebSocket-Version': '13'}) + + def test_protocol(self): + class ProtoSocket(websocket.WebSocket): + def select_subprotocol(self, protocol): + return 'gazonk' + + ws = ProtoSocket() + sock = FakeSocket() + ws.accept(sock, {'upgrade': 'websocket', + 'Sec-WebSocket-Version': '13', + 'Sec-WebSocket-Key': 'DKURYVK9cRFul1vOZVA56Q==', + 'Sec-WebSocket-Protocol': 'foobar gazonk'}) + self.assertEqual(sock.data[:13], b'HTTP/1.1 101 ') + self.assertTrue(b'\r\nSec-WebSocket-Protocol: gazonk\r\n' in sock.data) + + def test_no_protocol(self): + ws = websocket.WebSocket() + sock = FakeSocket() + ws.accept(sock, {'upgrade': 'websocket', + 'Sec-WebSocket-Version': '13', + 'Sec-WebSocket-Key': 'DKURYVK9cRFul1vOZVA56Q=='}) + self.assertEqual(sock.data[:13], b'HTTP/1.1 101 ') + self.assertFalse(b'\r\nSec-WebSocket-Protocol:' in sock.data) + + def test_missing_protocol(self): + ws = websocket.WebSocket() + sock = FakeSocket() + self.assertRaises(Exception, ws.accept, + sock, {'upgrade': 'websocket', + 'Sec-WebSocket-Version': '13', + 'Sec-WebSocket-Key': 'DKURYVK9cRFul1vOZVA56Q==', + 'Sec-WebSocket-Protocol': 'foobar gazonk'}) + + def test_protocol(self): + class ProtoSocket(websocket.WebSocket): + def select_subprotocol(self, protocol): + return 'oddball' + + ws = ProtoSocket() + sock = FakeSocket() + self.assertRaises(Exception, ws.accept, + sock, {'upgrade': 'websocket', + 'Sec-WebSocket-Version': '13', + 'Sec-WebSocket-Key': 'DKURYVK9cRFul1vOZVA56Q==', + 'Sec-WebSocket-Protocol': 'foobar gazonk'}) + +class PingPongTest(unittest.TestCase): + def setUp(self): + self.ws = websocket.WebSocket() + self.sock = FakeSocket() + self.ws.accept(self.sock, {'upgrade': 'websocket', + 'Sec-WebSocket-Version': '13', + 'Sec-WebSocket-Key': 'DKURYVK9cRFul1vOZVA56Q=='}) + self.assertEqual(self.sock.data[:13], b'HTTP/1.1 101 ') + self.sock.data = b'' + + def test_ping(self): + self.ws.ping() + self.assertEqual(self.sock.data, b'\x89\x00') + + def test_pong(self): + self.ws.pong() + self.assertEqual(self.sock.data, b'\x8a\x00') + + def test_ping_data(self): + self.ws.ping(b'foo') + self.assertEqual(self.sock.data, b'\x89\x03foo') + + def test_pong_data(self): + self.ws.pong(b'foo') + self.assertEqual(self.sock.data, b'\x8a\x03foo') + +class HyBiEncodeDecodeTestCase(unittest.TestCase): + def test_decode_hybi_text(self): + buf = b'\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58' + ws = websocket.WebSocket() + res = ws._decode_hybi(buf) + + self.assertEqual(res['fin'], 1) + self.assertEqual(res['opcode'], 0x1) + self.assertEqual(res['masked'], True) + self.assertEqual(res['length'], len(buf)) + self.assertEqual(res['payload'], b'Hello') + + def test_decode_hybi_binary(self): + buf = b'\x82\x04\x01\x02\x03\x04' + ws = websocket.WebSocket() + res = ws._decode_hybi(buf) + + self.assertEqual(res['fin'], 1) + self.assertEqual(res['opcode'], 0x2) + self.assertEqual(res['length'], len(buf)) + self.assertEqual(res['payload'], b'\x01\x02\x03\x04') + + def test_decode_hybi_extended_16bit_binary(self): + data = (b'\x01\x02\x03\x04' * 65) # len > 126 -- len == 260 + buf = b'\x82\x7e\x01\x04' + data + ws = websocket.WebSocket() + res = ws._decode_hybi(buf) + + self.assertEqual(res['fin'], 1) + self.assertEqual(res['opcode'], 0x2) + self.assertEqual(res['length'], len(buf)) + self.assertEqual(res['payload'], data) + + def test_decode_hybi_extended_64bit_binary(self): + data = (b'\x01\x02\x03\x04' * 65) # len > 126 -- len == 260 + buf = b'\x82\x7f\x00\x00\x00\x00\x00\x00\x01\x04' + data + ws = websocket.WebSocket() + res = ws._decode_hybi(buf) + + self.assertEqual(res['fin'], 1) + self.assertEqual(res['opcode'], 0x2) + self.assertEqual(res['length'], len(buf)) + self.assertEqual(res['payload'], data) + + def test_decode_hybi_multi(self): + buf1 = b'\x01\x03\x48\x65\x6c' + buf2 = b'\x80\x02\x6c\x6f' + + ws = websocket.WebSocket() + + res1 = ws._decode_hybi(buf1) + self.assertEqual(res1['fin'], 0) + self.assertEqual(res1['opcode'], 0x1) + self.assertEqual(res1['length'], len(buf1)) + self.assertEqual(res1['payload'], b'Hel') + + res2 = ws._decode_hybi(buf2) + self.assertEqual(res2['fin'], 1) + self.assertEqual(res2['opcode'], 0x0) + self.assertEqual(res2['length'], len(buf2)) + self.assertEqual(res2['payload'], b'lo') + + def test_encode_hybi_basic(self): + ws = websocket.WebSocket() + res = ws._encode_hybi(0x1, b'Hello') + expected = b'\x81\x05\x48\x65\x6c\x6c\x6f' + + self.assertEqual(res, expected) diff --git a/systemvm/debian/root/noVNC/utils/websockify/tests/test_websocketproxy.py b/systemvm/debian/root/noVNC/utils/websockify/tests/test_websocketproxy.py new file mode 100644 index 000000000000..c0a8d93779a9 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/tests/test_websocketproxy.py @@ -0,0 +1,240 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright(c) 2015 Red Hat, Inc All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" Unit tests for websocketproxy """ + +import sys +import unittest +import unittest +import socket + +from mox3 import stubout + +from websockify import websockifyserver +from websockify import websocketproxy +from websockify import token_plugins +from websockify import auth_plugins + +if sys.version_info >= (2,7): + from jwcrypto import jwt + +try: + from StringIO import StringIO + BytesIO = StringIO +except ImportError: + from io import StringIO + from io import BytesIO + + +class FakeSocket(object): + def __init__(self, data=''): + if isinstance(data, bytes): + self._data = data + else: + self._data = data.encode('latin_1') + + def recv(self, amt, flags=None): + res = self._data[0:amt] + if not (flags & socket.MSG_PEEK): + self._data = self._data[amt:] + + return res + + def makefile(self, mode='r', buffsize=None): + if 'b' in mode: + return BytesIO(self._data) + else: + return StringIO(self._data.decode('latin_1')) + + +class FakeServer(object): + class EClose(Exception): + pass + + def __init__(self): + self.token_plugin = None + self.auth_plugin = None + self.wrap_cmd = None + self.ssl_target = None + self.unix_target = None + +class ProxyRequestHandlerTestCase(unittest.TestCase): + def setUp(self): + super(ProxyRequestHandlerTestCase, self).setUp() + self.stubs = stubout.StubOutForTesting() + self.handler = websocketproxy.ProxyRequestHandler( + FakeSocket(''), "127.0.0.1", FakeServer()) + self.handler.path = "https://localhost:6080/websockify?token=blah" + self.handler.headers = None + self.stubs.Set(websockifyserver.WebSockifyServer, 'socket', + staticmethod(lambda *args, **kwargs: None)) + + def tearDown(self): + self.stubs.UnsetAll() + super(ProxyRequestHandlerTestCase, self).tearDown() + + def test_get_target(self): + class TestPlugin(token_plugins.BasePlugin): + def lookup(self, token): + return ("some host", "some port") + + host, port = self.handler.get_target( + TestPlugin(None)) + + self.assertEqual(host, "some host") + self.assertEqual(port, "some port") + + def test_get_target_unix_socket(self): + class TestPlugin(token_plugins.BasePlugin): + def lookup(self, token): + return ("unix_socket", "/tmp/socket") + + _, socket = self.handler.get_target( + TestPlugin(None)) + + self.assertEqual(socket, "/tmp/socket") + + def test_get_target_raises_error_on_unknown_token(self): + class TestPlugin(token_plugins.BasePlugin): + def lookup(self, token): + return None + + self.assertRaises(FakeServer.EClose, self.handler.get_target, + TestPlugin(None)) + + def test_token_plugin(self): + class TestPlugin(token_plugins.BasePlugin): + def lookup(self, token): + return (self.source + token).split(',') + + self.stubs.Set(websocketproxy.ProxyRequestHandler, 'send_auth_error', + staticmethod(lambda *args, **kwargs: None)) + + self.handler.server.token_plugin = TestPlugin("somehost,") + self.handler.validate_connection() + + self.assertEqual(self.handler.server.target_host, "somehost") + self.assertEqual(self.handler.server.target_port, "blah") + + if sys.version_info >= (2,7): + def test_asymmetric_jws_token_plugin(self): + key = jwt.JWK() + private_key = open("./tests/fixtures/private.pem", "rb").read() + key.import_from_pem(private_key) + jwt_token = jwt.JWT({"alg": "RS256"}, {'host': "remote_host", 'port': "remote_port"}) + jwt_token.make_signed_token(key) + self.handler.path = "https://localhost:6080/websockify?token={jwt_token}".format(jwt_token=jwt_token.serialize()) + + self.stubs.Set(websocketproxy.ProxyRequestHandler, 'send_auth_error', + staticmethod(lambda *args, **kwargs: None)) + + self.handler.server.token_plugin = token_plugins.JWTTokenApi("./tests/fixtures/public.pem") + self.handler.validate_connection() + + self.assertEqual(self.handler.server.target_host, "remote_host") + self.assertEqual(self.handler.server.target_port, "remote_port") + + def test_asymmetric_jws_token_plugin_with_illigal_key_exception(self): + key = jwt.JWK() + private_key = open("./tests/fixtures/private.pem", "rb").read() + key.import_from_pem(private_key) + jwt_token = jwt.JWT({"alg": "RS256"}, {'host': "remote_host", 'port': "remote_port"}) + jwt_token.make_signed_token(key) + self.handler.path = "https://localhost:6080/websockify?token={jwt_token}".format(jwt_token=jwt_token.serialize()) + + self.stubs.Set(websocketproxy.ProxyRequestHandler, 'send_auth_error', + staticmethod(lambda *args, **kwargs: None)) + + self.handler.server.token_plugin = token_plugins.JWTTokenApi("wrong.pub") + self.assertRaises(self.handler.server.EClose, + self.handler.validate_connection) + + + def test_symmetric_jws_token_plugin(self): + secret = open("./tests/fixtures/symmetric.key").read() + key = jwt.JWK() + key.import_key(kty="oct",k=secret) + jwt_token = jwt.JWT({"alg": "HS256"}, {'host': "remote_host", 'port': "remote_port"}) + jwt_token.make_signed_token(key) + self.handler.path = "https://localhost:6080/websockify?token={jwt_token}".format(jwt_token=jwt_token.serialize()) + + self.stubs.Set(websocketproxy.ProxyRequestHandler, 'send_auth_error', + staticmethod(lambda *args, **kwargs: None)) + + self.handler.server.token_plugin = token_plugins.JWTTokenApi("./tests/fixtures/symmetric.key") + self.handler.validate_connection() + + self.assertEqual(self.handler.server.target_host, "remote_host") + self.assertEqual(self.handler.server.target_port, "remote_port") + + def test_symmetric_jws_token_plugin_with_illigal_key_exception(self): + secret = open("./tests/fixtures/symmetric.key").read() + key = jwt.JWK() + key.import_key(kty="oct",k=secret) + jwt_token = jwt.JWT({"alg": "HS256"}, {'host': "remote_host", 'port': "remote_port"}) + jwt_token.make_signed_token(key) + self.handler.path = "https://localhost:6080/websockify?token={jwt_token}".format(jwt_token=jwt_token.serialize()) + + self.stubs.Set(websocketproxy.ProxyRequestHandler, 'send_auth_error', + staticmethod(lambda *args, **kwargs: None)) + + self.handler.server.token_plugin = token_plugins.JWTTokenApi("wrong_sauce") + self.assertRaises(self.handler.server.EClose, + self.handler.validate_connection) + + def test_asymmetric_jwe_token_plugin(self): + private_key = jwt.JWK() + public_key = jwt.JWK() + private_key_data = open("./tests/fixtures/private.pem", "rb").read() + public_key_data = open("./tests/fixtures/public.pem", "rb").read() + private_key.import_from_pem(private_key_data) + public_key.import_from_pem(public_key_data) + jwt_token = jwt.JWT({"alg": "RS256"}, {'host': "remote_host", 'port': "remote_port"}) + jwt_token.make_signed_token(private_key) + jwe_token = jwt.JWT(header={"alg": "RSA1_5", "enc": "A256CBC-HS512"}, + claims=jwt_token.serialize()) + jwe_token.make_encrypted_token(public_key) + + self.handler.path = "https://localhost:6080/websockify?token={jwt_token}".format(jwt_token=jwe_token.serialize()) + + self.stubs.Set(websocketproxy.ProxyRequestHandler, 'send_auth_error', + staticmethod(lambda *args, **kwargs: None)) + + self.handler.server.token_plugin = token_plugins.JWTTokenApi("./tests/fixtures/private.pem") + self.handler.validate_connection() + + self.assertEqual(self.handler.server.target_host, "remote_host") + self.assertEqual(self.handler.server.target_port, "remote_port") + + def test_auth_plugin(self): + class TestPlugin(auth_plugins.BasePlugin): + def authenticate(self, headers, target_host, target_port): + if target_host == self.source: + raise auth_plugins.AuthenticationError(response_msg="some_error") + + self.stubs.Set(websocketproxy.ProxyRequestHandler, 'send_auth_error', + staticmethod(lambda *args, **kwargs: None)) + + self.handler.server.auth_plugin = TestPlugin("somehost") + self.handler.server.target_host = "somehost" + self.handler.server.target_port = "someport" + + self.assertRaises(auth_plugins.AuthenticationError, + self.handler.auth_connection) + + self.handler.server.target_host = "someotherhost" + self.handler.auth_connection() + diff --git a/systemvm/debian/root/noVNC/utils/websockify/tests/test_websockifyserver.py b/systemvm/debian/root/noVNC/utils/websockify/tests/test_websockifyserver.py new file mode 100644 index 000000000000..b9312dc60770 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/tests/test_websockifyserver.py @@ -0,0 +1,456 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright(c)2013 NTT corp. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" Unit tests for websockifyserver """ +import errno +import os +import logging +import select +import shutil +import socket +import ssl +from mox3 import stubout +import sys +import tempfile +import unittest +import socket +import signal +from websockify import websockifyserver + +try: + from BaseHTTPServer import BaseHTTPRequestHandler +except ImportError: + from http.server import BaseHTTPRequestHandler + +try: + from StringIO import StringIO + BytesIO = StringIO +except ImportError: + from io import StringIO + from io import BytesIO + + + + +def raise_oserror(*args, **kwargs): + raise OSError('fake error') + + +class FakeSocket(object): + def __init__(self, data=''): + if isinstance(data, bytes): + self._data = data + else: + self._data = data.encode('latin_1') + + def recv(self, amt, flags=None): + res = self._data[0:amt] + if not (flags & socket.MSG_PEEK): + self._data = self._data[amt:] + + return res + + def makefile(self, mode='r', buffsize=None): + if 'b' in mode: + return BytesIO(self._data) + else: + return StringIO(self._data.decode('latin_1')) + + +class WebSockifyRequestHandlerTestCase(unittest.TestCase): + def setUp(self): + super(WebSockifyRequestHandlerTestCase, self).setUp() + self.stubs = stubout.StubOutForTesting() + self.tmpdir = tempfile.mkdtemp('-websockify-tests') + # Mock this out cause it screws tests up + self.stubs.Set(os, 'chdir', lambda *args, **kwargs: None) + self.stubs.Set(BaseHTTPRequestHandler, 'send_response', + lambda *args, **kwargs: None) + + def fake_send_error(self, code, message=None, explain=None): + self.last_code = code + + self.stubs.Set(BaseHTTPRequestHandler, 'send_error', + fake_send_error) + + def tearDown(self): + """Called automatically after each test.""" + self.stubs.UnsetAll() + os.rmdir(self.tmpdir) + super(WebSockifyRequestHandlerTestCase, self).tearDown() + + def _get_server(self, handler_class=websockifyserver.WebSockifyRequestHandler, + **kwargs): + web = kwargs.pop('web', self.tmpdir) + return websockifyserver.WebSockifyServer( + handler_class, listen_host='localhost', + listen_port=80, key=self.tmpdir, web=web, + record=self.tmpdir, daemon=False, ssl_only=0, idle_timeout=1, + **kwargs) + + def test_normal_get_with_only_upgrade_returns_error(self): + server = self._get_server(web=None) + handler = websockifyserver.WebSockifyRequestHandler( + FakeSocket('GET /tmp.txt HTTP/1.1'), '127.0.0.1', server) + + def fake_send_response(self, code, message=None): + self.last_code = code + + self.stubs.Set(BaseHTTPRequestHandler, 'send_response', + fake_send_response) + + handler.do_GET() + self.assertEqual(handler.last_code, 405) + + def test_list_dir_with_file_only_returns_error(self): + server = self._get_server(file_only=True) + handler = websockifyserver.WebSockifyRequestHandler( + FakeSocket('GET / HTTP/1.1'), '127.0.0.1', server) + + def fake_send_response(self, code, message=None): + self.last_code = code + + self.stubs.Set(BaseHTTPRequestHandler, 'send_response', + fake_send_response) + + handler.path = '/' + handler.do_GET() + self.assertEqual(handler.last_code, 404) + + +class WebSockifyServerTestCase(unittest.TestCase): + def setUp(self): + super(WebSockifyServerTestCase, self).setUp() + self.stubs = stubout.StubOutForTesting() + self.tmpdir = tempfile.mkdtemp('-websockify-tests') + # Mock this out cause it screws tests up + self.stubs.Set(os, 'chdir', lambda *args, **kwargs: None) + + def tearDown(self): + """Called automatically after each test.""" + self.stubs.UnsetAll() + os.rmdir(self.tmpdir) + super(WebSockifyServerTestCase, self).tearDown() + + def _get_server(self, handler_class=websockifyserver.WebSockifyRequestHandler, + **kwargs): + return websockifyserver.WebSockifyServer( + handler_class, listen_host='localhost', + listen_port=80, key=self.tmpdir, web=self.tmpdir, + record=self.tmpdir, **kwargs) + + def test_daemonize_raises_error_while_closing_fds(self): + server = self._get_server(daemon=True, ssl_only=1, idle_timeout=1) + self.stubs.Set(os, 'fork', lambda *args: 0) + self.stubs.Set(signal, 'signal', lambda *args: None) + self.stubs.Set(os, 'setsid', lambda *args: None) + self.stubs.Set(os, 'close', raise_oserror) + self.assertRaises(OSError, server.daemonize, keepfd=None, chdir='./') + + def test_daemonize_ignores_ebadf_error_while_closing_fds(self): + def raise_oserror_ebadf(fd): + raise OSError(errno.EBADF, 'fake error') + + server = self._get_server(daemon=True, ssl_only=1, idle_timeout=1) + self.stubs.Set(os, 'fork', lambda *args: 0) + self.stubs.Set(os, 'setsid', lambda *args: None) + self.stubs.Set(signal, 'signal', lambda *args: None) + self.stubs.Set(os, 'close', raise_oserror_ebadf) + self.stubs.Set(os, 'open', raise_oserror) + self.assertRaises(OSError, server.daemonize, keepfd=None, chdir='./') + + def test_handshake_fails_on_not_ready(self): + server = self._get_server(daemon=True, ssl_only=0, idle_timeout=1) + + def fake_select(rlist, wlist, xlist, timeout=None): + return ([], [], []) + + self.stubs.Set(select, 'select', fake_select) + self.assertRaises( + websockifyserver.WebSockifyServer.EClose, server.do_handshake, + FakeSocket(), '127.0.0.1') + + def test_empty_handshake_fails(self): + server = self._get_server(daemon=True, ssl_only=0, idle_timeout=1) + + sock = FakeSocket('') + + def fake_select(rlist, wlist, xlist, timeout=None): + return ([sock], [], []) + + self.stubs.Set(select, 'select', fake_select) + self.assertRaises( + websockifyserver.WebSockifyServer.EClose, server.do_handshake, + sock, '127.0.0.1') + + def test_handshake_policy_request(self): + # TODO(directxman12): implement + pass + + def test_handshake_ssl_only_without_ssl_raises_error(self): + server = self._get_server(daemon=True, ssl_only=1, idle_timeout=1) + + sock = FakeSocket('some initial data') + + def fake_select(rlist, wlist, xlist, timeout=None): + return ([sock], [], []) + + self.stubs.Set(select, 'select', fake_select) + self.assertRaises( + websockifyserver.WebSockifyServer.EClose, server.do_handshake, + sock, '127.0.0.1') + + def test_do_handshake_no_ssl(self): + class FakeHandler(object): + CALLED = False + def __init__(self, *args, **kwargs): + type(self).CALLED = True + + FakeHandler.CALLED = False + + server = self._get_server( + handler_class=FakeHandler, daemon=True, + ssl_only=0, idle_timeout=1) + + sock = FakeSocket('some initial data') + + def fake_select(rlist, wlist, xlist, timeout=None): + return ([sock], [], []) + + self.stubs.Set(select, 'select', fake_select) + self.assertEqual(server.do_handshake(sock, '127.0.0.1'), sock) + self.assertTrue(FakeHandler.CALLED, True) + + def test_do_handshake_ssl(self): + # TODO(directxman12): implement this + pass + + def test_do_handshake_ssl_without_ssl_raises_error(self): + # TODO(directxman12): implement this + pass + + def test_do_handshake_ssl_without_cert_raises_error(self): + server = self._get_server(daemon=True, ssl_only=0, idle_timeout=1, + cert='afdsfasdafdsafdsafdsafdas') + + sock = FakeSocket("\x16some ssl data") + + def fake_select(rlist, wlist, xlist, timeout=None): + return ([sock], [], []) + + self.stubs.Set(select, 'select', fake_select) + self.assertRaises( + websockifyserver.WebSockifyServer.EClose, server.do_handshake, + sock, '127.0.0.1') + + def test_do_handshake_ssl_error_eof_raises_close_error(self): + server = self._get_server(daemon=True, ssl_only=0, idle_timeout=1) + + sock = FakeSocket("\x16some ssl data") + + def fake_select(rlist, wlist, xlist, timeout=None): + return ([sock], [], []) + + def fake_wrap_socket(*args, **kwargs): + raise ssl.SSLError(ssl.SSL_ERROR_EOF) + + class fake_create_default_context(): + def __init__(self, purpose): + self.verify_mode = None + self.options = 0 + def load_cert_chain(self, certfile, keyfile, password): + pass + def set_default_verify_paths(self): + pass + def load_verify_locations(self, cafile): + pass + def wrap_socket(self, *args, **kwargs): + raise ssl.SSLError(ssl.SSL_ERROR_EOF) + + self.stubs.Set(select, 'select', fake_select) + if (hasattr(ssl, 'create_default_context')): + # for recent versions of python + self.stubs.Set(ssl, 'create_default_context', fake_create_default_context) + else: + # for fallback for old versions of python + self.stubs.Set(ssl, 'wrap_socket', fake_wrap_socket) + self.assertRaises( + websockifyserver.WebSockifyServer.EClose, server.do_handshake, + sock, '127.0.0.1') + + def test_do_handshake_ssl_sets_ciphers(self): + test_ciphers = 'TEST-CIPHERS-1:TEST-CIPHER-2' + + class FakeHandler(object): + def __init__(self, *args, **kwargs): + pass + + server = self._get_server(handler_class=FakeHandler, daemon=True, + idle_timeout=1, ssl_ciphers=test_ciphers) + sock = FakeSocket("\x16some ssl data") + + def fake_select(rlist, wlist, xlist, timeout=None): + return ([sock], [], []) + + class fake_create_default_context(): + CIPHERS = '' + def __init__(self, purpose): + self.verify_mode = None + self.options = 0 + def load_cert_chain(self, certfile, keyfile, password): + pass + def set_default_verify_paths(self): + pass + def load_verify_locations(self, cafile): + pass + def wrap_socket(self, *args, **kwargs): + pass + def set_ciphers(self, ciphers_to_set): + fake_create_default_context.CIPHERS = ciphers_to_set + + self.stubs.Set(select, 'select', fake_select) + if (hasattr(ssl, 'create_default_context')): + # for recent versions of python + self.stubs.Set(ssl, 'create_default_context', fake_create_default_context) + server.do_handshake(sock, '127.0.0.1') + self.assertEqual(fake_create_default_context.CIPHERS, test_ciphers) + else: + # for fallback for old versions of python + # not supperted, nothing to test + pass + + def test_do_handshake_ssl_sets_opions(self): + test_options = 0xCAFEBEEF + + class FakeHandler(object): + def __init__(self, *args, **kwargs): + pass + + server = self._get_server(handler_class=FakeHandler, daemon=True, + idle_timeout=1, ssl_options=test_options) + sock = FakeSocket("\x16some ssl data") + + def fake_select(rlist, wlist, xlist, timeout=None): + return ([sock], [], []) + + class fake_create_default_context(object): + OPTIONS = 0 + def __init__(self, purpose): + self.verify_mode = None + self._options = 0 + def load_cert_chain(self, certfile, keyfile, password): + pass + def set_default_verify_paths(self): + pass + def load_verify_locations(self, cafile): + pass + def wrap_socket(self, *args, **kwargs): + pass + def get_options(self): + return self._options + def set_options(self, val): + fake_create_default_context.OPTIONS = val + options = property(get_options, set_options) + + self.stubs.Set(select, 'select', fake_select) + if (hasattr(ssl, 'create_default_context')): + # for recent versions of python + self.stubs.Set(ssl, 'create_default_context', fake_create_default_context) + server.do_handshake(sock, '127.0.0.1') + self.assertEqual(fake_create_default_context.OPTIONS, test_options) + else: + # for fallback for old versions of python + # not supperted, nothing to test + pass + + def test_fallback_sigchld_handler(self): + # TODO(directxman12): implement this + pass + + def test_start_server_error(self): + server = self._get_server(daemon=False, ssl_only=1, idle_timeout=1) + sock = server.socket('localhost') + + def fake_select(rlist, wlist, xlist, timeout=None): + raise Exception("fake error") + + self.stubs.Set(websockifyserver.WebSockifyServer, 'socket', + lambda *args, **kwargs: sock) + self.stubs.Set(websockifyserver.WebSockifyServer, 'daemonize', + lambda *args, **kwargs: None) + self.stubs.Set(select, 'select', fake_select) + server.start_server() + + def test_start_server_keyboardinterrupt(self): + server = self._get_server(daemon=False, ssl_only=0, idle_timeout=1) + sock = server.socket('localhost') + + def fake_select(rlist, wlist, xlist, timeout=None): + raise KeyboardInterrupt + + self.stubs.Set(websockifyserver.WebSockifyServer, 'socket', + lambda *args, **kwargs: sock) + self.stubs.Set(websockifyserver.WebSockifyServer, 'daemonize', + lambda *args, **kwargs: None) + self.stubs.Set(select, 'select', fake_select) + server.start_server() + + def test_start_server_systemexit(self): + server = self._get_server(daemon=False, ssl_only=0, idle_timeout=1) + sock = server.socket('localhost') + + def fake_select(rlist, wlist, xlist, timeout=None): + sys.exit() + + self.stubs.Set(websockifyserver.WebSockifyServer, 'socket', + lambda *args, **kwargs: sock) + self.stubs.Set(websockifyserver.WebSockifyServer, 'daemonize', + lambda *args, **kwargs: None) + self.stubs.Set(select, 'select', fake_select) + server.start_server() + + def test_socket_set_keepalive_options(self): + keepcnt = 12 + keepidle = 34 + keepintvl = 56 + + server = self._get_server(daemon=False, ssl_only=0, idle_timeout=1) + sock = server.socket('localhost', + tcp_keepcnt=keepcnt, + tcp_keepidle=keepidle, + tcp_keepintvl=keepintvl) + + if hasattr(socket, 'TCP_KEEPCNT'): + self.assertEqual(sock.getsockopt(socket.SOL_TCP, + socket.TCP_KEEPCNT), keepcnt) + self.assertEqual(sock.getsockopt(socket.SOL_TCP, + socket.TCP_KEEPIDLE), keepidle) + self.assertEqual(sock.getsockopt(socket.SOL_TCP, + socket.TCP_KEEPINTVL), keepintvl) + + sock = server.socket('localhost', + tcp_keepalive=False, + tcp_keepcnt=keepcnt, + tcp_keepidle=keepidle, + tcp_keepintvl=keepintvl) + + if hasattr(socket, 'TCP_KEEPCNT'): + self.assertNotEqual(sock.getsockopt(socket.SOL_TCP, + socket.TCP_KEEPCNT), keepcnt) + self.assertNotEqual(sock.getsockopt(socket.SOL_TCP, + socket.TCP_KEEPIDLE), keepidle) + self.assertNotEqual(sock.getsockopt(socket.SOL_TCP, + socket.TCP_KEEPINTVL), keepintvl) diff --git a/systemvm/debian/root/noVNC/utils/websockify/tox.ini b/systemvm/debian/root/noVNC/utils/websockify/tox.ini new file mode 100644 index 000000000000..79f7201db087 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/tox.ini @@ -0,0 +1,17 @@ +# Tox (http://tox.testrun.org/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + +[tox] +envlist = py24,py26,py27,py33,py34 + +[testenv] +commands = nosetests {posargs} +deps = -r{toxinidir}/test-requirements.txt + +# At some point we should enable this since tox expects it to exist but +# the code will need pep8ising first. +#[testenv:pep8] +#commands = flake8 +#dep = flake8 diff --git a/systemvm/debian/root/noVNC/utils/websockify/websockify.py b/systemvm/debian/root/noVNC/utils/websockify/websockify.py new file mode 120000 index 000000000000..e5224d533ef2 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/websockify.py @@ -0,0 +1 @@ +run \ No newline at end of file diff --git a/systemvm/debian/root/noVNC/utils/websockify/websockify/__init__.py b/systemvm/debian/root/noVNC/utils/websockify/websockify/__init__.py new file mode 100644 index 000000000000..37a6f47b4524 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/websockify/__init__.py @@ -0,0 +1,2 @@ +from websockify.websocket import * +from websockify.websocketproxy import * diff --git a/systemvm/debian/root/noVNC/utils/websockify/websockify/__main__.py b/systemvm/debian/root/noVNC/utils/websockify/websockify/__main__.py new file mode 100644 index 000000000000..8378d4677b99 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/websockify/__main__.py @@ -0,0 +1,4 @@ +import websockify + +if __name__ == '__main__': + websockify.websocketproxy.websockify_init() diff --git a/systemvm/debian/root/noVNC/utils/websockify/websockify/auth_plugins.py b/systemvm/debian/root/noVNC/utils/websockify/websockify/auth_plugins.py new file mode 100644 index 000000000000..2d636c2d41c2 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/websockify/auth_plugins.py @@ -0,0 +1,102 @@ +class BasePlugin(object): + def __init__(self, src=None): + self.source = src + + def authenticate(self, headers, target_host, target_port): + pass + + +class AuthenticationError(Exception): + def __init__(self, log_msg=None, response_code=403, response_headers={}, response_msg=None): + self.code = response_code + self.headers = response_headers + self.msg = response_msg + + if log_msg is None: + log_msg = response_msg + + super(AuthenticationError, self).__init__('%s %s' % (self.code, log_msg)) + + +class InvalidOriginError(AuthenticationError): + def __init__(self, expected, actual): + self.expected_origin = expected + self.actual_origin = actual + + super(InvalidOriginError, self).__init__( + response_msg='Invalid Origin', + log_msg="Invalid Origin Header: Expected one of " + "%s, got '%s'" % (expected, actual)) + + +class BasicHTTPAuth(object): + """Verifies Basic Auth headers. Specify src as username:password""" + + def __init__(self, src=None): + self.src = src + + def authenticate(self, headers, target_host, target_port): + import base64 + auth_header = headers.get('Authorization') + if auth_header: + if not auth_header.startswith('Basic '): + self.auth_error() + + try: + user_pass_raw = base64.b64decode(auth_header[6:]) + except TypeError: + self.auth_error() + + try: + # http://stackoverflow.com/questions/7242316/what-encoding-should-i-use-for-http-basic-authentication + user_pass_as_text = user_pass_raw.decode('ISO-8859-1') + except UnicodeDecodeError: + self.auth_error() + + user_pass = user_pass_as_text.split(':', 1) + if len(user_pass) != 2: + self.auth_error() + + if not self.validate_creds(*user_pass): + self.demand_auth() + + else: + self.demand_auth() + + def validate_creds(self, username, password): + if '%s:%s' % (username, password) == self.src: + return True + else: + return False + + def auth_error(self): + raise AuthenticationError(response_code=403) + + def demand_auth(self): + raise AuthenticationError(response_code=401, + response_headers={'WWW-Authenticate': 'Basic realm="Websockify"'}) + +class ExpectOrigin(object): + def __init__(self, src=None): + if src is None: + self.source = [] + else: + self.source = src.split() + + def authenticate(self, headers, target_host, target_port): + origin = headers.get('Origin', None) + if origin is None or origin not in self.source: + raise InvalidOriginError(expected=self.source, actual=origin) + +class ClientCertCNAuth(object): + """Verifies client by SSL certificate. Specify src as whitespace separated list of common names.""" + + def __init__(self, src=None): + if src is None: + self.source = [] + else: + self.source = src.split() + + def authenticate(self, headers, target_host, target_port): + if headers.get('SSL_CLIENT_S_DN_CN', None) not in self.source: + raise AuthenticationError(response_code=403) diff --git a/systemvm/debian/root/noVNC/utils/websockify/websockify/sysloghandler.py b/systemvm/debian/root/noVNC/utils/websockify/websockify/sysloghandler.py new file mode 100644 index 000000000000..92ca66f268a6 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/websockify/sysloghandler.py @@ -0,0 +1,118 @@ +import logging.handlers as handlers, socket, os, time + + +class WebsockifySysLogHandler(handlers.SysLogHandler): + """ + A handler class that sends proper Syslog-formatted messages, + as defined by RFC 5424. + """ + + _legacy_head_fmt = '<{pri}>{ident}[{pid}]: ' + _rfc5424_head_fmt = '<{pri}>1 {timestamp} {hostname} {ident} {pid} - - ' + _head_fmt = _rfc5424_head_fmt + _legacy = False + _timestamp_fmt = '%Y-%m-%dT%H:%M:%SZ' + _max_hostname = 255 + _max_ident = 24 #safer for old daemons + _send_length = False + _tail = '\n' + + + ident = None + + + def __init__(self, address=('localhost', handlers.SYSLOG_UDP_PORT), + facility=handlers.SysLogHandler.LOG_USER, + socktype=None, ident=None, legacy=False): + """ + Initialize a handler. + + If address is specified as a string, a UNIX socket is used. To log to a + local syslogd, "WebsockifySysLogHandler(address="/dev/log")" can be + used. If facility is not specified, LOG_USER is used. If socktype is + specified as socket.SOCK_DGRAM or socket.SOCK_STREAM, that specific + socket type will be used. For Unix sockets, you can also specify a + socktype of None, in which case socket.SOCK_DGRAM will be used, falling + back to socket.SOCK_STREAM. If ident is specified, this string will be + used as the application name in all messages sent. Set legacy to True + to use the old version of the protocol. + """ + + self.ident = ident + + if legacy: + self._legacy = True + self._head_fmt = self._legacy_head_fmt + + handlers.SysLogHandler.__init__(self, address, facility, socktype) + + + def emit(self, record): + """ + Emit a record. + + The record is formatted, and then sent to the syslog server. If + exception information is present, it is NOT sent to the server. + """ + + try: + # Gather info. + text = self.format(record).replace(self._tail, ' ') + if not text: # nothing to log + return + + pri = self.encodePriority(self.facility, + self.mapPriority(record.levelname)) + + timestamp = time.strftime(self._timestamp_fmt, time.gmtime()); + + hostname = socket.gethostname()[:self._max_hostname] + + if self.ident: + ident = self.ident[:self._max_ident] + else: + ident = '' + + pid = os.getpid() # shouldn't need truncation + + # Format the header. + head = { + 'pri': pri, + 'timestamp': timestamp, + 'hostname': hostname, + 'ident': ident, + 'pid': pid, + } + msg = self._head_fmt.format(**head).encode('ascii', 'ignore') + + # Encode text as plain ASCII if possible, else use UTF-8 with BOM. + try: + msg += text.encode('ascii') + except UnicodeEncodeError: + msg += text.encode('utf-8-sig') + + # Add length or tail character, if necessary. + if self.socktype != socket.SOCK_DGRAM: + if self._send_length: + msg = ('%d ' % len(msg)).encode('ascii') + msg + else: + msg += self._tail.encode('ascii') + + # Send the message. + if self.unixsocket: + try: + self.socket.send(msg) + except socket.error: + self._connect_unixsocket(self.address) + self.socket.send(msg) + + else: + if self.socktype == socket.SOCK_DGRAM: + self.socket.sendto(msg, self.address) + else: + self.socket.sendall(msg) + + except (KeyboardInterrupt, SystemExit): + raise + except: + self.handleError(record) diff --git a/systemvm/debian/root/noVNC/utils/websockify/websockify/token_plugins.py b/systemvm/debian/root/noVNC/utils/websockify/websockify/token_plugins.py new file mode 100644 index 000000000000..03800e7b80d8 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/websockify/token_plugins.py @@ -0,0 +1,159 @@ +from __future__ import print_function +import os +import sys + +class BasePlugin(object): + def __init__(self, src): + self.source = src + + def lookup(self, token): + return None + + +class ReadOnlyTokenFile(BasePlugin): + # source is a token file with lines like + # token: host:port + # or a directory of such files + def __init__(self, *args, **kwargs): + super(ReadOnlyTokenFile, self).__init__(*args, **kwargs) + self._targets = None + + def _load_targets(self): + if os.path.isdir(self.source): + cfg_files = [os.path.join(self.source, f) for + f in os.listdir(self.source)] + else: + cfg_files = [self.source] + + self._targets = {} + index = 1 + for f in cfg_files: + for line in [l.strip() for l in open(f).readlines()]: + if line and not line.startswith('#'): + try: + tok, target = line.split(': ') + self._targets[tok] = target.strip().rsplit(':', 1) + except ValueError: + print >>sys.stderr, "Syntax error in %s on line %d" % (self.source, index) + index += 1 + + def lookup(self, token): + if self._targets is None: + self._load_targets() + + if token in self._targets: + return self._targets[token] + else: + return None + + +# the above one is probably more efficient, but this one is +# more backwards compatible (although in most cases +# ReadOnlyTokenFile should suffice) +class TokenFile(ReadOnlyTokenFile): + # source is a token file with lines like + # token: host:port + # or a directory of such files + def lookup(self, token): + self._load_targets() + + return super(TokenFile, self).lookup(token) + + +class BaseTokenAPI(BasePlugin): + # source is a url with a '%s' in it where the token + # should go + + # we import things on demand so that other plugins + # in this file can be used w/o unnecessary dependencies + + def process_result(self, resp): + host, port = resp.text.split(':') + port = port.encode('ascii','ignore') + return [ host, port ] + + def lookup(self, token): + import requests + + resp = requests.get(self.source % token) + + if resp.ok: + return self.process_result(resp) + else: + return None + + +class JSONTokenApi(BaseTokenAPI): + # source is a url with a '%s' in it where the token + # should go + + def process_result(self, resp): + resp_json = resp.json() + return (resp_json['host'], resp_json['port']) + + +class JWTTokenApi(BasePlugin): + # source is a JWT-token, with hostname and port included + # Both JWS as JWE tokens are accepted. With regards to JWE tokens, the key is re-used for both validation and decryption. + + def lookup(self, token): + try: + from jwcrypto import jwt + import json + + key = jwt.JWK() + + try: + with open(self.source, 'rb') as key_file: + key_data = key_file.read() + except Exception as e: + print("Error loading key file: %s" % str(e), file=sys.stderr) + return None + + try: + key.import_from_pem(key_data) + except: + try: + key.import_key(k=key_data.decode('utf-8'),kty='oct') + except: + print('Failed to correctly parse key data!', file=sys.stderr) + return None + + try: + token = jwt.JWT(key=key, jwt=token) + parsed_header = json.loads(token.header) + + if 'enc' in parsed_header: + # Token is encrypted, so we need to decrypt by passing the claims to a new instance + token = jwt.JWT(key=key, jwt=token.claims) + + parsed = json.loads(token.claims) + + return (parsed['host'], parsed['port']) + except Exception as e: + print("Failed to parse token: %s" % str(e), file=sys.stderr) + return None + except ImportError as e: + print("package jwcrypto not found, are you sure you've installed it correctly?", file=sys.stderr) + return None + +class TokenRedis(object): + def __init__(self, src): + self._server, self._port = src.split(":") + + def lookup(self, token): + try: + import redis + import simplejson + except ImportError as e: + print("package redis or simplejson not found, are you sure you've installed them correctly?", file=sys.stderr) + return None + + client = redis.Redis(host=self._server,port=self._port) + stuff = client.get(token) + if stuff is None: + return None + else: + combo = simplejson.loads(stuff.decode("utf-8")) + pair = combo["host"] + return pair.split(':') diff --git a/systemvm/debian/root/noVNC/utils/websockify/websockify/websocket.py b/systemvm/debian/root/noVNC/utils/websockify/websockify/websocket.py new file mode 100644 index 000000000000..c8226cca1dfc --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/websockify/websocket.py @@ -0,0 +1,828 @@ +#!/usr/bin/env python + +''' +Python WebSocket library +Copyright 2011 Joel Martin +Copyright 2016 Pierre Ossman +Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3) + +Supports following protocol versions: + - http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07 + - http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10 + - http://tools.ietf.org/html/rfc6455 +''' + +import sys +import array +import email +import errno +import random +import socket +import ssl +import struct +from base64 import b64encode +from hashlib import sha1 + +try: + import numpy +except ImportError: + import warnings + warnings.warn("no 'numpy' module, HyBi protocol will be slower") + numpy = None + +# python 3.0 differences +try: + from urllib.parse import urlparse +except ImportError: + from urlparse import urlparse + +# SSLWant*Error is 2.7.9+ +try: + class WebSocketWantReadError(ssl.SSLWantReadError): + pass + class WebSocketWantWriteError(ssl.SSLWantWriteError): + pass +except AttributeError: + class WebSocketWantReadError(OSError): + def __init__(self): + OSError.__init__(self, errno.EWOULDBLOCK) + class WebSocketWantWriteError(OSError): + def __init__(self): + OSError.__init__(self, errno.EWOULDBLOCK) + +class WebSocket(object): + """WebSocket protocol socket like class. + + This provides access to the WebSocket protocol by behaving much + like a real socket would. It shares many similarities with + ssl.SSLSocket. + + The WebSocket protocols requires extra data to be sent and received + compared to the application level data. This means that a socket + that is ready to be read may not hold enough data to decode any + application data, and a socket that is ready to be written to may + not have enough space for an entire WebSocket frame. This is + handled by the exceptions WebSocketWantReadError and + WebSocketWantWriteError. When these are raised the caller must wait + for the socket to become ready again and call the relevant function + again. + + A connection is established by using either connect() or accept(), + depending on if a client or server session is desired. See the + respective functions for details. + + The following methods are passed on to the underlying socket: + + - fileno + - getpeername, getsockname + - getsockopt, setsockopt + - gettimeout, settimeout + - setblocking + """ + + GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + + def __init__(self): + """Creates an unconnected WebSocket""" + + self._state = "new" + + self._partial_msg = ''.encode("ascii") + + self._recv_buffer = ''.encode("ascii") + self._recv_queue = [] + self._send_buffer = ''.encode("ascii") + + self._sent_close = False + self._received_close = False + + self.close_code = None + self.close_reason = None + + self.socket = None + + def __getattr__(self, name): + # These methods are just redirected to the underlying socket + if name in ["fileno", + "getpeername", "getsockname", + "getsockopt", "setsockopt", + "gettimeout", "settimeout", + "setblocking"]: + assert self.socket is not None + return getattr(self.socket, name) + else: + raise AttributeError("%s instance has no attribute '%s'" % + (self.__class__.__name__, name)) + + def connect(self, uri, origin=None, protocols=[]): + """Establishes a new connection to a WebSocket server. + + This method connects to the host specified by uri and + negotiates a WebSocket connection. origin should be specified + in accordance with RFC 6454 if known. A list of valid + sub-protocols can be specified in the protocols argument. + + The data will be sent in the clear if the "ws" scheme is used, + and encrypted if the "wss" scheme is used. + + Both WebSocketWantReadError and WebSocketWantWriteError can be + raised whilst negotiating the connection. Repeated calls to + connect() must retain the same arguments. + """ + + self.client = True; + + uri = urlparse(uri) + + port = uri.port + if uri.scheme in ("ws", "http"): + if not port: + port = 80 + elif uri.scheme in ("wss", "https"): + if not port: + port = 443 + else: + raise Exception("Unknown scheme '%s'" % uri.scheme) + + # This is a state machine in order to handle + # WantRead/WantWrite events + + if self._state == "new": + self.socket = socket.create_connection((uri.hostname, port)) + + if uri.scheme in ("wss", "https"): + self.socket = ssl.wrap_socket(self.socket) + self._state = "ssl_handshake" + else: + self._state = "headers" + + if self._state == "ssl_handshake": + self.socket.do_handshake() + self._state = "headers" + + if self._state == "headers": + self._key = '' + for i in range(16): + self._key += chr(random.randrange(256)) + if sys.hexversion >= 0x3000000: + self._key = bytes(self._key, "latin-1") + self._key = b64encode(self._key).decode("ascii") + + path = uri.path + if not path: + path = "/" + + self._queue_str("GET %s HTTP/1.1\r\n" % path) + self._queue_str("Host: %s\r\n" % uri.hostname) + self._queue_str("Upgrade: websocket\r\n") + self._queue_str("Connection: upgrade\r\n") + self._queue_str("Sec-WebSocket-Key: %s\r\n" % self._key) + self._queue_str("Sec-WebSocket-Version: 13\r\n") + + if origin is not None: + self._queue_str("Origin: %s\r\n" % origin) + if len(protocols) > 0: + self._queue_str("Sec-WebSocket-Protocol: %s\r\n" % ", ".join(protocols)) + + self._queue_str("\r\n") + + self._state = "send_headers" + + if self._state == "send_headers": + self._flush() + self._state = "response" + + if self._state == "response": + if not self._recv(): + raise Exception("Socket closed unexpectedly") + + if self._recv_buffer.find('\r\n\r\n'.encode("ascii")) == -1: + raise WebSocketWantReadError + + (request, self._recv_buffer) = self._recv_buffer.split('\r\n'.encode("ascii"), 1) + request = request.decode("latin-1") + + words = request.split() + if (len(words) < 2) or (words[0] != "HTTP/1.1"): + raise Exception("Invalid response") + if words[1] != "101": + raise Exception("WebSocket request denied: %s" % " ".join(words[1:])) + + (headers, self._recv_buffer) = self._recv_buffer.split('\r\n\r\n'.encode("ascii"), 1) + headers = headers.decode('latin-1') + '\r\n' + headers = email.message_from_string(headers) + + if headers.get("Upgrade", "").lower() != "websocket": + print(type(headers)) + raise Exception("Missing or incorrect upgrade header") + + accept = headers.get('Sec-WebSocket-Accept') + if accept is None: + raise Exception("Missing Sec-WebSocket-Accept header"); + + expected = sha1((self._key + self.GUID).encode("ascii")).digest() + expected = b64encode(expected).decode("ascii") + + del self._key + + if accept != expected: + raise Exception("Invalid Sec-WebSocket-Accept header"); + + self.protocol = headers.get('Sec-WebSocket-Protocol') + if len(protocols) == 0: + if self.protocol is not None: + raise Exception("Unexpected Sec-WebSocket-Protocol header") + else: + if self.protocol not in protocols: + raise Exception("Invalid protocol chosen by server") + + self._state = "done" + + return + + raise Exception("WebSocket is in an invalid state") + + def accept(self, socket, headers): + """Establishes a new WebSocket session with a client. + + This method negotiates a WebSocket connection with an incoming + client. The caller must provide the client socket and the + headers from the HTTP request. + + A server can identify that a client is requesting a WebSocket + connection by looking at the "Upgrade" header. It will include + the value "websocket" in such cases. + + WebSocketWantWriteError can be raised if the response cannot be + sent right away. Repeated calls to accept() does not need to + retain the arguments. + """ + + # This is a state machine in order to handle + # WantRead/WantWrite events + + if self._state == "new": + self.client = False + self.socket = socket + + if headers.get("upgrade", "").lower() != "websocket": + raise Exception("Missing or incorrect upgrade header") + + ver = headers.get('Sec-WebSocket-Version') + if ver is None: + raise Exception("Missing Sec-WebSocket-Version header"); + + # HyBi-07 report version 7 + # HyBi-08 - HyBi-12 report version 8 + # HyBi-13 reports version 13 + if ver in ['7', '8', '13']: + self.version = "hybi-%02d" % int(ver) + else: + raise Exception("Unsupported protocol version %s" % ver) + + key = headers.get('Sec-WebSocket-Key') + if key is None: + raise Exception("Missing Sec-WebSocket-Key header"); + + # Generate the hash value for the accept header + accept = sha1((key + self.GUID).encode("ascii")).digest() + accept = b64encode(accept).decode("ascii") + + self.protocol = '' + protocols = headers.get('Sec-WebSocket-Protocol', '').split(',') + if protocols: + self.protocol = self.select_subprotocol(protocols) + # We are required to choose one of the protocols + # presented by the client + if self.protocol not in protocols: + raise Exception('Invalid protocol selected') + + self._queue_str("HTTP/1.1 101 Switching Protocols\r\n") + self._queue_str("Upgrade: websocket\r\n") + self._queue_str("Connection: Upgrade\r\n") + self._queue_str("Sec-WebSocket-Accept: %s\r\n" % accept) + + if self.protocol: + self._queue_str("Sec-WebSocket-Protocol: %s\r\n" % self.protocol) + + self._queue_str("\r\n") + + self._state = "flush" + + if self._state == "flush": + self._flush() + self._state = "done" + + return + + raise Exception("WebSocket is in an invalid state") + + def select_subprotocol(self, protocols): + """Returns which sub-protocol should be used. + + This method does not select any sub-protocol by default and is + meant to be overridden by an implementation that wishes to make + use of sub-protocols. It will be called during handling of + accept(). + """ + return "" + + def handle_ping(self, data): + """Called when a WebSocket ping message is received. + + This will be called whilst processing recv()/recvmsg(). The + default implementation sends a pong reply back.""" + self.pong(data) + + def handle_pong(self, data): + """Called when a WebSocket pong message is received. + + This will be called whilst processing recv()/recvmsg(). The + default implementation does nothing.""" + pass + + def recv(self): + """Read data from the WebSocket. + + This will return any available data on the socket (which may + be the empty string if the peer sent an empty message or + messages). If the socket is closed then None will be + returned. The reason for the close is found in the + 'close_code' and 'close_reason' properties. + + Unlike recvmsg() this method may return data from more than one + WebSocket message. It is however not guaranteed to return all + buffered data. Callers should continue calling recv() whilst + pending() returns True. + + Both WebSocketWantReadError and WebSocketWantWriteError can be + raised when calling recv(). + """ + return self.recvmsg() + + def recvmsg(self): + """Read a single message from the WebSocket. + + This will return a single WebSocket message from the socket + (which will be the empty string if the peer sent an empty + message). If the socket is closed then None will be + returned. The reason for the close is found in the + 'close_code' and 'close_reason' properties. + + Unlike recv() this method will not return data from more than + one WebSocket message. Callers should continue calling + recvmsg() whilst pending() returns True. + + Both WebSocketWantReadError and WebSocketWantWriteError can be + raised when calling recvmsg(). + """ + # May have been called to flush out a close + if self._received_close: + self._flush() + return None + + # Anything already queued? + if self.pending(): + return self._recvmsg() + # Note: If self._recvmsg() raised WebSocketWantReadError, + # we cannot proceed to self._recv() here as we may + # have already called it once as part of the caller's + # "while websock.pending():" loop + + # Nope, let's try to read a bit + if not self._recv_frames(): + return None + + # Anything queued now? + return self._recvmsg() + + def pending(self): + """Check if any WebSocket data is pending. + + This method will return True as long as there are WebSocket + frames that have yet been processed. A single recv() from the + underlying socket may return multiple WebSocket frames and it + is therefore important that a caller continues calling recv() + or recvmsg() as long as pending() returns True. + + Note that this function merely tells if there are raw WebSocket + frames pending. Those frames may not contain any application + data. + """ + return len(self._recv_queue) > 0 + + def send(self, bytes): + """Write data to the WebSocket + + This will queue the given data and attempt to send it to the + peer. Unlike sendmsg() this method might coalesce the data with + data from other calls, or split it over multiple messages. + + WebSocketWantWriteError can be raised if there is insufficient + space in the underlying socket. + """ + return self.sendmsg(bytes) + + def sendmsg(self, msg): + """Write a single message to the WebSocket + + This will queue the given message and attempt to send it to the + peer. Unlike send() this method will preserve the data as a + single WebSocket message. + + WebSocketWantWriteError can be raised if there is insufficient + space in the underlying socket. + """ + if not self._sent_close: + # Only called to flush? + if msg: + self._sendmsg(0x2, msg) + + self._flush() + return len(msg) + + def ping(self, data=''.encode('ascii')): + """Write a ping message to the WebSocket.""" + self._sendmsg(0x9, data) + + def pong(self, data=''.encode('ascii')): + """Write a pong message to the WebSocket.""" + self._sendmsg(0xA, data) + + def shutdown(self, how, code=1000, reason=None): + """Gracefully terminate the WebSocket connection. + + This will start the process to terminate the WebSocket + connection. The caller must continue to calling recv() or + recvmsg() after this function in order to wait for the peer to + acknowledge the close. Calls to send() and sendmsg() will be + ignored. + + WebSocketWantWriteError can be raised if there is insufficient + space in the underlying socket for the close message. + + The how argument is currently ignored. + """ + + # Already closing? + if self._sent_close: + self._flush() + return + + # Special code to indicate that we closed the connection + if not self._received_close: + self.close_code = 1000 + self.close_reason = "Locally initiated close" + + self._sent_close = True + + msg = ''.encode('ascii') + if code is not None: + msg += struct.pack(">H", code) + if reason is not None: + msg += reason.encode("UTF-8") + + self._sendmsg(0x8, msg) + + def close(self, code=1000, reason=None): + """Terminate the WebSocket connection immediately. + + This will close the WebSocket connection directly after sending + a close message to the peer. + + WebSocketWantWriteError can be raised if there is insufficient + space in the underlying socket for the close message. + """ + self.shutdown(socket.SHUT_RDWR, code, reason) + self._close() + + def _recv(self): + # Fetches more data from the socket to the buffer + assert self.socket is not None + + while True: + try: + data = self.socket.recv(4096) + except (socket.error, OSError): + exc = sys.exc_info()[1] + if hasattr(exc, 'errno'): + err = exc.errno + else: + err = exc[0] + + if err == errno.EWOULDBLOCK: + raise WebSocketWantReadError + + raise + + if len(data) == 0: + return False + + self._recv_buffer += data + + # Support for SSLSocket like objects + if hasattr(self.socket, "pending"): + if not self.socket.pending(): + break + else: + break + + return True + + def _recv_frames(self): + # Fetches more data and decodes the frames + if not self._recv(): + if self.close_code is None: + self.close_code = 1006 + self.close_reason = "Connection closed abnormally" + self._sent_close = self._received_close = True + self._close() + return False + + while True: + frame = self._decode_hybi(self._recv_buffer) + if frame is None: + break + self._recv_buffer = self._recv_buffer[frame['length']:] + self._recv_queue.append(frame) + + return True + + def _recvmsg(self): + # Process pending frames and returns any application data + while self._recv_queue: + frame = self._recv_queue.pop(0) + + if not self.client and not frame['masked']: + self.shutdown(socket.SHUT_RDWR, 1002, "Procotol error: Frame not masked") + continue + if self.client and frame['masked']: + self.shutdown(socket.SHUT_RDWR, 1002, "Procotol error: Frame masked") + continue + + if frame["opcode"] == 0x0: + if not self._partial_msg: + self.shutdown(socket.SHUT_RDWR, 1002, "Procotol error: Unexpected continuation frame") + continue + + self._partial_msg += frame["payload"] + + if frame["fin"]: + msg = self._partial_msg + self._partial_msg = ''.decode("ascii") + return msg + elif frame["opcode"] == 0x1: + self.shutdown(socket.SHUT_RDWR, 1003, "Unsupported: Text frames are not supported") + elif frame["opcode"] == 0x2: + if self._partial_msg: + self.shutdown(socket.SHUT_RDWR, 1002, "Procotol error: Unexpected new frame") + continue + + if frame["fin"]: + return frame["payload"] + else: + self._partial_msg = frame["payload"] + elif frame["opcode"] == 0x8: + if self._received_close: + continue + + self._received_close = True + + if self._sent_close: + self._close() + return None + + if not frame["fin"]: + self.shutdown(socket.SHUT_RDWR, 1003, "Unsupported: Fragmented close") + continue + + code = None + reason = None + if len(frame["payload"]) >= 2: + code = struct.unpack(">H", frame["payload"][:2])[0] + if len(frame["payload"]) > 2: + reason = frame["payload"][2:] + try: + reason = reason.decode("UTF-8") + except UnicodeDecodeError: + self.shutdown(socket.SHUT_RDWR, 1002, "Procotol error: Invalid UTF-8 in close") + continue + + if code is None: + self.close_code = code = 1005 + self.close_reason = "No close status code specified by peer" + else: + self.close_code = code + if reason is not None: + self.close_reason = reason + + self.shutdown(None, code, reason) + return None + elif frame["opcode"] == 0x9: + if not frame["fin"]: + self.shutdown(socket.SHUT_RDWR, 1003, "Unsupported: Fragmented ping") + continue + + self.handle_ping(frame["payload"]) + elif frame["opcode"] == 0xA: + if not frame["fin"]: + self.shutdown(socket.SHUT_RDWR, 1003, "Unsupported: Fragmented pong") + continue + + self.handle_pong(frame["payload"]) + else: + self.shutdown(socket.SHUT_RDWR, 1003, "Unsupported: Unknown opcode 0x%02x" % frame["opcode"]) + + raise WebSocketWantReadError + + def _flush(self): + # Writes pending data to the socket + if not self._send_buffer: + return + + assert self.socket is not None + + try: + sent = self.socket.send(self._send_buffer) + except (socket.error, OSError): + exc = sys.exc_info()[1] + if hasattr(exc, 'errno'): + err = exc.errno + else: + err = exc[0] + + if err == errno.EWOULDBLOCK: + raise WebSocketWantWriteError + + raise + + self._send_buffer = self._send_buffer[sent:] + + if self._send_buffer: + raise WebSocketWantWriteError + + # We had a pending close and we've flushed the buffer, + # time to end things + if self._received_close and self._sent_close: + self._close() + + def _send(self, data): + # Queues data and attempts to send it + self._send_buffer += data + self._flush() + + def _queue_str(self, string): + # Queue some data to be sent later. + # Only used by the connecting methods. + self._send_buffer += string.encode("latin-1") + + def _sendmsg(self, opcode, msg): + # Sends a standard data message + if self.client: + mask = '' + for i in range(4): + mask += chr(random.randrange(256)) + if sys.hexversion >= 0x3000000: + mask = bytes(mask, "latin-1") + frame = self._encode_hybi(opcode, msg, mask) + else: + frame = self._encode_hybi(opcode, msg) + + return self._send(frame) + + def _close(self): + # Close the underlying socket + self.socket.close() + self.socket = None + + def _mask(self, buf, mask): + # Mask a frame + return self._unmask(buf, mask) + + def _unmask(self, buf, mask): + # Unmask a frame + if numpy: + plen = len(buf) + pstart = 0 + pend = plen + b = c = ''.encode('ascii') + if plen >= 4: + dtype=numpy.dtype('') + mask = numpy.frombuffer(mask, dtype, count=1) + data = numpy.frombuffer(buf, dtype, count=int(plen / 4)) + #b = numpy.bitwise_xor(data, mask).data + b = numpy.bitwise_xor(data, mask).tostring() + + if plen % 4: + dtype=numpy.dtype('B') + if sys.byteorder == 'big': + dtype = dtype.newbyteorder('>') + mask = numpy.frombuffer(mask, dtype, count=(plen % 4)) + data = numpy.frombuffer(buf, dtype, + offset=plen - (plen % 4), count=(plen % 4)) + c = numpy.bitwise_xor(data, mask).tostring() + return b + c + else: + # Slower fallback + if sys.hexversion < 0x3000000: + mask = [ ord(c) for c in mask ] + data = array.array('B') + data.fromstring(buf) + for i in range(len(data)): + data[i] ^= mask[i % 4] + return data.tostring() + + def _encode_hybi(self, opcode, buf, mask_key=None, fin=True): + """ Encode a HyBi style WebSocket frame. + Optional opcode: + 0x0 - continuation + 0x1 - text frame + 0x2 - binary frame + 0x8 - connection close + 0x9 - ping + 0xA - pong + """ + + b1 = opcode & 0x0f + if fin: + b1 |= 0x80 + + mask_bit = 0 + if mask_key is not None: + mask_bit = 0x80 + buf = self._mask(buf, mask_key) + + payload_len = len(buf) + if payload_len <= 125: + header = struct.pack('>BB', b1, payload_len | mask_bit) + elif payload_len > 125 and payload_len < 65536: + header = struct.pack('>BBH', b1, 126 | mask_bit, payload_len) + elif payload_len >= 65536: + header = struct.pack('>BBQ', b1, 127 | mask_bit, payload_len) + + if mask_key is not None: + return header + mask_key + buf + else: + return header + buf + + def _decode_hybi(self, buf): + """ Decode HyBi style WebSocket packets. + Returns: + {'fin' : boolean, + 'opcode' : number, + 'masked' : boolean, + 'length' : encoded_length, + 'payload' : decoded_buffer} + """ + + f = {'fin' : 0, + 'opcode' : 0, + 'masked' : False, + 'length' : 0, + 'payload' : None} + + blen = len(buf) + hlen = 2 + + if blen < hlen: + return None + + b1, b2 = struct.unpack(">BB", buf[:2]) + f['opcode'] = b1 & 0x0f + f['fin'] = not not (b1 & 0x80) + f['masked'] = not not (b2 & 0x80) + + if f['masked']: + hlen += 4 + if blen < hlen: + return None + + length = b2 & 0x7f + + if length == 126: + hlen += 2 + if blen < hlen: + return None + length, = struct.unpack('>H', buf[2:4]) + elif length == 127: + hlen += 8 + if blen < hlen: + return None + length, = struct.unpack('>Q', buf[2:10]) + + f['length'] = hlen + length + + if blen < f['length']: + return None + + if f['masked']: + # unmask payload + mask_key = buf[hlen-4:hlen] + f['payload'] = self._unmask(buf[hlen:(hlen+length)], mask_key) + else: + f['payload'] = buf[hlen:(hlen+length)] + + return f + diff --git a/systemvm/debian/root/noVNC/utils/websockify/websockify/websocketproxy.py b/systemvm/debian/root/noVNC/utils/websockify/websockify/websocketproxy.py new file mode 100644 index 000000000000..3d49d19c0433 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/websockify/websocketproxy.py @@ -0,0 +1,942 @@ +#!/usr/bin/env python + +''' +A WebSocket to TCP socket proxy with support for "wss://" encryption. +Copyright 2011 Joel Martin +Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3) + +You can make a cert/key with openssl using: +openssl req -new -x509 -days 365 -nodes -out self.pem -keyout self.pem +as taken from http://docs.python.org/dev/library/ssl.html#certificates + +''' + +import signal, socket, optparse, time, os, sys, subprocess, logging, errno, ssl +try: + from socketserver import ThreadingMixIn +except ImportError: + from SocketServer import ThreadingMixIn + +try: + from http.server import HTTPServer +except ImportError: + from BaseHTTPServer import HTTPServer + +import select + +import sys +sys.path.insert(0, r'/root/noVNC/utils') +sys.path.insert(1, r'/root/noVNC/utils/websockify') +from websockify import websockifyserver +from websockify import auth_plugins as auth +import pyDes + +try: + from urllib.parse import parse_qs, urlparse +except ImportError: + from cgi import parse_qs + from urlparse import urlparse + +from rijndael_cs import decrypt +from hashlib import sha256 +from time import time + +class ProxyRequestHandler(websockifyserver.WebSockifyRequestHandler): + + buffer_size = 65536 + + traffic_legend = """ +Traffic Legend: + } - Client receive + }. - Client receive partial + { - Target receive + + > - Target send + >. - Target send partial + < - Client send + <. - Client send partial +""" + + def send_auth_error(self, ex): + self.send_response(ex.code, ex.msg) + self.send_header('Content-Type', 'text/html') + for name, val in ex.headers.items(): + self.send_header(name, val) + + self.end_headers() + + def validate_connection(self): + if not self.server.token_plugin: + return + + host, port = self.get_target(self.server.token_plugin) + if host == 'unix_socket': + self.server.unix_target = port + + else: + self.server.target_host = host + self.server.target_port = port + + def auth_connection(self): + if not self.server.auth_plugin: + return + + try: + # get client certificate data + client_cert_data = self.request.getpeercert() + # extract subject information + client_cert_subject = client_cert_data['subject'] + # flatten data structure + client_cert_subject = dict([x[0] for x in client_cert_subject]) + # add common name to headers (apache +StdEnvVars style) + self.headers['SSL_CLIENT_S_DN_CN'] = client_cert_subject['commonName'] + except (TypeError, AttributeError, KeyError): + # not a SSL connection or client presented no certificate with valid data + pass + + try: + self.server.auth_plugin.authenticate( + headers=self.headers, target_host=self.server.target_host, + target_port=self.server.target_port) + except auth.AuthenticationError: + ex = sys.exc_info()[1] + self.send_auth_error(ex) + raise + + def new_websocket_client(self): + """ + Called after a new WebSocket connection has been established. + """ + + self.novnc_key = 'default.novnc.encryption.key' + try: + arguments = decrypt(self.novnc_key.ljust(32, '\0'), self.path.lstrip('/')) + arguments, signature = arguments.split('|') + self.target_host, self.target_port, self.client_ip, self.timestamp, self.password = arguments.rsplit(':', 4) + except Exception, e: + arguments = decrypt(self.novnc_key[:32], self.path.lstrip('/')) + arguments, signature = arguments.split('|') + self.target_host, self.target_port, self.client_ip, self.timestamp, self.password = arguments.rsplit(':', 4) + + # Check signature + if sha256(self.novnc_key+arguments).hexdigest() != signature: + self.send_close() + raise self.server.EClose('Not accepted: Invalid signature') + # Check client ip address + if self.client_ip != self.client_address[0]: + self.log_message("Mismatched client ip (%s to management and %s to console)" % (self.client_ip, self.client_address[0])) + self.send_close() + raise self.server.EClose('Not accepted: Invalid client ip address') + # Check timestamp + if int(self.timestamp) < time()-300: + self.send_close() + raise self.server.EClose('Not accepted: Timestamp older than 5 minutes') + + # Checking for a token is done in validate_connection() + + # Connect to the target + if self.server.wrap_cmd: + msg = "connecting to command: '%s' (port %s)" % (" ".join(self.server.wrap_cmd), self.server.target_port) + elif self.server.unix_target: + msg = "connecting to unix socket: %s" % self.server.unix_target + else: + msg = "connecting to: %s:%s" % ( + self.target_host, self.target_port) + + if self.server.ssl_target: + msg += " (using SSL)" + self.log_message(msg) + + try: + tsock = websockifyserver.WebSockifyServer.socket(self.target_host, + self.target_port, + connect=True, + use_ssl=self.server.ssl_target, + unix_socket=self.server.unix_target) + except Exception: + self.log_message("Failed to connect to %s:%s", + self.target_host, self.target_port) + raise self.CClose(1011, "Failed to connect to downstream server") + + self.request.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) + if not self.server.wrap_cmd and not self.server.unix_target: + tsock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) + + self.print_traffic(self.traffic_legend) + + # Start authenticating + try: + self.do_vnc_auth(tsock, self.password) + except: + if tsock: + tsock.shutdown(socket.SHUT_RDWR) + tsock.close() + if self.verbose: + self.log_message("%s:%s: Closed target", + self.server.target_host, self.server.target_port) + raise + + # Start proxying + try: + self.do_proxy(tsock) + finally: + if tsock: + tsock.shutdown(socket.SHUT_RDWR) + tsock.close() + if self.verbose: + self.log_message("%s:%s: Closed target", + self.server.target_host, self.server.target_port) + + def get_target(self, target_plugin): + """ + Gets a token from either the path or the host, + depending on --host-token, and looks up a target + for that token using the token plugin. Used by + validate_connection() to set target_host and target_port. + """ + # The files in targets contain the lines + # in the form of token: host:port + + if self.host_token: + # Use hostname as token + token = self.headers.get('Host') + + # Remove port from hostname, as it'll always be the one where + # websockify listens (unless something between the client and + # websockify is redirecting traffic, but that's beside the point) + if token: + token = token.partition(':')[0] + + else: + # Extract the token parameter from url + args = parse_qs(urlparse(self.path)[4]) # 4 is the query from url + + if 'token' in args and len(args['token']): + token = args['token'][0].rstrip('\n') + else: + token = None + + if token is None: + raise self.server.EClose("Token not present") + + result_pair = target_plugin.lookup(token) + + if result_pair is not None: + return result_pair + else: + raise self.server.EClose("Token '%s' not found" % token) + + def do_vnc_auth(self, target, password): + self.log_message("Start VNC authenticating ...") + + state = "protocol_version" + tqueue = [] + rlist = [target] + + if self.server.heartbeat: + now = time.time() + self.heartbeat = now + self.server.heartbeat + else: + self.heartbeat = None + + while True: + wlist = [] + if self.heartbeat is not None: + now = time.time() + if now > self.heartbeat: + self.heartbeat = now + self.server.heartbeat + self.send_ping() + + if tqueue: wlist.append(target) + try: + ins, outs, excepts = select.select(rlist, wlist, [], 1) + except (select.error, OSError): + exc = sys.exc_info()[1] + if hasattr(exc, 'errno'): + err = exc.errno + else: + err = exc[0] + + if err != errno.EINTR: + raise + else: + continue + + if excepts: raise Exception("Socket exception") + + if target in outs: + # Send queued client data to the target + dat = tqueue.pop(0) + sent = target.send(dat) + if sent == len(dat): + self.print_traffic(">") + if state == "vnc_auth_done": + break + else: + # requeue the remaining data + tqueue.insert(0, dat[sent:]) + self.print_traffic(".>") + + if target in ins: + # Receive target data, encode it and queue for client + buf = target.recv(self.buffer_size) + if len(buf) == 0: + if self.verbose: + self.log_message("%s:%s: Target closed connection", + self.server.target_host, self.server.target_port) + raise self.CClose(1000, "Target closed") + if state == "protocol_version": + protocol_version = str(buf).rstrip() + self.log_message("Protocol version is %s" % protocol_version) + if not protocol_version.startswith("RFB 003"): + raise Exception("Protocol version %s is not supported" % protocol_version) + tqueue.append("RFB 003.008\n") + state = "security_info" + elif state == "security_info": + security_codes = [ord(c) for c in buf] + security_num_type = security_codes[0] + if security_num_type == 0: + raise Exception("Security failure") + elif security_num_type != 1: + raise Exception("Unsupported security type") + rfb_auth_scheme = security_codes[1] + self.log_message("Authenticating using scheme: %s" % rfb_auth_scheme) + if rfb_auth_scheme == 0: + raise Exception("Auth failure") + elif rfb_auth_scheme == 1: + self.log_message("No auth required") + break + elif rfb_auth_scheme == 2: + tqueue.append("\x02") + else: + raise Exception("Authentication scheme %s unsupported" % rfb_auth_scheme) + state = "vnc_auth" + elif state == "vnc_auth": + challenge = buf + key = [ord(c) for c in password[:8]] + secretkey = [] + for key_byte in key: + key_bin = bin(key_byte) + key_reverse = key_bin[-1:1:-1] + key_reverse = key_reverse + (8 - len(key_reverse))*'0' + secretkey_int=int(key_reverse,2) + secretkey.append(secretkey_int) + k = pyDes.des(str(bytearray(secretkey)), pyDes.ECB, IV=None, pad=None, padmode=None) + d = k.encrypt(challenge) + tqueue.append(d) + state = "vnc_auth_result" + elif state == "vnc_auth_result": + vnc_auth_result = [ord(c) for c in buf] + vnc_auth_code = str(vnc_auth_result[0]) + str(vnc_auth_result[1]) + str(vnc_auth_result[2]) + str(vnc_auth_result[3]) + if vnc_auth_code == "0000": + self.log_message("Authentication OK") + tqueue.append("\x01") + elif vnc_auth_code == "0001": + raise Exception("Authentication failed") + elif vnc_auth_code == "0002": + raise Exception("Too many auth attempts") + else: + raise Exception("Unknown Authentication result") + state = "vnc_auth_done" + + self.print_traffic("{") + + def do_proxy(self, target): + """ + Proxy client WebSocket to normal target socket. + """ + cqueue = [] + c_pend = 0 + tqueue = [] + rlist = [self.request, target] + + if self.server.heartbeat: + now = time.time() + self.heartbeat = now + self.server.heartbeat + else: + self.heartbeat = None + + while True: + wlist = [] + + if self.heartbeat is not None: + now = time.time() + if now > self.heartbeat: + self.heartbeat = now + self.server.heartbeat + self.send_ping() + + if tqueue: wlist.append(target) + if cqueue or c_pend: wlist.append(self.request) + try: + ins, outs, excepts = select.select(rlist, wlist, [], 1) + except (select.error, OSError): + exc = sys.exc_info()[1] + if hasattr(exc, 'errno'): + err = exc.errno + else: + err = exc[0] + + if err != errno.EINTR: + raise + else: + continue + + if excepts: raise Exception("Socket exception") + + if self.request in outs: + # Send queued target data to the client + c_pend = self.send_frames(cqueue) + + cqueue = [] + + if self.request in ins: + # Receive client data, decode it, and queue for target + bufs, closed = self.recv_frames() + tqueue.extend(bufs) + + if closed: + # TODO: What about blocking on client socket? + if self.verbose: + self.log_message("%s:%s: Client closed connection", + self.server.target_host, self.server.target_port) + raise self.CClose(closed['code'], closed['reason']) + + + if target in outs: + # Send queued client data to the target + dat = tqueue.pop(0) + sent = target.send(dat) + if sent == len(dat): + self.print_traffic(">") + else: + # requeue the remaining data + tqueue.insert(0, dat[sent:]) + self.print_traffic(".>") + + + if target in ins: + # Receive target data, encode it and queue for client + buf = target.recv(self.buffer_size) + if len(buf) == 0: + if self.verbose: + self.log_message("%s:%s: Target closed connection", + self.server.target_host, self.server.target_port) + raise self.CClose(1000, "Target closed") + + cqueue.append(buf) + self.print_traffic("{") + +class WebSocketProxy(websockifyserver.WebSockifyServer): + """ + Proxy traffic to and from a WebSockets client to a normal TCP + socket server target. + """ + + buffer_size = 65536 + + def __init__(self, RequestHandlerClass=ProxyRequestHandler, *args, **kwargs): + # Save off proxy specific options + self.target_host = kwargs.pop('target_host', None) + self.target_port = kwargs.pop('target_port', None) + self.wrap_cmd = kwargs.pop('wrap_cmd', None) + self.wrap_mode = kwargs.pop('wrap_mode', None) + self.unix_target = kwargs.pop('unix_target', None) + self.ssl_target = kwargs.pop('ssl_target', None) + self.heartbeat = kwargs.pop('heartbeat', None) + + self.token_plugin = kwargs.pop('token_plugin', None) + self.host_token = kwargs.pop('host_token', None) + self.auth_plugin = kwargs.pop('auth_plugin', None) + + # Last 3 timestamps command was run + self.wrap_times = [0, 0, 0] + + if self.wrap_cmd: + wsdir = os.path.dirname(sys.argv[0]) + rebinder_path = [os.path.join(wsdir, "..", "lib"), + os.path.join(wsdir, "..", "lib", "websockify"), + wsdir] + self.rebinder = None + + for rdir in rebinder_path: + rpath = os.path.join(rdir, "rebind.so") + if os.path.exists(rpath): + self.rebinder = rpath + break + + if not self.rebinder: + raise Exception("rebind.so not found, perhaps you need to run make") + self.rebinder = os.path.abspath(self.rebinder) + + self.target_host = "127.0.0.1" # Loopback + # Find a free high port + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.bind(('', 0)) + self.target_port = sock.getsockname()[1] + sock.close() + + os.environ.update({ + "LD_PRELOAD": self.rebinder, + "REBIND_OLD_PORT": str(kwargs['listen_port']), + "REBIND_NEW_PORT": str(self.target_port)}) + + websockifyserver.WebSockifyServer.__init__(self, RequestHandlerClass, *args, **kwargs) + + def run_wrap_cmd(self): + self.msg("Starting '%s'", " ".join(self.wrap_cmd)) + self.wrap_times.append(time.time()) + self.wrap_times.pop(0) + self.cmd = subprocess.Popen( + self.wrap_cmd, env=os.environ, preexec_fn=_subprocess_setup) + self.spawn_message = True + + def started(self): + """ + Called after Websockets server startup (i.e. after daemonize) + """ + # Need to call wrapped command after daemonization so we can + # know when the wrapped command exits + if self.wrap_cmd: + dst_string = "'%s' (port %s)" % (" ".join(self.wrap_cmd), self.target_port) + elif self.unix_target: + dst_string = self.unix_target + else: + dst_string = "%s:%s" % (self.target_host, self.target_port) + + if self.listen_fd != None: + src_string = "inetd" + else: + src_string = "%s:%s" % (self.listen_host, self.listen_port) + + if self.token_plugin: + msg = " - proxying from %s to targets generated by %s" % ( + src_string, type(self.token_plugin).__name__) + else: + msg = " - proxying from %s to %s" % ( + src_string, dst_string) + + if self.ssl_target: + msg += " (using SSL)" + + self.msg("%s", msg) + + if self.wrap_cmd: + self.run_wrap_cmd() + + def poll(self): + # If we are wrapping a command, check it's status + + if self.wrap_cmd and self.cmd: + ret = self.cmd.poll() + if ret != None: + self.vmsg("Wrapped command exited (or daemon). Returned %s" % ret) + self.cmd = None + + if self.wrap_cmd and self.cmd == None: + # Response to wrapped command being gone + if self.wrap_mode == "ignore": + pass + elif self.wrap_mode == "exit": + sys.exit(ret) + elif self.wrap_mode == "respawn": + now = time.time() + avg = sum(self.wrap_times)/len(self.wrap_times) + if (now - avg) < 10: + # 3 times in the last 10 seconds + if self.spawn_message: + self.warn("Command respawning too fast") + self.spawn_message = False + else: + self.run_wrap_cmd() + + +def _subprocess_setup(): + # Python installs a SIGPIPE handler by default. This is usually not what + # non-Python successfulbprocesses expect. + signal.signal(signal.SIGPIPE, signal.SIG_DFL) + + +try : + # First try SSL options for Python 3.4 and above + SSL_OPTIONS = { + 'default': ssl.OP_ALL, + 'tlsv1_1': ssl.PROTOCOL_TLS | ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | + ssl.OP_NO_TLSv1, + 'tlsv1_2': ssl.PROTOCOL_TLS | ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | + ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1, + 'tlsv1_3': ssl.PROTOCOL_TLS | ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | + ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_TLSv1_2, + } +except AttributeError: + try: + # Python 3.3 uses a different scheme for SSL options + # tlsv1_3 is not supported on older Python versions + SSL_OPTIONS = { + 'default': ssl.OP_ALL, + 'tlsv1_1': ssl.PROTOCOL_TLSv1 | ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | + ssl.OP_NO_TLSv1, + 'tlsv1_2': ssl.PROTOCOL_TLSv1 | ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | + ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1, + } + except AttributeError: + # Python 2.6 does not support TLS v1.2, and uses a different scheme + # for SSL options + SSL_OPTIONS = { + 'default': ssl.PROTOCOL_SSLv23, + 'tlsv1_1': ssl.PROTOCOL_TLSv1, + } + +def select_ssl_version(version): + """Returns SSL options for the most secure TSL version available on this + Python version""" + if version in SSL_OPTIONS: + return SSL_OPTIONS[version] + else: + # It so happens that version names sorted lexicographically form a list + # from the least to the most secure + keys = list(SSL_OPTIONS.keys()) + keys.sort() + fallback = keys[-1] + logger = logging.getLogger(WebSocketProxy.log_prefix) + logger.warn("TLS version %s unsupported. Falling back to %s", + version, fallback) + + return SSL_OPTIONS[fallback] + +def websockify_init(): + # Setup basic logging to stderr. + logger = logging.getLogger(WebSocketProxy.log_prefix) + logger.propagate = False + logger.setLevel(logging.INFO) + stderr_handler = logging.StreamHandler() + stderr_handler.setLevel(logging.DEBUG) + log_formatter = logging.Formatter("%(message)s") + stderr_handler.setFormatter(log_formatter) + logger.addHandler(stderr_handler) + + # Setup optparse. + usage = "\n %prog [options]" + usage += " [source_addr:]source_port [target_addr:target_port]" + usage += "\n %prog [options]" + usage += " [source_addr:]source_port -- WRAP_COMMAND_LINE" + parser = optparse.OptionParser(usage=usage) + parser.add_option("--verbose", "-v", action="store_true", + help="verbose messages") + parser.add_option("--traffic", action="store_true", + help="per frame traffic") + parser.add_option("--record", + help="record sessions to FILE.[session_number]", metavar="FILE") + parser.add_option("--daemon", "-D", + dest="daemon", action="store_true", + help="become a daemon (background process)") + parser.add_option("--run-once", action="store_true", + help="handle a single WebSocket connection and exit") + parser.add_option("--timeout", type=int, default=0, + help="after TIMEOUT seconds exit when not connected") + parser.add_option("--idle-timeout", type=int, default=0, + help="server exits after TIMEOUT seconds if there are no " + "active connections") + parser.add_option("--cert", default="self.pem", + help="SSL certificate file") + parser.add_option("--key", default=None, + help="SSL key file (if separate from cert)") + parser.add_option("--key-password", default=None, + help="SSL key password") + parser.add_option("--ssl-only", action="store_true", + help="disallow non-encrypted client connections") + parser.add_option("--ssl-target", action="store_true", + help="connect to SSL target as SSL client") + parser.add_option("--verify-client", action="store_true", + help="require encrypted client to present a valid certificate " + "(needs Python 2.7.9 or newer or Python 3.4 or newer)") + parser.add_option("--cafile", metavar="FILE", + help="file of concatenated certificates of authorities trusted " + "for validating clients (only effective with --verify-client). " + "If omitted, system default list of CAs is used.") + parser.add_option("--ssl-version", type="choice", default="default", + choices=["default", "tlsv1_1", "tlsv1_2", "tlsv1_3"], action="store", + help="minimum TLS version to use (default, tlsv1_1, tlsv1_2, tlsv1_3)") + parser.add_option("--ssl-ciphers", action="store", + help="list of ciphers allowed for connection. For a list of " + "supported ciphers run `openssl ciphers`") + parser.add_option("--unix-target", + help="connect to unix socket target", metavar="FILE") + parser.add_option("--inetd", + help="inetd mode, receive listening socket from stdin", action="store_true") + parser.add_option("--web", default=None, metavar="DIR", + help="run webserver on same port. Serve files from DIR.") + parser.add_option("--web-auth", action="store_true", + help="require authentication to access webserver.") + parser.add_option("--wrap-mode", default="exit", metavar="MODE", + choices=["exit", "ignore", "respawn"], + help="action to take when the wrapped program exits " + "or daemonizes: exit (default), ignore, respawn") + parser.add_option("--prefer-ipv6", "-6", + action="store_true", dest="source_is_ipv6", + help="prefer IPv6 when resolving source_addr") + parser.add_option("--libserver", action="store_true", + help="use Python library SocketServer engine") + parser.add_option("--target-config", metavar="FILE", + dest="target_cfg", + help="Configuration file containing valid targets " + "in the form 'token: host:port' or, alternatively, a " + "directory containing configuration files of this form " + "(DEPRECATED: use `--token-plugin TokenFile --token-source " + " path/to/token/file` instead)") + parser.add_option("--token-plugin", default=None, metavar="CLASS", + help="use a Python class, usually one from websockify.token_plugins, " + "such as TokenFile, to process tokens into host:port pairs") + parser.add_option("--token-source", default=None, metavar="ARG", + help="an argument to be passed to the token plugin " + "on instantiation") + parser.add_option("--host-token", action="store_true", + help="use the host HTTP header as token instead of the " + "token URL query parameter") + parser.add_option("--auth-plugin", default=None, metavar="CLASS", + help="use a Python class, usually one from websockify.auth_plugins, " + "such as BasicHTTPAuth, to determine if a connection is allowed") + parser.add_option("--auth-source", default=None, metavar="ARG", + help="an argument to be passed to the auth plugin " + "on instantiation") + parser.add_option("--heartbeat", type=int, default=0, metavar="INTERVAL", + help="send a ping to the client every INTERVAL seconds") + parser.add_option("--log-file", metavar="FILE", + dest="log_file", + help="File where logs will be saved") + parser.add_option("--syslog", default=None, metavar="SERVER", + help="Log to syslog server. SERVER can be local socket, " + "such as /dev/log, or a UDP host:port pair.") + parser.add_option("--legacy-syslog", action="store_true", + help="Use the old syslog protocol instead of RFC 5424. " + "Use this if the messages produced by websockify seem abnormal.") + + (opts, args) = parser.parse_args() + + + # Validate options. + + if opts.token_source and not opts.token_plugin: + parser.error("You must use --token-plugin to use --token-source") + + if opts.host_token and not opts.token_plugin: + parser.error("You must use --token-plugin to use --host-token") + + if opts.auth_source and not opts.auth_plugin: + parser.error("You must use --auth-plugin to use --auth-source") + + if opts.web_auth and not opts.auth_plugin: + parser.error("You must use --auth-plugin to use --web-auth") + + if opts.web_auth and not opts.web: + parser.error("You must use --web to use --web-auth") + + if opts.legacy_syslog and not opts.syslog: + parser.error("You must use --syslog to use --legacy-syslog") + + + opts.ssl_options = select_ssl_version(opts.ssl_version) + del opts.ssl_version + + + if opts.log_file: + # Setup logging to user-specified file. + opts.log_file = os.path.abspath(opts.log_file) + log_file_handler = logging.FileHandler(opts.log_file) + log_file_handler.setLevel(logging.DEBUG) + log_file_handler.setFormatter(log_formatter) + logger.addHandler(log_file_handler) + + del opts.log_file + + if opts.syslog: + # Determine how to connect to syslog... + if opts.syslog.count(':'): + # User supplied a host:port pair. + syslog_host, syslog_port = opts.syslog.rsplit(':', 1) + try: + syslog_port = int(syslog_port) + except ValueError: + parser.error("Error parsing syslog port") + syslog_dest = (syslog_host, syslog_port) + else: + # User supplied a local socket file. + syslog_dest = os.path.abspath(opts.syslog) + + from websockify.sysloghandler import WebsockifySysLogHandler + + # Determine syslog facility. + if opts.daemon: + syslog_facility = WebsockifySysLogHandler.LOG_DAEMON + else: + syslog_facility = WebsockifySysLogHandler.LOG_USER + + # Start logging to syslog. + syslog_handler = WebsockifySysLogHandler(address=syslog_dest, + facility=syslog_facility, + ident='websockify', + legacy=opts.legacy_syslog) + syslog_handler.setLevel(logging.DEBUG) + syslog_handler.setFormatter(log_formatter) + logger.addHandler(syslog_handler) + + del opts.syslog + del opts.legacy_syslog + + if opts.verbose: + logger.setLevel(logging.DEBUG) + + + # Transform to absolute path as daemon may chdir + if opts.target_cfg: + opts.target_cfg = os.path.abspath(opts.target_cfg) + + if opts.target_cfg: + opts.token_plugin = 'TokenFile' + opts.token_source = opts.target_cfg + + del opts.target_cfg + + if sys.argv.count('--'): + opts.wrap_cmd = args[1:] + else: + opts.wrap_cmd = None + + if not websockifyserver.ssl and opts.ssl_target: + parser.error("SSL target requested and Python SSL module not loaded."); + + if opts.ssl_only and not os.path.exists(opts.cert): + parser.error("SSL only and %s not found" % opts.cert) + + if opts.inetd: + opts.listen_fd = sys.stdin.fileno() + else: + if len(args) < 1: + parser.error("Too few arguments") + arg = args.pop(0) + # Parse host:port and convert ports to numbers + if arg.count(':') > 0: + opts.listen_host, opts.listen_port = arg.rsplit(':', 1) + opts.listen_host = opts.listen_host.strip('[]') + else: + opts.listen_host, opts.listen_port = '', arg + + try: + opts.listen_port = int(opts.listen_port) + except ValueError: + parser.error("Error parsing listen port") + + del opts.inetd + + if opts.wrap_cmd or opts.unix_target or opts.token_plugin: + opts.target_host = None + opts.target_port = None + elif len(args) >= 1: + arg = args.pop(0) + if arg.count(':') > 0: + opts.target_host, opts.target_port = arg.rsplit(':', 1) + opts.target_host = opts.target_host.strip('[]') + else: + parser.error("Error parsing target") + + try: + opts.target_port = int(opts.target_port) + except ValueError: + parser.error("Error parsing target port") + + if len(args) > 1 and opts.wrap_cmd == None: + parser.error("Too many arguments") + + if opts.token_plugin is not None: + if '.' not in opts.token_plugin: + opts.token_plugin = ( + 'websockify.token_plugins.%s' % opts.token_plugin) + + token_plugin_module, token_plugin_cls = opts.token_plugin.rsplit('.', 1) + + __import__(token_plugin_module) + token_plugin_cls = getattr(sys.modules[token_plugin_module], token_plugin_cls) + + opts.token_plugin = token_plugin_cls(opts.token_source) + + del opts.token_source + + if opts.auth_plugin is not None: + if '.' not in opts.auth_plugin: + opts.auth_plugin = 'websockify.auth_plugins.%s' % opts.auth_plugin + + auth_plugin_module, auth_plugin_cls = opts.auth_plugin.rsplit('.', 1) + + __import__(auth_plugin_module) + auth_plugin_cls = getattr(sys.modules[auth_plugin_module], auth_plugin_cls) + + opts.auth_plugin = auth_plugin_cls(opts.auth_source) + + del opts.auth_source + + # Create and start the WebSockets proxy + libserver = opts.libserver + del opts.libserver + if libserver: + # Use standard Python SocketServer framework + server = LibProxyServer(**opts.__dict__) + server.serve_forever() + else: + # Use internal service framework + server = WebSocketProxy(**opts.__dict__) + server.start_server() + + +class LibProxyServer(ThreadingMixIn, HTTPServer): + """ + Just like WebSocketProxy, but uses standard Python SocketServer + framework. + """ + + def __init__(self, RequestHandlerClass=ProxyRequestHandler, **kwargs): + # Save off proxy specific options + self.target_host = kwargs.pop('target_host', None) + self.target_port = kwargs.pop('target_port', None) + self.wrap_cmd = kwargs.pop('wrap_cmd', None) + self.wrap_mode = kwargs.pop('wrap_mode', None) + self.unix_target = kwargs.pop('unix_target', None) + self.ssl_target = kwargs.pop('ssl_target', None) + self.token_plugin = kwargs.pop('token_plugin', None) + self.auth_plugin = kwargs.pop('auth_plugin', None) + self.heartbeat = kwargs.pop('heartbeat', None) + + self.token_plugin = None + self.auth_plugin = None + self.daemon = False + + # Server configuration + listen_host = kwargs.pop('listen_host', '') + listen_port = kwargs.pop('listen_port', None) + web = kwargs.pop('web', '') + + # Configuration affecting base request handler + self.only_upgrade = not web + self.verbose = kwargs.pop('verbose', False) + record = kwargs.pop('record', '') + if record: + self.record = os.path.abspath(record) + self.run_once = kwargs.pop('run_once', False) + self.handler_id = 0 + + for arg in kwargs.keys(): + print("warning: option %s ignored when using --libserver" % arg) + + if web: + os.chdir(web) + + HTTPServer.__init__(self, (listen_host, listen_port), + RequestHandlerClass) + + + def process_request(self, request, client_address): + """Override process_request to implement a counter""" + self.handler_id += 1 + ThreadingMixIn.process_request(self, request, client_address) + + +if __name__ == '__main__': + websockify_init() diff --git a/systemvm/debian/root/noVNC/utils/websockify/websockify/websocketserver.py b/systemvm/debian/root/noVNC/utils/websockify/websockify/websocketserver.py new file mode 100644 index 000000000000..d2e02c99bbba --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/websockify/websocketserver.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python + +''' +Python WebSocket server base +Copyright 2011 Joel Martin +Copyright 2016-2018 Pierre Ossman +Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3) +''' + +import sys + +# python 3.0 differences +try: + from http.server import BaseHTTPRequestHandler, HTTPServer +except ImportError: + from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer + +from websockify.websocket import WebSocket, WebSocketWantReadError, WebSocketWantWriteError + +class WebSocketRequestHandlerMixIn: + """WebSocket request handler mix-in class + + This class modifies and existing request handler to handle + WebSocket requests. The request handler will continue to function + as before, except that WebSocket requests are intercepted and the + methods handle_upgrade() and handle_websocket() are called. The + standard do_GET() will be called for normal requests. + + The class instance SocketClass can be overridden with the class to + use for the WebSocket connection. + """ + + SocketClass = WebSocket + + def handle_one_request(self): + """Extended request handler + + This is where WebSocketRequestHandler redirects requests to the + new methods. Any sub-classes must call this method in order for + the calls to function. + """ + self._real_do_GET = self.do_GET + self.do_GET = self._websocket_do_GET + try: + # super() only works for new style classes + if issubclass(WebSocketRequestHandlerMixIn, object): + super(WebSocketRequestHandlerMixIn, self).handle_one_request() + else: + # Assume handle_one_request() hasn't been overriden + BaseHTTPRequestHandler.handle_one_request(self) + finally: + self.do_GET = self._real_do_GET + + def _websocket_do_GET(self): + # Checks if it is a websocket request and redirects + self.do_GET = self._real_do_GET + + if (self.headers.get('upgrade') and + self.headers.get('upgrade').lower() == 'websocket'): + self.handle_upgrade() + else: + self.do_GET() + + def handle_upgrade(self): + """Initial handler for a WebSocket request + + This method is called when a WebSocket is requested. By default + it will create a WebSocket object and perform the negotiation. + The WebSocket object will then replace the request object and + handle_websocket() will be called. + """ + websocket = self.SocketClass() + try: + websocket.accept(self.request, self.headers) + except Exception: + exc = sys.exc_info()[1] + self.send_error(400, str(exc)) + return + + self.log_request(101) + + self.request = websocket + + # Other requests cannot follow Websocket data + self.close_connection = True + + self.handle_websocket() + + def handle_websocket(self): + """Handle a WebSocket connection. + + This is called when the WebSocket is ready to be used. A + sub-class should perform the necessary communication here and + return once done. + """ + pass + +# Convenient ready made classes + +class WebSocketRequestHandler(WebSocketRequestHandlerMixIn, + BaseHTTPRequestHandler): + pass + +class WebSocketServer(HTTPServer): + pass diff --git a/systemvm/debian/root/noVNC/utils/websockify/websockify/websockifyserver.py b/systemvm/debian/root/noVNC/utils/websockify/websockify/websockifyserver.py new file mode 100644 index 000000000000..fbde0beb8708 --- /dev/null +++ b/systemvm/debian/root/noVNC/utils/websockify/websockify/websockifyserver.py @@ -0,0 +1,872 @@ +#!/usr/bin/env python + +''' +Python WebSocket server base with support for "wss://" encryption. +Copyright 2011 Joel Martin +Copyright 2016 Pierre Ossman +Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3) + +You can make a cert/key with openssl using: +openssl req -new -x509 -days 365 -nodes -out self.pem -keyout self.pem +as taken from http://docs.python.org/dev/library/ssl.html#certificates + +''' + +import os, sys, time, errno, signal, socket, select, logging, re +import multiprocessing + +# Imports that vary by python version + +# python 3.0 differences +if sys.hexversion > 0x3000000: + s2b = lambda s: s.encode('latin_1') +else: + s2b = lambda s: s # No-op + +try: + from http.server import SimpleHTTPRequestHandler +except ImportError: + from SimpleHTTPServer import SimpleHTTPRequestHandler + +# Degraded functionality if these imports are missing +for mod, msg in [('ssl', 'TLS/SSL/wss is disabled'), + ('resource', 'daemonizing is disabled')]: + try: + globals()[mod] = __import__(mod) + except ImportError: + globals()[mod] = None + print("WARNING: no '%s' module, %s" % (mod, msg)) + +if sys.platform == 'win32': + # make sockets pickle-able/inheritable + import multiprocessing.reduction + +from websockify.websocket import WebSocket, WebSocketWantReadError, WebSocketWantWriteError +from websockify.websocketserver import WebSocketRequestHandlerMixIn + +class CompatibleWebSocket(WebSocket): + def select_subprotocol(self, protocols): + # Handle old websockify clients that still specifiy a sub-protocol + if 'binary' in protocols: + return 'binary' + else: + return '' + +# HTTP handler with WebSocket upgrade support +class WebSockifyRequestHandler(WebSocketRequestHandlerMixIn, SimpleHTTPRequestHandler): + """ + WebSocket Request Handler Class, derived from SimpleHTTPRequestHandler. + Must be sub-classed with new_websocket_client method definition. + The request handler can be configured by setting optional + attributes on the server object: + + * only_upgrade: If true, SimpleHTTPRequestHandler will not be enabled, + only websocket is allowed. + * verbose: If true, verbose logging is activated. + * daemon: Running as daemon, do not write to console etc + * record: Record raw frame data as JavaScript array into specified filename + * run_once: Handle a single request + * handler_id: A sequence number for this connection, appended to record filename + """ + server_version = "WebSockify" + + protocol_version = "HTTP/1.1" + + SocketClass = CompatibleWebSocket + + # An exception while the WebSocket client was connected + class CClose(Exception): + pass + + def __init__(self, req, addr, server): + # Retrieve a few configuration variables from the server + self.only_upgrade = getattr(server, "only_upgrade", False) + self.verbose = getattr(server, "verbose", False) + self.daemon = getattr(server, "daemon", False) + self.record = getattr(server, "record", False) + self.run_once = getattr(server, "run_once", False) + self.rec = None + self.handler_id = getattr(server, "handler_id", False) + self.file_only = getattr(server, "file_only", False) + self.traffic = getattr(server, "traffic", False) + self.web_auth = getattr(server, "web_auth", False) + self.host_token = getattr(server, "host_token", False) + + self.logger = getattr(server, "logger", None) + if self.logger is None: + self.logger = WebSockifyServer.get_logger() + + SimpleHTTPRequestHandler.__init__(self, req, addr, server) + + def log_message(self, format, *args): + self.logger.info("%s - - [%s] %s" % (self.client_address[0], self.log_date_time_string(), format % args)) + + # + # WebSocketRequestHandler logging/output functions + # + + def print_traffic(self, token="."): + """ Show traffic flow mode. """ + if self.traffic: + sys.stdout.write(token) + sys.stdout.flush() + + def msg(self, msg, *args, **kwargs): + """ Output message with handler_id prefix. """ + prefix = "% 3d: " % self.handler_id + self.logger.log(logging.INFO, "%s%s" % (prefix, msg), *args, **kwargs) + + def vmsg(self, msg, *args, **kwargs): + """ Same as msg() but as debug. """ + prefix = "% 3d: " % self.handler_id + self.logger.log(logging.DEBUG, "%s%s" % (prefix, msg), *args, **kwargs) + + def warn(self, msg, *args, **kwargs): + """ Same as msg() but as warning. """ + prefix = "% 3d: " % self.handler_id + self.logger.log(logging.WARN, "%s%s" % (prefix, msg), *args, **kwargs) + + # + # Main WebSocketRequestHandler methods + # + def send_frames(self, bufs=None): + """ Encode and send WebSocket frames. Any frames already + queued will be sent first. If buf is not set then only queued + frames will be sent. Returns True if any frames could not be + fully sent, in which case the caller should call again when + the socket is ready. """ + + tdelta = int(time.time()*1000) - self.start_time + + if bufs: + for buf in bufs: + if self.rec: + # Python 3 compatible conversion + bufstr = buf.decode('latin1').encode('unicode_escape').decode('ascii').replace("'", "\\'") + self.rec.write("'{{{0}{{{1}',\n".format(tdelta, bufstr)) + self.send_parts.append(buf) + + # Flush any previously queued data + try: + self.request.sendmsg('') + except WebSocketWantWriteError: + return True + + while self.send_parts: + # Send pending frames + buf = self.send_parts.pop(0) + try: + self.request.sendmsg(buf) + except WebSocketWantWriteError: + self.print_traffic("<.") + return True + self.print_traffic("<") + + return False + + def recv_frames(self): + """ Receive and decode WebSocket frames. + + Returns: + (bufs_list, closed_string) + """ + + closed = False + bufs = [] + tdelta = int(time.time()*1000) - self.start_time + + while True: + try: + buf = self.request.recvmsg() + except WebSocketWantReadError: + self.print_traffic("}.") + break + + if buf is None: + closed = {'code': self.request.close_code, + 'reason': self.request.close_reason} + return bufs, closed + + self.print_traffic("}") + + if self.rec: + # Python 3 compatible conversion + bufstr = buf.decode('latin1').encode('unicode_escape').decode('ascii').replace("'", "\\'") + self.rec.write("'}}{0}}}{1}',\n".format(tdelta, bufstr)) + + bufs.append(buf) + + if not self.request.pending(): + break + + return bufs, closed + + def send_close(self, code=1000, reason=''): + """ Send a WebSocket orderly close frame. """ + self.request.shutdown(socket.SHUT_RDWR, code, reason) + + def send_pong(self, data=''.encode('ascii')): + """ Send a WebSocket pong frame. """ + self.request.pong(data) + + def send_ping(self, data=''.encode('ascii')): + """ Send a WebSocket ping frame. """ + self.request.ping(data) + + def handle_upgrade(self): + # ensure connection is authorized, and determine the target + self.validate_connection() + self.auth_connection() + + WebSocketRequestHandlerMixIn.handle_upgrade(self) + + def handle_websocket(self): + # Indicate to server that a Websocket upgrade was done + self.server.ws_connection = True + # Initialize per client settings + self.send_parts = [] + self.recv_part = None + self.start_time = int(time.time()*1000) + + # client_address is empty with, say, UNIX domain sockets + client_addr = "" + is_ssl = False + try: + client_addr = self.client_address[0] + is_ssl = self.client_address[2] + except IndexError: + pass + + if is_ssl: + self.stype = "SSL/TLS (wss://)" + else: + self.stype = "Plain non-SSL (ws://)" + + self.log_message("%s: %s WebSocket connection", client_addr, + self.stype) + if self.path != '/': + self.log_message("%s: Path: '%s'", client_addr, self.path) + + if self.record: + # Record raw frame data as JavaScript array + fname = "%s.%s" % (self.record, + self.handler_id) + self.log_message("opening record file: %s", fname) + self.rec = open(fname, 'w+') + self.rec.write("var VNC_frame_data = [\n") + + try: + self.new_websocket_client() + except self.CClose: + # Close the client + _, exc, _ = sys.exc_info() + self.send_close(exc.args[0], exc.args[1]) + + def do_GET(self): + if self.web_auth: + # ensure connection is authorized, this seems to apply to list_directory() as well + self.auth_connection() + + if self.only_upgrade: + self.send_error(405, "Method Not Allowed") + else: + if not re.match('^\/((images|core|vendor)\/)?[a-zA-Z0-9_\/]+\.[a-zA-Z0-9_]+(\?.*)?$', self.path): + self.last_code = 400 + self.last_message = "400 Bad Request" + SimpleHTTPRequestHandler.send_response(self,self.last_code,self.last_message) + else: + SimpleHTTPRequestHandler.do_GET(self) + + def list_directory(self, path): + if self.file_only: + self.send_error(404, "No such file") + else: + return SimpleHTTPRequestHandler.list_directory(self, path) + + def new_websocket_client(self): + """ Do something with a WebSockets client connection. """ + raise Exception("WebSocketRequestHandler.new_websocket_client() must be overloaded") + + def validate_connection(self): + """ Ensure that the connection has a valid token, and set the target. """ + pass + + def auth_connection(self): + """ Ensure that the connection is authorized. """ + pass + + def do_HEAD(self): + if self.web_auth: + self.auth_connection() + + if self.only_upgrade: + self.send_error(405, "Method Not Allowed") + else: + SimpleHTTPRequestHandler.do_HEAD(self) + + def finish(self): + if self.rec: + self.rec.write("'EOF'];\n") + self.rec.close() + SimpleHTTPRequestHandler.finish(self) + + def handle(self): + # When using run_once, we have a single process, so + # we cannot loop in BaseHTTPRequestHandler.handle; we + # must return and handle new connections + if self.run_once: + self.handle_one_request() + else: + SimpleHTTPRequestHandler.handle(self) + + def log_request(self, code='-', size='-'): + if self.verbose: + SimpleHTTPRequestHandler.log_request(self, code, size) + + +class WebSockifyServer(object): + """ + WebSockets server class. + As an alternative, the standard library SocketServer can be used + """ + + policy_response = """\n""" + log_prefix = "websocket" + + # An exception before the WebSocket connection was established + class EClose(Exception): + pass + + class Terminate(Exception): + pass + + def __init__(self, RequestHandlerClass, listen_fd=None, + listen_host='', listen_port=None, source_is_ipv6=False, + verbose=False, cert='', key='', key_password=None, ssl_only=None, + verify_client=False, cafile=None, + daemon=False, record='', web='', web_auth=False, + file_only=False, + run_once=False, timeout=0, idle_timeout=0, traffic=False, + tcp_keepalive=True, tcp_keepcnt=None, tcp_keepidle=None, + tcp_keepintvl=None, ssl_ciphers=None, ssl_options=0): + + # settings + self.RequestHandlerClass = RequestHandlerClass + self.verbose = verbose + self.listen_fd = listen_fd + self.listen_host = listen_host + self.listen_port = listen_port + self.prefer_ipv6 = source_is_ipv6 + self.ssl_only = ssl_only + self.ssl_ciphers = ssl_ciphers + self.ssl_options = ssl_options + self.verify_client = verify_client + self.daemon = daemon + self.run_once = run_once + self.timeout = timeout + self.idle_timeout = idle_timeout + self.traffic = traffic + self.file_only = file_only + self.web_auth = web_auth + + self.launch_time = time.time() + self.ws_connection = False + self.handler_id = 1 + self.terminating = False + + self.logger = self.get_logger() + self.tcp_keepalive = tcp_keepalive + self.tcp_keepcnt = tcp_keepcnt + self.tcp_keepidle = tcp_keepidle + self.tcp_keepintvl = tcp_keepintvl + + # keyfile path must be None if not specified + self.key = None + self.key_password = key_password + + # Make paths settings absolute + self.cert = os.path.abspath(cert) + self.web = self.record = self.cafile = '' + if key: + self.key = os.path.abspath(key) + if web: + self.web = os.path.abspath(web) + if record: + self.record = os.path.abspath(record) + if cafile: + self.cafile = os.path.abspath(cafile) + + if self.web: + os.chdir(self.web) + self.only_upgrade = not self.web + + # Sanity checks + if not ssl and self.ssl_only: + raise Exception("No 'ssl' module and SSL-only specified") + if self.daemon and not resource: + raise Exception("Module 'resource' required to daemonize") + + # Show configuration + self.msg("WebSocket server settings:") + if self.listen_fd != None: + self.msg(" - Listen for inetd connections") + else: + self.msg(" - Listen on %s:%s", + self.listen_host, self.listen_port) + if self.web: + if self.file_only: + self.msg(" - Web server (no directory listings). Web root: %s", self.web) + else: + self.msg(" - Web server. Web root: %s", self.web) + if ssl: + if os.path.exists(self.cert): + self.msg(" - SSL/TLS support") + if self.ssl_only: + self.msg(" - Deny non-SSL/TLS connections") + else: + self.msg(" - No SSL/TLS support (no cert file)") + else: + self.msg(" - No SSL/TLS support (no 'ssl' module)") + if self.daemon: + self.msg(" - Backgrounding (daemon)") + if self.record: + self.msg(" - Recording to '%s.*'", self.record) + + # + # WebSockifyServer static methods + # + + @staticmethod + def get_logger(): + return logging.getLogger("%s.%s" % ( + WebSockifyServer.log_prefix, + WebSockifyServer.__class__.__name__)) + + @staticmethod + def socket(host, port=None, connect=False, prefer_ipv6=False, + unix_socket=None, use_ssl=False, tcp_keepalive=True, + tcp_keepcnt=None, tcp_keepidle=None, tcp_keepintvl=None): + """ Resolve a host (and optional port) to an IPv4 or IPv6 + address. Create a socket. Bind to it if listen is set, + otherwise connect to it. Return the socket. + """ + flags = 0 + if host == '': + host = None + if connect and not (port or unix_socket): + raise Exception("Connect mode requires a port") + if use_ssl and not ssl: + raise Exception("SSL socket requested but Python SSL module not loaded."); + if not connect and use_ssl: + raise Exception("SSL only supported in connect mode (for now)") + if not connect: + flags = flags | socket.AI_PASSIVE + + if not unix_socket: + addrs = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM, + socket.IPPROTO_TCP, flags) + if not addrs: + raise Exception("Could not resolve host '%s'" % host) + addrs.sort(key=lambda x: x[0]) + if prefer_ipv6: + addrs.reverse() + sock = socket.socket(addrs[0][0], addrs[0][1]) + + if tcp_keepalive: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + if tcp_keepcnt: + sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPCNT, + tcp_keepcnt) + if tcp_keepidle: + sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPIDLE, + tcp_keepidle) + if tcp_keepintvl: + sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPINTVL, + tcp_keepintvl) + + if connect: + sock.connect(addrs[0][4]) + if use_ssl: + sock = ssl.wrap_socket(sock) + else: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(addrs[0][4]) + sock.listen(100) + else: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(unix_socket) + + return sock + + @staticmethod + def daemonize(keepfd=None, chdir='/'): + + if keepfd is None: + keepfd = [] + + os.umask(0) + if chdir: + os.chdir(chdir) + else: + os.chdir('/') + os.setgid(os.getgid()) # relinquish elevations + os.setuid(os.getuid()) # relinquish elevations + + # Double fork to daemonize + if os.fork() > 0: os._exit(0) # Parent exits + os.setsid() # Obtain new process group + if os.fork() > 0: os._exit(0) # Parent exits + + # Signal handling + signal.signal(signal.SIGTERM, signal.SIG_IGN) + signal.signal(signal.SIGINT, signal.SIG_IGN) + + # Close open files + maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] + if maxfd == resource.RLIM_INFINITY: maxfd = 256 + for fd in reversed(range(maxfd)): + try: + if fd not in keepfd: + os.close(fd) + except OSError: + _, exc, _ = sys.exc_info() + if exc.errno != errno.EBADF: raise + + # Redirect I/O to /dev/null + os.dup2(os.open(os.devnull, os.O_RDWR), sys.stdin.fileno()) + os.dup2(os.open(os.devnull, os.O_RDWR), sys.stdout.fileno()) + os.dup2(os.open(os.devnull, os.O_RDWR), sys.stderr.fileno()) + + def do_handshake(self, sock, address): + """ + do_handshake does the following: + - Peek at the first few bytes from the socket. + - If the connection is an HTTPS/SSL/TLS connection then SSL + wrap the socket. + - Read from the (possibly wrapped) socket. + - If we have received a HTTP GET request and the webserver + functionality is enabled, answer it, close the socket and + return. + - Assume we have a WebSockets connection, parse the client + handshake data. + - Send a WebSockets handshake server response. + - Return the socket for this WebSocket client. + """ + ready = select.select([sock], [], [], 3)[0] + + if not ready: + raise self.EClose("") + # Peek, but do not read the data so that we have a opportunity + # to SSL wrap the socket first + handshake = sock.recv(1024, socket.MSG_PEEK) + #self.msg("Handshake [%s]" % handshake) + + if not handshake: + raise self.EClose("") + + elif handshake[0] in ("\x16", "\x80", 22, 128): + # SSL wrap the connection + if not ssl: + raise self.EClose("SSL connection but no 'ssl' module") + if not os.path.exists(self.cert): + raise self.EClose("SSL connection but '%s' not found" + % self.cert) + retsock = None + try: + if (hasattr(ssl, 'create_default_context') + and callable(ssl.create_default_context)): + # create new-style SSL wrapping for extended features + context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + if self.ssl_ciphers is not None: + context.set_ciphers(self.ssl_ciphers) + context.options = self.ssl_options + context.load_cert_chain(certfile=self.cert, keyfile=self.key, password=self.key_password) + if self.verify_client: + context.verify_mode = ssl.CERT_REQUIRED + if self.cafile: + context.load_verify_locations(cafile=self.cafile) + else: + context.set_default_verify_paths() + retsock = context.wrap_socket( + sock, + server_side=True) + else: + if self.verify_client: + raise self.EClose("Client certificate verification requested, but this Python is too old.") + # new-style SSL wrapping is not needed, using to old style + retsock = ssl.wrap_socket( + sock, + server_side=True, + certfile=self.cert, + keyfile=self.key) + except ssl.SSLError: + _, x, _ = sys.exc_info() + if x.args[0] == ssl.SSL_ERROR_EOF: + if len(x.args) > 1: + raise self.EClose(x.args[1]) + else: + raise self.EClose("Got SSL_ERROR_EOF") + else: + raise + + elif self.ssl_only: + raise self.EClose("non-SSL connection received but disallowed") + + else: + retsock = sock + + # If the address is like (host, port), we are extending it + # with a flag indicating SSL. Not many other options + # available... + if len(address) == 2: + address = (address[0], address[1], (retsock != sock)) + + self.RequestHandlerClass(retsock, address, self) + + # Return the WebSockets socket which may be SSL wrapped + return retsock + + # + # WebSockifyServer logging/output functions + # + + def msg(self, *args, **kwargs): + """ Output message as info """ + self.logger.log(logging.INFO, *args, **kwargs) + + def vmsg(self, *args, **kwargs): + """ Same as msg() but as debug. """ + self.logger.log(logging.DEBUG, *args, **kwargs) + + def warn(self, *args, **kwargs): + """ Same as msg() but as warning. """ + self.logger.log(logging.WARN, *args, **kwargs) + + + # + # Events that can/should be overridden in sub-classes + # + def started(self): + """ Called after WebSockets startup """ + self.vmsg("WebSockets server started") + + def poll(self): + """ Run periodically while waiting for connections. """ + #self.vmsg("Running poll()") + pass + + def terminate(self): + if not self.terminating: + self.terminating = True + raise self.Terminate() + + def multiprocessing_SIGCHLD(self, sig, stack): + # TODO: figure out a way to actually log this information without + # calling `log` in the signal handlers + multiprocessing.active_children() + + def fallback_SIGCHLD(self, sig, stack): + # Reap zombies when using os.fork() (python 2.4) + # TODO: figure out a way to actually log this information without + # calling `log` in the signal handlers + try: + result = os.waitpid(-1, os.WNOHANG) + while result[0]: + self.vmsg("Reaped child process %s" % result[0]) + result = os.waitpid(-1, os.WNOHANG) + except (OSError): + pass + + def do_SIGINT(self, sig, stack): + # TODO: figure out a way to actually log this information without + # calling `log` in the signal handlers + self.terminate() + + def do_SIGTERM(self, sig, stack): + # TODO: figure out a way to actually log this information without + # calling `log` in the signal handlers + self.terminate() + + def top_new_client(self, startsock, address): + """ Do something with a WebSockets client connection. """ + # handler process + client = None + try: + try: + client = self.do_handshake(startsock, address) + except self.EClose: + _, exc, _ = sys.exc_info() + # Connection was not a WebSockets connection + if exc.args[0]: + self.msg("%s: %s" % (address[0], exc.args[0])) + except WebSockifyServer.Terminate: + raise + except Exception: + _, exc, _ = sys.exc_info() + self.msg("handler exception: %s" % str(exc)) + self.vmsg("exception", exc_info=True) + finally: + + if client and client != startsock: + # Close the SSL wrapped socket + # Original socket closed by caller + client.close() + + def get_log_fd(self): + """ + Get file descriptors for the loggers. + They should not be closed when the process is forked. + """ + descriptors = [] + for handler in self.logger.parent.handlers: + if isinstance(handler, logging.FileHandler): + descriptors.append(handler.stream.fileno()) + + return descriptors + + def start_server(self): + """ + Daemonize if requested. Listen for for connections. Run + do_handshake() method for each connection. If the connection + is a WebSockets client then call new_websocket_client() method (which must + be overridden) for each new client connection. + """ + + if self.listen_fd != None: + lsock = socket.fromfd(self.listen_fd, socket.AF_INET, socket.SOCK_STREAM) + if sys.hexversion < 0x3000000: + # For python 2 we have to wrap the "raw" socket into a socket object, + # otherwise ssl wrap_socket doesn't work. + lsock = socket.socket(_sock=lsock) + else: + lsock = self.socket(self.listen_host, self.listen_port, False, + self.prefer_ipv6, + tcp_keepalive=self.tcp_keepalive, + tcp_keepcnt=self.tcp_keepcnt, + tcp_keepidle=self.tcp_keepidle, + tcp_keepintvl=self.tcp_keepintvl) + + if self.daemon: + keepfd = self.get_log_fd() + keepfd.append(lsock.fileno()) + self.daemonize(keepfd=keepfd, chdir=self.web) + + self.started() # Some things need to happen after daemonizing + + # Allow override of signals + original_signals = { + signal.SIGINT: signal.getsignal(signal.SIGINT), + signal.SIGTERM: signal.getsignal(signal.SIGTERM), + } + if getattr(signal, 'SIGCHLD', None) is not None: + original_signals[signal.SIGCHLD] = signal.getsignal(signal.SIGCHLD) + signal.signal(signal.SIGINT, self.do_SIGINT) + signal.signal(signal.SIGTERM, self.do_SIGTERM) + # make sure that _cleanup is called when children die + # by calling active_children on SIGCHLD + if getattr(signal, 'SIGCHLD', None) is not None: + signal.signal(signal.SIGCHLD, self.multiprocessing_SIGCHLD) + + last_active_time = self.launch_time + try: + while True: + try: + try: + startsock = None + pid = err = 0 + child_count = 0 + + # Collect zombie child processes + child_count = len(multiprocessing.active_children()) + + time_elapsed = time.time() - self.launch_time + if self.timeout and time_elapsed > self.timeout: + self.msg('listener exit due to --timeout %s' + % self.timeout) + break + + if self.idle_timeout: + idle_time = 0 + if child_count == 0: + idle_time = time.time() - last_active_time + else: + idle_time = 0 + last_active_time = time.time() + + if idle_time > self.idle_timeout and child_count == 0: + self.msg('listener exit due to --idle-timeout %s' + % self.idle_timeout) + break + + try: + self.poll() + + ready = select.select([lsock], [], [], 1)[0] + if lsock in ready: + startsock, address = lsock.accept() + else: + continue + except self.Terminate: + raise + except Exception: + _, exc, _ = sys.exc_info() + if hasattr(exc, 'errno'): + err = exc.errno + elif hasattr(exc, 'args'): + err = exc.args[0] + else: + err = exc[0] + if err == errno.EINTR: + self.vmsg("Ignoring interrupted syscall") + continue + else: + raise + + if self.run_once: + # Run in same process if run_once + self.top_new_client(startsock, address) + if self.ws_connection : + self.msg('%s: exiting due to --run-once' + % address[0]) + break + else: + self.vmsg('%s: new handler Process' % address[0]) + p = multiprocessing.Process( + target=self.top_new_client, + args=(startsock, address)) + p.start() + # child will not return + + # parent process + self.handler_id += 1 + + except (self.Terminate, SystemExit, KeyboardInterrupt): + self.msg("In exit") + # terminate all child processes + if not self.run_once: + children = multiprocessing.active_children() + + for child in children: + self.msg("Terminating child %s" % child.pid) + child.terminate() + + break + except Exception: + exc = sys.exc_info()[1] + self.msg("handler exception: %s", str(exc)) + self.vmsg("exception", exc_info=True) + + finally: + if startsock: + startsock.close() + finally: + # Close listen port + self.vmsg("Closing socket listening at %s:%s", + self.listen_host, self.listen_port) + lsock.close() + + # Restore signals + for sig, func in original_signals.items(): + signal.signal(sig, func) + + diff --git a/systemvm/debian/root/noVNC/vendor/browser-es-module-loader/README.md b/systemvm/debian/root/noVNC/vendor/browser-es-module-loader/README.md new file mode 100644 index 000000000000..c26867f979ff --- /dev/null +++ b/systemvm/debian/root/noVNC/vendor/browser-es-module-loader/README.md @@ -0,0 +1,15 @@ +Custom Browser ES Module Loader +=============================== + +This is a module loader using babel and the ES Module Loader polyfill. +It's based heavily on +https://github.com/ModuleLoader/browser-es-module-loader, but uses +WebWorkers to compile the modules in the background. + +To generate, run `rollup -c` in this directory, and then run `browserify +src/babel-worker.js > dist/babel-worker.js`. + +LICENSE +------- + +MIT diff --git a/systemvm/debian/root/noVNC/vendor/browser-es-module-loader/rollup.config.js b/systemvm/debian/root/noVNC/vendor/browser-es-module-loader/rollup.config.js new file mode 100644 index 000000000000..4bf4a5fd18c1 --- /dev/null +++ b/systemvm/debian/root/noVNC/vendor/browser-es-module-loader/rollup.config.js @@ -0,0 +1,16 @@ +import nodeResolve from 'rollup-plugin-node-resolve'; + +export default { + entry: 'src/browser-es-module-loader.js', + dest: 'dist/browser-es-module-loader.js', + format: 'umd', + moduleName: 'BrowserESModuleLoader', + sourceMap: true, + + plugins: [ + nodeResolve(), + ], + + // skip rollup warnings (specifically the eval warning) + onwarn: function() {} +}; diff --git a/systemvm/debian/root/noVNC/vendor/browser-es-module-loader/src/babel-worker.js b/systemvm/debian/root/noVNC/vendor/browser-es-module-loader/src/babel-worker.js new file mode 100644 index 000000000000..007bd6850cb0 --- /dev/null +++ b/systemvm/debian/root/noVNC/vendor/browser-es-module-loader/src/babel-worker.js @@ -0,0 +1,25 @@ +/*import { transform as babelTransform } from 'babel-core'; +import babelTransformDynamicImport from 'babel-plugin-syntax-dynamic-import'; +import babelTransformES2015ModulesSystemJS from 'babel-plugin-transform-es2015-modules-systemjs';*/ + +// sadly, due to how rollup works, we can't use es6 imports here +var babelTransform = require('babel-core').transform; +var babelTransformDynamicImport = require('babel-plugin-syntax-dynamic-import'); +var babelTransformES2015ModulesSystemJS = require('babel-plugin-transform-es2015-modules-systemjs'); +var babelPresetES2015 = require('babel-preset-es2015'); + +self.onmessage = function (evt) { + // transform source with Babel + var output = babelTransform(evt.data.source, { + compact: false, + filename: evt.data.key + '!transpiled', + sourceFileName: evt.data.key, + moduleIds: false, + sourceMaps: 'inline', + babelrc: false, + plugins: [babelTransformDynamicImport, babelTransformES2015ModulesSystemJS], + presets: [babelPresetES2015], + }); + + self.postMessage({key: evt.data.key, code: output.code, source: evt.data.source}); +}; diff --git a/systemvm/debian/root/noVNC/vendor/browser-es-module-loader/src/browser-es-module-loader.js b/systemvm/debian/root/noVNC/vendor/browser-es-module-loader/src/browser-es-module-loader.js new file mode 100644 index 000000000000..efae617061f3 --- /dev/null +++ b/systemvm/debian/root/noVNC/vendor/browser-es-module-loader/src/browser-es-module-loader.js @@ -0,0 +1,280 @@ +import RegisterLoader from 'es-module-loader/core/register-loader.js'; +import { InternalModuleNamespace as ModuleNamespace } from 'es-module-loader/core/loader-polyfill.js'; + +import { baseURI, global, isBrowser } from 'es-module-loader/core/common.js'; +import { resolveIfNotPlain } from 'es-module-loader/core/resolve.js'; + +var loader; + +// + + + + + + + + + + + + +
+ +
+
Send CtrlAltDel
+
Clipboard
+
+
+
Loading
+
+
+
+ +
+ + diff --git a/systemvm/test/runtests.sh b/systemvm/test/runtests.sh index 94958e531b05..87cc75ea84a0 100644 --- a/systemvm/test/runtests.sh +++ b/systemvm/test/runtests.sh @@ -23,19 +23,19 @@ export PYTHONDONTWRITEBYTECODE=False echo "Running pycodestyle to check systemvm/python code for errors" pycodestyle --max-line-length=179 *py -pycodestyle --max-line-length=179 --exclude=monitorServices.py,baremetal-vr.py,passwd_server_ip.py `find ../debian -name \*.py` +pycodestyle --max-line-length=179 --exclude=monitorServices.py,baremetal-vr.py,passwd_server_ip.py `find ../debian -name \*.py |grep -v /root/noVNC` if [ $? -gt 0 ] then - echo "Pylint failed, please check your code" + echo "pycodestyle failed, please check your code" exit 1 fi echo "Running pylint to check systemvm/python code for errors" pylint --disable=R,C,W *.py -pylint --disable=R,C,W `find ../debian -name \*.py` +pylint --disable=R,C,W `find ../debian -name \*.py |grep -v /root/noVNC` if [ $? -gt 0 ] then - echo "Pylint failed, please check your code" + echo "pylint failed, please check your code" exit 1 fi diff --git a/tools/appliance/systemvmtemplate/scripts/install_systemvm_packages.sh b/tools/appliance/systemvmtemplate/scripts/install_systemvm_packages.sh index 1f18b2527342..8a10cdaa9f8a 100644 --- a/tools/appliance/systemvmtemplate/scripts/install_systemvm_packages.sh +++ b/tools/appliance/systemvmtemplate/scripts/install_systemvm_packages.sh @@ -64,7 +64,7 @@ function install_packages() { ipset \ iptables-persistent \ libtcnative-1 libssl-dev libapr1-dev \ - python-flask \ + python-flask python-numpy \ haproxy \ haveged \ radvd \