diff --git a/systemvm/agent/noVNC/app/error-handler.js b/systemvm/agent/noVNC/app/error-handler.js index 81a6cba8e6e..67b63720cba 100644 --- a/systemvm/agent/noVNC/app/error-handler.js +++ b/systemvm/agent/noVNC/app/error-handler.js @@ -6,61 +6,74 @@ * See README.md for usage and integration instructions. */ -// NB: this should *not* be included as a module until we have -// native support in the browsers, so that our error handler -// can catch script-loading errors. +// Fallback for all uncought errors +function handleError(event, err) { + try { + const msg = document.getElementById('noVNC_fallback_errormsg'); -// No ES6 can be used in this file since it's used for the translation -/* eslint-disable prefer-arrow-callback */ - -(function _scope() { - "use strict"; - - // Fallback for all uncought errors - function handleError(event, err) { - try { - const msg = document.getElementById('noVNC_fallback_errormsg'); - - // Only show the initial error - if (msg.hasChildNodes()) { - return false; - } - - let div = document.createElement("div"); - div.classList.add('noVNC_message'); - div.appendChild(document.createTextNode(event.message)); - msg.appendChild(div); - - if (event.filename) { - div = document.createElement("div"); - div.className = 'noVNC_location'; - let text = event.filename; - if (event.lineno !== undefined) { - text += ":" + event.lineno; - if (event.colno !== undefined) { - text += ":" + event.colno; - } - } - div.appendChild(document.createTextNode(text)); - msg.appendChild(div); - } - - if (err && err.stack) { - div = document.createElement("div"); - div.className = 'noVNC_stack'; - div.appendChild(document.createTextNode(err.stack)); - msg.appendChild(div); - } - - document.getElementById('noVNC_fallback_error') - .classList.add("noVNC_open"); - } catch (exc) { - document.write("noVNC encountered an error."); + // Work around Firefox bug: + // https://bugzilla.mozilla.org/show_bug.cgi?id=1685038 + if (event.message === "ResizeObserver loop completed with undelivered notifications.") { + return false; } - // Don't return true since this would prevent the error - // from being printed to the browser console. - return false; + + // Only show the initial error + if (msg.hasChildNodes()) { + return false; + } + + let div = document.createElement("div"); + div.classList.add('noVNC_message'); + div.appendChild(document.createTextNode(event.message)); + msg.appendChild(div); + + if (event.filename) { + div = document.createElement("div"); + div.className = 'noVNC_location'; + let text = event.filename; + if (event.lineno !== undefined) { + text += ":" + event.lineno; + if (event.colno !== undefined) { + text += ":" + event.colno; + } + } + div.appendChild(document.createTextNode(text)); + msg.appendChild(div); + } + + if (err && err.stack) { + div = document.createElement("div"); + div.className = 'noVNC_stack'; + div.appendChild(document.createTextNode(err.stack)); + msg.appendChild(div); + } + + document.getElementById('noVNC_fallback_error') + .classList.add("noVNC_open"); + + } catch (exc) { + document.write("noVNC encountered an error."); } - window.addEventListener('error', function onerror(evt) { handleError(evt, evt.error); }); - window.addEventListener('unhandledrejection', function onreject(evt) { handleError(evt.reason, evt.reason); }); -})(); + + // Try to disable keyboard interaction, best effort + try { + // Remove focus from the currently focused element in order to + // prevent keyboard interaction from continuing + if (document.activeElement) { document.activeElement.blur(); } + + // Don't let any element be focusable when showing the error + let keyboardFocusable = 'a[href], button, input, textarea, select, details, [tabindex]'; + document.querySelectorAll(keyboardFocusable).forEach((elem) => { + elem.setAttribute("tabindex", "-1"); + }); + } catch (exc) { + // Do nothing + } + + // Don't return true since this would prevent the error + // from being printed to the browser console. + return false; +} + +window.addEventListener('error', evt => handleError(evt, evt.error)); +window.addEventListener('unhandledrejection', evt => handleError(evt.reason, evt.reason)); diff --git a/systemvm/agent/noVNC/app/images/icons/Makefile b/systemvm/agent/noVNC/app/images/icons/Makefile index be564b43b93..03eaed0711d 100644 --- a/systemvm/agent/noVNC/app/images/icons/Makefile +++ b/systemvm/agent/noVNC/app/images/icons/Makefile @@ -1,42 +1,42 @@ -ICONS := \ - novnc-16x16.png \ - novnc-24x24.png \ - novnc-32x32.png \ - novnc-48x48.png \ - novnc-64x64.png +BROWSER_SIZES := 16 24 32 48 64 +#ANDROID_SIZES := 72 96 144 192 +# FIXME: The ICO is limited to 8 icons due to a Chrome bug: +# https://bugs.chromium.org/p/chromium/issues/detail?id=1381393 +ANDROID_SIZES := 96 144 192 +WEB_ICON_SIZES := $(BROWSER_SIZES) $(ANDROID_SIZES) -ANDROID_LAUNCHER := \ - novnc-48x48.png \ - novnc-72x72.png \ - novnc-96x96.png \ - novnc-144x144.png \ - novnc-192x192.png +#IOS_1X_SIZES := 20 29 40 76 # No such devices exist anymore +IOS_2X_SIZES := 40 58 80 120 152 167 +IOS_3X_SIZES := 60 87 120 180 +ALL_IOS_SIZES := $(IOS_1X_SIZES) $(IOS_2X_SIZES) $(IOS_3X_SIZES) -IPHONE_LAUNCHER := \ - novnc-60x60.png \ - novnc-120x120.png - -IPAD_LAUNCHER := \ - novnc-76x76.png \ - novnc-152x152.png - -ALL_ICONS := $(ICONS) $(ANDROID_LAUNCHER) $(IPHONE_LAUNCHER) $(IPAD_LAUNCHER) +ALL_ICONS := \ + $(ALL_IOS_SIZES:%=novnc-ios-%.png) \ + novnc.ico all: $(ALL_ICONS) -novnc-16x16.png: novnc-icon-sm.svg - convert -density 90 \ - -background transparent "$<" "$@" -novnc-24x24.png: novnc-icon-sm.svg - convert -density 135 \ - -background transparent "$<" "$@" -novnc-32x32.png: novnc-icon-sm.svg - convert -density 180 \ - -background transparent "$<" "$@" +# Our testing shows that the ICO file need to be sorted in largest to +# smallest to get the apporpriate behviour +WEB_ICON_SIZES_REVERSE := $(shell echo $(WEB_ICON_SIZES) | tr ' ' '\n' | sort -nr | tr '\n' ' ') +WEB_BASE_ICONS := $(WEB_ICON_SIZES_REVERSE:%=novnc-%.png) +.INTERMEDIATE: $(WEB_BASE_ICONS) +novnc.ico: $(WEB_BASE_ICONS) + convert $(WEB_BASE_ICONS) "$@" + +# General conversion novnc-%.png: novnc-icon.svg - convert -density $$[`echo $* | cut -d x -f 1` * 90 / 48] \ - -background transparent "$<" "$@" + convert -depth 8 -background transparent \ + -size $*x$* "$(lastword $^)" "$@" + +# iOS icons use their own SVG +novnc-ios-%.png: novnc-ios-icon.svg + convert -depth 8 -background transparent \ + -size $*x$* "$(lastword $^)" "$@" + +# The smallest sizes are generated using a different SVG +novnc-16.png novnc-24.png novnc-32.png: novnc-icon-sm.svg clean: rm -f *.png diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-ios-120.png b/systemvm/agent/noVNC/app/images/icons/novnc-ios-120.png new file mode 100644 index 00000000000..e69de29bb2d diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-ios-152.png b/systemvm/agent/noVNC/app/images/icons/novnc-ios-152.png new file mode 100644 index 00000000000..e69de29bb2d diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-ios-167.png b/systemvm/agent/noVNC/app/images/icons/novnc-ios-167.png new file mode 100644 index 00000000000..e69de29bb2d diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-ios-180.png b/systemvm/agent/noVNC/app/images/icons/novnc-ios-180.png new file mode 100644 index 00000000000..e69de29bb2d diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-ios-40.png b/systemvm/agent/noVNC/app/images/icons/novnc-ios-40.png new file mode 100644 index 00000000000..e69de29bb2d diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-ios-58.png b/systemvm/agent/noVNC/app/images/icons/novnc-ios-58.png new file mode 100644 index 00000000000..e69de29bb2d diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-ios-60.png b/systemvm/agent/noVNC/app/images/icons/novnc-ios-60.png new file mode 100644 index 00000000000..e69de29bb2d diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-ios-80.png b/systemvm/agent/noVNC/app/images/icons/novnc-ios-80.png new file mode 100644 index 00000000000..e69de29bb2d diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-ios-87.png b/systemvm/agent/noVNC/app/images/icons/novnc-ios-87.png new file mode 100644 index 00000000000..e69de29bb2d diff --git a/systemvm/agent/noVNC/app/images/icons/novnc-ios-icon.svg b/systemvm/agent/noVNC/app/images/icons/novnc-ios-icon.svg new file mode 100644 index 00000000000..009452ac63d --- /dev/null +++ b/systemvm/agent/noVNC/app/images/icons/novnc-ios-icon.svg @@ -0,0 +1,183 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/systemvm/agent/noVNC/app/images/icons/novnc.ico b/systemvm/agent/noVNC/app/images/icons/novnc.ico new file mode 100644 index 00000000000..e69de29bb2d diff --git a/systemvm/agent/noVNC/app/locale/es.json b/systemvm/agent/noVNC/app/locale/es.json index 23f23f4972f..b9e663a3d55 100644 --- a/systemvm/agent/noVNC/app/locale/es.json +++ b/systemvm/agent/noVNC/app/locale/es.json @@ -4,9 +4,9 @@ "Connected (unencrypted) to ": "Conectado (sin encriptación) a", "Disconnecting...": "Desconectando...", "Disconnected": "Desconectado", - "Must set host": "Debes configurar el host", + "Must set host": "Se debe configurar el host", "Reconnecting...": "Reconectando...", - "Password is required": "Contraseña es obligatoria", + "Password is required": "La contraseña es obligatoria", "Disconnect timeout": "Tiempo de desconexión agotado", "noVNC encountered an error:": "noVNC ha encontrado un error:", "Hide/Show the control bar": "Ocultar/Mostrar la barra de control", @@ -41,6 +41,7 @@ "Clear": "Vaciar", "Fullscreen": "Pantalla Completa", "Settings": "Configuraciones", + "Encrypt": "Encriptar", "Shared Mode": "Modo Compartido", "View Only": "Solo visualización", "Clip to Window": "Recortar al tamaño de la ventana", @@ -51,18 +52,17 @@ "Remote Resizing": "Cambio de tamaño remoto", "Advanced": "Avanzado", "Local Cursor": "Cursor Local", - "Repeater ID:": "ID del Repetidor", + "Repeater ID:": "ID del Repetidor:", "WebSocket": "WebSocket", - "Encrypt": "", - "Host:": "Host", - "Port:": "Puesto", - "Path:": "Ruta", + "Host:": "Host:", + "Port:": "Puerto:", + "Path:": "Ruta:", "Automatic Reconnect": "Reconexión automática", - "Reconnect Delay (ms):": "Retraso en la reconexión (ms)", - "Logging:": "Logging", + "Reconnect Delay (ms):": "Retraso en la reconexión (ms):", + "Logging:": "Registrando:", "Disconnect": "Desconectar", "Connect": "Conectar", - "Password:": "Contraseña", + "Password:": "Contraseña:", "Cancel": "Cancelar", - "Canvas not supported.": "Canvas no está soportado" + "Canvas not supported.": "Canvas no soportado." } \ No newline at end of file diff --git a/systemvm/agent/noVNC/app/locale/fr.json b/systemvm/agent/noVNC/app/locale/fr.json new file mode 100644 index 00000000000..22531f73b92 --- /dev/null +++ b/systemvm/agent/noVNC/app/locale/fr.json @@ -0,0 +1,78 @@ +{ + "HTTPS is required for full functionality": "", + "Connecting...": "En cours de connexion...", + "Disconnecting...": "Déconnexion en cours...", + "Reconnecting...": "Reconnexion en cours...", + "Internal error": "Erreur interne", + "Must set host": "Doit définir l'hôte", + "Connected (encrypted) to ": "Connecté (chiffré) à ", + "Connected (unencrypted) to ": "Connecté (non chiffré) à ", + "Something went wrong, connection is closed": "Quelque chose s'est mal passé, la connexion a été fermée", + "Failed to connect to server": "Échec de connexion au serveur", + "Disconnected": "Déconnecté", + "New connection has been rejected with reason: ": "Une nouvelle connexion a été rejetée avec motif : ", + "New connection has been rejected": "Une nouvelle connexion a été rejetée", + "Credentials are required": "Les identifiants sont requis", + "noVNC encountered an error:": "noVNC a rencontré une erreur :", + "Hide/Show the control bar": "Masquer/Afficher la barre de contrôle", + "Drag": "Faire glisser", + "Move/Drag Viewport": "Déplacer/faire glisser le Viewport", + "Keyboard": "Clavier", + "Show Keyboard": "Afficher le clavier", + "Extra keys": "Touches supplémentaires", + "Show Extra Keys": "Afficher les touches supplémentaires", + "Ctrl": "Ctrl", + "Toggle Ctrl": "Basculer Ctrl", + "Alt": "Alt", + "Toggle Alt": "Basculer Alt", + "Toggle Windows": "Basculer Windows", + "Windows": "Windows", + "Send Tab": "Envoyer l'onglet", + "Tab": "l'onglet", + "Esc": "Esc", + "Send Escape": "Envoyer Escape", + "Ctrl+Alt+Del": "Ctrl+Alt+Del", + "Send Ctrl-Alt-Del": "Envoyer Ctrl-Alt-Del", + "Shutdown/Reboot": "Arrêter/Redémarrer", + "Shutdown/Reboot...": "Arrêter/Redémarrer...", + "Power": "Alimentation", + "Shutdown": "Arrêter", + "Reboot": "Redémarrer", + "Reset": "Réinitialiser", + "Clipboard": "Presse-papiers", + "Edit clipboard content in the textarea below.": "", + "Settings": "Paramètres", + "Shared Mode": "Mode partagé", + "View Only": "Afficher uniquement", + "Clip to Window": "Clip à fenêtre", + "Scaling Mode:": "Mode mise à l'échelle :", + "None": "Aucun", + "Local Scaling": "Mise à l'échelle locale", + "Remote Resizing": "Redimensionnement à distance", + "Advanced": "Avancé", + "Quality:": "Qualité :", + "Compression level:": "Niveau de compression :", + "Repeater ID:": "ID Répéteur :", + "WebSocket": "WebSocket", + "Encrypt": "Chiffrer", + "Host:": "Hôte :", + "Port:": "Port :", + "Path:": "Chemin :", + "Automatic Reconnect": "Reconnecter automatiquemen", + "Reconnect Delay (ms):": "Délai de reconnexion (ms) :", + "Show Dot when No Cursor": "Afficher le point lorsqu'il n'y a pas de curseur", + "Logging:": "Se connecter :", + "Version:": "Version :", + "Disconnect": "Déconnecter", + "Connect": "Connecter", + "Server identity": "", + "The server has provided the following identifying information:": "", + "Fingerprint:": "", + "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "", + "Approve": "", + "Reject": "", + "Username:": "Nom d'utilisateur :", + "Password:": "Mot de passe :", + "Send Credentials": "Envoyer les identifiants", + "Cancel": "Annuler" +} \ No newline at end of file diff --git a/systemvm/agent/noVNC/app/locale/it.json b/systemvm/agent/noVNC/app/locale/it.json new file mode 100644 index 00000000000..6fd25702b78 --- /dev/null +++ b/systemvm/agent/noVNC/app/locale/it.json @@ -0,0 +1,72 @@ +{ + "Connecting...": "Connessione in corso...", + "Disconnecting...": "Disconnessione...", + "Reconnecting...": "Riconnessione...", + "Internal error": "Errore interno", + "Must set host": "Devi impostare l'host", + "Connected (encrypted) to ": "Connesso (crittografato) a ", + "Connected (unencrypted) to ": "Connesso (non crittografato) a", + "Something went wrong, connection is closed": "Qualcosa è andato storto, la connessione è stata chiusa", + "Failed to connect to server": "Impossibile connettersi al server", + "Disconnected": "Disconnesso", + "New connection has been rejected with reason: ": "La nuova connessione è stata rifiutata con motivo: ", + "New connection has been rejected": "La nuova connessione è stata rifiutata", + "Credentials are required": "Le credenziali sono obbligatorie", + "noVNC encountered an error:": "noVNC ha riscontrato un errore:", + "Hide/Show the control bar": "Nascondi/Mostra la barra di controllo", + "Drag": "", + "Move/Drag Viewport": "", + "Keyboard": "Tastiera", + "Show Keyboard": "Mostra tastiera", + "Extra keys": "Tasti Aggiuntivi", + "Show Extra Keys": "Mostra Tasti Aggiuntivi", + "Ctrl": "Ctrl", + "Toggle Ctrl": "Tieni premuto Ctrl", + "Alt": "Alt", + "Toggle Alt": "Tieni premuto Alt", + "Toggle Windows": "Tieni premuto Windows", + "Windows": "Windows", + "Send Tab": "Invia Tab", + "Tab": "Tab", + "Esc": "Esc", + "Send Escape": "Invia Esc", + "Ctrl+Alt+Del": "Ctrl+Alt+Canc", + "Send Ctrl-Alt-Del": "Invia Ctrl-Alt-Canc", + "Shutdown/Reboot": "Spegnimento/Riavvio", + "Shutdown/Reboot...": "Spegnimento/Riavvio...", + "Power": "Alimentazione", + "Shutdown": "Spegnimento", + "Reboot": "Riavvio", + "Reset": "Reset", + "Clipboard": "Clipboard", + "Clear": "Pulisci", + "Fullscreen": "Schermo intero", + "Settings": "Impostazioni", + "Shared Mode": "Modalità condivisa", + "View Only": "Sola Visualizzazione", + "Clip to Window": "", + "Scaling Mode:": "Modalità di ridimensionamento:", + "None": "Nessuna", + "Local Scaling": "Ridimensionamento Locale", + "Remote Resizing": "Ridimensionamento Remoto", + "Advanced": "Avanzate", + "Quality:": "Qualità:", + "Compression level:": "Livello Compressione:", + "Repeater ID:": "ID Ripetitore:", + "WebSocket": "WebSocket", + "Encrypt": "Crittografa", + "Host:": "Host:", + "Port:": "Porta:", + "Path:": "Percorso:", + "Automatic Reconnect": "Riconnessione Automatica", + "Reconnect Delay (ms):": "Ritardo Riconnessione (ms):", + "Show Dot when No Cursor": "Mostra Punto quando Nessun Cursore", + "Logging:": "", + "Version:": "Versione:", + "Disconnect": "Disconnetti", + "Connect": "Connetti", + "Username:": "Utente:", + "Password:": "Password:", + "Send Credentials": "Invia Credenziale", + "Cancel": "Annulla" +} \ No newline at end of file diff --git a/systemvm/agent/noVNC/app/locale/ja.json b/systemvm/agent/noVNC/app/locale/ja.json index e5fe3401fcb..43fc5bf38c6 100644 --- a/systemvm/agent/noVNC/app/locale/ja.json +++ b/systemvm/agent/noVNC/app/locale/ja.json @@ -6,21 +6,16 @@ "Must set host": "ホストを設定する必要があります", "Connected (encrypted) to ": "接続しました (暗号化済み): ", "Connected (unencrypted) to ": "接続しました (暗号化されていません): ", - "Something went wrong, connection is closed": "何かが問題で、接続が閉じられました", + "Something went wrong, connection is closed": "何らかの問題で、接続が閉じられました", "Failed to connect to server": "サーバーへの接続に失敗しました", "Disconnected": "切断しました", "New connection has been rejected with reason: ": "新規接続は次の理由で拒否されました: ", "New connection has been rejected": "新規接続は拒否されました", - "Password is required": "パスワードが必要です", + "Credentials are required": "資格情報が必要です", "noVNC encountered an error:": "noVNC でエラーが発生しました:", "Hide/Show the control bar": "コントロールバーを隠す/表示する", + "Drag": "ドラッグ", "Move/Drag Viewport": "ビューポートを移動/ドラッグ", - "viewport drag": "ビューポートをドラッグ", - "Active Mouse Button": "アクティブなマウスボタン", - "No mousebutton": "マウスボタンなし", - "Left mousebutton": "左マウスボタン", - "Middle mousebutton": "中マウスボタン", - "Right mousebutton": "右マウスボタン", "Keyboard": "キーボード", "Show Keyboard": "キーボードを表示", "Extra keys": "追加キー", @@ -55,6 +50,8 @@ "Local Scaling": "ローカルスケーリング", "Remote Resizing": "リモートでリサイズ", "Advanced": "高度", + "Quality:": "品質:", + "Compression level:": "圧縮レベル:", "Repeater ID:": "リピーター ID:", "WebSocket": "WebSocket", "Encrypt": "暗号化", @@ -65,9 +62,11 @@ "Reconnect Delay (ms):": "再接続する遅延 (ミリ秒):", "Show Dot when No Cursor": "カーソルがないときにドットを表示", "Logging:": "ロギング:", + "Version:": "バージョン:", "Disconnect": "切断", "Connect": "接続", + "Username:": "ユーザー名:", "Password:": "パスワード:", - "Send Password": "パスワードを送信", + "Send Credentials": "資格情報を送信", "Cancel": "キャンセル" } \ No newline at end of file diff --git a/systemvm/agent/noVNC/app/locale/pt_BR.json b/systemvm/agent/noVNC/app/locale/pt_BR.json new file mode 100644 index 00000000000..aa130f764bf --- /dev/null +++ b/systemvm/agent/noVNC/app/locale/pt_BR.json @@ -0,0 +1,72 @@ +{ + "Connecting...": "Conectando...", + "Disconnecting...": "Desconectando...", + "Reconnecting...": "Reconectando...", + "Internal error": "Erro interno", + "Must set host": "É necessário definir o host", + "Connected (encrypted) to ": "Conectado (com criptografia) a ", + "Connected (unencrypted) to ": "Conectado (sem criptografia) a ", + "Something went wrong, connection is closed": "Algo deu errado. A conexão foi encerrada.", + "Failed to connect to server": "Falha ao conectar-se ao servidor", + "Disconnected": "Desconectado", + "New connection has been rejected with reason: ": "A nova conexão foi rejeitada pelo motivo: ", + "New connection has been rejected": "A nova conexão foi rejeitada", + "Credentials are required": "Credenciais são obrigatórias", + "noVNC encountered an error:": "O noVNC encontrou um erro:", + "Hide/Show the control bar": "Esconder/mostrar a barra de controles", + "Drag": "Arrastar", + "Move/Drag Viewport": "Mover/arrastar a janela", + "Keyboard": "Teclado", + "Show Keyboard": "Mostrar teclado", + "Extra keys": "Teclas adicionais", + "Show Extra Keys": "Mostar teclas adicionais", + "Ctrl": "Ctrl", + "Toggle Ctrl": "Pressionar/soltar Ctrl", + "Alt": "Alt", + "Toggle Alt": "Pressionar/soltar Alt", + "Toggle Windows": "Pressionar/soltar Windows", + "Windows": "Windows", + "Send Tab": "Enviar Tab", + "Tab": "Tab", + "Esc": "Esc", + "Send Escape": "Enviar Esc", + "Ctrl+Alt+Del": "Ctrl+Alt+Del", + "Send Ctrl-Alt-Del": "Enviar Ctrl-Alt-Del", + "Shutdown/Reboot": "Desligar/reiniciar", + "Shutdown/Reboot...": "Desligar/reiniciar...", + "Power": "Ligar", + "Shutdown": "Desligar", + "Reboot": "Reiniciar", + "Reset": "Reiniciar (forçado)", + "Clipboard": "Área de transferência", + "Clear": "Limpar", + "Fullscreen": "Tela cheia", + "Settings": "Configurações", + "Shared Mode": "Modo compartilhado", + "View Only": "Apenas visualizar", + "Clip to Window": "Recortar à janela", + "Scaling Mode:": "Modo de dimensionamento:", + "None": "Nenhum", + "Local Scaling": "Local", + "Remote Resizing": "Remoto", + "Advanced": "Avançado", + "Quality:": "Qualidade:", + "Compression level:": "Nível de compressão:", + "Repeater ID:": "ID do repetidor:", + "WebSocket": "WebSocket", + "Encrypt": "Criptografar", + "Host:": "Host:", + "Port:": "Porta:", + "Path:": "Caminho:", + "Automatic Reconnect": "Reconexão automática", + "Reconnect Delay (ms):": "Atraso da reconexão (ms)", + "Show Dot when No Cursor": "Mostrar ponto quando não há cursor", + "Logging:": "Registros:", + "Version:": "Versão:", + "Disconnect": "Desconectar", + "Connect": "Conectar", + "Username:": "Nome de usuário:", + "Password:": "Senha:", + "Send Credentials": "Enviar credenciais", + "Cancel": "Cancelar" +} \ No newline at end of file diff --git a/systemvm/agent/noVNC/app/locale/ru.json b/systemvm/agent/noVNC/app/locale/ru.json index 52e57f37f1b..cab97396ec2 100644 --- a/systemvm/agent/noVNC/app/locale/ru.json +++ b/systemvm/agent/noVNC/app/locale/ru.json @@ -9,26 +9,21 @@ "Something went wrong, connection is closed": "Что-то пошло не так, подключение разорвано", "Failed to connect to server": "Ошибка подключения к серверу", "Disconnected": "Отключено", - "New connection has been rejected with reason: ": "Подключиться не удалось: ", - "New connection has been rejected": "Подключиться не удалось", - "Password is required": "Требуется пароль", + "New connection has been rejected with reason: ": "Новое соединение отклонено по причине: ", + "New connection has been rejected": "Новое соединение отклонено", + "Credentials are required": "Требуются учетные данные", "noVNC encountered an error:": "Ошибка noVNC: ", "Hide/Show the control bar": "Скрыть/Показать контрольную панель", + "Drag": "Переместить", "Move/Drag Viewport": "Переместить окно", - "viewport drag": "Переместить окно", - "Active Mouse Button": "Активировать кнопки мыши", - "No mousebutton": "Отключить кнопки мыши", - "Left mousebutton": "Левая кнопка мыши", - "Middle mousebutton": "Средняя кнопка мыши", - "Right mousebutton": "Правая кнопка мыши", "Keyboard": "Клавиатура", "Show Keyboard": "Показать клавиатуру", - "Extra keys": "Доп. кнопки", - "Show Extra Keys": "Показать дополнительные кнопки", + "Extra keys": "Дополнительные Кнопки", + "Show Extra Keys": "Показать Дополнительные Кнопки", "Ctrl": "Ctrl", - "Toggle Ctrl": "Передать нажатие Ctrl", + "Toggle Ctrl": "Переключение нажатия Ctrl", "Alt": "Alt", - "Toggle Alt": "Передать нажатие Alt", + "Toggle Alt": "Переключение нажатия Alt", "Toggle Windows": "Переключение вкладок", "Windows": "Вкладка", "Send Tab": "Передать нажатие Tab", @@ -48,13 +43,15 @@ "Fullscreen": "Во весь экран", "Settings": "Настройки", "Shared Mode": "Общий режим", - "View Only": "Просмотр", + "View Only": "Только Просмотр", "Clip to Window": "В окно", "Scaling Mode:": "Масштаб:", "None": "Нет", "Local Scaling": "Локльный масштаб", - "Remote Resizing": "Удаленный масштаб", + "Remote Resizing": "Удаленная перенастройка размера", "Advanced": "Дополнительно", + "Quality:": "Качество", + "Compression level:": "Уровень Сжатия", "Repeater ID:": "Идентификатор ID:", "WebSocket": "WebSocket", "Encrypt": "Шифрование", @@ -65,9 +62,11 @@ "Reconnect Delay (ms):": "Задержка переподключения (мс):", "Show Dot when No Cursor": "Показать точку вместо курсора", "Logging:": "Лог:", + "Version:": "Версия", "Disconnect": "Отключение", "Connect": "Подключение", + "Username:": "Имя Пользователя", "Password:": "Пароль:", - "Send Password": "Пароль: ", + "Send Credentials": "Передача Учетных Данных", "Cancel": "Выход" } \ No newline at end of file diff --git a/systemvm/agent/noVNC/app/locale/sv.json b/systemvm/agent/noVNC/app/locale/sv.json index e46df45b585..077ef42c80b 100644 --- a/systemvm/agent/noVNC/app/locale/sv.json +++ b/systemvm/agent/noVNC/app/locale/sv.json @@ -1,4 +1,5 @@ { + "HTTPS is required for full functionality": "HTTPS krävs för full funktionalitet", "Connecting...": "Ansluter...", "Disconnecting...": "Kopplar ner...", "Reconnecting...": "Återansluter...", @@ -39,8 +40,8 @@ "Reboot": "Boota om", "Reset": "Återställ", "Clipboard": "Urklipp", - "Clear": "Rensa", - "Fullscreen": "Fullskärm", + "Edit clipboard content in the textarea below.": "Redigera urklippets innehåll i fältet nedan.", + "Full Screen": "Fullskärm", "Settings": "Inställningar", "Shared Mode": "Delat Läge", "View Only": "Endast Visning", @@ -65,6 +66,13 @@ "Version:": "Version:", "Disconnect": "Koppla från", "Connect": "Anslut", + "Server identity": "Server-identitet", + "The server has provided the following identifying information:": "Servern har gett följande identifierande information:", + "Fingerprint:": "Fingeravtryck:", + "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Kontrollera att informationen är korrekt och tryck sedan \"Godkänn\". Tryck annars \"Neka\".", + "Approve": "Godkänn", + "Reject": "Neka", + "Credentials": "Användaruppgifter", "Username:": "Användarnamn:", "Password:": "Lösenord:", "Send Credentials": "Skicka Användaruppgifter", diff --git a/systemvm/agent/noVNC/app/localization.js b/systemvm/agent/noVNC/app/localization.js index 100901c9d26..84341da6f9c 100644 --- a/systemvm/agent/noVNC/app/localization.js +++ b/systemvm/agent/noVNC/app/localization.js @@ -103,13 +103,20 @@ export class Localizer { return items.indexOf(searchElement) !== -1; } + function translateString(str) { + // We assume surrounding whitespace, and whitespace around line + // breaks is just for source formatting + str = str.split("\n").map(s => s.trim()).join(" ").trim(); + return self.get(str); + } + function translateAttribute(elem, attr) { - const str = self.get(elem.getAttribute(attr)); + const str = translateString(elem.getAttribute(attr)); elem.setAttribute(attr, str); } function translateTextNode(node) { - const str = self.get(node.data.trim()); + const str = translateString(node.data); node.data = str; } diff --git a/systemvm/agent/noVNC/app/styles/base.css b/systemvm/agent/noVNC/app/styles/base.css index ca236a9e112..86411c63df9 100644 --- a/systemvm/agent/noVNC/app/styles/base.css +++ b/systemvm/agent/noVNC/app/styles/base.css @@ -19,10 +19,23 @@ * 10000: Max (used for polyfills) */ +/* + * State variables (set on :root): + * + * noVNC_loading: Page is still loading + * noVNC_connecting: Connecting to server + * noVNC_reconnecting: Re-establishing a connection + * noVNC_connected: Connected to server (most common state) + * noVNC_disconnecting: Disconnecting from server + */ + +:root { + font-family: sans-serif; +} + body { margin:0; padding:0; - font-family: Helvetica; /*Background image with light grey curve.*/ background-color:#494949; background-repeat:no-repeat; @@ -78,144 +91,6 @@ html { 50% { box-shadow: 60px 10px 0 rgba(255, 255, 255, 0); width: 10px; } } -/* ---------------------------------------- - * Input Elements - * ---------------------------------------- - */ - -input:not([type]), -input[type=date], -input[type=datetime-local], -input[type=email], -input[type=month], -input[type=number], -input[type=password], -input[type=search], -input[type=tel], -input[type=text], -input[type=time], -input[type=url], -input[type=week], -textarea { - /* Disable default rendering */ - -webkit-appearance: none; - -moz-appearance: none; - background: none; - - margin: 2px; - padding: 2px; - border: 1px solid rgb(192, 192, 192); - border-radius: 5px; - color: black; - background: linear-gradient(to top, rgb(255, 255, 255) 80%, rgb(240, 240, 240)); -} - -input[type=button], -input[type=color], -input[type=reset], -input[type=submit], -select { - /* Disable default rendering */ - -webkit-appearance: none; - -moz-appearance: none; - background: none; - - margin: 2px; - padding: 2px; - border: 1px solid rgb(192, 192, 192); - border-bottom-width: 2px; - border-radius: 5px; - color: black; - background: linear-gradient(to top, rgb(255, 255, 255), rgb(240, 240, 240)); - - /* This avoids it jumping around when :active */ - vertical-align: middle; -} - -input[type=button], -input[type=color], -input[type=reset], -input[type=submit] { - padding-left: 20px; - padding-right: 20px; -} - -option { - color: black; - background: white; -} - -input:not([type]):focus, -input[type=button]:focus, -input[type=color]:focus, -input[type=date]:focus, -input[type=datetime-local]:focus, -input[type=email]:focus, -input[type=month]:focus, -input[type=number]:focus, -input[type=password]:focus, -input[type=reset]:focus, -input[type=search]:focus, -input[type=submit]:focus, -input[type=tel]:focus, -input[type=text]:focus, -input[type=time]:focus, -input[type=url]:focus, -input[type=week]:focus, -select:focus, -textarea:focus { - box-shadow: 0px 0px 3px rgba(74, 144, 217, 0.5); - border-color: rgb(74, 144, 217); - outline: none; -} - -input[type=button]::-moz-focus-inner, -input[type=color]::-moz-focus-inner, -input[type=reset]::-moz-focus-inner, -input[type=submit]::-moz-focus-inner { - border: none; -} - -input:not([type]):disabled, -input[type=button]:disabled, -input[type=color]:disabled, -input[type=date]:disabled, -input[type=datetime-local]:disabled, -input[type=email]:disabled, -input[type=month]:disabled, -input[type=number]:disabled, -input[type=password]:disabled, -input[type=reset]:disabled, -input[type=search]:disabled, -input[type=submit]:disabled, -input[type=tel]:disabled, -input[type=text]:disabled, -input[type=time]:disabled, -input[type=url]:disabled, -input[type=week]:disabled, -select:disabled, -textarea:disabled { - color: rgb(128, 128, 128); - background: rgb(240, 240, 240); -} - -input[type=button]:active, -input[type=color]:active, -input[type=reset]:active, -input[type=submit]:active, -select:active { - border-bottom-width: 1px; - margin-top: 3px; -} - -:root:not(.noVNC_touch) input[type=button]:hover:not(:disabled), -:root:not(.noVNC_touch) input[type=color]:hover:not(:disabled), -:root:not(.noVNC_touch) input[type=reset]:hover:not(:disabled), -:root:not(.noVNC_touch) input[type=submit]:hover:not(:disabled), -:root:not(.noVNC_touch) select:hover:not(:disabled) { - background: linear-gradient(to top, rgb(255, 255, 255), rgb(250, 250, 250)); -} - /* ---------------------------------------- * WebKit centering hacks * ---------------------------------------- @@ -242,13 +117,15 @@ select:active { pointer-events: auto; } .noVNC_vcenter { - display: flex; + display: flex !important; flex-direction: column; justify-content: center; position: fixed; top: 0; left: 0; height: 100%; + margin: 0 !important; + padding: 0 !important; pointer-events: none; } .noVNC_vcenter > * { @@ -272,13 +149,20 @@ select:active { #noVNC_fallback_error { z-index: 1000; visibility: hidden; + /* Put a dark background in front of everything but the error, + and don't let mouse events pass through */ + background: rgba(0, 0, 0, 0.8); + pointer-events: all; } #noVNC_fallback_error.noVNC_open { visibility: visible; } #noVNC_fallback_error > div { - max-width: 90%; + max-width: calc(100vw - 30px - 30px); + max-height: calc(100vh - 30px - 30px); + overflow: auto; + padding: 15px; transition: 0.5s ease-in-out; @@ -317,7 +201,6 @@ select:active { } #noVNC_fallback_error .noVNC_stack { - max-height: 50vh; padding: 10px; margin: 10px; font-size: 0.8em; @@ -361,6 +244,9 @@ select:active { background-color: rgb(110, 132, 163); border-radius: 0 10px 10px 0; + user-select: none; + -webkit-user-select: none; + -webkit-touch-callout: none; /* Disable iOS image long-press popup */ } #noVNC_control_bar.noVNC_open { box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); @@ -401,7 +287,7 @@ select:active { cursor: pointer; border-radius: 5px; background-color: rgb(83, 99, 122); - background-image: url("../images/handle_bg.svg"); + background-image: url("../images/handle_bg.png"); background-repeat: no-repeat; background-position: right; box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.5); @@ -409,7 +295,7 @@ select:active { #noVNC_control_bar_handle:after { content: ""; transition: transform 0.5s ease-in-out; - background: url("../images/handle.svg"); + background: url("../images/handle.png"); position: absolute; top: 22px; /* (50px-6px)/2 */ right: 5px; @@ -433,38 +319,50 @@ select:active { .noVNC_right #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after { transform: none; } +/* Larger touch area for the handle, used when a touch screen is available */ #noVNC_control_bar_handle div { position: absolute; right: -35px; top: 0; width: 50px; - height: 50px; -} -:root:not(.noVNC_touch) #noVNC_control_bar_handle div { + height: 100%; display: none; } +@media (any-pointer: coarse) { + #noVNC_control_bar_handle div { + display: initial; + } +} .noVNC_right #noVNC_control_bar_handle div { left: -35px; right: auto; } -#noVNC_control_bar .noVNC_scroll { +#noVNC_control_bar > .noVNC_scroll { max-height: 100vh; /* Chrome is buggy with 100% */ overflow-x: hidden; overflow-y: auto; - padding: 0 10px 0 5px; + padding: 0 10px; } -.noVNC_right #noVNC_control_bar .noVNC_scroll { - padding: 0 5px 0 10px; + +#noVNC_control_bar > .noVNC_scroll > * { + display: block; + margin: 10px auto; } /* Control bar hint */ -#noVNC_control_bar_hint { +#noVNC_hint_anchor { position: fixed; - left: calc(100vw - 50px); + right: -50px; + left: auto; +} +#noVNC_control_bar_anchor.noVNC_right + #noVNC_hint_anchor { + left: -50px; right: auto; - top: 50%; - transform: translateY(-50%) scale(0); +} +#noVNC_control_bar_hint { + position: relative; + transform: scale(0); width: 100px; height: 50%; max-height: 600px; @@ -477,61 +375,65 @@ select:active { border-radius: 10px; transition-delay: 0s; } -#noVNC_control_bar_anchor.noVNC_right #noVNC_control_bar_hint{ - left: auto; - right: calc(100vw - 50px); -} #noVNC_control_bar_hint.noVNC_active { visibility: visible; opacity: 1; transition-delay: 0.2s; - transform: translateY(-50%) scale(1); + transform: scale(1); +} +#noVNC_control_bar_hint.noVNC_notransition { + transition: none !important; } -/* General button style */ -.noVNC_button { - display: block; +/* Control bar buttons */ +#noVNC_control_bar .noVNC_button { padding: 4px 4px; - margin: 10px 0; vertical-align: middle; border:1px solid rgba(255, 255, 255, 0.2); border-radius: 6px; + background-color: transparent; + background-image: unset; /* we don't want the gradiant from input.css */ } -.noVNC_button.noVNC_selected { +#noVNC_control_bar .noVNC_button.noVNC_selected { border-color: rgba(0, 0, 0, 0.8); - background: rgba(0, 0, 0, 0.5); + background-color: rgba(0, 0, 0, 0.5); } -.noVNC_button:disabled { - opacity: 0.4; +#noVNC_control_bar .noVNC_button.noVNC_selected:not(:disabled):hover { + border-color: rgba(0, 0, 0, 0.4); + background-color: rgba(0, 0, 0, 0.2); } -.noVNC_button:focus { - outline: none; +#noVNC_control_bar .noVNC_button:not(:disabled):hover { + background-color: rgba(255, 255, 255, 0.2); } -.noVNC_button:active { +#noVNC_control_bar .noVNC_button:not(:disabled):active { padding-top: 5px; padding-bottom: 3px; } -/* Android browsers don't properly update hover state if touch events - * are intercepted, but focus should be safe to display */ -:root:not(.noVNC_touch) .noVNC_button.noVNC_selected:hover, -.noVNC_button.noVNC_selected:focus { - border-color: rgba(0, 0, 0, 0.4); - background: rgba(0, 0, 0, 0.2); +#noVNC_control_bar .noVNC_button.noVNC_hidden { + display: none !important; } -:root:not(.noVNC_touch) .noVNC_button:hover, -.noVNC_button:focus { - background: rgba(255, 255, 255, 0.2); -} -.noVNC_button.noVNC_hidden { - display: none; + +/* Android browsers don't properly update hover state if touch events are + * intercepted, like they are when clicking on the remote screen. */ +@media (any-pointer: coarse) { + #noVNC_control_bar .noVNC_button:not(:disabled):hover { + background-color: transparent; + } + #noVNC_control_bar .noVNC_button.noVNC_selected:not(:disabled):hover { + border-color: rgba(0, 0, 0, 0.8); + background-color: rgba(0, 0, 0, 0.5); + } } + /* Panels */ .noVNC_panel { transform: translateX(25px); transition: 0.5s ease-in-out; + box-sizing: border-box; /* so max-width don't have to care about padding */ + max-width: calc(100vw - 75px - 25px); /* minus left and right margins */ max-height: 100vh; /* Chrome is buggy with 100% */ overflow-x: hidden; overflow-y: auto; @@ -563,6 +465,17 @@ select:active { transform: translateX(-75px); } +.noVNC_panel > * { + display: block; + margin: 10px auto; +} +.noVNC_panel > *:first-child { + margin-top: 0 !important; +} +.noVNC_panel > *:last-child { + margin-bottom: 0 !important; +} + .noVNC_panel hr { border: none; border-top: 1px solid rgb(192, 192, 192); @@ -571,6 +484,11 @@ select:active { .noVNC_panel label { display: block; white-space: nowrap; + margin: 5px; +} + +.noVNC_panel li { + margin: 5px; } .noVNC_panel .noVNC_heading { @@ -581,7 +499,6 @@ select:active { padding-right: 8px; color: white; font-size: 20px; - margin-bottom: 10px; white-space: nowrap; } .noVNC_panel .noVNC_heading img { @@ -597,7 +514,7 @@ select:active { cursor: pointer; } .noVNC_expander::before { - content: url("../images/expander.svg"); + content: url("../images/expander.png"); display: inline-block; margin-right: 5px; transition: 0.2s ease-in-out; @@ -622,6 +539,12 @@ select:active { font-size: 13px; } +.noVNC_logo + hr { + /* Remove all but top border */ + border: none; + border-top: 1px solid rgba(255, 255, 255, 0.2); +} + :root:not(.noVNC_connected) #noVNC_view_drag_button { display: none; } @@ -630,8 +553,15 @@ select:active { :root:not(.noVNC_connected) #noVNC_mobile_buttons { display: none; } -:root:not(.noVNC_touch) #noVNC_mobile_buttons { - display: none; +@media not all and (any-pointer: coarse) { + /* FIXME: The button for the virtual keyboard is the only button in this + group of "mobile buttons". It is bad to assume that no touch + devices have physical keyboards available. Hopefully we can get + a media query for this: + https://github.com/w3c/csswg-drafts/issues/3871 */ + :root.noVNC_connected #noVNC_mobile_buttons { + display: none; + } } /* Extra manual keys */ @@ -642,7 +572,7 @@ select:active { #noVNC_modifiers { background-color: rgb(92, 92, 92); border: none; - padding: 0 10px; + padding: 10px; } /* Shutdown/Reboot */ @@ -663,13 +593,16 @@ select:active { :root:not(.noVNC_connected) #noVNC_clipboard_button { display: none; } -#noVNC_clipboard { - /* Full screen, minus padding and left and right margins */ - max-width: calc(100vw - 2*15px - 75px - 25px); -} #noVNC_clipboard_text { - width: 500px; + width: 360px; + min-width: 150px; + height: 160px; + min-height: 70px; + + box-sizing: border-box; max-width: 100%; + /* minus approximate height of title, height of subtitle, and margin */ + max-height: calc(100vh - 10em - 25px); } :root:not(.noVNC_connected) #noVNC_fullscreen_button { @@ -681,7 +614,6 @@ select:active { } #noVNC_settings ul { list-style: none; - margin: 0px; padding: 0px; } #noVNC_setting_port { @@ -757,25 +689,25 @@ select:active { background: rgba(128,128,128,0.9); } #noVNC_status.noVNC_status_normal::before { - content: url("../images/info.svg") " "; + content: url("../images/info.png") " "; } #noVNC_status.noVNC_status_error { background: rgba(200,55,55,0.9); } #noVNC_status.noVNC_status_error::before { - content: url("../images/error.svg") " "; + content: url("../images/error.png") " "; } #noVNC_status.noVNC_status_warn { background: rgba(180,180,30,0.9); } #noVNC_status.noVNC_status_warn::before { - content: url("../images/warning.svg") " "; + content: url("../images/warning.png") " "; } #noVNC_status.noVNC_status_tls_success { background: rgba(6, 199, 38, 0.9); } #noVNC_status.noVNC_status_tls_success::before { - content: url("../images/connect.svg") " "; + content: url("../images/connect.png") " "; } /* ---------------------------------------- @@ -813,36 +745,32 @@ select:active { font-size: calc(25vw - 30px); } } -#noVNC_connect_button { - cursor: pointer; +#noVNC_connect_dlg div { + padding: 12px; - padding: 10px; - - color: white; background-color: rgb(110, 132, 163); border-radius: 12px; - text-align: center; font-size: 20px; box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); } -#noVNC_connect_button div { - margin: 2px; +#noVNC_connect_button { + width: 100%; padding: 5px 30px; - border: 1px solid rgb(83, 99, 122); - border-bottom-width: 2px; + + cursor: pointer; + + border-color: rgb(83, 99, 122); border-radius: 5px; + background: linear-gradient(to top, rgb(110, 132, 163), rgb(99, 119, 147)); + color: white; /* This avoids it jumping around when :active */ vertical-align: middle; } -#noVNC_connect_button div:active { - border-bottom-width: 1px; - margin-top: 3px; -} -:root:not(.noVNC_touch) #noVNC_connect_button div:hover { +#noVNC_connect_button:hover { background: linear-gradient(to top, rgb(110, 132, 163), rgb(105, 125, 155)); } @@ -851,6 +779,23 @@ select:active { height: 1.3em; } +/* ---------------------------------------- + * Server verification Dialog + * ---------------------------------------- + */ + +#noVNC_verify_server_dlg { + position: relative; + + transform: translateY(-50px); +} +#noVNC_verify_server_dlg.noVNC_open { + transform: translateY(0); +} +#noVNC_fingerprint_block { + margin: 10px; +} + /* ---------------------------------------- * Password Dialog * ---------------------------------------- @@ -864,12 +809,8 @@ select:active { #noVNC_credentials_dlg.noVNC_open { transform: translateY(0); } -#noVNC_credentials_dlg ul { - list-style: none; - margin: 0px; - padding: 0px; -} -.noVNC_hidden { +#noVNC_username_block.noVNC_hidden, +#noVNC_password_block.noVNC_hidden { display: none; } @@ -881,7 +822,11 @@ select:active { /* Transition screen */ #noVNC_transition { - display: none; + transition: 0.5s ease-in-out; + + display: flex; + opacity: 0; + visibility: hidden; position: fixed; top: 0; @@ -902,7 +847,8 @@ select:active { :root.noVNC_connecting #noVNC_transition, :root.noVNC_disconnecting #noVNC_transition, :root.noVNC_reconnecting #noVNC_transition { - display: flex; + opacity: 1; + visibility: visible; } :root:not(.noVNC_reconnecting) #noVNC_cancel_reconnect_button { display: none; @@ -918,6 +864,12 @@ select:active { background-color: #313131; border-bottom-right-radius: 800px 600px; /*border-top-left-radius: 800px 600px;*/ + + /* If selection isn't disabled, long-pressing stuff in the sidebar + can accidentally select the container or the canvas. This can + happen when attempting to move the handle. */ + user-select: none; + -webkit-user-select: none; } #noVNC_keyboardinput { diff --git a/systemvm/agent/noVNC/app/styles/input.css b/systemvm/agent/noVNC/app/styles/input.css new file mode 100644 index 00000000000..eaf083c7e17 --- /dev/null +++ b/systemvm/agent/noVNC/app/styles/input.css @@ -0,0 +1,281 @@ +/* + * noVNC general input element CSS + * Copyright (C) 2022 The noVNC Authors + * noVNC is licensed under the MPL 2.0 (see LICENSE.txt) + * This file is licensed under the 2-Clause BSD license (see LICENSE.txt). + */ + +/* + * Common for all inputs + */ +input, input::file-selector-button, button, select, textarea { + /* Respect standard font settings */ + font: inherit; + + /* Disable default rendering */ + appearance: none; + background: none; + + padding: 5px; + border: 1px solid rgb(192, 192, 192); + border-radius: 5px; + color: black; + --bg-gradient: linear-gradient(to top, rgb(255, 255, 255) 80%, rgb(240, 240, 240)); + background-image: var(--bg-gradient); +} + +/* + * Buttons + */ +input[type=button], +input[type=color], +input[type=image], +input[type=reset], +input[type=submit], +input::file-selector-button, +button, +select { + border-bottom-width: 2px; + + /* This avoids it jumping around when :active */ + vertical-align: middle; + margin-top: 0; + + padding-left: 20px; + padding-right: 20px; + + /* Disable Chrome's touch tap highlight */ + -webkit-tap-highlight-color: transparent; +} + +/* + * Select dropdowns + */ +select { + --select-arrow: url('data:image/svg+xml;utf8, \ + \ + \ + '); + background-image: var(--select-arrow), var(--bg-gradient); + background-position: calc(100% - 7px), left top; + background-repeat: no-repeat; + padding-right: calc(2*7px + 8px); + padding-left: 7px; +} +/* FIXME: :active isn't set when the is opened in Firefox: + https://bugzilla.mozilla.org/show_bug.cgi?id=1805406 */ +select:active { + /* Rotated arrow */ + background-image: url('data:image/svg+xml;utf8, \ + \ + \ + '), var(--bg-gradient); +} +option { + color: black; + background: white; +} + +/* + * Checkboxes + */ +input[type=checkbox] { + background-color: white; + background-image: unset; + border: 1px solid dimgrey; + border-radius: 3px; + width: 13px; + height: 13px; + padding: 0; + margin-right: 6px; + vertical-align: bottom; + transition: 0.2s background-color linear; +} +input[type=checkbox]:checked { + background-color: rgb(110, 132, 163); + border-color: rgb(110, 132, 163); +} +input[type=checkbox]:checked::after { + content: ""; + display: block; /* width & height doesn't work on inline elements */ + position: relative; + top: 0; + left: 3px; + width: 3px; + height: 7px; + border: 1px solid white; + border-width: 0 2px 2px 0; + transform: rotate(40deg); +} + +/* + * Radiobuttons + */ +input[type=radio] { + border-radius: 50%; + border: 1px solid dimgrey; + width: 12px; + height: 12px; + padding: 0; + margin-right: 6px; + transition: 0.2s border linear; +} +input[type=radio]:checked { + border: 6px solid rgb(110, 132, 163); +} + +/* + * Range sliders + */ +input[type=range] { + border: unset; + border-radius: 3px; + height: 20px; + padding: 0; + background: transparent; +} +/* -webkit-slider.. & -moz-range.. cant be in selector lists: + https://bugs.chromium.org/p/chromium/issues/detail?id=1154623 */ +input[type=range]::-webkit-slider-runnable-track { + background-color: rgb(110, 132, 163); + height: 6px; + border-radius: 3px; +} +input[type=range]::-moz-range-track { + background-color: rgb(110, 132, 163); + height: 6px; + border-radius: 3px; +} +input[type=range]::-webkit-slider-thumb { + appearance: none; + width: 18px; + height: 20px; + border-radius: 5px; + background-color: white; + border: 1px solid dimgray; + margin-top: -7px; +} +input[type=range]::-moz-range-thumb { + appearance: none; + width: 18px; + height: 20px; + border-radius: 5px; + background-color: white; + border: 1px solid dimgray; + margin-top: -7px; +} + +/* + * File choosers + */ +input[type=file] { + background-image: none; + border: none; +} +input::file-selector-button { + margin-right: 6px; +} + +/* + * Hover + */ +input[type=button]:hover, +input[type=color]:hover, +input[type=image]:hover, +input[type=reset]:hover, +input[type=submit]:hover, +input::file-selector-button:hover, +button:hover { + background-image: linear-gradient(to top, rgb(255, 255, 255), rgb(250, 250, 250)); +} +select:hover { + background-image: var(--select-arrow), + linear-gradient(to top, rgb(255, 255, 255), rgb(250, 250, 250)); + background-position: calc(100% - 7px), left top; + background-repeat: no-repeat; +} +@media (any-pointer: coarse) { + /* We don't want a hover style after touch input */ + input[type=button]:hover, + input[type=color]:hover, + input[type=image]:hover, + input[type=reset]:hover, + input[type=submit]:hover, + input::file-selector-button:hover, + button:hover { + background-image: var(--bg-gradient); + } + select:hover { + background-image: var(--select-arrow), var(--bg-gradient); + } +} + +/* + * Active (clicked) + */ +input[type=button]:active, +input[type=color]:active, +input[type=image]:active, +input[type=reset]:active, +input[type=submit]:active, +input::file-selector-button:active, +button:active, +select:active { + border-bottom-width: 1px; + margin-top: 1px; +} + +/* + * Focus (tab) + */ +input:focus-visible, +input:focus-visible::file-selector-button, +button:focus-visible, +select:focus-visible, +textarea:focus-visible { + outline: 2px solid rgb(74, 144, 217); + outline-offset: 1px; +} +input[type=file]:focus-visible { + outline: none; /* We outline the button instead of the entire element */ +} + +/* + * Disabled + */ +input:disabled, +input:disabled::file-selector-button, +button:disabled, +select:disabled, +textarea:disabled { + opacity: 0.4; +} +input[type=button]:disabled, +input[type=color]:disabled, +input[type=image]:disabled, +input[type=reset]:disabled, +input[type=submit]:disabled, +input:disabled::file-selector-button, +button:disabled, +select:disabled { + background-image: var(--bg-gradient); + border-bottom-width: 2px; + margin-top: 0; +} +input[type=file]:disabled { + background-image: none; +} +select:disabled { + background-image: var(--select-arrow), var(--bg-gradient); +} +input[type=image]:disabled { + /* See Firefox bug: + https://bugzilla.mozilla.org/show_bug.cgi?id=1798304 */ + cursor: default; +} diff --git a/systemvm/agent/noVNC/app/ui.js b/systemvm/agent/noVNC/app/ui.js index 05026acedb3..ee738c20125 100644 --- a/systemvm/agent/noVNC/app/ui.js +++ b/systemvm/agent/noVNC/app/ui.js @@ -8,7 +8,8 @@ import * as Log from '../core/util/logging.js'; import _, { l10n } from './localization.js'; -import { isTouchDevice, isSafari, hasScrollbarGutter, dragThreshold } +import { isTouchDevice, isMac, isIOS, isAndroid, isChromeOS, isSafari, + hasScrollbarGutter, dragThreshold } from '../core/util/browser.js'; import { setCapture, getPointerEvent } from '../core/util/events.js'; import KeyTable from "../core/input/keysym.js"; @@ -63,7 +64,21 @@ const UI = { // Translate the DOM l10n.translateDOM(); - WebUtil.fetchJSON('./package.json') + // We rely on modern APIs which might not be available in an + // insecure context + if (!window.isSecureContext) { + // FIXME: This gets hidden when connecting + UI.showStatus(_("HTTPS is required for full functionality"), 'error'); + } + + // Try to fetch version number + fetch('./package.json') + .then((response) => { + if (!response.ok) { + throw Error("" + response.status + " " + response.statusText); + } + return response.json(); + }) .then((packageInfo) => { Array.from(document.getElementsByClassName('noVNC_version')).forEach(el => el.innerText = packageInfo.version); }) @@ -76,7 +91,6 @@ const UI = { // Adapt the interface for touch screen devices if (isTouchDevice) { - document.documentElement.classList.add("noVNC_touch"); // Remove the address bar setTimeout(() => window.scrollTo(0, 1), 100); } @@ -316,6 +330,10 @@ const UI = { document.getElementById("noVNC_cancel_reconnect_button") .addEventListener('click', UI.cancelReconnect); + document.getElementById("noVNC_approve_server_button") + .addEventListener('click', UI.approveServer); + document.getElementById("noVNC_reject_server_button") + .addEventListener('click', UI.rejectServer); document.getElementById("noVNC_credentials_button") .addEventListener('click', UI.setCredentials); }, @@ -445,6 +463,8 @@ const UI = { // State change closes dialogs as they may not be relevant // anymore UI.closeAllPanels(); + document.getElementById('noVNC_verify_server_dlg') + .classList.remove('noVNC_open'); document.getElementById('noVNC_credentials_dlg') .classList.remove('noVNC_open'); }, @@ -589,10 +609,20 @@ const UI = { // Consider this a movement of the handle UI.controlbarDrag = true; + + // The user has "followed" hint, let's hide it until the next drag + UI.showControlbarHint(false, false); }, - showControlbarHint(show) { + showControlbarHint(show, animate=true) { const hint = document.getElementById('noVNC_control_bar_hint'); + + if (animate) { + hint.classList.remove("noVNC_notransition"); + } else { + hint.classList.add("noVNC_notransition"); + } + if (show) { hint.classList.add("noVNC_active"); } else { @@ -773,11 +803,6 @@ const UI = { } } } else { - /*Weird IE9 error leads to 'null' appearring - in textboxes instead of ''.*/ - if (value === null) { - value = ""; - } ctrl.value = value; } }, @@ -1055,8 +1080,10 @@ const UI = { credentials: { password: password } }); UI.rfb.addEventListener("connect", UI.connectFinished); UI.rfb.addEventListener("disconnect", UI.disconnectFinished); + UI.rfb.addEventListener("serververification", UI.serverVerify); UI.rfb.addEventListener("credentialsrequired", UI.credentials); UI.rfb.addEventListener("securityfailure", UI.securityFailed); + UI.rfb.addEventListener("clippingviewport", UI.updateViewDrag); UI.rfb.addEventListener("capabilities", UI.updatePowerButton); UI.rfb.addEventListener("clipboard", UI.clipboardReceive); UI.rfb.addEventListener("bell", UI.bell); @@ -1142,7 +1169,9 @@ const UI = { } else { UI.showStatus(_("Failed to connect to server / access token has expired"), 'error'); } - } else if (UI.getSetting('reconnect', false) === true && !UI.inhibitReconnect) { + } + // If reconnecting is allowed process it now + if (UI.getSetting('reconnect', false) === true && !UI.inhibitReconnect) { UI.updateVisualState('reconnecting'); const delay = parseInt(UI.getSetting('reconnect_delay')); @@ -1176,6 +1205,37 @@ const UI = { /* ------^------- * /CONNECTION * ============== + * SERVER VERIFY + * ------v------*/ + + async serverVerify(e) { + const type = e.detail.type; + if (type === 'RSA') { + const publickey = e.detail.publickey; + let fingerprint = await window.crypto.subtle.digest("SHA-1", publickey); + // The same fingerprint format as RealVNC + fingerprint = Array.from(new Uint8Array(fingerprint).slice(0, 8)).map( + x => x.toString(16).padStart(2, '0')).join('-'); + document.getElementById('noVNC_verify_server_dlg').classList.add('noVNC_open'); + document.getElementById('noVNC_fingerprint').innerHTML = fingerprint; + } + }, + + approveServer(e) { + e.preventDefault(); + document.getElementById('noVNC_verify_server_dlg').classList.remove('noVNC_open'); + UI.rfb.approveServer(); + }, + + rejectServer(e) { + e.preventDefault(); + document.getElementById('noVNC_verify_server_dlg').classList.remove('noVNC_open'); + UI.disconnect(); + }, + +/* ------^------- + * /SERVER VERIFY + * ============== * PASSWORD * ------v------*/ @@ -1275,13 +1335,25 @@ const UI = { const scaling = UI.getSetting('resize') === 'scale'; + // Some platforms have overlay scrollbars that are difficult + // to use in our case, which means we have to force panning + // FIXME: Working scrollbars can still be annoying to use with + // touch, so we should ideally be able to have both + // panning and scrollbars at the same time + + let brokenScrollbars = false; + + if (!hasScrollbarGutter) { + if (isIOS() || isAndroid() || isMac() || isChromeOS()) { + brokenScrollbars = true; + } + } + if (scaling) { // Can't be clipping if viewport is scaled to fit UI.forceSetting('view_clip', false); UI.rfb.clipViewport = false; - } else if (!hasScrollbarGutter) { - // Some platforms have scrollbars that are difficult - // to use in our case, so we always use our own panning + } else if (brokenScrollbars) { UI.forceSetting('view_clip', true); UI.rfb.clipViewport = true; } else { @@ -1312,7 +1384,8 @@ const UI = { const viewDragButton = document.getElementById('noVNC_view_drag_button'); - if (!UI.rfb.clipViewport && UI.rfb.dragViewport) { + if ((!UI.rfb.clipViewport || !UI.rfb.clippingViewport) && + UI.rfb.dragViewport) { // We are no longer clipping the viewport. Make sure // viewport drag isn't active when it can't be used. UI.rfb.dragViewport = false; @@ -1329,6 +1402,8 @@ const UI = { } else { viewDragButton.classList.add("noVNC_hidden"); } + + viewDragButton.disabled = !UI.rfb.clippingViewport; }, /* ------^------- @@ -1713,12 +1788,18 @@ const UI = { }; // Set up translations -const LINGUAS = ["cs", "de", "el", "es", "ja", "ko", "nl", "pl", "ru", "sv", "tr", "zh_CN", "zh_TW"]; +const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "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') + fetch('app/locale/' + l10n.language + '.json') + .then((response) => { + if (!response.ok) { + throw Error("" + response.status + " " + response.statusText); + } + return response.json(); + }) .then((translations) => { l10n.dictionary = translations; }) .catch(err => Log.Error("Failed to load translations: " + err)) .then(UI.prime); diff --git a/systemvm/agent/noVNC/app/webutil.js b/systemvm/agent/noVNC/app/webutil.js index 568f0e24b4d..7ab5aa170a2 100644 --- a/systemvm/agent/noVNC/app/webutil.js +++ b/systemvm/agent/noVNC/app/webutil.js @@ -20,10 +20,19 @@ export function initLogging(level) { } // Read a query string variable +// A URL with a query parameter can look like this (But will most probably get logged on the http server): +// https://www.example.com?myqueryparam=myvalue +// +// For privacy (Using a hastag #, the parameters will not be sent to the server) +// the url can be requested in the following way: +// https://www.example.com#myqueryparam=myvalue&password=secreatvalue +// +// Even Mixing public and non public parameters will work: +// https://www.example.com?nonsecretparam=example.com#password=secreatvalue export function getQueryVar(name, defVal) { "use strict"; const re = new RegExp('.*[?&]' + name + '=([^]*)'), - match = document.location.href.match(re); + match = ''.concat(document.location.href, window.location.hash).match(re); if (typeof defVal === 'undefined') { defVal = null; } if (match) { @@ -37,7 +46,7 @@ export function getQueryVar(name, defVal) { export function getHashVar(name, defVal) { "use strict"; const re = new RegExp('.*[]' + name + '=([^&]*)'), - match = document.location.hash.match(re); + match = document.location.hash.match(re); if (typeof defVal === 'undefined') { defVal = null; } if (match) { @@ -156,65 +165,3 @@ export function eraseSetting(name) { // value change. delete settings[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 paramEq = encodeURIComponent(param) + "="; - let query; - if (elem.search) { - query = elem.search.slice(1).split('&'); - } else { - query = []; - } - - if (!query.some(v => v.startsWith(paramEq))) { - query.push(paramEq + encodeURIComponent(value)); - elem.search = "?" + query.join("&"); - } - - // some browsers (e.g. IE11) may occasionally omit the leading slash - // in the elem.pathname string. Handle that case gracefully. - if (elem.pathname.charAt(0) == "/") { - return elem.pathname.slice(1) + elem.search + elem.hash; - } - - return elem.pathname + elem.search + elem.hash; -} - -// sadly, we can't use the Fetch API until we decide to drop -// IE11 support or polyfill promises and fetch in IE11. -// resolve will receive an object on success, while reject -// will receive either an event or an error on failure. -export function fetchJSON(path) { - return new Promise((resolve, reject) => { - // NB: IE11 doesn't support JSON as a responseType - const req = new XMLHttpRequest(); - req.open('GET', path); - - req.onload = () => { - if (req.status === 200) { - let resObj; - try { - resObj = JSON.parse(req.responseText); - } catch (err) { - reject(err); - } - resolve(resObj); - } else { - reject(new Error("XHR got non-200 status while trying to load '" + path + "': " + req.status)); - } - }; - - req.onerror = evt => reject(new Error("XHR encountered an error while trying to load '" + path + "': " + evt.message)); - - req.ontimeout = evt => reject(new Error("XHR timed out while trying to load '" + path + "'")); - - req.send(); - }); -} diff --git a/systemvm/agent/noVNC/core/decoders/jpeg.js b/systemvm/agent/noVNC/core/decoders/jpeg.js new file mode 100644 index 00000000000..e1f2bdf875c --- /dev/null +++ b/systemvm/agent/noVNC/core/decoders/jpeg.js @@ -0,0 +1,141 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2019 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +export default class JPEGDecoder { + constructor() { + // RealVNC will reuse the quantization tables + // and Huffman tables, so we need to cache them. + this._quantTables = []; + this._huffmanTables = []; + this._cachedQuantTables = []; + this._cachedHuffmanTables = []; + + this._jpegLength = 0; + this._segments = []; + } + + decodeRect(x, y, width, height, sock, display, depth) { + // A rect of JPEG encodings is simply a JPEG file + if (!this._parseJPEG(sock.rQslice(0))) { + return false; + } + const data = sock.rQshiftBytes(this._jpegLength); + if (this._quantTables.length != 0 && this._huffmanTables.length != 0) { + // If there are quantization tables and Huffman tables in the JPEG + // image, we can directly render it. + display.imageRect(x, y, width, height, "image/jpeg", data); + return true; + } else { + // Otherwise we need to insert cached tables. + const sofIndex = this._segments.findIndex( + x => x[1] == 0xC0 || x[1] == 0xC2 + ); + if (sofIndex == -1) { + throw new Error("Illegal JPEG image without SOF"); + } + let segments = this._segments.slice(0, sofIndex); + segments = segments.concat(this._quantTables.length ? + this._quantTables : + this._cachedQuantTables); + segments.push(this._segments[sofIndex]); + segments = segments.concat(this._huffmanTables.length ? + this._huffmanTables : + this._cachedHuffmanTables, + this._segments.slice(sofIndex + 1)); + let length = 0; + for (let i = 0; i < segments.length; i++) { + length += segments[i].length; + } + const data = new Uint8Array(length); + length = 0; + for (let i = 0; i < segments.length; i++) { + data.set(segments[i], length); + length += segments[i].length; + } + display.imageRect(x, y, width, height, "image/jpeg", data); + return true; + } + } + + _parseJPEG(buffer) { + if (this._quantTables.length != 0) { + this._cachedQuantTables = this._quantTables; + } + if (this._huffmanTables.length != 0) { + this._cachedHuffmanTables = this._huffmanTables; + } + this._quantTables = []; + this._huffmanTables = []; + this._segments = []; + let i = 0; + let bufferLength = buffer.length; + while (true) { + let j = i; + if (j + 2 > bufferLength) { + return false; + } + if (buffer[j] != 0xFF) { + throw new Error("Illegal JPEG marker received (byte: " + + buffer[j] + ")"); + } + const type = buffer[j+1]; + j += 2; + if (type == 0xD9) { + this._jpegLength = j; + this._segments.push(buffer.slice(i, j)); + return true; + } else if (type == 0xDA) { + // start of scan + let hasFoundEndOfScan = false; + for (let k = j + 3; k + 1 < bufferLength; k++) { + if (buffer[k] == 0xFF && buffer[k+1] != 0x00 && + !(buffer[k+1] >= 0xD0 && buffer[k+1] <= 0xD7)) { + j = k; + hasFoundEndOfScan = true; + break; + } + } + if (!hasFoundEndOfScan) { + return false; + } + this._segments.push(buffer.slice(i, j)); + i = j; + continue; + } else if (type >= 0xD0 && type < 0xD9 || type == 0x01) { + // No length after marker + this._segments.push(buffer.slice(i, j)); + i = j; + continue; + } + if (j + 2 > bufferLength) { + return false; + } + const length = (buffer[j] << 8) + buffer[j+1] - 2; + if (length < 0) { + throw new Error("Illegal JPEG length received (length: " + + length + ")"); + } + j += 2; + if (j + length > bufferLength) { + return false; + } + j += length; + const segment = buffer.slice(i, j); + if (type == 0xC4) { + // Huffman tables + this._huffmanTables.push(segment); + } else if (type == 0xDB) { + // Quantization tables + this._quantTables.push(segment); + } + this._segments.push(segment); + i = j; + } + } +} diff --git a/systemvm/agent/noVNC/core/decoders/raw.js b/systemvm/agent/noVNC/core/decoders/raw.js index e8ea178e8f5..d08f7ba9500 100644 --- a/systemvm/agent/noVNC/core/decoders/raw.js +++ b/systemvm/agent/noVNC/core/decoders/raw.js @@ -51,7 +51,7 @@ export default class RawDecoder { // Max sure the image is fully opaque for (let i = 0; i < pixels; i++) { - data[i * 4 + 3] = 255; + data[index + i * 4 + 3] = 255; } display.blitImage(x, curY, width, currHeight, data, index); diff --git a/systemvm/agent/noVNC/core/decoders/zrle.js b/systemvm/agent/noVNC/core/decoders/zrle.js new file mode 100644 index 00000000000..97fbd58e7a1 --- /dev/null +++ b/systemvm/agent/noVNC/core/decoders/zrle.js @@ -0,0 +1,185 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2021 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + * + */ + +import Inflate from "../inflator.js"; + +const ZRLE_TILE_WIDTH = 64; +const ZRLE_TILE_HEIGHT = 64; + +export default class ZRLEDecoder { + constructor() { + this._length = 0; + this._inflator = new Inflate(); + + this._pixelBuffer = new Uint8Array(ZRLE_TILE_WIDTH * ZRLE_TILE_HEIGHT * 4); + this._tileBuffer = new Uint8Array(ZRLE_TILE_WIDTH * ZRLE_TILE_HEIGHT * 4); + } + + decodeRect(x, y, width, height, sock, display, depth) { + if (this._length === 0) { + if (sock.rQwait("ZLib data length", 4)) { + return false; + } + this._length = sock.rQshift32(); + } + if (sock.rQwait("Zlib data", this._length)) { + return false; + } + + const data = sock.rQshiftBytes(this._length); + + this._inflator.setInput(data); + + for (let ty = y; ty < y + height; ty += ZRLE_TILE_HEIGHT) { + let th = Math.min(ZRLE_TILE_HEIGHT, y + height - ty); + + for (let tx = x; tx < x + width; tx += ZRLE_TILE_WIDTH) { + let tw = Math.min(ZRLE_TILE_WIDTH, x + width - tx); + + const tileSize = tw * th; + const subencoding = this._inflator.inflate(1)[0]; + if (subencoding === 0) { + // raw data + const data = this._readPixels(tileSize); + display.blitImage(tx, ty, tw, th, data, 0, false); + } else if (subencoding === 1) { + // solid + const background = this._readPixels(1); + display.fillRect(tx, ty, tw, th, [background[0], background[1], background[2]]); + } else if (subencoding >= 2 && subencoding <= 16) { + const data = this._decodePaletteTile(subencoding, tileSize, tw, th); + display.blitImage(tx, ty, tw, th, data, 0, false); + } else if (subencoding === 128) { + const data = this._decodeRLETile(tileSize); + display.blitImage(tx, ty, tw, th, data, 0, false); + } else if (subencoding >= 130 && subencoding <= 255) { + const data = this._decodeRLEPaletteTile(subencoding - 128, tileSize); + display.blitImage(tx, ty, tw, th, data, 0, false); + } else { + throw new Error('Unknown subencoding: ' + subencoding); + } + } + } + this._length = 0; + return true; + } + + _getBitsPerPixelInPalette(paletteSize) { + if (paletteSize <= 2) { + return 1; + } else if (paletteSize <= 4) { + return 2; + } else if (paletteSize <= 16) { + return 4; + } + } + + _readPixels(pixels) { + let data = this._pixelBuffer; + const buffer = this._inflator.inflate(3*pixels); + for (let i = 0, j = 0; i < pixels*4; i += 4, j += 3) { + data[i] = buffer[j]; + data[i + 1] = buffer[j + 1]; + data[i + 2] = buffer[j + 2]; + data[i + 3] = 255; // Add the Alpha + } + return data; + } + + _decodePaletteTile(paletteSize, tileSize, tilew, tileh) { + const data = this._tileBuffer; + const palette = this._readPixels(paletteSize); + const bitsPerPixel = this._getBitsPerPixelInPalette(paletteSize); + const mask = (1 << bitsPerPixel) - 1; + + let offset = 0; + let encoded = this._inflator.inflate(1)[0]; + + for (let y=0; y>shift) & mask; + + data[offset] = palette[indexInPalette * 4]; + data[offset + 1] = palette[indexInPalette * 4 + 1]; + data[offset + 2] = palette[indexInPalette * 4 + 2]; + data[offset + 3] = palette[indexInPalette * 4 + 3]; + offset += 4; + shift-=bitsPerPixel; + } + if (shift<8-bitsPerPixel && y= 128) { + indexInPalette -= 128; + length = this._readRLELength(); + } + if (indexInPalette > paletteSize) { + throw new Error('Too big index in palette: ' + indexInPalette + ', palette size: ' + paletteSize); + } + if (offset + length > tileSize) { + throw new Error('Too big rle length in palette mode: ' + length + ', allowed length is: ' + (tileSize - offset)); + } + + for (let j = 0; j < length; j++) { + data[offset * 4] = palette[indexInPalette * 4]; + data[offset * 4 + 1] = palette[indexInPalette * 4 + 1]; + data[offset * 4 + 2] = palette[indexInPalette * 4 + 2]; + data[offset * 4 + 3] = palette[indexInPalette * 4 + 3]; + offset++; + } + } + return data; + } + + _readRLELength() { + let length = 0; + let current = 0; + do { + current = this._inflator.inflate(1)[0]; + length += current; + } while (current === 255); + return length + 1; + } +} diff --git a/systemvm/agent/noVNC/core/des.js b/systemvm/agent/noVNC/core/des.js index d2f807b828f..ba1ebde013c 100644 --- a/systemvm/agent/noVNC/core/des.js +++ b/systemvm/agent/noVNC/core/des.js @@ -81,7 +81,7 @@ 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]; + 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; diff --git a/systemvm/agent/noVNC/core/display.js b/systemvm/agent/noVNC/core/display.js index 8eaa8001cc6..bf8d5fab699 100644 --- a/systemvm/agent/noVNC/core/display.js +++ b/systemvm/agent/noVNC/core/display.js @@ -8,7 +8,6 @@ import * as Log from './util/logging.js'; import Base64 from "./base64.js"; -import { supportsImageMetadata } from './util/browser.js'; import { toSigned32bit } from './util/int.js'; export default class Display { @@ -56,11 +55,6 @@ export default class Display { Log.Debug("User Agent: " + navigator.userAgent); - // Check canvas features - if (!('createImageData' in this._drawCtx)) { - throw new Error("Canvas does not support createImageData"); - } - Log.Debug("<< Display.constructor"); // ===== PROPERTIES ===== @@ -230,6 +224,18 @@ export default class Display { this.viewportChangePos(0, 0); } + getImageData() { + return this._drawCtx.getImageData(0, 0, this.width, this.height); + } + + toDataURL(type, encoderOptions) { + return this._backbuffer.toDataURL(type, encoderOptions); + } + + toBlob(callback, type, quality) { + return this._backbuffer.toBlob(callback, type, quality); + } + // Track what parts of the visible canvas that need updating _damage(x, y, w, h) { if (x < this._damageBounds.left) { @@ -393,13 +399,7 @@ export default class Display { let data = new Uint8ClampedArray(arr.buffer, arr.byteOffset + offset, width * height * 4); - let img; - if (supportsImageMetadata) { - img = new ImageData(data, width, height); - } else { - img = this._drawCtx.createImageData(width, height); - img.data.set(data); - } + let img = new ImageData(data, width, height); this._drawCtx.putImageData(img, x, y); this._damage(x, y, width, height); } @@ -494,8 +494,7 @@ export default class Display { this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true); break; case 'img': - /* IE tends to set "complete" prematurely, so check dimensions */ - if (a.img.complete && (a.img.width !== 0) && (a.img.height !== 0)) { + if (a.img.complete) { if (a.img.width !== a.width || a.img.height !== a.height) { Log.Error("Decoded image has incorrect dimensions. Got " + a.img.width + "x" + a.img.height + ". Expected " + diff --git a/systemvm/agent/noVNC/core/encodings.js b/systemvm/agent/noVNC/core/encodings.js index 51c09929168..2041b6e0212 100644 --- a/systemvm/agent/noVNC/core/encodings.js +++ b/systemvm/agent/noVNC/core/encodings.js @@ -12,7 +12,9 @@ export const encodings = { encodingRRE: 2, encodingHextile: 5, encodingTight: 7, + encodingZRLE: 16, encodingTightPNG: -260, + encodingJPEG: 21, pseudoEncodingQualityLevel9: -23, pseudoEncodingQualityLevel0: -32, @@ -38,7 +40,9 @@ export function encodingName(num) { case encodings.encodingRRE: return "RRE"; case encodings.encodingHextile: return "Hextile"; case encodings.encodingTight: return "Tight"; + case encodings.encodingZRLE: return "ZRLE"; case encodings.encodingTightPNG: return "TightPNG"; + case encodings.encodingJPEG: return "JPEG"; default: return "[unknown encoding " + num + "]"; } } diff --git a/systemvm/agent/noVNC/core/input/domkeytable.js b/systemvm/agent/noVNC/core/input/domkeytable.js index b84ad45de55..f79aeadfa47 100644 --- a/systemvm/agent/noVNC/core/input/domkeytable.js +++ b/systemvm/agent/noVNC/core/input/domkeytable.js @@ -35,7 +35,7 @@ function addNumpad(key, standard, numpad) { DOMKeyTable[key] = [standard, standard, standard, numpad]; } -// 2.2. Modifier Keys +// 3.2. Modifier Keys addLeftRight("Alt", KeyTable.XK_Alt_L, KeyTable.XK_Alt_R); addStandard("AltGraph", KeyTable.XK_ISO_Level3_Shift); @@ -49,25 +49,27 @@ addStandard("ScrollLock", KeyTable.XK_Scroll_Lock); addLeftRight("Shift", KeyTable.XK_Shift_L, KeyTable.XK_Shift_R); // - Symbol // - SymbolLock +// - Hyper +// - Super -// 2.3. Whitespace Keys +// 3.3. Whitespace Keys addNumpad("Enter", KeyTable.XK_Return, KeyTable.XK_KP_Enter); addStandard("Tab", KeyTable.XK_Tab); addNumpad(" ", KeyTable.XK_space, KeyTable.XK_KP_Space); -// 2.4. Navigation Keys +// 3.4. Navigation Keys addNumpad("ArrowDown", KeyTable.XK_Down, KeyTable.XK_KP_Down); -addNumpad("ArrowUp", KeyTable.XK_Up, KeyTable.XK_KP_Up); addNumpad("ArrowLeft", KeyTable.XK_Left, KeyTable.XK_KP_Left); addNumpad("ArrowRight", KeyTable.XK_Right, KeyTable.XK_KP_Right); +addNumpad("ArrowUp", KeyTable.XK_Up, KeyTable.XK_KP_Up); addNumpad("End", KeyTable.XK_End, KeyTable.XK_KP_End); addNumpad("Home", KeyTable.XK_Home, KeyTable.XK_KP_Home); addNumpad("PageDown", KeyTable.XK_Next, KeyTable.XK_KP_Next); addNumpad("PageUp", KeyTable.XK_Prior, KeyTable.XK_KP_Prior); -// 2.5. Editing Keys +// 3.5. Editing Keys addStandard("Backspace", KeyTable.XK_BackSpace); // Browsers send "Clear" for the numpad 5 without NumLock because @@ -85,7 +87,7 @@ addStandard("Paste", KeyTable.XF86XK_Paste); addStandard("Redo", KeyTable.XK_Redo); addStandard("Undo", KeyTable.XK_Undo); -// 2.6. UI Keys +// 3.6. UI Keys // - Accept // - Again (could just be XK_Redo) @@ -103,7 +105,7 @@ addStandard("Select", KeyTable.XK_Select); addStandard("ZoomIn", KeyTable.XF86XK_ZoomIn); addStandard("ZoomOut", KeyTable.XF86XK_ZoomOut); -// 2.7. Device Keys +// 3.7. Device Keys addStandard("BrightnessDown", KeyTable.XF86XK_MonBrightnessDown); addStandard("BrightnessUp", KeyTable.XF86XK_MonBrightnessUp); @@ -116,10 +118,10 @@ addStandard("Hibernate", KeyTable.XF86XK_Hibernate); addStandard("Standby", KeyTable.XF86XK_Standby); addStandard("WakeUp", KeyTable.XF86XK_WakeUp); -// 2.8. IME and Composition Keys +// 3.8. IME and Composition Keys addStandard("AllCandidates", KeyTable.XK_MultipleCandidate); -addStandard("Alphanumeric", KeyTable.XK_Eisu_Shift); // could also be _Eisu_Toggle +addStandard("Alphanumeric", KeyTable.XK_Eisu_toggle); addStandard("CodeInput", KeyTable.XK_Codeinput); addStandard("Compose", KeyTable.XK_Multi_key); addStandard("Convert", KeyTable.XK_Henkan); @@ -137,7 +139,7 @@ addStandard("PreviousCandidate", KeyTable.XK_PreviousCandidate); addStandard("SingleCandidate", KeyTable.XK_SingleCandidate); addStandard("HangulMode", KeyTable.XK_Hangul); addStandard("HanjaMode", KeyTable.XK_Hangul_Hanja); -addStandard("JunjuaMode", KeyTable.XK_Hangul_Jeonja); +addStandard("JunjaMode", KeyTable.XK_Hangul_Jeonja); addStandard("Eisu", KeyTable.XK_Eisu_toggle); addStandard("Hankaku", KeyTable.XK_Hankaku); addStandard("Hiragana", KeyTable.XK_Hiragana); @@ -147,9 +149,9 @@ addStandard("KanjiMode", KeyTable.XK_Kanji); addStandard("Katakana", KeyTable.XK_Katakana); addStandard("Romaji", KeyTable.XK_Romaji); addStandard("Zenkaku", KeyTable.XK_Zenkaku); -addStandard("ZenkakuHanaku", KeyTable.XK_Zenkaku_Hankaku); +addStandard("ZenkakuHankaku", KeyTable.XK_Zenkaku_Hankaku); -// 2.9. General-Purpose Function Keys +// 3.9. General-Purpose Function Keys addStandard("F1", KeyTable.XK_F1); addStandard("F2", KeyTable.XK_F2); @@ -188,7 +190,7 @@ addStandard("F34", KeyTable.XK_F34); addStandard("F35", KeyTable.XK_F35); // - Soft1... -// 2.10. Multimedia Keys +// 3.10. Multimedia Keys // - ChannelDown // - ChannelUp @@ -200,6 +202,7 @@ addStandard("MailSend", KeyTable.XF86XK_Send); addStandard("MediaFastForward", KeyTable.XF86XK_AudioForward); addStandard("MediaPause", KeyTable.XF86XK_AudioPause); addStandard("MediaPlay", KeyTable.XF86XK_AudioPlay); +// - MediaPlayPause addStandard("MediaRecord", KeyTable.XF86XK_AudioRecord); addStandard("MediaRewind", KeyTable.XF86XK_AudioRewind); addStandard("MediaStop", KeyTable.XF86XK_AudioStop); @@ -211,12 +214,12 @@ addStandard("Print", KeyTable.XK_Print); addStandard("Save", KeyTable.XF86XK_Save); addStandard("SpellCheck", KeyTable.XF86XK_Spell); -// 2.11. Multimedia Numpad Keys +// 3.11. Multimedia Numpad Keys // - Key11 // - Key12 -// 2.12. Audio Keys +// 3.12. Audio Keys // - AudioBalanceLeft // - AudioBalanceRight @@ -236,16 +239,17 @@ addStandard("AudioVolumeMute", KeyTable.XF86XK_AudioMute); // - MicrophoneVolumeUp addStandard("MicrophoneVolumeMute", KeyTable.XF86XK_AudioMicMute); -// 2.13. Speech Keys +// 3.13. Speech Keys // - SpeechCorrectionList // - SpeechInputToggle -// 2.14. Application Keys +// 3.14. Application Keys addStandard("LaunchApplication1", KeyTable.XF86XK_MyComputer); addStandard("LaunchApplication2", KeyTable.XF86XK_Calculator); addStandard("LaunchCalendar", KeyTable.XF86XK_Calendar); +// - LaunchContacts addStandard("LaunchMail", KeyTable.XF86XK_Mail); addStandard("LaunchMediaPlayer", KeyTable.XF86XK_AudioMedia); addStandard("LaunchMusicPlayer", KeyTable.XF86XK_Music); @@ -256,7 +260,7 @@ addStandard("LaunchWebBrowser", KeyTable.XF86XK_WWW); addStandard("LaunchWebCam", KeyTable.XF86XK_WebCam); addStandard("LaunchWordProcessor", KeyTable.XF86XK_Word); -// 2.15. Browser Keys +// 3.15. Browser Keys addStandard("BrowserBack", KeyTable.XF86XK_Back); addStandard("BrowserFavorites", KeyTable.XF86XK_Favorites); @@ -266,15 +270,15 @@ addStandard("BrowserRefresh", KeyTable.XF86XK_Refresh); addStandard("BrowserSearch", KeyTable.XF86XK_Search); addStandard("BrowserStop", KeyTable.XF86XK_Stop); -// 2.16. Mobile Phone Keys +// 3.16. Mobile Phone Keys // - A whole bunch... -// 2.17. TV Keys +// 3.17. TV Keys // - A whole bunch... -// 2.18. Media Controller Keys +// 3.18. Media Controller Keys // - A whole bunch... addStandard("Dimmer", KeyTable.XF86XK_BrightnessAdjust); diff --git a/systemvm/agent/noVNC/core/input/keyboard.js b/systemvm/agent/noVNC/core/input/keyboard.js index 9e6af2ac753..ddb5ce0994d 100644 --- a/systemvm/agent/noVNC/core/input/keyboard.js +++ b/systemvm/agent/noVNC/core/input/keyboard.js @@ -20,16 +20,13 @@ export default class Keyboard { this._keyDownList = {}; // List of depressed keys // (even if they are happy) - this._pendingKey = null; // Key waiting for keypress this._altGrArmed = false; // Windows AltGr detection // keep these here so we can refer to them later this._eventHandlers = { 'keyup': this._handleKeyUp.bind(this), 'keydown': this._handleKeyDown.bind(this), - 'keypress': this._handleKeyPress.bind(this), 'blur': this._allKeysUp.bind(this), - 'checkalt': this._checkAlt.bind(this), }; // ===== EVENT HANDLERS ===== @@ -62,9 +59,7 @@ export default class Keyboard { } // Unstable, but we don't have anything else to go on - // (don't use it for 'keypress' events thought since - // WebKit sets it to the same as charCode) - if (e.keyCode && (e.type !== 'keypress')) { + if (e.keyCode) { // 229 is used for composition events if (e.keyCode !== 229) { return 'Platform' + e.keyCode; @@ -158,6 +153,16 @@ export default class Keyboard { keysym = this._keyDownList[code]; } + // macOS doesn't send proper key releases if a key is pressed + // while meta is held down + if ((browser.isMac() || browser.isIOS()) && + (e.metaKey && code !== 'MetaLeft' && code !== 'MetaRight')) { + this._sendKeyEvent(keysym, code, true); + this._sendKeyEvent(keysym, code, false); + stopEvent(e); + return; + } + // macOS doesn't send proper key events for modifiers, only // state change events. That gets extra confusing for CapsLock // which toggles on each press, but not on release. So pretend @@ -169,20 +174,20 @@ export default class Keyboard { return; } - // If this is a legacy browser then we'll need to wait for - // a keypress event as well - // (IE and Edge has a broken KeyboardEvent.key, so we can't - // just check for the presence of that field) - if (!keysym && (!e.key || browser.isIE() || browser.isEdge())) { - this._pendingKey = code; - // However we might not get a keypress event if the key - // is non-printable, which needs some special fallback - // handling - setTimeout(this._handleKeyPressTimeout.bind(this), 10, e); + // Windows doesn't send proper key releases for a bunch of + // Japanese IM keys so we have to fake the release right away + const jpBadKeys = [ KeyTable.XK_Zenkaku_Hankaku, + KeyTable.XK_Eisu_toggle, + KeyTable.XK_Katakana, + KeyTable.XK_Hiragana, + KeyTable.XK_Romaji ]; + if (browser.isWindows() && jpBadKeys.includes(keysym)) { + this._sendKeyEvent(keysym, code, true); + this._sendKeyEvent(keysym, code, false); + stopEvent(e); return; } - this._pendingKey = null; stopEvent(e); // Possible start of AltGr sequence? (see above) @@ -197,69 +202,6 @@ export default class Keyboard { 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); @@ -312,30 +254,6 @@ export default class Keyboard { Log.Debug("<< Keyboard.allKeysUp"); } - // Alt workaround for Firefox on Windows, see below - _checkAlt(e) { - if (e.skipCheckAlt) { - return; - } - if (e.altKey) { - return; - } - - const target = this._target; - const downList = this._keyDownList; - ['AltLeft', 'AltRight'].forEach((code) => { - if (!(code in downList)) { - return; - } - - const event = new KeyboardEvent('keyup', - { key: downList[code], - code: code }); - event.skipCheckAlt = true; - target.dispatchEvent(event); - }); - } - // ===== PUBLIC METHODS ===== grab() { @@ -343,41 +261,18 @@ export default class Keyboard { this._target.addEventListener('keydown', this._eventHandlers.keydown); this._target.addEventListener('keyup', this._eventHandlers.keyup); - this._target.addEventListener('keypress', this._eventHandlers.keypress); // Release (key up) if window loses focus window.addEventListener('blur', this._eventHandlers.blur); - // Firefox on Windows has broken handling of Alt, so we need to - // poll as best we can for releases (still doesn't prevent the - // menu from popping up though as we can't call - // preventDefault()) - if (browser.isWindows() && browser.isFirefox()) { - const handler = this._eventHandlers.checkalt; - ['mousedown', 'mouseup', 'mousemove', 'wheel', - 'touchstart', 'touchend', 'touchmove', - 'keydown', 'keyup'].forEach(type => - document.addEventListener(type, handler, - { capture: true, - passive: true })); - } - //Log.Debug("<< Keyboard.grab"); } ungrab() { //Log.Debug(">> Keyboard.ungrab"); - if (browser.isWindows() && browser.isFirefox()) { - const handler = this._eventHandlers.checkalt; - ['mousedown', 'mouseup', 'mousemove', 'wheel', - 'touchstart', 'touchend', 'touchmove', - 'keydown', 'keyup'].forEach(type => document.removeEventListener(type, handler)); - } - this._target.removeEventListener('keydown', this._eventHandlers.keydown); this._target.removeEventListener('keyup', this._eventHandlers.keyup); - this._target.removeEventListener('keypress', this._eventHandlers.keypress); window.removeEventListener('blur', this._eventHandlers.blur); // Release (key up) all keys that are in a down state diff --git a/systemvm/agent/noVNC/core/input/util.js b/systemvm/agent/noVNC/core/input/util.js index 1b98040be23..58f84e5587f 100644 --- a/systemvm/agent/noVNC/core/input/util.js +++ b/systemvm/agent/noVNC/core/input/util.js @@ -22,9 +22,8 @@ export function getKeycode(evt) { } // The de-facto standard is to use Windows Virtual-Key codes - // in the 'keyCode' field for non-printable characters. However - // Webkit sets it to the same as charCode in 'keypress' events. - if ((evt.type !== 'keypress') && (evt.keyCode in vkeys)) { + // in the 'keyCode' field for non-printable characters + if (evt.keyCode in vkeys) { let code = vkeys[evt.keyCode]; // macOS has messed up this code for some reason @@ -69,26 +68,6 @@ export function getKeycode(evt) { 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'; @@ -110,18 +89,7 @@ export function getKey(evt) { return 'Delete'; } - // IE and Edge need special handling, but for everyone else we - // can trust the value provided - if (!browser.isIE() && !browser.isEdge()) { - return evt.key; - } - - // IE and Edge have broken handling of AltGraph so we can only - // trust them for non-printable characters (and unfortunately - // they also specify 'Unidentified' for some problem keys) - if ((evt.key.length !== 1) && (evt.key !== 'Unidentified')) { - return evt.key; - } + return evt.key; } // Try to deduce it based on the physical key @@ -189,6 +157,21 @@ export function getKeysym(evt) { } } + // Windows sends alternating symbols for some keys when using a + // Japanese layout. We have no way of synchronising with the IM + // running on the remote system, so we send some combined keysym + // instead and hope for the best. + if (browser.isWindows()) { + switch (key) { + case 'Zenkaku': + case 'Hankaku': + return KeyTable.XK_Zenkaku_Hankaku; + case 'Romaji': + case 'KanaMode': + return KeyTable.XK_Romaji; + } + } + return DOMKeyTable[key][location]; } diff --git a/systemvm/agent/noVNC/core/input/vkeys.js b/systemvm/agent/noVNC/core/input/vkeys.js index f84109b2559..dacc3580947 100644 --- a/systemvm/agent/noVNC/core/input/vkeys.js +++ b/systemvm/agent/noVNC/core/input/vkeys.js @@ -13,7 +13,6 @@ export default { 0x08: 'Backspace', 0x09: 'Tab', 0x0a: 'NumpadClear', - 0x0c: 'Numpad5', // IE11 sends evt.keyCode: 12 when numlock is off 0x0d: 'Enter', 0x10: 'ShiftLeft', 0x11: 'ControlLeft', diff --git a/systemvm/agent/noVNC/core/input/xtscancodes.js b/systemvm/agent/noVNC/core/input/xtscancodes.js index 514809c6fa2..8ab9c17fde2 100644 --- a/systemvm/agent/noVNC/core/input/xtscancodes.js +++ b/systemvm/agent/noVNC/core/input/xtscancodes.js @@ -1,8 +1,8 @@ /* - * This file is auto-generated from keymaps.csv on 2017-05-31 16:20 - * Database checksum sha256(92fd165507f2a3b8c5b3fa56e425d45788dbcb98cf067a307527d91ce22cab94) + * This file is auto-generated from keymaps.csv + * Database checksum sha256(76d68c10e97d37fe2ea459e210125ae41796253fb217e900bf2983ade13a7920) * To re-generate, run: - * keymap-gen --lang=js code-map keymaps.csv html atset1 + * keymap-gen code-map --lang=js keymaps.csv html atset1 */ export default { "Again": 0xe005, /* html:Again (Again) -> linux:129 (KEY_AGAIN) -> atset1:57349 */ @@ -111,6 +111,8 @@ export default { "KeyX": 0x2d, /* html:KeyX (KeyX) -> linux:45 (KEY_X) -> atset1:45 */ "KeyY": 0x15, /* html:KeyY (KeyY) -> linux:21 (KEY_Y) -> atset1:21 */ "KeyZ": 0x2c, /* html:KeyZ (KeyZ) -> linux:44 (KEY_Z) -> atset1:44 */ + "Lang1": 0x72, /* html:Lang1 (Lang1) -> linux:122 (KEY_HANGEUL) -> atset1:114 */ + "Lang2": 0x71, /* html:Lang2 (Lang2) -> linux:123 (KEY_HANJA) -> atset1:113 */ "Lang3": 0x78, /* html:Lang3 (Lang3) -> linux:90 (KEY_KATAKANA) -> atset1:120 */ "Lang4": 0x77, /* html:Lang4 (Lang4) -> linux:91 (KEY_HIRAGANA) -> atset1:119 */ "Lang5": 0x76, /* html:Lang5 (Lang5) -> linux:85 (KEY_ZENKAKUHANKAKU) -> atset1:118 */ diff --git a/systemvm/agent/noVNC/core/ra2.js b/systemvm/agent/noVNC/core/ra2.js new file mode 100644 index 00000000000..81a8a8952d7 --- /dev/null +++ b/systemvm/agent/noVNC/core/ra2.js @@ -0,0 +1,567 @@ +import Base64 from './base64.js'; +import { encodeUTF8 } from './util/strings.js'; +import EventTargetMixin from './util/eventtarget.js'; + +export class AESEAXCipher { + constructor() { + this._rawKey = null; + this._ctrKey = null; + this._cbcKey = null; + this._zeroBlock = new Uint8Array(16); + this._prefixBlock0 = this._zeroBlock; + this._prefixBlock1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + this._prefixBlock2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + } + + async _encryptBlock(block) { + const encrypted = await window.crypto.subtle.encrypt({ + name: "AES-CBC", + iv: this._zeroBlock, + }, this._cbcKey, block); + return new Uint8Array(encrypted).slice(0, 16); + } + + async _initCMAC() { + const k1 = await this._encryptBlock(this._zeroBlock); + const k2 = new Uint8Array(16); + const v = k1[0] >>> 6; + for (let i = 0; i < 15; i++) { + k2[i] = (k1[i + 1] >> 6) | (k1[i] << 2); + k1[i] = (k1[i + 1] >> 7) | (k1[i] << 1); + } + const lut = [0x0, 0x87, 0x0e, 0x89]; + k2[14] ^= v >>> 1; + k2[15] = (k1[15] << 2) ^ lut[v]; + k1[15] = (k1[15] << 1) ^ lut[v >> 1]; + this._k1 = k1; + this._k2 = k2; + } + + async _encryptCTR(data, counter) { + const encrypted = await window.crypto.subtle.encrypt({ + "name": "AES-CTR", + counter: counter, + length: 128 + }, this._ctrKey, data); + return new Uint8Array(encrypted); + } + + async _decryptCTR(data, counter) { + const decrypted = await window.crypto.subtle.decrypt({ + "name": "AES-CTR", + counter: counter, + length: 128 + }, this._ctrKey, data); + return new Uint8Array(decrypted); + } + + async _computeCMAC(data, prefixBlock) { + if (prefixBlock.length !== 16) { + return null; + } + const n = Math.floor(data.length / 16); + const m = Math.ceil(data.length / 16); + const r = data.length - n * 16; + const cbcData = new Uint8Array((m + 1) * 16); + cbcData.set(prefixBlock); + cbcData.set(data, 16); + if (r === 0) { + for (let i = 0; i < 16; i++) { + cbcData[n * 16 + i] ^= this._k1[i]; + } + } else { + cbcData[(n + 1) * 16 + r] = 0x80; + for (let i = 0; i < 16; i++) { + cbcData[(n + 1) * 16 + i] ^= this._k2[i]; + } + } + let cbcEncrypted = await window.crypto.subtle.encrypt({ + name: "AES-CBC", + iv: this._zeroBlock, + }, this._cbcKey, cbcData); + + cbcEncrypted = new Uint8Array(cbcEncrypted); + const mac = cbcEncrypted.slice(cbcEncrypted.length - 32, cbcEncrypted.length - 16); + return mac; + } + + async setKey(key) { + this._rawKey = key; + this._ctrKey = await window.crypto.subtle.importKey( + "raw", key, {"name": "AES-CTR"}, false, ["encrypt", "decrypt"]); + this._cbcKey = await window.crypto.subtle.importKey( + "raw", key, {"name": "AES-CBC"}, false, ["encrypt", "decrypt"]); + await this._initCMAC(); + } + + async encrypt(message, associatedData, nonce) { + const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0); + const encrypted = await this._encryptCTR(message, nCMAC); + const adCMAC = await this._computeCMAC(associatedData, this._prefixBlock1); + const mac = await this._computeCMAC(encrypted, this._prefixBlock2); + for (let i = 0; i < 16; i++) { + mac[i] ^= nCMAC[i] ^ adCMAC[i]; + } + const res = new Uint8Array(16 + encrypted.length); + res.set(encrypted); + res.set(mac, encrypted.length); + return res; + } + + async decrypt(encrypted, associatedData, nonce, mac) { + const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0); + const adCMAC = await this._computeCMAC(associatedData, this._prefixBlock1); + const computedMac = await this._computeCMAC(encrypted, this._prefixBlock2); + for (let i = 0; i < 16; i++) { + computedMac[i] ^= nCMAC[i] ^ adCMAC[i]; + } + if (computedMac.length !== mac.length) { + return null; + } + for (let i = 0; i < mac.length; i++) { + if (computedMac[i] !== mac[i]) { + return null; + } + } + const res = await this._decryptCTR(encrypted, nCMAC); + return res; + } +} + +export class RA2Cipher { + constructor() { + this._cipher = new AESEAXCipher(); + this._counter = new Uint8Array(16); + } + + async setKey(key) { + await this._cipher.setKey(key); + } + + async makeMessage(message) { + const ad = new Uint8Array([(message.length & 0xff00) >>> 8, message.length & 0xff]); + const encrypted = await this._cipher.encrypt(message, ad, this._counter); + for (let i = 0; i < 16 && this._counter[i]++ === 255; i++); + const res = new Uint8Array(message.length + 2 + 16); + res.set(ad); + res.set(encrypted, 2); + return res; + } + + async receiveMessage(length, encrypted, mac) { + const ad = new Uint8Array([(length & 0xff00) >>> 8, length & 0xff]); + const res = await this._cipher.decrypt(encrypted, ad, this._counter, mac); + for (let i = 0; i < 16 && this._counter[i]++ === 255; i++); + return res; + } +} + +export class RSACipher { + constructor(keyLength) { + this._key = null; + this._keyLength = keyLength; + this._keyBytes = Math.ceil(keyLength / 8); + this._n = null; + this._e = null; + this._d = null; + this._nBigInt = null; + this._eBigInt = null; + this._dBigInt = null; + } + + _base64urlDecode(data) { + data = data.replace(/-/g, "+").replace(/_/g, "/"); + data = data.padEnd(Math.ceil(data.length / 4) * 4, "="); + return Base64.decode(data); + } + + _u8ArrayToBigInt(arr) { + let hex = '0x'; + for (let i = 0; i < arr.length; i++) { + hex += arr[i].toString(16).padStart(2, '0'); + } + return BigInt(hex); + } + + _padArray(arr, length) { + const res = new Uint8Array(length); + res.set(arr, length - arr.length); + return res; + } + + _bigIntToU8Array(bigint, padLength=0) { + let hex = bigint.toString(16); + if (padLength === 0) { + padLength = Math.ceil(hex.length / 2) * 2; + } + hex = hex.padStart(padLength * 2, '0'); + const length = hex.length / 2; + const arr = new Uint8Array(length); + for (let i = 0; i < length; i++) { + arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); + } + return arr; + } + + _modPow(b, e, m) { + if (m === 1n) { + return 0; + } + let r = 1n; + b = b % m; + while (e > 0) { + if (e % 2n === 1n) { + r = (r * b) % m; + } + e = e / 2n; + b = (b * b) % m; + } + return r; + } + + async generateKey() { + this._key = await window.crypto.subtle.generateKey( + { + name: "RSA-OAEP", + modulusLength: this._keyLength, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: {name: "SHA-256"}, + }, + true, ["encrypt", "decrypt"]); + const privateKey = await window.crypto.subtle.exportKey("jwk", this._key.privateKey); + this._n = this._padArray(this._base64urlDecode(privateKey.n), this._keyBytes); + this._nBigInt = this._u8ArrayToBigInt(this._n); + this._e = this._padArray(this._base64urlDecode(privateKey.e), this._keyBytes); + this._eBigInt = this._u8ArrayToBigInt(this._e); + this._d = this._padArray(this._base64urlDecode(privateKey.d), this._keyBytes); + this._dBigInt = this._u8ArrayToBigInt(this._d); + } + + setPublicKey(n, e) { + if (n.length !== this._keyBytes || e.length !== this._keyBytes) { + return; + } + this._n = new Uint8Array(this._keyBytes); + this._e = new Uint8Array(this._keyBytes); + this._n.set(n); + this._e.set(e); + this._nBigInt = this._u8ArrayToBigInt(this._n); + this._eBigInt = this._u8ArrayToBigInt(this._e); + } + + encrypt(message) { + if (message.length > this._keyBytes - 11) { + return null; + } + const ps = new Uint8Array(this._keyBytes - message.length - 3); + window.crypto.getRandomValues(ps); + for (let i = 0; i < ps.length; i++) { + ps[i] = Math.floor(ps[i] * 254 / 255 + 1); + } + const em = new Uint8Array(this._keyBytes); + em[1] = 0x02; + em.set(ps, 2); + em.set(message, ps.length + 3); + const emBigInt = this._u8ArrayToBigInt(em); + const c = this._modPow(emBigInt, this._eBigInt, this._nBigInt); + return this._bigIntToU8Array(c, this._keyBytes); + } + + decrypt(message) { + if (message.length !== this._keyBytes) { + return null; + } + const msgBigInt = this._u8ArrayToBigInt(message); + const emBigInt = this._modPow(msgBigInt, this._dBigInt, this._nBigInt); + const em = this._bigIntToU8Array(emBigInt, this._keyBytes); + if (em[0] !== 0x00 || em[1] !== 0x02) { + return null; + } + let i = 2; + for (; i < em.length; i++) { + if (em[i] === 0x00) { + break; + } + } + if (i === em.length) { + return null; + } + return em.slice(i + 1, em.length); + } + + get keyLength() { + return this._keyLength; + } + + get n() { + return this._n; + } + + get e() { + return this._e; + } + + get d() { + return this._d; + } +} + +export default class RSAAESAuthenticationState extends EventTargetMixin { + constructor(sock, getCredentials) { + super(); + this._hasStarted = false; + this._checkSock = null; + this._checkCredentials = null; + this._approveServerResolve = null; + this._sockReject = null; + this._credentialsReject = null; + this._approveServerReject = null; + this._sock = sock; + this._getCredentials = getCredentials; + } + + _waitSockAsync(len) { + return new Promise((resolve, reject) => { + const hasData = () => !this._sock.rQwait('RA2', len); + if (hasData()) { + resolve(); + } else { + this._checkSock = () => { + if (hasData()) { + resolve(); + this._checkSock = null; + this._sockReject = null; + } + }; + this._sockReject = reject; + } + }); + } + + _waitApproveKeyAsync() { + return new Promise((resolve, reject) => { + this._approveServerResolve = resolve; + this._approveServerReject = reject; + }); + } + + _waitCredentialsAsync(subtype) { + const hasCredentials = () => { + if (subtype === 1 && this._getCredentials().username !== undefined && + this._getCredentials().password !== undefined) { + return true; + } else if (subtype === 2 && this._getCredentials().password !== undefined) { + return true; + } + return false; + }; + return new Promise((resolve, reject) => { + if (hasCredentials()) { + resolve(); + } else { + this._checkCredentials = () => { + if (hasCredentials()) { + resolve(); + this._checkCredentials = null; + this._credentialsReject = null; + } + }; + this._credentialsReject = reject; + } + }); + } + + checkInternalEvents() { + if (this._checkSock !== null) { + this._checkSock(); + } + if (this._checkCredentials !== null) { + this._checkCredentials(); + } + } + + approveServer() { + if (this._approveServerResolve !== null) { + this._approveServerResolve(); + this._approveServerResolve = null; + } + } + + disconnect() { + if (this._sockReject !== null) { + this._sockReject(new Error("disconnect normally")); + this._sockReject = null; + } + if (this._credentialsReject !== null) { + this._credentialsReject(new Error("disconnect normally")); + this._credentialsReject = null; + } + if (this._approveServerReject !== null) { + this._approveServerReject(new Error("disconnect normally")); + this._approveServerReject = null; + } + } + + async negotiateRA2neAuthAsync() { + this._hasStarted = true; + // 1: Receive server public key + await this._waitSockAsync(4); + const serverKeyLengthBuffer = this._sock.rQslice(0, 4); + const serverKeyLength = this._sock.rQshift32(); + if (serverKeyLength < 1024) { + throw new Error("RA2: server public key is too short: " + serverKeyLength); + } else if (serverKeyLength > 8192) { + throw new Error("RA2: server public key is too long: " + serverKeyLength); + } + const serverKeyBytes = Math.ceil(serverKeyLength / 8); + await this._waitSockAsync(serverKeyBytes * 2); + const serverN = this._sock.rQshiftBytes(serverKeyBytes); + const serverE = this._sock.rQshiftBytes(serverKeyBytes); + const serverRSACipher = new RSACipher(serverKeyLength); + serverRSACipher.setPublicKey(serverN, serverE); + const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2); + serverPublickey.set(serverKeyLengthBuffer); + serverPublickey.set(serverN, 4); + serverPublickey.set(serverE, 4 + serverKeyBytes); + + // verify server public key + this.dispatchEvent(new CustomEvent("serververification", { + detail: { type: "RSA", publickey: serverPublickey } + })); + await this._waitApproveKeyAsync(); + + // 2: Send client public key + const clientKeyLength = 2048; + const clientKeyBytes = Math.ceil(clientKeyLength / 8); + const clientRSACipher = new RSACipher(clientKeyLength); + await clientRSACipher.generateKey(); + const clientN = clientRSACipher.n; + const clientE = clientRSACipher.e; + const clientPublicKey = new Uint8Array(4 + clientKeyBytes * 2); + clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24; + clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16; + clientPublicKey[2] = (clientKeyLength & 0xff00) >>> 8; + clientPublicKey[3] = clientKeyLength & 0xff; + clientPublicKey.set(clientN, 4); + clientPublicKey.set(clientE, 4 + clientKeyBytes); + this._sock.send(clientPublicKey); + + // 3: Send client random + const clientRandom = new Uint8Array(16); + window.crypto.getRandomValues(clientRandom); + const clientEncryptedRandom = serverRSACipher.encrypt(clientRandom); + const clientRandomMessage = new Uint8Array(2 + serverKeyBytes); + clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8; + clientRandomMessage[1] = serverKeyBytes & 0xff; + clientRandomMessage.set(clientEncryptedRandom, 2); + this._sock.send(clientRandomMessage); + + // 4: Receive server random + await this._waitSockAsync(2); + if (this._sock.rQshift16() !== clientKeyBytes) { + throw new Error("RA2: wrong encrypted message length"); + } + const serverEncryptedRandom = this._sock.rQshiftBytes(clientKeyBytes); + const serverRandom = clientRSACipher.decrypt(serverEncryptedRandom); + if (serverRandom === null || serverRandom.length !== 16) { + throw new Error("RA2: corrupted server encrypted random"); + } + + // 5: Compute session keys and set ciphers + let clientSessionKey = new Uint8Array(32); + let serverSessionKey = new Uint8Array(32); + clientSessionKey.set(serverRandom); + clientSessionKey.set(clientRandom, 16); + serverSessionKey.set(clientRandom); + serverSessionKey.set(serverRandom, 16); + clientSessionKey = await window.crypto.subtle.digest("SHA-1", clientSessionKey); + clientSessionKey = new Uint8Array(clientSessionKey).slice(0, 16); + serverSessionKey = await window.crypto.subtle.digest("SHA-1", serverSessionKey); + serverSessionKey = new Uint8Array(serverSessionKey).slice(0, 16); + const clientCipher = new RA2Cipher(); + await clientCipher.setKey(clientSessionKey); + const serverCipher = new RA2Cipher(); + await serverCipher.setKey(serverSessionKey); + + // 6: Compute and exchange hashes + let serverHash = new Uint8Array(8 + serverKeyBytes * 2 + clientKeyBytes * 2); + let clientHash = new Uint8Array(8 + serverKeyBytes * 2 + clientKeyBytes * 2); + serverHash.set(serverPublickey); + serverHash.set(clientPublicKey, 4 + serverKeyBytes * 2); + clientHash.set(clientPublicKey); + clientHash.set(serverPublickey, 4 + clientKeyBytes * 2); + serverHash = await window.crypto.subtle.digest("SHA-1", serverHash); + clientHash = await window.crypto.subtle.digest("SHA-1", clientHash); + serverHash = new Uint8Array(serverHash); + clientHash = new Uint8Array(clientHash); + this._sock.send(await clientCipher.makeMessage(clientHash)); + await this._waitSockAsync(2 + 20 + 16); + if (this._sock.rQshift16() !== 20) { + throw new Error("RA2: wrong server hash"); + } + const serverHashReceived = await serverCipher.receiveMessage( + 20, this._sock.rQshiftBytes(20), this._sock.rQshiftBytes(16)); + if (serverHashReceived === null) { + throw new Error("RA2: failed to authenticate the message"); + } + for (let i = 0; i < 20; i++) { + if (serverHashReceived[i] !== serverHash[i]) { + throw new Error("RA2: wrong server hash"); + } + } + + // 7: Receive subtype + await this._waitSockAsync(2 + 1 + 16); + if (this._sock.rQshift16() !== 1) { + throw new Error("RA2: wrong subtype"); + } + let subtype = (await serverCipher.receiveMessage( + 1, this._sock.rQshiftBytes(1), this._sock.rQshiftBytes(16))); + if (subtype === null) { + throw new Error("RA2: failed to authenticate the message"); + } + subtype = subtype[0]; + if (subtype === 1) { + if (this._getCredentials().username === undefined || + this._getCredentials().password === undefined) { + this.dispatchEvent(new CustomEvent( + "credentialsrequired", + { detail: { types: ["username", "password"] } })); + } + } else if (subtype === 2) { + if (this._getCredentials().password === undefined) { + this.dispatchEvent(new CustomEvent( + "credentialsrequired", + { detail: { types: ["password"] } })); + } + } else { + throw new Error("RA2: wrong subtype"); + } + await this._waitCredentialsAsync(subtype); + let username; + if (subtype === 1) { + username = encodeUTF8(this._getCredentials().username).slice(0, 255); + } else { + username = ""; + } + const password = encodeUTF8(this._getCredentials().password).slice(0, 255); + const credentials = new Uint8Array(username.length + password.length + 2); + credentials[0] = username.length; + credentials[username.length + 1] = password.length; + for (let i = 0; i < username.length; i++) { + credentials[i + 1] = username.charCodeAt(i); + } + for (let i = 0; i < password.length; i++) { + credentials[username.length + 2 + i] = password.charCodeAt(i); + } + this._sock.send(await clientCipher.makeMessage(credentials)); + } + + get hasStarted() { + return this._hasStarted; + } + + set hasStarted(s) { + this._hasStarted = s; + } +} \ No newline at end of file diff --git a/systemvm/agent/noVNC/core/rfb.js b/systemvm/agent/noVNC/core/rfb.js index 056e55f439a..f92255808e3 100644 --- a/systemvm/agent/noVNC/core/rfb.js +++ b/systemvm/agent/noVNC/core/rfb.js @@ -26,7 +26,8 @@ import KeyTable from "./input/keysym.js"; import USKeyTable from "./input/uskeysym.js"; import XtScancode from "./input/xtscancodes.js"; import { encodings } from "./encodings.js"; -import "./util/polyfill.js"; +import RSAAESAuthenticationState from "./ra2.js"; +import { MD5 } from "./util/md5.js"; import RawDecoder from "./decoders/raw.js"; import CopyRectDecoder from "./decoders/copyrect.js"; @@ -34,6 +35,8 @@ import RREDecoder from "./decoders/rre.js"; import HextileDecoder from "./decoders/hextile.js"; import TightDecoder from "./decoders/tight.js"; import TightPNGDecoder from "./decoders/tightpng.js"; +import ZRLEDecoder from "./decoders/zrle.js"; +import JPEGDecoder from "./decoders/jpeg.js"; // How many seconds to wait for a disconnect to finish const DISCONNECT_TIMEOUT = 3; @@ -52,6 +55,22 @@ const GESTURE_SCRLSENS = 50; const DOUBLE_TAP_TIMEOUT = 1000; const DOUBLE_TAP_THRESHOLD = 50; +// Security types +const securityTypeNone = 1; +const securityTypeVNCAuth = 2; +const securityTypeRA2ne = 6; +const securityTypeTight = 16; +const securityTypeVeNCrypt = 19; +const securityTypeXVP = 22; +const securityTypeARD = 30; +const securityTypeMSLogonII = 113; + +// Special Tight security types +const securityTypeUnixLogon = 129; + +// VeNCrypt security types +const securityTypePlain = 256; + // Extended clipboard pseudo-encoding formats const extendedClipboardFormatText = 1; /*eslint-disable no-unused-vars */ @@ -68,20 +87,31 @@ const extendedClipboardActionPeek = 1 << 26; const extendedClipboardActionNotify = 1 << 27; const extendedClipboardActionProvide = 1 << 28; - export default class RFB extends EventTargetMixin { - constructor(target, url, options) { + constructor(target, urlOrChannel, options) { if (!target) { throw new Error("Must specify target"); } - if (!url) { - throw new Error("Must specify URL"); + if (!urlOrChannel) { + throw new Error("Must specify URL, WebSocket or RTCDataChannel"); + } + + // We rely on modern APIs which might not be available in an + // insecure context + if (!window.isSecureContext) { + Log.Error("noVNC requires a secure context (TLS). Expect crashes!"); } super(); this._target = target; - this._url = url; + + if (typeof urlOrChannel === "string") { + this._url = urlOrChannel; + } else { + this._url = null; + this._rawChannel = urlOrChannel; + } // Connection details options = options || {}; @@ -95,6 +125,7 @@ export default class RFB extends EventTargetMixin { this._rfbInitState = ''; this._rfbAuthScheme = -1; this._rfbCleanDisconnect = true; + this._rfbRSAAESAuthenticationState = null; // Server capabilities this._rfbVersion = 0; @@ -131,6 +162,7 @@ export default class RFB extends EventTargetMixin { this._flushing = false; // Display flushing state this._keyboard = null; // Keyboard input handler object this._gestures = null; // Gesture input handler object + this._resizeObserver = null; // Resize observer object // Timers this._disconnTimer = null; // disconnection timer @@ -168,10 +200,12 @@ export default class RFB extends EventTargetMixin { // Bound event handlers this._eventHandlers = { focusCanvas: this._focusCanvas.bind(this), - windowResize: this._windowResize.bind(this), + handleResize: this._handleResize.bind(this), handleMouse: this._handleMouse.bind(this), handleWheel: this._handleWheel.bind(this), handleGesture: this._handleGesture.bind(this), + handleRSAAESCredentialsRequired: this._handleRSAAESCredentialsRequired.bind(this), + handleRSAAESServerVerification: this._handleRSAAESServerVerification.bind(this), }; // main setup @@ -188,8 +222,6 @@ export default class RFB extends EventTargetMixin { this._canvas.style.margin = 'auto'; // Some browsers add an outline on focus this._canvas.style.outline = 'none'; - // IE miscalculates width without this :( - this._canvas.style.flexShrink = '0'; this._canvas.width = 0; this._canvas.height = 0; this._canvas.tabIndex = -1; @@ -216,6 +248,8 @@ export default class RFB extends EventTargetMixin { this._decoders[encodings.encodingHextile] = new HextileDecoder(); this._decoders[encodings.encodingTight] = new TightDecoder(); this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder(); + this._decoders[encodings.encodingZRLE] = new ZRLEDecoder(); + this._decoders[encodings.encodingJPEG] = new JPEGDecoder(); // NB: nothing that needs explicit teardown should be done // before this point, since this can throw an exception @@ -233,58 +267,17 @@ export default class RFB extends EventTargetMixin { this._gestures = new GestureHandler(); this._sock = new Websock(); - this._sock.on('message', () => { - this._handleMessage(); - }); - this._sock.on('open', () => { - if ((this._rfbConnectionState === 'connecting') && - (this._rfbInitState === '')) { - this._rfbInitState = 'ProtocolVersion'; - Log.Debug("Starting VNC handshake"); - } else { - this._fail("Unexpected server connection while " + - this._rfbConnectionState); - } - }); - this._sock.on('close', (e) => { - Log.Debug("WebSocket on-close event"); - let msg = ""; - if (e.code) { - msg = "(code: " + e.code; - if (e.reason) { - msg += ", reason: " + e.reason; - } - msg += ")"; - } - switch (this._rfbConnectionState) { - case 'connecting': - this._fail("Connection closed " + msg); - break; - case 'connected': - // Handle disconnects that were initiated server-side - this._updateConnectionState('disconnecting'); - this._updateConnectionState('disconnected'); - break; - case 'disconnecting': - // Normal disconnection path - this._updateConnectionState('disconnected'); - break; - case 'disconnected': - this._fail("Unexpected server disconnect " + - "when already disconnected " + msg); - break; - default: - this._fail("Unexpected server disconnect before connecting " + - msg); - break; - } - this._sock.off('close'); - }); - this._sock.on('error', e => Log.Warn("WebSocket on-error event")); + this._sock.on('open', this._socketOpen.bind(this)); + this._sock.on('close', this._socketClose.bind(this)); + this._sock.on('message', this._handleMessage.bind(this)); + this._sock.on('error', this._socketError.bind(this)); - // Slight delay of the actual connection so that the caller has - // time to set up callbacks - setTimeout(this._updateConnectionState.bind(this, 'connecting')); + this._expectedClientWidth = null; + this._expectedClientHeight = null; + this._resizeObserver = new ResizeObserver(this._eventHandlers.handleResize); + + // All prepared, kick off the connection + this._updateConnectionState('connecting'); Log.Debug("<< RFB.constructor"); @@ -295,6 +288,7 @@ export default class RFB extends EventTargetMixin { this._viewOnly = false; this._clipViewport = false; + this._clippingViewport = false; this._scaleViewport = false; this._resizeSession = false; @@ -326,6 +320,16 @@ export default class RFB extends EventTargetMixin { get capabilities() { return this._capabilities; } + get clippingViewport() { return this._clippingViewport; } + _setClippingViewport(on) { + if (on === this._clippingViewport) { + return; + } + this._clippingViewport = on; + this.dispatchEvent(new CustomEvent("clippingviewport", + { detail: this._clippingViewport })); + } + get touchButton() { return 0; } set touchButton(button) { Log.Warn("Using old API!"); } @@ -413,11 +417,20 @@ export default class RFB extends EventTargetMixin { this._sock.off('error'); this._sock.off('message'); this._sock.off('open'); + if (this._rfbRSAAESAuthenticationState !== null) { + this._rfbRSAAESAuthenticationState.disconnect(); + } + } + + approveServer() { + if (this._rfbRSAAESAuthenticationState !== null) { + this._rfbRSAAESAuthenticationState.approveServer(); + } } sendCredentials(creds) { this._rfbCredentials = creds; - setTimeout(this._initMsg.bind(this), 0); + this._resumeAuthentication(); } sendText(text) { @@ -500,8 +513,8 @@ export default class RFB extends EventTargetMixin { } } - focus() { - this._canvas.focus(); + focus(options) { + this._canvas.focus(options); } blur() { @@ -517,31 +530,66 @@ export default class RFB extends EventTargetMixin { this._clipboardText = text; RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]); } else { - let data = new Uint8Array(text.length); - for (let i = 0; i < text.length; i++) { - // FIXME: text can have values outside of Latin1/Uint8 - data[i] = text.charCodeAt(i); + let length, i; + let data; + + length = 0; + // eslint-disable-next-line no-unused-vars + for (let codePoint of text) { + length++; + } + + data = new Uint8Array(length); + + i = 0; + for (let codePoint of text) { + let code = codePoint.codePointAt(0); + + /* Only ISO 8859-1 is supported */ + if (code > 0xff) { + code = 0x3f; // '?' + } + + data[i++] = code; } RFB.messages.clientCutText(this._sock, data); } } + getImageData() { + return this._display.getImageData(); + } + + toDataURL(type, encoderOptions) { + return this._display.toDataURL(type, encoderOptions); + } + + toBlob(callback, type, quality) { + return this._display.toBlob(callback, type, quality); + } + // ===== PRIVATE METHODS ===== _connect() { Log.Debug(">> RFB.connect"); - Log.Info("connecting to " + this._url); - - try { - // WebSocket.onopen transitions to the RFB init states + if (this._url) { + Log.Info(`connecting to ${this._url}`); this._sock.open(this._url, this._wsProtocols); - } catch (e) { - if (e.name === 'SyntaxError') { - this._fail("Invalid host or port (" + e + ")"); - } else { - this._fail("Error when opening socket (" + e + ")"); + } else { + Log.Info(`attaching ${this._rawChannel} to Websock`); + this._sock.attach(this._rawChannel); + + if (this._sock.readyState === 'closed') { + throw Error("Cannot use already closed WebSocket/RTCDataChannel"); + } + + if (this._sock.readyState === 'open') { + // FIXME: _socketOpen() can in theory call _fail(), which + // isn't allowed this early, but I'm not sure that can + // happen without a bug messing up our state variables + this._socketOpen(); } } @@ -553,9 +601,8 @@ export default class RFB extends EventTargetMixin { this._cursor.attach(this._canvas); this._refreshCursor(); - // Monitor size changes of the screen - // FIXME: Use ResizeObserver, or hidden overflow - window.addEventListener('resize', this._eventHandlers.windowResize); + // Monitor size changes of the screen element + this._resizeObserver.observe(this._screen); // Always grab focus on some kind of click event this._canvas.addEventListener("mousedown", this._eventHandlers.focusCanvas); @@ -596,7 +643,7 @@ export default class RFB extends EventTargetMixin { this._canvas.removeEventListener('contextmenu', this._eventHandlers.handleMouse); this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas); this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas); - window.removeEventListener('resize', this._eventHandlers.windowResize); + this._resizeObserver.disconnect(); this._keyboard.ungrab(); this._gestures.detach(); this._sock.close(); @@ -615,12 +662,64 @@ export default class RFB extends EventTargetMixin { Log.Debug("<< RFB.disconnect"); } + _socketOpen() { + if ((this._rfbConnectionState === 'connecting') && + (this._rfbInitState === '')) { + this._rfbInitState = 'ProtocolVersion'; + Log.Debug("Starting VNC handshake"); + } else { + this._fail("Unexpected server connection while " + + this._rfbConnectionState); + } + } + + _socketClose(e) { + Log.Debug("WebSocket on-close event"); + let msg = ""; + if (e.code) { + msg = "(code: " + e.code; + if (e.reason) { + msg += ", reason: " + e.reason; + } + msg += ")"; + } + switch (this._rfbConnectionState) { + case 'connecting': + this._fail("Connection closed " + msg); + break; + case 'connected': + // Handle disconnects that were initiated server-side + this._updateConnectionState('disconnecting'); + this._updateConnectionState('disconnected'); + break; + case 'disconnecting': + // Normal disconnection path + this._updateConnectionState('disconnected'); + break; + case 'disconnected': + this._fail("Unexpected server disconnect " + + "when already disconnected " + msg); + break; + default: + this._fail("Unexpected server disconnect before connecting " + + msg); + break; + } + this._sock.off('close'); + // Delete reference to raw channel to allow cleanup. + this._rawChannel = null; + } + + _socketError(e) { + Log.Warn("WebSocket on-error event"); + } + _focusCanvas(event) { if (!this.focusOnClick) { return; } - this.focus(); + this.focus({ preventScroll: true }); } _setDesktopName(name) { @@ -630,7 +729,26 @@ export default class RFB extends EventTargetMixin { { detail: { name: this._fbName } })); } - _windowResize(event) { + _saveExpectedClientSize() { + this._expectedClientWidth = this._screen.clientWidth; + this._expectedClientHeight = this._screen.clientHeight; + } + + _currentClientSize() { + return [this._screen.clientWidth, this._screen.clientHeight]; + } + + _clientHasExpectedSize() { + const [currentWidth, currentHeight] = this._currentClientSize(); + return currentWidth == this._expectedClientWidth && + currentHeight == this._expectedClientHeight; + } + + _handleResize() { + // Don't change anything if the client size is already as expected + if (this._clientHasExpectedSize()) { + return; + } // If the window resized then our screen element might have // as well. Update the viewport dimensions. window.requestAnimationFrame(() => { @@ -670,6 +788,16 @@ export default class RFB extends EventTargetMixin { const size = this._screenSize(); this._display.viewportChangeSize(size.w, size.h); this._fixScrollbars(); + this._setClippingViewport(size.w < this._display.width || + size.h < this._display.height); + } else { + this._setClippingViewport(false); + } + + // When changing clipping we might show or hide scrollbars. + // This causes the expected client dimensions to change. + if (curClip !== newClip) { + this._saveExpectedClientSize(); } } @@ -695,6 +823,7 @@ export default class RFB extends EventTargetMixin { } const size = this._screenSize(); + RFB.messages.setDesktopSize(this._sock, Math.floor(size.w), Math.floor(size.h), this._screenID, this._screenFlags); @@ -710,12 +839,13 @@ export default class RFB extends EventTargetMixin { } _fixScrollbars() { - // This is a hack because Chrome screws up the calculation - // for when scrollbars are needed. So to fix it we temporarily - // toggle them off and on. + // This is a hack because Safari on macOS screws up the calculation + // for when scrollbars are needed. We get scrollbars when making the + // browser smaller, despite remote resize being enabled. So to fix it + // we temporarily toggle them off and on. const orig = this._screen.style.overflow; this._screen.style.overflow = 'hidden'; - // Force Chrome to recalculate the layout by asking for + // Force Safari to recalculate the layout by asking for // an element's dimensions this._screen.getBoundingClientRect(); this._screen.style.overflow = orig; @@ -880,8 +1010,15 @@ export default class RFB extends EventTargetMixin { } } break; + case 'connecting': + while (this._rfbConnectionState === 'connecting') { + if (!this._initMsg()) { + break; + } + } + break; default: - this._initMsg(); + Log.Error("Got data while in an invalid state"); break; } } @@ -1253,13 +1390,13 @@ export default class RFB extends EventTargetMixin { break; case "003.003": case "003.006": // UltraVNC - case "003.889": // Apple Remote Desktop this._rfbVersion = 3.3; break; case "003.007": this._rfbVersion = 3.7; break; case "003.008": + case "003.889": // Apple Remote Desktop case "004.000": // Intel AMT KVM case "004.001": // RealVNC 4.6 case "005.000": // RealVNC 5.3 @@ -1290,18 +1427,23 @@ export default class RFB extends EventTargetMixin { this._rfbInitState = 'Security'; } - _negotiateSecurity() { - // Polyfill since IE and PhantomJS doesn't have - // TypedArray.includes() - function includes(item, array) { - for (let i = 0; i < array.length; i++) { - if (array[i] === item) { - return true; - } - } - return false; - } + _isSupportedSecurityType(type) { + const clientTypes = [ + securityTypeNone, + securityTypeVNCAuth, + securityTypeRA2ne, + securityTypeTight, + securityTypeVeNCrypt, + securityTypeXVP, + securityTypeARD, + securityTypeMSLogonII, + securityTypePlain, + ]; + return clientTypes.includes(type); + } + + _negotiateSecurity() { if (this._rfbVersion >= 3.7) { // Server sends supported list, client decides const numTypes = this._sock.rQshift8(); @@ -1311,24 +1453,23 @@ export default class RFB extends EventTargetMixin { this._rfbInitState = "SecurityReason"; this._securityContext = "no security types"; this._securityStatus = 1; - return this._initMsg(); + return true; } const types = this._sock.rQshiftBytes(numTypes); Log.Debug("Server security types: " + types); - // Look for each auth in preferred order - if (includes(1, types)) { - this._rfbAuthScheme = 1; // None - } else if (includes(22, types)) { - this._rfbAuthScheme = 22; // XVP - } else if (includes(16, types)) { - this._rfbAuthScheme = 16; // Tight - } else if (includes(2, types)) { - this._rfbAuthScheme = 2; // VNC Auth - } else if (includes(19, types)) { - this._rfbAuthScheme = 19; // VeNCrypt Auth - } else { + // Look for a matching security type in the order that the + // server prefers + this._rfbAuthScheme = -1; + for (let type of types) { + if (this._isSupportedSecurityType(type)) { + this._rfbAuthScheme = type; + break; + } + } + + if (this._rfbAuthScheme === -1) { return this._fail("Unsupported security types (types: " + types + ")"); } @@ -1342,14 +1483,14 @@ export default class RFB extends EventTargetMixin { this._rfbInitState = "SecurityReason"; this._securityContext = "authentication scheme"; this._securityStatus = 1; - return this._initMsg(); + return true; } } this._rfbInitState = 'Authentication'; Log.Debug('Authenticating using scheme: ' + this._rfbAuthScheme); - return this._initMsg(); // jump to authentication + return true; } _handleSecurityReason() { @@ -1399,7 +1540,7 @@ export default class RFB extends EventTargetMixin { this._rfbCredentials.username + this._rfbCredentials.target; this._sock.sendString(xvpAuthStr); - this._rfbAuthScheme = 2; + this._rfbAuthScheme = securityTypeVNCAuth; return this._negotiateAuthentication(); } @@ -1457,40 +1598,66 @@ export default class RFB extends EventTargetMixin { subtypes.push(this._sock.rQshift32()); } - // 256 = Plain subtype - if (subtypes.indexOf(256) != -1) { - // 0x100 = 256 - this._sock.send([0, 0, 1, 0]); - this._rfbVeNCryptState = 4; - } else { - return this._fail("VeNCrypt Plain subtype not offered by server"); - } - } + // Look for a matching security type in the order that the + // server prefers + this._rfbAuthScheme = -1; + for (let type of subtypes) { + // Avoid getting in to a loop + if (type === securityTypeVeNCrypt) { + continue; + } - // negotiated Plain subtype, server waits for password - if (this._rfbVeNCryptState == 4) { - if (!this._rfbCredentials.username || - !this._rfbCredentials.password) { - this.dispatchEvent(new CustomEvent( - "credentialsrequired", - { detail: { types: ["username", "password"] } })); - return false; + if (this._isSupportedSecurityType(type)) { + this._rfbAuthScheme = type; + break; + } } - const user = encodeUTF8(this._rfbCredentials.username); - const pass = encodeUTF8(this._rfbCredentials.password); + if (this._rfbAuthScheme === -1) { + return this._fail("Unsupported security types (types: " + subtypes + ")"); + } - // XXX we assume lengths are <= 255 (should not be an issue in the real world) - this._sock.send([0, 0, 0, user.length]); - this._sock.send([0, 0, 0, pass.length]); - this._sock.sendString(user); - this._sock.sendString(pass); + this._sock.send([this._rfbAuthScheme >> 24, + this._rfbAuthScheme >> 16, + this._rfbAuthScheme >> 8, + this._rfbAuthScheme]); - this._rfbInitState = "SecurityResult"; + this._rfbVeNCryptState == 4; return true; } } + _negotiatePlainAuth() { + if (this._rfbCredentials.username === undefined || + this._rfbCredentials.password === undefined) { + this.dispatchEvent(new CustomEvent( + "credentialsrequired", + { detail: { types: ["username", "password"] } })); + return false; + } + + const user = encodeUTF8(this._rfbCredentials.username); + const pass = encodeUTF8(this._rfbCredentials.password); + + this._sock.send([ + (user.length >> 24) & 0xFF, + (user.length >> 16) & 0xFF, + (user.length >> 8) & 0xFF, + user.length & 0xFF + ]); + this._sock.send([ + (pass.length >> 24) & 0xFF, + (pass.length >> 16) & 0xFF, + (pass.length >> 8) & 0xFF, + pass.length & 0xFF + ]); + this._sock.sendString(user); + this._sock.sendString(pass); + + this._rfbInitState = "SecurityResult"; + return true; + } + _negotiateStdVNCAuth() { if (this._sock.rQwait("auth challenge", 16)) { return false; } @@ -1509,6 +1676,117 @@ export default class RFB extends EventTargetMixin { return true; } + _negotiateARDAuth() { + + if (this._rfbCredentials.username === undefined || + this._rfbCredentials.password === undefined) { + this.dispatchEvent(new CustomEvent( + "credentialsrequired", + { detail: { types: ["username", "password"] } })); + return false; + } + + if (this._rfbCredentials.ardPublicKey != undefined && + this._rfbCredentials.ardCredentials != undefined) { + // if the async web crypto is done return the results + this._sock.send(this._rfbCredentials.ardCredentials); + this._sock.send(this._rfbCredentials.ardPublicKey); + this._rfbCredentials.ardCredentials = null; + this._rfbCredentials.ardPublicKey = null; + this._rfbInitState = "SecurityResult"; + return true; + } + + if (this._sock.rQwait("read ard", 4)) { return false; } + + let generator = this._sock.rQshiftBytes(2); // DH base generator value + + let keyLength = this._sock.rQshift16(); + + if (this._sock.rQwait("read ard keylength", keyLength*2, 4)) { return false; } + + // read the server values + let prime = this._sock.rQshiftBytes(keyLength); // predetermined prime modulus + let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key + + let clientPrivateKey = window.crypto.getRandomValues(new Uint8Array(keyLength)); + let padding = Array.from(window.crypto.getRandomValues(new Uint8Array(64)), byte => String.fromCharCode(65+byte%26)).join(''); + + this._negotiateARDAuthAsync(generator, keyLength, prime, serverPublicKey, clientPrivateKey, padding); + + return false; + } + + _modPow(base, exponent, modulus) { + + let baseHex = "0x"+Array.from(base, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join(''); + let exponentHex = "0x"+Array.from(exponent, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join(''); + let modulusHex = "0x"+Array.from(modulus, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join(''); + + let b = BigInt(baseHex); + let e = BigInt(exponentHex); + let m = BigInt(modulusHex); + let r = 1n; + b = b % m; + while (e > 0) { + if (e % 2n === 1n) { + r = (r * b) % m; + } + e = e / 2n; + b = (b * b) % m; + } + let hexResult = r.toString(16); + + while (hexResult.length/2 String.fromCharCode(byte)).join(''); + let aesKey = await window.crypto.subtle.importKey("raw", MD5(keyString), {name: "AES-CBC"}, false, ["encrypt"]); + let data = new Uint8Array(string.length); + for (let i = 0; i < string.length; ++i) { + data[i] = string.charCodeAt(i); + } + let encrypted = new Uint8Array(data.length); + for (let i=0;i this._rfbCredentials); + this._rfbRSAAESAuthenticationState.addEventListener( + "serververification", this._eventHandlers.handleRSAAESServerVerification); + this._rfbRSAAESAuthenticationState.addEventListener( + "credentialsrequired", this._eventHandlers.handleRSAAESCredentialsRequired); + } + this._rfbRSAAESAuthenticationState.checkInternalEvents(); + if (!this._rfbRSAAESAuthenticationState.hasStarted) { + this._rfbRSAAESAuthenticationState.negotiateRA2neAuthAsync() + .catch((e) => { + if (e.message !== "disconnect normally") { + this._fail(e.message); + } + }).then(() => { + this.dispatchEvent(new CustomEvent('securityresult')); + this._rfbInitState = "SecurityResult"; + return true; + }).finally(() => { + this._rfbRSAAESAuthenticationState.removeEventListener( + "serververification", this._eventHandlers.handleRSAAESServerVerification); + this._rfbRSAAESAuthenticationState.removeEventListener( + "credentialsrequired", this._eventHandlers.handleRSAAESCredentialsRequired); + this._rfbRSAAESAuthenticationState = null; + }); + } + return false; + } + + _negotiateMSLogonIIAuth() { + if (this._sock.rQwait("mslogonii dh param", 24)) { return false; } + + if (this._rfbCredentials.username === undefined || + this._rfbCredentials.password === undefined) { + this.dispatchEvent(new CustomEvent( + "credentialsrequired", + { detail: { types: ["username", "password"] } })); + return false; + } + + const g = this._sock.rQshiftBytes(8); + const p = this._sock.rQshiftBytes(8); + const A = this._sock.rQshiftBytes(8); + const b = window.crypto.getRandomValues(new Uint8Array(8)); + const B = new Uint8Array(this._modPow(g, b, p)); + const secret = new Uint8Array(this._modPow(A, b, p)); + + const des = new DES(secret); + const username = encodeUTF8(this._rfbCredentials.username).substring(0, 255); + const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63); + const usernameBytes = new Uint8Array(256); + const passwordBytes = new Uint8Array(64); + window.crypto.getRandomValues(usernameBytes); + window.crypto.getRandomValues(passwordBytes); + for (let i = 0; i < username.length; i++) { + usernameBytes[i] = username.charCodeAt(i); + } + usernameBytes[username.length] = 0; + for (let i = 0; i < password.length; i++) { + passwordBytes[i] = password.charCodeAt(i); + } + passwordBytes[password.length] = 0; + let x = new Uint8Array(secret); + for (let i = 0; i < 32; i++) { + for (let j = 0; j < 8; j++) { + x[j] ^= usernameBytes[i * 8 + j]; + } + x = des.enc8(x); + usernameBytes.set(x, i * 8); + } + x = new Uint8Array(secret); + for (let i = 0; i < 8; i++) { + for (let j = 0; j < 8; j++) { + x[j] ^= passwordBytes[i * 8 + j]; + } + x = des.enc8(x); + passwordBytes.set(x, i * 8); + } + this._sock.send(B); + this._sock.send(usernameBytes); + this._sock.send(passwordBytes); + this._rfbInitState = "SecurityResult"; + return true; + } + _negotiateAuthentication() { switch (this._rfbAuthScheme) { // Let CloudStack handle the authentication (RFB 3.8 requires the client to select the auth scheme) - case 1: // no auth - case 2: // VNC authentication - case 19: // VeNCrypt Security Type - if (this._rfbVersion >= 3.8) { - this._rfbInitState = 'SecurityResult'; - return true; - } - this._rfbInitState = 'ClientInitialisation'; - return this._initMsg(); + case securityTypeNone: + case securityTypeVNCAuth: + case securityTypeVeNCrypt: + this._rfbInitState = 'SecurityResult'; + return true; - case 22: // XVP auth + case securityTypeXVP: return this._negotiateXvpAuth(); - case 16: // TightVNC Security Type + case securityTypeARD: + return this._negotiateARDAuth(); + + case securityTypeTight: return this._negotiateTightAuth(); - case 129: // TightVNC UNIX Security Type + case securityTypePlain: + return this._negotiatePlainAuth(); + + case securityTypeUnixLogon: return this._negotiateTightUnixAuth(); + case securityTypeRA2ne: + return this._negotiateRA2neAuth(); + + case securityTypeMSLogonII: + return this._negotiateMSLogonIIAuth(); + default: return this._fail("Unsupported auth scheme (scheme: " + this._rfbAuthScheme + ")"); @@ -1661,6 +2041,13 @@ export default class RFB extends EventTargetMixin { } _handleSecurityResult() { + // There is no security choice, and hence no security result + // until RFB 3.7 + if (this._rfbVersion < 3.7) { + this._rfbInitState = 'ClientInitialisation'; + return true; + } + if (this._sock.rQwait('VNC auth response ', 4)) { return false; } const status = this._sock.rQshift32(); @@ -1668,13 +2055,13 @@ export default class RFB extends EventTargetMixin { if (status === 0) { // OK this._rfbInitState = 'ClientInitialisation'; Log.Debug('Authentication OK'); - return this._initMsg(); + return true; } else { if (this._rfbVersion >= 3.8) { this._rfbInitState = "SecurityReason"; this._securityContext = "security result"; this._securityStatus = status; - return this._initMsg(); + return true; } else { this.dispatchEvent(new CustomEvent( "securityfailure", @@ -1782,6 +2169,8 @@ export default class RFB extends EventTargetMixin { if (this._fbDepth == 24) { encs.push(encodings.encodingTight); encs.push(encodings.encodingTightPNG); + encs.push(encodings.encodingZRLE); + encs.push(encodings.encodingJPEG); encs.push(encodings.encodingHextile); encs.push(encodings.encodingRRE); } @@ -1848,6 +2237,14 @@ export default class RFB extends EventTargetMixin { } } + // Resume authentication handshake after it was paused for some + // reason, e.g. waiting for a password from the user + _resumeAuthentication() { + // We use setTimeout() so it's run in its own context, just like + // it originally did via the WebSocket's event handler + setTimeout(this._initMsg.bind(this), 0); + } + _handleSetColourMapMsg() { Log.Debug("SetColorMapEntries"); @@ -2208,15 +2605,7 @@ export default class RFB extends EventTargetMixin { return this._handleCursor(); case encodings.pseudoEncodingQEMUExtendedKeyEvent: - // Old Safari doesn't support creating keyboard events - try { - const keyboardEvent = document.createEvent("keyboardEvent"); - if (keyboardEvent.code !== undefined) { - this._qemuExtKeyEventSupported = true; - } - } catch (err) { - // Do nothing - } + this._qemuExtKeyEventSupported = true; return true; case encodings.pseudoEncodingDesktopName: @@ -2518,6 +2907,9 @@ export default class RFB extends EventTargetMixin { this._updateScale(); this._updateContinuousUpdates(); + + // Keep this size until browser client size changes + this._saveExpectedClientSize(); } _xvpOp(ver, op) { diff --git a/systemvm/agent/noVNC/core/util/browser.js b/systemvm/agent/noVNC/core/util/browser.js index 15548014229..bbc9f5c1ebe 100644 --- a/systemvm/agent/noVNC/core/util/browser.js +++ b/systemvm/agent/noVNC/core/util/browser.js @@ -45,15 +45,6 @@ try { export const supportsCursorURIs = _supportsCursorURIs; -let _supportsImageMetadata = false; -try { - new ImageData(new Uint8ClampedArray(4), 1, 1); - _supportsImageMetadata = true; -} catch (ex) { - // ignore failure -} -export const supportsImageMetadata = _supportsImageMetadata; - let _hasScrollbarGutter = true; try { // Create invisible container @@ -86,35 +77,76 @@ export const hasScrollbarGutter = _hasScrollbarGutter; * It's better to use feature detection than platform detection. */ +/* OS */ + export function isMac() { - return navigator && !!(/mac/i).exec(navigator.platform); + return !!(/mac/i).exec(navigator.platform); } export function isWindows() { - return navigator && !!(/win/i).exec(navigator.platform); + return !!(/win/i).exec(navigator.platform); } export function isIOS() { - return navigator && - (!!(/ipad/i).exec(navigator.platform) || + return (!!(/ipad/i).exec(navigator.platform) || !!(/iphone/i).exec(navigator.platform) || !!(/ipod/i).exec(navigator.platform)); } +export function isAndroid() { + /* Android sets navigator.platform to Linux :/ */ + return !!navigator.userAgent.match('Android '); +} + +export function isChromeOS() { + /* ChromeOS sets navigator.platform to Linux :/ */ + return !!navigator.userAgent.match(' CrOS '); +} + +/* Browser */ + export function isSafari() { - return navigator && (navigator.userAgent.indexOf('Safari') !== -1 && - navigator.userAgent.indexOf('Chrome') === -1); -} - -export function isIE() { - return navigator && !!(/trident/i).exec(navigator.userAgent); -} - -export function isEdge() { - return navigator && !!(/edge/i).exec(navigator.userAgent); + return !!navigator.userAgent.match('Safari/...') && + !navigator.userAgent.match('Chrome/...') && + !navigator.userAgent.match('Chromium/...') && + !navigator.userAgent.match('Epiphany/...'); } export function isFirefox() { - return navigator && !!(/firefox/i).exec(navigator.userAgent); + return !!navigator.userAgent.match('Firefox/...') && + !navigator.userAgent.match('Seamonkey/...'); } +export function isChrome() { + return !!navigator.userAgent.match('Chrome/...') && + !navigator.userAgent.match('Chromium/...') && + !navigator.userAgent.match('Edg/...') && + !navigator.userAgent.match('OPR/...'); +} + +export function isChromium() { + return !!navigator.userAgent.match('Chromium/...'); +} + +export function isOpera() { + return !!navigator.userAgent.match('OPR/...'); +} + +export function isEdge() { + return !!navigator.userAgent.match('Edg/...'); +} + +/* Engine */ + +export function isGecko() { + return !!navigator.userAgent.match('Gecko/...'); +} + +export function isWebKit() { + return !!navigator.userAgent.match('AppleWebKit/...') && + !navigator.userAgent.match('Chrome/...'); +} + +export function isBlink() { + return !!navigator.userAgent.match('Chrome/...'); +} diff --git a/systemvm/agent/noVNC/core/util/cursor.js b/systemvm/agent/noVNC/core/util/cursor.js index 4db1dab23fb..3000cf0e6b0 100644 --- a/systemvm/agent/noVNC/core/util/cursor.js +++ b/systemvm/agent/noVNC/core/util/cursor.js @@ -18,6 +18,10 @@ export default class Cursor { this._canvas.style.position = 'fixed'; this._canvas.style.zIndex = '65535'; this._canvas.style.pointerEvents = 'none'; + // Safari on iOS can select the cursor image + // https://bugs.webkit.org/show_bug.cgi?id=249223 + this._canvas.style.userSelect = 'none'; + this._canvas.style.WebkitUserSelect = 'none'; // Can't use "display" because of Firefox bug #1445997 this._canvas.style.visibility = 'hidden'; } @@ -43,9 +47,6 @@ export default class Cursor { if (useFallback) { document.body.appendChild(this._canvas); - // FIXME: These don't fire properly except for mouse - /// movement in IE. We want to also capture element - // movement, size changes, visibility, etc. const options = { capture: true, passive: true }; this._target.addEventListener('mouseover', this._eventHandlers.mouseover, options); this._target.addEventListener('mouseleave', this._eventHandlers.mouseleave, options); @@ -90,14 +91,7 @@ export default class Cursor { this._canvas.width = w; this._canvas.height = h; - let img; - try { - // IE doesn't support this - img = new ImageData(new Uint8ClampedArray(rgba), w, h); - } catch (ex) { - img = ctx.createImageData(w, h); - img.data.set(new Uint8ClampedArray(rgba)); - } + let img = new ImageData(new Uint8ClampedArray(rgba), w, h); ctx.clearRect(0, 0, w, h); ctx.putImageData(img, 0, 0); diff --git a/systemvm/agent/noVNC/core/util/events.js b/systemvm/agent/noVNC/core/util/events.js index 39eefd4596c..eb09fe1e29b 100644 --- a/systemvm/agent/noVNC/core/util/events.js +++ b/systemvm/agent/noVNC/core/util/events.js @@ -65,10 +65,6 @@ export function setCapture(target) { target.setCapture(); document.captureElement = target; - - // IE releases capture on 'click' events which might not trigger - target.addEventListener('mouseup', releaseCapture); - } else { // Release any existing capture in case this method is // called multiple times without coordination diff --git a/systemvm/agent/noVNC/core/util/md5.js b/systemvm/agent/noVNC/core/util/md5.js new file mode 100644 index 00000000000..49762ef9e6b --- /dev/null +++ b/systemvm/agent/noVNC/core/util/md5.js @@ -0,0 +1,79 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2021 The noVNC Authors + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +/* + * Performs MD5 hashing on a string of binary characters, returns an array of bytes + */ + +export function MD5(d) { + let r = M(V(Y(X(d), 8 * d.length))); + return r; +} + +function M(d) { + let f = new Uint8Array(d.length); + for (let i=0;i> 2); + for (let m = 0; m < r.length; m++) r[m] = 0; + for (let m = 0; m < 8 * d.length; m += 8) r[m >> 5] |= (255 & d.charCodeAt(m / 8)) << m % 32; + return r; +} + +function V(d) { + let r = ""; + for (let m = 0; m < 32 * d.length; m += 8) r += String.fromCharCode(d[m >> 5] >>> m % 32 & 255); + return r; +} + +function Y(d, g) { + d[g >> 5] |= 128 << g % 32, d[14 + (g + 64 >>> 9 << 4)] = g; + let m = 1732584193, f = -271733879, r = -1732584194, i = 271733878; + for (let n = 0; n < d.length; n += 16) { + let h = m, + t = f, + g = r, + e = i; + f = ii(f = ii(f = ii(f = ii(f = hh(f = hh(f = hh(f = hh(f = gg(f = gg(f = gg(f = gg(f = ff(f = ff(f = ff(f = ff(f, r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 0], 7, -680876936), f, r, d[n + 1], 12, -389564586), m, f, d[n + 2], 17, 606105819), i, m, d[n + 3], 22, -1044525330), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 4], 7, -176418897), f, r, d[n + 5], 12, 1200080426), m, f, d[n + 6], 17, -1473231341), i, m, d[n + 7], 22, -45705983), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 8], 7, 1770035416), f, r, d[n + 9], 12, -1958414417), m, f, d[n + 10], 17, -42063), i, m, d[n + 11], 22, -1990404162), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 12], 7, 1804603682), f, r, d[n + 13], 12, -40341101), m, f, d[n + 14], 17, -1502002290), i, m, d[n + 15], 22, 1236535329), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 1], 5, -165796510), f, r, d[n + 6], 9, -1069501632), m, f, d[n + 11], 14, 643717713), i, m, d[n + 0], 20, -373897302), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 5], 5, -701558691), f, r, d[n + 10], 9, 38016083), m, f, d[n + 15], 14, -660478335), i, m, d[n + 4], 20, -405537848), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 9], 5, 568446438), f, r, d[n + 14], 9, -1019803690), m, f, d[n + 3], 14, -187363961), i, m, d[n + 8], 20, 1163531501), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 13], 5, -1444681467), f, r, d[n + 2], 9, -51403784), m, f, d[n + 7], 14, 1735328473), i, m, d[n + 12], 20, -1926607734), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 5], 4, -378558), f, r, d[n + 8], 11, -2022574463), m, f, d[n + 11], 16, 1839030562), i, m, d[n + 14], 23, -35309556), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 1], 4, -1530992060), f, r, d[n + 4], 11, 1272893353), m, f, d[n + 7], 16, -155497632), i, m, d[n + 10], 23, -1094730640), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 13], 4, 681279174), f, r, d[n + 0], 11, -358537222), m, f, d[n + 3], 16, -722521979), i, m, d[n + 6], 23, 76029189), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 9], 4, -640364487), f, r, d[n + 12], 11, -421815835), m, f, d[n + 15], 16, 530742520), i, m, d[n + 2], 23, -995338651), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 0], 6, -198630844), f, r, d[n + 7], 10, 1126891415), m, f, d[n + 14], 15, -1416354905), i, m, d[n + 5], 21, -57434055), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 12], 6, 1700485571), f, r, d[n + 3], 10, -1894986606), m, f, d[n + 10], 15, -1051523), i, m, d[n + 1], 21, -2054922799), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 8], 6, 1873313359), f, r, d[n + 15], 10, -30611744), m, f, d[n + 6], 15, -1560198380), i, m, d[n + 13], 21, 1309151649), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 4], 6, -145523070), f, r, d[n + 11], 10, -1120210379), m, f, d[n + 2], 15, 718787259), i, m, d[n + 9], 21, -343485551), m = add(m, h), f = add(f, t), r = add(r, g), i = add(i, e); + } + return Array(m, f, r, i); +} + +function cmn(d, g, m, f, r, i) { + return add(rol(add(add(g, d), add(f, i)), r), m); +} + +function ff(d, g, m, f, r, i, n) { + return cmn(g & m | ~g & f, d, g, r, i, n); +} + +function gg(d, g, m, f, r, i, n) { + return cmn(g & f | m & ~f, d, g, r, i, n); +} + +function hh(d, g, m, f, r, i, n) { + return cmn(g ^ m ^ f, d, g, r, i, n); +} + +function ii(d, g, m, f, r, i, n) { + return cmn(m ^ (g | ~f), d, g, r, i, n); +} + +function add(d, g) { + let m = (65535 & d) + (65535 & g); + return (d >> 16) + (g >> 16) + (m >> 16) << 16 | 65535 & m; +} + +function rol(d, g) { + return d << g | d >>> 32 - g; +} \ No newline at end of file diff --git a/systemvm/agent/noVNC/core/util/polyfill.js b/systemvm/agent/noVNC/core/util/polyfill.js deleted file mode 100644 index 0e458c8606b..00000000000 --- a/systemvm/agent/noVNC/core/util/polyfill.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * noVNC: HTML5 VNC client - * Copyright (C) 2020 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; - } -})(); - -/* Number.isInteger() (taken from MDN) */ -Number.isInteger = Number.isInteger || function isInteger(value) { - return typeof value === 'number' && - isFinite(value) && - Math.floor(value) === value; -}; diff --git a/systemvm/agent/noVNC/core/websock.js b/systemvm/agent/noVNC/core/websock.js index 3156aed6f5e..37b33fcc585 100644 --- a/systemvm/agent/noVNC/core/websock.js +++ b/systemvm/agent/noVNC/core/websock.js @@ -1,10 +1,10 @@ /* - * Websock: high-performance binary WebSockets + * Websock: high-performance buffering wrapper * Copyright (C) 2019 The noVNC Authors * Licensed under MPL 2.0 (see LICENSE.txt) * - * Websock is similar to the standard WebSocket object but with extra - * buffer handling. + * Websock is similar to the standard WebSocket / RTCDataChannel object + * but with extra buffer handling. * * Websock has built-in receive queue buffering; the message event * does not contain actual data but is simply a notification that @@ -17,14 +17,39 @@ import * as Log from './util/logging.js'; // this has performance issues in some versions Chromium, and // doesn't gain a tremendous amount of performance increase in Firefox // at the moment. It may be valuable to turn it on in the future. -// Also copyWithin() for TypedArrays is not supported in IE 11 or -// Safari 13 (at the moment we want to support Safari 11). -const ENABLE_COPYWITHIN = false; const MAX_RQ_GROW_SIZE = 40 * 1024 * 1024; // 40 MiB +// Constants pulled from RTCDataChannelState enum +// https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/readyState#RTCDataChannelState_enum +const DataChannel = { + CONNECTING: "connecting", + OPEN: "open", + CLOSING: "closing", + CLOSED: "closed" +}; + +const ReadyStates = { + CONNECTING: [WebSocket.CONNECTING, DataChannel.CONNECTING], + OPEN: [WebSocket.OPEN, DataChannel.OPEN], + CLOSING: [WebSocket.CLOSING, DataChannel.CLOSING], + CLOSED: [WebSocket.CLOSED, DataChannel.CLOSED], +}; + +// Properties a raw channel must have, WebSocket and RTCDataChannel are two examples +const rawChannelProps = [ + "send", + "close", + "binaryType", + "onerror", + "onmessage", + "onopen", + "protocol", + "readyState", +]; + export default class Websock { constructor() { - this._websocket = null; // WebSocket object + this._websocket = null; // WebSocket or RTCDataChannel object this._rQi = 0; // Receive queue index this._rQlen = 0; // Next write position in the receive queue @@ -46,6 +71,29 @@ export default class Websock { } // Getters and Setters + + get readyState() { + let subState; + + if (this._websocket === null) { + return "unused"; + } + + subState = this._websocket.readyState; + + if (ReadyStates.CONNECTING.includes(subState)) { + return "connecting"; + } else if (ReadyStates.OPEN.includes(subState)) { + return "open"; + } else if (ReadyStates.CLOSING.includes(subState)) { + return "closing"; + } else if (ReadyStates.CLOSED.includes(subState)) { + return "closed"; + } + + return "unknown"; + } + get sQ() { return this._sQ; } @@ -143,7 +191,7 @@ export default class Websock { // Send Queue flush() { - if (this._sQlen > 0 && this._websocket.readyState === WebSocket.OPEN) { + if (this._sQlen > 0 && this.readyState === 'open') { this._websocket.send(this._encodeMessage()); this._sQlen = 0; } @@ -180,12 +228,25 @@ export default class Websock { } open(uri, protocols) { + this.attach(new WebSocket(uri, protocols)); + } + + attach(rawChannel) { this.init(); - this._websocket = new WebSocket(uri, protocols); - this._websocket.binaryType = 'arraybuffer'; + // Must get object and class methods to be compatible with the tests. + const channelProps = [...Object.keys(rawChannel), ...Object.getOwnPropertyNames(Object.getPrototypeOf(rawChannel))]; + for (let i = 0; i < rawChannelProps.length; i++) { + const prop = rawChannelProps[i]; + if (channelProps.indexOf(prop) < 0) { + throw new Error('Raw channel missing property: ' + prop); + } + } + this._websocket = rawChannel; + this._websocket.binaryType = "arraybuffer"; this._websocket.onmessage = this._recvMessage.bind(this); + this._websocket.onopen = () => { Log.Debug('>> WebSock.onopen'); if (this._websocket.protocol) { @@ -195,11 +256,13 @@ export default class Websock { this._eventHandlers.open(); Log.Debug("<< WebSock.onopen"); }; + this._websocket.onclose = (e) => { Log.Debug(">> WebSock.onclose"); this._eventHandlers.close(e); Log.Debug("<< WebSock.onclose"); }; + this._websocket.onerror = (e) => { Log.Debug(">> WebSock.onerror: " + e); this._eventHandlers.error(e); @@ -209,8 +272,8 @@ export default class Websock { close() { if (this._websocket) { - if ((this._websocket.readyState === WebSocket.OPEN) || - (this._websocket.readyState === WebSocket.CONNECTING)) { + if (this.readyState === 'connecting' || + this.readyState === 'open') { Log.Info("Closing WebSocket connection"); this._websocket.close(); } @@ -256,11 +319,7 @@ export default class Websock { this._rQ = new Uint8Array(this._rQbufferSize); this._rQ.set(new Uint8Array(oldRQbuffer, this._rQi, this._rQlen - this._rQi)); } else { - if (ENABLE_COPYWITHIN) { - this._rQ.copyWithin(0, this._rQi, this._rQlen); - } else { - this._rQ.set(new Uint8Array(this._rQ.buffer, this._rQi, this._rQlen - this._rQi)); - } + this._rQ.copyWithin(0, this._rQi, this._rQlen); } this._rQlen = this._rQlen - this._rQi; diff --git a/systemvm/agent/noVNC/docs/novnc_proxy.1 b/systemvm/agent/noVNC/docs/novnc_proxy.1 new file mode 100644 index 00000000000..11a003b3aa5 --- /dev/null +++ b/systemvm/agent/noVNC/docs/novnc_proxy.1 @@ -0,0 +1,37 @@ +.TH novnc_proxy 1 "June 25, 2020" "version 1.2.0" "USER COMMANDS" + +.SH NAME +novnc_proxy - noVNC proxy server +.SH SYNOPSIS +.B novnc_proxy [--listen PORT] [--vnc VNC_HOST:PORT] [--cert CERT] [--ssl-only] + +Starts the WebSockets proxy and a mini-webserver and +provides a cut-and-paste URL to go to. + + --listen PORT Port for proxy/webserver to listen on + Default: 6080 + --vnc VNC_HOST:PORT VNC server host:port proxy target + Default: localhost:5900 + --cert CERT Path to combined cert/key file, or just + the cert file if used with --key + Default: self.pem + --key KEY Path to key file, when not combined with cert + --web WEB Path to web files (e.g. vnc.html) + Default: ./ + --ssl-only Disable non-https connections. + + --record FILE Record traffic to FILE.session.js + + --syslog SERVER Can be local socket such as /dev/log, or a UDP host:port pair. + + --heartbeat SEC send a ping to the client every SEC seconds + --timeout SEC after SEC seconds exit when not connected + --idle-timeout SEC server exits after SEC seconds if there are no + active connections + +.SH AUTHOR +The noVNC Authors +https://github.com/novnc/noVNC + +.SH SEE ALSO +websockify(1), nova-novncproxy(1) diff --git a/systemvm/agent/noVNC/package.json b/systemvm/agent/noVNC/package.json index 8fc04e50ae2..5847887f1fe 100644 --- a/systemvm/agent/noVNC/package.json +++ b/systemvm/agent/noVNC/package.json @@ -1,6 +1,6 @@ { "name": "@novnc/novnc", - "version": "1.2.0", + "version": "1.4.0", "description": "An HTML5 VNC client", "browser": "lib/rfb", "directories": { @@ -21,7 +21,7 @@ "scripts": { "lint": "eslint app core po/po2js po/xgettext-html tests utils", "test": "karma start karma.conf.js", - "prepublish": "node ./utils/use_require.js --as commonjs --clean" + "prepublish": "node ./utils/convert.js --clean" }, "repository": { "type": "git", @@ -29,8 +29,6 @@ }, "author": "Joel Martin (https://github.com/kanaka)", "contributors": [ - "Solly Ross (https://github.com/directxman12)", - "Peter Åstrand (https://github.com/astrand)", "Samuel Mannehed (https://github.com/samhed)", "Pierre Ossman (https://github.com/CendioOssman)" ], @@ -40,42 +38,36 @@ }, "homepage": "https://github.com/novnc/noVNC", "devDependencies": { - "@babel/core": "*", - "@babel/plugin-syntax-dynamic-import": "*", - "@babel/plugin-transform-modules-amd": "*", - "@babel/plugin-transform-modules-commonjs": "*", - "@babel/plugin-transform-modules-systemjs": "*", - "@babel/plugin-transform-modules-umd": "*", - "@babel/preset-env": "*", - "@babel/cli": "*", - "babel-plugin-import-redirect": "*", - "browserify": "*", - "babelify": "*", - "core-js": "*", - "chai": "*", - "commander": "*", - "es-module-loader": "*", - "eslint": "*", - "fs-extra": "*", - "jsdom": "*", - "karma": "*", - "karma-mocha": "*", - "karma-chrome-launcher": "*", - "@chiragrupani/karma-chromium-edge-launcher": "*", - "karma-firefox-launcher": "*", - "karma-ie-launcher": "*", - "karma-mocha-reporter": "*", - "karma-safari-launcher": "*", - "karma-script-launcher": "*", - "karma-sinon-chai": "*", - "mocha": "*", - "node-getopt": "*", - "po2json": "*", - "requirejs": "*", - "rollup": "*", - "rollup-plugin-node-resolve": "*", - "sinon": "*", - "sinon-chai": "*" + "@babel/core": "latest", + "@babel/plugin-syntax-dynamic-import": "latest", + "@babel/plugin-transform-modules-commonjs": "latest", + "@babel/preset-env": "latest", + "@babel/cli": "latest", + "babel-plugin-import-redirect": "latest", + "browserify": "latest", + "babelify": "latest", + "core-js": "latest", + "chai": "latest", + "commander": "latest", + "es-module-loader": "latest", + "eslint": "latest", + "fs-extra": "latest", + "jsdom": "latest", + "karma": "latest", + "karma-mocha": "latest", + "karma-chrome-launcher": "latest", + "@chiragrupani/karma-chromium-edge-launcher": "latest", + "karma-firefox-launcher": "latest", + "karma-ie-launcher": "latest", + "karma-mocha-reporter": "latest", + "karma-safari-launcher": "latest", + "karma-script-launcher": "latest", + "karma-sinon-chai": "latest", + "mocha": "latest", + "node-getopt": "latest", + "po2json": "latest", + "sinon": "latest", + "sinon-chai": "latest" }, "dependencies": {}, "keywords": [ diff --git a/systemvm/agent/noVNC/po/fr.po b/systemvm/agent/noVNC/po/fr.po new file mode 100644 index 00000000000..748a4dbed8b --- /dev/null +++ b/systemvm/agent/noVNC/po/fr.po @@ -0,0 +1,300 @@ +# French translations for noVNC package +# Traductions françaises du paquet noVNC. +# Copyright (C) 2021 The noVNC Authors +# This file is distributed under the same license as the noVNC package. +# Jose , 2021. +# Lowxorx , 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: noVNC 1.2.0\n" +"Report-Msgid-Bugs-To: novnc@googlegroups.com\n" +"POT-Creation-Date: 2020-07-03 16:11+0200\n" +"PO-Revision-Date: 2022-04-25 23:40+0200\n" +"Last-Translator: Lowxorx \n" +"Language-Team: French\n" +"Language: fr\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:394 +msgid "Connecting..." +msgstr "En cours de connexion..." + +#: ../app/ui.js:401 +msgid "Disconnecting..." +msgstr "Déconnexion en cours..." + +#: ../app/ui.js:407 +msgid "Reconnecting..." +msgstr "Reconnexion en cours..." + +#: ../app/ui.js:412 +msgid "Internal error" +msgstr "Erreur interne" + +#: ../app/ui.js:1008 +msgid "Must set host" +msgstr "Doit définir l'hôte" + +#: ../app/ui.js:1090 +msgid "Connected (encrypted) to " +msgstr "Connecté (chiffré) à " + +#: ../app/ui.js:1092 +msgid "Connected (unencrypted) to " +msgstr "Connecté (non chiffré) à " + +#: ../app/ui.js:1115 +msgid "Something went wrong, connection is closed" +msgstr "Quelque chose s'est mal passé, la connexion a été fermée" + +#: ../app/ui.js:1118 +msgid "Failed to connect to server" +msgstr "Échec de connexion au serveur" + +#: ../app/ui.js:1128 +msgid "Disconnected" +msgstr "Déconnecté" + +#: ../app/ui.js:1143 +msgid "New connection has been rejected with reason: " +msgstr "Une nouvelle connexion a été rejetée avec motif : " + +#: ../app/ui.js:1146 +msgid "New connection has been rejected" +msgstr "Une nouvelle connexion a été rejetée" + +#: ../app/ui.js:1181 +msgid "Credentials are required" +msgstr "Les identifiants sont requis" + +#: ../vnc.html:74 +msgid "noVNC encountered an error:" +msgstr "noVNC a rencontré une erreur :" + +#: ../vnc.html:84 +msgid "Hide/Show the control bar" +msgstr "Masquer/Afficher la barre de contrôle" + +#: ../vnc.html:91 +msgid "Drag" +msgstr "Faire glisser" + +#: ../vnc.html:91 +msgid "Move/Drag Viewport" +msgstr "Déplacer/faire glisser le Viewport" + +#: ../vnc.html:97 +msgid "Keyboard" +msgstr "Clavier" + +#: ../vnc.html:97 +msgid "Show Keyboard" +msgstr "Afficher le clavier" + +#: ../vnc.html:102 +msgid "Extra keys" +msgstr "Touches supplémentaires" + +#: ../vnc.html:102 +msgid "Show Extra Keys" +msgstr "Afficher les touches supplémentaires" + +#: ../vnc.html:107 +msgid "Ctrl" +msgstr "Ctrl" + +#: ../vnc.html:107 +msgid "Toggle Ctrl" +msgstr "Basculer Ctrl" + +#: ../vnc.html:110 +msgid "Alt" +msgstr "Alt" + +#: ../vnc.html:110 +msgid "Toggle Alt" +msgstr "Basculer Alt" + +#: ../vnc.html:113 +msgid "Toggle Windows" +msgstr "Basculer Windows" + +#: ../vnc.html:113 +msgid "Windows" +msgstr "Windows" + +#: ../vnc.html:116 +msgid "Send Tab" +msgstr "Envoyer l'onglet" + +#: ../vnc.html:116 +msgid "Tab" +msgstr "l'onglet" + +#: ../vnc.html:119 +msgid "Esc" +msgstr "Esc" + +#: ../vnc.html:119 +msgid "Send Escape" +msgstr "Envoyer Escape" + +#: ../vnc.html:122 +msgid "Ctrl+Alt+Del" +msgstr "Ctrl+Alt+Del" + +#: ../vnc.html:122 +msgid "Send Ctrl-Alt-Del" +msgstr "Envoyer Ctrl-Alt-Del" + +#: ../vnc.html:129 +msgid "Shutdown/Reboot" +msgstr "Arrêter/Redémarrer" + +#: ../vnc.html:129 +msgid "Shutdown/Reboot..." +msgstr "Arrêter/Redémarrer..." + +#: ../vnc.html:135 +msgid "Power" +msgstr "Alimentation" + +#: ../vnc.html:137 +msgid "Shutdown" +msgstr "Arrêter" + +#: ../vnc.html:138 +msgid "Reboot" +msgstr "Redémarrer" + +#: ../vnc.html:139 +msgid "Reset" +msgstr "Réinitialiser" + +#: ../vnc.html:144 ../vnc.html:150 +msgid "Clipboard" +msgstr "Presse-papiers" + +#: ../vnc.html:154 +msgid "Clear" +msgstr "Effacer" + +#: ../vnc.html:160 +msgid "Fullscreen" +msgstr "Plein écran" + +#: ../vnc.html:165 ../vnc.html:172 +msgid "Settings" +msgstr "Paramètres" + +#: ../vnc.html:175 +msgid "Shared Mode" +msgstr "Mode partagé" + +#: ../vnc.html:178 +msgid "View Only" +msgstr "Afficher uniquement" + +#: ../vnc.html:182 +msgid "Clip to Window" +msgstr "Clip à fenêtre" + +#: ../vnc.html:185 +msgid "Scaling Mode:" +msgstr "Mode mise à l'échelle :" + +#: ../vnc.html:187 +msgid "None" +msgstr "Aucun" + +#: ../vnc.html:188 +msgid "Local Scaling" +msgstr "Mise à l'échelle locale" + +#: ../vnc.html:189 +msgid "Remote Resizing" +msgstr "Redimensionnement à distance" + +#: ../vnc.html:194 +msgid "Advanced" +msgstr "Avancé" + +#: ../vnc.html:197 +msgid "Quality:" +msgstr "Qualité :" + +#: ../vnc.html:201 +msgid "Compression level:" +msgstr "Niveau de compression :" + +#: ../vnc.html:206 +msgid "Repeater ID:" +msgstr "ID Répéteur :" + +#: ../vnc.html:210 +msgid "WebSocket" +msgstr "WebSocket" + +#: ../vnc.html:213 +msgid "Encrypt" +msgstr "Chiffrer" + +#: ../vnc.html:216 +msgid "Host:" +msgstr "Hôte :" + +#: ../vnc.html:220 +msgid "Port:" +msgstr "Port :" + +#: ../vnc.html:224 +msgid "Path:" +msgstr "Chemin :" + +#: ../vnc.html:231 +msgid "Automatic Reconnect" +msgstr "Reconnecter automatiquemen" + +#: ../vnc.html:234 +msgid "Reconnect Delay (ms):" +msgstr "Délai de reconnexion (ms) :" + +#: ../vnc.html:239 +msgid "Show Dot when No Cursor" +msgstr "Afficher le point lorsqu'il n'y a pas de curseur" + +#: ../vnc.html:244 +msgid "Logging:" +msgstr "Se connecter :" + +#: ../vnc.html:253 +msgid "Version:" +msgstr "Version :" + +#: ../vnc.html:261 +msgid "Disconnect" +msgstr "Déconnecter" + +#: ../vnc.html:280 +msgid "Connect" +msgstr "Connecter" + +#: ../vnc.html:290 +msgid "Username:" +msgstr "Nom d'utilisateur :" + +#: ../vnc.html:294 +msgid "Password:" +msgstr "Mot de passe :" + +#: ../vnc.html:298 +msgid "Send Credentials" +msgstr "Envoyer les identifiants" + +#: ../vnc.html:308 +msgid "Cancel" +msgstr "Annuler" diff --git a/systemvm/agent/noVNC/po/it.po b/systemvm/agent/noVNC/po/it.po new file mode 100644 index 00000000000..d08ec53849d --- /dev/null +++ b/systemvm/agent/noVNC/po/it.po @@ -0,0 +1,300 @@ +# Italian translations for noVNC +# Traduzione italiana di noVNC +# Copyright (C) 2022 The noVNC Authors +# This file is distributed under the same license as the noVNC package. +# Fabio Fantoni , 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: noVNC 1.3.0\n" +"Report-Msgid-Bugs-To: novnc@googlegroups.com\n" +"POT-Creation-Date: 2021-08-27 16:03+0200\n" +"PO-Revision-Date: 2022-09-08 13:27+0200\n" +"Last-Translator: Fabio Fantoni \n" +"Language-Team: Italian\n" +"Language: it\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 3.1.1\n" + +#: ../app/ui.js:400 +msgid "Connecting..." +msgstr "Connessione in corso..." + +#: ../app/ui.js:407 +msgid "Disconnecting..." +msgstr "Disconnessione..." + +#: ../app/ui.js:413 +msgid "Reconnecting..." +msgstr "Riconnessione..." + +#: ../app/ui.js:418 +msgid "Internal error" +msgstr "Errore interno" + +#: ../app/ui.js:1009 +msgid "Must set host" +msgstr "Devi impostare l'host" + +#: ../app/ui.js:1091 +msgid "Connected (encrypted) to " +msgstr "Connesso (crittografato) a " + +#: ../app/ui.js:1093 +msgid "Connected (unencrypted) to " +msgstr "Connesso (non crittografato) a" + +#: ../app/ui.js:1116 +msgid "Something went wrong, connection is closed" +msgstr "Qualcosa è andato storto, la connessione è stata chiusa" + +#: ../app/ui.js:1119 +msgid "Failed to connect to server" +msgstr "Impossibile connettersi al server" + +#: ../app/ui.js:1129 +msgid "Disconnected" +msgstr "Disconnesso" + +#: ../app/ui.js:1144 +msgid "New connection has been rejected with reason: " +msgstr "La nuova connessione è stata rifiutata con motivo: " + +#: ../app/ui.js:1147 +msgid "New connection has been rejected" +msgstr "La nuova connessione è stata rifiutata" + +#: ../app/ui.js:1182 +msgid "Credentials are required" +msgstr "Le credenziali sono obbligatorie" + +#: ../vnc.html:61 +msgid "noVNC encountered an error:" +msgstr "noVNC ha riscontrato un errore:" + +#: ../vnc.html:71 +msgid "Hide/Show the control bar" +msgstr "Nascondi/Mostra la barra di controllo" + +#: ../vnc.html:78 +msgid "Drag" +msgstr "" + +#: ../vnc.html:78 +msgid "Move/Drag Viewport" +msgstr "" + +#: ../vnc.html:84 +msgid "Keyboard" +msgstr "Tastiera" + +#: ../vnc.html:84 +msgid "Show Keyboard" +msgstr "Mostra tastiera" + +#: ../vnc.html:89 +msgid "Extra keys" +msgstr "Tasti Aggiuntivi" + +#: ../vnc.html:89 +msgid "Show Extra Keys" +msgstr "Mostra Tasti Aggiuntivi" + +#: ../vnc.html:94 +msgid "Ctrl" +msgstr "Ctrl" + +#: ../vnc.html:94 +msgid "Toggle Ctrl" +msgstr "Tieni premuto Ctrl" + +#: ../vnc.html:97 +msgid "Alt" +msgstr "Alt" + +#: ../vnc.html:97 +msgid "Toggle Alt" +msgstr "Tieni premuto Alt" + +#: ../vnc.html:100 +msgid "Toggle Windows" +msgstr "Tieni premuto Windows" + +#: ../vnc.html:100 +msgid "Windows" +msgstr "Windows" + +#: ../vnc.html:103 +msgid "Send Tab" +msgstr "Invia Tab" + +#: ../vnc.html:103 +msgid "Tab" +msgstr "Tab" + +#: ../vnc.html:106 +msgid "Esc" +msgstr "Esc" + +#: ../vnc.html:106 +msgid "Send Escape" +msgstr "Invia Esc" + +#: ../vnc.html:109 +msgid "Ctrl+Alt+Del" +msgstr "Ctrl+Alt+Canc" + +#: ../vnc.html:109 +msgid "Send Ctrl-Alt-Del" +msgstr "Invia Ctrl-Alt-Canc" + +#: ../vnc.html:116 +msgid "Shutdown/Reboot" +msgstr "Spegnimento/Riavvio" + +#: ../vnc.html:116 +msgid "Shutdown/Reboot..." +msgstr "Spegnimento/Riavvio..." + +#: ../vnc.html:122 +msgid "Power" +msgstr "Alimentazione" + +#: ../vnc.html:124 +msgid "Shutdown" +msgstr "Spegnimento" + +#: ../vnc.html:125 +msgid "Reboot" +msgstr "Riavvio" + +#: ../vnc.html:126 +msgid "Reset" +msgstr "Reset" + +#: ../vnc.html:131 ../vnc.html:137 +msgid "Clipboard" +msgstr "Clipboard" + +#: ../vnc.html:141 +msgid "Clear" +msgstr "Pulisci" + +#: ../vnc.html:147 +msgid "Fullscreen" +msgstr "Schermo intero" + +#: ../vnc.html:152 ../vnc.html:159 +msgid "Settings" +msgstr "Impostazioni" + +#: ../vnc.html:162 +msgid "Shared Mode" +msgstr "Modalità condivisa" + +#: ../vnc.html:165 +msgid "View Only" +msgstr "Sola Visualizzazione" + +#: ../vnc.html:169 +msgid "Clip to Window" +msgstr "" + +#: ../vnc.html:172 +msgid "Scaling Mode:" +msgstr "Modalità di ridimensionamento:" + +#: ../vnc.html:174 +msgid "None" +msgstr "Nessuna" + +#: ../vnc.html:175 +msgid "Local Scaling" +msgstr "Ridimensionamento Locale" + +#: ../vnc.html:176 +msgid "Remote Resizing" +msgstr "Ridimensionamento Remoto" + +#: ../vnc.html:181 +msgid "Advanced" +msgstr "Avanzate" + +#: ../vnc.html:184 +msgid "Quality:" +msgstr "Qualità:" + +#: ../vnc.html:188 +msgid "Compression level:" +msgstr "Livello Compressione:" + +#: ../vnc.html:193 +msgid "Repeater ID:" +msgstr "ID Ripetitore:" + +#: ../vnc.html:197 +msgid "WebSocket" +msgstr "WebSocket" + +#: ../vnc.html:200 +msgid "Encrypt" +msgstr "Crittografa" + +#: ../vnc.html:203 +msgid "Host:" +msgstr "Host:" + +#: ../vnc.html:207 +msgid "Port:" +msgstr "Porta:" + +#: ../vnc.html:211 +msgid "Path:" +msgstr "Percorso:" + +#: ../vnc.html:218 +msgid "Automatic Reconnect" +msgstr "Riconnessione Automatica" + +#: ../vnc.html:221 +msgid "Reconnect Delay (ms):" +msgstr "Ritardo Riconnessione (ms):" + +#: ../vnc.html:226 +msgid "Show Dot when No Cursor" +msgstr "Mostra Punto quando Nessun Cursore" + +#: ../vnc.html:231 +msgid "Logging:" +msgstr "" + +#: ../vnc.html:240 +msgid "Version:" +msgstr "Versione:" + +#: ../vnc.html:248 +msgid "Disconnect" +msgstr "Disconnetti" + +#: ../vnc.html:267 +msgid "Connect" +msgstr "Connetti" + +#: ../vnc.html:277 +msgid "Username:" +msgstr "Utente:" + +#: ../vnc.html:281 +msgid "Password:" +msgstr "Password:" + +#: ../vnc.html:285 +msgid "Send Credentials" +msgstr "Invia Credenziale" + +#: ../vnc.html:295 +msgid "Cancel" +msgstr "Annulla" diff --git a/systemvm/agent/noVNC/po/pt_BR.po b/systemvm/agent/noVNC/po/pt_BR.po new file mode 100644 index 00000000000..77951aef03a --- /dev/null +++ b/systemvm/agent/noVNC/po/pt_BR.po @@ -0,0 +1,299 @@ +# Portuguese translations for noVNC package. +# Copyright (C) 2021 The noVNC Authors +# This file is distributed under the same license as the noVNC package. +# , 2021. +# +msgid "" +msgstr "" +"Project-Id-Version: noVNC 1.2.0\n" +"Report-Msgid-Bugs-To: novnc@googlegroups.com\n" +"POT-Creation-Date: 2021-03-15 21:55-0300\n" +"PO-Revision-Date: 2021-03-15 22:09-0300\n" +"Last-Translator: \n" +"Language-Team: Brazilian Portuguese\n" +"Language: pt_BR\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.4.1\n" + +#: ../app/ui.js:400 +msgid "Connecting..." +msgstr "Conectando..." + +#: ../app/ui.js:407 +msgid "Disconnecting..." +msgstr "Desconectando..." + +#: ../app/ui.js:413 +msgid "Reconnecting..." +msgstr "Reconectando..." + +#: ../app/ui.js:418 +msgid "Internal error" +msgstr "Erro interno" + +#: ../app/ui.js:1009 +msgid "Must set host" +msgstr "É necessário definir o host" + +#: ../app/ui.js:1091 +msgid "Connected (encrypted) to " +msgstr "Conectado (com criptografia) a " + +#: ../app/ui.js:1093 +msgid "Connected (unencrypted) to " +msgstr "Conectado (sem criptografia) a " + +#: ../app/ui.js:1116 +msgid "Something went wrong, connection is closed" +msgstr "Algo deu errado. A conexão foi encerrada." + +#: ../app/ui.js:1119 +msgid "Failed to connect to server" +msgstr "Falha ao conectar-se ao servidor" + +#: ../app/ui.js:1129 +msgid "Disconnected" +msgstr "Desconectado" + +#: ../app/ui.js:1144 +msgid "New connection has been rejected with reason: " +msgstr "A nova conexão foi rejeitada pelo motivo: " + +#: ../app/ui.js:1147 +msgid "New connection has been rejected" +msgstr "A nova conexão foi rejeitada" + +#: ../app/ui.js:1182 +msgid "Credentials are required" +msgstr "Credenciais são obrigatórias" + +#: ../vnc.html:61 +msgid "noVNC encountered an error:" +msgstr "O noVNC encontrou um erro:" + +#: ../vnc.html:71 +msgid "Hide/Show the control bar" +msgstr "Esconder/mostrar a barra de controles" + +#: ../vnc.html:78 +msgid "Drag" +msgstr "Arrastar" + +#: ../vnc.html:78 +msgid "Move/Drag Viewport" +msgstr "Mover/arrastar a janela" + +#: ../vnc.html:84 +msgid "Keyboard" +msgstr "Teclado" + +#: ../vnc.html:84 +msgid "Show Keyboard" +msgstr "Mostrar teclado" + +#: ../vnc.html:89 +msgid "Extra keys" +msgstr "Teclas adicionais" + +#: ../vnc.html:89 +msgid "Show Extra Keys" +msgstr "Mostar teclas adicionais" + +#: ../vnc.html:94 +msgid "Ctrl" +msgstr "Ctrl" + +#: ../vnc.html:94 +msgid "Toggle Ctrl" +msgstr "Pressionar/soltar Ctrl" + +#: ../vnc.html:97 +msgid "Alt" +msgstr "Alt" + +#: ../vnc.html:97 +msgid "Toggle Alt" +msgstr "Pressionar/soltar Alt" + +#: ../vnc.html:100 +msgid "Toggle Windows" +msgstr "Pressionar/soltar Windows" + +#: ../vnc.html:100 +msgid "Windows" +msgstr "Windows" + +#: ../vnc.html:103 +msgid "Send Tab" +msgstr "Enviar Tab" + +#: ../vnc.html:103 +msgid "Tab" +msgstr "Tab" + +#: ../vnc.html:106 +msgid "Esc" +msgstr "Esc" + +#: ../vnc.html:106 +msgid "Send Escape" +msgstr "Enviar Esc" + +#: ../vnc.html:109 +msgid "Ctrl+Alt+Del" +msgstr "Ctrl+Alt+Del" + +#: ../vnc.html:109 +msgid "Send Ctrl-Alt-Del" +msgstr "Enviar Ctrl-Alt-Del" + +#: ../vnc.html:116 +msgid "Shutdown/Reboot" +msgstr "Desligar/reiniciar" + +#: ../vnc.html:116 +msgid "Shutdown/Reboot..." +msgstr "Desligar/reiniciar..." + +#: ../vnc.html:122 +msgid "Power" +msgstr "Ligar" + +#: ../vnc.html:124 +msgid "Shutdown" +msgstr "Desligar" + +#: ../vnc.html:125 +msgid "Reboot" +msgstr "Reiniciar" + +#: ../vnc.html:126 +msgid "Reset" +msgstr "Reiniciar (forçado)" + +#: ../vnc.html:131 ../vnc.html:137 +msgid "Clipboard" +msgstr "Área de transferência" + +#: ../vnc.html:141 +msgid "Clear" +msgstr "Limpar" + +#: ../vnc.html:147 +msgid "Fullscreen" +msgstr "Tela cheia" + +#: ../vnc.html:152 ../vnc.html:159 +msgid "Settings" +msgstr "Configurações" + +#: ../vnc.html:162 +msgid "Shared Mode" +msgstr "Modo compartilhado" + +#: ../vnc.html:165 +msgid "View Only" +msgstr "Apenas visualizar" + +#: ../vnc.html:169 +msgid "Clip to Window" +msgstr "Recortar à janela" + +#: ../vnc.html:172 +msgid "Scaling Mode:" +msgstr "Modo de dimensionamento:" + +#: ../vnc.html:174 +msgid "None" +msgstr "Nenhum" + +#: ../vnc.html:175 +msgid "Local Scaling" +msgstr "Local" + +#: ../vnc.html:176 +msgid "Remote Resizing" +msgstr "Remoto" + +#: ../vnc.html:181 +msgid "Advanced" +msgstr "Avançado" + +#: ../vnc.html:184 +msgid "Quality:" +msgstr "Qualidade:" + +#: ../vnc.html:188 +msgid "Compression level:" +msgstr "Nível de compressão:" + +#: ../vnc.html:193 +msgid "Repeater ID:" +msgstr "ID do repetidor:" + +#: ../vnc.html:197 +msgid "WebSocket" +msgstr "WebSocket" + +#: ../vnc.html:200 +msgid "Encrypt" +msgstr "Criptografar" + +#: ../vnc.html:203 +msgid "Host:" +msgstr "Host:" + +#: ../vnc.html:207 +msgid "Port:" +msgstr "Porta:" + +#: ../vnc.html:211 +msgid "Path:" +msgstr "Caminho:" + +#: ../vnc.html:218 +msgid "Automatic Reconnect" +msgstr "Reconexão automática" + +#: ../vnc.html:221 +msgid "Reconnect Delay (ms):" +msgstr "Atraso da reconexão (ms)" + +#: ../vnc.html:226 +msgid "Show Dot when No Cursor" +msgstr "Mostrar ponto quando não há cursor" + +#: ../vnc.html:231 +msgid "Logging:" +msgstr "Registros:" + +#: ../vnc.html:240 +msgid "Version:" +msgstr "Versão:" + +#: ../vnc.html:248 +msgid "Disconnect" +msgstr "Desconectar" + +#: ../vnc.html:267 +msgid "Connect" +msgstr "Conectar" + +#: ../vnc.html:277 +msgid "Username:" +msgstr "Nome de usuário:" + +#: ../vnc.html:281 +msgid "Password:" +msgstr "Senha:" + +#: ../vnc.html:285 +msgid "Send Credentials" +msgstr "Enviar credenciais" + +#: ../vnc.html:295 +msgid "Cancel" +msgstr "Cancelar" diff --git a/systemvm/agent/noVNC/utils/convert.js b/systemvm/agent/noVNC/utils/convert.js new file mode 100755 index 00000000000..aeba49d9fd5 --- /dev/null +++ b/systemvm/agent/noVNC/utils/convert.js @@ -0,0 +1,140 @@ +#!/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'); + +program + .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('--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'), + vendor: path.resolve(__dirname, '..', 'vendor'), + libDirBase: path.resolve(__dirname, '..', 'lib'), +}; + +// util.promisify requires Node.js 8.x, so we have our own +function promisify(original) { + return function promiseWrap() { + 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); + +const readdir = promisify(fs.readdir); +const lstat = promisify(fs.lstat); + +const ensureDir = promisify(fse.ensureDir); + +const babelTransformFile = promisify(babel.transformFile); + +// walkDir *recursively* walks directories trees, +// calling the callback for all normal files found. +function walkDir(basePath, cb, filter) { + return readdir(basePath) + .then((files) => { + const paths = files.map(filename => path.join(basePath, 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 makeLibFiles(sourceMaps) { + // NB: we need to make a copy of babelOpts, since babel sets some defaults on it + const babelOpts = () => ({ + plugins: [], + presets: [ + [ '@babel/preset-env', + { modules: 'commonjs' } ] + ], + ast: false, + sourceMaps: sourceMaps, + }); + + fse.ensureDirSync(paths.libDirBase); + + const outFiles = []; + + const handleDir = (vendorRewrite, inPathBase, filename) => Promise.resolve() + .then(() => { + const outPath = path.join(paths.libDirBase, path.relative(inPathBase, filename)); + + if (path.extname(filename) !== '.js') { + return; // skip non-javascript files + } + return Promise.resolve() + .then(() => ensureDir(path.dirname(outPath))) + .then(() => { + const opts = babelOpts(); + // Adjust for the fact that we move the core files relative + // to the vendor directory + if (vendorRewrite) { + opts.plugins.push(["import-redirect", + {"root": paths.libDirBase, + "redirect": { "vendor/(.+)": "./vendor/$1"}}]); + } + + return babelTransformFile(filename, opts) + .then((res) => { + console.log(`Writing ${outPath}`); + const {map} = res; + let {code} = res; + if (sourceMaps === true) { + // append URL for external source map + code += `\n//# sourceMappingURL=${path.basename(outPath)}.map\n`; + } + outFiles.push(`${outPath}`); + return writeFile(outPath, code) + .then(() => { + if (sourceMaps === true || sourceMaps === 'both') { + console.log(` and ${outPath}.map`); + outFiles.push(`${outPath}.map`); + return writeFile(`${outPath}.map`, JSON.stringify(map)); + } + }); + }); + }); + }); + + Promise.resolve() + .then(() => { + const handler = handleDir.bind(null, false, paths.main); + return walkDir(paths.vendor, handler); + }) + .then(() => { + const handler = handleDir.bind(null, true, paths.core); + return walkDir(paths.core, handler); + }) + .catch((err) => { + console.error(`Failure converting modules: ${err}`); + process.exit(1); + }); +} + +let options = program.opts(); + +if (options.clean) { + console.log(`Removing ${paths.libDirBase}`); + fse.removeSync(paths.libDirBase); +} + +makeLibFiles(options.withSourceMaps); diff --git a/systemvm/agent/noVNC/vendor/browser-es-module-loader/README.md b/systemvm/agent/noVNC/vendor/browser-es-module-loader/README.md deleted file mode 100644 index a50cc37de2a..00000000000 --- a/systemvm/agent/noVNC/vendor/browser-es-module-loader/README.md +++ /dev/null @@ -1,15 +0,0 @@ -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 `npx rollup -c` in this directory, and then run -`./genworker.js`. - -LICENSE -------- - -MIT diff --git a/systemvm/agent/noVNC/vendor/browser-es-module-loader/genworker.js b/systemvm/agent/noVNC/vendor/browser-es-module-loader/genworker.js deleted file mode 100755 index dbf5d2fc801..00000000000 --- a/systemvm/agent/noVNC/vendor/browser-es-module-loader/genworker.js +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env node - -var fs = require("fs"); -var browserify = require("browserify"); - -browserify("src/babel-worker.js") - .transform("babelify", { - presets: [ [ "@babel/preset-env", { targets: "ie >= 11" } ] ], - global: true, - ignore: [ "../../node_modules/core-js" ] - }) - .bundle() - .pipe(fs.createWriteStream("dist/babel-worker.js")); 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 deleted file mode 100644 index 33a4a24aa5a..00000000000 --- a/systemvm/agent/noVNC/vendor/browser-es-module-loader/rollup.config.js +++ /dev/null @@ -1,15 +0,0 @@ -import nodeResolve from 'rollup-plugin-node-resolve'; - -export default { - input: 'src/browser-es-module-loader.js', - output: { - file: 'dist/browser-es-module-loader.js', - format: 'umd', - name: 'BrowserESModuleLoader', - sourcemap: true, - }, - - plugins: [ - nodeResolve(), - ], -}; 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 deleted file mode 100644 index 19e23bf6c0a..00000000000 --- a/systemvm/agent/noVNC/vendor/browser-es-module-loader/src/babel-worker.js +++ /dev/null @@ -1,23 +0,0 @@ -// Polyfills needed for Babel to function -require("core-js"); - -var babelTransform = require('@babel/core').transform; -var babelTransformDynamicImport = require('@babel/plugin-syntax-dynamic-import'); -var babelTransformModulesSystemJS = require('@babel/plugin-transform-modules-systemjs'); -var babelPresetEnv = require('@babel/preset-env'); - -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, babelTransformModulesSystemJS], - presets: [ [ babelPresetEnv, { targets: 'ie >= 11' } ] ], - }); - - 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 deleted file mode 100644 index 9e50b8b5b22..00000000000 --- a/systemvm/agent/noVNC/vendor/browser-es-module-loader/src/browser-es-module-loader.js +++ /dev/null @@ -1,279 +0,0 @@ -import RegisterLoader from 'es-module-loader/core/register-loader.js'; - -import { baseURI, global, isBrowser } from 'es-module-loader/core/common.js'; -import { resolveIfNotPlain } from 'es-module-loader/core/resolve.js'; - -var loader; - -// + + + + - - - - - - + - @@ -87,6 +68,8 @@ noVNC + + Clipboard + + Edit clipboard content in the textarea below. + - + title="Full Screen"> @@ -173,10 +159,10 @@ title="Settings"> + + Settings + - - Settings - Shared Mode @@ -280,39 +266,69 @@ - - + + + + + - noVNC - - Connect - + noVNC + + + Connect + + + + + + + Server identity + + + The server has provided the following identifying information: + + + Fingerprint: + + + + Please verify that the information is correct and press + "Approve". Otherwise press "Reject". + + + + + + + + - - - Username: - - - - Password: - - - - - - + + Credentials + + + Username: + + + + Password: + + + + + diff --git a/systemvm/agent/noVNC/vnc_lite.html b/systemvm/agent/noVNC/vnc_lite.html index 0be2b53845f..1eea4602222 100644 --- a/systemvm/agent/noVNC/vnc_lite.html +++ b/systemvm/agent/noVNC/vnc_lite.html @@ -16,12 +16,6 @@ --> noVNC - - - - - - - - - - - -
+ Edit clipboard content in the textarea below. +
noVNC