mirror of
				https://github.com/apache/cloudstack.git
				synced 2025-10-26 08:42:29 +01:00 
			
		
		
		
	Upgrade noVNC from 1.4.0 to 1.6.0 (#11119)
This commit is contained in:
		
							parent
							
								
									86827f871d
								
							
						
					
					
						commit
						f2bda46419
					
				| @ -75,7 +75,7 @@ repos: | ||||
|         name: run codespell | ||||
|         description: Check spelling with codespell | ||||
|         args: [--ignore-words=.github/linters/codespell.txt] | ||||
|         exclude: ^ui/package\.json$|^ui/package-lock\.json$|^ui/public/js/less\.min\.js$|^ui/public/locales/.*[^n].*\.json$ | ||||
|         exclude: ^systemvm/agent/noVNC/|^ui/package\.json$|^ui/package-lock\.json$|^ui/public/js/less\.min\.js$|^ui/public/locales/.*[^n].*\.json$ | ||||
|   - repo: https://github.com/pycqa/flake8 | ||||
|     rev: 7.0.0 | ||||
|     hooks: | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2019 The noVNC Authors | ||||
|  * Copyright (C) 2019 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
|  | ||||
| @ -14,7 +14,7 @@ | ||||
|     "Password is required": "Je vyžadováno heslo", | ||||
|     "noVNC encountered an error:": "noVNC narazilo na chybu:", | ||||
|     "Hide/Show the control bar": "Skrýt/zobrazit ovládací panel", | ||||
|     "Move/Drag Viewport": "Přesunout/přetáhnout výřez", | ||||
|     "Move/Drag viewport": "Přesunout/přetáhnout výřez", | ||||
|     "viewport drag": "přesun výřezu", | ||||
|     "Active Mouse Button": "Aktivní tlačítka myši", | ||||
|     "No mousebutton": "Žádné", | ||||
| @ -22,9 +22,9 @@ | ||||
|     "Middle mousebutton": "Prostřední tlačítko myši", | ||||
|     "Right mousebutton": "Pravé tlačítko myši", | ||||
|     "Keyboard": "Klávesnice", | ||||
|     "Show Keyboard": "Zobrazit klávesnici", | ||||
|     "Show keyboard": "Zobrazit klávesnici", | ||||
|     "Extra keys": "Extra klávesy", | ||||
|     "Show Extra Keys": "Zobrazit extra klávesy", | ||||
|     "Show extra keys": "Zobrazit extra klávesy", | ||||
|     "Ctrl": "Ctrl", | ||||
|     "Toggle Ctrl": "Přepnout Ctrl", | ||||
|     "Alt": "Alt", | ||||
| @ -45,13 +45,13 @@ | ||||
|     "Clear": "Vymazat", | ||||
|     "Fullscreen": "Celá obrazovka", | ||||
|     "Settings": "Nastavení", | ||||
|     "Shared Mode": "Sdílený režim", | ||||
|     "View Only": "Pouze prohlížení", | ||||
|     "Clip to Window": "Přizpůsobit oknu", | ||||
|     "Scaling Mode:": "Přizpůsobení velikosti", | ||||
|     "Shared mode": "Sdílený režim", | ||||
|     "View only": "Pouze prohlížení", | ||||
|     "Clip to window": "Přizpůsobit oknu", | ||||
|     "Scaling mode:": "Přizpůsobení velikosti", | ||||
|     "None": "Žádné", | ||||
|     "Local Scaling": "Místní", | ||||
|     "Remote Resizing": "Vzdálené", | ||||
|     "Local scaling": "Místní", | ||||
|     "Remote resizing": "Vzdálené", | ||||
|     "Advanced": "Pokročilé", | ||||
|     "Repeater ID:": "ID opakovače", | ||||
|     "WebSocket": "WebSocket", | ||||
| @ -59,9 +59,9 @@ | ||||
|     "Host:": "Hostitel:", | ||||
|     "Port:": "Port:", | ||||
|     "Path:": "Cesta", | ||||
|     "Automatic Reconnect": "Automatická obnova připojení", | ||||
|     "Reconnect Delay (ms):": "Zpoždění připojení (ms)", | ||||
|     "Show Dot when No Cursor": "Tečka místo chybějícího kurzoru myši", | ||||
|     "Automatic reconnect": "Automatická obnova připojení", | ||||
|     "Reconnect delay (ms):": "Zpoždění připojení (ms)", | ||||
|     "Show dot when no cursor": "Tečka místo chybějícího kurzoru myši", | ||||
|     "Logging:": "Logování:", | ||||
|     "Disconnect": "Odpojit", | ||||
|     "Connect": "Připojit", | ||||
|  | ||||
| @ -13,7 +13,7 @@ | ||||
|     "Password is required": "Passwort ist erforderlich", | ||||
|     "noVNC encountered an error:": "Ein Fehler ist aufgetreten:", | ||||
|     "Hide/Show the control bar": "Kontrollleiste verstecken/anzeigen", | ||||
|     "Move/Drag Viewport": "Ansichtsfenster verschieben/ziehen", | ||||
|     "Move/Drag viewport": "Ansichtsfenster verschieben/ziehen", | ||||
|     "viewport drag": "Ansichtsfenster ziehen", | ||||
|     "Active Mouse Button": "Aktive Maustaste", | ||||
|     "No mousebutton": "Keine Maustaste", | ||||
| @ -21,9 +21,9 @@ | ||||
|     "Middle mousebutton": "Mittlere Maustaste", | ||||
|     "Right mousebutton": "Rechte Maustaste", | ||||
|     "Keyboard": "Tastatur", | ||||
|     "Show Keyboard": "Tastatur anzeigen", | ||||
|     "Show keyboard": "Tastatur anzeigen", | ||||
|     "Extra keys": "Zusatztasten", | ||||
|     "Show Extra Keys": "Zusatztasten anzeigen", | ||||
|     "Show extra keys": "Zusatztasten anzeigen", | ||||
|     "Ctrl": "Strg", | ||||
|     "Toggle Ctrl": "Strg umschalten", | ||||
|     "Alt": "Alt", | ||||
| @ -44,13 +44,13 @@ | ||||
|     "Clear": "Löschen", | ||||
|     "Fullscreen": "Vollbild", | ||||
|     "Settings": "Einstellungen", | ||||
|     "Shared Mode": "Geteilter Modus", | ||||
|     "View Only": "Nur betrachten", | ||||
|     "Clip to Window": "Auf Fenster begrenzen", | ||||
|     "Scaling Mode:": "Skalierungsmodus:", | ||||
|     "Shared mode": "Geteilter Modus", | ||||
|     "View only": "Nur betrachten", | ||||
|     "Clip to window": "Auf Fenster begrenzen", | ||||
|     "Scaling mode:": "Skalierungsmodus:", | ||||
|     "None": "Keiner", | ||||
|     "Local Scaling": "Lokales skalieren", | ||||
|     "Remote Resizing": "Serverseitiges skalieren", | ||||
|     "Local scaling": "Lokales skalieren", | ||||
|     "Remote resizing": "Serverseitiges skalieren", | ||||
|     "Advanced": "Erweitert", | ||||
|     "Repeater ID:": "Repeater ID:", | ||||
|     "WebSocket": "WebSocket", | ||||
| @ -58,12 +58,17 @@ | ||||
|     "Host:": "Server:", | ||||
|     "Port:": "Port:", | ||||
|     "Path:": "Pfad:", | ||||
|     "Automatic Reconnect": "Automatisch wiederverbinden", | ||||
|     "Reconnect Delay (ms):": "Wiederverbindungsverzögerung (ms):", | ||||
|     "Automatic reconnect": "Automatisch wiederverbinden", | ||||
|     "Reconnect delay (ms):": "Wiederverbindungsverzögerung (ms):", | ||||
|     "Logging:": "Protokollierung:", | ||||
|     "Disconnect": "Verbindung trennen", | ||||
|     "Connect": "Verbinden", | ||||
|     "Password:": "Passwort:", | ||||
|     "Cancel": "Abbrechen", | ||||
|     "Canvas not supported.": "Canvas nicht unterstützt." | ||||
|     "Canvas not supported.": "Canvas nicht unterstützt.", | ||||
|     "Disconnect timeout": "Zeitüberschreitung beim Trennen", | ||||
|     "Local Downscaling": "Lokales herunterskalieren", | ||||
|     "Local Cursor": "Lokaler Mauszeiger", | ||||
|     "Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "'Clipping-Modus' aktiviert, Scrollbalken in 'IE-Vollbildmodus' werden nicht unterstützt", | ||||
|     "True Color": "True Color" | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| { | ||||
|     "HTTPS is required for full functionality": "Το HTTPS είναι απαιτούμενο για πλήρη λειτουργικότητα", | ||||
|     "Connecting...": "Συνδέεται...", | ||||
|     "Disconnecting...": "Aποσυνδέεται...", | ||||
|     "Reconnecting...": "Επανασυνδέεται...", | ||||
| @ -7,19 +8,15 @@ | ||||
|     "Connected (encrypted) to ": "Συνδέθηκε (κρυπτογραφημένα) με το ", | ||||
|     "Connected (unencrypted) to ": "Συνδέθηκε (μη κρυπτογραφημένα) με το ", | ||||
|     "Something went wrong, connection is closed": "Κάτι πήγε στραβά, η σύνδεση διακόπηκε", | ||||
|     "Failed to connect to server": "Αποτυχία στη σύνδεση με το διακομιστή", | ||||
|     "Disconnected": "Αποσυνδέθηκε", | ||||
|     "New connection has been rejected with reason: ": "Η νέα σύνδεση απορρίφθηκε διότι: ", | ||||
|     "New connection has been rejected": "Η νέα σύνδεση απορρίφθηκε ", | ||||
|     "Password is required": "Απαιτείται ο κωδικός πρόσβασης", | ||||
|     "Credentials are required": "Απαιτούνται διαπιστευτήρια", | ||||
|     "noVNC encountered an error:": "το noVNC αντιμετώπισε ένα σφάλμα:", | ||||
|     "Hide/Show the control bar": "Απόκρυψη/Εμφάνιση γραμμής ελέγχου", | ||||
|     "Drag": "Σύρσιμο", | ||||
|     "Move/Drag Viewport": "Μετακίνηση/Σύρσιμο Θεατού πεδίου", | ||||
|     "viewport drag": "σύρσιμο θεατού πεδίου", | ||||
|     "Active Mouse Button": "Ενεργό Πλήκτρο Ποντικιού", | ||||
|     "No mousebutton": "Χωρίς Πλήκτρο Ποντικιού", | ||||
|     "Left mousebutton": "Αριστερό Πλήκτρο Ποντικιού", | ||||
|     "Middle mousebutton": "Μεσαίο Πλήκτρο Ποντικιού", | ||||
|     "Right mousebutton": "Δεξί Πλήκτρο Ποντικιού", | ||||
|     "Keyboard": "Πληκτρολόγιο", | ||||
|     "Show Keyboard": "Εμφάνιση Πληκτρολογίου", | ||||
|     "Extra keys": "Επιπλέον πλήκτρα", | ||||
| @ -28,6 +25,8 @@ | ||||
|     "Toggle Ctrl": "Εναλλαγή Ctrl", | ||||
|     "Alt": "Alt", | ||||
|     "Toggle Alt": "Εναλλαγή Alt", | ||||
|     "Toggle Windows": "Εναλλαγή Παράθυρων", | ||||
|     "Windows": "Παράθυρα", | ||||
|     "Send Tab": "Αποστολή Tab", | ||||
|     "Tab": "Tab", | ||||
|     "Esc": "Esc", | ||||
| @ -41,8 +40,8 @@ | ||||
|     "Reboot": "Επανεκκίνηση", | ||||
|     "Reset": "Επαναφορά", | ||||
|     "Clipboard": "Πρόχειρο", | ||||
|     "Clear": "Καθάρισμα", | ||||
|     "Fullscreen": "Πλήρης Οθόνη", | ||||
|     "Edit clipboard content in the textarea below.": "Επεξεργαστείτε το περιεχόμενο του πρόχειρου στην περιοχή κειμένου παρακάτω.", | ||||
|     "Full Screen": "Πλήρης Οθόνη", | ||||
|     "Settings": "Ρυθμίσεις", | ||||
|     "Shared Mode": "Κοινόχρηστη Λειτουργία", | ||||
|     "View Only": "Μόνο Θέαση", | ||||
| @ -52,6 +51,8 @@ | ||||
|     "Local Scaling": "Τοπική Κλιμάκωση", | ||||
|     "Remote Resizing": "Απομακρυσμένη Αλλαγή μεγέθους", | ||||
|     "Advanced": "Για προχωρημένους", | ||||
|     "Quality:": "Ποιότητα:", | ||||
|     "Compression level:": "Επίπεδο συμπίεσης:", | ||||
|     "Repeater ID:": "Repeater ID:", | ||||
|     "WebSocket": "WebSocket", | ||||
|     "Encrypt": "Κρυπτογράφηση", | ||||
| @ -60,10 +61,40 @@ | ||||
|     "Path:": "Διαδρομή:", | ||||
|     "Automatic Reconnect": "Αυτόματη επανασύνδεση", | ||||
|     "Reconnect Delay (ms):": "Καθυστέρηση επανασύνδεσης (ms):", | ||||
|     "Show Dot when No Cursor": "Εμφάνιση Τελείας όταν δεν υπάρχει Δρομέας", | ||||
|     "Logging:": "Καταγραφή:", | ||||
|     "Version:": "Έκδοση:", | ||||
|     "Disconnect": "Αποσύνδεση", | ||||
|     "Connect": "Σύνδεση", | ||||
|     "Server identity": "Ταυτότητα Διακομιστή", | ||||
|     "The server has provided the following identifying information:": "Ο διακομιστής παρείχε την ακόλουθη πληροφορία ταυτοποίησης:", | ||||
|     "Fingerprint:": "Δακτυλικό αποτύπωμα:", | ||||
|     "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Παρακαλώ επαληθεύσετε ότι η πληροφορία είναι σωστή και πιέστε \"Αποδοχή\". Αλλιώς πιέστε \"Απόρριψη\".", | ||||
|     "Approve": "Αποδοχή", | ||||
|     "Reject": "Απόρριψη", | ||||
|     "Credentials": "Διαπιστευτήρια", | ||||
|     "Username:": "Κωδικός Χρήστη:", | ||||
|     "Password:": "Κωδικός Πρόσβασης:", | ||||
|     "Send Credentials": "Αποστολή Διαπιστευτηρίων", | ||||
|     "Cancel": "Ακύρωση", | ||||
|     "Canvas not supported.": "Δεν υποστηρίζεται το στοιχείο Canvas" | ||||
|     "Password is required": "Απαιτείται ο κωδικός πρόσβασης", | ||||
|     "viewport drag": "σύρσιμο θεατού πεδίου", | ||||
|     "Active Mouse Button": "Ενεργό Πλήκτρο Ποντικιού", | ||||
|     "No mousebutton": "Χωρίς Πλήκτρο Ποντικιού", | ||||
|     "Left mousebutton": "Αριστερό Πλήκτρο Ποντικιού", | ||||
|     "Middle mousebutton": "Μεσαίο Πλήκτρο Ποντικιού", | ||||
|     "Right mousebutton": "Δεξί Πλήκτρο Ποντικιού", | ||||
|     "Clear": "Καθάρισμα", | ||||
|     "Canvas not supported.": "Δεν υποστηρίζεται το στοιχείο Canvas", | ||||
|     "Disconnect timeout": "Παρέλευση χρονικού ορίου αποσύνδεσης", | ||||
|     "Local Downscaling": "Τοπική Συρρίκνωση", | ||||
|     "Local Cursor": "Τοπικός Δρομέας", | ||||
|     "Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "Εφαρμογή λειτουργίας αποκοπής αφού δεν υποστηρίζονται οι λωρίδες κύλισης σε πλήρη οθόνη στον IE", | ||||
|     "True Color": "Πραγματικά Χρώματα", | ||||
|     "Style:": "Στυλ:", | ||||
|     "default": "προεπιλεγμένο", | ||||
|     "Apply": "Εφαρμογή", | ||||
|     "Connection": "Σύνδεση", | ||||
|     "Token:": "Διακριτικό:", | ||||
|     "Send Password": "Αποστολή Κωδικού Πρόσβασης" | ||||
| } | ||||
|  | ||||
| @ -10,7 +10,7 @@ | ||||
|     "Disconnect timeout": "Tiempo de desconexión agotado", | ||||
|     "noVNC encountered an error:": "noVNC ha encontrado un error:", | ||||
|     "Hide/Show the control bar": "Ocultar/Mostrar la barra de control", | ||||
|     "Move/Drag Viewport": "Mover/Arrastrar la ventana", | ||||
|     "Move/Drag viewport": "Mover/Arrastrar la ventana", | ||||
|     "viewport drag": "Arrastrar la ventana", | ||||
|     "Active Mouse Button": "Botón activo del ratón", | ||||
|     "No mousebutton": "Ningún botón del ratón", | ||||
| @ -18,7 +18,7 @@ | ||||
|     "Middle mousebutton": "Botón central del ratón", | ||||
|     "Right mousebutton": "Botón derecho del ratón", | ||||
|     "Keyboard": "Teclado", | ||||
|     "Show Keyboard": "Mostrar teclado", | ||||
|     "Show keyboard": "Mostrar teclado", | ||||
|     "Extra keys": "Teclas adicionales", | ||||
|     "Show Extra Keys": "Mostrar Teclas Adicionales", | ||||
|     "Ctrl": "Ctrl", | ||||
| @ -43,13 +43,13 @@ | ||||
|     "Settings": "Configuraciones", | ||||
|     "Encrypt": "Encriptar", | ||||
|     "Shared Mode": "Modo Compartido", | ||||
|     "View Only": "Solo visualización", | ||||
|     "Clip to Window": "Recortar al tamaño de la ventana", | ||||
|     "Scaling Mode:": "Modo de escalado:", | ||||
|     "View only": "Solo visualización", | ||||
|     "Clip to window": "Recortar al tamaño de la ventana", | ||||
|     "Scaling mode:": "Modo de escalado:", | ||||
|     "None": "Ninguno", | ||||
|     "Local Scaling": "Escalado Local", | ||||
|     "Local Downscaling": "Reducción de escala local", | ||||
|     "Remote Resizing": "Cambio de tamaño remoto", | ||||
|     "Remote resizing": "Cambio de tamaño remoto", | ||||
|     "Advanced": "Avanzado", | ||||
|     "Local Cursor": "Cursor Local", | ||||
|     "Repeater ID:": "ID del Repetidor:", | ||||
| @ -57,8 +57,8 @@ | ||||
|     "Host:": "Host:", | ||||
|     "Port:": "Puerto:", | ||||
|     "Path:": "Ruta:", | ||||
|     "Automatic Reconnect": "Reconexión automática", | ||||
|     "Reconnect Delay (ms):": "Retraso en la reconexión (ms):", | ||||
|     "Automatic reconnect": "Reconexión automática", | ||||
|     "Reconnect delay (ms):": "Retraso en la reconexión (ms):", | ||||
|     "Logging:": "Registrando:", | ||||
|     "Disconnect": "Desconectar", | ||||
|     "Connect": "Conectar", | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| { | ||||
|     "HTTPS is required for full functionality": "", | ||||
|     "Running without HTTPS is not recommended, crashes or other issues are likely.": "Lancer sans HTTPS n'est pas recommandé, crashs ou autres problèmes en vue.", | ||||
|     "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", | ||||
|     "Failed to connect to server: ": "Échec de connexion au serveur ", | ||||
|     "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", | ||||
| @ -16,19 +16,19 @@ | ||||
|     "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", | ||||
|     "Move/Drag viewport": "Déplacer la fenêtre de visualisation", | ||||
|     "Keyboard": "Clavier", | ||||
|     "Show Keyboard": "Afficher le clavier", | ||||
|     "Show keyboard": "Afficher le clavier", | ||||
|     "Extra keys": "Touches supplémentaires", | ||||
|     "Show Extra Keys": "Afficher les 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", | ||||
|     "Windows": "Fenêtre", | ||||
|     "Send Tab": "Envoyer Tab", | ||||
|     "Tab": "Tabulation", | ||||
|     "Esc": "Esc", | ||||
|     "Send Escape": "Envoyer Escape", | ||||
|     "Ctrl+Alt+Del": "Ctrl+Alt+Del", | ||||
| @ -40,15 +40,16 @@ | ||||
|     "Reboot": "Redémarrer", | ||||
|     "Reset": "Réinitialiser", | ||||
|     "Clipboard": "Presse-papiers", | ||||
|     "Edit clipboard content in the textarea below.": "", | ||||
|     "Edit clipboard content in the textarea below.": "Editer le contenu du presse-papier dans la zone ci-dessous.", | ||||
|     "Full screen": "Plein écran", | ||||
|     "Settings": "Paramètres", | ||||
|     "Shared Mode": "Mode partagé", | ||||
|     "View Only": "Afficher uniquement", | ||||
|     "Clip to Window": "Clip à fenêtre", | ||||
|     "Scaling Mode:": "Mode mise à l'échelle :", | ||||
|     "Shared mode": "Mode partagé", | ||||
|     "View only": "Afficher uniquement", | ||||
|     "Clip to window": "Ajuster à la fenêtre", | ||||
|     "Scaling mode:": "Mode mise à l'échelle :", | ||||
|     "None": "Aucun", | ||||
|     "Local Scaling": "Mise à l'échelle locale", | ||||
|     "Remote Resizing": "Redimensionnement à distance", | ||||
|     "Local scaling": "Mise à l'échelle locale", | ||||
|     "Remote resizing": "Redimensionnement à distance", | ||||
|     "Advanced": "Avancé", | ||||
|     "Quality:": "Qualité :", | ||||
|     "Compression level:": "Niveau de compression :", | ||||
| @ -58,21 +59,24 @@ | ||||
|     "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", | ||||
|     "Automatic reconnect": "Reconnecter automatiquement", | ||||
|     "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": "", | ||||
|     "Server identity": "Identité du serveur", | ||||
|     "The server has provided the following identifying information:": "Le serveur a fourni l'identification suivante :", | ||||
|     "Fingerprint:": "Empreinte digitale :", | ||||
|     "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "SVP, verifiez que l'information est correcte et pressez \"Accepter\". Sinon pressez \"Refuser\".", | ||||
|     "Approve": "Accepter", | ||||
|     "Reject": "Refuser", | ||||
|     "Credentials": "Envoyer les identifiants", | ||||
|     "Username:": "Nom d'utilisateur :", | ||||
|     "Password:": "Mot de passe :", | ||||
|     "Send Credentials": "Envoyer les identifiants", | ||||
|     "Cancel": "Annuler" | ||||
|     "Send credentials": "Envoyer les identifiants", | ||||
|     "Cancel": "Annuler", | ||||
|     "Must set host": "Doit définir l'hôte", | ||||
|     "Clear": "Effacer" | ||||
| } | ||||
|  | ||||
| @ -14,10 +14,8 @@ | ||||
|     "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", | ||||
|     "Show keyboard": "Mostra tastiera", | ||||
|     "Extra keys": "Tasti Aggiuntivi", | ||||
|     "Show Extra Keys": "Mostra Tasti Aggiuntivi", | ||||
|     "Ctrl": "Ctrl", | ||||
| @ -42,10 +40,9 @@ | ||||
|     "Clear": "Pulisci", | ||||
|     "Fullscreen": "Schermo intero", | ||||
|     "Settings": "Impostazioni", | ||||
|     "Shared Mode": "Modalità condivisa", | ||||
|     "Shared mode": "Modalità condivisa", | ||||
|     "View Only": "Sola Visualizzazione", | ||||
|     "Clip to Window": "", | ||||
|     "Scaling Mode:": "Modalità di ridimensionamento:", | ||||
|     "Scaling mode:": "Modalità di ridimensionamento:", | ||||
|     "None": "Nessuna", | ||||
|     "Local Scaling": "Ridimensionamento Locale", | ||||
|     "Remote Resizing": "Ridimensionamento Remoto", | ||||
| @ -61,7 +58,6 @@ | ||||
|     "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", | ||||
|  | ||||
| @ -1,12 +1,14 @@ | ||||
| { | ||||
|     "Running without HTTPS is not recommended, crashes or other issues are likely.": "HTTPS接続なしで実行することは推奨されません。クラッシュしたりその他の問題が発生したりする可能性があります。", | ||||
|     "Connecting...": "接続しています...", | ||||
|     "Disconnecting...": "切断しています...", | ||||
|     "Reconnecting...": "再接続しています...", | ||||
|     "Internal error": "内部エラー", | ||||
|     "Must set host": "ホストを設定する必要があります", | ||||
|     "Failed to connect to server: ": "サーバーへの接続に失敗しました: ", | ||||
|     "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: ": "新規接続は次の理由で拒否されました: ", | ||||
| @ -15,16 +17,16 @@ | ||||
|     "noVNC encountered an error:": "noVNC でエラーが発生しました:", | ||||
|     "Hide/Show the control bar": "コントロールバーを隠す/表示する", | ||||
|     "Drag": "ドラッグ", | ||||
|     "Move/Drag Viewport": "ビューポートを移動/ドラッグ", | ||||
|     "Move/Drag viewport": "ビューポートを移動/ドラッグ", | ||||
|     "Keyboard": "キーボード", | ||||
|     "Show Keyboard": "キーボードを表示", | ||||
|     "Show keyboard": "キーボードを表示", | ||||
|     "Extra keys": "追加キー", | ||||
|     "Show Extra Keys": "追加キーを表示", | ||||
|     "Show extra keys": "追加キーを表示", | ||||
|     "Ctrl": "Ctrl", | ||||
|     "Toggle Ctrl": "Ctrl キーを切り替え", | ||||
|     "Toggle Ctrl": "Ctrl キーをトグル", | ||||
|     "Alt": "Alt", | ||||
|     "Toggle Alt": "Alt キーを切り替え", | ||||
|     "Toggle Windows": "Windows キーを切り替え", | ||||
|     "Toggle Alt": "Alt キーをトグル", | ||||
|     "Toggle Windows": "Windows キーをトグル", | ||||
|     "Windows": "Windows", | ||||
|     "Send Tab": "Tab キーを送信", | ||||
|     "Tab": "Tab", | ||||
| @ -39,16 +41,16 @@ | ||||
|     "Reboot": "再起動", | ||||
|     "Reset": "リセット", | ||||
|     "Clipboard": "クリップボード", | ||||
|     "Clear": "クリア", | ||||
|     "Fullscreen": "全画面表示", | ||||
|     "Edit clipboard content in the textarea below.": "以下の入力欄からクリップボードの内容を編集できます。", | ||||
|     "Full screen": "全画面表示", | ||||
|     "Settings": "設定", | ||||
|     "Shared Mode": "共有モード", | ||||
|     "View Only": "表示のみ", | ||||
|     "Clip to Window": "ウィンドウにクリップ", | ||||
|     "Scaling Mode:": "スケーリングモード:", | ||||
|     "Shared mode": "共有モード", | ||||
|     "View only": "表示専用", | ||||
|     "Clip to window": "ウィンドウにクリップ", | ||||
|     "Scaling mode:": "スケーリングモード:", | ||||
|     "None": "なし", | ||||
|     "Local Scaling": "ローカルスケーリング", | ||||
|     "Remote Resizing": "リモートでリサイズ", | ||||
|     "Local scaling": "ローカルでスケーリング", | ||||
|     "Remote resizing": "リモートでリサイズ", | ||||
|     "Advanced": "高度", | ||||
|     "Quality:": "品質:", | ||||
|     "Compression level:": "圧縮レベル:", | ||||
| @ -58,15 +60,22 @@ | ||||
|     "Host:": "ホスト:", | ||||
|     "Port:": "ポート:", | ||||
|     "Path:": "パス:", | ||||
|     "Automatic Reconnect": "自動再接続", | ||||
|     "Reconnect Delay (ms):": "再接続する遅延 (ミリ秒):", | ||||
|     "Show Dot when No Cursor": "カーソルがないときにドットを表示", | ||||
|     "Automatic reconnect": "自動再接続", | ||||
|     "Reconnect delay (ms):": "再接続する遅延 (ミリ秒):", | ||||
|     "Show dot when no cursor": "カーソルがないときにドットを表示する", | ||||
|     "Logging:": "ロギング:", | ||||
|     "Version:": "バージョン:", | ||||
|     "Disconnect": "切断", | ||||
|     "Connect": "接続", | ||||
|     "Server identity": "サーバーの識別情報", | ||||
|     "The server has provided the following identifying information:": "サーバーは以下の識別情報を提供しています:", | ||||
|     "Fingerprint:": "フィンガープリント:", | ||||
|     "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "この情報が正しい場合は「承認」を、そうでない場合は「拒否」を押してください。", | ||||
|     "Approve": "承認", | ||||
|     "Reject": "拒否", | ||||
|     "Credentials": "資格情報", | ||||
|     "Username:": "ユーザー名:", | ||||
|     "Password:": "パスワード:", | ||||
|     "Send Credentials": "資格情報を送信", | ||||
|     "Send credentials": "資格情報を送信", | ||||
|     "Cancel": "キャンセル" | ||||
| } | ||||
|  | ||||
| @ -14,7 +14,7 @@ | ||||
|     "Password is required": "비밀번호가 필요합니다.", | ||||
|     "noVNC encountered an error:": "noVNC에 오류가 발생했습니다:", | ||||
|     "Hide/Show the control bar": "컨트롤 바 숨기기/보이기", | ||||
|     "Move/Drag Viewport": "움직이기/드래그 뷰포트", | ||||
|     "Move/Drag viewport": "움직이기/드래그 뷰포트", | ||||
|     "viewport drag": "뷰포트 드래그", | ||||
|     "Active Mouse Button": "마우스 버튼 활성화", | ||||
|     "No mousebutton": "마우스 버튼 없음", | ||||
| @ -22,9 +22,9 @@ | ||||
|     "Middle mousebutton": "중간 마우스 버튼", | ||||
|     "Right mousebutton": "오른쪽 마우스 버튼", | ||||
|     "Keyboard": "키보드", | ||||
|     "Show Keyboard": "키보드 보이기", | ||||
|     "Show keyboard": "키보드 보이기", | ||||
|     "Extra keys": "기타 키들", | ||||
|     "Show Extra Keys": "기타 키들 보이기", | ||||
|     "Show extra keys": "기타 키들 보이기", | ||||
|     "Ctrl": "Ctrl", | ||||
|     "Toggle Ctrl": "Ctrl 켜기/끄기", | ||||
|     "Alt": "Alt", | ||||
| @ -45,13 +45,13 @@ | ||||
|     "Clear": "지우기", | ||||
|     "Fullscreen": "전체화면", | ||||
|     "Settings": "설정", | ||||
|     "Shared Mode": "공유 모드", | ||||
|     "View Only": "보기 전용", | ||||
|     "Clip to Window": "창에 클립", | ||||
|     "Scaling Mode:": "스케일링 모드:", | ||||
|     "Shared mode": "공유 모드", | ||||
|     "View only": "보기 전용", | ||||
|     "Clip to window": "창에 클립", | ||||
|     "Scaling mode:": "스케일링 모드:", | ||||
|     "None": "없음", | ||||
|     "Local Scaling": "로컬 스케일링", | ||||
|     "Remote Resizing": "원격 크기 조절", | ||||
|     "Local scaling": "로컬 스케일링", | ||||
|     "Remote resizing": "원격 크기 조절", | ||||
|     "Advanced": "고급", | ||||
|     "Repeater ID:": "중계 ID", | ||||
|     "WebSocket": "웹소켓", | ||||
| @ -59,8 +59,8 @@ | ||||
|     "Host:": "호스트:", | ||||
|     "Port:": "포트:", | ||||
|     "Path:": "위치:", | ||||
|     "Automatic Reconnect": "자동 재연결", | ||||
|     "Reconnect Delay (ms):": "재연결 지연 시간 (ms)", | ||||
|     "Automatic reconnect": "자동 재연결", | ||||
|     "Reconnect delay (ms):": "재연결 지연 시간 (ms)", | ||||
|     "Logging:": "로깅", | ||||
|     "Disconnect": "연결 해제", | ||||
|     "Connect": "연결", | ||||
|  | ||||
| @ -1,36 +1,32 @@ | ||||
| { | ||||
|     "Connecting...": "Verbinden...", | ||||
|     "Disconnecting...": "Verbinding verbreken...", | ||||
|     "Running without HTTPS is not recommended, crashes or other issues are likely.": "Het is niet aan te raden om zonder HTTPS te werken, crashes of andere problemen zijn dan waarschijnlijk.", | ||||
|     "Connecting...": "Aan het verbinden…", | ||||
|     "Disconnecting...": "Bezig om verbinding te verbreken...", | ||||
|     "Reconnecting...": "Opnieuw verbinding maken...", | ||||
|     "Internal error": "Interne fout", | ||||
|     "Must set host": "Host moeten worden ingesteld", | ||||
|     "Failed to connect to server: ": "Verbinding maken met server is mislukt", | ||||
|     "Connected (encrypted) to ": "Verbonden (versleuteld) met ", | ||||
|     "Connected (unencrypted) to ": "Verbonden (onversleuteld) met ", | ||||
|     "Something went wrong, connection is closed": "Er iets fout gelopen, verbinding werd verbroken", | ||||
|     "Failed to connect to server": "Verbinding maken met server is mislukt", | ||||
|     "Disconnected": "Verbinding verbroken", | ||||
|     "New connection has been rejected with reason: ": "Nieuwe verbinding is geweigerd omwille van de volgende reden: ", | ||||
|     "New connection has been rejected with reason: ": "Nieuwe verbinding is geweigerd met de volgende reden: ", | ||||
|     "New connection has been rejected": "Nieuwe verbinding is geweigerd", | ||||
|     "Password is required": "Wachtwoord is vereist", | ||||
|     "Credentials are required": "Inloggegevens zijn nodig", | ||||
|     "noVNC encountered an error:": "noVNC heeft een fout bemerkt:", | ||||
|     "Hide/Show the control bar": "Verberg/Toon de bedieningsbalk", | ||||
|     "Move/Drag Viewport": "Verplaats/Versleep Kijkvenster", | ||||
|     "viewport drag": "kijkvenster slepen", | ||||
|     "Active Mouse Button": "Actieve Muisknop", | ||||
|     "No mousebutton": "Geen muisknop", | ||||
|     "Left mousebutton": "Linker muisknop", | ||||
|     "Middle mousebutton": "Middelste muisknop", | ||||
|     "Right mousebutton": "Rechter muisknop", | ||||
|     "Drag": "Sleep", | ||||
|     "Move/Drag viewport": "Verplaats/Versleep Kijkvenster", | ||||
|     "Keyboard": "Toetsenbord", | ||||
|     "Show Keyboard": "Toon Toetsenbord", | ||||
|     "Show keyboard": "Toon Toetsenbord", | ||||
|     "Extra keys": "Extra toetsen", | ||||
|     "Show Extra Keys": "Toon Extra Toetsen", | ||||
|     "Show extra keys": "Toon Extra Toetsen", | ||||
|     "Ctrl": "Ctrl", | ||||
|     "Toggle Ctrl": "Ctrl omschakelen", | ||||
|     "Alt": "Alt", | ||||
|     "Toggle Alt": "Alt omschakelen", | ||||
|     "Toggle Windows": "Windows omschakelen", | ||||
|     "Windows": "Windows", | ||||
|     "Toggle Windows": "Vensters omschakelen", | ||||
|     "Windows": "Vensters", | ||||
|     "Send Tab": "Tab Sturen", | ||||
|     "Tab": "Tab", | ||||
|     "Esc": "Esc", | ||||
| @ -44,30 +40,56 @@ | ||||
|     "Reboot": "Herstarten", | ||||
|     "Reset": "Resetten", | ||||
|     "Clipboard": "Klembord", | ||||
|     "Clear": "Wissen", | ||||
|     "Fullscreen": "Volledig Scherm", | ||||
|     "Edit clipboard content in the textarea below.": "Edit de inhoud van het klembord in het tekstveld hieronder", | ||||
|     "Full screen": "Volledig Scherm", | ||||
|     "Settings": "Instellingen", | ||||
|     "Shared Mode": "Gedeelde Modus", | ||||
|     "View Only": "Alleen Kijken", | ||||
|     "Clip to Window": "Randen buiten venster afsnijden", | ||||
|     "Scaling Mode:": "Schaalmodus:", | ||||
|     "Shared mode": "Gedeelde Modus", | ||||
|     "View only": "Alleen Kijken", | ||||
|     "Clip to window": "Randen buiten venster afsnijden", | ||||
|     "Scaling mode:": "Schaalmodus:", | ||||
|     "None": "Geen", | ||||
|     "Local Scaling": "Lokaal Schalen", | ||||
|     "Remote Resizing": "Op Afstand Formaat Wijzigen", | ||||
|     "Local scaling": "Lokaal Schalen", | ||||
|     "Remote resizing": "Op Afstand Formaat Wijzigen", | ||||
|     "Advanced": "Geavanceerd", | ||||
|     "Quality:": "Kwaliteit:", | ||||
|     "Compression level:": "Compressieniveau:", | ||||
|     "Repeater ID:": "Repeater ID:", | ||||
|     "WebSocket": "WebSocket", | ||||
|     "Encrypt": "Versleutelen", | ||||
|     "Host:": "Host:", | ||||
|     "Port:": "Poort:", | ||||
|     "Path:": "Pad:", | ||||
|     "Automatic Reconnect": "Automatisch Opnieuw Verbinden", | ||||
|     "Reconnect Delay (ms):": "Vertraging voor Opnieuw Verbinden (ms):", | ||||
|     "Show Dot when No Cursor": "Geef stip weer indien geen cursor", | ||||
|     "Automatic reconnect": "Automatisch Opnieuw Verbinden", | ||||
|     "Reconnect delay (ms):": "Vertraging voor Opnieuw Verbinden (ms):", | ||||
|     "Show dot when no cursor": "Geef stip weer indien geen cursor", | ||||
|     "Logging:": "Logmeldingen:", | ||||
|     "Version:": "Versie:", | ||||
|     "Disconnect": "Verbinding verbreken", | ||||
|     "Connect": "Verbinden", | ||||
|     "Server identity": "Serveridentiteit", | ||||
|     "The server has provided the following identifying information:": "De server geeft de volgende identificerende informatie:", | ||||
|     "Fingerprint:": "Vingerafdruk:", | ||||
|     "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Verifieer dat de informatie is correct en druk “OK”. Druk anders op “Afwijzen”.", | ||||
|     "Approve": "OK", | ||||
|     "Reject": "Afwijzen", | ||||
|     "Credentials": "Inloggegevens", | ||||
|     "Username:": "Gebruikersnaam:", | ||||
|     "Password:": "Wachtwoord:", | ||||
|     "Send credentials": "Stuur inloggegevens", | ||||
|     "Cancel": "Annuleren", | ||||
|     "Must set host": "Host moeten worden ingesteld", | ||||
|     "Password is required": "Wachtwoord is vereist", | ||||
|     "viewport drag": "kijkvenster slepen", | ||||
|     "Active Mouse Button": "Actieve Muisknop", | ||||
|     "No mousebutton": "Geen muisknop", | ||||
|     "Left mousebutton": "Linker muisknop", | ||||
|     "Middle mousebutton": "Middelste muisknop", | ||||
|     "Right mousebutton": "Rechter muisknop", | ||||
|     "Clear": "Wissen", | ||||
|     "Send Password": "Verzend Wachtwoord:", | ||||
|     "Cancel": "Annuleren" | ||||
|     "Disconnect timeout": "Timeout tijdens verbreken van verbinding", | ||||
|     "Local Downscaling": "Lokaal Neerschalen", | ||||
|     "Local Cursor": "Lokale Cursor", | ||||
|     "Canvas not supported.": "Canvas wordt niet ondersteund.", | ||||
|     "Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "''Clipping mode' ingeschakeld, omdat schuifbalken in volledige-scherm-modus in IE niet worden ondersteund" | ||||
| } | ||||
|  | ||||
| @ -21,9 +21,9 @@ | ||||
|     "Middle mousebutton": "Środkowy przycisk myszy", | ||||
|     "Right mousebutton": "Prawy przycisk myszy", | ||||
|     "Keyboard": "Klawiatura", | ||||
|     "Show Keyboard": "Pokaż klawiaturę", | ||||
|     "Show keyboard": "Pokaż klawiaturę", | ||||
|     "Extra keys": "Przyciski dodatkowe", | ||||
|     "Show Extra Keys": "Pokaż przyciski dodatkowe", | ||||
|     "Show extra keys": "Pokaż przyciski dodatkowe", | ||||
|     "Ctrl": "Ctrl", | ||||
|     "Toggle Ctrl": "Przełącz Ctrl", | ||||
|     "Alt": "Alt", | ||||
| @ -49,8 +49,8 @@ | ||||
|     "Clip to Window": "Przytnij do Okna", | ||||
|     "Scaling Mode:": "Tryb Skalowania:", | ||||
|     "None": "Brak", | ||||
|     "Local Scaling": "Skalowanie lokalne", | ||||
|     "Remote Resizing": "Skalowanie zdalne", | ||||
|     "Local scaling": "Skalowanie lokalne", | ||||
|     "Remote resizing": "Skalowanie zdalne", | ||||
|     "Advanced": "Zaawansowane", | ||||
|     "Repeater ID:": "ID Repeatera:", | ||||
|     "WebSocket": "WebSocket", | ||||
| @ -58,12 +58,23 @@ | ||||
|     "Host:": "Host:", | ||||
|     "Port:": "Port:", | ||||
|     "Path:": "Ścieżka:", | ||||
|     "Automatic Reconnect": "Automatycznie wznawiaj połączenie", | ||||
|     "Reconnect Delay (ms):": "Opóźnienie wznawiania (ms):", | ||||
|     "Automatic reconnect": "Automatycznie wznawiaj połączenie", | ||||
|     "Reconnect delay (ms):": "Opóźnienie wznawiania (ms):", | ||||
|     "Logging:": "Poziom logowania:", | ||||
|     "Disconnect": "Rozłącz", | ||||
|     "Connect": "Połącz", | ||||
|     "Password:": "Hasło:", | ||||
|     "Cancel": "Anuluj", | ||||
|     "Canvas not supported.": "Element Canvas nie jest wspierany." | ||||
|     "Canvas not supported.": "Element Canvas nie jest wspierany.", | ||||
|     "Disconnect timeout": "Timeout rozłączenia", | ||||
|     "Local Downscaling": "Downscaling lokalny", | ||||
|     "Local Cursor": "Lokalny kursor", | ||||
|     "Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "Wymuszam clipping mode ponieważ paski przewijania nie są wspierane przez IE w trybie pełnoekranowym", | ||||
|     "True Color": "True Color", | ||||
|     "Style:": "Styl:", | ||||
|     "default": "domyślny", | ||||
|     "Apply": "Zapisz", | ||||
|     "Connection": "Połączenie", | ||||
|     "Token:": "Token:", | ||||
|     "Send Password": "Wyślij Hasło" | ||||
| } | ||||
|  | ||||
| @ -15,11 +15,11 @@ | ||||
|     "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", | ||||
|     "Move/Drag viewport": "Mover/arrastar a janela", | ||||
|     "Keyboard": "Teclado", | ||||
|     "Show Keyboard": "Mostrar teclado", | ||||
|     "Show keyboard": "Mostrar teclado", | ||||
|     "Extra keys": "Teclas adicionais", | ||||
|     "Show Extra Keys": "Mostar teclas adicionais", | ||||
|     "Show extra keys": "Mostar teclas adicionais", | ||||
|     "Ctrl": "Ctrl", | ||||
|     "Toggle Ctrl": "Pressionar/soltar Ctrl", | ||||
|     "Alt": "Alt", | ||||
| @ -42,13 +42,13 @@ | ||||
|     "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:", | ||||
|     "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", | ||||
|     "Local scaling": "Local", | ||||
|     "Remote resizing": "Remoto", | ||||
|     "Advanced": "Avançado", | ||||
|     "Quality:": "Qualidade:", | ||||
|     "Compression level:": "Nível de compressão:", | ||||
| @ -58,15 +58,15 @@ | ||||
|     "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", | ||||
|     "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", | ||||
|     "Send credentials": "Enviar credenciais", | ||||
|     "Cancel": "Cancelar" | ||||
| } | ||||
|  | ||||
| @ -15,16 +15,16 @@ | ||||
|     "noVNC encountered an error:": "Ошибка noVNC: ", | ||||
|     "Hide/Show the control bar": "Скрыть/Показать контрольную панель", | ||||
|     "Drag": "Переместить", | ||||
|     "Move/Drag Viewport": "Переместить окно", | ||||
|     "Move/Drag viewport": "Переместить окно", | ||||
|     "Keyboard": "Клавиатура", | ||||
|     "Show Keyboard": "Показать клавиатуру", | ||||
|     "Show keyboard": "Показать клавиатуру", | ||||
|     "Extra keys": "Дополнительные Кнопки", | ||||
|     "Show Extra Keys": "Показать Дополнительные Кнопки", | ||||
|     "Ctrl": "Ctrl", | ||||
|     "Toggle Ctrl": "Переключение нажатия Ctrl", | ||||
|     "Toggle Ctrl": "Зажать Ctrl", | ||||
|     "Alt": "Alt", | ||||
|     "Toggle Alt": "Переключение нажатия Alt", | ||||
|     "Toggle Windows": "Переключение вкладок", | ||||
|     "Toggle Alt": "Зажать Alt", | ||||
|     "Toggle Windows": "Зажать Windows", | ||||
|     "Windows": "Вкладка", | ||||
|     "Send Tab": "Передать нажатие Tab", | ||||
|     "Tab": "Tab", | ||||
| @ -42,13 +42,13 @@ | ||||
|     "Clear": "Очистить", | ||||
|     "Fullscreen": "Во весь экран", | ||||
|     "Settings": "Настройки", | ||||
|     "Shared Mode": "Общий режим", | ||||
|     "Shared mode": "Общий режим", | ||||
|     "View Only": "Только Просмотр", | ||||
|     "Clip to Window": "В окно", | ||||
|     "Scaling Mode:": "Масштаб:", | ||||
|     "Clip to window": "В окно", | ||||
|     "Scaling mode:": "Масштаб:", | ||||
|     "None": "Нет", | ||||
|     "Local Scaling": "Локльный масштаб", | ||||
|     "Remote Resizing": "Удаленная перенастройка размера", | ||||
|     "Local scaling": "Локальный масштаб", | ||||
|     "Remote resizing": "Удаленная перенастройка размера", | ||||
|     "Advanced": "Дополнительно", | ||||
|     "Quality:": "Качество", | ||||
|     "Compression level:": "Уровень Сжатия", | ||||
| @ -58,9 +58,9 @@ | ||||
|     "Host:": "Сервер:", | ||||
|     "Port:": "Порт:", | ||||
|     "Path:": "Путь:", | ||||
|     "Automatic Reconnect": "Автоматическое переподключение", | ||||
|     "Reconnect Delay (ms):": "Задержка переподключения (мс):", | ||||
|     "Show Dot when No Cursor": "Показать точку вместо курсора", | ||||
|     "Automatic reconnect": "Автоматическое переподключение", | ||||
|     "Reconnect delay (ms):": "Задержка переподключения (мс):", | ||||
|     "Show dot when no cursor": "Показать точку вместо курсора", | ||||
|     "Logging:": "Лог:", | ||||
|     "Version:": "Версия", | ||||
|     "Disconnect": "Отключение", | ||||
|  | ||||
| @ -1,10 +1,10 @@ | ||||
| { | ||||
|     "HTTPS is required for full functionality": "HTTPS krävs för full funktionalitet", | ||||
|     "Running without HTTPS is not recommended, crashes or other issues are likely.": "Det är ej rekommenderat att köra utan HTTPS, krascher och andra problem är troliga.", | ||||
|     "Connecting...": "Ansluter...", | ||||
|     "Disconnecting...": "Kopplar ner...", | ||||
|     "Reconnecting...": "Återansluter...", | ||||
|     "Internal error": "Internt fel", | ||||
|     "Must set host": "Du måste specifiera en värd", | ||||
|     "Failed to connect to server: ": "Misslyckades att ansluta till servern: ", | ||||
|     "Connected (encrypted) to ": "Ansluten (krypterat) till ", | ||||
|     "Connected (unencrypted) to ": "Ansluten (okrypterat) till ", | ||||
|     "Something went wrong, connection is closed": "Något gick fel, anslutningen avslutades", | ||||
| @ -16,11 +16,11 @@ | ||||
|     "noVNC encountered an error:": "noVNC stötte på ett problem:", | ||||
|     "Hide/Show the control bar": "Göm/Visa kontrollbaren", | ||||
|     "Drag": "Dra", | ||||
|     "Move/Drag Viewport": "Flytta/Dra Vyn", | ||||
|     "Move/Drag viewport": "Flytta/Dra vyn", | ||||
|     "Keyboard": "Tangentbord", | ||||
|     "Show Keyboard": "Visa Tangentbord", | ||||
|     "Show keyboard": "Visa tangentbord", | ||||
|     "Extra keys": "Extraknappar", | ||||
|     "Show Extra Keys": "Visa Extraknappar", | ||||
|     "Show extra keys": "Visa extraknappar", | ||||
|     "Ctrl": "Ctrl", | ||||
|     "Toggle Ctrl": "Växla Ctrl", | ||||
|     "Alt": "Alt", | ||||
| @ -41,15 +41,15 @@ | ||||
|     "Reset": "Återställ", | ||||
|     "Clipboard": "Urklipp", | ||||
|     "Edit clipboard content in the textarea below.": "Redigera urklippets innehåll i fältet nedan.", | ||||
|     "Full Screen": "Fullskärm", | ||||
|     "Full screen": "Fullskärm", | ||||
|     "Settings": "Inställningar", | ||||
|     "Shared Mode": "Delat Läge", | ||||
|     "View Only": "Endast Visning", | ||||
|     "Clip to Window": "Begränsa till Fönster", | ||||
|     "Scaling Mode:": "Skalningsläge:", | ||||
|     "Shared mode": "Delat läge", | ||||
|     "View only": "Endast visning", | ||||
|     "Clip to window": "Begränsa till fönster", | ||||
|     "Scaling mode:": "Skalningsläge:", | ||||
|     "None": "Ingen", | ||||
|     "Local Scaling": "Lokal Skalning", | ||||
|     "Remote Resizing": "Ändra Storlek", | ||||
|     "Local scaling": "Lokal skalning", | ||||
|     "Remote resizing": "Ändra storlek", | ||||
|     "Advanced": "Avancerat", | ||||
|     "Quality:": "Kvalitet:", | ||||
|     "Compression level:": "Kompressionsnivå:", | ||||
| @ -59,9 +59,9 @@ | ||||
|     "Host:": "Värd:", | ||||
|     "Port:": "Port:", | ||||
|     "Path:": "Sökväg:", | ||||
|     "Automatic Reconnect": "Automatisk Återanslutning", | ||||
|     "Reconnect Delay (ms):": "Fördröjning (ms):", | ||||
|     "Show Dot when No Cursor": "Visa prick när ingen muspekare finns", | ||||
|     "Automatic reconnect": "Automatisk återanslutning", | ||||
|     "Reconnect delay (ms):": "Fördröjning (ms):", | ||||
|     "Show dot when no cursor": "Visa prick när ingen muspekare finns", | ||||
|     "Logging:": "Loggning:", | ||||
|     "Version:": "Version:", | ||||
|     "Disconnect": "Koppla från", | ||||
| @ -75,6 +75,9 @@ | ||||
|     "Credentials": "Användaruppgifter", | ||||
|     "Username:": "Användarnamn:", | ||||
|     "Password:": "Lösenord:", | ||||
|     "Send Credentials": "Skicka Användaruppgifter", | ||||
|     "Cancel": "Avbryt" | ||||
|     "Send credentials": "Skicka användaruppgifter", | ||||
|     "Cancel": "Avbryt", | ||||
|     "Must set host": "Du måste specifiera en värd", | ||||
|     "HTTPS is required for full functionality": "HTTPS krävs för full funktionalitet", | ||||
|     "Clear": "Rensa" | ||||
| } | ||||
|  | ||||
| @ -23,7 +23,7 @@ | ||||
|     "Keyboard": "Klavye", | ||||
|     "Show Keyboard": "Klavye Düzenini Göster", | ||||
|     "Extra keys": "Ekstra tuşlar", | ||||
|     "Show Extra Keys": "Ekstra tuşları göster", | ||||
|     "Show extra keys": "Ekstra tuşları göster", | ||||
|     "Ctrl": "Ctrl", | ||||
|     "Toggle Ctrl": "Ctrl Değiştir ", | ||||
|     "Alt": "Alt", | ||||
|  | ||||
| @ -1,69 +1,93 @@ | ||||
| { | ||||
|     "Running without HTTPS is not recommended, crashes or other issues are likely.": "不建议在没有 HTTPS 的情况下运行,可能会出现崩溃或出现其他问题。", | ||||
|     "Connecting...": "连接中...", | ||||
|     "Disconnecting...": "正在断开连接...", | ||||
|     "Reconnecting...": "重新连接中...", | ||||
|     "Internal error": "内部错误", | ||||
|     "Must set host": "请提供主机名", | ||||
|     "Connected (encrypted) to ": "已连接到(加密)", | ||||
|     "Connected (unencrypted) to ": "已连接到(未加密)", | ||||
|     "Something went wrong, connection is closed": "发生错误,连接已关闭", | ||||
|     "Must set host": "必须设置主机", | ||||
|     "Failed to connect to server: ": "无法连接到服务器:", | ||||
|     "Connected (encrypted) to ": "已连接(已加密)到", | ||||
|     "Connected (unencrypted) to ": "已连接(未加密)到", | ||||
|     "Something went wrong, connection is closed": "出了点问题,连接已关闭", | ||||
|     "Failed to connect to server": "无法连接到服务器", | ||||
|     "Disconnected": "已断开连接", | ||||
|     "New connection has been rejected with reason: ": "连接被拒绝,原因:", | ||||
|     "New connection has been rejected": "连接被拒绝", | ||||
|     "Password is required": "请提供密码", | ||||
|     "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": "显示/隐藏控制栏", | ||||
|     "Move/Drag Viewport": "拖放显示范围", | ||||
|     "viewport drag": "显示范围拖放", | ||||
|     "Active Mouse Button": "启动鼠标按鍵", | ||||
|     "No mousebutton": "禁用鼠标按鍵", | ||||
|     "Left mousebutton": "鼠标左鍵", | ||||
|     "Middle mousebutton": "鼠标中鍵", | ||||
|     "Right mousebutton": "鼠标右鍵", | ||||
|     "Drag": "拖动", | ||||
|     "Move/Drag viewport": "移动/拖动窗口", | ||||
|     "Keyboard": "键盘", | ||||
|     "Show Keyboard": "显示键盘", | ||||
|     "Show keyboard": "显示键盘", | ||||
|     "Extra keys": "额外按键", | ||||
|     "Show Extra Keys": "显示额外按键", | ||||
|     "Show extra keys": "显示额外按键", | ||||
|     "Ctrl": "Ctrl", | ||||
|     "Toggle Ctrl": "切换 Ctrl", | ||||
|     "Alt": "Alt", | ||||
|     "Toggle Alt": "切换 Alt", | ||||
|     "Toggle Windows": "切换窗口", | ||||
|     "Windows": "窗口", | ||||
|     "Send Tab": "发送 Tab 键", | ||||
|     "Tab": "Tab", | ||||
|     "Esc": "Esc", | ||||
|     "Send Escape": "发送 Escape 键", | ||||
|     "Ctrl+Alt+Del": "Ctrl-Alt-Del", | ||||
|     "Send Ctrl-Alt-Del": "发送 Ctrl-Alt-Del 键", | ||||
|     "Shutdown/Reboot": "关机/重新启动", | ||||
|     "Shutdown/Reboot...": "关机/重新启动...", | ||||
|     "Ctrl+Alt+Del": "Ctrl+Alt+Del", | ||||
|     "Send Ctrl-Alt-Del": "发送 Ctrl+Alt+Del 键", | ||||
|     "Shutdown/Reboot": "关机/重启", | ||||
|     "Shutdown/Reboot...": "关机/重启...", | ||||
|     "Power": "电源", | ||||
|     "Shutdown": "关机", | ||||
|     "Reboot": "重新启动", | ||||
|     "Reboot": "重启", | ||||
|     "Reset": "重置", | ||||
|     "Clipboard": "剪贴板", | ||||
|     "Clear": "清除", | ||||
|     "Fullscreen": "全屏", | ||||
|     "Edit clipboard content in the textarea below.": "在下面的文本区域中编辑剪贴板内容。", | ||||
|     "Full screen": "全屏", | ||||
|     "Settings": "设置", | ||||
|     "Shared Mode": "分享模式", | ||||
|     "View Only": "仅查看", | ||||
|     "Clip to Window": "限制/裁切窗口大小", | ||||
|     "Scaling Mode:": "缩放模式:", | ||||
|     "Shared mode": "分享模式", | ||||
|     "View only": "仅查看", | ||||
|     "Clip to window": "限制/裁切窗口大小", | ||||
|     "Scaling mode:": "缩放模式:", | ||||
|     "None": "无", | ||||
|     "Local Scaling": "本地缩放", | ||||
|     "Remote Resizing": "远程调整大小", | ||||
|     "Local scaling": "本地缩放", | ||||
|     "Remote resizing": "远程调整大小", | ||||
|     "Advanced": "高级", | ||||
|     "Quality:": "品质:", | ||||
|     "Compression level:": "压缩级别:", | ||||
|     "Repeater ID:": "中继站 ID", | ||||
|     "WebSocket": "WebSocket", | ||||
|     "Encrypt": "加密", | ||||
|     "Host:": "主机:", | ||||
|     "Port:": "端口:", | ||||
|     "Path:": "路径:", | ||||
|     "Automatic Reconnect": "自动重新连接", | ||||
|     "Reconnect Delay (ms):": "重新连接间隔 (ms):", | ||||
|     "Automatic reconnect": "自动重新连接", | ||||
|     "Reconnect delay (ms):": "重新连接间隔 (ms):", | ||||
|     "Show dot when no cursor": "无光标时显示点", | ||||
|     "Logging:": "日志级别:", | ||||
|     "Disconnect": "中断连接", | ||||
|     "Version:": "版本:", | ||||
|     "Disconnect": "断开连接", | ||||
|     "Connect": "连接", | ||||
|     "Server identity": "服务器身份", | ||||
|     "The server has provided the following identifying information:": "服务器提供了以下识别信息:", | ||||
|     "Fingerprint:": "指纹:", | ||||
|     "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "请核实信息是否正确,并按 “同意”,否则按 “拒绝”。", | ||||
|     "Approve": "同意", | ||||
|     "Reject": "拒绝", | ||||
|     "Credentials": "凭证", | ||||
|     "Username:": "用户名:", | ||||
|     "Password:": "密码:", | ||||
|     "Cancel": "取消" | ||||
|     "Send credentials": "发送凭证", | ||||
|     "Cancel": "取消", | ||||
|     "Password is required": "请提供密码", | ||||
|     "Disconnect timeout": "超时断开", | ||||
|     "viewport drag": "窗口拖动", | ||||
|     "Active Mouse Button": "启动鼠标按键", | ||||
|     "No mousebutton": "禁用鼠标按键", | ||||
|     "Left mousebutton": "鼠标左键", | ||||
|     "Middle mousebutton": "鼠标中键", | ||||
|     "Right mousebutton": "鼠标右键", | ||||
|     "Clear": "清除", | ||||
|     "Local Downscaling": "降低本地尺寸", | ||||
|     "Local Cursor": "本地光标", | ||||
|     "Canvas not supported.": "不支持 Canvas。" | ||||
| } | ||||
|  | ||||
| @ -14,7 +14,7 @@ | ||||
|     "Password is required": "請提供密碼", | ||||
|     "noVNC encountered an error:": "noVNC 遇到一個錯誤:", | ||||
|     "Hide/Show the control bar": "顯示/隱藏控制列", | ||||
|     "Move/Drag Viewport": "拖放顯示範圍", | ||||
|     "Move/Drag viewport": "拖放顯示範圍", | ||||
|     "viewport drag": "顯示範圍拖放", | ||||
|     "Active Mouse Button": "啟用滑鼠按鍵", | ||||
|     "No mousebutton": "無滑鼠按鍵", | ||||
| @ -22,9 +22,9 @@ | ||||
|     "Middle mousebutton": "滑鼠中鍵", | ||||
|     "Right mousebutton": "滑鼠右鍵", | ||||
|     "Keyboard": "鍵盤", | ||||
|     "Show Keyboard": "顯示鍵盤", | ||||
|     "Show keyboard": "顯示鍵盤", | ||||
|     "Extra keys": "額外按鍵", | ||||
|     "Show Extra Keys": "顯示額外按鍵", | ||||
|     "Show extra keys": "顯示額外按鍵", | ||||
|     "Ctrl": "Ctrl", | ||||
|     "Toggle Ctrl": "切換 Ctrl", | ||||
|     "Alt": "Alt", | ||||
| @ -45,13 +45,13 @@ | ||||
|     "Clear": "清除", | ||||
|     "Fullscreen": "全螢幕", | ||||
|     "Settings": "設定", | ||||
|     "Shared Mode": "分享模式", | ||||
|     "View Only": "僅檢視", | ||||
|     "Clip to Window": "限制/裁切視窗大小", | ||||
|     "Scaling Mode:": "縮放模式:", | ||||
|     "Shared mode": "分享模式", | ||||
|     "View only": "僅檢視", | ||||
|     "Clip to window": "限制/裁切視窗大小", | ||||
|     "Scaling mode:": "縮放模式:", | ||||
|     "None": "無", | ||||
|     "Local Scaling": "本機縮放", | ||||
|     "Remote Resizing": "遠端調整大小", | ||||
|     "Local scaling": "本機縮放", | ||||
|     "Remote resizing": "遠端調整大小", | ||||
|     "Advanced": "進階", | ||||
|     "Repeater ID:": "中繼站 ID", | ||||
|     "WebSocket": "WebSocket", | ||||
| @ -59,8 +59,8 @@ | ||||
|     "Host:": "主機:", | ||||
|     "Port:": "連接埠:", | ||||
|     "Path:": "路徑:", | ||||
|     "Automatic Reconnect": "自動重新連線", | ||||
|     "Reconnect Delay (ms):": "重新連線間隔 (ms):", | ||||
|     "Automatic reconnect": "自動重新連線", | ||||
|     "Reconnect delay (ms):": "重新連線間隔 (ms):", | ||||
|     "Logging:": "日誌級別:", | ||||
|     "Disconnect": "中斷連線", | ||||
|     "Connect": "連線", | ||||
|  | ||||
| @ -1,13 +1,13 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2018 The noVNC Authors | ||||
|  * Copyright (C) 2018 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
|  */ | ||||
| 
 | ||||
