diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java index 8496301aa43..875bbc524bb 100644 --- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java @@ -19,6 +19,8 @@ package com.cloud.consoleproxy; import com.cloud.utils.component.Manager; import com.cloud.vm.ConsoleProxyVO; +import org.apache.cloudstack.framework.config.ConfigKey; + public interface ConsoleProxyManager extends Manager, ConsoleProxyService { public static final int DEFAULT_PROXY_CAPACITY = 50; @@ -31,9 +33,14 @@ public interface ConsoleProxyManager extends Manager, ConsoleProxyService { public static final int DEFAULT_PROXY_URL_PORT = 80; public static final int DEFAULT_PROXY_SESSION_TIMEOUT = 300000; // 5 minutes + public static final int DEFAULT_NOVNC_PORT = 8080; + public static final String ALERT_SUBJECT = "proxy-alert"; public static final String CERTIFICATE_NAME = "CPVMCertificate"; + 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 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 368fc33876c..8638fb59222 100644 --- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java @@ -32,6 +32,8 @@ import javax.naming.ConfigurationException; 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; @@ -154,7 +156,8 @@ import com.google.gson.GsonBuilder; // 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 @@ -1741,4 +1744,14 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy _consoleProxyAllocators = consoleProxyAllocators; } + @Override + public String getConfigComponentName() { + return ConsoleProxyManager.class.getSimpleName(); + } + + @Override + public ConfigKey[] getConfigKeys() { + return new ConfigKey[] { NoVncConsoleDefault }; + } + } diff --git a/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java b/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java index ae9b5c548e5..ed73625d7e9 100644 --- a/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java +++ b/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java @@ -41,6 +41,12 @@ import org.apache.log4j.Logger; import org.springframework.stereotype.Component; import org.springframework.web.context.support.SpringBeanAutowiringSupport; + +import com.cloud.vm.VmDetailConstants; +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; @@ -59,10 +65,7 @@ import com.cloud.utils.db.TransactionLegacy; import com.cloud.vm.UserVmDetailVO; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachineManager; -import com.cloud.vm.VmDetailConstants; import com.cloud.vm.dao.UserVmDetailsDao; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; /** * Thumbnail access : /console?cmd=thumbnail&vm=xxx&w=xxx&h=xxx @@ -478,7 +481,12 @@ public class ConsoleProxyServlet extends HttpServlet { param.setClientTunnelSession(parsedHostInfo.third()); } - sb.append("/ajax?token=" + encryptor.encryptObject(ConsoleProxyClientParam.class, param)); + if (param.getHypervHost() != null || !ConsoleProxyManager.NoVncConsoleDefault.value()) { + sb.append("/ajax?token=" + encryptor.encryptObject(ConsoleProxyClientParam.class, param)); + } else { + sb.append("/resource/noVNC/vnc_lite.html?port=" + ConsoleProxyManager.DEFAULT_NOVNC_PORT + "&token=" + + encryptor.encryptObject(ConsoleProxyClientParam.class, param)); + } // for console access, we need guest OS type to help implement keyboard long guestOs = vm.getGuestOSId(); diff --git a/services/console-proxy/server/pom.xml b/services/console-proxy/server/pom.xml index 4bc6593843b..2d43ebf7508 100644 --- a/services/console-proxy/server/pom.xml +++ b/services/console-proxy/server/pom.xml @@ -50,6 +50,21 @@ cloudstack-service-console-proxy-rdpclient ${project.version} + + javax.websocket + javax.websocket-api + 1.0 + + + org.eclipse.jetty + jetty-server + ${cs.jetty.version} + + + org.eclipse.jetty.websocket + websocket-server + ${cs.jetty.version} + diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java index 2161de233d4..7a70a38b786 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java @@ -32,6 +32,7 @@ import java.util.Properties; import java.util.concurrent.Executor; import org.apache.log4j.xml.DOMConfigurator; +import org.eclipse.jetty.websocket.api.Session; import com.cloud.consoleproxy.util.Logger; import com.cloud.utils.PropertiesUtil; @@ -344,12 +345,22 @@ public class ConsoleProxy { server.createContext("/ajaximg", new ConsoleProxyAjaxImageHandler()); server.setExecutor(new ThreadExecutor()); // creates a default executor server.start(); + + ConsoleProxyNoVNCServer noVNCServer = getNoVNCServer(); + noVNCServer.start(); + } catch (Exception e) { s_logger.error(e.getMessage(), e); System.exit(1); } } + private static ConsoleProxyNoVNCServer getNoVNCServer() { + if (httpListenPort == 443) + return new ConsoleProxyNoVNCServer(ksBits, ksPassword); + return new ConsoleProxyNoVNCServer(); + } + private static void startupHttpCmdPort() { try { s_logger.info("Listening for HTTP CMDs on port " + httpCmdListenPort); @@ -395,7 +406,7 @@ public class ConsoleProxy { String clientKey = param.getClientMapKey(); synchronized (connectionMap) { viewer = connectionMap.get(clientKey); - if (viewer == null) { + if (viewer == null || viewer.getClass() == ConsoleProxyNoVncClient.class) { viewer = getClient(param); viewer.initClient(param); connectionMap.put(clientKey, viewer); @@ -429,7 +440,7 @@ public class ConsoleProxy { String clientKey = param.getClientMapKey(); synchronized (connectionMap) { ConsoleProxyClient viewer = connectionMap.get(clientKey); - if (viewer == null) { + if (viewer == null || viewer.getClass() == ConsoleProxyNoVncClient.class) { authenticationExternally(param); viewer = getClient(param); viewer.initClient(param); @@ -521,4 +532,40 @@ public class ConsoleProxy { new Thread(r).start(); } } + + public static ConsoleProxyNoVncClient getNoVncViewer(ConsoleProxyClientParam param, String ajaxSession, + Session session) throws AuthenticationException { + boolean reportLoadChange = false; + String clientKey = param.getClientMapKey(); + synchronized (connectionMap) { + ConsoleProxyClient viewer = connectionMap.get(clientKey); + if (viewer == null || viewer.getClass() != ConsoleProxyNoVncClient.class) { + authenticationExternally(param); + viewer = new ConsoleProxyNoVncClient(session); + viewer.initClient(param); + + connectionMap.put(clientKey, viewer); + reportLoadChange = true; + } else { + if (param.getClientHostPassword() == null || param.getClientHostPassword().isEmpty() || + !param.getClientHostPassword().equals(viewer.getClientHostPassword())) + throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": bad sid"); + + if (!viewer.isFrontEndAlive()) { + authenticationExternally(param); + viewer.initClient(param); + reportLoadChange = true; + } + } + + if (reportLoadChange) { + ConsoleProxyClientStatsCollector statsCollector = getStatsCollector(); + String loadInfo = statsCollector.getStatsReport(); + reportLoadInfo(loadInfo); + if (s_logger.isDebugEnabled()) + s_logger.debug("Report load change : " + loadInfo); + } + return (ConsoleProxyNoVncClient)viewer; + } + } } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java new file mode 100644 index 00000000000..349d98408a1 --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java @@ -0,0 +1,145 @@ +// 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; + +import java.io.IOException; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.cloud.consoleproxy.util.Logger; + +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; +import org.eclipse.jetty.websocket.api.extensions.Frame; +import org.eclipse.jetty.websocket.server.WebSocketHandler; +import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; + +@WebSocket +public class ConsoleProxyNoVNCHandler extends WebSocketHandler { + + private ConsoleProxyNoVncClient viewer; + private static final Logger s_logger = Logger.getLogger(ConsoleProxyNoVNCHandler.class); + + public ConsoleProxyNoVNCHandler() { + super(); + } + + @Override + public void configure(WebSocketServletFactory webSocketServletFactory) { + webSocketServletFactory.register(ConsoleProxyNoVNCHandler.class); + } + + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + + if (this.getWebSocketFactory().isUpgradeRequest(request, response)) { + response.addHeader("Sec-WebSocket-Protocol", "binary"); + if (this.getWebSocketFactory().acceptWebSocket(request, response)) { + baseRequest.setHandled(true); + return; + } + + if (response.isCommitted()) { + return; + } + } + + super.handle(target, baseRequest, request, response); + } + + @OnWebSocketConnect + public void onConnect(final Session session) throws IOException, InterruptedException { + + String queries = session.getUpgradeRequest().getQueryString(); + Map queryMap = ConsoleProxyHttpHandlerHelper.getQueryMap(queries); + + String host = queryMap.get("host"); + String portStr = queryMap.get("port"); + String sid = queryMap.get("sid"); + String tag = queryMap.get("tag"); + String ticket = queryMap.get("ticket"); + String ajaxSessionIdStr = queryMap.get("sess"); + String console_url = queryMap.get("consoleurl"); + String console_host_session = queryMap.get("sessionref"); + String vm_locale = queryMap.get("locale"); + String hypervHost = queryMap.get("hypervHost"); + String username = queryMap.get("username"); + String password = queryMap.get("password"); + + if (tag == null) + tag = ""; + + long ajaxSessionId = 0; + int port; + + if (host == null || portStr == null || sid == null) + throw new IllegalArgumentException(); + + try { + port = Integer.parseInt(portStr); + } catch (NumberFormatException e) { + s_logger.warn("Invalid number parameter in query string: " + portStr); + throw new IllegalArgumentException(e); + } + + if (ajaxSessionIdStr != null) { + try { + ajaxSessionId = Long.parseLong(ajaxSessionIdStr); + } catch (NumberFormatException e) { + s_logger.warn("Invalid number parameter in query string: " + ajaxSessionIdStr); + throw new IllegalArgumentException(e); + } + } + + try { + ConsoleProxyClientParam param = new ConsoleProxyClientParam(); + param.setClientHostAddress(host); + param.setClientHostPort(port); + param.setClientHostPassword(sid); + param.setClientTag(tag); + param.setTicket(ticket); + param.setClientTunnelUrl(console_url); + param.setClientTunnelSession(console_host_session); + param.setLocale(vm_locale); + param.setHypervHost(hypervHost); + param.setUsername(username); + param.setPassword(password); + viewer = ConsoleProxy.getNoVncViewer(param, ajaxSessionIdStr, session); + } catch (Exception e) { + s_logger.warn("Failed to create viewer due to " + e.getMessage(), e); + return; + } + } + + @OnWebSocketClose + public void onClose(Session session, int statusCode, String reason) throws IOException, InterruptedException { + ConsoleProxy.removeViewer(viewer); + } + + @OnWebSocketFrame + public void onFrame(Frame f) throws IOException { + viewer.sendClientFrame(f); + } +} diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCServer.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCServer.java new file mode 100644 index 00000000000..28d179ba6fc --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCServer.java @@ -0,0 +1,79 @@ +// 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; + +import java.io.ByteArrayInputStream; +import java.security.KeyStore; + +import com.cloud.consoleproxy.util.Logger; + +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +public class ConsoleProxyNoVNCServer { + + private static final Logger s_logger = Logger.getLogger(ConsoleProxyNoVNCServer.class); + private static final int wsPort = 8080; + + private Server server; + + public ConsoleProxyNoVNCServer() { + this.server = new Server(wsPort); + ConsoleProxyNoVNCHandler handler = new ConsoleProxyNoVNCHandler(); + this.server.setHandler(handler); + } + + public ConsoleProxyNoVNCServer(byte[] ksBits, String ksPassword) { + this.server = new Server(); + ConsoleProxyNoVNCHandler handler = new ConsoleProxyNoVNCHandler(); + this.server.setHandler(handler); + + try { + final HttpConfiguration httpConfig = new HttpConfiguration(); + httpConfig.setSecureScheme("https"); + httpConfig.setSecurePort(wsPort); + + final HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig); + httpsConfig.addCustomizer(new SecureRequestCustomizer()); + + final SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + char[] passphrase = ksPassword != null ? ksPassword.toCharArray() : null; + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(new ByteArrayInputStream(ksBits), passphrase); + sslContextFactory.setKeyStore(ks); + sslContextFactory.setKeyStorePassword(ksPassword); + sslContextFactory.setKeyManagerPassword(ksPassword); + + final ServerConnector sslConnector = new ServerConnector(server, + new SslConnectionFactory(sslContextFactory, "http/1.1"), + new HttpConnectionFactory(httpsConfig)); + sslConnector.setPort(wsPort); + server.addConnector(sslConnector); + } catch (Exception e) { + s_logger.error("Unable to secure server due to exception ", e); + } + } + + public void start() throws Exception { + this.server.start(); + } +} diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java new file mode 100644 index 00000000000..97963f80caf --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java @@ -0,0 +1,238 @@ +// 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; + +import org.apache.log4j.Logger; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.extensions.Frame; + +import java.awt.Image; +import java.io.IOException; +import java.net.URI; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.util.List; + +import com.cloud.consoleproxy.vnc.NoVncClient; + +public class ConsoleProxyNoVncClient implements ConsoleProxyClient { + private static final Logger s_logger = Logger.getLogger(ConsoleProxyNoVncClient.class); + private static int nextClientId = 0; + + private NoVncClient client; + private Session session; + + protected int clientId = getNextClientId(); + protected long ajaxSessionId = 0; + + protected long createTime = System.currentTimeMillis(); + protected long lastFrontEndActivityTime = System.currentTimeMillis(); + + private boolean connectionAlive; + + private ConsoleProxyClientParam clientParam; + + public ConsoleProxyNoVncClient(Session session) { + this.session = session; + } + + private int getNextClientId() { + return ++nextClientId; + } + + @Override + public void sendClientRawKeyboardEvent(InputEventType event, int code, int modifiers) { + } + + @Override + public void sendClientMouseEvent(InputEventType event, int x, int y, int code, int modifiers) { + } + + @Override + public boolean isHostConnected() { + return connectionAlive; + } + + @Override + public boolean isFrontEndAlive() { + if (!connectionAlive || System.currentTimeMillis() + - getClientLastFrontEndActivityTime() > ConsoleProxy.VIEWER_LINGER_SECONDS * 1000) { + s_logger.info("Front end has been idle for too long"); + return false; + } + return true; + } + + public void sendClientFrame(Frame f) throws IOException { + byte[] data = new byte[f.getPayloadLength()]; + f.getPayload().get(data); + client.write(data); + } + + @Override + public void initClient(ConsoleProxyClientParam param) { + setClientParam(param); + client = new NoVncClient(); + connectionAlive = true; + + updateFrontEndActivityTime(); + Thread worker = new Thread(new Runnable() { + public void run() { + try { + + String tunnelUrl = param.getClientTunnelUrl(); + String tunnelSession = param.getClientTunnelSession(); + + try { + if (tunnelUrl != null && !tunnelUrl.isEmpty() && tunnelSession != null + && !tunnelSession.isEmpty()) { + URI uri = new URI(tunnelUrl); + s_logger.info("Connect to VNC server via tunnel. url: " + tunnelUrl + ", session: " + + tunnelSession); + + ConsoleProxy.ensureRoute(uri.getHost()); + client.connectTo(uri.getHost(), uri.getPort(), uri.getPath() + "?" + uri.getQuery(), + tunnelSession, "https".equalsIgnoreCase(uri.getScheme())); + } else { + s_logger.info("Connect to VNC server directly. host: " + getClientHostAddress() + ", port: " + + getClientHostPort()); + ConsoleProxy.ensureRoute(getClientHostAddress()); + client.connectTo(getClientHostAddress(), getClientHostPort()); + } + } catch (UnknownHostException e) { + s_logger.error("Unexpected exception", e); + } catch (IOException e) { + s_logger.error("Unexpected exception", e); + } catch (Throwable e) { + s_logger.error("Unexpected exception", e); + } + + String ver = client.handshake(); + session.getRemote().sendBytes(ByteBuffer.wrap(ver.getBytes(), 0, ver.length())); + + byte[] b = client.authenticate(getClientHostPassword()); + session.getRemote().sendBytes(ByteBuffer.wrap(b, 0, 4)); + + int readBytes; + while (connectionAlive) { + b = new byte[100]; + readBytes = client.read(b); + if (readBytes == -1) { + break; + } + if (readBytes > 0) { + session.getRemote().sendBytes(ByteBuffer.wrap(b, 0, readBytes)); + updateFrontEndActivityTime(); + } + } + connectionAlive = false; + } catch (IOException e) { + e.printStackTrace(); + } + } + + }); + worker.start(); + } + + private void setClientParam(ConsoleProxyClientParam param) { + this.clientParam = param; + } + + @Override + public void closeClient() { + this.connectionAlive = false; + ConsoleProxy.removeViewer(this); + } + + @Override + public int getClientId() { + return this.clientId; + } + + @Override + public long getAjaxSessionId() { + return this.ajaxSessionId; + } + + @Override + public AjaxFIFOImageCache getAjaxImageCache() { + // Unimplemented + return null; + } + + @Override + public Image getClientScaledImage(int width, int height) { + // Unimplemented + return null; + } + + @Override + public String onAjaxClientStart(String title, List languages, String guest) { + // Unimplemented + return null; + } + + @Override + public String onAjaxClientUpdate() { + // Unimplemented + return null; + } + + @Override + public String onAjaxClientKickoff() { + // Unimplemented + return null; + } + + @Override + public long getClientCreateTime() { + return createTime; + } + + public void updateFrontEndActivityTime() { + lastFrontEndActivityTime = System.currentTimeMillis(); + } + + @Override + public long getClientLastFrontEndActivityTime() { + return lastFrontEndActivityTime; + } + + @Override + public String getClientHostAddress() { + return clientParam.getClientHostAddress(); + } + + @Override + public int getClientHostPort() { + return clientParam.getClientHostPort(); + } + + @Override + public String getClientHostPassword() { + return clientParam.getClientHostPassword(); + } + + @Override + public String getClientTag() { + if (clientParam.getClientTag() != null) + return clientParam.getClientTag(); + return ""; + } + +} diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyResourceHandler.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyResourceHandler.java index 86591208704..d5dbe0831d5 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyResourceHandler.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyResourceHandler.java @@ -54,6 +54,7 @@ public class ConsoleProxyResourceHandler implements HttpHandler { s_validResourceFolders.put("js", ""); s_validResourceFolders.put("css", ""); s_validResourceFolders.put("html", ""); + s_validResourceFolders.put("noVNC", ""); } public ConsoleProxyResourceHandler() { diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java new file mode 100644 index 00000000000..9a4372544fc --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java @@ -0,0 +1,219 @@ +// 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.vnc; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.Socket; +import java.net.UnknownHostException; +import java.nio.charset.Charset; +import java.security.spec.KeySpec; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; + +import com.cloud.consoleproxy.util.Logger; +import com.cloud.consoleproxy.util.RawHTTP; + +public class NoVncClient { + private static final Logger s_logger = Logger.getLogger(NoVncClient.class); + + private Socket socket; + private DataInputStream is; + private DataOutputStream os; + + public NoVncClient() { + } + + public void connectTo(String host, int port, String path, String session, boolean useSSL) throws UnknownHostException, IOException { + if (port < 0) { + if (useSSL) + port = 443; + else + port = 80; + } + + RawHTTP tunnel = new RawHTTP("CONNECT", host, port, path, session, useSSL); + socket = tunnel.connect(); + setStreams(); + } + + public void connectTo(String host, int port) throws UnknownHostException, IOException { + // Connect to server + s_logger.info("Connecting to VNC server " + host + ":" + port + "..."); + socket = new Socket(host, port); + setStreams(); + } + + private void setStreams() throws IOException { + this.is = new DataInputStream(this.socket.getInputStream()); + this.os = new DataOutputStream(this.socket.getOutputStream()); + } + + /** + * Handshake with VNC server. + */ + public String handshake() throws IOException { + + // Read protocol version + byte[] buf = new byte[12]; + is.readFully(buf); + String rfbProtocol = new String(buf); + + // Server should use RFB protocol 3.x + if (!rfbProtocol.contains(RfbConstants.RFB_PROTOCOL_VERSION_MAJOR)) { + s_logger.error("Cannot handshake with VNC server. Unsupported protocol version: \"" + rfbProtocol + "\"."); + throw new RuntimeException( + "Cannot handshake with VNC server. Unsupported protocol version: \"" + rfbProtocol + "\"."); + } + + // Proxy that we support RFB 3.3 only + return RfbConstants.RFB_PROTOCOL_VERSION + "\n"; + } + + /** + * VNC authentication. + */ + public byte[] authenticate(String password) + throws IOException { + // Read security type + int authType = is.readInt(); + + switch (authType) { + case RfbConstants.CONNECTION_FAILED: { + // Server forbids to connect. Read reason and throw exception + int length = is.readInt(); + byte[] buf = new byte[length]; + is.readFully(buf); + String reason = new String(buf, RfbConstants.CHARSET); + + s_logger.error("Authentication to VNC server is failed. Reason: " + reason); + throw new RuntimeException("Authentication to VNC server is failed. Reason: " + reason); + } + + case RfbConstants.NO_AUTH: { + // Client can connect without authorization. Nothing to do. + break; + } + + case RfbConstants.VNC_AUTH: { + s_logger.info("VNC server requires password authentication"); + doVncAuth(is, os, password); + break; + } + + default: + s_logger.error("Unsupported VNC protocol authorization scheme, scheme code: " + authType + "."); + throw new RuntimeException( + "Unsupported VNC protocol authorization scheme, scheme code: " + authType + "."); + } + // Since we've taken care of the auth, we tell the client that there's no auth + // going on + return new byte[] { 0, 0, 0, 1 }; + } + + /** + * Encode client password and send it to server. + */ + private void doVncAuth(DataInputStream in, DataOutputStream out, String password) throws IOException { + + // Read challenge + byte[] challenge = new byte[16]; + in.readFully(challenge); + + // Encode challenge with password + byte[] response; + try { + response = encodePassword(challenge, password); + } catch (Exception e) { + s_logger.error("Cannot encrypt client password to send to server: " + e.getMessage()); + throw new RuntimeException("Cannot encrypt client password to send to server: " + e.getMessage()); + } + + // Send encoded challenge + out.write(response); + out.flush(); + + // Read security result + int authResult = in.readInt(); + + switch (authResult) { + case RfbConstants.VNC_AUTH_OK: { + // Nothing to do + break; + } + + case RfbConstants.VNC_AUTH_TOO_MANY: + s_logger.error("Connection to VNC server failed: too many wrong attempts."); + throw new RuntimeException("Connection to VNC server failed: too many wrong attempts."); + + case RfbConstants.VNC_AUTH_FAILED: + s_logger.error("Connection to VNC server failed: wrong password."); + throw new RuntimeException("Connection to VNC server failed: wrong password."); + + default: + s_logger.error("Connection to VNC server failed, reason code: " + authResult); + throw new RuntimeException("Connection to VNC server failed, reason code: " + authResult); + } + } + + private 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; + } + + public 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 + byte[] passwordAsciiBytes = password.getBytes(Charset.availableCharsets().get("US-ASCII")); + System.arraycopy(passwordAsciiBytes, 0, key, 0, Math.min(password.length(), 8)); + + // Flip bytes (reverse bits) in key + for (int i = 0; i < key.length; i++) { + key[i] = flipByte(key[i]); + } + + 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; + } + + public int read(byte[] b) throws IOException { + return is.read(b); + } + + public void write(byte[] b) throws IOException { + os.write(b); + } + +} \ No newline at end of file diff --git a/systemvm/agent/noVNC/.eslintignore b/systemvm/agent/noVNC/.eslintignore new file mode 100644 index 00000000000..d38162800ad --- /dev/null +++ b/systemvm/agent/noVNC/.eslintignore @@ -0,0 +1 @@ +**/xtscancodes.js diff --git a/systemvm/agent/noVNC/.eslintrc b/systemvm/agent/noVNC/.eslintrc new file mode 100644 index 00000000000..900a7186efc --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/.github/ISSUE_TEMPLATE/bug_report.md b/systemvm/agent/noVNC/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000000..94ac6f8dc6e --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/.github/ISSUE_TEMPLATE/feature_request.md b/systemvm/agent/noVNC/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000000..066b2d920a2 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/.gitignore b/systemvm/agent/noVNC/.gitignore new file mode 100644 index 00000000000..c178dbab43d --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/.gitmodules b/systemvm/agent/noVNC/.gitmodules new file mode 100644 index 00000000000..e69de29bb2d diff --git a/systemvm/agent/noVNC/.travis.yml b/systemvm/agent/noVNC/.travis.yml new file mode 100644 index 00000000000..78b521a80ba --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/AUTHORS b/systemvm/agent/noVNC/AUTHORS new file mode 100644 index 00000000000..dec0e89329a --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/LICENSE.txt b/systemvm/agent/noVNC/LICENSE.txt new file mode 100644 index 00000000000..20f3eb025fe --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/README.md b/systemvm/agent/noVNC/README.md new file mode 100644 index 00000000000..566b8e4f5af --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/VERSION b/systemvm/agent/noVNC/VERSION new file mode 100644 index 00000000000..9084fa2f716 --- /dev/null +++ b/systemvm/agent/noVNC/VERSION @@ -0,0 +1 @@ +1.1.0 diff --git a/systemvm/agent/noVNC/app/error-handler.js b/systemvm/agent/noVNC/app/error-handler.js new file mode 100644 index 00000000000..8e294166fc6 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/app/images/alt.svg b/systemvm/agent/noVNC/app/images/alt.svg new file mode 100644 index 00000000000..e5bb4612ece --- /dev/null +++ b/systemvm/agent/noVNC/app/images/alt.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/clipboard.svg b/systemvm/agent/noVNC/app/images/clipboard.svg new file mode 100644 index 00000000000..79af2752504 --- /dev/null +++ b/systemvm/agent/noVNC/app/images/clipboard.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/connect.svg b/systemvm/agent/noVNC/app/images/connect.svg new file mode 100644 index 00000000000..56cde414b46 --- /dev/null +++ b/systemvm/agent/noVNC/app/images/connect.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/ctrl.svg b/systemvm/agent/noVNC/app/images/ctrl.svg new file mode 100644 index 00000000000..856e9395829 --- /dev/null +++ b/systemvm/agent/noVNC/app/images/ctrl.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/ctrlaltdel.svg b/systemvm/agent/noVNC/app/images/ctrlaltdel.svg new file mode 100644 index 00000000000..d7744ea31da --- /dev/null +++ b/systemvm/agent/noVNC/app/images/ctrlaltdel.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/disconnect.svg b/systemvm/agent/noVNC/app/images/disconnect.svg new file mode 100644 index 00000000000..6be7d187657 --- /dev/null +++ b/systemvm/agent/noVNC/app/images/disconnect.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/drag.svg b/systemvm/agent/noVNC/app/images/drag.svg new file mode 100644 index 00000000000..139caf947cd --- /dev/null +++ b/systemvm/agent/noVNC/app/images/drag.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/error.svg b/systemvm/agent/noVNC/app/images/error.svg new file mode 100644 index 00000000000..8356d3f1374 --- /dev/null +++ b/systemvm/agent/noVNC/app/images/error.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/esc.svg b/systemvm/agent/noVNC/app/images/esc.svg new file mode 100644 index 00000000000..830152b5f93 --- /dev/null +++ b/systemvm/agent/noVNC/app/images/esc.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/expander.svg b/systemvm/agent/noVNC/app/images/expander.svg new file mode 100644 index 00000000000..e1635358be9 --- /dev/null +++ b/systemvm/agent/noVNC/app/images/expander.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/fullscreen.svg b/systemvm/agent/noVNC/app/images/fullscreen.svg new file mode 100644 index 00000000000..29bd05da14d --- /dev/null +++ b/systemvm/agent/noVNC/app/images/fullscreen.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/handle.svg b/systemvm/agent/noVNC/app/images/handle.svg new file mode 100644 index 00000000000..4a7a126f9d3 --- /dev/null +++ b/systemvm/agent/noVNC/app/images/handle.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/handle_bg.svg b/systemvm/agent/noVNC/app/images/handle_bg.svg new file mode 100644 index 00000000000..7579c42cb78 --- /dev/null +++ b/systemvm/agent/noVNC/app/images/handle_bg.svg @@ -0,0 +1,172 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/icons/Makefile b/systemvm/agent/noVNC/app/images/icons/Makefile new file mode 100644 index 00000000000..be564b43b93 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/app/images/icons/novnc-120x120.png b/systemvm/agent/noVNC/app/images/icons/novnc-120x120.png new file mode 100644 index 00000000000..40823efbadf Binary files /dev/null and b/systemvm/agent/noVNC/app/images/icons/novnc-120x120.png differ diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-144x144.png b/systemvm/agent/noVNC/app/images/icons/novnc-144x144.png new file mode 100644 index 00000000000..eee71f11c74 Binary files /dev/null and b/systemvm/agent/noVNC/app/images/icons/novnc-144x144.png differ diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-152x152.png b/systemvm/agent/noVNC/app/images/icons/novnc-152x152.png new file mode 100644 index 00000000000..0694b2de39b Binary files /dev/null and b/systemvm/agent/noVNC/app/images/icons/novnc-152x152.png differ diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-16x16.png b/systemvm/agent/noVNC/app/images/icons/novnc-16x16.png new file mode 100644 index 00000000000..42108f40999 Binary files /dev/null and b/systemvm/agent/noVNC/app/images/icons/novnc-16x16.png differ diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-192x192.png b/systemvm/agent/noVNC/app/images/icons/novnc-192x192.png new file mode 100644 index 00000000000..ef9201f4370 Binary files /dev/null and b/systemvm/agent/noVNC/app/images/icons/novnc-192x192.png differ diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-24x24.png b/systemvm/agent/noVNC/app/images/icons/novnc-24x24.png new file mode 100644 index 00000000000..110613594b8 Binary files /dev/null and b/systemvm/agent/noVNC/app/images/icons/novnc-24x24.png differ diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-32x32.png b/systemvm/agent/noVNC/app/images/icons/novnc-32x32.png new file mode 100644 index 00000000000..ff00dc305a7 Binary files /dev/null and b/systemvm/agent/noVNC/app/images/icons/novnc-32x32.png differ diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-48x48.png b/systemvm/agent/noVNC/app/images/icons/novnc-48x48.png new file mode 100644 index 00000000000..f24cd6cc939 Binary files /dev/null and b/systemvm/agent/noVNC/app/images/icons/novnc-48x48.png differ diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-60x60.png b/systemvm/agent/noVNC/app/images/icons/novnc-60x60.png new file mode 100644 index 00000000000..06b0d609a0f Binary files /dev/null and b/systemvm/agent/noVNC/app/images/icons/novnc-60x60.png differ diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-64x64.png b/systemvm/agent/noVNC/app/images/icons/novnc-64x64.png new file mode 100644 index 00000000000..6d0fb34181b Binary files /dev/null and b/systemvm/agent/noVNC/app/images/icons/novnc-64x64.png differ diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-72x72.png b/systemvm/agent/noVNC/app/images/icons/novnc-72x72.png new file mode 100644 index 00000000000..23163a22d06 Binary files /dev/null and b/systemvm/agent/noVNC/app/images/icons/novnc-72x72.png differ diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-76x76.png b/systemvm/agent/noVNC/app/images/icons/novnc-76x76.png new file mode 100644 index 00000000000..aef61c48043 Binary files /dev/null and b/systemvm/agent/noVNC/app/images/icons/novnc-76x76.png differ diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-96x96.png b/systemvm/agent/noVNC/app/images/icons/novnc-96x96.png new file mode 100644 index 00000000000..1a77c53f4cb Binary files /dev/null and b/systemvm/agent/noVNC/app/images/icons/novnc-96x96.png differ diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-icon-sm.svg b/systemvm/agent/noVNC/app/images/icons/novnc-icon-sm.svg new file mode 100644 index 00000000000..aa1c6f185bc --- /dev/null +++ b/systemvm/agent/noVNC/app/images/icons/novnc-icon-sm.svg @@ -0,0 +1,163 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-icon.svg b/systemvm/agent/noVNC/app/images/icons/novnc-icon.svg new file mode 100644 index 00000000000..1efff912d48 --- /dev/null +++ b/systemvm/agent/noVNC/app/images/icons/novnc-icon.svg @@ -0,0 +1,163 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/info.svg b/systemvm/agent/noVNC/app/images/info.svg new file mode 100644 index 00000000000..557b772ff72 --- /dev/null +++ b/systemvm/agent/noVNC/app/images/info.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/keyboard.svg b/systemvm/agent/noVNC/app/images/keyboard.svg new file mode 100644 index 00000000000..137b350ab5d --- /dev/null +++ b/systemvm/agent/noVNC/app/images/keyboard.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/mouse_left.svg b/systemvm/agent/noVNC/app/images/mouse_left.svg new file mode 100644 index 00000000000..ce4cca41c79 --- /dev/null +++ b/systemvm/agent/noVNC/app/images/mouse_left.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/mouse_middle.svg b/systemvm/agent/noVNC/app/images/mouse_middle.svg new file mode 100644 index 00000000000..6603425cb3e --- /dev/null +++ b/systemvm/agent/noVNC/app/images/mouse_middle.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/mouse_none.svg b/systemvm/agent/noVNC/app/images/mouse_none.svg new file mode 100644 index 00000000000..3e0f838a77a --- /dev/null +++ b/systemvm/agent/noVNC/app/images/mouse_none.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/mouse_right.svg b/systemvm/agent/noVNC/app/images/mouse_right.svg new file mode 100644 index 00000000000..f4bad76797c --- /dev/null +++ b/systemvm/agent/noVNC/app/images/mouse_right.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/power.svg b/systemvm/agent/noVNC/app/images/power.svg new file mode 100644 index 00000000000..4925d3e8eba --- /dev/null +++ b/systemvm/agent/noVNC/app/images/power.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/settings.svg b/systemvm/agent/noVNC/app/images/settings.svg new file mode 100644 index 00000000000..dbb2e80a5b3 --- /dev/null +++ b/systemvm/agent/noVNC/app/images/settings.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/tab.svg b/systemvm/agent/noVNC/app/images/tab.svg new file mode 100644 index 00000000000..1ccb3229cdd --- /dev/null +++ b/systemvm/agent/noVNC/app/images/tab.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/toggleextrakeys.svg b/systemvm/agent/noVNC/app/images/toggleextrakeys.svg new file mode 100644 index 00000000000..b578c0d4062 --- /dev/null +++ b/systemvm/agent/noVNC/app/images/toggleextrakeys.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/warning.svg b/systemvm/agent/noVNC/app/images/warning.svg new file mode 100644 index 00000000000..7114f9b1235 --- /dev/null +++ b/systemvm/agent/noVNC/app/images/warning.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/windows.svg b/systemvm/agent/noVNC/app/images/windows.svg new file mode 100644 index 00000000000..270405c7ff2 --- /dev/null +++ b/systemvm/agent/noVNC/app/images/windows.svg @@ -0,0 +1,85 @@ + + + +image/svg+xml + + + + + + + + + + \ No newline at end of file diff --git a/systemvm/agent/noVNC/app/locale/cs.json b/systemvm/agent/noVNC/app/locale/cs.json new file mode 100644 index 00000000000..589145ef36e --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/app/locale/de.json b/systemvm/agent/noVNC/app/locale/de.json new file mode 100644 index 00000000000..62e73360f50 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/app/locale/el.json b/systemvm/agent/noVNC/app/locale/el.json new file mode 100644 index 00000000000..f801251c59a --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/app/locale/es.json b/systemvm/agent/noVNC/app/locale/es.json new file mode 100644 index 00000000000..23f23f4972f --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/app/locale/ko.json b/systemvm/agent/noVNC/app/locale/ko.json new file mode 100644 index 00000000000..e4ecddcfda6 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/app/locale/nl.json b/systemvm/agent/noVNC/app/locale/nl.json new file mode 100644 index 00000000000..0cdcc92a9b3 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/app/locale/pl.json b/systemvm/agent/noVNC/app/locale/pl.json new file mode 100644 index 00000000000..006ac7a55f5 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/app/locale/ru.json b/systemvm/agent/noVNC/app/locale/ru.json new file mode 100644 index 00000000000..52e57f37f1b --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/app/locale/sv.json b/systemvm/agent/noVNC/app/locale/sv.json new file mode 100644 index 00000000000..d49ea540d93 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/app/locale/tr.json b/systemvm/agent/noVNC/app/locale/tr.json new file mode 100644 index 00000000000..451c1b8a643 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/app/locale/zh_CN.json b/systemvm/agent/noVNC/app/locale/zh_CN.json new file mode 100644 index 00000000000..b66995620ff --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/app/locale/zh_TW.json b/systemvm/agent/noVNC/app/locale/zh_TW.json new file mode 100644 index 00000000000..8ddf813f084 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/app/localization.js b/systemvm/agent/noVNC/app/localization.js new file mode 100644 index 00000000000..100901c9d26 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/app/sounds/CREDITS b/systemvm/agent/noVNC/app/sounds/CREDITS new file mode 100644 index 00000000000..ec1fb556b2b --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/app/sounds/bell.mp3 b/systemvm/agent/noVNC/app/sounds/bell.mp3 new file mode 100644 index 00000000000..fdbf149a1e9 Binary files /dev/null and b/systemvm/agent/noVNC/app/sounds/bell.mp3 differ diff --git a/systemvm/agent/noVNC/app/sounds/bell.oga b/systemvm/agent/noVNC/app/sounds/bell.oga new file mode 100644 index 00000000000..144d2b3670e Binary files /dev/null and b/systemvm/agent/noVNC/app/sounds/bell.oga differ diff --git a/systemvm/agent/noVNC/app/styles/Orbitron700.ttf b/systemvm/agent/noVNC/app/styles/Orbitron700.ttf new file mode 100644 index 00000000000..e28729dc56b Binary files /dev/null and b/systemvm/agent/noVNC/app/styles/Orbitron700.ttf differ diff --git a/systemvm/agent/noVNC/app/styles/Orbitron700.woff b/systemvm/agent/noVNC/app/styles/Orbitron700.woff new file mode 100644 index 00000000000..61db630cce1 Binary files /dev/null and b/systemvm/agent/noVNC/app/styles/Orbitron700.woff differ diff --git a/systemvm/agent/noVNC/app/styles/base.css b/systemvm/agent/noVNC/app/styles/base.css new file mode 100644 index 00000000000..3ca9894dc74 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/app/ui.js b/systemvm/agent/noVNC/app/ui.js new file mode 100644 index 00000000000..13d1c015871 --- /dev/null +++ b/systemvm/agent/noVNC/app/ui.js @@ -0,0 +1,1660 @@ +/* + * 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', false); + 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; + + var urlParams = new URLSearchParams(window.location.search); + var param = urlParams.get('token'); + if (param) { + url += "?token=" + param + } + + 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/agent/noVNC/app/webutil.js b/systemvm/agent/noVNC/app/webutil.js new file mode 100644 index 00000000000..98e1d9e68da --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/base64.js b/systemvm/agent/noVNC/core/base64.js new file mode 100644 index 00000000000..88e745466e5 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/decoders/copyrect.js b/systemvm/agent/noVNC/core/decoders/copyrect.js new file mode 100644 index 00000000000..a78ded754f9 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/decoders/hextile.js b/systemvm/agent/noVNC/core/decoders/hextile.js new file mode 100644 index 00000000000..aa76d2f37b1 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/decoders/raw.js b/systemvm/agent/noVNC/core/decoders/raw.js new file mode 100644 index 00000000000..f676e0d941f --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/decoders/rre.js b/systemvm/agent/noVNC/core/decoders/rre.js new file mode 100644 index 00000000000..57414a098ff --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/decoders/tight.js b/systemvm/agent/noVNC/core/decoders/tight.js new file mode 100644 index 00000000000..bcda04ce7f8 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/decoders/tightpng.js b/systemvm/agent/noVNC/core/decoders/tightpng.js new file mode 100644 index 00000000000..7bbde3a43b5 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/des.js b/systemvm/agent/noVNC/core/des.js new file mode 100644 index 00000000000..d2f807b828f --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/display.js b/systemvm/agent/noVNC/core/display.js new file mode 100644 index 00000000000..1528384d3af --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/encodings.js b/systemvm/agent/noVNC/core/encodings.js new file mode 100644 index 00000000000..9fd38d58fcc --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/inflator.js b/systemvm/agent/noVNC/core/inflator.js new file mode 100644 index 00000000000..0eab8fe48c2 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/input/domkeytable.js b/systemvm/agent/noVNC/core/input/domkeytable.js new file mode 100644 index 00000000000..60ae3f91902 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/input/fixedkeys.js b/systemvm/agent/noVNC/core/input/fixedkeys.js new file mode 100644 index 00000000000..4d09f2f7e08 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/input/keyboard.js b/systemvm/agent/noVNC/core/input/keyboard.js new file mode 100644 index 00000000000..9dbc8d6e6ed --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/input/keysym.js b/systemvm/agent/noVNC/core/input/keysym.js new file mode 100644 index 00000000000..22ba0584eae --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/input/keysymdef.js b/systemvm/agent/noVNC/core/input/keysymdef.js new file mode 100644 index 00000000000..951cacab673 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/input/mouse.js b/systemvm/agent/noVNC/core/input/mouse.js new file mode 100644 index 00000000000..58a2982a961 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/input/util.js b/systemvm/agent/noVNC/core/input/util.js new file mode 100644 index 00000000000..f177ef53d36 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/input/vkeys.js b/systemvm/agent/noVNC/core/input/vkeys.js new file mode 100644 index 00000000000..f84109b2559 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/input/xtscancodes.js b/systemvm/agent/noVNC/core/input/xtscancodes.js new file mode 100644 index 00000000000..514809c6fa2 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/rfb.js b/systemvm/agent/noVNC/core/rfb.js new file mode 100644 index 00000000000..e40df6659e5 --- /dev/null +++ b/systemvm/agent/noVNC/core/rfb.js @@ -0,0 +1,2060 @@ +/* + * 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 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 = false; + 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); + } + + 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); + } + + sendCtrlEsc() { + if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; } + Log.Info("Sending Ctrl-Esc"); + + this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true); + this.sendKey(KeyTable.XK_Escape, "Escape", true); + this.sendKey(KeyTable.XK_Escape, "Escape", 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 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"); + } + + 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([0]); // ClientInitialisation for exclusive access + 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/agent/noVNC/core/util/browser.js b/systemvm/agent/noVNC/core/util/browser.js new file mode 100644 index 00000000000..8996cfeda71 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/util/cursor.js b/systemvm/agent/noVNC/core/util/cursor.js new file mode 100644 index 00000000000..0d0b754a863 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/util/events.js b/systemvm/agent/noVNC/core/util/events.js new file mode 100644 index 00000000000..f1222796a7e --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/util/eventtarget.js b/systemvm/agent/noVNC/core/util/eventtarget.js new file mode 100644 index 00000000000..f54ca9bf112 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/util/logging.js b/systemvm/agent/noVNC/core/util/logging.js new file mode 100644 index 00000000000..4c8943d0054 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/util/polyfill.js b/systemvm/agent/noVNC/core/util/polyfill.js new file mode 100644 index 00000000000..648ceebc3c2 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/util/strings.js b/systemvm/agent/noVNC/core/util/strings.js new file mode 100644 index 00000000000..61f4f237d93 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/core/websock.js b/systemvm/agent/noVNC/core/websock.js new file mode 100644 index 00000000000..51b9a66fb68 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/docs/API-internal.md b/systemvm/agent/noVNC/docs/API-internal.md new file mode 100644 index 00000000000..0b29afb61fc --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/docs/API.md b/systemvm/agent/noVNC/docs/API.md new file mode 100644 index 00000000000..d587429c176 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/docs/EMBEDDING.md b/systemvm/agent/noVNC/docs/EMBEDDING.md new file mode 100644 index 00000000000..5399b48ba76 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/docs/LIBRARY.md b/systemvm/agent/noVNC/docs/LIBRARY.md new file mode 100644 index 00000000000..63f55e8f179 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/docs/LICENSE.BSD-2-Clause b/systemvm/agent/noVNC/docs/LICENSE.BSD-2-Clause new file mode 100644 index 00000000000..9d66ec911bf --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/docs/LICENSE.BSD-3-Clause b/systemvm/agent/noVNC/docs/LICENSE.BSD-3-Clause new file mode 100644 index 00000000000..e160466c4e0 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/docs/LICENSE.MPL-2.0 b/systemvm/agent/noVNC/docs/LICENSE.MPL-2.0 new file mode 100644 index 00000000000..14e2f777f6c --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/docs/LICENSE.OFL-1.1 b/systemvm/agent/noVNC/docs/LICENSE.OFL-1.1 new file mode 100644 index 00000000000..77b17316cf1 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/docs/flash_policy.txt b/systemvm/agent/noVNC/docs/flash_policy.txt new file mode 100644 index 00000000000..df325c0ddf5 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/docs/links b/systemvm/agent/noVNC/docs/links new file mode 100644 index 00000000000..31544ce0e12 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/docs/notes b/systemvm/agent/noVNC/docs/notes new file mode 100644 index 00000000000..dfef0bd6afe --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/docs/rfb_notes b/systemvm/agent/noVNC/docs/rfb_notes new file mode 100644 index 00000000000..643e16c01e7 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/docs/rfbproto-3.3.pdf b/systemvm/agent/noVNC/docs/rfbproto-3.3.pdf new file mode 100644 index 00000000000..56b876436a9 Binary files /dev/null and b/systemvm/agent/noVNC/docs/rfbproto-3.3.pdf differ diff --git a/systemvm/agent/noVNC/docs/rfbproto-3.7.pdf b/systemvm/agent/noVNC/docs/rfbproto-3.7.pdf new file mode 100644 index 00000000000..1ef54623c17 Binary files /dev/null and b/systemvm/agent/noVNC/docs/rfbproto-3.7.pdf differ diff --git a/systemvm/agent/noVNC/docs/rfbproto-3.8.pdf b/systemvm/agent/noVNC/docs/rfbproto-3.8.pdf new file mode 100644 index 00000000000..8f0730fb7f9 Binary files /dev/null and b/systemvm/agent/noVNC/docs/rfbproto-3.8.pdf differ diff --git a/systemvm/agent/noVNC/karma.conf.js b/systemvm/agent/noVNC/karma.conf.js new file mode 100644 index 00000000000..5cbd7a5de86 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/package.json b/systemvm/agent/noVNC/package.json new file mode 100644 index 00000000000..2d84a5f38e5 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/po/Makefile b/systemvm/agent/noVNC/po/Makefile new file mode 100644 index 00000000000..6dbd83043f7 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/po/cs.po b/systemvm/agent/noVNC/po/cs.po new file mode 100644 index 00000000000..2b1efd8d918 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/po/de.po b/systemvm/agent/noVNC/po/de.po new file mode 100644 index 00000000000..0c3fa0d482a --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/po/el.po b/systemvm/agent/noVNC/po/el.po new file mode 100644 index 00000000000..5213ae5423a --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/po/es.po b/systemvm/agent/noVNC/po/es.po new file mode 100644 index 00000000000..e15655fbfc9 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/po/ko.po b/systemvm/agent/noVNC/po/ko.po new file mode 100644 index 00000000000..87ae1069741 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/po/nl.po b/systemvm/agent/noVNC/po/nl.po new file mode 100644 index 00000000000..343204a9fd2 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/po/noVNC.pot b/systemvm/agent/noVNC/po/noVNC.pot new file mode 100644 index 00000000000..200be01de6e --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/po/pl.po b/systemvm/agent/noVNC/po/pl.po new file mode 100644 index 00000000000..5acfdc4f4b8 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/po/po2js b/systemvm/agent/noVNC/po/po2js new file mode 100755 index 00000000000..03c14900fff --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/po/ru.po b/systemvm/agent/noVNC/po/ru.po new file mode 100644 index 00000000000..fb5d0875ef8 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/po/sv.po b/systemvm/agent/noVNC/po/sv.po new file mode 100644 index 00000000000..f7955662957 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/po/tr.po b/systemvm/agent/noVNC/po/tr.po new file mode 100644 index 00000000000..8b5c1813455 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/po/xgettext-html b/systemvm/agent/noVNC/po/xgettext-html new file mode 100755 index 00000000000..547f5687698 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/po/zh_CN.po b/systemvm/agent/noVNC/po/zh_CN.po new file mode 100644 index 00000000000..78bfb958d5d --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/po/zh_TW.po b/systemvm/agent/noVNC/po/zh_TW.po new file mode 100644 index 00000000000..9ddf550c1d2 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/tests/.eslintrc b/systemvm/agent/noVNC/tests/.eslintrc new file mode 100644 index 00000000000..545fa2ed25e --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/tests/assertions.js b/systemvm/agent/noVNC/tests/assertions.js new file mode 100644 index 00000000000..07a5c297768 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/tests/fake.websocket.js b/systemvm/agent/noVNC/tests/fake.websocket.js new file mode 100644 index 00000000000..68ab3f8487d --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/tests/karma-test-main.js b/systemvm/agent/noVNC/tests/karma-test-main.js new file mode 100644 index 00000000000..28436667e6d --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/tests/playback-ui.js b/systemvm/agent/noVNC/tests/playback-ui.js new file mode 100644 index 00000000000..65c715a9fe5 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/tests/playback.js b/systemvm/agent/noVNC/tests/playback.js new file mode 100644 index 00000000000..5bd8103a840 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/tests/test.base64.js b/systemvm/agent/noVNC/tests/test.base64.js new file mode 100644 index 00000000000..04bd207b7cf --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/tests/test.display.js b/systemvm/agent/noVNC/tests/test.display.js new file mode 100644 index 00000000000..b359550326d --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/tests/test.helper.js b/systemvm/agent/noVNC/tests/test.helper.js new file mode 100644 index 00000000000..d44bab0fe2d --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/tests/test.keyboard.js b/systemvm/agent/noVNC/tests/test.keyboard.js new file mode 100644 index 00000000000..77fe3f6f968 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/tests/test.localization.js b/systemvm/agent/noVNC/tests/test.localization.js new file mode 100644 index 00000000000..9570c1798a9 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/tests/test.mouse.js b/systemvm/agent/noVNC/tests/test.mouse.js new file mode 100644 index 00000000000..78c74f15724 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/tests/test.rfb.js b/systemvm/agent/noVNC/tests/test.rfb.js new file mode 100644 index 00000000000..99c9c90c8ff --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/tests/test.util.js b/systemvm/agent/noVNC/tests/test.util.js new file mode 100644 index 00000000000..201acc8bb0d --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/tests/test.websock.js b/systemvm/agent/noVNC/tests/test.websock.js new file mode 100644 index 00000000000..30e19e9de71 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/tests/test.webutil.js b/systemvm/agent/noVNC/tests/test.webutil.js new file mode 100644 index 00000000000..72e194210d5 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/tests/vnc_playback.html b/systemvm/agent/noVNC/tests/vnc_playback.html new file mode 100644 index 00000000000..4fd74658053 --- /dev/null +++ b/systemvm/agent/noVNC/tests/vnc_playback.html @@ -0,0 +1,43 @@ + + + + VNC Playback + + + + + + + + + + + Iterations:   + Perftest:  + Realtime:   + +   + +

+ + Results:
+ + +

+ +
+
Loading
+
+ + + + diff --git a/systemvm/agent/noVNC/utils/.eslintrc b/systemvm/agent/noVNC/utils/.eslintrc new file mode 100644 index 00000000000..b7dc129f139 --- /dev/null +++ b/systemvm/agent/noVNC/utils/.eslintrc @@ -0,0 +1,8 @@ +{ + "env": { + "node": true + }, + "rules": { + "no-console": 0 + } +} \ No newline at end of file diff --git a/systemvm/agent/noVNC/utils/README.md b/systemvm/agent/noVNC/utils/README.md new file mode 100644 index 00000000000..32582e65ea8 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/utils/b64-to-binary.pl b/systemvm/agent/noVNC/utils/b64-to-binary.pl new file mode 100755 index 00000000000..280e28c93f0 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/utils/genkeysymdef.js b/systemvm/agent/noVNC/utils/genkeysymdef.js new file mode 100755 index 00000000000..d21773f9f65 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/utils/img2js.py b/systemvm/agent/noVNC/utils/img2js.py new file mode 100755 index 00000000000..ceab6bf7543 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/utils/json2graph.py b/systemvm/agent/noVNC/utils/json2graph.py new file mode 100755 index 00000000000..bdaeeccaf21 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/utils/launch.sh b/systemvm/agent/noVNC/utils/launch.sh new file mode 100755 index 00000000000..162607eb05c --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/utils/u2x11 b/systemvm/agent/noVNC/utils/u2x11 new file mode 100755 index 00000000000..fd3e4ba88a5 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/utils/use_require.js b/systemvm/agent/noVNC/utils/use_require.js new file mode 100755 index 00000000000..248792718c9 --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/utils/use_require_helpers.js b/systemvm/agent/noVNC/utils/use_require_helpers.js new file mode 100644 index 00000000000..a4f99c7045c --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/utils/validate b/systemvm/agent/noVNC/utils/validate new file mode 100755 index 00000000000..a6b5507d2ac --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/vendor/browser-es-module-loader/README.md b/systemvm/agent/noVNC/vendor/browser-es-module-loader/README.md new file mode 100644 index 00000000000..c26867f979f --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/vendor/browser-es-module-loader/rollup.config.js b/systemvm/agent/noVNC/vendor/browser-es-module-loader/rollup.config.js new file mode 100644 index 00000000000..4bf4a5fd18c --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/vendor/browser-es-module-loader/src/babel-worker.js b/systemvm/agent/noVNC/vendor/browser-es-module-loader/src/babel-worker.js new file mode 100644 index 00000000000..007bd6850cb --- /dev/null +++ b/systemvm/agent/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/agent/noVNC/vendor/browser-es-module-loader/src/browser-es-module-loader.js b/systemvm/agent/noVNC/vendor/browser-es-module-loader/src/browser-es-module-loader.js new file mode 100644 index 00000000000..efae617061f --- /dev/null +++ b/systemvm/agent/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; + +// + + + + + + + + + + + + + + +
+
+
noVNC encountered an error:
+
+
+
+
+ + +
+ +
+
+ +
+ +

no
VNC

+ + + + + +
+ + + + + +
+ + +
+ +
+
+ + + + + + +
+
+
+ + + +
+
+
+ Power +
+ + + +
+
+ + + +
+
+
+ Clipboard +
+ +
+ +
+
+ + + + + + +
+
+
    +
  • + Settings +
  • +
  • + +
  • +
  • + +
  • +

  • +
  • + +
  • +
  • + + +
  • +

  • +
  • +
    Advanced
    +
      +
    • + + +
    • +
    • +
      WebSocket
      +
        +
      • + +
      • +
      • + + +
      • +
      • + + +
      • +
      • + + +
      • +
      +
    • +

    • +
    • + +
    • +
    • + + +
    • +

    • +
    • + +
    • +

    • + +
    • + +
    • +
    +
  • +
+
+
+ + + + +
+
+ +
+ +
+ + +
+ + +
+
+ +
+ Connect +
+
+
+ + +
+
+
    +
  • + + +
  • +
  • + +
  • +
+
+
+ + +
+
+
+ +
+
+
+ + +
+ + +
+ + + + diff --git a/systemvm/agent/noVNC/vnc_lite.html b/systemvm/agent/noVNC/vnc_lite.html new file mode 100644 index 00000000000..12ac1d53b82 --- /dev/null +++ b/systemvm/agent/noVNC/vnc_lite.html @@ -0,0 +1,219 @@ + + + + + + noVNC + + + + + + + + + + + + + + + + + +
+
Loading
+
Send CtrlAltDel
+
Send CtrlEsc
+
+
+ +
+ + diff --git a/systemvm/debian/etc/iptables/iptables-consoleproxy b/systemvm/debian/etc/iptables/iptables-consoleproxy index 9a1c9855eed..631a4b079a8 100644 --- a/systemvm/debian/etc/iptables/iptables-consoleproxy +++ b/systemvm/debian/etc/iptables/iptables-consoleproxy @@ -35,4 +35,5 @@ 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 COMMIT diff --git a/systemvm/systemvm-agent-descriptor.xml b/systemvm/systemvm-agent-descriptor.xml index a3f0453cffd..74b154387c3 100644 --- a/systemvm/systemvm-agent-descriptor.xml +++ b/systemvm/systemvm-agent-descriptor.xml @@ -112,5 +112,14 @@ *.key + + agent/noVNC + noVNC + 555 + 555 + + **/* + +