| /* | ||||
|  * Localization Utilities | ||||
|  * Localization utilities | ||||
|  */ | ||||
| 
 | ||||
| export class Localizer { | ||||
| @ -16,13 +16,19 @@ export class Localizer { | ||||
|         this.language = 'en'; | ||||
| 
 | ||||
|         // Current dictionary of translations
 | ||||
|         this.dictionary = undefined; | ||||
|         this._dictionary = undefined; | ||||
|     } | ||||
| 
 | ||||
|     // Configure suitable language based on user preferences
 | ||||
|     setup(supportedLanguages) { | ||||
|     async setup(supportedLanguages, baseURL) { | ||||
|         this.language = 'en'; // Default: US English
 | ||||
|         this._dictionary = undefined; | ||||
| 
 | ||||
|         this._setupLanguage(supportedLanguages); | ||||
|         await this._setupDictionary(baseURL); | ||||
|     } | ||||
| 
 | ||||
|     _setupLanguage(supportedLanguages) { | ||||
|         /* | ||||
|          * Navigator.languages only available in Chrome (32+) and FireFox (32+) | ||||
|          * Fall back to navigator.language for other browsers | ||||
| @ -40,12 +46,6 @@ export class Localizer { | ||||
|                 .replace("_", "-") | ||||
|                 .split("-"); | ||||
| 
 | ||||
|             // Built-in default?
 | ||||
|             if ((userLang[0] === 'en') && | ||||
|                 ((userLang[1] === undefined) || (userLang[1] === 'us'))) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // First pass: perfect match
 | ||||
|             for (let j = 0; j < supportedLanguages.length; j++) { | ||||
|                 const supLang = supportedLanguages[j] | ||||
| @ -64,7 +64,12 @@ export class Localizer { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Second pass: fallback
 | ||||
|             // Second pass: English fallback
 | ||||
|             if (userLang[0] === 'en') { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Third pass pass: other fallback
 | ||||
|             for (let j = 0;j < supportedLanguages.length;j++) { | ||||
|                 const supLang = supportedLanguages[j] | ||||
|                     .toLowerCase() | ||||
| @ -84,10 +89,32 @@ export class Localizer { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async _setupDictionary(baseURL) { | ||||
|         if (baseURL) { | ||||
|             if (!baseURL.endsWith("/")) { | ||||
|                 baseURL = baseURL + "/"; | ||||
|             } | ||||
|         } else { | ||||
|             baseURL = ""; | ||||
|         } | ||||
| 
 | ||||
|         if (this.language === "en") { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         let response = await fetch(baseURL + this.language + ".json"); | ||||
|         if (!response.ok) { | ||||
|             throw Error("" + response.status + " " + response.statusText); | ||||
|         } | ||||
| 
 | ||||
|         this._dictionary = await response.json(); | ||||
|     } | ||||
| 
 | ||||
|     // Retrieve localised text
 | ||||
|     get(id) { | ||||
|         if (typeof this.dictionary !== 'undefined' && this.dictionary[id]) { | ||||
|             return this.dictionary[id]; | ||||
|         if (typeof this._dictionary !== 'undefined' && | ||||
|             this._dictionary[id]) { | ||||
|             return this._dictionary[id]; | ||||
|         } else { | ||||
|             return id; | ||||
|         } | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC base CSS | ||||
|  * Copyright (C) 2019 The noVNC Authors | ||||
|  * Copyright (C) 2019 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). | ||||
|  */ | ||||
| @ -31,6 +31,7 @@ | ||||
| 
 | ||||
| :root { | ||||
|     font-family: sans-serif; | ||||
|     line-height: 1.6; | ||||
| } | ||||
| 
 | ||||
| body { | ||||
| @ -53,7 +54,7 @@ html { | ||||
| } | ||||
| 
 | ||||
| .noVNC_disabled { | ||||
|   color: rgb(128, 128, 128); | ||||
|     color: var(--novnc-grey); | ||||
| } | ||||
| 
 | ||||
| /* ---------------------------------------- | ||||
| @ -174,7 +175,7 @@ html { | ||||
|     font-weight: bold; | ||||
|     color: #fff; | ||||
| 
 | ||||
|   border-radius: 10px; | ||||
|     border-radius: 12px; | ||||
|     box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); | ||||
|     background: rgba(200,55,55,0.8); | ||||
| } | ||||
| @ -213,7 +214,7 @@ html { | ||||
| } | ||||
| 
 | ||||
| /* ---------------------------------------- | ||||
|  * Control Bar | ||||
|  * Control bar | ||||
|  * ---------------------------------------- | ||||
|  */ | ||||
| 
 | ||||
| @ -224,7 +225,7 @@ html { | ||||
| 
 | ||||
|     transition: 0.5s ease-in-out; | ||||
| 
 | ||||
|   /* Edge misrenders animations without this */ | ||||
|     /* Edge misrenders animations wihthout this */ | ||||
|     transform: translateX(0); | ||||
| } | ||||
| :root.noVNC_connected #noVNC_control_bar_anchor.noVNC_idle { | ||||
| @ -241,8 +242,8 @@ html { | ||||
| 
 | ||||
|     transition: 0.5s ease-in-out; | ||||
| 
 | ||||
|   background-color: rgb(110, 132, 163); | ||||
|   border-radius: 0 10px 10px 0; | ||||
|     background-color: var(--novnc-blue); | ||||
|     border-radius: 0 12px 12px 0; | ||||
| 
 | ||||
|     user-select: none; | ||||
|     -webkit-user-select: none; | ||||
| @ -267,7 +268,7 @@ html { | ||||
| } | ||||
| .noVNC_right #noVNC_control_bar { | ||||
|     left: 100%; | ||||
|   border-radius: 10px 0 0 10px; | ||||
|     border-radius: 12px 0 0 12px; | ||||
| } | ||||
| .noVNC_right #noVNC_control_bar.noVNC_open { | ||||
|     left: 0; | ||||
| @ -285,8 +286,8 @@ html { | ||||
|     height: 50px; | ||||
|     z-index: -1; | ||||
|     cursor: pointer; | ||||
|   border-radius: 5px; | ||||
|   background-color: rgb(83, 99, 122); | ||||
|     border-radius: 6px; | ||||
|     background-color: var(--novnc-darkblue); | ||||
|     background-image: url("../images/handle_bg.png"); | ||||
|     background-repeat: no-repeat; | ||||
|     background-position: right; | ||||
| @ -371,8 +372,8 @@ html { | ||||
|     opacity: 0; | ||||
|     transition: 0.2s ease-in-out; | ||||
|     background: transparent; | ||||
|   box-shadow: 0 0 10px black, inset 0 0 10px 10px rgba(110, 132, 163, 0.8); | ||||
|   border-radius: 10px; | ||||
|     box-shadow: 0 0 10px black, inset 0 0 10px 10px var(--novnc-darkblue); | ||||
|     border-radius: 12px; | ||||
|     transition-delay: 0s; | ||||
| } | ||||
| #noVNC_control_bar_hint.noVNC_active { | ||||
| @ -387,45 +388,21 @@ html { | ||||
| 
 | ||||
| /* Control bar buttons */ | ||||
| #noVNC_control_bar .noVNC_button { | ||||
|     min-width: 24px; | ||||
|     padding: 4px 4px; | ||||
|     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_control_bar .noVNC_button.noVNC_selected { | ||||
|     border-color: rgba(0, 0, 0, 0.8); | ||||
|     background-color: rgba(0, 0, 0, 0.5); | ||||
| } | ||||
| #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_control_bar .noVNC_button:not(:disabled):hover { | ||||
|   background-color: rgba(255, 255, 255, 0.2); | ||||
| } | ||||
| #noVNC_control_bar .noVNC_button:not(:disabled):active { | ||||
|   padding-top: 5px; | ||||
|   padding-bottom: 3px; | ||||
| } | ||||
| #noVNC_control_bar .noVNC_button.noVNC_hidden { | ||||
|     display: none !important; | ||||
| } | ||||
| 
 | ||||
| /* 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); | ||||
| @ -444,7 +421,7 @@ html { | ||||
|     padding: 15px; | ||||
| 
 | ||||
|     background: #fff; | ||||
|   border-radius: 10px; | ||||
|     border-radius: 12px; | ||||
|     color: #000; | ||||
|     border: 2px solid #E0E0E0; | ||||
|     box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); | ||||
| @ -478,7 +455,8 @@ html { | ||||
| 
 | ||||
| .noVNC_panel hr { | ||||
|     border: none; | ||||
|   border-top: 1px solid rgb(192, 192, 192); | ||||
|     border-top: 1px solid var(--novnc-lightgrey); | ||||
|     width: 100%; /* <hr> inside a flexbox will otherwise be 0px wide */ | ||||
| } | ||||
| 
 | ||||
| .noVNC_panel label { | ||||
| @ -486,27 +464,58 @@ html { | ||||
|     white-space: nowrap; | ||||
|     margin: 5px; | ||||
| } | ||||
| @media (max-width: 540px) { | ||||
|     /* Allow wrapping on small screens */ | ||||
|     .noVNC_panel label { | ||||
|         white-space: unset; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| .noVNC_panel li { | ||||
|     margin: 5px; | ||||
| } | ||||
| 
 | ||||
| .noVNC_panel button, | ||||
| .noVNC_panel select, | ||||
| .noVNC_panel textarea, | ||||
| .noVNC_panel input:not([type=checkbox]):not([type=radio]) { | ||||
|     margin-left: 6px; | ||||
|     /* Prevent inputs in panels from being too wide */ | ||||
|     max-width: calc(100% - 6px - var(--input-xpadding) * 2); | ||||
| } | ||||
| 
 | ||||
| .noVNC_panel .noVNC_heading { | ||||
|   background-color: rgb(110, 132, 163); | ||||
|   border-radius: 5px; | ||||
|   padding: 5px; | ||||
|     background-color: var(--novnc-blue); | ||||
|     border-radius: 6px; | ||||
|     padding: 5px 8px; | ||||
|     /* Compensate for padding in image */ | ||||
|   padding-right: 8px; | ||||
|     padding-right: 11px; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     gap: 6px; | ||||
|     color: white; | ||||
|     font-size: 20px; | ||||
|     font-weight: bold; | ||||
|     white-space: nowrap; | ||||
| } | ||||
| .noVNC_panel .noVNC_heading img { | ||||
|     vertical-align: bottom; | ||||
| } | ||||
| 
 | ||||
| .noVNC_submit { | ||||
|   float: right; | ||||
| .noVNC_panel form { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     gap: 12px | ||||
| } | ||||
| 
 | ||||
| .noVNC_panel .button_row { | ||||
|     margin-top: 10px; | ||||
|     display: flex; | ||||
|     gap: 10px; | ||||
|     justify-content: space-between; | ||||
| } | ||||
| .noVNC_panel .button_row *:only-child { | ||||
|     margin-left: auto; /* Align single buttons to the right */ | ||||
| } | ||||
| 
 | ||||
| /* Expanders */ | ||||
| @ -526,8 +535,8 @@ html { | ||||
|     margin: 5px; | ||||
|     margin-left: 10px; | ||||
|     padding: 5px; | ||||
|   background: rgba(0, 0, 0, 0.05); | ||||
|   border-radius: 5px; | ||||
|     background: rgba(0, 0, 0, 0.04); | ||||
|     border-radius: 6px; | ||||
| } | ||||
| .noVNC_expander:not(.noVNC_open) ~ * { | ||||
|     display: none; | ||||
| @ -570,7 +579,7 @@ html { | ||||
| } | ||||
| 
 | ||||
| #noVNC_modifiers { | ||||
|   background-color: rgb(92, 92, 92); | ||||
|     background-color: var(--novnc-darkgrey); | ||||
|     border: none; | ||||
|     padding: 10px; | ||||
| } | ||||
| @ -633,13 +642,13 @@ html { | ||||
|     margin-left: 1rem; | ||||
| } | ||||
| 
 | ||||
| /* Connection Controls */ | ||||
| /* Connection controls */ | ||||
| :root:not(.noVNC_connected) #noVNC_disconnect_button { | ||||
|     display: none; | ||||
| } | ||||
| 
 | ||||
| /* ---------------------------------------- | ||||
|  * Status Dialog | ||||
|  * Status dialog | ||||
|  * ---------------------------------------- | ||||
|  */ | ||||
| 
 | ||||
| @ -665,7 +674,7 @@ html { | ||||
|     justify-content: center; | ||||
|     align-content: center; | ||||
| 
 | ||||
|   line-height: 25px; | ||||
|     line-height: 1.6; | ||||
|     word-wrap: break-word; | ||||
|     color: #fff; | ||||
| 
 | ||||
| @ -711,7 +720,7 @@ html { | ||||
| } | ||||
| 
 | ||||
| /* ---------------------------------------- | ||||
|  * Connect Dialog | ||||
|  * Connect dialog | ||||
|  * ---------------------------------------- | ||||
|  */ | ||||
| 
 | ||||
| @ -735,7 +744,7 @@ html { | ||||
|     font-size: 80px; | ||||
|     text-align: center; | ||||
| 
 | ||||
|   border-radius: 5px; | ||||
|     border-radius: 6px; | ||||
| } | ||||
| @media (max-width: 440px) { | ||||
|     #noVNC_connect_dlg { | ||||
| @ -746,9 +755,9 @@ html { | ||||
|     } | ||||
| } | ||||
| #noVNC_connect_dlg div { | ||||
|   padding: 12px; | ||||
|     padding: 18px; | ||||
| 
 | ||||
|   background-color: rgb(110, 132, 163); | ||||
|     background-color: var(--novnc-darkgrey); | ||||
|     border-radius: 12px; | ||||
|     text-align: center; | ||||
|     font-size: 20px; | ||||
| @ -757,21 +766,17 @@ html { | ||||
| } | ||||
| #noVNC_connect_button { | ||||
|     width: 100%; | ||||
|   padding: 5px 30px; | ||||
| 
 | ||||
|     padding: 6px 30px; | ||||
|     cursor: pointer; | ||||
| 
 | ||||
|   border-color: rgb(83, 99, 122); | ||||
|   border-radius: 5px; | ||||
| 
 | ||||
|   background: linear-gradient(to top, rgb(110, 132, 163), rgb(99, 119, 147)); | ||||
|     border-color: transparent; | ||||
|     border-radius: 12px; | ||||
|     background-color: var(--novnc-blue); | ||||
|     color: white; | ||||
| 
 | ||||
|   /* This avoids it jumping around when :active */ | ||||
|   vertical-align: middle; | ||||
| } | ||||
| #noVNC_connect_button:hover { | ||||
|   background: linear-gradient(to top, rgb(110, 132, 163), rgb(105, 125, 155)); | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|     place-items: center; | ||||
|     gap: 4px; | ||||
| } | ||||
| 
 | ||||
| #noVNC_connect_button img { | ||||
| @ -780,7 +785,7 @@ html { | ||||
| } | ||||
| 
 | ||||
| /* ---------------------------------------- | ||||
|  * Server verification Dialog | ||||
|  * Server verification dialog | ||||
|  * ---------------------------------------- | ||||
|  */ | ||||
| 
 | ||||
| @ -797,7 +802,7 @@ html { | ||||
| } | ||||
| 
 | ||||
| /* ---------------------------------------- | ||||
|  * Password Dialog | ||||
|  * Password dialog | ||||
|  * ---------------------------------------- | ||||
|  */ | ||||
| 
 | ||||
| @ -816,7 +821,7 @@ html { | ||||
| 
 | ||||
| 
 | ||||
| /* ---------------------------------------- | ||||
|  * Main Area | ||||
|  * Main area | ||||
|  * ---------------------------------------- | ||||
|  */ | ||||
| 
 | ||||
| @ -895,13 +900,13 @@ html { | ||||
| } | ||||
| 
 | ||||
| .noVNC_logo { | ||||
|   color:yellow; | ||||
|     color: var(--novnc-yellow); | ||||
|     font-family: 'Orbitron', 'OrbitronTTF', sans-serif; | ||||
|   line-height:90%; | ||||
|     line-height: 0.9; | ||||
|     text-shadow: 0.1em 0.1em 0 black; | ||||
| } | ||||
| .noVNC_logo span{ | ||||
|   color:green; | ||||
|     color: var(--novnc-green); | ||||
| } | ||||
| 
 | ||||
| #noVNC_bell { | ||||
|  | ||||
							
								
								
									
										30
									
								
								systemvm/agent/noVNC/app/styles/constants.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								systemvm/agent/noVNC/app/styles/constants.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | ||||
| /* | ||||
|  * noVNC general CSS constant variables | ||||
|  * Copyright (C) 2025 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). | ||||
|  */ | ||||
| 
 | ||||
| /* ---------- COLORS ----------- */ | ||||
| 
 | ||||
| :root { | ||||
|     --novnc-grey: rgb(128, 128, 128); | ||||
|     --novnc-lightgrey: rgb(192, 192, 192); | ||||
|     --novnc-darkgrey: rgb(92, 92, 92); | ||||
| 
 | ||||
|     /* Transparent to make button colors adapt to the background */ | ||||
|     --novnc-buttongrey: rgba(192, 192, 192, 0.5); | ||||
| 
 | ||||
|     --novnc-blue: rgb(110, 132, 163); | ||||
|     --novnc-lightblue: rgb(74, 144, 217); | ||||
|     --novnc-darkblue: rgb(83, 99, 122); | ||||
| 
 | ||||
|     --novnc-green: rgb(0, 128, 0); | ||||
|     --novnc-yellow: rgb(255, 255, 0); | ||||
| } | ||||
| 
 | ||||
| /* ------ MISC PROPERTIES ------ */ | ||||
| 
 | ||||
| :root { | ||||
|     --input-xpadding: 1em; | ||||
| } | ||||
| @ -1,32 +1,170 @@ | ||||
| /* | ||||
|  * noVNC general input element CSS | ||||
|  * Copyright (C) 2022 The noVNC Authors | ||||
|  * Copyright (C) 2025 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 { | ||||
| /* ------- SHARED BETWEEN INPUT ELEMENTS -------- */ | ||||
| 
 | ||||
| input, | ||||
| textarea, | ||||
| button, | ||||
| select, | ||||
| input::file-selector-button { | ||||
|     padding: 0.5em var(--input-xpadding); | ||||
|     border-radius: 6px; | ||||
|     appearance: none; | ||||
|     text-overflow: ellipsis; | ||||
| 
 | ||||
|     /* 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); | ||||
|     line-height: 1.6; | ||||
| } | ||||
| input:disabled, | ||||
| textarea:disabled, | ||||
| button:disabled, | ||||
| select:disabled, | ||||
| label[disabled] { | ||||
|     opacity: 0.4; | ||||
| } | ||||
| 
 | ||||
| /* | ||||
|  * Buttons | ||||
|  */ | ||||
| input:focus-visible, | ||||
| textarea:focus-visible, | ||||
| button:focus-visible, | ||||
| select:focus-visible, | ||||
| input:focus-visible::file-selector-button { | ||||
|     outline: 2px solid var(--novnc-lightblue); | ||||
|     outline-offset: 1px; | ||||
| } | ||||
| 
 | ||||
| /* ------- TEXT INPUT -------- */ | ||||
| 
 | ||||
| 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 { | ||||
|     border: 1px solid var(--novnc-lightgrey); | ||||
|     /* Account for borders on text inputs, buttons dont have borders */ | ||||
|     padding: calc(0.5em - 1px) var(--input-xpadding); | ||||
| } | ||||
| input:not([type]):focus-visible, | ||||
| input[type=date]:focus-visible, | ||||
| input[type=datetime-local]:focus-visible, | ||||
| input[type=email]:focus-visible, | ||||
| input[type=month]:focus-visible, | ||||
| input[type=number]:focus-visible, | ||||
| input[type=password]:focus-visible, | ||||
| input[type=search]:focus-visible, | ||||
| input[type=tel]:focus-visible, | ||||
| input[type=text]:focus-visible, | ||||
| input[type=time]:focus-visible, | ||||
| input[type=url]:focus-visible, | ||||
| input[type=week]:focus-visible, | ||||
| textarea:focus-visible { | ||||
|     outline-offset: -1px; | ||||
| } | ||||
| 
 | ||||
| textarea { | ||||
|     margin: unset; /* Remove Firefox's built in margin */ | ||||
|     /* Prevent layout from shifting when scrollbars show */ | ||||
|     scrollbar-gutter: stable; | ||||
|     /* Make textareas show at minimum one line. This does not work when | ||||
|        using box-sizing border-box, in which case, vertical padding and | ||||
|        border width needs to be taken into account. */ | ||||
|     min-height: 1lh; | ||||
|     vertical-align: baseline; /* Firefox gives "text-bottom" by default */ | ||||
| } | ||||
| 
 | ||||
| /* ------- NUMBER PICKERS ------- */ | ||||
| 
 | ||||
| /* We can't style the number spinner buttons: | ||||
|    https://github.com/w3c/csswg-drafts/issues/8777 */ | ||||
| input[type=number]::-webkit-inner-spin-button, | ||||
| input[type=number]::-webkit-outer-spin-button { | ||||
|     /* Get rid of increase/decrease buttons in WebKit */ | ||||
|     appearance: none; | ||||
| } | ||||
| input[type=number] { | ||||
|     /* Get rid of increase/decrease buttons in Firefox */ | ||||
|     appearance: textfield; | ||||
| } | ||||
| 
 | ||||
| /* ------- BUTTON ACTIVATIONS -------- */ | ||||
| 
 | ||||
| /* A color overlay that depends on the activation level. The level can then be | ||||
|    set for different states on an element, for example hover and click on a | ||||
|    <button>. */ | ||||
| input, button, select, option, | ||||
| input::file-selector-button, | ||||
| .button-activations { | ||||
|     --button-activation-level: 0; | ||||
|     /* Note that CSS variables aren't functions, beware when inheriting */ | ||||
|     --button-activation-alpha: calc(0.08 * var(--button-activation-level)); | ||||
|     /* FIXME: We want the image() function instead of the linear-gradient() | ||||
|               function below. But it's not supported in the browsers yet. */ | ||||
|     --button-activation-overlay: | ||||
|         linear-gradient(rgba(0, 0, 0, var(--button-activation-alpha)) | ||||
|         100%, transparent); | ||||
|     --button-activation-overlay-light: | ||||
|         linear-gradient(rgba(255, 255, 255, calc(0.23 * var(--button-activation-level))) | ||||
|         100%, transparent); | ||||
| } | ||||
| .button-activations { | ||||
|     background-image: var(--button-activation-overlay); | ||||
| 
 | ||||
|     /* Disable Chrome's touch tap highlight to avoid conflicts with overlay */ | ||||
|     -webkit-tap-highlight-color: transparent; | ||||
| } | ||||
| /* When we want the light overlay on activations instead. | ||||
|    This is best used on elements with darker backgrounds. */ | ||||
| .button-activations.light-overlay { | ||||
|     background-image: var(--button-activation-overlay-light); | ||||
|     /* Can't use the normal blend mode since that gives washed out colors. */ | ||||
|     /* FIXME: For elements with these activation overlays we'd like only | ||||
|               the luminosity to change. The proprty "background-blend-mode" set | ||||
|               to "luminosity" sounds good, but it doesn't work as intended, | ||||
|               see: https://bugzilla.mozilla.org/show_bug.cgi?id=1806417 */ | ||||
|     background-blend-mode: overlay; | ||||
| } | ||||
| 
 | ||||
| input:hover, button:hover, select:hover, option:hover, | ||||
| input::file-selector-button:hover, | ||||
| .button-activations:hover { | ||||
|     --button-activation-level: 1; | ||||
| } | ||||
| /* Unfortunately we have to disable the :hover effect on touch devices, | ||||
|    otherwise the style lingers after tapping the button. */ | ||||
| @media (any-pointer: coarse) { | ||||
|     input:hover, button:hover, select:hover, option:hover, | ||||
|     input::file-selector-button:hover, | ||||
|     .button-activations:hover { | ||||
|         --button-activation-level: 0; | ||||
|     } | ||||
| } | ||||
| input:active, button:active, select:active, option:active, | ||||
| input::file-selector-button:active, | ||||
| .button-activations:active { | ||||
|     --button-activation-level: 2; | ||||
| } | ||||
| input:disabled, button:disabled, select:disabled, select:disabled option, | ||||
| input:disabled::file-selector-button, | ||||
| .button-activations:disabled { | ||||
|     --button-activation-level: 0; | ||||
| } | ||||
| 
 | ||||
| /* ------- BUTTONS -------- */ | ||||
| 
 | ||||
| input[type=button], | ||||
| input[type=color], | ||||
| input[type=image], | ||||
| @ -35,227 +173,16 @@ 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; | ||||
| 
 | ||||
|     min-width: 8em; | ||||
|     border: none; | ||||
|     color: black; | ||||
|     font-weight: bold; | ||||
|     background-color: var(--novnc-buttongrey); | ||||
|     background-image: var(--button-activation-overlay); | ||||
|     cursor: pointer; | ||||
|     /* Disable Chrome's touch tap highlight */ | ||||
|     -webkit-tap-highlight-color: transparent; | ||||
| } | ||||
| 
 | ||||
| /* | ||||
|  * Select dropdowns | ||||
|  */ | ||||
| select { | ||||
|   --select-arrow: url('data:image/svg+xml;utf8, \ | ||||
|       <svg width="8" height="6" version="1.1" viewBox="0 0 8 6" \ | ||||
|            xmlns="http://www.w3.org/2000/svg"> \ | ||||
|           <path d="m6.5 1.5 -2.5 3 -2.5 -3 5 0" stroke-width="3" \ | ||||
|                 stroke="rgb(31,31,31)" fill="none" \ | ||||
|                 stroke-linecap="round" stroke-linejoin="round" /> \ | ||||
|       </svg>'); | ||||
|   background-image: var(--select-arrow), var(--bg-gradient); | ||||
|   background-position: calc(100% - 7px), left top; | ||||
|   background-repeat: no-repeat; | ||||
|   padding-right: calc(2*7px + 8px); | ||||
|   padding-left: 7px; | ||||
| } | ||||
| /* FIXME: :active isn't set when the <select> is opened in Firefox: | ||||
|           https://bugzilla.mozilla.org/show_bug.cgi?id=1805406 */ | ||||
| select:active { | ||||
|   /* Rotated arrow */ | ||||
|   background-image: url('data:image/svg+xml;utf8, \ | ||||
|       <svg width="8" height="6" version="1.1" viewBox="0 0 8 6" \ | ||||
|            xmlns="http://www.w3.org/2000/svg" transform="rotate(180)" > \ | ||||
|           <path d="m6.5 1.5 -2.5 3 -2.5 -3 5 0" stroke-width="3" \ | ||||
|                 stroke="rgb(31,31,31)" fill="none" \ | ||||
|                 stroke-linecap="round" stroke-linejoin="round" /> \ | ||||
|       </svg>'), var(--bg-gradient); | ||||
| } | ||||
| option { | ||||
|   color: black; | ||||
|   background: white; | ||||
| } | ||||
| 
 | ||||
| /* | ||||
|  * Checkboxes | ||||
|  */ | ||||
| input[type=checkbox] { | ||||
|   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, | ||||
| @ -264,18 +191,438 @@ 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; | ||||
| } | ||||
| 
 | ||||
| input[type=button], | ||||
| input[type=color], | ||||
| input[type=reset], | ||||
| input[type=submit] { | ||||
|     /* Workaround for text-overflow bugs in Firefox and Chromium: | ||||
|         https://bugzilla.mozilla.org/show_bug.cgi?id=1800077 | ||||
|         https://bugs.chromium.org/p/chromium/issues/detail?id=1383144 */ | ||||
|     overflow: clip; | ||||
| } | ||||
| 
 | ||||
| /* ------- COLOR PICKERS ------- */ | ||||
| 
 | ||||
| input[type=color] { | ||||
|     min-width: unset; | ||||
|     box-sizing: content-box; | ||||
|     width: 1.4em; | ||||
|     height: 1.4em; | ||||
| } | ||||
| input[type=color]::-webkit-color-swatch-wrapper { | ||||
|     padding: 0; | ||||
| } | ||||
| /* -webkit-color-swatch & -moz-color-swatch cant be in a selector list: | ||||
|    https://bugs.chromium.org/p/chromium/issues/detail?id=1154623 */ | ||||
| input[type=color]::-webkit-color-swatch { | ||||
|     border: none; | ||||
|     border-radius: 6px; | ||||
| } | ||||
| input[type=color]::-moz-color-swatch { | ||||
|     border: none; | ||||
|     border-radius: 6px; | ||||
| } | ||||
| 
 | ||||
| /* -- SHARED BETWEEN CHECKBOXES, RADIOBUTTONS AND THE TOGGLE CLASS -- */ | ||||
| 
 | ||||
| input[type=radio], | ||||
| input[type=checkbox] { | ||||
|     display: inline-flex; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|     background-color: var(--novnc-buttongrey); | ||||
|     background-image: var(--button-activation-overlay); | ||||
|     /* Disable Chrome's touch tap highlight to avoid conflicts with overlay */ | ||||
|     -webkit-tap-highlight-color: transparent; | ||||
|     width: 16px; | ||||
|     --checkradio-height: 16px; | ||||
|     height: var(--checkradio-height); | ||||
|     padding: 0; | ||||
|     margin: 0 6px 0 0; | ||||
|     /* Don't have transitions for outline in order to be consistent | ||||
|        with other elements */ | ||||
|     transition: all 0.2s, outline-color 0s, outline-offset 0s; | ||||
| 
 | ||||
|     /* A transparent outline in order to work around a graphical clipping issue | ||||
|        in WebKit. See bug: https://bugs.webkit.org/show_bug.cgi?id=256003 */ | ||||
|     outline: 1px solid transparent; | ||||
|     position: relative; /* Since ::before & ::after are absolute positioned */ | ||||
| 
 | ||||
|     /* We want to align with the middle of capital letters, this requires | ||||
|        a workaround. The default behavior is to align the bottom of the element | ||||
|        on top of the text baseline, this is too far up. | ||||
|        We want to push the element down half the difference in height between | ||||
|        it and a capital X. In our font, the height of a capital "X" is 0.698em. | ||||
|      */ | ||||
|     vertical-align: calc(0px - (var(--checkradio-height) - 0.698em) / 2); | ||||
|     /* FIXME: Could write 1cap instead of 0.698em, but it's only supported in | ||||
|               Firefox as of 2023 */ | ||||
|     /* FIXME: We probably want to use round() here, see bug 8148 */ | ||||
| } | ||||
| input[type=radio]:focus-visible, | ||||
| input[type=checkbox]:focus-visible { | ||||
|     outline-color: var(--novnc-lightblue); | ||||
| } | ||||
| input[type=checkbox]::before, | ||||
| input[type=checkbox]:not(.toggle)::after, | ||||
| input[type=radio]::before, | ||||
| input[type=radio]::after { | ||||
|     content: ""; | ||||
|     display: block; /* width & height doesn't work on inline elements */ | ||||
|     transition: inherit; | ||||
|     /* Let's prevent the pseudo-elements from taking up layout space so that | ||||
|        the ::before and ::after pseudo-elements can be in the same place. This | ||||
|        is also required for vertical-align: baseline to work like we want it to | ||||
|        on radio/checkboxes. If the pseudo-elements take up layout space, the | ||||
|        baseline of text inside them will be used instead. */ | ||||
|     position: absolute; | ||||
| } | ||||
| input[type=checkbox]:not(.toggle)::after, | ||||
| input[type=radio]::after { | ||||
|     width: 10px; | ||||
|     height: 2px; | ||||
|     background-color: transparent; | ||||
|     border-radius: 2px; | ||||
| } | ||||
| 
 | ||||
| /* ------- CHECKBOXES ------- */ | ||||
| 
 | ||||
| input[type=checkbox]:not(.toggle) { | ||||
|     border-radius: 4px; | ||||
| } | ||||
| input[type=checkbox]:not(.toggle):checked, | ||||
| input[type=checkbox]:not(.toggle):indeterminate { | ||||
|     background-color: var(--novnc-blue); | ||||
|     background-image: var(--button-activation-overlay-light); | ||||
|     background-blend-mode: overlay; | ||||
| } | ||||
| input[type=checkbox]:not(.toggle)::before { | ||||
|     width: 25%; | ||||
|     height: 55%; | ||||
|     border-style: solid; | ||||
|     border-color: transparent; | ||||
|     border-width: 0 2px 2px 0; | ||||
|     border-radius: 1px; | ||||
|     transform: translateY(-1px) rotate(35deg); | ||||
| } | ||||
| input[type=checkbox]:not(.toggle):checked::before { | ||||
|     border-color: white; | ||||
| } | ||||
| input[type=checkbox]:not(.toggle):indeterminate::after { | ||||
|     background-color: white; | ||||
| } | ||||
| 
 | ||||
| /* ------- RADIO BUTTONS ------- */ | ||||
| 
 | ||||
| input[type=radio] { | ||||
|     border-radius: 50%; | ||||
|     border: 1px solid transparent; /* To ensure a smooth transition */ | ||||
| } | ||||
| input[type=radio]:checked { | ||||
|     border: 4px solid var(--novnc-blue); | ||||
|     background-color: white; | ||||
|     /* button-activation-overlay should be removed from the radio | ||||
|        element to not interfere with button-activation-overlay-light | ||||
|        that is set on the ::before element. */ | ||||
|     background-image: none; | ||||
| } | ||||
| input[type=radio]::before { | ||||
|     width: inherit; | ||||
|     height: inherit; | ||||
|     border-radius: inherit; | ||||
|     /* We can achieve the highlight overlay effect on border colors by | ||||
|        setting button-activation-overlay-light on an element that stays | ||||
|        on top (z-axis) of the element with a border. */ | ||||
|     background-image: var(--button-activation-overlay-light); | ||||
|     mix-blend-mode: overlay; | ||||
|     opacity: 0; | ||||
| } | ||||
| input[type=radio]:checked::before { | ||||
|     opacity: 1; | ||||
| } | ||||
| input[type=radio]:indeterminate::after { | ||||
|     background-color: black; | ||||
| } | ||||
| 
 | ||||
| /* ------- TOGGLE SWITCHES ------- */ | ||||
| 
 | ||||
| /* These are meant to be used instead of checkboxes in some cases. If all of | ||||
|    the following critera are true you should use a toggle switch: | ||||
| 
 | ||||
|     * The choice is a simple ON/OFF or ENABLE/DISABLE | ||||
|     * The choice doesn't give the feeling of "I agree" or "I confirm" | ||||
|     * There are not multiple related & grouped options | ||||
|  */ | ||||
| 
 | ||||
| input[type=checkbox].toggle { | ||||
|     display: inline-block; | ||||
|     --checkradio-height: 18px; /* Height value used in calc, see above */ | ||||
|     width: 31px; | ||||
|     cursor: pointer; | ||||
|     user-select: none; | ||||
|     -webkit-user-select: none; | ||||
|     border-radius: 9px; | ||||
| } | ||||
| input[type=checkbox].toggle:disabled { | ||||
|     cursor: default; | ||||
| } | ||||
| input[type=checkbox].toggle:indeterminate { | ||||
|     background-color: var(--novnc-buttongrey); | ||||
|     background-image: var(--button-activation-overlay); | ||||
| } | ||||
| input[type=checkbox].toggle:checked { | ||||
|     background-color: var(--novnc-blue); | ||||
|     background-image: var(--button-activation-overlay-light); | ||||
|     background-blend-mode: overlay; | ||||
| } | ||||
| input[type=checkbox].toggle::before { | ||||
|     --circle-diameter: 10px; | ||||
|     --circle-offset: 4px; | ||||
|     width: var(--circle-diameter); | ||||
|     height: var(--circle-diameter); | ||||
|     top: var(--circle-offset); | ||||
|     left: var(--circle-offset); | ||||
|     background: white; | ||||
|     border-radius: 6px; | ||||
| } | ||||
| input[type=checkbox].toggle:checked::before { | ||||
|     left: calc(100% - var(--circle-offset) - var(--circle-diameter)); | ||||
| } | ||||
| input[type=checkbox].toggle:indeterminate::before { | ||||
|     left: calc(50% - var(--circle-diameter) / 2); | ||||
| } | ||||
| 
 | ||||
| /* ------- RANGE SLIDERS ------- */ | ||||
| 
 | ||||
| input[type=range] { | ||||
|     border: unset; | ||||
|     border-radius: 8px; | ||||
|     height: 15px; | ||||
|     padding: 0; | ||||
|     background: transparent; | ||||
|     /* Needed to get properly rounded corners on -moz-range-progress | ||||
|        when the thumb is all the way to the right. Without overflow | ||||
|        hidden, the pointy edges of the progress track shows to the | ||||
|        right of the thumb. */ | ||||
|     overflow: hidden; | ||||
| } | ||||
| @supports selector(::-webkit-slider-thumb) { | ||||
|     input[type=range] { | ||||
|         /* Needs a fixed width to match clip-path */ | ||||
|         width: 125px; | ||||
|         /* overflow: hidden is not ideal for hiding the left part of the box | ||||
|            shadow of -webkit-slider-thumb since it doesn't match the smaller | ||||
|            border-radius of the progress track. The below clip-path has two | ||||
|            circular sides to make the ends of the track have correctly rounded | ||||
|            corners. The clip path shape looks something like this: | ||||
| 
 | ||||
|                   +-------------------------------+ | ||||
|               /---|                               |---\ | ||||
|              |                                         | | ||||
|               \---|                               |---/ | ||||
|                   +-------------------------------+ | ||||
| 
 | ||||
|            The larger middle part of the clip path is made to have room for the | ||||
|            thumb. By using margins on the track, we prevent the thumb from | ||||
|            touching the ends of the track. | ||||
|          */ | ||||
|         clip-path: path(' \ | ||||
|          M 4.5 3 \ | ||||
|          L 4.5 0 \ | ||||
|          L 120.5 0 \ | ||||
|          L 120.5 3 \ | ||||
|          A 1 1 0 0 1 120.5 12 \ | ||||
|          L 120.5 15 \ | ||||
|          L 4.5 15 \ | ||||
|          L 4.5 12 \ | ||||
|          A 1 1 0 0 1 4.5 3 \ | ||||
|         '); | ||||
|     } | ||||
| } | ||||
| input[type=range]:hover { | ||||
|     cursor: grab; | ||||
| } | ||||
| input[type=range]:active { | ||||
|     cursor: grabbing; | ||||
| } | ||||
| input[type=range]:disabled { | ||||
|     cursor: default; | ||||
| } | ||||
| input[type=range]:focus-visible { | ||||
|     clip-path: none; /* Otherwise it hides the outline */ | ||||
| } | ||||
| /* -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: var(--novnc-buttongrey); | ||||
|     height: 7px; | ||||
|     border-radius: 4px; | ||||
|     margin: 0 3px; | ||||
| } | ||||
| input[type=range]::-moz-range-track { | ||||
|     background-color: var(--novnc-buttongrey); | ||||
|     height: 7px; | ||||
|     border-radius: 4px; | ||||
| } | ||||
| input[type=range]::-moz-range-progress { | ||||
|     background-color: var(--novnc-blue); | ||||
|     height: 9px; | ||||
|     /* Needs rounded corners only on the left side. Otherwise the rounding of | ||||
|        the progress track starts before the thumb, when the thumb is close to | ||||
|        the left edge. */ | ||||
|     border-radius: 5px 0 0 5px; | ||||
| } | ||||
| input[type=range]::-webkit-slider-thumb { | ||||
|     appearance: none; | ||||
|     width: 15px; | ||||
|     height: 15px; | ||||
|     border-radius: 50%; | ||||
|     background-color: white; | ||||
|     background-image: var(--button-activation-overlay); | ||||
|     /* Disable Chrome's touch tap highlight to avoid conflicts with overlay */ | ||||
|     -webkit-tap-highlight-color: transparent; | ||||
|     border: 3px solid var(--novnc-blue); | ||||
|     margin-top: -4px; /* (track height / 2) - (thumb height /2) */ | ||||
| 
 | ||||
|     /* Since there is no way to style the left part of the range track in | ||||
|        webkit, we add a large shadow (1000px wide) to the left of the thumb and | ||||
|        then crop it with a clip-path shaped like this: | ||||
|                               ___ | ||||
|         +-------------------/     \ | ||||
|         |      progress     |Thumb| | ||||
|         +-------------------\ ___ / | ||||
| 
 | ||||
|         The large left part of the shadow is clipped by another clip-path on on | ||||
|         the main range input element. */ | ||||
|     /* FIXME: We can remove the box shadow workaround when this is standardized: | ||||
|               https://github.com/w3c/csswg-drafts/issues/4410 */ | ||||
| 
 | ||||
|     box-shadow: calc(-100vw - 8px) 0 0 100vw var(--novnc-blue); | ||||
|     clip-path: path(' \ | ||||
|      M -1000 3 \ | ||||
|      L 3 3 \ | ||||
|      L 15 7.5 \ | ||||
|      A 1 1 0 0 1 0 7.5 \ | ||||
|      A 1 1 0 0 1 15 7.5 \ | ||||
|      L 3 12 \ | ||||
|      L -1000 12 Z \ | ||||
|     '); | ||||
| } | ||||
| input[type=range]::-moz-range-thumb { | ||||
|     appearance: none; | ||||
|     width: 15px; | ||||
|     height: 15px; | ||||
|     border-radius: 50%; | ||||
|     box-sizing: border-box; | ||||
|     background-color: white; | ||||
|     background-image: var(--button-activation-overlay); | ||||
|     border: 3px solid var(--novnc-blue); | ||||
|     margin-top: -7px; | ||||
| } | ||||
| 
 | ||||
| /* ------- FILE CHOOSERS ------- */ | ||||
| 
 | ||||
| input[type=file] { | ||||
|     background-image: none; | ||||
|     border: none; | ||||
| } | ||||
| input::file-selector-button { | ||||
|     margin-right: 6px; | ||||
| } | ||||
| input[type=file]:focus-visible { | ||||
|     outline: none; /* We outline the button instead of the entire element */ | ||||
| } | ||||
| 
 | ||||
| /* ------- SELECT BUTTONS ------- */ | ||||
| 
 | ||||
| select { | ||||
|     --select-arrow: url('data:image/svg+xml;utf8, \ | ||||
|         <svg width="11" height="6" version="1.1" viewBox="0 0 11 6" \ | ||||
|              xmlns="http://www.w3.org/2000/svg"> \ | ||||
|             <path d="m10.5.5-5 5-5-5" fill="none" \ | ||||
|                   stroke="black" stroke-width="1.5" \ | ||||
|                   stroke-linecap="round" stroke-linejoin="round"/> \ | ||||
|         </svg>'); | ||||
| 
 | ||||
|     /* FIXME: A bug in Firefox, requires a workaround for the background: | ||||
|               https://bugzilla.mozilla.org/show_bug.cgi?id=1810958 */ | ||||
|     /* The dropdown list will show the select element's background above and | ||||
|        below the options in Firefox. We want the entire dropdown to be white. */ | ||||
|     background-color: white; | ||||
|     /* However, we don't want the select element to actually show a white | ||||
|        background, so let's place a gradient above it with the color we want. */ | ||||
|     --grey-background: linear-gradient(var(--novnc-buttongrey) 100%, | ||||
|                                        transparent); | ||||
|     background-image: | ||||
|         var(--select-arrow), | ||||
|         var(--button-activation-overlay), | ||||
|         var(--grey-background); | ||||
|     background-position: calc(100% - var(--input-xpadding)), left top, left top; | ||||
|     background-repeat: no-repeat; | ||||
|     padding-right: calc(2*var(--input-xpadding) + 11px); | ||||
|     overflow: auto; | ||||
| } | ||||
| /* FIXME: :active isn't set when the <select> is opened in Firefox: | ||||
|           https://bugzilla.mozilla.org/show_bug.cgi?id=1805406 */ | ||||
| select:active { | ||||
|     /* Rotated arrow */ | ||||
|     background-image: url('data:image/svg+xml;utf8, \ | ||||
|         <svg width="11" height="6" version="1.1" viewBox="0 0 11 6" \ | ||||
|              xmlns="http://www.w3.org/2000/svg" transform="rotate(180)"> \ | ||||
|             <path d="m10.5.5-5 5-5-5" fill="none" \ | ||||
|                   stroke="black" stroke-width="1.5" \ | ||||
|                   stroke-linecap="round" stroke-linejoin="round"/> \ | ||||
|         </svg>'), | ||||
|         var(--button-activation-overlay), | ||||
|         var(--grey-background); | ||||
| } | ||||
| select:disabled { | ||||
|     background-image: | ||||
|         var(--select-arrow), | ||||
|         var(--grey-background); | ||||
| } | ||||
| /* Note that styling for <option> doesn't work in all browsers | ||||
|    since its often drawn directly by the OS. We are generally very | ||||
|    limited in what we can change here. */ | ||||
| option { | ||||
|     /* Prevent Chrome from inheriting background-color from the <select> */ | ||||
|     background-color: white; | ||||
|     color: black; | ||||
|     font-weight: normal; | ||||
|     background-image: var(--button-activation-overlay); | ||||
| } | ||||
| option:checked { | ||||
|     background-color: var(--novnc-lightgrey); | ||||
| } | ||||
| /* Change the look when the <select> isn't used as a dropdown. When "size" | ||||
|    or "multiple" are set, these elements behaves more like lists. */ | ||||
| select[size]:not([size="1"]), select[multiple] { | ||||
|     background-color: white; | ||||
|     background-image: unset; /* Don't show the arrow and other gradients */ | ||||
|     border: 1px solid var(--novnc-lightgrey); | ||||
|     padding: 0; | ||||
|     font-weight: normal; /* Without this, options get bold font in WebKit. */ | ||||
| 
 | ||||
|     /* As an exception to the "list"-look, multi-selects in Chrome on Android, | ||||
|        and Safari on iOS, are unfortunately designed to be shown as a single | ||||
|        line. We can mitigate this inconsistency by at least fixing the height | ||||
|        here. By setting a min-height that matches other input elements, it | ||||
|        doesn't look too much out of place: | ||||
|          (1px border * 2) + (6.5px padding * 2) + 24px line-height = 39px */ | ||||
|     min-height: 39px; | ||||
| } | ||||
| select[size]:not([size="1"]):focus-visible, | ||||
| select[multiple]:focus-visible { | ||||
|     /* Text input style focus-visible highlight */ | ||||
|     outline-offset: -1px; | ||||
| } | ||||
| select[size]:not([size="1"]) option, select[multiple] option { | ||||
|     overflow: hidden; | ||||
|     text-overflow: ellipsis; | ||||
|     padding: 4px var(--input-xpadding); | ||||
| } | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2019 The noVNC Authors | ||||
|  * Copyright (C) 2019 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
| @ -20,8 +20,12 @@ import * as WebUtil from "./webutil.js"; | ||||
| 
 | ||||
| const PAGE_TITLE = "noVNC"; | ||||
| 
 | ||||
| const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"]; | ||||
| 
 | ||||
| const UI = { | ||||
| 
 | ||||
|     customSettings: {}, | ||||
| 
 | ||||
|     connected: false, | ||||
|     desktopName: "", | ||||
| 
 | ||||
| @ -41,23 +45,33 @@ const UI = { | ||||
|     inhibitReconnect: true, | ||||
|     reconnectCallback: null, | ||||
|     reconnectPassword: null, | ||||
| 
 | ||||
|     fullScreen: false, | ||||
| 
 | ||||
|     prime() { | ||||
|         return WebUtil.initSettings().then(() => { | ||||
|             if (document.readyState === "interactive" || document.readyState === "complete") { | ||||
|                 return UI.start(); | ||||
|     async start(options={}) { | ||||
|         UI.customSettings = options.settings || {}; | ||||
|         if (UI.customSettings.defaults === undefined) { | ||||
|             UI.customSettings.defaults = {}; | ||||
|         } | ||||
|         if (UI.customSettings.mandatory === undefined) { | ||||
|             UI.customSettings.mandatory = {}; | ||||
|         } | ||||
| 
 | ||||
|             return new Promise((resolve, reject) => { | ||||
|                 document.addEventListener('DOMContentLoaded', () => UI.start().then(resolve).catch(reject)); | ||||
|             }); | ||||
|         }); | ||||
|     }, | ||||
|         // Set up translations
 | ||||
|         try { | ||||
|             await l10n.setup(LINGUAS, "app/locale/"); | ||||
|         } catch (err) { | ||||
|             Log.Error("Failed to load translations: " + err); | ||||
|         } | ||||
| 
 | ||||
|     // Render default UI and initialize settings menu
 | ||||
|     start() { | ||||
|         // Initialize setting storage
 | ||||
|         await WebUtil.initSettings(); | ||||
| 
 | ||||
|         // Wait for the page to load
 | ||||
|         if (document.readyState !== "interactive" && document.readyState !== "complete") { | ||||
|             await new Promise((resolve, reject) => { | ||||
|                 document.addEventListener('DOMContentLoaded', resolve); | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         UI.initSettings(); | ||||
| 
 | ||||
| @ -68,26 +82,24 @@ const UI = { | ||||
|         // insecure context
 | ||||
|         if (!window.isSecureContext) { | ||||
|             // FIXME: This gets hidden when connecting
 | ||||
|             UI.showStatus(_("HTTPS is required for full functionality"), 'error'); | ||||
|             UI.showStatus(_("Running without HTTPS is not recommended, crashes or other issues are likely."), 'error'); | ||||
|         } | ||||
| 
 | ||||
|         // Try to fetch version number
 | ||||
|         fetch('./package.json') | ||||
|             .then((response) => { | ||||
|         try { | ||||
|             let response = await fetch('./package.json'); | ||||
|             if (!response.ok) { | ||||
|                 throw Error("" + response.status + " " + response.statusText); | ||||
|             } | ||||
|                 return response.json(); | ||||
|             }) | ||||
|             .then((packageInfo) => { | ||||
| 
 | ||||
|             let packageInfo = await response.json(); | ||||
|             Array.from(document.getElementsByClassName('noVNC_version')).forEach(el => el.innerText = packageInfo.version); | ||||
|             }) | ||||
|             .catch((err) => { | ||||
|         } catch (err) { | ||||
|             Log.Error("Couldn't fetch package.json: " + err); | ||||
|             Array.from(document.getElementsByClassName('noVNC_version_wrapper')) | ||||
|                 .concat(Array.from(document.getElementsByClassName('noVNC_version_separator'))) | ||||
|                 .forEach(el => el.style.display = 'none'); | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         // Adapt the interface for touch screen devices
 | ||||
|         if (isTouchDevice) { | ||||
| @ -122,7 +134,7 @@ const UI = { | ||||
| 
 | ||||
|         document.documentElement.classList.remove("noVNC_loading"); | ||||
| 
 | ||||
|         let autoconnect = WebUtil.getConfigVar('autoconnect', false); | ||||
|         let autoconnect = UI.getSetting('autoconnect'); | ||||
|         if (autoconnect === 'true' || autoconnect == '1') { | ||||
|             autoconnect = true; | ||||
|             UI.connect(); | ||||
| @ -131,8 +143,6 @@ const UI = { | ||||
|             // Show the connect panel on first load unless autoconnecting
 | ||||
|             UI.openConnectPanel(); | ||||
|         } | ||||
| 
 | ||||
|         return Promise.resolve(UI.rfb); | ||||
|     }, | ||||
| 
 | ||||
|     initFullscreen() { | ||||
| @ -160,6 +170,8 @@ const UI = { | ||||
|         UI.initSetting('logging', 'warn'); | ||||
|         UI.updateLogging(); | ||||
| 
 | ||||
|         UI.setupSettingLabels(); | ||||
| 
 | ||||
|         // if port == 80 (or 443) then it won't be present and should be
 | ||||
|         // set manually
 | ||||
|         let port = window.location.port; | ||||
| @ -177,19 +189,20 @@ const UI = { | ||||
|         UI.initSetting('port', port); | ||||
|         UI.initSetting('token', window.location.token); | ||||
|         UI.initSetting('encrypt', (window.location.protocol === "https:")); | ||||
|         UI.initSetting('password'); | ||||
|         UI.initSetting('autoconnect', false); | ||||
|         UI.initSetting('view_clip', false); | ||||
|         UI.initSetting('resize', 'off'); | ||||
|         UI.initSetting('quality', 6); | ||||
|         UI.initSetting('compression', 2); | ||||
|         UI.initSetting('shared', true); | ||||
|         UI.initSetting('bell', 'on'); | ||||
|         UI.initSetting('view_only', false); | ||||
|         UI.initSetting('show_dot', false); | ||||
|         UI.initSetting('path', 'websockify'); | ||||
|         UI.initSetting('repeaterID', ''); | ||||
|         UI.initSetting('reconnect', false); | ||||
|         UI.initSetting('reconnect_delay', 5000); | ||||
| 
 | ||||
|         UI.setupSettingLabels(); | ||||
|     }, | ||||
|     // Adds a link to the label elements on the corresponding input elements
 | ||||
|     setupSettingLabels() { | ||||
| @ -767,6 +780,10 @@ const UI = { | ||||
| 
 | ||||
|     // Initial page load read/initialization of settings
 | ||||
|     initSetting(name, defVal) { | ||||
|         // Has the user overridden the default value?
 | ||||
|         if (name in UI.customSettings.defaults) { | ||||
|             defVal = UI.customSettings.defaults[name]; | ||||
|         } | ||||
|         // Check Query string followed by cookie
 | ||||
|         let val = WebUtil.getConfigVar(name); | ||||
|         if (val === null) { | ||||
| @ -774,6 +791,11 @@ const UI = { | ||||
|         } | ||||
|         WebUtil.setSetting(name, val); | ||||
|         UI.updateSetting(name); | ||||
|         // Has the user forced a value?
 | ||||
|         if (name in UI.customSettings.mandatory) { | ||||
|             val = UI.customSettings.mandatory[name]; | ||||
|             UI.forceSetting(name, val); | ||||
|         } | ||||
|         return val; | ||||
|     }, | ||||
| 
 | ||||
| @ -792,9 +814,12 @@ const UI = { | ||||
|         let value = UI.getSetting(name); | ||||
| 
 | ||||
|         const ctrl = document.getElementById('noVNC_setting_' + name); | ||||
|         if (ctrl === null) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (ctrl.type === 'checkbox') { | ||||
|             ctrl.checked = value; | ||||
| 
 | ||||
|         } else if (typeof ctrl.options !== 'undefined') { | ||||
|             for (let i = 0; i < ctrl.options.length; i += 1) { | ||||
|                 if (ctrl.options[i].value === value) { | ||||
| @ -827,7 +852,8 @@ const UI = { | ||||
|     getSetting(name) { | ||||
|         const ctrl = document.getElementById('noVNC_setting_' + name); | ||||
|         let val = WebUtil.readSetting(name); | ||||
|         if (typeof val !== 'undefined' && val !== null && ctrl.type === 'checkbox') { | ||||
|         if (typeof val !== 'undefined' && val !== null && | ||||
|             ctrl !== null && ctrl.type === 'checkbox') { | ||||
|             if (val.toString().toLowerCase() in {'0': 1, 'no': 1, 'false': 1}) { | ||||
|                 val = false; | ||||
|             } else { | ||||
| @ -842,14 +868,22 @@ const UI = { | ||||
|     // disable the labels that belong to disabled input elements.
 | ||||
|     disableSetting(name) { | ||||
|         const ctrl = document.getElementById('noVNC_setting_' + name); | ||||
|         if (ctrl !== null) { | ||||
|             ctrl.disabled = true; | ||||
|             if (ctrl.label !== undefined) { | ||||
|                 ctrl.label.classList.add('noVNC_disabled'); | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     enableSetting(name) { | ||||
|         const ctrl = document.getElementById('noVNC_setting_' + name); | ||||
|         if (ctrl !== null) { | ||||
|             ctrl.disabled = false; | ||||
|             if (ctrl.label !== undefined) { | ||||
|                 ctrl.label.classList.remove('noVNC_disabled'); | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
| /* ------^------- | ||||
| @ -1040,7 +1074,7 @@ const UI = { | ||||
|         const extra = UI.getSetting('extra'); | ||||
| 
 | ||||
|         if (typeof password === 'undefined') { | ||||
|             password = WebUtil.getConfigVar('password'); | ||||
|             password = UI.getSetting('password'); | ||||
|             UI.reconnectPassword = password; | ||||
|         } | ||||
| 
 | ||||
| @ -1050,36 +1084,55 @@ const UI = { | ||||
| 
 | ||||
|         UI.hideStatus(); | ||||
| 
 | ||||
|         if (!host) { | ||||
|             Log.Error("Can't connect when host is: " + host); | ||||
|             UI.showStatus(_("Must set host"), 'error'); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         UI.closeConnectPanel(); | ||||
| 
 | ||||
|         UI.updateVisualState('connecting'); | ||||
| 
 | ||||
|         let url; | ||||
| 
 | ||||
|         url = UI.getSetting('encrypt') ? 'wss' : 'ws'; | ||||
|         if (host) { | ||||
|             url = new URL("https://" + host); | ||||
| 
 | ||||
|         url += '://' + host; | ||||
|             url.protocol = UI.getSetting('encrypt') ? 'wss:' : 'ws:'; | ||||
|             if (port) { | ||||
|             url += ':' + port; | ||||
|                 url.port = port; | ||||
|             } | ||||
|         url += '/' + path; | ||||
|         url += '?token=' + token; | ||||
| 
 | ||||
|             // "./" is needed to force URL() to interpret the path-variable as
 | ||||
|             // a path and not as an URL. This is relevant if for example path
 | ||||
|             // starts with more than one "/", in which case it would be
 | ||||
|             // interpreted as a host name instead.
 | ||||
|             url = new URL("./" + path, url); | ||||
|         } else { | ||||
|             // Current (May 2024) browsers support relative WebSocket
 | ||||
|             // URLs natively, but we need to support older browsers for
 | ||||
|             // some time.
 | ||||
|             url = new URL(path, location.href); | ||||
|             url.protocol = (window.location.protocol === "https:") ? 'wss:' : 'ws:'; | ||||
|         } | ||||
| 
 | ||||
|         url.href += '/' + path; | ||||
|         url.href += '?token=' + token; | ||||
| 
 | ||||
|         if (extra) { | ||||
|             url += '&extra=' + extra | ||||
|             url.href += '&extra=' + extra | ||||
|         } | ||||
| 
 | ||||
|         UI.rfb = new RFB(document.getElementById('noVNC_container'), url, | ||||
| 
 | ||||
|         try { | ||||
|             UI.rfb = new RFB(document.getElementById('noVNC_container'), | ||||
|                              url.href, | ||||
|                              { shared: UI.getSetting('shared'), | ||||
|                                repeaterID: UI.getSetting('repeaterID'), | ||||
|                                language: WebUtil.getConfigVar('language'), | ||||
|                                credentials: { password: password } }); | ||||
|         } catch (exc) { | ||||
|             Log.Error("Failed to connect to server: " + exc); | ||||
|             UI.updateVisualState('disconnected'); | ||||
|             UI.showStatus(_("Failed to connect to server: ") + exc, 'error'); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         UI.rfb.addEventListener("connect", UI.connectFinished); | ||||
|         UI.rfb.addEventListener("disconnect", UI.disconnectFinished); | ||||
|         UI.rfb.addEventListener("serververification", UI.serverVerify); | ||||
| @ -1758,7 +1811,7 @@ const UI = { | ||||
|     }, | ||||
| 
 | ||||
|     bell(e) { | ||||
|         if (WebUtil.getConfigVar('bell', 'on') === 'on') { | ||||
|         if (UI.getSetting('bell') === 'on') { | ||||
|             const promise = document.getElementById('noVNC_bell').play(); | ||||
|             // The standards disagree on the return value here
 | ||||
|             if (promise) { | ||||
| @ -1789,22 +1842,4 @@ const UI = { | ||||
|  */ | ||||
| }; | ||||
| 
 | ||||
| // Set up translations
 | ||||
| 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 { | ||||
|     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); | ||||
| } | ||||
| 
 | ||||
| export default UI; | ||||
|  | ||||
| @ -1,21 +1,21 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2019 The noVNC Authors | ||||
|  * Copyright (C) 2019 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
|  */ | ||||
| 
 | ||||
| import { initLogging as mainInitLogging } from '../core/util/logging.js'; | ||||
| import * as Log from '../core/util/logging.js'; | ||||
| 
 | ||||
| // init log level reading the logging HTTP param
 | ||||
| export function initLogging(level) { | ||||
|     "use strict"; | ||||
|     if (typeof level !== "undefined") { | ||||
|         mainInitLogging(level); | ||||
|         Log.initLogging(level); | ||||
|     } else { | ||||
|         const param = document.location.href.match(/logging=([A-Za-z0-9._-]*)/); | ||||
|         mainInitLogging(param || undefined); | ||||
|         Log.initLogging(param || undefined); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -25,14 +25,14 @@ export function initLogging(level) { | ||||
| //
 | ||||
| // 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
 | ||||
| // https://www.example.com#myqueryparam=myvalue&password=secretvalue
 | ||||
| //
 | ||||
| // Even Mixing public and non public parameters will work:
 | ||||
| // https://www.example.com?nonsecretparam=example.com#password=secreatvalue
 | ||||
| // Even mixing public and non public parameters will work:
 | ||||
| // https://www.example.com?nonsecretparam=example.com#password=secretvalue
 | ||||
| export function getQueryVar(name, defVal) { | ||||
|     "use strict"; | ||||
|     const re = new RegExp('.*[?&]' + name + '=([^&#]*)'), | ||||
|           match = ''.concat(document.location.href, window.location.hash).match(re); | ||||
|           match = document.location.href.match(re); | ||||
|     if (typeof defVal === 'undefined') { defVal = null; } | ||||
| 
 | ||||
|     if (match) { | ||||
| @ -138,13 +138,22 @@ export function writeSetting(name, value) { | ||||
|     "use strict"; | ||||
|     if (settings[name] === value) return; | ||||
|     settings[name] = value; | ||||
|     if (window.chrome && window.chrome.storage) { | ||||
|         window.chrome.storage.sync.set(settings); | ||||
|     } else { | ||||
|         localStorageSet(name, value); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export function readSetting(name, defaultValue) { | ||||
|     "use strict"; | ||||
|     let value; | ||||
|     if ((name in settings) || (window.chrome && window.chrome.storage)) { | ||||
|         value = settings[name]; | ||||
| 
 | ||||
|     } else { | ||||
|         value = localStorageGet(name); | ||||
|         settings[name] = value; | ||||
|     } | ||||
|     if (typeof value === "undefined") { | ||||
|         value = null; | ||||
|     } | ||||
| @ -164,4 +173,73 @@ export function eraseSetting(name) { | ||||
|     // between this delete and the next read, it could lead to an unexpected
 | ||||
|     // value change.
 | ||||
|     delete settings[name]; | ||||
|     if (window.chrome && window.chrome.storage) { | ||||
|         window.chrome.storage.sync.remove(name); | ||||
|     } else { | ||||
|         localStorageRemove(name); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| let loggedMsgs = []; | ||||
| function logOnce(msg, level = "warn") { | ||||
|     if (!loggedMsgs.includes(msg)) { | ||||
|         switch (level) { | ||||
|             case "error": | ||||
|                 Log.Error(msg); | ||||
|                 break; | ||||
|             case "warn": | ||||
|                 Log.Warn(msg); | ||||
|                 break; | ||||
|             case "debug": | ||||
|                 Log.Debug(msg); | ||||
|                 break; | ||||
|             default: | ||||
|                 Log.Info(msg); | ||||
|         } | ||||
|         loggedMsgs.push(msg); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| let cookiesMsg = "Couldn't access noVNC settings, are cookies disabled?"; | ||||
| 
 | ||||
| function localStorageGet(name) { | ||||
|     let r; | ||||
|     try { | ||||
|         r = localStorage.getItem(name); | ||||
|     } catch (e) { | ||||
|         if (e instanceof DOMException) { | ||||
|             logOnce(cookiesMsg); | ||||
|             logOnce("'localStorage.getItem(" + name + ")' failed: " + e, | ||||
|                     "debug"); | ||||
|         } else { | ||||
|             throw e; | ||||
|         } | ||||
|     } | ||||
|     return r; | ||||
| } | ||||
| function localStorageSet(name, value) { | ||||
|     try { | ||||
|         localStorage.setItem(name, value); | ||||
|     } catch (e) { | ||||
|         if (e instanceof DOMException) { | ||||
|             logOnce(cookiesMsg); | ||||
|             logOnce("'localStorage.setItem(" + name + "," + value + | ||||
|                     ")' failed: " + e, "debug"); | ||||
|         } else { | ||||
|             throw e; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| function localStorageRemove(name) { | ||||
|     try { | ||||
|         localStorage.removeItem(name); | ||||
|     } catch (e) { | ||||
|         if (e instanceof DOMException) { | ||||
|             logOnce(cookiesMsg); | ||||
|             logOnce("'localStorage.removeItem(" + name + ")' failed: " + e, | ||||
|                     "debug"); | ||||
|         } else { | ||||
|             throw e; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										178
									
								
								systemvm/agent/noVNC/core/crypto/aes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								systemvm/agent/noVNC/core/crypto/aes.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,178 @@ | ||||
| export class AESECBCipher { | ||||
|     constructor() { | ||||
|         this._key = null; | ||||
|     } | ||||
| 
 | ||||
|     get algorithm() { | ||||
|         return { name: "AES-ECB" }; | ||||
|     } | ||||
| 
 | ||||
|     static async importKey(key, _algorithm, extractable, keyUsages) { | ||||
|         const cipher = new AESECBCipher; | ||||
|         await cipher._importKey(key, extractable, keyUsages); | ||||
|         return cipher; | ||||
|     } | ||||
| 
 | ||||
|     async _importKey(key, extractable, keyUsages) { | ||||
|         this._key = await window.crypto.subtle.importKey( | ||||
|             "raw", key, {name: "AES-CBC"}, extractable, keyUsages); | ||||
|     } | ||||
| 
 | ||||
|     async encrypt(_algorithm, plaintext) { | ||||
|         const x = new Uint8Array(plaintext); | ||||
|         if (x.length % 16 !== 0 || this._key === null) { | ||||
|             return null; | ||||
|         } | ||||
|         const n = x.length / 16; | ||||
|         for (let i = 0; i < n; i++) { | ||||
|             const y = new Uint8Array(await window.crypto.subtle.encrypt({ | ||||
|                 name: "AES-CBC", | ||||
|                 iv: new Uint8Array(16), | ||||
|             }, this._key, x.slice(i * 16, i * 16 + 16))).slice(0, 16); | ||||
|             x.set(y, i * 16); | ||||
|         } | ||||
|         return x; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class AESEAXCipher { | ||||
|     constructor() { | ||||
|         this._rawKey = null; | ||||
|         this._ctrKey = null; | ||||
|         this._cbcKey = null; | ||||
|         this._zeroBlock = new Uint8Array(16); | ||||
|         this._prefixBlock0 = this._zeroBlock; | ||||
|         this._prefixBlock1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); | ||||
|         this._prefixBlock2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); | ||||
|     } | ||||
| 
 | ||||
|     get algorithm() { | ||||
|         return { name: "AES-EAX" }; | ||||
|     } | ||||
| 
 | ||||
|     async _encryptBlock(block) { | ||||
|         const encrypted = await window.crypto.subtle.encrypt({ | ||||
|             name: "AES-CBC", | ||||
|             iv: this._zeroBlock, | ||||
|         }, this._cbcKey, block); | ||||
|         return new Uint8Array(encrypted).slice(0, 16); | ||||
|     } | ||||
| 
 | ||||
|     async _initCMAC() { | ||||
|         const k1 = await this._encryptBlock(this._zeroBlock); | ||||
|         const k2 = new Uint8Array(16); | ||||
|         const v = k1[0] >>> 6; | ||||
|         for (let i = 0; i < 15; i++) { | ||||
|             k2[i] = (k1[i + 1] >> 6) | (k1[i] << 2); | ||||
|             k1[i] = (k1[i + 1] >> 7) | (k1[i] << 1); | ||||
|         } | ||||
|         const lut = [0x0, 0x87, 0x0e, 0x89]; | ||||
|         k2[14] ^= v >>> 1; | ||||
|         k2[15] = (k1[15] << 2) ^ lut[v]; | ||||
|         k1[15] = (k1[15] << 1) ^ lut[v >> 1]; | ||||
|         this._k1 = k1; | ||||
|         this._k2 = k2; | ||||
|     } | ||||
| 
 | ||||
|     async _encryptCTR(data, counter) { | ||||
|         const encrypted = await window.crypto.subtle.encrypt({ | ||||
|             name: "AES-CTR", | ||||
|             counter: counter, | ||||
|             length: 128 | ||||
|         }, this._ctrKey, data); | ||||
|         return new Uint8Array(encrypted); | ||||
|     } | ||||
| 
 | ||||
|     async _decryptCTR(data, counter) { | ||||
|         const decrypted = await window.crypto.subtle.decrypt({ | ||||
|             name: "AES-CTR", | ||||
|             counter: counter, | ||||
|             length: 128 | ||||
|         }, this._ctrKey, data); | ||||
|         return new Uint8Array(decrypted); | ||||
|     } | ||||
| 
 | ||||
|     async _computeCMAC(data, prefixBlock) { | ||||
|         if (prefixBlock.length !== 16) { | ||||
|             return null; | ||||
|         } | ||||
|         const n = Math.floor(data.length / 16); | ||||
|         const m = Math.ceil(data.length / 16); | ||||
|         const r = data.length - n * 16; | ||||
|         const cbcData = new Uint8Array((m + 1) * 16); | ||||
|         cbcData.set(prefixBlock); | ||||
|         cbcData.set(data, 16); | ||||
|         if (r === 0) { | ||||
|             for (let i = 0; i < 16; i++) { | ||||
|                 cbcData[n * 16 + i] ^= this._k1[i]; | ||||
|             } | ||||
|         } else { | ||||
|             cbcData[(n + 1) * 16 + r] = 0x80; | ||||
|             for (let i = 0; i < 16; i++) { | ||||
|                 cbcData[(n + 1) * 16 + i] ^= this._k2[i]; | ||||
|             } | ||||
|         } | ||||
|         let cbcEncrypted = await window.crypto.subtle.encrypt({ | ||||
|             name: "AES-CBC", | ||||
|             iv: this._zeroBlock, | ||||
|         }, this._cbcKey, cbcData); | ||||
| 
 | ||||
|         cbcEncrypted = new Uint8Array(cbcEncrypted); | ||||
|         const mac = cbcEncrypted.slice(cbcEncrypted.length - 32, cbcEncrypted.length - 16); | ||||
|         return mac; | ||||
|     } | ||||
| 
 | ||||
|     static async importKey(key, _algorithm, _extractable, _keyUsages) { | ||||
|         const cipher = new AESEAXCipher; | ||||
|         await cipher._importKey(key); | ||||
|         return cipher; | ||||
|     } | ||||
| 
 | ||||
|     async _importKey(key) { | ||||
|         this._rawKey = key; | ||||
|         this._ctrKey = await window.crypto.subtle.importKey( | ||||
|             "raw", key, {name: "AES-CTR"}, false, ["encrypt", "decrypt"]); | ||||
|         this._cbcKey = await window.crypto.subtle.importKey( | ||||
|             "raw", key, {name: "AES-CBC"}, false, ["encrypt"]); | ||||
|         await this._initCMAC(); | ||||
|     } | ||||
| 
 | ||||
|     async encrypt(algorithm, message) { | ||||
|         const ad = algorithm.additionalData; | ||||
|         const nonce = algorithm.iv; | ||||
|         const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0); | ||||
|         const encrypted = await this._encryptCTR(message, nCMAC); | ||||
|         const adCMAC = await this._computeCMAC(ad, this._prefixBlock1); | ||||
|         const mac = await this._computeCMAC(encrypted, this._prefixBlock2); | ||||
|         for (let i = 0; i < 16; i++) { | ||||
|             mac[i] ^= nCMAC[i] ^ adCMAC[i]; | ||||
|         } | ||||
|         const res = new Uint8Array(16 + encrypted.length); | ||||
|         res.set(encrypted); | ||||
|         res.set(mac, encrypted.length); | ||||
|         return res; | ||||
|     } | ||||
| 
 | ||||
|     async decrypt(algorithm, data) { | ||||
|         const encrypted = data.slice(0, data.length - 16); | ||||
|         const ad = algorithm.additionalData; | ||||
|         const nonce = algorithm.iv; | ||||
|         const mac = data.slice(data.length - 16); | ||||
|         const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0); | ||||
|         const adCMAC = await this._computeCMAC(ad, this._prefixBlock1); | ||||
|         const computedMac = await this._computeCMAC(encrypted, this._prefixBlock2); | ||||
|         for (let i = 0; i < 16; i++) { | ||||
|             computedMac[i] ^= nCMAC[i] ^ adCMAC[i]; | ||||
|         } | ||||
|         if (computedMac.length !== mac.length) { | ||||
|             return null; | ||||
|         } | ||||
|         for (let i = 0; i < mac.length; i++) { | ||||
|             if (computedMac[i] !== mac[i]) { | ||||
|                 return null; | ||||
|             } | ||||
|         } | ||||
|         const res = await this._decryptCTR(encrypted, nCMAC); | ||||
|         return res; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										34
									
								
								systemvm/agent/noVNC/core/crypto/bigint.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								systemvm/agent/noVNC/core/crypto/bigint.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | ||||
| export function modPow(b, e, m) { | ||||
|     let r = 1n; | ||||
|     b = b % m; | ||||
|     while (e > 0n) { | ||||
|         if ((e & 1n) === 1n) { | ||||
|             r = (r * b) % m; | ||||
|         } | ||||
|         e = e >> 1n; | ||||
|         b = (b * b) % m; | ||||
|     } | ||||
|     return r; | ||||
| } | ||||
| 
 | ||||
| export function bigIntToU8Array(bigint, padLength=0) { | ||||
|     let hex = bigint.toString(16); | ||||
|     if (padLength === 0) { | ||||
|         padLength = Math.ceil(hex.length / 2); | ||||
|     } | ||||
|     hex = hex.padStart(padLength * 2, '0'); | ||||
|     const length = hex.length / 2; | ||||
|     const arr = new Uint8Array(length); | ||||
|     for (let i = 0; i < length; i++) { | ||||
|         arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); | ||||
|     } | ||||
|     return arr; | ||||
| } | ||||
| 
 | ||||
| export function u8ArrayToBigInt(arr) { | ||||
|     let hex = '0x'; | ||||
|     for (let i = 0; i < arr.length; i++) { | ||||
|         hex += arr[i].toString(16).padStart(2, '0'); | ||||
|     } | ||||
|     return BigInt(hex); | ||||
| } | ||||
							
								
								
									
										90
									
								
								systemvm/agent/noVNC/core/crypto/crypto.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								systemvm/agent/noVNC/core/crypto/crypto.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,90 @@ | ||||
| import { AESECBCipher, AESEAXCipher } from "./aes.js"; | ||||
| import { DESCBCCipher, DESECBCipher } from "./des.js"; | ||||
| import { RSACipher } from "./rsa.js"; | ||||
| import { DHCipher } from "./dh.js"; | ||||
| import { MD5 } from "./md5.js"; | ||||
| 
 | ||||
| // A single interface for the cryptographic algorithms not supported by SubtleCrypto.
 | ||||
| // Both synchronous and asynchronous implmentations are allowed.
 | ||||
| class LegacyCrypto { | ||||
|     constructor() { | ||||
|         this._algorithms = { | ||||
|             "AES-ECB": AESECBCipher, | ||||
|             "AES-EAX": AESEAXCipher, | ||||
|             "DES-ECB": DESECBCipher, | ||||
|             "DES-CBC": DESCBCCipher, | ||||
|             "RSA-PKCS1-v1_5": RSACipher, | ||||
|             "DH": DHCipher, | ||||
|             "MD5": MD5, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     encrypt(algorithm, key, data) { | ||||
|         if (key.algorithm.name !== algorithm.name) { | ||||
|             throw new Error("algorithm does not match"); | ||||
|         } | ||||
|         if (typeof key.encrypt !== "function") { | ||||
|             throw new Error("key does not support encryption"); | ||||
|         } | ||||
|         return key.encrypt(algorithm, data); | ||||
|     } | ||||
| 
 | ||||
|     decrypt(algorithm, key, data) { | ||||
|         if (key.algorithm.name !== algorithm.name) { | ||||
|             throw new Error("algorithm does not match"); | ||||
|         } | ||||
|         if (typeof key.decrypt !== "function") { | ||||
|             throw new Error("key does not support encryption"); | ||||
|         } | ||||
|         return key.decrypt(algorithm, data); | ||||
|     } | ||||
| 
 | ||||
|     importKey(format, keyData, algorithm, extractable, keyUsages) { | ||||
|         if (format !== "raw") { | ||||
|             throw new Error("key format is not supported"); | ||||
|         } | ||||
|         const alg = this._algorithms[algorithm.name]; | ||||
|         if (typeof alg === "undefined" || typeof alg.importKey !== "function") { | ||||
|             throw new Error("algorithm is not supported"); | ||||
|         } | ||||
|         return alg.importKey(keyData, algorithm, extractable, keyUsages); | ||||
|     } | ||||
| 
 | ||||
|     generateKey(algorithm, extractable, keyUsages) { | ||||
|         const alg = this._algorithms[algorithm.name]; | ||||
|         if (typeof alg === "undefined" || typeof alg.generateKey !== "function") { | ||||
|             throw new Error("algorithm is not supported"); | ||||
|         } | ||||
|         return alg.generateKey(algorithm, extractable, keyUsages); | ||||
|     } | ||||
| 
 | ||||
|     exportKey(format, key) { | ||||
|         if (format !== "raw") { | ||||
|             throw new Error("key format is not supported"); | ||||
|         } | ||||
|         if (typeof key.exportKey !== "function") { | ||||
|             throw new Error("key does not support exportKey"); | ||||
|         } | ||||
|         return key.exportKey(); | ||||
|     } | ||||
| 
 | ||||
|     digest(algorithm, data) { | ||||
|         const alg = this._algorithms[algorithm]; | ||||
|         if (typeof alg !== "function") { | ||||
|             throw new Error("algorithm is not supported"); | ||||
|         } | ||||
|         return alg(data); | ||||
|     } | ||||
| 
 | ||||
|     deriveBits(algorithm, key, length) { | ||||
|         if (key.algorithm.name !== algorithm.name) { | ||||
|             throw new Error("algorithm does not match"); | ||||
|         } | ||||
|         if (typeof key.deriveBits !== "function") { | ||||
|             throw new Error("key does not support deriveBits"); | ||||
|         } | ||||
|         return key.deriveBits(algorithm, length); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default new LegacyCrypto; | ||||
| @ -128,7 +128,7 @@ const SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d, | ||||
| 
 | ||||
| /* eslint-enable comma-spacing */ | ||||
| 
 | ||||
| export default class DES { | ||||
| class DES { | ||||
|     constructor(password) { | ||||
|         this.keys = []; | ||||
| 
 | ||||
| @ -258,9 +258,73 @@ export default class DES { | ||||
|         } | ||||
|         return b; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|     // Encrypt 16 bytes of text using passwd as key
 | ||||
|     encrypt(t) { | ||||
|         return this.enc8(t.slice(0, 8)).concat(this.enc8(t.slice(8, 16))); | ||||
| export class DESECBCipher { | ||||
|     constructor() { | ||||
|         this._cipher = null; | ||||
|     } | ||||
| 
 | ||||
|     get algorithm() { | ||||
|         return { name: "DES-ECB" }; | ||||
|     } | ||||
| 
 | ||||
|     static importKey(key, _algorithm, _extractable, _keyUsages) { | ||||
|         const cipher = new DESECBCipher; | ||||
|         cipher._importKey(key); | ||||
|         return cipher; | ||||
|     } | ||||
| 
 | ||||
|     _importKey(key, _extractable, _keyUsages) { | ||||
|         this._cipher = new DES(key); | ||||
|     } | ||||
| 
 | ||||
|     encrypt(_algorithm, plaintext) { | ||||
|         const x = new Uint8Array(plaintext); | ||||
|         if (x.length % 8 !== 0 || this._cipher === null) { | ||||
|             return null; | ||||
|         } | ||||
|         const n = x.length / 8; | ||||
|         for (let i = 0; i < n; i++) { | ||||
|             x.set(this._cipher.enc8(x.slice(i * 8, i * 8 + 8)), i * 8); | ||||
|         } | ||||
|         return x; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class DESCBCCipher { | ||||
|     constructor() { | ||||
|         this._cipher = null; | ||||
|     } | ||||
| 
 | ||||
|     get algorithm() { | ||||
|         return { name: "DES-CBC" }; | ||||
|     } | ||||
| 
 | ||||
|     static importKey(key, _algorithm, _extractable, _keyUsages) { | ||||
|         const cipher = new DESCBCCipher; | ||||
|         cipher._importKey(key); | ||||
|         return cipher; | ||||
|     } | ||||
| 
 | ||||
|     _importKey(key) { | ||||
|         this._cipher = new DES(key); | ||||
|     } | ||||
| 
 | ||||
|     encrypt(algorithm, plaintext) { | ||||
|         const x = new Uint8Array(plaintext); | ||||
|         let y = new Uint8Array(algorithm.iv); | ||||
|         if (x.length % 8 !== 0 || this._cipher === null) { | ||||
|             return null; | ||||
|         } | ||||
|         const n = x.length / 8; | ||||
|         for (let i = 0; i < n; i++) { | ||||
|             for (let j = 0; j < 8; j++) { | ||||
|                 y[j] ^= plaintext[i * 8 + j]; | ||||
|             } | ||||
|             y = this._cipher.enc8(y); | ||||
|             x.set(y, i * 8); | ||||
|         } | ||||
|         return x; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										55
									
								
								systemvm/agent/noVNC/core/crypto/dh.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								systemvm/agent/noVNC/core/crypto/dh.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,55 @@ | ||||
| import { modPow, bigIntToU8Array, u8ArrayToBigInt } from "./bigint.js"; | ||||
| 
 | ||||
| class DHPublicKey { | ||||
|     constructor(key) { | ||||
|         this._key = key; | ||||
|     } | ||||
| 
 | ||||
|     get algorithm() { | ||||
|         return { name: "DH" }; | ||||
|     } | ||||
| 
 | ||||
|     exportKey() { | ||||
|         return this._key; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class DHCipher { | ||||
|     constructor() { | ||||
|         this._g = null; | ||||
|         this._p = null; | ||||
|         this._gBigInt = null; | ||||
|         this._pBigInt = null; | ||||
|         this._privateKey = null; | ||||
|     } | ||||
| 
 | ||||
|     get algorithm() { | ||||
|         return { name: "DH" }; | ||||
|     } | ||||
| 
 | ||||
|     static generateKey(algorithm, _extractable) { | ||||
|         const cipher = new DHCipher; | ||||
|         cipher._generateKey(algorithm); | ||||
|         return { privateKey: cipher, publicKey: new DHPublicKey(cipher._publicKey) }; | ||||
|     } | ||||
| 
 | ||||
|     _generateKey(algorithm) { | ||||
|         const g = algorithm.g; | ||||
|         const p = algorithm.p; | ||||
|         this._keyBytes = p.length; | ||||
|         this._gBigInt = u8ArrayToBigInt(g); | ||||
|         this._pBigInt = u8ArrayToBigInt(p); | ||||
|         this._privateKey = window.crypto.getRandomValues(new Uint8Array(this._keyBytes)); | ||||
|         this._privateKeyBigInt = u8ArrayToBigInt(this._privateKey); | ||||
|         this._publicKey = bigIntToU8Array(modPow( | ||||
|             this._gBigInt, this._privateKeyBigInt, this._pBigInt), this._keyBytes); | ||||
|     } | ||||
| 
 | ||||
|     deriveBits(algorithm, length) { | ||||
|         const bytes = Math.ceil(length / 8); | ||||
|         const pkey = new Uint8Array(algorithm.public); | ||||
|         const len = bytes > this._keyBytes ? bytes : this._keyBytes; | ||||
|         const secret = modPow(u8ArrayToBigInt(pkey), this._privateKeyBigInt, this._pBigInt); | ||||
|         return bigIntToU8Array(secret, len).slice(0, len); | ||||
|     } | ||||
| } | ||||
| @ -1,18 +1,21 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2021 The noVNC Authors | ||||
|  * 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 | ||||
|  * Performs MD5 hashing on an array of bytes, returns an array of bytes | ||||
|  */ | ||||
| 
 | ||||
| export function MD5(d) { | ||||
|     let r = M(V(Y(X(d), 8 * d.length))); | ||||
|     return r; | ||||
| export async function MD5(d) { | ||||
|     let s = ""; | ||||
|     for (let i = 0; i < d.length; i++) { | ||||
|         s += String.fromCharCode(d[i]); | ||||
|     } | ||||
|     return M(V(Y(X(s), 8 * s.length))); | ||||
| } | ||||
| 
 | ||||
| function M(d) { | ||||
							
								
								
									
										132
									
								
								systemvm/agent/noVNC/core/crypto/rsa.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								systemvm/agent/noVNC/core/crypto/rsa.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,132 @@ | ||||
| import Base64 from "../base64.js"; | ||||
| import { modPow, bigIntToU8Array, u8ArrayToBigInt } from "./bigint.js"; | ||||
| 
 | ||||
| export class RSACipher { | ||||
|     constructor() { | ||||
|         this._keyLength = 0; | ||||
|         this._keyBytes = 0; | ||||
|         this._n = null; | ||||
|         this._e = null; | ||||
|         this._d = null; | ||||
|         this._nBigInt = null; | ||||
|         this._eBigInt = null; | ||||
|         this._dBigInt = null; | ||||
|         this._extractable = false; | ||||
|     } | ||||
| 
 | ||||
|     get algorithm() { | ||||
|         return { name: "RSA-PKCS1-v1_5" }; | ||||
|     } | ||||
| 
 | ||||
|     _base64urlDecode(data) { | ||||
|         data = data.replace(/-/g, "+").replace(/_/g, "/"); | ||||
|         data = data.padEnd(Math.ceil(data.length / 4) * 4, "="); | ||||
|         return Base64.decode(data); | ||||
|     } | ||||
| 
 | ||||
|     _padArray(arr, length) { | ||||
|         const res = new Uint8Array(length); | ||||
|         res.set(arr, length - arr.length); | ||||
|         return res; | ||||
|     } | ||||
| 
 | ||||
|     static async generateKey(algorithm, extractable, _keyUsages) { | ||||
|         const cipher = new RSACipher; | ||||
|         await cipher._generateKey(algorithm, extractable); | ||||
|         return { privateKey: cipher }; | ||||
|     } | ||||
| 
 | ||||
|     async _generateKey(algorithm, extractable) { | ||||
|         this._keyLength = algorithm.modulusLength; | ||||
|         this._keyBytes = Math.ceil(this._keyLength / 8); | ||||
|         const key = await window.crypto.subtle.generateKey( | ||||
|             { | ||||
|                 name: "RSA-OAEP", | ||||
|                 modulusLength: algorithm.modulusLength, | ||||
|                 publicExponent: algorithm.publicExponent, | ||||
|                 hash: {name: "SHA-256"}, | ||||
|             }, | ||||
|             true, ["encrypt", "decrypt"]); | ||||
|         const privateKey = await window.crypto.subtle.exportKey("jwk", key.privateKey); | ||||
|         this._n = this._padArray(this._base64urlDecode(privateKey.n), this._keyBytes); | ||||
|         this._nBigInt = u8ArrayToBigInt(this._n); | ||||
|         this._e = this._padArray(this._base64urlDecode(privateKey.e), this._keyBytes); | ||||
|         this._eBigInt = u8ArrayToBigInt(this._e); | ||||
|         this._d = this._padArray(this._base64urlDecode(privateKey.d), this._keyBytes); | ||||
|         this._dBigInt = u8ArrayToBigInt(this._d); | ||||
|         this._extractable = extractable; | ||||
|     } | ||||
| 
 | ||||
|     static async importKey(key, _algorithm, extractable, keyUsages) { | ||||
|         if (keyUsages.length !== 1 || keyUsages[0] !== "encrypt") { | ||||
|             throw new Error("only support importing RSA public key"); | ||||
|         } | ||||
|         const cipher = new RSACipher; | ||||
|         await cipher._importKey(key, extractable); | ||||
|         return cipher; | ||||
|     } | ||||
| 
 | ||||
|     async _importKey(key, extractable) { | ||||
|         const n = key.n; | ||||
|         const e = key.e; | ||||
|         if (n.length !== e.length) { | ||||
|             throw new Error("the sizes of modulus and public exponent do not match"); | ||||
|         } | ||||
|         this._keyBytes = n.length; | ||||
|         this._keyLength = this._keyBytes * 8; | ||||
|         this._n = new Uint8Array(this._keyBytes); | ||||
|         this._e = new Uint8Array(this._keyBytes); | ||||
|         this._n.set(n); | ||||
|         this._e.set(e); | ||||
|         this._nBigInt = u8ArrayToBigInt(this._n); | ||||
|         this._eBigInt = u8ArrayToBigInt(this._e); | ||||
|         this._extractable = extractable; | ||||
|     } | ||||
| 
 | ||||
|     async encrypt(_algorithm, message) { | ||||
|         if (message.length > this._keyBytes - 11) { | ||||
|             return null; | ||||
|         } | ||||
|         const ps = new Uint8Array(this._keyBytes - message.length - 3); | ||||
|         window.crypto.getRandomValues(ps); | ||||
|         for (let i = 0; i < ps.length; i++) { | ||||
|             ps[i] = Math.floor(ps[i] * 254 / 255 + 1); | ||||
|         } | ||||
|         const em = new Uint8Array(this._keyBytes); | ||||
|         em[1] = 0x02; | ||||
|         em.set(ps, 2); | ||||
|         em.set(message, ps.length + 3); | ||||
|         const emBigInt = u8ArrayToBigInt(em); | ||||
|         const c = modPow(emBigInt, this._eBigInt, this._nBigInt); | ||||
|         return bigIntToU8Array(c, this._keyBytes); | ||||
|     } | ||||
| 
 | ||||
|     async decrypt(_algorithm, message) { | ||||
|         if (message.length !== this._keyBytes) { | ||||
|             return null; | ||||
|         } | ||||
|         const msgBigInt = u8ArrayToBigInt(message); | ||||
|         const emBigInt = modPow(msgBigInt, this._dBigInt, this._nBigInt); | ||||
|         const em = bigIntToU8Array(emBigInt, this._keyBytes); | ||||
|         if (em[0] !== 0x00 || em[1] !== 0x02) { | ||||
|             return null; | ||||
|         } | ||||
|         let i = 2; | ||||
|         for (; i < em.length; i++) { | ||||
|             if (em[i] === 0x00) { | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|         if (i === em.length) { | ||||
|             return null; | ||||
|         } | ||||
|         return em.slice(i + 1, em.length); | ||||
|     } | ||||
| 
 | ||||
|     async exportKey() { | ||||
|         if (!this._extractable) { | ||||
|             throw new Error("key is not extractable"); | ||||
|         } | ||||
|         return { n: this._n, e: this._e, d: this._d }; | ||||
|     } | ||||
| } | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2019 The noVNC Authors | ||||
|  * Copyright (C) 2019 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
|  | ||||
							
								
								
									
										321
									
								
								systemvm/agent/noVNC/core/decoders/h264.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										321
									
								
								systemvm/agent/noVNC/core/decoders/h264.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,321 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2024 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
|  * | ||||
|  */ | ||||
| 
 | ||||
| import * as Log from '../util/logging.js'; | ||||
| 
 | ||||
| export class H264Parser { | ||||
|     constructor(data) { | ||||
|         this._data = data; | ||||
|         this._index = 0; | ||||
|         this.profileIdc = null; | ||||
|         this.constraintSet = null; | ||||
|         this.levelIdc = null; | ||||
|     } | ||||
| 
 | ||||
|     _getStartSequenceLen(index) { | ||||
|         let data = this._data; | ||||
|         if (data[index + 0] == 0 && data[index + 1] == 0 && data[index + 2] == 0 && data[index + 3] == 1) { | ||||
|             return 4; | ||||
|         } | ||||
|         if (data[index + 0] == 0 && data[index + 1] == 0 && data[index + 2] == 1) { | ||||
|             return 3; | ||||
|         } | ||||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     _indexOfNextNalUnit(index) { | ||||
|         let data = this._data; | ||||
|         for (let i = index; i < data.length; ++i) { | ||||
|             if (this._getStartSequenceLen(i) != 0) { | ||||
|                 return i; | ||||
|             } | ||||
|         } | ||||
|         return -1; | ||||
|     } | ||||
| 
 | ||||
|     _parseSps(index) { | ||||
|         this.profileIdc = this._data[index]; | ||||
|         this.constraintSet = this._data[index + 1]; | ||||
|         this.levelIdc = this._data[index + 2]; | ||||
|     } | ||||
| 
 | ||||
|     _parseNalUnit(index) { | ||||
|         const firstByte = this._data[index]; | ||||
|         if (firstByte & 0x80) { | ||||
|             throw new Error('H264 parsing sanity check failed, forbidden zero bit is set'); | ||||
|         } | ||||
|         const unitType = firstByte & 0x1f; | ||||
| 
 | ||||
|         switch (unitType) { | ||||
|             case 1: // coded slice, non-idr
 | ||||
|                 return { slice: true }; | ||||
|             case 5: // coded slice, idr
 | ||||
|                 return { slice: true, key: true }; | ||||
|             case 6: // sei
 | ||||
|                 return {}; | ||||
|             case 7: // sps
 | ||||
|                 this._parseSps(index + 1); | ||||
|                 return {}; | ||||
|             case 8: // pps
 | ||||
|                 return {}; | ||||
|             default: | ||||
|                 Log.Warn("Unhandled unit type: ", unitType); | ||||
|                 break; | ||||
|         } | ||||
|         return {}; | ||||
|     } | ||||
| 
 | ||||
|     parse() { | ||||
|         const startIndex = this._index; | ||||
|         let isKey = false; | ||||
| 
 | ||||
|         while (this._index < this._data.length) { | ||||
|             const startSequenceLen = this._getStartSequenceLen(this._index); | ||||
|             if (startSequenceLen == 0) { | ||||
|                 throw new Error('Invalid start sequence in bit stream'); | ||||
|             } | ||||
| 
 | ||||
|             const { slice, key } = this._parseNalUnit(this._index + startSequenceLen); | ||||
| 
 | ||||
|             let nextIndex = this._indexOfNextNalUnit(this._index + startSequenceLen); | ||||
|             if (nextIndex == -1) { | ||||
|                 this._index = this._data.length; | ||||
|             } else { | ||||
|                 this._index = nextIndex; | ||||
|             } | ||||
| 
 | ||||
|             if (key) { | ||||
|                 isKey = true; | ||||
|             } | ||||
|             if (slice) { | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (startIndex === this._index) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|             frame: this._data.subarray(startIndex, this._index), | ||||
|             key: isKey, | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class H264Context { | ||||
|     constructor(width, height) { | ||||
|         this.lastUsed = 0; | ||||
|         this._width = width; | ||||
|         this._height = height; | ||||
|         this._profileIdc = null; | ||||
|         this._constraintSet = null; | ||||
|         this._levelIdc = null; | ||||
|         this._decoder = null; | ||||
|         this._pendingFrames = []; | ||||
|     } | ||||
| 
 | ||||
|     _handleFrame(frame) { | ||||
|         let pending = this._pendingFrames.shift(); | ||||
|         if (pending === undefined) { | ||||
|             throw new Error("Pending frame queue empty when receiving frame from decoder"); | ||||
|         } | ||||
| 
 | ||||
|         if (pending.timestamp != frame.timestamp) { | ||||
|             throw new Error("Video frame timestamp mismatch. Expected " + | ||||
|                 frame.timestamp + " but but got " + pending.timestamp); | ||||
|         } | ||||
| 
 | ||||
|         pending.frame = frame; | ||||
|         pending.ready = true; | ||||
|         pending.resolve(); | ||||
| 
 | ||||
|         if (!pending.keep) { | ||||
|             frame.close(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     _handleError(e) { | ||||
|         throw new Error("Failed to decode frame: " + e.message); | ||||
|     } | ||||
| 
 | ||||
|     _configureDecoder(profileIdc, constraintSet, levelIdc) { | ||||
|         if (this._decoder === null || this._decoder.state === 'closed') { | ||||
|             this._decoder = new VideoDecoder({ | ||||
|                 output: frame => this._handleFrame(frame), | ||||
|                 error: e => this._handleError(e), | ||||
|             }); | ||||
|         } | ||||
|         const codec = 'avc1.' + | ||||
|             profileIdc.toString(16).padStart(2, '0') + | ||||
|             constraintSet.toString(16).padStart(2, '0') + | ||||
|             levelIdc.toString(16).padStart(2, '0'); | ||||
|         this._decoder.configure({ | ||||
|             codec: codec, | ||||
|             codedWidth: this._width, | ||||
|             codedHeight: this._height, | ||||
|             optimizeForLatency: true, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     _preparePendingFrame(timestamp) { | ||||
|         let pending = { | ||||
|             timestamp: timestamp, | ||||
|             promise: null, | ||||
|             resolve: null, | ||||
|             frame: null, | ||||
|             ready: false, | ||||
|             keep: false, | ||||
|         }; | ||||
|         pending.promise = new Promise((resolve) => { | ||||
|             pending.resolve = resolve; | ||||
|         }); | ||||
|         this._pendingFrames.push(pending); | ||||
| 
 | ||||
|         return pending; | ||||
|     } | ||||
| 
 | ||||
|     decode(payload) { | ||||
|         let parser = new H264Parser(payload); | ||||
|         let result = null; | ||||
| 
 | ||||
|         // Ideally, this timestamp should come from the server, but we'll just
 | ||||
|         // approximate it instead.
 | ||||
|         let timestamp = Math.round(window.performance.now() * 1e3); | ||||
| 
 | ||||
|         while (true) { | ||||
|             let encodedFrame = parser.parse(); | ||||
|             if (encodedFrame === null) { | ||||
|                 break; | ||||
|             } | ||||
| 
 | ||||
|             if (parser.profileIdc !== null) { | ||||
|                 self._profileIdc = parser.profileIdc; | ||||
|                 self._constraintSet = parser.constraintSet; | ||||
|                 self._levelIdc = parser.levelIdc; | ||||
|             } | ||||
| 
 | ||||
|             if (this._decoder === null || this._decoder.state !== 'configured') { | ||||
|                 if (!encodedFrame.key) { | ||||
|                     Log.Warn("Missing key frame. Can't decode until one arrives"); | ||||
|                     continue; | ||||
|                 } | ||||
|                 if (self._profileIdc === null) { | ||||
|                     Log.Warn('Cannot config decoder. Have not received SPS and PPS yet.'); | ||||
|                     continue; | ||||
|                 } | ||||
|                 this._configureDecoder(self._profileIdc, self._constraintSet, | ||||
|                                        self._levelIdc); | ||||
|             } | ||||
| 
 | ||||
|             result = this._preparePendingFrame(timestamp); | ||||
| 
 | ||||
|             const chunk = new EncodedVideoChunk({ | ||||
|                 timestamp: timestamp, | ||||
|                 type: encodedFrame.key ? 'key' : 'delta', | ||||
|                 data: encodedFrame.frame, | ||||
|             }); | ||||
| 
 | ||||
|             try { | ||||
|                 this._decoder.decode(chunk); | ||||
|             } catch (e) { | ||||
|                 Log.Warn("Failed to decode:", e); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // We only keep last frame of each payload
 | ||||
|         if (result !== null) { | ||||
|             result.keep = true; | ||||
|         } | ||||
| 
 | ||||
|         return result; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default class H264Decoder { | ||||
|     constructor() { | ||||
|         this._tick = 0; | ||||
|         this._contexts = {}; | ||||
|     } | ||||
| 
 | ||||
|     _contextId(x, y, width, height) { | ||||
|         return [x, y, width, height].join(','); | ||||
|     } | ||||
| 
 | ||||
|     _findOldestContextId() { | ||||
|         let oldestTick = Number.MAX_VALUE; | ||||
|         let oldestKey = undefined; | ||||
|         for (const [key, value] of Object.entries(this._contexts)) { | ||||
|             if (value.lastUsed < oldestTick) { | ||||
|                 oldestTick = value.lastUsed; | ||||
|                 oldestKey = key; | ||||
|             } | ||||
|         } | ||||
|         return oldestKey; | ||||
|     } | ||||
| 
 | ||||
|     _createContext(x, y, width, height) { | ||||
|         const maxContexts = 64; | ||||
|         if (Object.keys(this._contexts).length >= maxContexts) { | ||||
|             let oldestContextId = this._findOldestContextId(); | ||||
|             delete this._contexts[oldestContextId]; | ||||
|         } | ||||
|         let context = new H264Context(width, height); | ||||
|         this._contexts[this._contextId(x, y, width, height)] = context; | ||||
|         return context; | ||||
|     } | ||||
| 
 | ||||
|     _getContext(x, y, width, height) { | ||||
|         let context = this._contexts[this._contextId(x, y, width, height)]; | ||||
|         return context !== undefined ? context : this._createContext(x, y, width, height); | ||||
|     } | ||||
| 
 | ||||
|     _resetContext(x, y, width, height) { | ||||
|         delete this._contexts[this._contextId(x, y, width, height)]; | ||||
|     } | ||||
| 
 | ||||
|     _resetAllContexts() { | ||||
|         this._contexts = {}; | ||||
|     } | ||||
| 
 | ||||
|     decodeRect(x, y, width, height, sock, display, depth) { | ||||
|         const resetContextFlag = 1; | ||||
|         const resetAllContextsFlag = 2; | ||||
| 
 | ||||
|         if (sock.rQwait("h264 header", 8)) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         const length = sock.rQshift32(); | ||||
|         const flags = sock.rQshift32(); | ||||
| 
 | ||||
|         if (sock.rQwait("h264 payload", length, 8)) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         if (flags & resetAllContextsFlag) { | ||||
|             this._resetAllContexts(); | ||||
|         } else if (flags & resetContextFlag) { | ||||
|             this._resetContext(x, y, width, height); | ||||
|         } | ||||
| 
 | ||||
|         let context = this._getContext(x, y, width, height); | ||||
|         context.lastUsed = this._tick++; | ||||
| 
 | ||||
|         if (length !== 0) { | ||||
|             let payload = sock.rQshiftBytes(length, false); | ||||
|             let frame = context.decode(payload); | ||||
|             if (frame !== null) { | ||||
|                 display.videoFrame(x, y, width, height, frame); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| } | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2019 The noVNC Authors | ||||
|  * Copyright (C) 2019 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
| @ -31,10 +31,7 @@ export default class HextileDecoder { | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             let rQ = sock.rQ; | ||||
|             let rQi = sock.rQi; | ||||
| 
 | ||||
|             let subencoding = rQ[rQi];  // Peek
 | ||||
|             let subencoding = sock.rQpeek8(); | ||||
|             if (subencoding > 30) {  // Raw
 | ||||
|                 throw new Error("Illegal hextile subencoding (subencoding: " + | ||||
|                             subencoding + ")"); | ||||
| @ -65,7 +62,7 @@ export default class HextileDecoder { | ||||
|                         return false; | ||||
|                     } | ||||
| 
 | ||||
|                     let subrects = rQ[rQi + bytes - 1];  // Peek
 | ||||
|                     let subrects = sock.rQpeekBytes(bytes).at(-1); | ||||
|                     if (subencoding & 0x10) {  // SubrectsColoured
 | ||||
|                         bytes += subrects * (4 + 2); | ||||
|                     } else { | ||||
| @ -79,7 +76,7 @@ export default class HextileDecoder { | ||||
|             } | ||||
| 
 | ||||
|             // We know the encoding and have a whole tile
 | ||||
|             rQi++; | ||||
|             sock.rQshift8(); | ||||
|             if (subencoding === 0) { | ||||
|                 if (this._lastsubencoding & 0x01) { | ||||
|                     // Weird: ignore blanks are RAW
 | ||||
| @ -89,42 +86,36 @@ export default class HextileDecoder { | ||||
|                 } | ||||
|             } else if (subencoding & 0x01) {  // Raw
 | ||||
|                 let pixels = tw * th; | ||||
|                 let data = sock.rQshiftBytes(pixels * 4, false); | ||||
|                 // Max sure the image is fully opaque
 | ||||
|                 for (let i = 0;i <  pixels;i++) { | ||||
|                     rQ[rQi + i * 4 + 3] = 255; | ||||
|                     data[i * 4 + 3] = 255; | ||||
|                 } | ||||
|                 display.blitImage(tx, ty, tw, th, rQ, rQi); | ||||
|                 rQi += bytes - 1; | ||||
|                 display.blitImage(tx, ty, tw, th, data, 0); | ||||
|             } else { | ||||
|                 if (subencoding & 0x02) {  // Background
 | ||||
|                     this._background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; | ||||
|                     rQi += 4; | ||||
|                     this._background = new Uint8Array(sock.rQshiftBytes(4)); | ||||
|                 } | ||||
|                 if (subencoding & 0x04) {  // Foreground
 | ||||
|                     this._foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; | ||||
|                     rQi += 4; | ||||
|                     this._foreground = new Uint8Array(sock.rQshiftBytes(4)); | ||||
|                 } | ||||
| 
 | ||||
|                 this._startTile(tx, ty, tw, th, this._background); | ||||
|                 if (subencoding & 0x08) {  // AnySubrects
 | ||||
|                     let subrects = rQ[rQi]; | ||||
|                     rQi++; | ||||
|                     let subrects = sock.rQshift8(); | ||||
| 
 | ||||
|                     for (let s = 0; s < subrects; s++) { | ||||
|                         let color; | ||||
|                         if (subencoding & 0x10) {  // SubrectsColoured
 | ||||
|                             color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; | ||||
|                             rQi += 4; | ||||
|                             color = sock.rQshiftBytes(4); | ||||
|                         } else { | ||||
|                             color = this._foreground; | ||||
|                         } | ||||
|                         const xy = rQ[rQi]; | ||||
|                         rQi++; | ||||
|                         const xy = sock.rQshift8(); | ||||
|                         const sx = (xy >> 4); | ||||
|                         const sy = (xy & 0x0f); | ||||
| 
 | ||||
|                         const wh = rQ[rQi]; | ||||
|                         rQi++; | ||||
|                         const wh = sock.rQshift8(); | ||||
|                         const sw = (wh >> 4) + 1; | ||||
|                         const sh = (wh & 0x0f) + 1; | ||||
| 
 | ||||
| @ -133,7 +124,6 @@ export default class HextileDecoder { | ||||
|                 } | ||||
|                 this._finishTile(display); | ||||
|             } | ||||
|             sock.rQi = rQi; | ||||
|             this._lastsubencoding = subencoding; | ||||
|             this._tiles--; | ||||
|         } | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2019 The noVNC Authors | ||||
|  * Copyright (C) 2019 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
| @ -11,131 +11,136 @@ 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))) { | ||||
|         while (true) { | ||||
|             let segment = this._readSegment(sock); | ||||
|             if (segment === null) { | ||||
|                 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.
 | ||||
|             this._segments.push(segment); | ||||
|             // End of image?
 | ||||
|             if (segment[1] === 0xD9) { | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         let huffmanTables = []; | ||||
|         let quantTables = []; | ||||
|         for (let segment of this._segments) { | ||||
|             let type = segment[1]; | ||||
|             if (type === 0xC4) { | ||||
|                 // Huffman tables
 | ||||
|                 huffmanTables.push(segment); | ||||
|             } else if (type === 0xDB) { | ||||
|                 // Quantization tables
 | ||||
|                 quantTables.push(segment); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         const sofIndex = this._segments.findIndex( | ||||
|             x => x[1] == 0xC0 || x[1] == 0xC2 | ||||
|         ); | ||||
|         if (sofIndex == -1) { | ||||
|             throw new Error("Illegal JPEG image without SOF"); | ||||
|         } | ||||
|             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; | ||||
| 
 | ||||
|         if (quantTables.length === 0) { | ||||
|             this._segments.splice(sofIndex+1, 0, | ||||
|                                   ...this._cachedQuantTables); | ||||
|         } | ||||
|         if (huffmanTables.length === 0) { | ||||
|             this._segments.splice(sofIndex+1, 0, | ||||
|                                   ...this._cachedHuffmanTables); | ||||
|         } | ||||
| 
 | ||||
|     _parseJPEG(buffer) { | ||||
|         if (this._quantTables.length != 0) { | ||||
|             this._cachedQuantTables = this._quantTables; | ||||
|         let length = 0; | ||||
|         for (let segment of this._segments) { | ||||
|             length += segment.length; | ||||
|         } | ||||
|         if (this._huffmanTables.length != 0) { | ||||
|             this._cachedHuffmanTables = this._huffmanTables; | ||||
| 
 | ||||
|         let data = new Uint8Array(length); | ||||
|         length = 0; | ||||
|         for (let segment of this._segments) { | ||||
|             data.set(segment, length); | ||||
|             length += segment.length; | ||||
|         } | ||||
|         this._quantTables = []; | ||||
|         this._huffmanTables = []; | ||||
| 
 | ||||
|         display.imageRect(x, y, width, height, "image/jpeg", data); | ||||
| 
 | ||||
|         if (huffmanTables.length !== 0) { | ||||
|             this._cachedHuffmanTables = huffmanTables; | ||||
|         } | ||||
|         if (quantTables.length !== 0) { | ||||
|             this._cachedQuantTables = quantTables; | ||||
|         } | ||||
| 
 | ||||
|         this._segments = []; | ||||
|         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; | ||||
|     } | ||||
| 
 | ||||
|     _readSegment(sock) { | ||||
|         if (sock.rQwait("JPEG", 2)) { | ||||
|             return null; | ||||
|         } | ||||
|                 if (!hasFoundEndOfScan) { | ||||
|                     return false; | ||||
| 
 | ||||
|         let marker = sock.rQshift8(); | ||||
|         if (marker != 0xFF) { | ||||
|             throw new Error("Illegal JPEG marker received (byte: " + | ||||
|                                marker + ")"); | ||||
|         } | ||||
|                 this._segments.push(buffer.slice(i, j)); | ||||
|                 i = j; | ||||
|                 continue; | ||||
|             } else if (type >= 0xD0 && type < 0xD9 || type == 0x01) { | ||||
|         let type = sock.rQshift8(); | ||||
|         if (type >= 0xD0 && type <= 0xD9 || type == 0x01) { | ||||
|             // No length after marker
 | ||||
|                 this._segments.push(buffer.slice(i, j)); | ||||
|                 i = j; | ||||
|                 continue; | ||||
|             return new Uint8Array([marker, type]); | ||||
|         } | ||||
|             if (j + 2 > bufferLength) { | ||||
|                 return false; | ||||
| 
 | ||||
|         if (sock.rQwait("JPEG", 2, 2)) { | ||||
|             return null; | ||||
|         } | ||||
|             const length = (buffer[j] << 8) + buffer[j+1] - 2; | ||||
|             if (length < 0) { | ||||
| 
 | ||||
|         let length = sock.rQshift16(); | ||||
|         if (length < 2) { | ||||
|             throw new Error("Illegal JPEG length received (length: " + | ||||
|                                length + ")"); | ||||
|         } | ||||
|             j += 2; | ||||
|             if (j + length > bufferLength) { | ||||
|                 return false; | ||||
| 
 | ||||
|         if (sock.rQwait("JPEG", length-2, 4)) { | ||||
|             return null; | ||||
|         } | ||||
|             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); | ||||
| 
 | ||||
|         let extra = 0; | ||||
|         if (type === 0xDA) { | ||||
|             // start of scan
 | ||||
|             extra += 2; | ||||
|             while (true) { | ||||
|                 if (sock.rQwait("JPEG", length-2+extra, 4)) { | ||||
|                     return null; | ||||
|                 } | ||||
|             this._segments.push(segment); | ||||
|             i = j; | ||||
|                 let data = sock.rQpeekBytes(length-2+extra, false); | ||||
|                 if (data.at(-2) === 0xFF && data.at(-1) !== 0x00 && | ||||
|                     !(data.at(-1) >= 0xD0 && data.at(-1) <= 0xD7)) { | ||||
|                     extra -= 2; | ||||
|                     break; | ||||
|                 } | ||||
|                 extra++; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         let segment = new Uint8Array(2 + length + extra); | ||||
|         segment[0] = marker; | ||||
|         segment[1] = type; | ||||
|         segment[2] = length >> 8; | ||||
|         segment[3] = length; | ||||
|         segment.set(sock.rQshiftBytes(length-2+extra, false), 4); | ||||
| 
 | ||||
|         return segment; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2019 The noVNC Authors | ||||
|  * Copyright (C) 2019 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
| @ -24,41 +24,34 @@ export default class RawDecoder { | ||||
|         const pixelSize = depth == 8 ? 1 : 4; | ||||
|         const bytesPerLine = width * pixelSize; | ||||
| 
 | ||||
|         while (this._lines > 0) { | ||||
|             if (sock.rQwait("RAW", bytesPerLine)) { | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             const curY = y + (height - this._lines); | ||||
|         const currHeight = Math.min(this._lines, | ||||
|                                     Math.floor(sock.rQlen / bytesPerLine)); | ||||
|         const pixels = width * currHeight; | ||||
| 
 | ||||
|         let data = sock.rQ; | ||||
|         let index = sock.rQi; | ||||
|             let data = sock.rQshiftBytes(bytesPerLine, false); | ||||
| 
 | ||||
|             // Convert data if needed
 | ||||
|             if (depth == 8) { | ||||
|             const newdata = new Uint8Array(pixels * 4); | ||||
|             for (let i = 0; i < pixels; i++) { | ||||
|                 newdata[i * 4 + 0] = ((data[index + i] >> 0) & 0x3) * 255 / 3; | ||||
|                 newdata[i * 4 + 1] = ((data[index + i] >> 2) & 0x3) * 255 / 3; | ||||
|                 newdata[i * 4 + 2] = ((data[index + i] >> 4) & 0x3) * 255 / 3; | ||||
|                 const newdata = new Uint8Array(width * 4); | ||||
|                 for (let i = 0; i < width; i++) { | ||||
|                     newdata[i * 4 + 0] = ((data[i] >> 0) & 0x3) * 255 / 3; | ||||
|                     newdata[i * 4 + 1] = ((data[i] >> 2) & 0x3) * 255 / 3; | ||||
|                     newdata[i * 4 + 2] = ((data[i] >> 4) & 0x3) * 255 / 3; | ||||
|                     newdata[i * 4 + 3] = 255; | ||||
|                 } | ||||
|                 data = newdata; | ||||
|             index = 0; | ||||
|             } | ||||
| 
 | ||||
|             // Max sure the image is fully opaque
 | ||||
|         for (let i = 0; i < pixels; i++) { | ||||
|             data[index + i * 4 + 3] = 255; | ||||
|             for (let i = 0; i < width; i++) { | ||||
|                 data[i * 4 + 3] = 255; | ||||
|             } | ||||
| 
 | ||||
|         display.blitImage(x, curY, width, currHeight, data, index); | ||||
|         sock.rQskipBytes(currHeight * bytesPerLine); | ||||
|         this._lines -= currHeight; | ||||
|         if (this._lines > 0) { | ||||
|             return false; | ||||
|             display.blitImage(x, curY, width, 1, data, 0); | ||||
|             this._lines--; | ||||
|         } | ||||
| 
 | ||||
|         return true; | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2019 The noVNC Authors | ||||
|  * Copyright (C) 2019 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2019 The noVNC Authors | ||||
|  * Copyright (C) 2019 The noVNC authors | ||||
|  * (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca) | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
| @ -76,12 +76,8 @@ export default class TightDecoder { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         const rQi = sock.rQi; | ||||
|         const rQ = sock.rQ; | ||||
| 
 | ||||
|         display.fillRect(x, y, width, height, | ||||
|                          [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2]], false); | ||||
|         sock.rQskipBytes(3); | ||||
|         let pixel = sock.rQshiftBytes(3); | ||||
|         display.fillRect(x, y, width, height, pixel, false); | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| @ -289,7 +285,73 @@ export default class TightDecoder { | ||||
|     } | ||||
| 
 | ||||
|     _gradientFilter(streamId, x, y, width, height, sock, display, depth) { | ||||
|         throw new Error("Gradient filter not implemented"); | ||||
|         // assume the TPIXEL is 3 bytes long
 | ||||
|         const uncompressedSize = width * height * 3; | ||||
|         let data; | ||||
| 
 | ||||
|         if (uncompressedSize === 0) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         if (uncompressedSize < 12) { | ||||
|             if (sock.rQwait("TIGHT", uncompressedSize)) { | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             data = sock.rQshiftBytes(uncompressedSize); | ||||
|         } else { | ||||
|             data = this._readData(sock); | ||||
|             if (data === null) { | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             this._zlibs[streamId].setInput(data); | ||||
|             data = this._zlibs[streamId].inflate(uncompressedSize); | ||||
|             this._zlibs[streamId].setInput(null); | ||||
|         } | ||||
| 
 | ||||
|         let rgbx = new Uint8Array(4 * width * height); | ||||
| 
 | ||||
|         let rgbxIndex = 0, dataIndex = 0; | ||||
|         let left = new Uint8Array(3); | ||||
|         for (let x = 0; x < width; x++) { | ||||
|             for (let c = 0; c < 3; c++) { | ||||
|                 const prediction = left[c]; | ||||
|                 const value = data[dataIndex++] + prediction; | ||||
|                 rgbx[rgbxIndex++] = value; | ||||
|                 left[c] = value; | ||||
|             } | ||||
|             rgbx[rgbxIndex++] = 255; | ||||
|         } | ||||
| 
 | ||||
|         let upperIndex = 0; | ||||
|         let upper = new Uint8Array(3), | ||||
|             upperleft = new Uint8Array(3); | ||||
|         for (let y = 1; y < height; y++) { | ||||
|             left.fill(0); | ||||
|             upperleft.fill(0); | ||||
|             for (let x = 0; x < width; x++) { | ||||
|                 for (let c = 0; c < 3; c++) { | ||||
|                     upper[c] = rgbx[upperIndex++]; | ||||
|                     let prediction = left[c] + upper[c] - upperleft[c]; | ||||
|                     if (prediction < 0) { | ||||
|                         prediction = 0; | ||||
|                     } else if (prediction > 255) { | ||||
|                         prediction = 255; | ||||
|                     } | ||||
|                     const value = data[dataIndex++] + prediction; | ||||
|                     rgbx[rgbxIndex++] = value; | ||||
|                     upperleft[c] = upper[c]; | ||||
|                     left[c] = value; | ||||
|                 } | ||||
|                 rgbx[rgbxIndex++] = 255; | ||||
|                 upperIndex++; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         display.blitImage(x, y, width, height, rgbx, 0, false); | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     _readData(sock) { | ||||
| @ -316,7 +378,7 @@ export default class TightDecoder { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         let data = sock.rQshiftBytes(this._len); | ||||
|         let data = sock.rQshiftBytes(this._len, false); | ||||
|         this._len = 0; | ||||
| 
 | ||||
|         return data; | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2019 The noVNC Authors | ||||
|  * Copyright (C) 2019 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
|  | ||||
							
								
								
									
										51
									
								
								systemvm/agent/noVNC/core/decoders/zlib.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								systemvm/agent/noVNC/core/decoders/zlib.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2024 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
|  * | ||||
|  */ | ||||
| 
 | ||||
| import Inflator from "../inflator.js"; | ||||
| 
 | ||||
| export default class ZlibDecoder { | ||||
|     constructor() { | ||||
|         this._zlib = new Inflator(); | ||||
|         this._length = 0; | ||||
|     } | ||||
| 
 | ||||
|     decodeRect(x, y, width, height, sock, display, depth) { | ||||
|         if ((width === 0) || (height === 0)) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         if (this._length === 0) { | ||||
|             if (sock.rQwait("ZLIB", 4)) { | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             this._length = sock.rQshift32(); | ||||
|         } | ||||
| 
 | ||||
|         if (sock.rQwait("ZLIB", this._length)) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         let data = new Uint8Array(sock.rQshiftBytes(this._length, false)); | ||||
|         this._length = 0; | ||||
| 
 | ||||
|         this._zlib.setInput(data); | ||||
|         data = this._zlib.inflate(width * height * 4); | ||||
|         this._zlib.setInput(null); | ||||
| 
 | ||||
|         // Max sure the image is fully opaque
 | ||||
|         for (let i = 0; i < width * height; i++) { | ||||
|             data[i * 4 + 3] = 255; | ||||
|         } | ||||
| 
 | ||||
|         display.blitImage(x, y, width, height, data, 0); | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| } | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2021 The noVNC Authors | ||||
|  * Copyright (C) 2021 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
| @ -32,7 +32,7 @@ export default class ZRLEDecoder { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         const data = sock.rQshiftBytes(this._length); | ||||
|         const data = sock.rQshiftBytes(this._length, false); | ||||
| 
 | ||||
|         this._inflator.setInput(data); | ||||
| 
 | ||||
|  | ||||
| @ -1,13 +1,13 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2020 The noVNC Authors | ||||
|  * Copyright (C) 2020 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
|  */ | ||||
| 
 | ||||
| import { deflateInit, deflate } from "../vendor/pako/lib/zlib/deflate.js"; | ||||
| import { Z_FULL_FLUSH } from "../vendor/pako/lib/zlib/deflate.js"; | ||||
| import { Z_FULL_FLUSH, Z_DEFAULT_COMPRESSION } from "../vendor/pako/lib/zlib/deflate.js"; | ||||
| import ZStream from "../vendor/pako/lib/zlib/zstream.js"; | ||||
| 
 | ||||
| export default class Deflator { | ||||
| @ -15,9 +15,8 @@ export default class Deflator { | ||||
|         this.strm = new ZStream(); | ||||
|         this.chunkSize = 1024 * 10 * 10; | ||||
|         this.outputBuffer = new Uint8Array(this.chunkSize); | ||||
|         this.windowBits = 5; | ||||
| 
 | ||||
|         deflateInit(this.strm, this.windowBits); | ||||
|         deflateInit(this.strm, Z_DEFAULT_COMPRESSION); | ||||
|     } | ||||
| 
 | ||||
|     deflate(inData) { | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2019 The noVNC Authors | ||||
|  * Copyright (C) 2019 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
| @ -15,7 +15,7 @@ export default class Display { | ||||
|         this._drawCtx = null; | ||||
| 
 | ||||
|         this._renderQ = [];  // queue drawing actions for in-oder rendering
 | ||||
|         this._flushing = false; | ||||
|         this._flushPromise = null; | ||||
| 
 | ||||
|         // the full frame buffer (logical canvas) size
 | ||||
|         this._fbWidth = 0; | ||||
| @ -61,10 +61,6 @@ export default class Display { | ||||
| 
 | ||||
|         this._scale = 1.0; | ||||
|         this._clipViewport = false; | ||||
| 
 | ||||
|         // ===== EVENT HANDLERS =====
 | ||||
| 
 | ||||
|         this.onflush = () => {}; // A flush request has finished
 | ||||
|     } | ||||
| 
 | ||||
|     // ===== PROPERTIES =====
 | ||||
| @ -306,9 +302,14 @@ export default class Display { | ||||
| 
 | ||||
|     flush() { | ||||
|         if (this._renderQ.length === 0) { | ||||
|             this.onflush(); | ||||
|             return Promise.resolve(); | ||||
|         } else { | ||||
|             this._flushing = true; | ||||
|             if (this._flushPromise === null) { | ||||
|                 this._flushPromise = new Promise((resolve) => { | ||||
|                     this._flushResolve = resolve; | ||||
|                 }); | ||||
|             } | ||||
|             return this._flushPromise; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -379,6 +380,17 @@ export default class Display { | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     videoFrame(x, y, width, height, frame) { | ||||
|         this._renderQPush({ | ||||
|             'type': 'frame', | ||||
|             'frame': frame, | ||||
|             'x': x, | ||||
|             'y': y, | ||||
|             'width': width, | ||||
|             'height': height | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     blitImage(x, y, width, height, arr, offset, fromQueue) { | ||||
|         if (this._renderQ.length !== 0 && !fromQueue) { | ||||
|             // NB(directxman12): it's technically more performant here to use preallocated arrays,
 | ||||
| @ -405,9 +417,16 @@ export default class Display { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     drawImage(img, x, y) { | ||||
|         this._drawCtx.drawImage(img, x, y); | ||||
|     drawImage(img, ...args) { | ||||
|         this._drawCtx.drawImage(img, ...args); | ||||
| 
 | ||||
|         if (args.length <= 4) { | ||||
|             const [x, y] = args; | ||||
|             this._damage(x, y, img.width, img.height); | ||||
|         } else { | ||||
|             const [,, sw, sh, dx, dy] = args; | ||||
|             this._damage(dx, dy, sw, sh); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     autoscale(containerWidth, containerHeight) { | ||||
| @ -510,6 +529,35 @@ export default class Display { | ||||
|                         ready = false; | ||||
|                     } | ||||
|                     break; | ||||
|                 case 'frame': | ||||
|                     if (a.frame.ready) { | ||||
|                         // The encoded frame may be larger than the rect due to
 | ||||
|                         // limitations of the encoder, so we need to crop the
 | ||||
|                         // frame.
 | ||||
|                         let frame = a.frame.frame; | ||||
|                         if (frame.codedWidth < a.width || frame.codedHeight < a.height) { | ||||
|                             Log.Warn("Decoded video frame does not cover its full rectangle area. Expecting at least " + | ||||
|                                       a.width + "x" + a.height + " but got " + | ||||
|                                       frame.codedWidth + "x" + frame.codedHeight); | ||||
|                         } | ||||
|                         const sx = 0; | ||||
|                         const sy = 0; | ||||
|                         const sw = a.width; | ||||
|                         const sh = a.height; | ||||
|                         const dx = a.x; | ||||
|                         const dy = a.y; | ||||
|                         const dw = sw; | ||||
|                         const dh = sh; | ||||
|                         this.drawImage(frame, sx, sy, sw, sh, dx, dy, dw, dh); | ||||
|                         frame.close(); | ||||
|                     } else { | ||||
|                         let display = this; | ||||
|                         a.frame.promise.then(() => { | ||||
|                             display._scanRenderQ(); | ||||
|                         }); | ||||
|                         ready = false; | ||||
|                     } | ||||
|                     break; | ||||
|             } | ||||
| 
 | ||||
|             if (ready) { | ||||
| @ -517,9 +565,11 @@ export default class Display { | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (this._renderQ.length === 0 && this._flushing) { | ||||
|             this._flushing = false; | ||||
|             this.onflush(); | ||||
|         if (this._renderQ.length === 0 && | ||||
|             this._flushPromise !== null) { | ||||
|             this._flushResolve(); | ||||
|             this._flushPromise = null; | ||||
|             this._flushResolve = null; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2019 The noVNC Authors | ||||
|  * Copyright (C) 2019 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
| @ -11,10 +11,12 @@ export const encodings = { | ||||
|     encodingCopyRect: 1, | ||||
|     encodingRRE: 2, | ||||
|     encodingHextile: 5, | ||||
|     encodingZlib: 6, | ||||
|     encodingTight: 7, | ||||
|     encodingZRLE: 16, | ||||
|     encodingTightPNG: -260, | ||||
|     encodingJPEG: 21, | ||||
|     encodingH264: 50, | ||||
| 
 | ||||
|     pseudoEncodingQualityLevel9: -23, | ||||
|     pseudoEncodingQualityLevel0: -32, | ||||
| @ -22,11 +24,13 @@ export const encodings = { | ||||
|     pseudoEncodingLastRect: -224, | ||||
|     pseudoEncodingCursor: -239, | ||||
|     pseudoEncodingQEMUExtendedKeyEvent: -258, | ||||
|     pseudoEncodingQEMULedEvent: -261, | ||||
|     pseudoEncodingDesktopName: -307, | ||||
|     pseudoEncodingExtendedDesktopSize: -308, | ||||
|     pseudoEncodingXvp: -309, | ||||
|     pseudoEncodingFence: -312, | ||||
|     pseudoEncodingContinuousUpdates: -313, | ||||
|     pseudoEncodingExtendedMouseButtons: -316, | ||||
|     pseudoEncodingCompressLevel9: -247, | ||||
|     pseudoEncodingCompressLevel0: -256, | ||||
|     pseudoEncodingVMwareCursor: 0x574d5664, | ||||
| @ -39,10 +43,12 @@ export function encodingName(num) { | ||||
|         case encodings.encodingCopyRect: return "CopyRect"; | ||||
|         case encodings.encodingRRE:      return "RRE"; | ||||
|         case encodings.encodingHextile:  return "Hextile"; | ||||
|         case encodings.encodingZlib:     return "Zlib"; | ||||
|         case encodings.encodingTight:    return "Tight"; | ||||
|         case encodings.encodingZRLE:     return "ZRLE"; | ||||
|         case encodings.encodingTightPNG: return "TightPNG"; | ||||
|         case encodings.encodingJPEG:     return "JPEG"; | ||||
|         case encodings.encodingH264:     return "H.264"; | ||||
|         default:                         return "[unknown encoding " + num + "]"; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2020 The noVNC Authors | ||||
|  * Copyright (C) 2020 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
| @ -14,9 +14,8 @@ export default class Inflate { | ||||
|         this.strm = new ZStream(); | ||||
|         this.chunkSize = 1024 * 10 * 10; | ||||
|         this.strm.output = new Uint8Array(this.chunkSize); | ||||
|         this.windowBits = 5; | ||||
| 
 | ||||
|         inflateInit(this.strm, this.windowBits); | ||||
|         inflateInit(this.strm); | ||||
|     } | ||||
| 
 | ||||
|     setInput(data) { | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2018 The noVNC Authors | ||||
|  * Copyright (C) 2018 The noVNC authors | ||||
|  * Licensed under MPL 2.0 or any later version (see LICENSE.txt) | ||||
|  */ | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2018 The noVNC Authors | ||||
|  * Copyright (C) 2018 The noVNC authors | ||||
|  * Licensed under MPL 2.0 or any later version (see LICENSE.txt) | ||||
|  */ | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2020 The noVNC Authors | ||||
|  * Copyright (C) 2020 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2019 The noVNC Authors | ||||
|  * Copyright (C) 2019 The noVNC authors | ||||
|  * Licensed under MPL 2.0 or any later version (see LICENSE.txt) | ||||
|  */ | ||||
| 
 | ||||
| @ -36,7 +36,7 @@ export default class Keyboard { | ||||
| 
 | ||||
|     // ===== PRIVATE METHODS =====
 | ||||
| 
 | ||||
|     _sendKeyEvent(keysym, code, down) { | ||||
|     _sendKeyEvent(keysym, code, down, numlock = null, capslock = null) { | ||||
|         if (down) { | ||||
|             this._keyDownList[code] = keysym; | ||||
|         } else { | ||||
| @ -48,8 +48,9 @@ export default class Keyboard { | ||||
|         } | ||||
| 
 | ||||
|         Log.Debug("onkeyevent " + (down ? "down" : "up") + | ||||
|                   ", keysym: " + keysym, ", code: " + code); | ||||
|         this.onkeyevent(keysym, code, down); | ||||
|                   ", keysym: " + keysym, ", code: " + code + | ||||
|                   ", numlock: " + numlock + ", capslock: " + capslock); | ||||
|         this.onkeyevent(keysym, code, down, numlock, capslock); | ||||
|     } | ||||
| 
 | ||||
|     _getKeyCode(e) { | ||||
| @ -86,6 +87,14 @@ export default class Keyboard { | ||||
|     _handleKeyDown(e) { | ||||
|         const code = this._getKeyCode(e); | ||||
|         let keysym = KeyboardUtil.getKeysym(e); | ||||
|         let numlock = e.getModifierState('NumLock'); | ||||
|         let capslock = e.getModifierState('CapsLock'); | ||||
| 
 | ||||
|         // getModifierState for NumLock is not supported on mac and ios and always returns false.
 | ||||
|         // Set to null to indicate unknown/unsupported instead.
 | ||||
|         if (browser.isMac() || browser.isIOS()) { | ||||
|             numlock = null; | ||||
|         } | ||||
| 
 | ||||
|         // Windows doesn't have a proper AltGr, but handles it using
 | ||||
|         // fake Ctrl+Alt. However the remote end might not be Windows,
 | ||||
| @ -107,7 +116,7 @@ export default class Keyboard { | ||||
|                 //        key to "AltGraph".
 | ||||
|                 keysym = KeyTable.XK_ISO_Level3_Shift; | ||||
|             } else { | ||||
|                 this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); | ||||
|                 this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true, numlock, capslock); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| @ -118,8 +127,8 @@ export default class Keyboard { | ||||
|                 // If it's a virtual keyboard then it should be
 | ||||
|                 // sufficient to just send press and release right
 | ||||
|                 // after each other
 | ||||
|                 this._sendKeyEvent(keysym, code, true); | ||||
|                 this._sendKeyEvent(keysym, code, false); | ||||
|                 this._sendKeyEvent(keysym, code, true, numlock, capslock); | ||||
|                 this._sendKeyEvent(keysym, code, false, numlock, capslock); | ||||
|             } | ||||
| 
 | ||||
|             stopEvent(e); | ||||
| @ -157,8 +166,8 @@ export default class Keyboard { | ||||
|         // 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); | ||||
|             this._sendKeyEvent(keysym, code, true, numlock, capslock); | ||||
|             this._sendKeyEvent(keysym, code, false, numlock, capslock); | ||||
|             stopEvent(e); | ||||
|             return; | ||||
|         } | ||||
| @ -168,8 +177,8 @@ export default class Keyboard { | ||||
|         // which toggles on each press, but not on release. So pretend
 | ||||
|         // it was a quick press and release of the button.
 | ||||
|         if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) { | ||||
|             this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true); | ||||
|             this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false); | ||||
|             this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true, numlock, capslock); | ||||
|             this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false, numlock, capslock); | ||||
|             stopEvent(e); | ||||
|             return; | ||||
|         } | ||||
| @ -182,8 +191,8 @@ export default class Keyboard { | ||||
|                             KeyTable.XK_Hiragana, | ||||
|                             KeyTable.XK_Romaji ]; | ||||
|         if (browser.isWindows() && jpBadKeys.includes(keysym)) { | ||||
|             this._sendKeyEvent(keysym, code, true); | ||||
|             this._sendKeyEvent(keysym, code, false); | ||||
|             this._sendKeyEvent(keysym, code, true, numlock, capslock); | ||||
|             this._sendKeyEvent(keysym, code, false, numlock, capslock); | ||||
|             stopEvent(e); | ||||
|             return; | ||||
|         } | ||||
| @ -194,12 +203,12 @@ export default class Keyboard { | ||||
|         if ((code === "ControlLeft") && browser.isWindows() && | ||||
|             !("ControlLeft" in this._keyDownList)) { | ||||
|             this._altGrArmed = true; | ||||
|             this._altGrTimeout = setTimeout(this._handleAltGrTimeout.bind(this), 100); | ||||
|             this._altGrTimeout = setTimeout(this._interruptAltGrSequence.bind(this), 100); | ||||
|             this._altGrCtrlTime = e.timeStamp; | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this._sendKeyEvent(keysym, code, true); | ||||
|         this._sendKeyEvent(keysym, code, true, numlock, capslock); | ||||
|     } | ||||
| 
 | ||||
|     _handleKeyUp(e) { | ||||
| @ -209,11 +218,7 @@ export default class Keyboard { | ||||
| 
 | ||||
|         // We can't get a release in the middle of an AltGr sequence, so
 | ||||
|         // abort that detection
 | ||||
|         if (this._altGrArmed) { | ||||
|             this._altGrArmed = false; | ||||
|             clearTimeout(this._altGrTimeout); | ||||
|             this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); | ||||
|         } | ||||
|         this._interruptAltGrSequence(); | ||||
| 
 | ||||
|         // See comment in _handleKeyDown()
 | ||||
|         if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) { | ||||
| @ -240,14 +245,20 @@ export default class Keyboard { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     _handleAltGrTimeout() { | ||||
|     _interruptAltGrSequence() { | ||||
|         if (this._altGrArmed) { | ||||
|             this._altGrArmed = false; | ||||
|             clearTimeout(this._altGrTimeout); | ||||
|             this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     _allKeysUp() { | ||||
|         Log.Debug(">> Keyboard.allKeysUp"); | ||||
| 
 | ||||
|         // Prevent control key being processed after losing focus.
 | ||||
|         this._interruptAltGrSequence(); | ||||
| 
 | ||||
|         for (let code in this._keyDownList) { | ||||
|             this._sendKeyEvent(this._keyDownList[code], code, false); | ||||
|         } | ||||
|  | ||||
| @ -67,7 +67,7 @@ export function getKeycode(evt) { | ||||
| // Get 'KeyboardEvent.key', handling legacy browsers
 | ||||
| export function getKey(evt) { | ||||
|     // Are we getting a proper key value?
 | ||||
|     if (evt.key !== undefined) { | ||||
|     if ((evt.key !== undefined) && (evt.key !== 'Unidentified')) { | ||||
|         // Mozilla isn't fully in sync with the spec yet
 | ||||
|         switch (evt.key) { | ||||
|             case 'OS': return 'Meta'; | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2018 The noVNC Authors | ||||
|  * Copyright (C) 2018 The noVNC authors | ||||
|  * Licensed under MPL 2.0 or any later version (see LICENSE.txt) | ||||
|  */ | ||||
| 
 | ||||
|  | ||||
| @ -1,146 +1,25 @@ | ||||
| import Base64 from './base64.js'; | ||||
| import { encodeUTF8 } from './util/strings.js'; | ||||
| import EventTargetMixin from './util/eventtarget.js'; | ||||
| import legacyCrypto from './crypto/crypto.js'; | ||||
| 
 | ||||
| export class AESEAXCipher { | ||||
| class RA2Cipher { | ||||
|     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._cipher = null; | ||||
|         this._counter = new Uint8Array(16); | ||||
|     } | ||||
| 
 | ||||
|     async setKey(key) { | ||||
|         await this._cipher.setKey(key); | ||||
|         this._cipher = await legacyCrypto.importKey( | ||||
|             "raw", key, { name: "AES-EAX" }, false, ["encrypt, decrypt"]); | ||||
|     } | ||||
| 
 | ||||
|     async makeMessage(message) { | ||||
|         const ad = new Uint8Array([(message.length & 0xff00) >>> 8, message.length & 0xff]); | ||||
|         const encrypted = await this._cipher.encrypt(message, ad, this._counter); | ||||
|         const encrypted = await legacyCrypto.encrypt({ | ||||
|             name: "AES-EAX", | ||||
|             iv: this._counter, | ||||
|             additionalData: ad, | ||||
|         }, this._cipher, message); | ||||
|         for (let i = 0; i < 16 && this._counter[i]++ === 255; i++); | ||||
|         const res = new Uint8Array(message.length + 2 + 16); | ||||
|         res.set(ad); | ||||
| @ -148,164 +27,18 @@ export class RA2Cipher { | ||||
|         return res; | ||||
|     } | ||||
| 
 | ||||
|     async receiveMessage(length, encrypted, mac) { | ||||
|     async receiveMessage(length, encrypted) { | ||||
|         const ad = new Uint8Array([(length & 0xff00) >>> 8, length & 0xff]); | ||||
|         const res = await this._cipher.decrypt(encrypted, ad, this._counter, mac); | ||||
|         const res = await legacyCrypto.decrypt({ | ||||
|             name: "AES-EAX", | ||||
|             iv: this._counter, | ||||
|             additionalData: ad, | ||||
|         }, this._cipher, encrypted); | ||||
|         for (let i = 0; i < 16 && this._counter[i]++ === 255; i++); | ||||
|         return res; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export 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(); | ||||
| @ -406,7 +139,7 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { | ||||
|         this._hasStarted = true; | ||||
|         // 1: Receive server public key
 | ||||
|         await this._waitSockAsync(4); | ||||
|         const serverKeyLengthBuffer = this._sock.rQslice(0, 4); | ||||
|         const serverKeyLengthBuffer = this._sock.rQpeekBytes(4); | ||||
|         const serverKeyLength = this._sock.rQshift32(); | ||||
|         if (serverKeyLength < 1024) { | ||||
|             throw new Error("RA2: server public key is too short: " + serverKeyLength); | ||||
| @ -417,26 +150,31 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { | ||||
|         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 serverRSACipher = await legacyCrypto.importKey( | ||||
|             "raw", { n: serverN, e: serverE }, { name: "RSA-PKCS1-v1_5" }, false, ["encrypt"]); | ||||
|         const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2); | ||||
|         serverPublickey.set(serverKeyLengthBuffer); | ||||
|         serverPublickey.set(serverN, 4); | ||||
|         serverPublickey.set(serverE, 4 + serverKeyBytes); | ||||
| 
 | ||||
|         // verify server public key
 | ||||
|         let approveKey = this._waitApproveKeyAsync(); | ||||
|         this.dispatchEvent(new CustomEvent("serververification", { | ||||
|             detail: { type: "RSA", publickey: serverPublickey } | ||||
|         })); | ||||
|         await this._waitApproveKeyAsync(); | ||||
|         await approveKey; | ||||
| 
 | ||||
|         // 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 clientRSACipher = (await legacyCrypto.generateKey({ | ||||
|             name: "RSA-PKCS1-v1_5", | ||||
|             modulusLength: clientKeyLength, | ||||
|             publicExponent: new Uint8Array([1, 0, 1]), | ||||
|         }, true, ["encrypt"])).privateKey; | ||||
|         const clientExportedRSAKey = await legacyCrypto.exportKey("raw", clientRSACipher); | ||||
|         const clientN = clientExportedRSAKey.n; | ||||
|         const clientE = clientExportedRSAKey.e; | ||||
|         const clientPublicKey = new Uint8Array(4 + clientKeyBytes * 2); | ||||
|         clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24; | ||||
|         clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16; | ||||
| @ -444,17 +182,20 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { | ||||
|         clientPublicKey[3] = clientKeyLength & 0xff; | ||||
|         clientPublicKey.set(clientN, 4); | ||||
|         clientPublicKey.set(clientE, 4 + clientKeyBytes); | ||||
|         this._sock.send(clientPublicKey); | ||||
|         this._sock.sQpushBytes(clientPublicKey); | ||||
|         this._sock.flush(); | ||||
| 
 | ||||
|         // 3: Send client random
 | ||||
|         const clientRandom = new Uint8Array(16); | ||||
|         window.crypto.getRandomValues(clientRandom); | ||||
|         const clientEncryptedRandom = serverRSACipher.encrypt(clientRandom); | ||||
|         const clientEncryptedRandom = await legacyCrypto.encrypt( | ||||
|             { name: "RSA-PKCS1-v1_5" }, serverRSACipher, clientRandom); | ||||
|         const clientRandomMessage = new Uint8Array(2 + serverKeyBytes); | ||||
|         clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8; | ||||
|         clientRandomMessage[1] = serverKeyBytes & 0xff; | ||||
|         clientRandomMessage.set(clientEncryptedRandom, 2); | ||||
|         this._sock.send(clientRandomMessage); | ||||
|         this._sock.sQpushBytes(clientRandomMessage); | ||||
|         this._sock.flush(); | ||||
| 
 | ||||
|         // 4: Receive server random
 | ||||
|         await this._waitSockAsync(2); | ||||
| @ -462,7 +203,8 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { | ||||
|             throw new Error("RA2: wrong encrypted message length"); | ||||
|         } | ||||
|         const serverEncryptedRandom = this._sock.rQshiftBytes(clientKeyBytes); | ||||
|         const serverRandom = clientRSACipher.decrypt(serverEncryptedRandom); | ||||
|         const serverRandom = await legacyCrypto.decrypt( | ||||
|             { name: "RSA-PKCS1-v1_5" }, clientRSACipher, serverEncryptedRandom); | ||||
|         if (serverRandom === null || serverRandom.length !== 16) { | ||||
|             throw new Error("RA2: corrupted server encrypted random"); | ||||
|         } | ||||
| @ -494,13 +236,14 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { | ||||
|         clientHash = await window.crypto.subtle.digest("SHA-1", clientHash); | ||||
|         serverHash = new Uint8Array(serverHash); | ||||
|         clientHash = new Uint8Array(clientHash); | ||||
|         this._sock.send(await clientCipher.makeMessage(clientHash)); | ||||
|         this._sock.sQpushBytes(await clientCipher.makeMessage(clientHash)); | ||||
|         this._sock.flush(); | ||||
|         await this._waitSockAsync(2 + 20 + 16); | ||||
|         if (this._sock.rQshift16() !== 20) { | ||||
|             throw new Error("RA2: wrong server hash"); | ||||
|         } | ||||
|         const serverHashReceived = await serverCipher.receiveMessage( | ||||
|             20, this._sock.rQshiftBytes(20), this._sock.rQshiftBytes(16)); | ||||
|             20, this._sock.rQshiftBytes(20 + 16)); | ||||
|         if (serverHashReceived === null) { | ||||
|             throw new Error("RA2: failed to authenticate the message"); | ||||
|         } | ||||
| @ -516,11 +259,12 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { | ||||
|             throw new Error("RA2: wrong subtype"); | ||||
|         } | ||||
|         let subtype = (await serverCipher.receiveMessage( | ||||
|             1, this._sock.rQshiftBytes(1), this._sock.rQshiftBytes(16))); | ||||
|             1, this._sock.rQshiftBytes(1 + 16))); | ||||
|         if (subtype === null) { | ||||
|             throw new Error("RA2: failed to authenticate the message"); | ||||
|         } | ||||
|         subtype = subtype[0]; | ||||
|         let waitCredentials = this._waitCredentialsAsync(subtype); | ||||
|         if (subtype === 1) { | ||||
|             if (this._getCredentials().username === undefined || | ||||
|                 this._getCredentials().password === undefined) { | ||||
| @ -537,7 +281,7 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { | ||||
|         } else { | ||||
|             throw new Error("RA2: wrong subtype"); | ||||
|         } | ||||
|         await this._waitCredentialsAsync(subtype); | ||||
|         await waitCredentials; | ||||
|         let username; | ||||
|         if (subtype === 1) { | ||||
|             username = encodeUTF8(this._getCredentials().username).slice(0, 255); | ||||
| @ -554,7 +298,8 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { | ||||
|         for (let i = 0; i < password.length; i++) { | ||||
|             credentials[username.length + 2 + i] = password.charCodeAt(i); | ||||
|         } | ||||
|         this._sock.send(await clientCipher.makeMessage(credentials)); | ||||
|         this._sock.sQpushBytes(await clientCipher.makeMessage(credentials)); | ||||
|         this._sock.flush(); | ||||
|     } | ||||
| 
 | ||||
|     get hasStarted() { | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2019 The noVNC Authors | ||||
|  * Copyright (C) 2019 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
| @ -9,10 +9,11 @@ | ||||
|  */ | ||||
| 
 | ||||
| import * as Log from './logging.js'; | ||||
| import Base64 from '../base64.js'; | ||||
| 
 | ||||
| // Touch detection
 | ||||
| export let isTouchDevice = ('ontouchstart' in document.documentElement) || | ||||
|                                  // requried for Chrome debugger
 | ||||
|                                  // required for Chrome debugger
 | ||||
|                                  (document.ontouchstart !== undefined) || | ||||
|                                  // required for MS Surface
 | ||||
|                                  (navigator.maxTouchPoints > 0) || | ||||
| @ -70,6 +71,86 @@ try { | ||||
| } | ||||
| export const hasScrollbarGutter = _hasScrollbarGutter; | ||||
| 
 | ||||
| export let supportsWebCodecsH264Decode = false; | ||||
| 
 | ||||
| async function _checkWebCodecsH264DecodeSupport() { | ||||
|     if (!('VideoDecoder' in window)) { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     // We'll need to make do with some placeholders here
 | ||||
|     const config = { | ||||
|         codec: 'avc1.42401f', | ||||
|         codedWidth: 1920, | ||||
|         codedHeight: 1080, | ||||
|         optimizeForLatency: true, | ||||
|     }; | ||||
| 
 | ||||
|     let support = await VideoDecoder.isConfigSupported(config); | ||||
|     if (!support.supported) { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     // Firefox incorrectly reports supports for H.264 under some
 | ||||
|     // circumstances, so we need to actually test a real frame
 | ||||
|     // https://bugzilla.mozilla.org/show_bug.cgi?id=1932392
 | ||||
| 
 | ||||
|     const data = new Uint8Array(Base64.decode( | ||||
|         'AAAAAWdCwBTZnpuAgICgAAADACAAAAZB4oVNAAAAAWjJYyyAAAABBgX//4Hc' + | ||||
|         'Rem95tlIt5Ys2CDZI+7veDI2NCAtIGNvcmUgMTY0IHIzMTA4IDMxZTE5Zjkg' + | ||||
|         'LSBILjI2NC9NUEVHLTQgQVZDIGNvZGVjIC0gQ29weWxlZnQgMjAwMy0yMDIz' + | ||||
|         'IC0gaHR0cDovL3d3dy52aWRlb2xhbi5vcmcveDI2NC5odG1sIC0gb3B0aW9u' + | ||||
|         'czogY2FiYWM9MCByZWY9NSBkZWJsb2NrPTE6MDowIGFuYWx5c2U9MHgxOjB4' + | ||||
|         'MTExIG1lPWhleCBzdWJtZT04IHBzeT0xIHBzeV9yZD0xLjAwOjAuMDAgbWl4' + | ||||
|         'ZWRfcmVmPTEgbWVfcmFuZ2U9MTYgY2hyb21hX21lPTEgdHJlbGxpcz0yIDh4' + | ||||
|         'OGRjdD0wIGNxbT0wIGRlYWR6b25lPTIxLDExIGZhc3RfcHNraXA9MSBjaHJv' + | ||||
|         'bWFfcXBfb2Zmc2V0PS0yIHRocmVhZHM9MSBsb29rYWhlYWRfdGhyZWFkcz0x' + | ||||
|         'IHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNpbWF0ZT0xIGludGVybGFjZWQ9' + | ||||
|         'MCBibHVyYXlfY29tcGF0PTAgY29uc3RyYWluZWRfaW50cmE9MCBiZnJhbWVz' + | ||||
|         'PTAgd2VpZ2h0cD0wIGtleWludD1pbmZpbml0ZSBrZXlpbnRfbWluPTI1IHNj' + | ||||
|         'ZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NTAgcmM9' + | ||||
|         'YWJyIG1idHJlZT0xIGJpdHJhdGU9NDAwIHJhdGV0b2w9MS4wIHFjb21wPTAu' + | ||||
|         'NjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFx' + | ||||
|         'PTE6MS4wMACAAAABZYiEBrxmKAAPVccAAS044AA5DRJMnkycJk4TPw==')); | ||||
| 
 | ||||
|     let gotframe = false; | ||||
|     let error = null; | ||||
| 
 | ||||
|     let decoder = new VideoDecoder({ | ||||
|         output: (frame) => { gotframe = true; }, | ||||
|         error: (e) => { error = e; }, | ||||
|     }); | ||||
|     let chunk = new EncodedVideoChunk({ | ||||
|         timestamp: 0, | ||||
|         type: 'key', | ||||
|         data: data, | ||||
|     }); | ||||
| 
 | ||||
|     decoder.configure(config); | ||||
|     decoder.decode(chunk); | ||||
|     try { | ||||
|         await decoder.flush(); | ||||
|     } catch (e) { | ||||
|         // Firefox incorrectly throws an exception here
 | ||||
|         // https://bugzilla.mozilla.org/show_bug.cgi?id=1932566
 | ||||
|         error = e; | ||||
|     } | ||||
| 
 | ||||
|     // Firefox fails to deliver the error on Windows, so we need to
 | ||||
|     // check if we got a frame instead
 | ||||
|     // https://bugzilla.mozilla.org/show_bug.cgi?id=1932579
 | ||||
|     if (!gotframe) { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     if (error !== null) { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     return true; | ||||
| } | ||||
| supportsWebCodecsH264Decode = await _checkWebCodecsH264DecodeSupport(); | ||||
| 
 | ||||
| /* | ||||
|  * The functions for detection of platforms and browsers below are exported | ||||
|  * but the use of these should be minimized as much as possible. | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2019 The noVNC Authors | ||||
|  * Copyright (C) 2019 The noVNC authors | ||||
|  * Licensed under MPL 2.0 or any later version (see LICENSE.txt) | ||||
|  */ | ||||
| 
 | ||||
| @ -69,8 +69,10 @@ export default class Cursor { | ||||
|             this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options); | ||||
|             this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options); | ||||
| 
 | ||||
|             if (document.contains(this._canvas)) { | ||||
|                 document.body.removeChild(this._canvas); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         this._target = null; | ||||
|     } | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2020 The noVNC Authors | ||||
|  * Copyright (C) 2020 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2018 The noVNC Authors | ||||
|  * Copyright (C) 2018 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2019 The noVNC Authors | ||||
|  * Copyright (C) 2019 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2020 The noVNC Authors | ||||
|  * Copyright (C) 2020 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2019 The noVNC Authors | ||||
|  * Copyright (C) 2019 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * noVNC: HTML5 VNC client | ||||
|  * Copyright (C) 2019 The noVNC Authors | ||||
|  * Copyright (C) 2019 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * See README.md for usage and integration instructions. | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| /* | ||||
|  * Websock: high-performance buffering wrapper | ||||
|  * Copyright (C) 2019 The noVNC Authors | ||||
|  * Copyright (C) 2019 The noVNC authors | ||||
|  * Licensed under MPL 2.0 (see LICENSE.txt) | ||||
|  * | ||||
|  * Websock is similar to the standard WebSocket / RTCDataChannel object | ||||
| @ -70,7 +70,7 @@ export default class Websock { | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     // Getters and Setters
 | ||||
|     // Getters and setters
 | ||||
| 
 | ||||
|     get readyState() { | ||||
|         let subState; | ||||
| @ -94,27 +94,7 @@ export default class Websock { | ||||
|         return "unknown"; | ||||
|     } | ||||
| 
 | ||||
|     get sQ() { | ||||
|         return this._sQ; | ||||
|     } | ||||
| 
 | ||||
|     get rQ() { | ||||
|         return this._rQ; | ||||
|     } | ||||
| 
 | ||||
|     get rQi() { | ||||
|         return this._rQi; | ||||
|     } | ||||
| 
 | ||||
|     set rQi(val) { | ||||
|         this._rQi = val; | ||||
|     } | ||||
| 
 | ||||
|     // Receive Queue
 | ||||
|     get rQlen() { | ||||
|         return this._rQlen - this._rQi; | ||||
|     } | ||||
| 
 | ||||
|     // Receive queue
 | ||||
|     rQpeek8() { | ||||
|         return this._rQ[this._rQi]; | ||||
|     } | ||||
| @ -141,42 +121,47 @@ export default class Websock { | ||||
|         for (let byte = bytes - 1; byte >= 0; byte--) { | ||||
|             res += this._rQ[this._rQi++] << (byte * 8); | ||||
|         } | ||||
|         return res; | ||||
|         return res >>> 0; | ||||
|     } | ||||
| 
 | ||||
|     rQshiftStr(len) { | ||||
|         if (typeof(len) === 'undefined') { len = this.rQlen; } | ||||
|         let str = ""; | ||||
|         // Handle large arrays in steps to avoid long strings on the stack
 | ||||
|         for (let i = 0; i < len; i += 4096) { | ||||
|             let part = this.rQshiftBytes(Math.min(4096, len - i)); | ||||
|             let part = this.rQshiftBytes(Math.min(4096, len - i), false); | ||||
|             str += String.fromCharCode.apply(null, part); | ||||
|         } | ||||
|         return str; | ||||
|     } | ||||
| 
 | ||||
|     rQshiftBytes(len) { | ||||
|         if (typeof(len) === 'undefined') { len = this.rQlen; } | ||||
|     rQshiftBytes(len, copy=true) { | ||||
|         this._rQi += len; | ||||
|         return new Uint8Array(this._rQ.buffer, this._rQi - len, len); | ||||
|         if (copy) { | ||||
|             return this._rQ.slice(this._rQi - len, this._rQi); | ||||
|         } else { | ||||
|             return this._rQ.subarray(this._rQi - len, this._rQi); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     rQshiftTo(target, len) { | ||||
|         if (len === undefined) { len = this.rQlen; } | ||||
|         // TODO: make this just use set with views when using a ArrayBuffer to store the rQ
 | ||||
|         target.set(new Uint8Array(this._rQ.buffer, this._rQi, len)); | ||||
|         this._rQi += len; | ||||
|     } | ||||
| 
 | ||||
|     rQslice(start, end = this.rQlen) { | ||||
|         return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start); | ||||
|     rQpeekBytes(len, copy=true) { | ||||
|         if (copy) { | ||||
|             return this._rQ.slice(this._rQi, this._rQi + len); | ||||
|         } else { | ||||
|             return this._rQ.subarray(this._rQi, this._rQi + len); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // Check to see if we must wait for 'num' bytes (default to FBU.bytes)
 | ||||
|     // to be available in the receive queue. Return true if we need to
 | ||||
|     // wait (and possibly print a debug message), otherwise false.
 | ||||
|     rQwait(msg, num, goback) { | ||||
|         if (this.rQlen < num) { | ||||
|         if (this._rQlen - this._rQi < num) { | ||||
|             if (goback) { | ||||
|                 if (this._rQi < goback) { | ||||
|                     throw new Error("rQwait cannot backup " + goback + " bytes"); | ||||
| @ -188,26 +173,61 @@ export default class Websock { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     // Send Queue
 | ||||
|     // Send queue
 | ||||
| 
 | ||||
|     sQpush8(num) { | ||||
|         this._sQensureSpace(1); | ||||
|         this._sQ[this._sQlen++] = num; | ||||
|     } | ||||
| 
 | ||||
|     sQpush16(num) { | ||||
|         this._sQensureSpace(2); | ||||
|         this._sQ[this._sQlen++] = (num >> 8) & 0xff; | ||||
|         this._sQ[this._sQlen++] = (num >> 0) & 0xff; | ||||
|     } | ||||
| 
 | ||||
|     sQpush32(num) { | ||||
|         this._sQensureSpace(4); | ||||
|         this._sQ[this._sQlen++] = (num >> 24) & 0xff; | ||||
|         this._sQ[this._sQlen++] = (num >> 16) & 0xff; | ||||
|         this._sQ[this._sQlen++] = (num >>  8) & 0xff; | ||||
|         this._sQ[this._sQlen++] = (num >>  0) & 0xff; | ||||
|     } | ||||
| 
 | ||||
|     sQpushString(str) { | ||||
|         let bytes = str.split('').map(chr => chr.charCodeAt(0)); | ||||
|         this.sQpushBytes(new Uint8Array(bytes)); | ||||
|     } | ||||
| 
 | ||||
|     sQpushBytes(bytes) { | ||||
|         for (let offset = 0;offset < bytes.length;) { | ||||
|             this._sQensureSpace(1); | ||||
| 
 | ||||
|             let chunkSize = this._sQbufferSize - this._sQlen; | ||||
|             if (chunkSize > bytes.length - offset) { | ||||
|                 chunkSize = bytes.length - offset; | ||||
|             } | ||||
| 
 | ||||
|             this._sQ.set(bytes.subarray(offset, offset + chunkSize), this._sQlen); | ||||
|             this._sQlen += chunkSize; | ||||
|             offset += chunkSize; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     flush() { | ||||
|         if (this._sQlen > 0 && this.readyState === 'open') { | ||||
|             this._websocket.send(this._encodeMessage()); | ||||
|             this._websocket.send(new Uint8Array(this._sQ.buffer, 0, this._sQlen)); | ||||
|             this._sQlen = 0; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     send(arr) { | ||||
|         this._sQ.set(arr, this._sQlen); | ||||
|         this._sQlen += arr.length; | ||||
|     _sQensureSpace(bytes) { | ||||
|         if (this._sQbufferSize - this._sQlen < bytes) { | ||||
|             this.flush(); | ||||
|         } | ||||
| 
 | ||||
|     sendString(str) { | ||||
|         this.send(str.split('').map(chr => chr.charCodeAt(0))); | ||||
|     } | ||||
| 
 | ||||
|     // Event Handlers
 | ||||
|     // Event handlers
 | ||||
|     off(evt) { | ||||
|         this._eventHandlers[evt] = () => {}; | ||||
|     } | ||||
| @ -283,17 +303,12 @@ export default class Websock { | ||||
|     } | ||||
| 
 | ||||
|     // private methods
 | ||||
|     _encodeMessage() { | ||||
|         // Put in a binary arraybuffer
 | ||||
|         // according to the spec, you can send ArrayBufferViews with the send method
 | ||||
|         return new Uint8Array(this._sQ.buffer, 0, this._sQlen); | ||||
|     } | ||||
| 
 | ||||
|     // We want to move all the unread data to the start of the queue,
 | ||||
|     // e.g. compacting.
 | ||||
|     // The function also expands the receive que if needed, and for
 | ||||
|     // performance reasons we combine these two actions to avoid
 | ||||
|     // unneccessary copying.
 | ||||
|     // unnecessary copying.
 | ||||
|     _expandCompactRQ(minFit) { | ||||
|         // if we're using less than 1/8th of the buffer even with the incoming bytes, compact in place
 | ||||
|         // instead of resizing
 | ||||
| @ -309,8 +324,8 @@ export default class Websock { | ||||
|         // we don't want to grow unboundedly
 | ||||
|         if (this._rQbufferSize > MAX_RQ_GROW_SIZE) { | ||||
|             this._rQbufferSize = MAX_RQ_GROW_SIZE; | ||||
|             if (this._rQbufferSize - this.rQlen < minFit) { | ||||
|                 throw new Error("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit"); | ||||
|             if (this._rQbufferSize - (this._rQlen - this._rQi) < minFit) { | ||||
|                 throw new Error("Receive queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit"); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| @ -327,25 +342,22 @@ export default class Websock { | ||||
|     } | ||||
| 
 | ||||
|     // push arraybuffer values onto the end of the receive que
 | ||||
|     _DecodeMessage(data) { | ||||
|         const u8 = new Uint8Array(data); | ||||
|         if (u8.length > this._rQbufferSize - this._rQlen) { | ||||
|             this._expandCompactRQ(u8.length); | ||||
|         } | ||||
|         this._rQ.set(u8, this._rQlen); | ||||
|         this._rQlen += u8.length; | ||||
|     } | ||||
| 
 | ||||
|     _recvMessage(e) { | ||||
|         this._DecodeMessage(e.data); | ||||
|         if (this.rQlen > 0) { | ||||
|             this._eventHandlers.message(); | ||||
|         if (this._rQlen == this._rQi) { | ||||
|             // All data has now been processed, this means we
 | ||||
|             // can reset the receive queue.
 | ||||
|             this._rQlen = 0; | ||||
|             this._rQi = 0; | ||||
|         } | ||||
|         const u8 = new Uint8Array(e.data); | ||||
|         if (u8.length > this._rQbufferSize - this._rQlen) { | ||||
|             this._expandCompactRQ(u8.length); | ||||
|         } | ||||
|         this._rQ.set(u8, this._rQlen); | ||||
|         this._rQlen += u8.length; | ||||
| 
 | ||||
|         if (this._rQlen - this._rQi > 0) { | ||||
|             this._eventHandlers.message(); | ||||
|         } else { | ||||
|             Log.Debug("Ignoring empty message"); | ||||
|         } | ||||
|  | ||||
							
								
								
									
										1
									
								
								systemvm/agent/noVNC/defaults.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								systemvm/agent/noVNC/defaults.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| {} | ||||
| @ -3,13 +3,13 @@ | ||||
| .SH NAME | ||||
| novnc_proxy - noVNC proxy server | ||||
| .SH SYNOPSIS | ||||
| .B novnc_proxy [--listen PORT] [--vnc VNC_HOST:PORT] [--cert CERT] [--ssl-only] | ||||
| .B novnc_proxy [--listen [HOST:]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 | ||||
|     --listen [HOST:]PORT  Port for proxy/webserver to listen on | ||||
|                           Default: 6080 (on all interfaces) | ||||
|     --vnc VNC_HOST:PORT   VNC server host:port proxy target | ||||
|                           Default: localhost:5900 | ||||
|     --cert CERT           Path to combined cert/key file, or just | ||||
| @ -30,7 +30,7 @@ provides a cut-and-paste URL to go to. | ||||
|                           active connections | ||||
| 
 | ||||
| .SH AUTHOR | ||||
| The noVNC Authors | ||||
| The noVNC authors | ||||
| https://github.com/novnc/noVNC | ||||
| 
 | ||||
| .SH SEE ALSO | ||||
|  | ||||
							
								
								
									
										1
									
								
								systemvm/agent/noVNC/mandatory.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								systemvm/agent/noVNC/mandatory.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| {} | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "@novnc/novnc", | ||||
|   "version": "1.4.0", | ||||
|   "version": "1.6.0", | ||||
|   "description": "An HTML5 VNC client", | ||||
|   "browser": "lib/rfb", | ||||
|   "directories": { | ||||
| @ -14,9 +14,7 @@ | ||||
|     "VERSION", | ||||
|     "docs/API.md", | ||||
|     "docs/LIBRARY.md", | ||||
|     "docs/LICENSE*", | ||||
|     "core", | ||||
|     "vendor/pako" | ||||
|     "docs/LICENSE*" | ||||
|   ], | ||||
|   "scripts": { | ||||
|     "lint": "eslint app core po/po2js po/xgettext-html tests utils", | ||||
| @ -39,19 +37,14 @@ | ||||
|   "homepage": "https://github.com/novnc/noVNC", | ||||
|   "devDependencies": { | ||||
|     "@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", | ||||
|     "globals": "latest", | ||||
|     "jsdom": "latest", | ||||
|     "karma": "latest", | ||||
|     "karma-mocha": "latest", | ||||
| @ -62,10 +55,8 @@ | ||||
|     "karma-mocha-reporter": "latest", | ||||
|     "karma-safari-launcher": "latest", | ||||
|     "karma-script-launcher": "latest", | ||||
|     "karma-sinon-chai": "latest", | ||||
|     "mocha": "latest", | ||||
|     "node-getopt": "latest", | ||||
|     "po2json": "latest", | ||||
|     "pofile": "latest", | ||||
|     "sinon": "latest", | ||||
|     "sinon-chai": "latest" | ||||
|   }, | ||||
|  | ||||
| @ -1,300 +1,345 @@ | ||||
| # French translations for noVNC package | ||||
| # Traductions françaises du paquet noVNC. | ||||
| # Copyright (C) 2021 The noVNC Authors | ||||
| # Copyright (C) 2021 The noVNC authors | ||||
| # This file is distributed under the same license as the noVNC package. | ||||
| # Jose <jose.matsuda@canada.ca>, 2021. | ||||
| # Lowxorx <lowxorx@lahan.fr>, 2022. | ||||
| # | ||||
| msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: noVNC 1.2.0\n" | ||||
| "Project-Id-Version: noVNC 1.6.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 <lowxorx@lahan.fr>\n" | ||||
| "POT-Creation-Date: 2025-02-14 10:14+0100\n" | ||||
| "PO-Revision-Date: 2025-02-17 10:04+0100\n" | ||||
| "Last-Translator: Martine & Philippe <martineke.breizh@gmail.com>\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" | ||||
| "X-Generator: Poedit 3.5\n" | ||||
| 
 | ||||
| #: ../app/ui.js:394 | ||||
| #: ../app/ui.js:84 | ||||
| msgid "" | ||||
| "Running without HTTPS is not recommended, crashes or other issues are likely." | ||||
| msgstr "" | ||||
| "Lancer sans HTTPS n'est pas recommandé, crashs ou autres problèmes en vue." | ||||
| 
 | ||||
| #: ../app/ui.js:413 | ||||
| msgid "Connecting..." | ||||
| msgstr "En cours de connexion..." | ||||
| 
 | ||||
| #: ../app/ui.js:401 | ||||
| #: ../app/ui.js:420 | ||||
| msgid "Disconnecting..." | ||||
| msgstr "Déconnexion en cours..." | ||||
| 
 | ||||
| #: ../app/ui.js:407 | ||||
| #: ../app/ui.js:426 | ||||
| msgid "Reconnecting..." | ||||
| msgstr "Reconnexion en cours..." | ||||
| 
 | ||||
| #: ../app/ui.js:412 | ||||
| #: ../app/ui.js:431 | ||||
| msgid "Internal error" | ||||
| msgstr "Erreur interne" | ||||
| 
 | ||||
| #: ../app/ui.js:1008 | ||||
| msgid "Must set host" | ||||
| msgstr "Doit définir l'hôte" | ||||
| #: ../app/ui.js:1079 | ||||
| msgid "Failed to connect to server: " | ||||
| msgstr "Échec de connexion au serveur " | ||||
| 
 | ||||
| #: ../app/ui.js:1090 | ||||
| #: ../app/ui.js:1145 | ||||
| msgid "Connected (encrypted) to " | ||||
| msgstr "Connecté (chiffré) à " | ||||
| 
 | ||||
| #: ../app/ui.js:1092 | ||||
| #: ../app/ui.js:1147 | ||||
| msgid "Connected (unencrypted) to " | ||||
| msgstr "Connecté (non chiffré) à " | ||||
| 
 | ||||
| #: ../app/ui.js:1115 | ||||
| #: ../app/ui.js:1170 | ||||
| msgid "Something went wrong, connection is closed" | ||||
| msgstr "Quelque chose s'est mal passé, la connexion a été fermée" | ||||
| 
 | ||||
| #: ../app/ui.js:1118 | ||||
| #: ../app/ui.js:1173 | ||||
| msgid "Failed to connect to server" | ||||
| msgstr "Échec de connexion au serveur" | ||||
| 
 | ||||
| #: ../app/ui.js:1128 | ||||
| #: ../app/ui.js:1185 | ||||
| msgid "Disconnected" | ||||
| msgstr "Déconnecté" | ||||
| 
 | ||||
| #: ../app/ui.js:1143 | ||||
| #: ../app/ui.js:1200 | ||||
| msgid "New connection has been rejected with reason: " | ||||
| msgstr "Une nouvelle connexion a été rejetée avec motif : " | ||||
| 
 | ||||
| #: ../app/ui.js:1146 | ||||
| #: ../app/ui.js:1203 | ||||
| msgid "New connection has been rejected" | ||||
| msgstr "Une nouvelle connexion a été rejetée" | ||||
| 
 | ||||
| #: ../app/ui.js:1181 | ||||
| #: ../app/ui.js:1269 | ||||
| msgid "Credentials are required" | ||||
| msgstr "Les identifiants sont requis" | ||||
| 
 | ||||
| #: ../vnc.html:74 | ||||
| #: ../vnc.html:106 | ||||
| msgid "noVNC encountered an error:" | ||||
| msgstr "noVNC a rencontré une erreur :" | ||||
| 
 | ||||
| #: ../vnc.html:84 | ||||
| #: ../vnc.html:116 | ||||
| msgid "Hide/Show the control bar" | ||||
| msgstr "Masquer/Afficher la barre de contrôle" | ||||
| 
 | ||||
| #: ../vnc.html:91 | ||||
| #: ../vnc.html:125 | ||||
| msgid "Drag" | ||||
| msgstr "Faire glisser" | ||||
| 
 | ||||
| #: ../vnc.html:91 | ||||
| msgid "Move/Drag Viewport" | ||||
| msgstr "Déplacer/faire glisser le Viewport" | ||||
| #: ../vnc.html:125 | ||||
| msgid "Move/Drag viewport" | ||||
| msgstr "Déplacer la fenêtre de visualisation" | ||||
| 
 | ||||
| #: ../vnc.html:97 | ||||
| #: ../vnc.html:131 | ||||
| msgid "Keyboard" | ||||
| msgstr "Clavier" | ||||
| 
 | ||||
| #: ../vnc.html:97 | ||||
| msgid "Show Keyboard" | ||||
| #: ../vnc.html:131 | ||||
| msgid "Show keyboard" | ||||
| msgstr "Afficher le clavier" | ||||
| 
 | ||||
| #: ../vnc.html:102 | ||||
| #: ../vnc.html:136 | ||||
| msgid "Extra keys" | ||||
| msgstr "Touches supplémentaires" | ||||
| 
 | ||||
| #: ../vnc.html:102 | ||||
| msgid "Show Extra Keys" | ||||
| #: ../vnc.html:136 | ||||
| msgid "Show extra keys" | ||||
| msgstr "Afficher les touches supplémentaires" | ||||
| 
 | ||||
| #: ../vnc.html:107 | ||||
| #: ../vnc.html:141 | ||||
| msgid "Ctrl" | ||||
| msgstr "Ctrl" | ||||
| 
 | ||||
| #: ../vnc.html:107 | ||||
| #: ../vnc.html:141 | ||||
| msgid "Toggle Ctrl" | ||||
| msgstr "Basculer Ctrl" | ||||
| 
 | ||||
| #: ../vnc.html:110 | ||||
| #: ../vnc.html:144 | ||||
| msgid "Alt" | ||||
| msgstr "Alt" | ||||
| 
 | ||||
| #: ../vnc.html:110 | ||||
| #: ../vnc.html:144 | ||||
| msgid "Toggle Alt" | ||||
| msgstr "Basculer Alt" | ||||
| 
 | ||||
| #: ../vnc.html:113 | ||||
| #: ../vnc.html:147 | ||||
| msgid "Toggle Windows" | ||||
| msgstr "Basculer Windows" | ||||
| 
 | ||||
| #: ../vnc.html:113 | ||||
| #: ../vnc.html:147 | ||||
| msgid "Windows" | ||||
| msgstr "Windows" | ||||
| msgstr "Fenêtre" | ||||
| 
 | ||||
| #: ../vnc.html:116 | ||||
| #: ../vnc.html:150 | ||||
| msgid "Send Tab" | ||||
| msgstr "Envoyer l'onglet" | ||||
| msgstr "Envoyer Tab" | ||||
| 
 | ||||
| #: ../vnc.html:116 | ||||
| #: ../vnc.html:150 | ||||
| msgid "Tab" | ||||
| msgstr "l'onglet" | ||||
| msgstr "Tabulation" | ||||
| 
 | ||||
| #: ../vnc.html:119 | ||||
| #: ../vnc.html:153 | ||||
| msgid "Esc" | ||||
| msgstr "Esc" | ||||
| 
 | ||||
| #: ../vnc.html:119 | ||||
| #: ../vnc.html:153 | ||||
| msgid "Send Escape" | ||||
| msgstr "Envoyer Escape" | ||||
| 
 | ||||
| #: ../vnc.html:122 | ||||
| #: ../vnc.html:156 | ||||
| msgid "Ctrl+Alt+Del" | ||||
| msgstr "Ctrl+Alt+Del" | ||||
| 
 | ||||
| #: ../vnc.html:122 | ||||
| #: ../vnc.html:156 | ||||
| msgid "Send Ctrl-Alt-Del" | ||||
| msgstr "Envoyer Ctrl-Alt-Del" | ||||
| 
 | ||||
| #: ../vnc.html:129 | ||||
| #: ../vnc.html:163 | ||||
| msgid "Shutdown/Reboot" | ||||
| msgstr "Arrêter/Redémarrer" | ||||
| 
 | ||||
| #: ../vnc.html:129 | ||||
| #: ../vnc.html:163 | ||||
| msgid "Shutdown/Reboot..." | ||||
| msgstr "Arrêter/Redémarrer..." | ||||
| 
 | ||||
| #: ../vnc.html:135 | ||||
| #: ../vnc.html:169 | ||||
| msgid "Power" | ||||
| msgstr "Alimentation" | ||||
| 
 | ||||
| #: ../vnc.html:137 | ||||
| #: ../vnc.html:171 | ||||
| msgid "Shutdown" | ||||
| msgstr "Arrêter" | ||||
| 
 | ||||
| #: ../vnc.html:138 | ||||
| #: ../vnc.html:172 | ||||
| msgid "Reboot" | ||||
| msgstr "Redémarrer" | ||||
| 
 | ||||
| #: ../vnc.html:139 | ||||
| #: ../vnc.html:173 | ||||
| msgid "Reset" | ||||
| msgstr "Réinitialiser" | ||||
| 
 | ||||
| #: ../vnc.html:144 ../vnc.html:150 | ||||
| #: ../vnc.html:178 ../vnc.html:184 | ||||
| msgid "Clipboard" | ||||
| msgstr "Presse-papiers" | ||||
| 
 | ||||
| #: ../vnc.html:154 | ||||
| msgid "Clear" | ||||
| msgstr "Effacer" | ||||
| #: ../vnc.html:186 | ||||
| msgid "Edit clipboard content in the textarea below." | ||||
| msgstr "Editer le contenu du presse-papier dans la zone ci-dessous." | ||||
| 
 | ||||
| #: ../vnc.html:160 | ||||
| msgid "Fullscreen" | ||||
| #: ../vnc.html:194 | ||||
| msgid "Full screen" | ||||
| msgstr "Plein écran" | ||||
| 
 | ||||
| #: ../vnc.html:165 ../vnc.html:172 | ||||
| #: ../vnc.html:199 ../vnc.html:205 | ||||
| msgid "Settings" | ||||
| msgstr "Paramètres" | ||||
| 
 | ||||
| #: ../vnc.html:175 | ||||
| msgid "Shared Mode" | ||||
| #: ../vnc.html:211 | ||||
| msgid "Shared mode" | ||||
| msgstr "Mode partagé" | ||||
| 
 | ||||
| #: ../vnc.html:178 | ||||
| msgid "View Only" | ||||
| #: ../vnc.html:218 | ||||
| msgid "View only" | ||||
| msgstr "Afficher uniquement" | ||||
| 
 | ||||
| #: ../vnc.html:182 | ||||
| msgid "Clip to Window" | ||||
| msgstr "Clip à fenêtre" | ||||
| #: ../vnc.html:226 | ||||
| msgid "Clip to window" | ||||
| msgstr "Ajuster à la fenêtre" | ||||
| 
 | ||||
| #: ../vnc.html:185 | ||||
| msgid "Scaling Mode:" | ||||
| #: ../vnc.html:231 | ||||
| msgid "Scaling mode:" | ||||
| msgstr "Mode mise à l'échelle :" | ||||
| 
 | ||||
| #: ../vnc.html:187 | ||||
| #: ../vnc.html:233 | ||||
| msgid "None" | ||||
| msgstr "Aucun" | ||||
| 
 | ||||
| #: ../vnc.html:188 | ||||
| msgid "Local Scaling" | ||||
| #: ../vnc.html:234 | ||||
| msgid "Local scaling" | ||||
| msgstr "Mise à l'échelle locale" | ||||
| 
 | ||||
| #: ../vnc.html:189 | ||||
| msgid "Remote Resizing" | ||||
| #: ../vnc.html:235 | ||||
| msgid "Remote resizing" | ||||
| msgstr "Redimensionnement à distance" | ||||
| 
 | ||||
| #: ../vnc.html:194 | ||||
| #: ../vnc.html:240 | ||||
| msgid "Advanced" | ||||
| msgstr "Avancé" | ||||
| 
 | ||||
| #: ../vnc.html:197 | ||||
| #: ../vnc.html:243 | ||||
| msgid "Quality:" | ||||
| msgstr "Qualité :" | ||||
| 
 | ||||
| #: ../vnc.html:201 | ||||
| #: ../vnc.html:247 | ||||
| msgid "Compression level:" | ||||
| msgstr "Niveau de compression :" | ||||
| 
 | ||||
| #: ../vnc.html:206 | ||||
| #: ../vnc.html:252 | ||||
| msgid "Repeater ID:" | ||||
| msgstr "ID Répéteur :" | ||||
| 
 | ||||
| #: ../vnc.html:210 | ||||
| #: ../vnc.html:256 | ||||
| msgid "WebSocket" | ||||
| msgstr "WebSocket" | ||||
| 
 | ||||
| #: ../vnc.html:213 | ||||
| #: ../vnc.html:261 | ||||
| msgid "Encrypt" | ||||
| msgstr "Chiffrer" | ||||
| 
 | ||||
| #: ../vnc.html:216 | ||||
| #: ../vnc.html:266 | ||||
| msgid "Host:" | ||||
| msgstr "Hôte :" | ||||
| 
 | ||||
| #: ../vnc.html:220 | ||||
| #: ../vnc.html:270 | ||||
| msgid "Port:" | ||||
| msgstr "Port :" | ||||
| 
 | ||||
| #: ../vnc.html:224 | ||||
| #: ../vnc.html:274 | ||||
| msgid "Path:" | ||||
| msgstr "Chemin :" | ||||
| 
 | ||||
| #: ../vnc.html:231 | ||||
| msgid "Automatic Reconnect" | ||||
| msgstr "Reconnecter automatiquemen" | ||||
| #: ../vnc.html:283 | ||||
| msgid "Automatic reconnect" | ||||
| msgstr "Reconnecter automatiquement" | ||||
| 
 | ||||
| #: ../vnc.html:234 | ||||
| msgid "Reconnect Delay (ms):" | ||||
| #: ../vnc.html:288 | ||||
| msgid "Reconnect delay (ms):" | ||||
| msgstr "Délai de reconnexion (ms) :" | ||||
| 
 | ||||
| #: ../vnc.html:239 | ||||
| msgid "Show Dot when No Cursor" | ||||
| #: ../vnc.html:295 | ||||
| msgid "Show dot when no cursor" | ||||
| msgstr "Afficher le point lorsqu'il n'y a pas de curseur" | ||||
| 
 | ||||
| #: ../vnc.html:244 | ||||
| #: ../vnc.html:302 | ||||
| msgid "Logging:" | ||||
| msgstr "Se connecter :" | ||||
| 
 | ||||
| #: ../vnc.html:253 | ||||
| #: ../vnc.html:311 | ||||
| msgid "Version:" | ||||
| msgstr "Version :" | ||||
| 
 | ||||
| #: ../vnc.html:261 | ||||
| #: ../vnc.html:319 | ||||
| msgid "Disconnect" | ||||
| msgstr "Déconnecter" | ||||
| 
 | ||||
| #: ../vnc.html:280 | ||||
| #: ../vnc.html:342 | ||||
| msgid "Connect" | ||||
| msgstr "Connecter" | ||||
| 
 | ||||
| #: ../vnc.html:290 | ||||
| #: ../vnc.html:351 | ||||
| msgid "Server identity" | ||||
| msgstr "Identité du serveur" | ||||
| 
 | ||||
| #: ../vnc.html:354 | ||||
| msgid "The server has provided the following identifying information:" | ||||
| msgstr "Le serveur a fourni l'identification suivante :" | ||||
| 
 | ||||
| #: ../vnc.html:357 | ||||
| msgid "Fingerprint:" | ||||
| msgstr "Empreinte digitale :" | ||||
| 
 | ||||
| #: ../vnc.html:361 | ||||
| msgid "" | ||||
| "Please verify that the information is correct and press \"Approve\". " | ||||
| "Otherwise press \"Reject\"." | ||||
| msgstr "" | ||||
| "SVP, verifiez que l'information est correcte et pressez \"Accepter\". Sinon " | ||||
| "pressez \"Refuser\"." | ||||
| 
 | ||||
| #: ../vnc.html:366 | ||||
| msgid "Approve" | ||||
| msgstr "Accepter" | ||||
| 
 | ||||
| #: ../vnc.html:367 | ||||
| msgid "Reject" | ||||
| msgstr "Refuser" | ||||
| 
 | ||||
| #: ../vnc.html:375 | ||||
| msgid "Credentials" | ||||
| msgstr "Envoyer les identifiants" | ||||
| 
 | ||||
| #: ../vnc.html:379 | ||||
| msgid "Username:" | ||||
| msgstr "Nom d'utilisateur :" | ||||
| 
 | ||||
| #: ../vnc.html:294 | ||||
| #: ../vnc.html:383 | ||||
| msgid "Password:" | ||||
| msgstr "Mot de passe :" | ||||
| 
 | ||||
| #: ../vnc.html:298 | ||||
| msgid "Send Credentials" | ||||
| #: ../vnc.html:387 | ||||
| msgid "Send credentials" | ||||
| msgstr "Envoyer les identifiants" | ||||
| 
 | ||||
| #: ../vnc.html:308 | ||||
| #: ../vnc.html:396 | ||||
| msgid "Cancel" | ||||
| msgstr "Annuler" | ||||
| 
 | ||||
| #~ msgid "Must set host" | ||||
| #~ msgstr "Doit définir l'hôte" | ||||
| 
 | ||||
| #~ msgid "Clear" | ||||
| #~ msgstr "Effacer" | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| # Italian translations for noVNC | ||||
| # Traduzione italiana di noVNC | ||||
| # Copyright (C) 2022 The noVNC Authors | ||||
| # Copyright (C) 2022 The noVNC authors | ||||
| # This file is distributed under the same license as the noVNC package. | ||||
| # Fabio Fantoni <fabio.fantoni@m2r.biz>, 2022. | ||||
| # | ||||
| @ -84,7 +84,7 @@ msgid "Drag" | ||||
| msgstr "" | ||||
| 
 | ||||
| #: ../vnc.html:78 | ||||
| msgid "Move/Drag Viewport" | ||||
| msgid "Move/Drag viewport" | ||||
| msgstr "" | ||||
| 
 | ||||
| #: ../vnc.html:84 | ||||
| @ -92,7 +92,7 @@ msgid "Keyboard" | ||||
| msgstr "Tastiera" | ||||
| 
 | ||||
| #: ../vnc.html:84 | ||||
| msgid "Show Keyboard" | ||||
| msgid "Show keyboard" | ||||
| msgstr "Mostra tastiera" | ||||
| 
 | ||||
| #: ../vnc.html:89 | ||||
| @ -192,7 +192,7 @@ msgid "Settings" | ||||
| msgstr "Impostazioni" | ||||
| 
 | ||||
| #: ../vnc.html:162 | ||||
| msgid "Shared Mode" | ||||
| msgid "Shared mode" | ||||
| msgstr "Modalità condivisa" | ||||
| 
 | ||||
| #: ../vnc.html:165 | ||||
| @ -200,11 +200,11 @@ msgid "View Only" | ||||
| msgstr "Sola Visualizzazione" | ||||
| 
 | ||||
| #: ../vnc.html:169 | ||||
| msgid "Clip to Window" | ||||
| msgid "Clip to window" | ||||
| msgstr "" | ||||
| 
 | ||||
| #: ../vnc.html:172 | ||||
| msgid "Scaling Mode:" | ||||
| msgid "Scaling mode:" | ||||
| msgstr "Modalità di ridimensionamento:" | ||||
| 
 | ||||
| #: ../vnc.html:174 | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| # Portuguese translations for noVNC package. | ||||
| # Copyright (C) 2021 The noVNC Authors | ||||
| # Copyright (C) 2021 The noVNC authors | ||||
| # This file is distributed under the same license as the noVNC package. | ||||
| #  <liddack@outlook.com>, 2021. | ||||
| # | ||||
| @ -83,7 +83,7 @@ msgid "Drag" | ||||
| msgstr "Arrastar" | ||||
| 
 | ||||
| #: ../vnc.html:78 | ||||
| msgid "Move/Drag Viewport" | ||||
| msgid "Move/Drag viewport" | ||||
| msgstr "Mover/arrastar a janela" | ||||
| 
 | ||||
| #: ../vnc.html:84 | ||||
| @ -91,7 +91,7 @@ msgid "Keyboard" | ||||
| msgstr "Teclado" | ||||
| 
 | ||||
| #: ../vnc.html:84 | ||||
| msgid "Show Keyboard" | ||||
| msgid "Show keyboard" | ||||
| msgstr "Mostrar teclado" | ||||
| 
 | ||||
| #: ../vnc.html:89 | ||||
| @ -99,7 +99,7 @@ msgid "Extra keys" | ||||
| msgstr "Teclas adicionais" | ||||
| 
 | ||||
| #: ../vnc.html:89 | ||||
| msgid "Show Extra Keys" | ||||
| msgid "Show extra keys" | ||||
| msgstr "Mostar teclas adicionais" | ||||
| 
 | ||||
| #: ../vnc.html:94 | ||||
| @ -191,19 +191,19 @@ msgid "Settings" | ||||
| msgstr "Configurações" | ||||
| 
 | ||||
| #: ../vnc.html:162 | ||||
| msgid "Shared Mode" | ||||
| msgid "Shared mode" | ||||
| msgstr "Modo compartilhado" | ||||
| 
 | ||||
| #: ../vnc.html:165 | ||||
| msgid "View Only" | ||||
| msgid "View only" | ||||
| msgstr "Apenas visualizar" | ||||
| 
 | ||||
| #: ../vnc.html:169 | ||||
| msgid "Clip to Window" | ||||
| msgid "Clip to window" | ||||
| msgstr "Recortar à janela" | ||||
| 
 | ||||
| #: ../vnc.html:172 | ||||
| msgid "Scaling Mode:" | ||||
| msgid "Scaling mode:" | ||||
| msgstr "Modo de dimensionamento:" | ||||
| 
 | ||||
| #: ../vnc.html:174 | ||||
| @ -211,11 +211,11 @@ msgid "None" | ||||
| msgstr "Nenhum" | ||||
| 
 | ||||
| #: ../vnc.html:175 | ||||
| msgid "Local Scaling" | ||||
| msgid "Local scaling" | ||||
| msgstr "Local" | ||||
| 
 | ||||
| #: ../vnc.html:176 | ||||
| msgid "Remote Resizing" | ||||
| msgid "Remote resizing" | ||||
| msgstr "Remoto" | ||||
| 
 | ||||
| #: ../vnc.html:181 | ||||
| @ -255,15 +255,15 @@ msgid "Path:" | ||||
| msgstr "Caminho:" | ||||
| 
 | ||||
| #: ../vnc.html:218 | ||||
| msgid "Automatic Reconnect" | ||||
| msgid "Automatic reconnect" | ||||
| msgstr "Reconexão automática" | ||||
| 
 | ||||
| #: ../vnc.html:221 | ||||
| msgid "Reconnect Delay (ms):" | ||||
| msgid "Reconnect delay (ms):" | ||||
| msgstr "Atraso da reconexão (ms)" | ||||
| 
 | ||||
| #: ../vnc.html:226 | ||||
| msgid "Show Dot when No Cursor" | ||||
| msgid "Show dot when no cursor" | ||||
| msgstr "Mostrar ponto quando não há cursor" | ||||
| 
 | ||||
| #: ../vnc.html:231 | ||||
| @ -291,7 +291,7 @@ msgid "Password:" | ||||
| msgstr "Senha:" | ||||
| 
 | ||||
| #: ../vnc.html:285 | ||||
| msgid "Send Credentials" | ||||
| msgid "Send credentials" | ||||
| msgstr "Enviar credenciais" | ||||
| 
 | ||||
| #: ../vnc.html:295 | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| #!/usr/bin/env node
 | ||||
| 
 | ||||
| const path = require('path'); | ||||
| const program = require('commander'); | ||||
| const { program } = require('commander'); | ||||
| const fs = require('fs'); | ||||
| const fse = require('fs-extra'); | ||||
| const babel = require('@babel/core'); | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
| 
 | ||||
|     <!-- | ||||
|     noVNC example: simple example using default UI | ||||
|     Copyright (C) 2019 The noVNC Authors | ||||
|     Copyright (C) 2019 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). | ||||
| 
 | ||||
| @ -16,6 +16,7 @@ | ||||
|     <title>noVNC</title> | ||||
| 
 | ||||
|     <link rel="icon" type="image/x-icon" href="app/images/icons/novnc.ico"> | ||||
|     <meta name="theme-color" content="#313131"> | ||||
| 
 | ||||
|     <!-- Apple iOS Safari settings --> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | ||||
| @ -36,6 +37,7 @@ | ||||
|     <link rel="apple-touch-icon" sizes="180x180" type="image/png" href="app/images/icons/novnc-ios-180.png"> | ||||
| 
 | ||||
|     <!-- Stylesheets --> | ||||
|     <link rel="stylesheet" href="app/styles/constants.css"> | ||||
|     <link rel="stylesheet" href="app/styles/base.css"> | ||||
|     <link rel="stylesheet" href="app/styles/input.css"> | ||||
| 
 | ||||
| @ -45,7 +47,56 @@ | ||||
|     <link rel="preload" as="image" href="app/images/warning.png"> | ||||
| 
 | ||||
|     <script type="module" crossorigin="anonymous" src="app/error-handler.js"></script> | ||||
|     <script type="module" crossorigin="anonymous" src="app/ui.js"></script> | ||||
| 
 | ||||
|     <script type="module"> | ||||
|         import UI from "./app/ui.js"; | ||||
|         import * as Log from './core/util/logging.js'; | ||||
| 
 | ||||
|         let response; | ||||
| 
 | ||||
|         let defaults = {}; | ||||
|         let mandatory = {}; | ||||
| 
 | ||||
|         // Default settings will be loaded from defaults.json. Mandatory | ||||
|         // settings will be loaded from mandatory.json, which the user | ||||
|         // cannot change. | ||||
| 
 | ||||
|         try { | ||||
|             response = await fetch('./defaults.json'); | ||||
|             if (!response.ok) { | ||||
|                 throw Error("" + response.status + " " + response.statusText); | ||||
|             } | ||||
| 
 | ||||
|             defaults = await response.json(); | ||||
|         } catch (err) { | ||||
|             Log.Error("Couldn't fetch defaults.json: " + err); | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             response = await fetch('./mandatory.json'); | ||||
|             if (!response.ok) { | ||||
|                 throw Error("" + response.status + " " + response.statusText); | ||||
|             } | ||||
| 
 | ||||
|             mandatory = await response.json(); | ||||
|         } catch (err) { | ||||
|             Log.Error("Couldn't fetch mandatory.json: " + err); | ||||
|         } | ||||
| 
 | ||||
|         // You can also override any defaults you need here: | ||||
|         // | ||||
|         // defaults['host'] = 'vnc.example.com'; | ||||
| 
 | ||||
|         // Or force a specific setting, preventing the user from | ||||
|         // changing it: | ||||
|         // | ||||
|         // mandatory['view_only'] = true; | ||||
| 
 | ||||
|         // See docs/EMBEDDING.md for a list of possible settings. | ||||
| 
 | ||||
|         UI.start({ settings: { defaults: defaults, | ||||
|                                mandatory: mandatory } }); | ||||
|     </script> | ||||
| </head> | ||||
| 
 | ||||
| <body id="body"> | ||||
| @ -58,7 +109,7 @@ | ||||
|         </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- noVNC Control Bar --> | ||||
|     <!-- noVNC control bar --> | ||||
|     <div id="noVNC_control_bar_anchor" class="noVNC_vcenter"> | ||||
| 
 | ||||
|         <div id="noVNC_control_bar"> | ||||
| @ -73,18 +124,18 @@ | ||||
|             <!-- Drag/Pan the viewport --> | ||||
|             <input type="image" alt="Drag" src="app/images/drag.png" | ||||
|                 id="noVNC_view_drag_button" class="noVNC_button noVNC_hidden" | ||||
|                 title="Move/Drag Viewport"> | ||||
|                 title="Move/Drag viewport"> | ||||
| 
 | ||||
|             <!--noVNC Touch Device only buttons--> | ||||
|             <!--noVNC touch device only buttons--> | ||||
|             <div id="noVNC_mobile_buttons" style="display: none;"> | ||||
|                 <input type="image" alt="Keyboard" src="app/images/keyboard.png" | ||||
|                     id="noVNC_keyboard_button" class="noVNC_button" title="Show Keyboard"> | ||||
|                     id="noVNC_keyboard_button" class="noVNC_button" title="Show keyboard"> | ||||
|             </div> | ||||
| 
 | ||||
|             <!-- Extra manual keys --> | ||||
|             <input type="image" alt="Extra keys" src="app/images/toggleextrakeys.png" | ||||
|                 id="noVNC_toggle_extra_keys_button" class="noVNC_button" | ||||
|                 title="Show Extra Keys"> | ||||
|                 title="Show extra keys"> | ||||
|             <div class="noVNC_vcenter"> | ||||
|             <div id="noVNC_modifiers" class="noVNC_panel"> | ||||
|                 <input type="image" alt="Ctrl" src="app/images/ctrl.png" | ||||
| @ -148,9 +199,9 @@ | ||||
|             </div> | ||||
| 
 | ||||
|             <!-- Toggle fullscreen --> | ||||
|             <input type="image" alt="Full Screen" src="app/images/fullscreen.png" | ||||
|             <input type="image" alt="Full screen" src="app/images/fullscreen.png" | ||||
|                 id="noVNC_fullscreen_button" class="noVNC_button noVNC_hidden" | ||||
|                 title="Full Screen"> | ||||
|                 title="Full screen"> | ||||
| 
 | ||||
|             <!-- Settings --> | ||||
|             <span style="display: none;"> | ||||
| @ -256,9 +307,9 @@ | ||||
|                     </ul> | ||||
|                 </div> | ||||
|             </div> | ||||
|             </span> | ||||
|             </div> | ||||
| 
 | ||||
|             <!-- Connection Controls --> | ||||
|             <!-- Connection controls --> | ||||
|             <input style="display: none;" type="image" alt="Disconnect" src="app/images/disconnect.png" | ||||
|                 id="noVNC_disconnect_button" class="noVNC_button" | ||||
|                 title="Disconnect"> | ||||
| @ -273,7 +324,7 @@ | ||||
|         </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Status Dialog --> | ||||
|     <!-- Status dialog --> | ||||
|     <div id="noVNC_status"></div> | ||||
| 
 | ||||
|     <!-- Connect button --> | ||||
| @ -288,7 +339,7 @@ | ||||
|         </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Server Key Verification Dialog --> | ||||
|     <!-- Server key verification dialog --> | ||||
|     <div class="noVNC_center noVNC_connect_layer"> | ||||
|     <div id="noVNC_verify_server_dlg" class="noVNC_panel"><form> | ||||
|         <div class="noVNC_heading"> | ||||
| @ -298,21 +349,21 @@ | ||||
|             The server has provided the following identifying information: | ||||
|         </div> | ||||
|         <div id="noVNC_fingerprint_block"> | ||||
|             <b>Fingerprint:</b> | ||||
|             Fingerprint: | ||||
|             <span id="noVNC_fingerprint"></span> | ||||
|         </div> | ||||
|         <div> | ||||
|             Please verify that the information is correct and press | ||||
|             "Approve". Otherwise press "Reject". | ||||
|         </div> | ||||
|         <div> | ||||
|             <input id="noVNC_approve_server_button" type="submit" value="Approve" class="noVNC_submit"> | ||||
|             <input id="noVNC_reject_server_button" type="button" value="Reject" class="noVNC_submit"> | ||||
|         <div class="button_row"> | ||||
|             <input id="noVNC_approve_server_button" type="submit" value="Approve"> | ||||
|             <input id="noVNC_reject_server_button" type="button" value="Reject"> | ||||
|         </div> | ||||
|     </form></div> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Password Dialog --> | ||||
|     <!-- Password dialog --> | ||||
|     <div class="noVNC_center noVNC_connect_layer"> | ||||
|     <div id="noVNC_credentials_dlg" class="noVNC_panel"><form> | ||||
|         <div class="noVNC_heading"> | ||||
| @ -326,17 +377,17 @@ | ||||
|             <label for="noVNC_password_input">Password:</label> | ||||
|             <input id="noVNC_password_input" type="password"> | ||||
|         </div> | ||||
|         <div> | ||||
|             <input id="noVNC_credentials_button" type="submit" value="Send Credentials" class="noVNC_submit"> | ||||
|         <div class="button_row"> | ||||
|             <input id="noVNC_credentials_button" type="submit" value="Send credentials"> | ||||
|         </div> | ||||
|     </form></div> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Transition Screens --> | ||||
|     <!-- Transition screens --> | ||||
|     <div id="noVNC_transition"> | ||||
|         <div id="noVNC_transition_text"></div> | ||||
|         <div> | ||||
|         <input type="button" id="noVNC_cancel_reconnect_button" value="Cancel" class="noVNC_submit"> | ||||
|         <input type="button" id="noVNC_cancel_reconnect_button" value="Cancel"> | ||||
|         </div> | ||||
|         <div class="noVNC_spinner"></div> | ||||
|     </div> | ||||
|  | ||||
| @ -7,7 +7,7 @@ | ||||
| 
 | ||||
|     This is a self-contained file which doesn't import WebUtil or external CSS. | ||||
| 
 | ||||
|     Copyright (C) 2019 The noVNC Authors | ||||
|     Copyright (C) 2019 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). | ||||
| 
 | ||||
| @ -87,7 +87,7 @@ | ||||
|         // When this function is called, the server requires | ||||
|         // credentials to authenticate | ||||
|         function credentialsAreRequired(e) { | ||||
|             const password = prompt("Password Required:"); | ||||
|             const password = prompt("Password required:"); | ||||
|             rfb.sendCredentials({ password: password }); | ||||
|         } | ||||
| 
 | ||||
| @ -119,20 +119,14 @@ | ||||
|         // query string. If the variable isn't defined in the URL | ||||
|         // it returns the default value instead. | ||||
|         function readQueryVariable(name, defaultValue) { | ||||
|             // A URL with a query parameter can look like this (But will most probably get logged on the http server): | ||||
|             // A URL with a query parameter can look like this: | ||||
|             // 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 | ||||
|             // | ||||
|             // Note that we use location.href instead of location.search | ||||
|             // because Firefox < 53 has a bug w.r.t location.search | ||||
|             const re = new RegExp('.*[?&]' + name + '=([^&#]*)'), | ||||
|                   match = ''.concat(document.location.href, window.location.hash).match(re); | ||||
|                   match = document.location.href.match(re); | ||||
| 
 | ||||
|             if (typeof defaultValue === 'undefined') { defaultValue = null; } | ||||
| 
 | ||||
|             if (match) { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user