Update noVNC to 1.4.0

Anton Moroz committed Jul 5, 2023 at 13:20 UTC fefce68687f18f64e034d55a10c3dfc2434d98d0
48 files changed +2539 -498
public/novnc/LICENSE.txt
+1 -1
@@ -1,4 +1,4 @@
1 -noVNC is Copyright (C) 2019 The noVNC Authors
1 +noVNC is Copyright (C) 2022 The noVNC Authors
2 (./AUTHORS)
3
4 The noVNC core library files are licensed under the MPL 2.0 (Mozilla
public/novnc/app/error-handler.js
+60 -47
@@ -6,61 +6,74 @@
6 * See README.md for usage and integration instructions.
7 */
8
9 -// NB: this should *not* be included as a module until we have
10 -// native support in the browsers, so that our error handler
11 -// can catch script-loading errors.
9 +// Fallback for all uncought errors
10 +function handleError(event, err) {
11 + try {
12 + const msg = document.getElementById('noVNC_fallback_errormsg');
13
13 -// No ES6 can be used in this file since it's used for the translation
14 -/* eslint-disable prefer-arrow-callback */
14 + // Work around Firefox bug:
15 + // https://bugzilla.mozilla.org/show_bug.cgi?id=1685038
16 + if (event.message === "ResizeObserver loop completed with undelivered notifications.") {
17 + return false;
18 + }
19
16 -(function _scope() {
17 - "use strict";
20 + // Only show the initial error
21 + if (msg.hasChildNodes()) {
22 + return false;
23 + }
24
19 - // Fallback for all uncought errors
20 - function handleError(event, err) {
21 - try {
22 - const msg = document.getElementById('noVNC_fallback_errormsg');
25 + let div = document.createElement("div");
26 + div.classList.add('noVNC_message');
27 + div.appendChild(document.createTextNode(event.message));
28 + msg.appendChild(div);
29
24 - // Only show the initial error
25 - if (msg.hasChildNodes()) {
26 - return false;
30 + if (event.filename) {
31 + div = document.createElement("div");
32 + div.className = 'noVNC_location';
33 + let text = event.filename;
34 + if (event.lineno !== undefined) {
35 + text += ":" + event.lineno;
36 + if (event.colno !== undefined) {
37 + text += ":" + event.colno;
38 + }
39 }
40 + div.appendChild(document.createTextNode(text));
41 + msg.appendChild(div);
42 + }
43
29 - let div = document.createElement("div");
30 - div.classList.add('noVNC_message');
31 - div.appendChild(document.createTextNode(event.message));
44 + if (err && err.stack) {
45 + div = document.createElement("div");
46 + div.className = 'noVNC_stack';
47 + div.appendChild(document.createTextNode(err.stack));
48 msg.appendChild(div);
49 + }
50
34 - if (event.filename) {
35 - div = document.createElement("div");
36 - div.className = 'noVNC_location';
37 - let text = event.filename;
38 - if (event.lineno !== undefined) {
39 - text += ":" + event.lineno;
40 - if (event.colno !== undefined) {
41 - text += ":" + event.colno;
42 - }
43 - }
44 - div.appendChild(document.createTextNode(text));
45 - msg.appendChild(div);
46 - }
51 + document.getElementById('noVNC_fallback_error')
52 + .classList.add("noVNC_open");
53
48 - if (err && err.stack) {
49 - div = document.createElement("div");
50 - div.className = 'noVNC_stack';
51 - div.appendChild(document.createTextNode(err.stack));
52 - msg.appendChild(div);
53 - }
54 + } catch (exc) {
55 + document.write("noVNC encountered an error.");
56 + }
57
55 - document.getElementById('noVNC_fallback_error')
56 - .classList.add("noVNC_open");
57 - } catch (exc) {
58 - document.write("noVNC encountered an error.");
59 - }
60 - // Don't return true since this would prevent the error
61 - // from being printed to the browser console.
62 - return false;
58 + // Try to disable keyboard interaction, best effort
59 + try {
60 + // Remove focus from the currently focused element in order to
61 + // prevent keyboard interaction from continuing
62 + if (document.activeElement) { document.activeElement.blur(); }
63 +
64 + // Don't let any element be focusable when showing the error
65 + let keyboardFocusable = 'a[href], button, input, textarea, select, details, [tabindex]';
66 + document.querySelectorAll(keyboardFocusable).forEach((elem) => {
67 + elem.setAttribute("tabindex", "-1");
68 + });
69 + } catch (exc) {
70 + // Do nothing
71 }
64 - window.addEventListener('error', function onerror(evt) { handleError(evt, evt.error); });
65 - window.addEventListener('unhandledrejection', function onreject(evt) { handleError(evt.reason, evt.reason); });
66 -})();
72 +
73 + // Don't return true since this would prevent the error
74 + // from being printed to the browser console.
75 + return false;
76 +}
77 +
78 +window.addEventListener('error', evt => handleError(evt, evt.error));
79 +window.addEventListener('unhandledrejection', evt => handleError(evt.reason, evt.reason));
public/novnc/app/images/icons/Makefile
+34 -34
@@ -1,42 +1,42 @@
1 -ICONS := \
2 - novnc-16x16.png \
3 - novnc-24x24.png \
4 - novnc-32x32.png \
5 - novnc-48x48.png \
6 - novnc-64x64.png
7 -
8 -ANDROID_LAUNCHER := \
9 - novnc-48x48.png \
10 - novnc-72x72.png \
11 - novnc-96x96.png \
12 - novnc-144x144.png \
13 - novnc-192x192.png
14 -
15 -IPHONE_LAUNCHER := \
16 - novnc-60x60.png \
17 - novnc-120x120.png
18 -
19 -IPAD_LAUNCHER := \
20 - novnc-76x76.png \
21 - novnc-152x152.png
22 -
23 -ALL_ICONS := $(ICONS) $(ANDROID_LAUNCHER) $(IPHONE_LAUNCHER) $(IPAD_LAUNCHER)
1 +BROWSER_SIZES := 16 24 32 48 64
2 +#ANDROID_SIZES := 72 96 144 192
3 +# FIXME: The ICO is limited to 8 icons due to a Chrome bug:
4 +# https://bugs.chromium.org/p/chromium/issues/detail?id=1381393
5 +ANDROID_SIZES := 96 144 192
6 +WEB_ICON_SIZES := $(BROWSER_SIZES) $(ANDROID_SIZES)
7 +
8 +#IOS_1X_SIZES := 20 29 40 76 # No such devices exist anymore
9 +IOS_2X_SIZES := 40 58 80 120 152 167
10 +IOS_3X_SIZES := 60 87 120 180
11 +ALL_IOS_SIZES := $(IOS_1X_SIZES) $(IOS_2X_SIZES) $(IOS_3X_SIZES)
12 +
13 +ALL_ICONS := \
14 + $(ALL_IOS_SIZES:%=novnc-ios-%.png) \
15 + novnc.ico
16
17 all: $(ALL_ICONS)
18
27 -novnc-16x16.png: novnc-icon-sm.svg
28 - convert -density 90 \
29 - -background transparent "$<" "$@"
30 -novnc-24x24.png: novnc-icon-sm.svg
31 - convert -density 135 \
32 - -background transparent "$<" "$@"
33 -novnc-32x32.png: novnc-icon-sm.svg
34 - convert -density 180 \
35 - -background transparent "$<" "$@"
19 +# Our testing shows that the ICO file need to be sorted in largest to
20 +# smallest to get the apporpriate behviour
21 +WEB_ICON_SIZES_REVERSE := $(shell echo $(WEB_ICON_SIZES) | tr ' ' '\n' | sort -nr | tr '\n' ' ')
22 +WEB_BASE_ICONS := $(WEB_ICON_SIZES_REVERSE:%=novnc-%.png)
23 +.INTERMEDIATE: $(WEB_BASE_ICONS)
24
25 +novnc.ico: $(WEB_BASE_ICONS)
26 + convert $(WEB_BASE_ICONS) "$@"
27 +
28 +# General conversion
29 novnc-%.png: novnc-icon.svg
38 - convert -density $$[`echo $* | cut -d x -f 1` * 90 / 48] \
39 - -background transparent "$<" "$@"
30 + convert -depth 8 -background transparent \
31 + -size $*x$* "$(lastword $^)" "$@"
32 +
33 +# iOS icons use their own SVG
34 +novnc-ios-%.png: novnc-ios-icon.svg
35 + convert -depth 8 -background transparent \
36 + -size $*x$* "$(lastword $^)" "$@"
37 +
38 +# The smallest sizes are generated using a different SVG
39 +novnc-16.png novnc-24.png novnc-32.png: novnc-icon-sm.svg
40
41 clean:
42 rm -f *.png
public/novnc/app/images/icons/novnc-120x120.png
Binary files a/public/novnc/app/images/icons/novnc-120x120.png and /dev/null differ
public/novnc/app/images/icons/novnc-144x144.png
Binary files a/public/novnc/app/images/icons/novnc-144x144.png and /dev/null differ
public/novnc/app/images/icons/novnc-152x152.png
Binary files a/public/novnc/app/images/icons/novnc-152x152.png and /dev/null differ
public/novnc/app/images/icons/novnc-16x16.png
Binary files a/public/novnc/app/images/icons/novnc-16x16.png and /dev/null differ
public/novnc/app/images/icons/novnc-192x192.png
Binary files a/public/novnc/app/images/icons/novnc-192x192.png and /dev/null differ
public/novnc/app/images/icons/novnc-24x24.png
Binary files a/public/novnc/app/images/icons/novnc-24x24.png and /dev/null differ
public/novnc/app/images/icons/novnc-32x32.png
Binary files a/public/novnc/app/images/icons/novnc-32x32.png and /dev/null differ
public/novnc/app/images/icons/novnc-48x48.png
Binary files a/public/novnc/app/images/icons/novnc-48x48.png and /dev/null differ
public/novnc/app/images/icons/novnc-60x60.png
Binary files a/public/novnc/app/images/icons/novnc-60x60.png and /dev/null differ
public/novnc/app/images/icons/novnc-64x64.png
Binary files a/public/novnc/app/images/icons/novnc-64x64.png and /dev/null differ
public/novnc/app/images/icons/novnc-72x72.png
Binary files a/public/novnc/app/images/icons/novnc-72x72.png and /dev/null differ
public/novnc/app/images/icons/novnc-76x76.png
Binary files a/public/novnc/app/images/icons/novnc-76x76.png and /dev/null differ
public/novnc/app/images/icons/novnc-96x96.png
Binary files a/public/novnc/app/images/icons/novnc-96x96.png and /dev/null differ
public/novnc/app/images/icons/novnc-ios-120.png
Binary files /dev/null and b/public/novnc/app/images/icons/novnc-ios-120.png differ
public/novnc/app/images/icons/novnc-ios-152.png
Binary files /dev/null and b/public/novnc/app/images/icons/novnc-ios-152.png differ
public/novnc/app/images/icons/novnc-ios-167.png
Binary files /dev/null and b/public/novnc/app/images/icons/novnc-ios-167.png differ
public/novnc/app/images/icons/novnc-ios-180.png
Binary files /dev/null and b/public/novnc/app/images/icons/novnc-ios-180.png differ
public/novnc/app/images/icons/novnc-ios-40.png
Binary files /dev/null and b/public/novnc/app/images/icons/novnc-ios-40.png differ
public/novnc/app/images/icons/novnc-ios-58.png
Binary files /dev/null and b/public/novnc/app/images/icons/novnc-ios-58.png differ
public/novnc/app/images/icons/novnc-ios-60.png
Binary files /dev/null and b/public/novnc/app/images/icons/novnc-ios-60.png differ
public/novnc/app/images/icons/novnc-ios-80.png
Binary files /dev/null and b/public/novnc/app/images/icons/novnc-ios-80.png differ
public/novnc/app/images/icons/novnc-ios-87.png
Binary files /dev/null and b/public/novnc/app/images/icons/novnc-ios-87.png differ
public/novnc/app/images/icons/novnc-ios-icon.svg new
+183
@@ -0,0 +1,183 @@
1 +<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2 +<!-- Created with Inkscape (http://www.inkscape.org/) -->
3 +
4 +<svg
5 + width="48"
6 + height="48"
7 + viewBox="0 0 48 48.000001"
8 + id="svg2"
9 + version="1.1"
10 + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
11 + sodipodi:docname="novnc-ios-icon.svg"
12 + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
13 + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
14 + xmlns="http://www.w3.org/2000/svg"
15 + xmlns:svg="http://www.w3.org/2000/svg"
16 + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
17 + xmlns:cc="http://creativecommons.org/ns#"
18 + xmlns:dc="http://purl.org/dc/elements/1.1/">
19 + <defs
20 + id="defs4" />
21 + <sodipodi:namedview
22 + id="base"
23 + pagecolor="#ffffff"
24 + bordercolor="#666666"
25 + borderopacity="1.0"
26 + inkscape:pageopacity="0.0"
27 + inkscape:pageshadow="2"
28 + inkscape:zoom="11.313708"
29 + inkscape:cx="27.356195"
30 + inkscape:cy="17.810253"
31 + inkscape:document-units="px"
32 + inkscape:current-layer="layer1"
33 + showgrid="false"
34 + units="px"
35 + inkscape:object-nodes="true"
36 + inkscape:snap-smooth-nodes="true"
37 + inkscape:snap-midpoints="true"
38 + inkscape:window-width="2560"
39 + inkscape:window-height="1371"
40 + inkscape:window-x="0"
41 + inkscape:window-y="0"
42 + inkscape:window-maximized="1"
43 + inkscape:showpageshadow="2"
44 + inkscape:pagecheckerboard="0"
45 + inkscape:deskcolor="#d1d1d1">
46 + <inkscape:grid
47 + type="xygrid"
48 + id="grid4169" />
49 + </sodipodi:namedview>
50 + <metadata
51 + id="metadata7">
52 + <rdf:RDF>
53 + <cc:Work
54 + rdf:about="">
55 + <dc:format>image/svg+xml</dc:format>
56 + <dc:type
57 + rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
58 + </cc:Work>
59 + </rdf:RDF>
60 + </metadata>
61 + <g
62 + inkscape:label="Layer 1"
63 + inkscape:groupmode="layer"
64 + id="layer1"
65 + transform="translate(0,-1004.3621)">
66 + <rect
67 + style="opacity:1;fill:#494949;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
68 + id="rect4167"
69 + width="48"
70 + height="48"
71 + x="0"
72 + y="1004.3621"
73 + inkscape:label="background" />
74 + <path
75 + style="opacity:1;fill:#313131;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
76 + d="m 0,1004.3621 v 48 h 20 c 15.512,0 28,-16.948 28,-38 v -10 z"
77 + id="rect4173"
78 + inkscape:connector-curvature="0"
79 + sodipodi:nodetypes="cccccc"
80 + inkscape:label="darker_grey_plate" />
81 + <g
82 + id="g4300"
83 + style="display:inline;fill:#000000;fill-opacity:1;stroke:none"
84 + transform="translate(0.5,0.5)"
85 + inkscape:label="shadows">
86 + <g
87 + id="g4302"
88 + style="fill:#000000;fill-opacity:1;stroke:none"
89 + inkscape:label="no">
90 + <path
91 + sodipodi:nodetypes="scsccsssscccs"
92 + d="m 11.986926,1016.3621 c 0.554325,0 1.025987,0.2121 1.414987,0.6362 0.398725,0.4138 0.600909,0.9155 0.598087,1.5052 v 6.8586 h -2 v -6.8914 c 0,-0.072 -0.03404,-0.1086 -0.102113,-0.1086 H 7.1021125 C 7.0340375,1018.3621 7,1018.3983 7,1018.4707 v 6.8914 H 5 v -9 z"
93 + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
94 + id="path4304"
95 + inkscape:connector-curvature="0"
96 + inkscape:label="n" />
97 + <path
98 + sodipodi:nodetypes="sscsscsscsscssssssssss"
99 + d="m 17.013073,1016.3621 h 4.973854 c 0.554325,0 1.025987,0.2121 1.414986,0.6362 0.398725,0.4138 0.598087,0.9155 0.598087,1.5052 v 4.7172 c 0,0.5897 -0.199362,1.0966 -0.598087,1.5207 -0.388999,0.4138 -0.860661,0.6207 -1.414986,0.6207 h -4.973854 c -0.554325,0 -1.030849,-0.2069 -1.429574,-0.6207 C 15.1945,1024.3173 15,1023.8104 15,1023.2207 v -4.7172 c 0,-0.5897 0.1945,-1.0914 0.583499,-1.5052 0.398725,-0.4241 0.875249,-0.6362 1.429574,-0.6362 z m 4.884815,2 h -4.795776 c -0.06808,0 -0.102112,0.036 -0.102112,0.1086 v 4.7828 c 0,0.072 0.03404,0.1086 0.102112,0.1086 h 4.795776 c 0.06807,0 0.102112,-0.036 0.102112,-0.1086 v -4.7828 c 0,-0.072 -0.03404,-0.1086 -0.102112,-0.1086 z"
100 + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
101 + id="path4306"
102 + inkscape:connector-curvature="0"
103 + inkscape:label="o" />
104 + </g>
105 + <g
106 + id="g4308"
107 + style="fill:#000000;fill-opacity:1;stroke:none"
108 + inkscape:label="VNC">
109 + <path
110 + sodipodi:nodetypes="cccccccc"
111 + d="m 12,1036.9177 4.768114,-8.5556 H 19 l -6,11 h -2 l -6,-11 h 2.2318854 z"
112 + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
113 + id="path4310"
114 + inkscape:connector-curvature="0"
115 + inkscape:label="V" />
116 + <path
117 + sodipodi:nodetypes="ccccccccccc"
118 + d="m 29,1036.3621 v -8 h 2 v 11 h -2 l -7,-8 v 8 h -2 v -11 h 2 z"
119 + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
120 + id="path4312"
121 + inkscape:connector-curvature="0"
122 + inkscape:label="N" />
123 + <path
124 + sodipodi:nodetypes="cssssccscsscscc"
125 + d="m 43,1030.3621 h -8.897887 c -0.06808,0 -0.102113,0.036 -0.102113,0.1069 v 6.7862 c 0,0.071 0.03404,0.1069 0.102113,0.1069 H 43 v 2 h -8.972339 c -0.56405,0 -1.045437,-0.2037 -1.444162,-0.6111 C 32.1945,1038.3334 32,1037.8292 32,1037.2385 v -6.7528 c 0,-0.5907 0.1945,-1.0898 0.583499,-1.4972 0.398725,-0.4176 0.880112,-0.6264 1.444162,-0.6264 H 43 Z"
126 + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
127 + id="path4314"
128 + inkscape:connector-curvature="0"
129 + inkscape:label="C" />
130 + </g>
131 + </g>
132 + <g
133 + id="g4291"
134 + style="stroke:none"
135 + inkscape:label="noVNC">
136 + <g
137 + id="g4282"
138 + style="stroke:none"
139 + inkscape:label="no">
140 + <path
141 + inkscape:connector-curvature="0"
142 + id="path4143"
143 + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
144 + d="m 11.986926,1016.3621 c 0.554325,0 1.025987,0.2121 1.414987,0.6362 0.398725,0.4138 0.600909,0.9155 0.598087,1.5052 l 0,6.8586 -2,0 0,-6.8914 c 0,-0.072 -0.03404,-0.1086 -0.102113,-0.1086 l -4.7957745,0 C 7.0340375,1018.3621 7,1018.3983 7,1018.4707 l 0,6.8914 -2,0 0,-9 z"
145 + sodipodi:nodetypes="scsccsssscccs"
146 + inkscape:label="n" />
147 + <path
148 + inkscape:connector-curvature="0"
149 + id="path4145"
150 + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#008000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
151 + d="m 17.013073,1016.3621 4.973854,0 c 0.554325,0 1.025987,0.2121 1.414986,0.6362 0.398725,0.4138 0.598087,0.9155 0.598087,1.5052 l 0,4.7172 c 0,0.5897 -0.199362,1.0966 -0.598087,1.5207 -0.388999,0.4138 -0.860661,0.6207 -1.414986,0.6207 l -4.973854,0 c -0.554325,0 -1.030849,-0.2069 -1.429574,-0.6207 C 15.1945,1024.3173 15,1023.8104 15,1023.2207 l 0,-4.7172 c 0,-0.5897 0.1945,-1.0914 0.583499,-1.5052 0.398725,-0.4241 0.875249,-0.6362 1.429574,-0.6362 z m 4.884815,2 -4.795776,0 c -0.06808,0 -0.102112,0.036 -0.102112,0.1086 l 0,4.7828 c 0,0.072 0.03404,0.1086 0.102112,0.1086 l 4.795776,0 c 0.06807,0 0.102112,-0.036 0.102112,-0.1086 l 0,-4.7828 c 0,-0.072 -0.03404,-0.1086 -0.102112,-0.1086 z"
152 + sodipodi:nodetypes="sscsscsscsscssssssssss"
153 + inkscape:label="o" />
154 + </g>
155 + <g
156 + id="g4286"
157 + style="stroke:none"
158 + inkscape:label="VNC">
159 + <path
160 + inkscape:connector-curvature="0"
161 + id="path4147"
162 + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
163 + d="m 12,1036.9177 4.768114,-8.5556 2.231886,0 -6,11 -2,0 -6,-11 2.2318854,0 z"
164 + sodipodi:nodetypes="cccccccc"
165 + inkscape:label="V" />
166 + <path
167 + inkscape:connector-curvature="0"
168 + id="path4149"
169 + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
170 + d="m 29,1036.3621 0,-8 2,0 0,11 -2,0 -7,-8 0,8 -2,0 0,-11 2,0 z"
171 + sodipodi:nodetypes="ccccccccccc"
172 + inkscape:label="N" />
173 + <path
174 + inkscape:connector-curvature="0"
175 + id="path4151"
176 + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:medium;line-height:125%;font-family:Orbitron;-inkscape-font-specification:'Orbitron Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffff00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
177 + d="m 43,1030.3621 -8.897887,0 c -0.06808,0 -0.102113,0.036 -0.102113,0.1069 l 0,6.7862 c 0,0.071 0.03404,0.1069 0.102113,0.1069 l 8.897887,0 0,2 -8.972339,0 c -0.56405,0 -1.045437,-0.2037 -1.444162,-0.6111 C 32.1945,1038.3334 32,1037.8292 32,1037.2385 l 0,-6.7528 c 0,-0.5907 0.1945,-1.0898 0.583499,-1.4972 0.398725,-0.4176 0.880112,-0.6264 1.444162,-0.6264 l 8.972339,0 z"
178 + sodipodi:nodetypes="cssssccscsscscc"
179 + inkscape:label="C" />
180 + </g>
181 + </g>
182 + </g>
183 +</svg>
public/novnc/app/images/icons/novnc.ico
Binary files /dev/null and b/public/novnc/app/images/icons/novnc.ico differ
public/novnc/app/locale/fr.json
+27 -21
@@ -1,21 +1,22 @@
1 {
2 + "HTTPS is required for full functionality": "",
3 "Connecting...": "En cours de connexion...",
4 "Disconnecting...": "Déconnexion en cours...",
5 "Reconnecting...": "Reconnexion en cours...",
6 "Internal error": "Erreur interne",
7 "Must set host": "Doit définir l'hôte",
7 - "Connected (encrypted) to ": "Connecté (crypté) à ",
8 - "Connected (unencrypted) to ": "Connecté (non crypté) à ",
9 - "Something went wrong, connection is closed": "Quelque chose est arrivé, la connexion est fermée",
8 + "Connected (encrypted) to ": "Connecté (chiffré) à ",
9 + "Connected (unencrypted) to ": "Connecté (non chiffré) à ",
10 + "Something went wrong, connection is closed": "Quelque chose s'est mal passé, la connexion a été fermée",
11 "Failed to connect to server": "Échec de connexion au serveur",
12 "Disconnected": "Déconnecté",
12 - "New connection has been rejected with reason: ": "Une nouvelle connexion a été rejetée avec raison: ",
13 + "New connection has been rejected with reason: ": "Une nouvelle connexion a été rejetée avec motif : ",
14 "New connection has been rejected": "Une nouvelle connexion a été rejetée",
15 "Credentials are required": "Les identifiants sont requis",
15 - "noVNC encountered an error:": "noVNC a rencontré une erreur:",
16 + "noVNC encountered an error:": "noVNC a rencontré une erreur :",
17 "Hide/Show the control bar": "Masquer/Afficher la barre de contrôle",
18 "Drag": "Faire glisser",
18 - "Move/Drag Viewport": "Déplacer/faire glisser Viewport",
19 + "Move/Drag Viewport": "Déplacer/faire glisser le Viewport",
20 "Keyboard": "Clavier",
21 "Show Keyboard": "Afficher le clavier",
22 "Extra keys": "Touches supplémentaires",
@@ -39,34 +40,39 @@
40 "Reboot": "Redémarrer",
41 "Reset": "Réinitialiser",
42 "Clipboard": "Presse-papiers",
42 - "Clear": "Effacer",
43 - "Fullscreen": "Plein écran",
43 + "Edit clipboard content in the textarea below.": "",
44 "Settings": "Paramètres",
45 "Shared Mode": "Mode partagé",
46 "View Only": "Afficher uniquement",
47 "Clip to Window": "Clip à fenêtre",
48 - "Scaling Mode:": "Mode mise à l'échelle:",
48 + "Scaling Mode:": "Mode mise à l'échelle :",
49 "None": "Aucun",
50 "Local Scaling": "Mise à l'échelle locale",
51 "Remote Resizing": "Redimensionnement à distance",
52 "Advanced": "Avancé",
53 - "Quality:": "Qualité:",
54 - "Compression level:": "Niveau de compression:",
55 - "Repeater ID:": "ID Répéteur:",
53 + "Quality:": "Qualité :",
54 + "Compression level:": "Niveau de compression :",
55 + "Repeater ID:": "ID Répéteur :",
56 "WebSocket": "WebSocket",
57 - "Encrypt": "Crypter",
58 - "Host:": "Hôte:",
59 - "Port:": "Port:",
60 - "Path:": "Chemin:",
57 + "Encrypt": "Chiffrer",
58 + "Host:": "Hôte :",
59 + "Port:": "Port :",
60 + "Path:": "Chemin :",
61 "Automatic Reconnect": "Reconnecter automatiquemen",
62 - "Reconnect Delay (ms):": "Délai de reconnexion (ms):",
62 + "Reconnect Delay (ms):": "Délai de reconnexion (ms) :",
63 "Show Dot when No Cursor": "Afficher le point lorsqu'il n'y a pas de curseur",
64 - "Logging:": "Se connecter:",
65 - "Version:": "Version:",
64 + "Logging:": "Se connecter :",
65 + "Version:": "Version :",
66 "Disconnect": "Déconnecter",
67 "Connect": "Connecter",
68 - "Username:": "Nom d'utilisateur:",
69 - "Password:": "Mot de passe:",
68 + "Server identity": "",
69 + "The server has provided the following identifying information:": "",
70 + "Fingerprint:": "",
71 + "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "",
72 + "Approve": "",
73 + "Reject": "",
74 + "Username:": "Nom d'utilisateur :",
75 + "Password:": "Mot de passe :",
76 "Send Credentials": "Envoyer les identifiants",
77 "Cancel": "Annuler"
78 }
\ No newline at end of file
public/novnc/app/locale/it.json new
+72
@@ -0,0 +1,72 @@
1 +{
2 + "Connecting...": "Connessione in corso...",
3 + "Disconnecting...": "Disconnessione...",
4 + "Reconnecting...": "Riconnessione...",
5 + "Internal error": "Errore interno",
6 + "Must set host": "Devi impostare l'host",
7 + "Connected (encrypted) to ": "Connesso (crittografato) a ",
8 + "Connected (unencrypted) to ": "Connesso (non crittografato) a",
9 + "Something went wrong, connection is closed": "Qualcosa è andato storto, la connessione è stata chiusa",
10 + "Failed to connect to server": "Impossibile connettersi al server",
11 + "Disconnected": "Disconnesso",
12 + "New connection has been rejected with reason: ": "La nuova connessione è stata rifiutata con motivo: ",
13 + "New connection has been rejected": "La nuova connessione è stata rifiutata",
14 + "Credentials are required": "Le credenziali sono obbligatorie",
15 + "noVNC encountered an error:": "noVNC ha riscontrato un errore:",
16 + "Hide/Show the control bar": "Nascondi/Mostra la barra di controllo",
17 + "Drag": "",
18 + "Move/Drag Viewport": "",
19 + "Keyboard": "Tastiera",
20 + "Show Keyboard": "Mostra tastiera",
21 + "Extra keys": "Tasti Aggiuntivi",
22 + "Show Extra Keys": "Mostra Tasti Aggiuntivi",
23 + "Ctrl": "Ctrl",
24 + "Toggle Ctrl": "Tieni premuto Ctrl",
25 + "Alt": "Alt",
26 + "Toggle Alt": "Tieni premuto Alt",
27 + "Toggle Windows": "Tieni premuto Windows",
28 + "Windows": "Windows",
29 + "Send Tab": "Invia Tab",
30 + "Tab": "Tab",
31 + "Esc": "Esc",
32 + "Send Escape": "Invia Esc",
33 + "Ctrl+Alt+Del": "Ctrl+Alt+Canc",
34 + "Send Ctrl-Alt-Del": "Invia Ctrl-Alt-Canc",
35 + "Shutdown/Reboot": "Spegnimento/Riavvio",
36 + "Shutdown/Reboot...": "Spegnimento/Riavvio...",
37 + "Power": "Alimentazione",
38 + "Shutdown": "Spegnimento",
39 + "Reboot": "Riavvio",
40 + "Reset": "Reset",
41 + "Clipboard": "Clipboard",
42 + "Clear": "Pulisci",
43 + "Fullscreen": "Schermo intero",
44 + "Settings": "Impostazioni",
45 + "Shared Mode": "Modalità condivisa",
46 + "View Only": "Sola Visualizzazione",
47 + "Clip to Window": "",
48 + "Scaling Mode:": "Modalità di ridimensionamento:",
49 + "None": "Nessuna",
50 + "Local Scaling": "Ridimensionamento Locale",
51 + "Remote Resizing": "Ridimensionamento Remoto",
52 + "Advanced": "Avanzate",
53 + "Quality:": "Qualità:",
54 + "Compression level:": "Livello Compressione:",
55 + "Repeater ID:": "ID Ripetitore:",
56 + "WebSocket": "WebSocket",
57 + "Encrypt": "Crittografa",
58 + "Host:": "Host:",
59 + "Port:": "Porta:",
60 + "Path:": "Percorso:",
61 + "Automatic Reconnect": "Riconnessione Automatica",
62 + "Reconnect Delay (ms):": "Ritardo Riconnessione (ms):",
63 + "Show Dot when No Cursor": "Mostra Punto quando Nessun Cursore",
64 + "Logging:": "",
65 + "Version:": "Versione:",
66 + "Disconnect": "Disconnetti",
67 + "Connect": "Connetti",
68 + "Username:": "Utente:",
69 + "Password:": "Password:",
70 + "Send Credentials": "Invia Credenziale",
71 + "Cancel": "Annulla"
72 +}
\ No newline at end of file
public/novnc/app/locale/sv.json
+10 -2
@@ -1,4 +1,5 @@
1 {
2 + "HTTPS is required for full functionality": "HTTPS krävs för full funktionalitet",
3 "Connecting...": "Ansluter...",
4 "Disconnecting...": "Kopplar ner...",
5 "Reconnecting...": "Återansluter...",
@@ -39,8 +40,8 @@
40 "Reboot": "Boota om",
41 "Reset": "Återställ",
42 "Clipboard": "Urklipp",
42 - "Clear": "Rensa",
43 - "Fullscreen": "Fullskärm",
43 + "Edit clipboard content in the textarea below.": "Redigera urklippets innehåll i fältet nedan.",
44 + "Full Screen": "Fullskärm",
45 "Settings": "Inställningar",
46 "Shared Mode": "Delat Läge",
47 "View Only": "Endast Visning",
@@ -65,6 +66,13 @@
66 "Version:": "Version:",
67 "Disconnect": "Koppla från",
68 "Connect": "Anslut",
69 + "Server identity": "Server-identitet",
70 + "The server has provided the following identifying information:": "Servern har gett följande identifierande information:",
71 + "Fingerprint:": "Fingeravtryck:",
72 + "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Kontrollera att informationen är korrekt och tryck sedan \"Godkänn\". Tryck annars \"Neka\".",
73 + "Approve": "Godkänn",
74 + "Reject": "Neka",
75 + "Credentials": "Användaruppgifter",
76 "Username:": "Användarnamn:",
77 "Password:": "Lösenord:",
78 "Send Credentials": "Skicka Användaruppgifter",
public/novnc/app/localization.js
+9 -2
@@ -103,13 +103,20 @@ export class Localizer {
103 return items.indexOf(searchElement) !== -1;
104 }
105
106 + function translateString(str) {
107 + // We assume surrounding whitespace, and whitespace around line
108 + // breaks is just for source formatting
109 + str = str.split("\n").map(s => s.trim()).join(" ").trim();
110 + return self.get(str);
111 + }
112 +
113 function translateAttribute(elem, attr) {
107 - const str = self.get(elem.getAttribute(attr));
114 + const str = translateString(elem.getAttribute(attr));
115 elem.setAttribute(attr, str);
116 }
117
118 function translateTextNode(node) {
112 - const str = self.get(node.data.trim());
119 + const str = translateString(node.data);
120 node.data = str;
121 }
122
public/novnc/app/styles/base.css
+166 -214
@@ -19,10 +19,23 @@
19 * 10000: Max (used for polyfills)
20 */
21
22 +/*
23 + * State variables (set on :root):
24 + *
25 + * noVNC_loading: Page is still loading
26 + * noVNC_connecting: Connecting to server
27 + * noVNC_reconnecting: Re-establishing a connection
28 + * noVNC_connected: Connected to server (most common state)
29 + * noVNC_disconnecting: Disconnecting from server
30 + */
31 +
32 +:root {
33 + font-family: sans-serif;
34 +}
35 +
36 body {
37 margin:0;
38 padding:0;
25 - font-family: Helvetica;
39 /*Background image with light grey curve.*/
40 background-color:#494949;
41 background-repeat:no-repeat;
@@ -78,144 +91,6 @@ html {
91 50% { box-shadow: 60px 10px 0 rgba(255, 255, 255, 0); width: 10px; }
92 }
93
81 -/* ----------------------------------------
82 - * Input Elements
83 - * ----------------------------------------
84 - */
85 -
86 -input:not([type]),
87 -input[type=date],
88 -input[type=datetime-local],
89 -input[type=email],
90 -input[type=month],
91 -input[type=number],
92 -input[type=password],
93 -input[type=search],
94 -input[type=tel],
95 -input[type=text],
96 -input[type=time],
97 -input[type=url],
98 -input[type=week],
99 -textarea {
100 - /* Disable default rendering */
101 - -webkit-appearance: none;
102 - -moz-appearance: none;
103 - background: none;
104 -
105 - margin: 2px;
106 - padding: 2px;
107 - border: 1px solid rgb(192, 192, 192);
108 - border-radius: 5px;
109 - color: black;
110 - background: linear-gradient(to top, rgb(255, 255, 255) 80%, rgb(240, 240, 240));
111 -}
112 -
113 -input[type=button],
114 -input[type=color],
115 -input[type=reset],
116 -input[type=submit],
117 -select {
118 - /* Disable default rendering */
119 - -webkit-appearance: none;
120 - -moz-appearance: none;
121 - background: none;
122 -
123 - margin: 2px;
124 - padding: 2px;
125 - border: 1px solid rgb(192, 192, 192);
126 - border-bottom-width: 2px;
127 - border-radius: 5px;
128 - color: black;
129 - background: linear-gradient(to top, rgb(255, 255, 255), rgb(240, 240, 240));
130 -
131 - /* This avoids it jumping around when :active */
132 - vertical-align: middle;
133 -}
134 -
135 -input[type=button],
136 -input[type=color],
137 -input[type=reset],
138 -input[type=submit] {
139 - padding-left: 20px;
140 - padding-right: 20px;
141 -}
142 -
143 -option {
144 - color: black;
145 - background: white;
146 -}
147 -
148 -input:not([type]):focus,
149 -input[type=button]:focus,
150 -input[type=color]:focus,
151 -input[type=date]:focus,
152 -input[type=datetime-local]:focus,
153 -input[type=email]:focus,
154 -input[type=month]:focus,
155 -input[type=number]:focus,
156 -input[type=password]:focus,
157 -input[type=reset]:focus,
158 -input[type=search]:focus,
159 -input[type=submit]:focus,
160 -input[type=tel]:focus,
161 -input[type=text]:focus,
162 -input[type=time]:focus,
163 -input[type=url]:focus,
164 -input[type=week]:focus,
165 -select:focus,
166 -textarea:focus {
167 - box-shadow: 0px 0px 3px rgba(74, 144, 217, 0.5);
168 - border-color: rgb(74, 144, 217);
169 - outline: none;
170 -}
171 -
172 -input[type=button]::-moz-focus-inner,
173 -input[type=color]::-moz-focus-inner,
174 -input[type=reset]::-moz-focus-inner,
175 -input[type=submit]::-moz-focus-inner {
176 - border: none;
177 -}
178 -
179 -input:not([type]):disabled,
180 -input[type=button]:disabled,
181 -input[type=color]:disabled,
182 -input[type=date]:disabled,
183 -input[type=datetime-local]:disabled,
184 -input[type=email]:disabled,
185 -input[type=month]:disabled,
186 -input[type=number]:disabled,
187 -input[type=password]:disabled,
188 -input[type=reset]:disabled,
189 -input[type=search]:disabled,
190 -input[type=submit]:disabled,
191 -input[type=tel]:disabled,
192 -input[type=text]:disabled,
193 -input[type=time]:disabled,
194 -input[type=url]:disabled,
195 -input[type=week]:disabled,
196 -select:disabled,
197 -textarea:disabled {
198 - color: rgb(128, 128, 128);
199 - background: rgb(240, 240, 240);
200 -}
201 -
202 -input[type=button]:active,
203 -input[type=color]:active,
204 -input[type=reset]:active,
205 -input[type=submit]:active,
206 -select:active {
207 - border-bottom-width: 1px;
208 - margin-top: 3px;
209 -}
210 -
211 -:root:not(.noVNC_touch) input[type=button]:hover:not(:disabled),
212 -:root:not(.noVNC_touch) input[type=color]:hover:not(:disabled),
213 -:root:not(.noVNC_touch) input[type=reset]:hover:not(:disabled),
214 -:root:not(.noVNC_touch) input[type=submit]:hover:not(:disabled),
215 -:root:not(.noVNC_touch) select:hover:not(:disabled) {
216 - background: linear-gradient(to top, rgb(255, 255, 255), rgb(250, 250, 250));
217 -}
218 -
94 /* ----------------------------------------
95 * WebKit centering hacks
96 * ----------------------------------------
@@ -242,13 +117,15 @@ select:active {
117 pointer-events: auto;
118 }
119 .noVNC_vcenter {
245 - display: flex;
120 + display: flex !important;
121 flex-direction: column;
122 justify-content: center;
123 position: fixed;
124 top: 0;
125 left: 0;
126 height: 100%;
127 + margin: 0 !important;
128 + padding: 0 !important;
129 pointer-events: none;
130 }
131 .noVNC_vcenter > * {
@@ -272,13 +149,20 @@ select:active {
149 #noVNC_fallback_error {
150 z-index: 1000;
151 visibility: hidden;
152 + /* Put a dark background in front of everything but the error,
153 + and don't let mouse events pass through */
154 + background: rgba(0, 0, 0, 0.8);
155 + pointer-events: all;
156 }
157 #noVNC_fallback_error.noVNC_open {
158 visibility: visible;
159 }
160
161 #noVNC_fallback_error > div {
281 - max-width: 90%;
162 + max-width: calc(100vw - 30px - 30px);
163 + max-height: calc(100vh - 30px - 30px);
164 + overflow: auto;
165 +
166 padding: 15px;
167
168 transition: 0.5s ease-in-out;
@@ -317,7 +201,6 @@ select:active {
201 }
202
203 #noVNC_fallback_error .noVNC_stack {
320 - max-height: 50vh;
204 padding: 10px;
205 margin: 10px;
206 font-size: 0.8em;
@@ -361,6 +244,9 @@ select:active {
244 background-color: rgb(110, 132, 163);
245 border-radius: 0 10px 10px 0;
246
247 + user-select: none;
248 + -webkit-user-select: none;
249 + -webkit-touch-callout: none; /* Disable iOS image long-press popup */
250 }
251 #noVNC_control_bar.noVNC_open {
252 box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
@@ -433,38 +319,50 @@ select:active {
319 .noVNC_right #noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after {
320 transform: none;
321 }
322 +/* Larger touch area for the handle, used when a touch screen is available */
323 #noVNC_control_bar_handle div {
324 position: absolute;
325 right: -35px;
326 top: 0;
327 width: 50px;
441 - height: 50px;
442 -}
443 -:root:not(.noVNC_touch) #noVNC_control_bar_handle div {
328 + height: 100%;
329 display: none;
330 }
331 +@media (any-pointer: coarse) {
332 + #noVNC_control_bar_handle div {
333 + display: initial;
334 + }
335 +}
336 .noVNC_right #noVNC_control_bar_handle div {
337 left: -35px;
338 right: auto;
339 }
340
451 -#noVNC_control_bar .noVNC_scroll {
341 +#noVNC_control_bar > .noVNC_scroll {
342 max-height: 100vh; /* Chrome is buggy with 100% */
343 overflow-x: hidden;
344 overflow-y: auto;
455 - padding: 0 10px 0 5px;
345 + padding: 0 10px;
346 }
457 -.noVNC_right #noVNC_control_bar .noVNC_scroll {
458 - padding: 0 5px 0 10px;
347 +
348 +#noVNC_control_bar > .noVNC_scroll > * {
349 + display: block;
350 + margin: 10px auto;
351 }
352
353 /* Control bar hint */
462 -#noVNC_control_bar_hint {
354 +#noVNC_hint_anchor {
355 position: fixed;
464 - left: calc(100vw - 50px);
356 + right: -50px;
357 + left: auto;
358 +}
359 +#noVNC_control_bar_anchor.noVNC_right + #noVNC_hint_anchor {
360 + left: -50px;
361 right: auto;
466 - top: 50%;
467 - transform: translateY(-50%) scale(0);
362 +}
363 +#noVNC_control_bar_hint {
364 + position: relative;
365 + transform: scale(0);
366 width: 100px;
367 height: 50%;
368 max-height: 600px;
@@ -477,61 +375,65 @@ select:active {
375 border-radius: 10px;
376 transition-delay: 0s;
377 }
480 -#noVNC_control_bar_anchor.noVNC_right #noVNC_control_bar_hint{
481 - left: auto;
482 - right: calc(100vw - 50px);
483 -}
378 #noVNC_control_bar_hint.noVNC_active {
379 visibility: visible;
380 opacity: 1;
381 transition-delay: 0.2s;
488 - transform: translateY(-50%) scale(1);
382 + transform: scale(1);
383 +}
384 +#noVNC_control_bar_hint.noVNC_notransition {
385 + transition: none !important;
386 }
387
491 -/* General button style */
492 -.noVNC_button {
493 - display: block;
388 +/* Control bar buttons */
389 +#noVNC_control_bar .noVNC_button {
390 padding: 4px 4px;
495 - margin: 10px 0;
391 vertical-align: middle;
392 border:1px solid rgba(255, 255, 255, 0.2);
393 border-radius: 6px;
394 + background-color: transparent;
395 + background-image: unset; /* we don't want the gradiant from input.css */
396 }
500 -.noVNC_button.noVNC_selected {
397 +#noVNC_control_bar .noVNC_button.noVNC_selected {
398 border-color: rgba(0, 0, 0, 0.8);
502 - background: rgba(0, 0, 0, 0.5);
399 + background-color: rgba(0, 0, 0, 0.5);
400 }
504 -.noVNC_button:disabled {
505 - opacity: 0.4;
401 +#noVNC_control_bar .noVNC_button.noVNC_selected:not(:disabled):hover {
402 + border-color: rgba(0, 0, 0, 0.4);
403 + background-color: rgba(0, 0, 0, 0.2);
404 }
507 -.noVNC_button:focus {
508 - outline: none;
405 +#noVNC_control_bar .noVNC_button:not(:disabled):hover {
406 + background-color: rgba(255, 255, 255, 0.2);
407 }
510 -.noVNC_button:active {
408 +#noVNC_control_bar .noVNC_button:not(:disabled):active {
409 padding-top: 5px;
410 padding-bottom: 3px;
411 }
514 -/* Android browsers don't properly update hover state if touch events
515 - * are intercepted, but focus should be safe to display */
516 -:root:not(.noVNC_touch) .noVNC_button.noVNC_selected:hover,
517 -.noVNC_button.noVNC_selected:focus {
518 - border-color: rgba(0, 0, 0, 0.4);
519 - background: rgba(0, 0, 0, 0.2);
520 -}
521 -:root:not(.noVNC_touch) .noVNC_button:hover,
522 -.noVNC_button:focus {
523 - background: rgba(255, 255, 255, 0.2);
412 +#noVNC_control_bar .noVNC_button.noVNC_hidden {
413 + display: none !important;
414 }
525 -.noVNC_button.noVNC_hidden {
526 - display: none;
415 +
416 +/* Android browsers don't properly update hover state if touch events are
417 + * intercepted, like they are when clicking on the remote screen. */
418 +@media (any-pointer: coarse) {
419 + #noVNC_control_bar .noVNC_button:not(:disabled):hover {
420 + background-color: transparent;
421 + }
422 + #noVNC_control_bar .noVNC_button.noVNC_selected:not(:disabled):hover {
423 + border-color: rgba(0, 0, 0, 0.8);
424 + background-color: rgba(0, 0, 0, 0.5);
425 + }
426 }
427
428 +
429 /* Panels */
430 .noVNC_panel {
431 transform: translateX(25px);
432
433 transition: 0.5s ease-in-out;
434
435 + box-sizing: border-box; /* so max-width don't have to care about padding */
436 + max-width: calc(100vw - 75px - 25px); /* minus left and right margins */
437 max-height: 100vh; /* Chrome is buggy with 100% */
438 overflow-x: hidden;
439 overflow-y: auto;
@@ -563,6 +465,17 @@ select:active {
465 transform: translateX(-75px);
466 }
467
468 +.noVNC_panel > * {
469 + display: block;
470 + margin: 10px auto;
471 +}
472 +.noVNC_panel > *:first-child {
473 + margin-top: 0 !important;
474 +}
475 +.noVNC_panel > *:last-child {
476 + margin-bottom: 0 !important;
477 +}
478 +
479 .noVNC_panel hr {
480 border: none;
481 border-top: 1px solid rgb(192, 192, 192);
@@ -571,6 +484,11 @@ select:active {
484 .noVNC_panel label {
485 display: block;
486 white-space: nowrap;
487 + margin: 5px;
488 +}
489 +
490 +.noVNC_panel li {
491 + margin: 5px;
492 }
493
494 .noVNC_panel .noVNC_heading {
@@ -581,7 +499,6 @@ select:active {
499 padding-right: 8px;
500 color: white;
501 font-size: 20px;
584 - margin-bottom: 10px;
502 white-space: nowrap;
503 }
504 .noVNC_panel .noVNC_heading img {
@@ -622,6 +539,12 @@ select:active {
539 font-size: 13px;
540 }
541
542 +.noVNC_logo + hr {
543 + /* Remove all but top border */
544 + border: none;
545 + border-top: 1px solid rgba(255, 255, 255, 0.2);
546 +}
547 +
548 :root:not(.noVNC_connected) #noVNC_view_drag_button {
549 display: none;
550 }
@@ -630,8 +553,15 @@ select:active {
553 :root:not(.noVNC_connected) #noVNC_mobile_buttons {
554 display: none;
555 }
633 -:root:not(.noVNC_touch) #noVNC_mobile_buttons {
634 - display: none;
556 +@media not all and (any-pointer: coarse) {
557 + /* FIXME: The button for the virtual keyboard is the only button in this
558 + group of "mobile buttons". It is bad to assume that no touch
559 + devices have physical keyboards available. Hopefully we can get
560 + a media query for this:
561 + https://github.com/w3c/csswg-drafts/issues/3871 */
562 + :root.noVNC_connected #noVNC_mobile_buttons {
563 + display: none;
564 + }
565 }
566
567 /* Extra manual keys */
@@ -642,7 +572,7 @@ select:active {
572 #noVNC_modifiers {
573 background-color: rgb(92, 92, 92);
574 border: none;
645 - padding: 0 10px;
575 + padding: 10px;
576 }
577
578 /* Shutdown/Reboot */
@@ -663,13 +593,16 @@ select:active {
593 :root:not(.noVNC_connected) #noVNC_clipboard_button {
594 display: none;
595 }
666 -#noVNC_clipboard {
667 - /* Full screen, minus padding and left and right margins */
668 - max-width: calc(100vw - 2*15px - 75px - 25px);
669 -}
596 #noVNC_clipboard_text {
671 - width: 500px;
597 + width: 360px;
598 + min-width: 150px;
599 + height: 160px;
600 + min-height: 70px;
601 +
602 + box-sizing: border-box;
603 max-width: 100%;
604 + /* minus approximate height of title, height of subtitle, and margin */
605 + max-height: calc(100vh - 10em - 25px);
606 }
607
608 /* Settings */
@@ -677,7 +610,6 @@ select:active {
610 }
611 #noVNC_settings ul {
612 list-style: none;
680 - margin: 0px;
613 padding: 0px;
614 }
615 #noVNC_setting_port {
@@ -803,36 +735,32 @@ select:active {
735 font-size: calc(25vw - 30px);
736 }
737 }
806 -#noVNC_connect_button {
807 - cursor: pointer;
808 -
809 - padding: 10px;
738 +#noVNC_connect_dlg div {
739 + padding: 12px;
740
811 - color: white;
741 background-color: rgb(110, 132, 163);
742 border-radius: 12px;
814 -
743 text-align: center;
744 font-size: 20px;
745
746 box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
747 }
820 -#noVNC_connect_button div {
821 - margin: 2px;
748 +#noVNC_connect_button {
749 + width: 100%;
750 padding: 5px 30px;
823 - border: 1px solid rgb(83, 99, 122);
824 - border-bottom-width: 2px;
751 +
752 + cursor: pointer;
753 +
754 + border-color: rgb(83, 99, 122);
755 border-radius: 5px;
756 +
757 background: linear-gradient(to top, rgb(110, 132, 163), rgb(99, 119, 147));
758 + color: white;
759
760 /* This avoids it jumping around when :active */
761 vertical-align: middle;
762 }
831 -#noVNC_connect_button div:active {
832 - border-bottom-width: 1px;
833 - margin-top: 3px;
834 -}
835 -:root:not(.noVNC_touch) #noVNC_connect_button div:hover {
763 +#noVNC_connect_button:hover {
764 background: linear-gradient(to top, rgb(110, 132, 163), rgb(105, 125, 155));
765 }
766
@@ -841,6 +769,23 @@ select:active {
769 height: 1.3em;
770 }
771
772 +/* ----------------------------------------
773 + * Server verification Dialog
774 + * ----------------------------------------
775 + */
776 +
777 +#noVNC_verify_server_dlg {
778 + position: relative;
779 +
780 + transform: translateY(-50px);
781 +}
782 +#noVNC_verify_server_dlg.noVNC_open {
783 + transform: translateY(0);
784 +}
785 +#noVNC_fingerprint_block {
786 + margin: 10px;
787 +}
788 +
789 /* ----------------------------------------
790 * Password Dialog
791 * ----------------------------------------
@@ -854,12 +799,8 @@ select:active {
799 #noVNC_credentials_dlg.noVNC_open {
800 transform: translateY(0);
801 }
857 -#noVNC_credentials_dlg ul {
858 - list-style: none;
859 - margin: 0px;
860 - padding: 0px;
861 -}
862 -.noVNC_hidden {
802 +#noVNC_username_block.noVNC_hidden,
803 +#noVNC_password_block.noVNC_hidden {
804 display: none;
805 }
806
@@ -871,7 +812,11 @@ select:active {
812
813 /* Transition screen */
814 #noVNC_transition {
874 - display: none;
815 + transition: 0.5s ease-in-out;
816 +
817 + display: flex;
818 + opacity: 0;
819 + visibility: hidden;
820
821 position: fixed;
822 top: 0;
@@ -892,7 +837,8 @@ select:active {
837 :root.noVNC_connecting #noVNC_transition,
838 :root.noVNC_disconnecting #noVNC_transition,
839 :root.noVNC_reconnecting #noVNC_transition {
895 - display: flex;
840 + opacity: 1;
841 + visibility: visible;
842 }
843 :root:not(.noVNC_reconnecting) #noVNC_cancel_reconnect_button {
844 display: none;
@@ -908,6 +854,12 @@ select:active {
854 background-color: #313131;
855 border-bottom-right-radius: 800px 600px;
856 /*border-top-left-radius: 800px 600px;*/
857 +
858 + /* If selection isn't disabled, long-pressing stuff in the sidebar
859 + can accidentally select the container or the canvas. This can
860 + happen when attempting to move the handle. */
861 + user-select: none;
862 + -webkit-user-select: none;
863 }
864
865 #noVNC_keyboardinput {
public/novnc/app/styles/input.css new
+281
@@ -0,0 +1,281 @@
1 +/*
2 + * noVNC general input element CSS
3 + * Copyright (C) 2022 The noVNC Authors
4 + * noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
5 + * This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
6 + */
7 +
8 +/*
9 + * Common for all inputs
10 + */
11 +input, input::file-selector-button, button, select, textarea {
12 + /* Respect standard font settings */
13 + font: inherit;
14 +
15 + /* Disable default rendering */
16 + appearance: none;
17 + background: none;
18 +
19 + padding: 5px;
20 + border: 1px solid rgb(192, 192, 192);
21 + border-radius: 5px;
22 + color: black;
23 + --bg-gradient: linear-gradient(to top, rgb(255, 255, 255) 80%, rgb(240, 240, 240));
24 + background-image: var(--bg-gradient);
25 +}
26 +
27 +/*
28 + * Buttons
29 + */
30 +input[type=button],
31 +input[type=color],
32 +input[type=image],
33 +input[type=reset],
34 +input[type=submit],
35 +input::file-selector-button,
36 +button,
37 +select {
38 + border-bottom-width: 2px;
39 +
40 + /* This avoids it jumping around when :active */
41 + vertical-align: middle;
42 + margin-top: 0;
43 +
44 + padding-left: 20px;
45 + padding-right: 20px;
46 +
47 + /* Disable Chrome's touch tap highlight */
48 + -webkit-tap-highlight-color: transparent;
49 +}
50 +
51 +/*
52 + * Select dropdowns
53 + */
54 +select {
55 + --select-arrow: url('data:image/svg+xml;utf8, \
56 + <svg width="8" height="6" version="1.1" viewBox="0 0 8 6" \
57 + xmlns="http://www.w3.org/2000/svg"> \
58 + <path d="m6.5 1.5 -2.5 3 -2.5 -3 5 0" stroke-width="3" \
59 + stroke="rgb(31,31,31)" fill="none" \
60 + stroke-linecap="round" stroke-linejoin="round" /> \
61 + </svg>');
62 + background-image: var(--select-arrow), var(--bg-gradient);
63 + background-position: calc(100% - 7px), left top;
64 + background-repeat: no-repeat;
65 + padding-right: calc(2*7px + 8px);
66 + padding-left: 7px;
67 +}
68 +/* FIXME: :active isn't set when the <select> is opened in Firefox:
69 + https://bugzilla.mozilla.org/show_bug.cgi?id=1805406 */
70 +select:active {
71 + /* Rotated arrow */
72 + background-image: url('data:image/svg+xml;utf8, \
73 + <svg width="8" height="6" version="1.1" viewBox="0 0 8 6" \
74 + xmlns="http://www.w3.org/2000/svg" transform="rotate(180)" > \
75 + <path d="m6.5 1.5 -2.5 3 -2.5 -3 5 0" stroke-width="3" \
76 + stroke="rgb(31,31,31)" fill="none" \
77 + stroke-linecap="round" stroke-linejoin="round" /> \
78 + </svg>'), var(--bg-gradient);
79 +}
80 +option {
81 + color: black;
82 + background: white;
83 +}
84 +
85 +/*
86 + * Checkboxes
87 + */
88 +input[type=checkbox] {
89 + background-color: white;
90 + background-image: unset;
91 + border: 1px solid dimgrey;
92 + border-radius: 3px;
93 + width: 13px;
94 + height: 13px;
95 + padding: 0;
96 + margin-right: 6px;
97 + vertical-align: bottom;
98 + transition: 0.2s background-color linear;
99 +}
100 +input[type=checkbox]:checked {
101 + background-color: rgb(110, 132, 163);
102 + border-color: rgb(110, 132, 163);
103 +}
104 +input[type=checkbox]:checked::after {
105 + content: "";
106 + display: block; /* width & height doesn't work on inline elements */
107 + position: relative;
108 + top: 0;
109 + left: 3px;
110 + width: 3px;
111 + height: 7px;
112 + border: 1px solid white;
113 + border-width: 0 2px 2px 0;
114 + transform: rotate(40deg);
115 +}
116 +
117 +/*
118 + * Radiobuttons
119 + */
120 +input[type=radio] {
121 + border-radius: 50%;
122 + border: 1px solid dimgrey;
123 + width: 12px;
124 + height: 12px;
125 + padding: 0;
126 + margin-right: 6px;
127 + transition: 0.2s border linear;
128 +}
129 +input[type=radio]:checked {
130 + border: 6px solid rgb(110, 132, 163);
131 +}
132 +
133 +/*
134 + * Range sliders
135 + */
136 +input[type=range] {
137 + border: unset;
138 + border-radius: 3px;
139 + height: 20px;
140 + padding: 0;
141 + background: transparent;
142 +}
143 +/* -webkit-slider.. & -moz-range.. cant be in selector lists:
144 + https://bugs.chromium.org/p/chromium/issues/detail?id=1154623 */
145 +input[type=range]::-webkit-slider-runnable-track {
146 + background-color: rgb(110, 132, 163);
147 + height: 6px;
148 + border-radius: 3px;
149 +}
150 +input[type=range]::-moz-range-track {
151 + background-color: rgb(110, 132, 163);
152 + height: 6px;
153 + border-radius: 3px;
154 +}
155 +input[type=range]::-webkit-slider-thumb {
156 + appearance: none;
157 + width: 18px;
158 + height: 20px;
159 + border-radius: 5px;
160 + background-color: white;
161 + border: 1px solid dimgray;
162 + margin-top: -7px;
163 +}
164 +input[type=range]::-moz-range-thumb {
165 + appearance: none;
166 + width: 18px;
167 + height: 20px;
168 + border-radius: 5px;
169 + background-color: white;
170 + border: 1px solid dimgray;
171 + margin-top: -7px;
172 +}
173 +
174 +/*
175 + * File choosers
176 + */
177 +input[type=file] {
178 + background-image: none;
179 + border: none;
180 +}
181 +input::file-selector-button {
182 + margin-right: 6px;
183 +}
184 +
185 +/*
186 + * Hover
187 + */
188 +input[type=button]:hover,
189 +input[type=color]:hover,
190 +input[type=image]:hover,
191 +input[type=reset]:hover,
192 +input[type=submit]:hover,
193 +input::file-selector-button:hover,
194 +button:hover {
195 + background-image: linear-gradient(to top, rgb(255, 255, 255), rgb(250, 250, 250));
196 +}
197 +select:hover {
198 + background-image: var(--select-arrow),
199 + linear-gradient(to top, rgb(255, 255, 255), rgb(250, 250, 250));
200 + background-position: calc(100% - 7px), left top;
201 + background-repeat: no-repeat;
202 +}
203 +@media (any-pointer: coarse) {
204 + /* We don't want a hover style after touch input */
205 + input[type=button]:hover,
206 + input[type=color]:hover,
207 + input[type=image]:hover,
208 + input[type=reset]:hover,
209 + input[type=submit]:hover,
210 + input::file-selector-button:hover,
211 + button:hover {
212 + background-image: var(--bg-gradient);
213 + }
214 + select:hover {
215 + background-image: var(--select-arrow), var(--bg-gradient);
216 + }
217 +}
218 +
219 +/*
220 + * Active (clicked)
221 + */
222 +input[type=button]:active,
223 +input[type=color]:active,
224 +input[type=image]:active,
225 +input[type=reset]:active,
226 +input[type=submit]:active,
227 +input::file-selector-button:active,
228 +button:active,
229 +select:active {
230 + border-bottom-width: 1px;
231 + margin-top: 1px;
232 +}
233 +
234 +/*
235 + * Focus (tab)
236 + */
237 +input:focus-visible,
238 +input:focus-visible::file-selector-button,
239 +button:focus-visible,
240 +select:focus-visible,
241 +textarea:focus-visible {
242 + outline: 2px solid rgb(74, 144, 217);
243 + outline-offset: 1px;
244 +}
245 +input[type=file]:focus-visible {
246 + outline: none; /* We outline the button instead of the entire element */
247 +}
248 +
249 +/*
250 + * Disabled
251 + */
252 +input:disabled,
253 +input:disabled::file-selector-button,
254 +button:disabled,
255 +select:disabled,
256 +textarea:disabled {
257 + opacity: 0.4;
258 +}
259 +input[type=button]:disabled,
260 +input[type=color]:disabled,
261 +input[type=image]:disabled,
262 +input[type=reset]:disabled,
263 +input[type=submit]:disabled,
264 +input:disabled::file-selector-button,
265 +button:disabled,
266 +select:disabled {
267 + background-image: var(--bg-gradient);
268 + border-bottom-width: 2px;
269 + margin-top: 0;
270 +}
271 +input[type=file]:disabled {
272 + background-image: none;
273 +}
274 +select:disabled {
275 + background-image: var(--select-arrow), var(--bg-gradient);
276 +}
277 +input[type=image]:disabled {
278 + /* See Firefox bug:
279 + https://bugzilla.mozilla.org/show_bug.cgi?id=1798304 */
280 + cursor: default;
281 +}
public/novnc/app/ui.js
+89 -20
@@ -8,7 +8,8 @@
8
9 import * as Log from '../core/util/logging.js';
10 import _, { l10n } from './localization.js';
11 -import { isTouchDevice, isSafari, hasScrollbarGutter, dragThreshold }
11 +import { isTouchDevice, isMac, isIOS, isAndroid, isChromeOS, isSafari,
12 + hasScrollbarGutter, dragThreshold }
13 from '../core/util/browser.js';
14 import { setCapture, getPointerEvent } from '../core/util/events.js';
15 import KeyTable from "../core/input/keysym.js";
@@ -79,13 +80,19 @@ const UI = {
80 // Render default UI and initialize settings menu
81 start() {
82
82 - if (urlargs.name) { document.title = urlargs.name + " - noVNC"; }
83 -
83 UI.initSettings();
84
85 // Translate the DOM
86 l10n.translateDOM();
87
88 + // We rely on modern APIs which might not be available in an
89 + // insecure context
90 + if (!window.isSecureContext) {
91 + // FIXME: This gets hidden when connecting
92 + UI.showStatus(_("HTTPS is required for full functionality"), 'error');
93 + }
94 +
95 + // Try to fetch version number
96 fetch('./package.json')
97 .then((response) => {
98 if (!response.ok) {
@@ -105,7 +112,6 @@ const UI = {
112
113 // Adapt the interface for touch screen devices
114 if (isTouchDevice) {
108 - document.documentElement.classList.add("noVNC_touch");
115 // Remove the address bar
116 setTimeout(() => window.scrollTo(0, 1), 100);
117 }
@@ -341,6 +347,10 @@ const UI = {
347 document.getElementById("noVNC_cancel_reconnect_button")
348 .addEventListener('click', UI.cancelReconnect);
349
350 + document.getElementById("noVNC_approve_server_button")
351 + .addEventListener('click', UI.approveServer);
352 + document.getElementById("noVNC_reject_server_button")
353 + .addEventListener('click', UI.rejectServer);
354 document.getElementById("noVNC_credentials_button")
355 .addEventListener('click', UI.setCredentials);
356 },
@@ -350,8 +360,6 @@ const UI = {
360 .addEventListener('click', UI.toggleClipboardPanel);
361 document.getElementById("noVNC_clipboard_text")
362 .addEventListener('change', UI.clipboardSend);
353 - document.getElementById("noVNC_clipboard_clear_button")
354 - .addEventListener('click', UI.clipboardClear);
363 },
364
365 // Add a call to save settings when the element changes,
@@ -470,6 +478,8 @@ const UI = {
478 // State change closes dialogs as they may not be relevant
479 // anymore
480 UI.closeAllPanels();
481 + document.getElementById('noVNC_verify_server_dlg')
482 + .classList.remove('noVNC_open');
483 document.getElementById('noVNC_credentials_dlg')
484 .classList.remove('noVNC_open');
485 },
@@ -602,10 +612,20 @@ const UI = {
612
613 // Consider this a movement of the handle
614 UI.controlbarDrag = true;
615 +
616 + // The user has "followed" hint, let's hide it until the next drag
617 + UI.showControlbarHint(false, false);
618 },
619
607 - showControlbarHint(show) {
620 + showControlbarHint(show, animate=true) {
621 const hint = document.getElementById('noVNC_control_bar_hint');
622 +
623 + if (animate) {
624 + hint.classList.remove("noVNC_notransition");
625 + } else {
626 + hint.classList.add("noVNC_notransition");
627 + }
628 +
629 if (show) {
630 hint.classList.add("noVNC_active");
631 } else {
@@ -979,11 +999,6 @@ const UI = {
999 Log.Debug("<< UI.clipboardReceive");
1000 },
1001
982 - clipboardClear() {
983 - document.getElementById('noVNC_clipboard_text').value = "";
984 - UI.rfb.clipboardPasteFrom("");
985 - },
986 -
1002 clipboardSend() {
1003 const text = document.getElementById('noVNC_clipboard_text').value;
1004 Log.Debug(">> UI.clipboardSend: " + text.substr(0, 40) + "...");
@@ -1039,14 +1054,19 @@ const UI = {
1054
1055 UI.updateVisualState('connecting');
1056
1057 +
1058 +
1059 +
1060 UI.rfb = new RFB(document.getElementById('noVNC_container'), urlargs.ws,
1061 { shared: UI.getSetting('shared'),
1062 repeaterID: UI.getSetting('repeaterID'),
1063 credentials: { password: password } });
1064 UI.rfb.addEventListener("connect", UI.connectFinished);
1065 UI.rfb.addEventListener("disconnect", UI.disconnectFinished);
1066 + UI.rfb.addEventListener("serververification", UI.serverVerify);
1067 UI.rfb.addEventListener("credentialsrequired", UI.credentials);
1068 UI.rfb.addEventListener("securityfailure", UI.securityFailed);
1069 + UI.rfb.addEventListener("clippingviewport", UI.updateViewDrag);
1070 UI.rfb.addEventListener("capabilities", UI.updatePowerButton);
1071 UI.rfb.addEventListener("clipboard", UI.clipboardReceive);
1072 UI.rfb.addEventListener("bell", UI.bell);
@@ -1133,7 +1153,9 @@ const UI = {
1153 } else {
1154 UI.showStatus(_("Failed to connect to server"), 'error');
1155 }
1136 - } else if (UI.getSetting('reconnect', false) === true && !UI.inhibitReconnect) {
1156 + }
1157 + // If reconnecting is allowed process it now
1158 + if (UI.getSetting('reconnect', false) === true && !UI.inhibitReconnect) {
1159 UI.updateVisualState('reconnecting');
1160
1161 const delay = parseInt(UI.getSetting('reconnect_delay'));
@@ -1144,6 +1166,7 @@ const UI = {
1166 UI.showStatus(_("Disconnected"), 'normal');
1167 }
1168
1169 +
1170 UI.openControlbar();
1171 UI.openConnectPanel();
1172 },
@@ -1165,6 +1188,37 @@ const UI = {
1188 /* ------^-------
1189 * /CONNECTION
1190 * ==============
1191 + * SERVER VERIFY
1192 + * ------v------*/
1193 +
1194 + async serverVerify(e) {
1195 + const type = e.detail.type;
1196 + if (type === 'RSA') {
1197 + const publickey = e.detail.publickey;
1198 + let fingerprint = await window.crypto.subtle.digest("SHA-1", publickey);
1199 + // The same fingerprint format as RealVNC
1200 + fingerprint = Array.from(new Uint8Array(fingerprint).slice(0, 8)).map(
1201 + x => x.toString(16).padStart(2, '0')).join('-');
1202 + document.getElementById('noVNC_verify_server_dlg').classList.add('noVNC_open');
1203 + document.getElementById('noVNC_fingerprint').innerHTML = fingerprint;
1204 + }
1205 + },
1206 +
1207 + approveServer(e) {
1208 + e.preventDefault();
1209 + document.getElementById('noVNC_verify_server_dlg').classList.remove('noVNC_open');
1210 + UI.rfb.approveServer();
1211 + },
1212 +
1213 + rejectServer(e) {
1214 + e.preventDefault();
1215 + document.getElementById('noVNC_verify_server_dlg').classList.remove('noVNC_open');
1216 + UI.disconnect();
1217 + },
1218 +
1219 +/* ------^-------
1220 + * /SERVER VERIFY
1221 + * ==============
1222 * PASSWORD
1223 * ------v------*/
1224
@@ -1288,13 +1342,25 @@ const UI = {
1342
1343 const scaling = UI.getSetting('resize') === 'scale';
1344
1345 + // Some platforms have overlay scrollbars that are difficult
1346 + // to use in our case, which means we have to force panning
1347 + // FIXME: Working scrollbars can still be annoying to use with
1348 + // touch, so we should ideally be able to have both
1349 + // panning and scrollbars at the same time
1350 +
1351 + let brokenScrollbars = false;
1352 +
1353 + if (!hasScrollbarGutter) {
1354 + if (isIOS() || isAndroid() || isMac() || isChromeOS()) {
1355 + brokenScrollbars = true;
1356 + }
1357 + }
1358 +
1359 if (scaling) {
1360 // Can't be clipping if viewport is scaled to fit
1361 UI.forceSetting('view_clip', false);
1362 UI.rfb.clipViewport = false;
1295 - } else if (!hasScrollbarGutter) {
1296 - // Some platforms have scrollbars that are difficult
1297 - // to use in our case, so we always use our own panning
1363 + } else if (brokenScrollbars) {
1364 UI.forceSetting('view_clip', true);
1365 UI.rfb.clipViewport = true;
1366 } else {
@@ -1325,7 +1391,8 @@ const UI = {
1391
1392 const viewDragButton = document.getElementById('noVNC_view_drag_button');
1393
1328 - if (!UI.rfb.clipViewport && UI.rfb.dragViewport) {
1394 + if ((!UI.rfb.clipViewport || !UI.rfb.clippingViewport) &&
1395 + UI.rfb.dragViewport) {
1396 // We are no longer clipping the viewport. Make sure
1397 // viewport drag isn't active when it can't be used.
1398 UI.rfb.dragViewport = false;
@@ -1342,6 +1409,8 @@ const UI = {
1409 } else {
1410 viewDragButton.classList.add("noVNC_hidden");
1411 }
1412 +
1413 + viewDragButton.disabled = !UI.rfb.clippingViewport;
1414 },
1415
1416 /* ------^-------
@@ -1670,9 +1739,9 @@ const UI = {
1739 },
1740
1741 updateDesktopName(e) {
1673 - //UI.desktopName = e.detail.name;
1742 + // UI.desktopName = e.detail.name;
1743 // Display the desktop name in the document title
1675 - //document.title = e.detail.name + " - " + PAGE_TITLE;
1744 + // document.title = e.detail.name + " - " + PAGE_TITLE;
1745 },
1746
1747 bell(e) {
@@ -1708,7 +1777,7 @@ const UI = {
1777 };
1778
1779 // Set up translations
1711 -const LINGUAS = ["cs", "de", "el", "es", "fr", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"];
1780 +const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"];
1781 l10n.setup(LINGUAS);
1782 if (l10n.language === "en" || l10n.dictionary !== undefined) {
1783 UI.prime();
public/novnc/app/webutil.js
+2 -2
@@ -32,7 +32,7 @@ export function initLogging(level) {
32 export function getQueryVar(name, defVal) {
33 "use strict";
34 const re = new RegExp('.*[?&]' + name + '=([^&#]*)'),
35 - match = ''.concat(document.location.href, window.location.hash).match(re);
35 + match = ''.concat(document.location.href, window.location.hash).match(re);
36 if (typeof defVal === 'undefined') { defVal = null; }
37
38 if (match) {
@@ -46,7 +46,7 @@ export function getQueryVar(name, defVal) {
46 export function getHashVar(name, defVal) {
47 "use strict";
48 const re = new RegExp('.*[&#]' + name + '=([^&]*)'),
49 - match = document.location.hash.match(re);
49 + match = document.location.hash.match(re);
50 if (typeof defVal === 'undefined') { defVal = null; }
51
52 if (match) {
public/novnc/core/decoders/jpeg.js new
+141
@@ -0,0 +1,141 @@
1 +/*
2 + * noVNC: HTML5 VNC client
3 + * Copyright (C) 2019 The noVNC Authors
4 + * Licensed under MPL 2.0 (see LICENSE.txt)
5 + *
6 + * See README.md for usage and integration instructions.
7 + *
8 + */
9 +
10 +export default class JPEGDecoder {
11 + constructor() {
12 + // RealVNC will reuse the quantization tables
13 + // and Huffman tables, so we need to cache them.
14 + this._quantTables = [];
15 + this._huffmanTables = [];
16 + this._cachedQuantTables = [];
17 + this._cachedHuffmanTables = [];
18 +
19 + this._jpegLength = 0;
20 + this._segments = [];
21 + }
22 +
23 + decodeRect(x, y, width, height, sock, display, depth) {
24 + // A rect of JPEG encodings is simply a JPEG file
25 + if (!this._parseJPEG(sock.rQslice(0))) {
26 + return false;
27 + }
28 + const data = sock.rQshiftBytes(this._jpegLength);
29 + if (this._quantTables.length != 0 && this._huffmanTables.length != 0) {
30 + // If there are quantization tables and Huffman tables in the JPEG
31 + // image, we can directly render it.
32 + display.imageRect(x, y, width, height, "image/jpeg", data);
33 + return true;
34 + } else {
35 + // Otherwise we need to insert cached tables.
36 + const sofIndex = this._segments.findIndex(
37 + x => x[1] == 0xC0 || x[1] == 0xC2
38 + );
39 + if (sofIndex == -1) {
40 + throw new Error("Illegal JPEG image without SOF");
41 + }
42 + let segments = this._segments.slice(0, sofIndex);
43 + segments = segments.concat(this._quantTables.length ?
44 + this._quantTables :
45 + this._cachedQuantTables);
46 + segments.push(this._segments[sofIndex]);
47 + segments = segments.concat(this._huffmanTables.length ?
48 + this._huffmanTables :
49 + this._cachedHuffmanTables,
50 + this._segments.slice(sofIndex + 1));
51 + let length = 0;
52 + for (let i = 0; i < segments.length; i++) {
53 + length += segments[i].length;
54 + }
55 + const data = new Uint8Array(length);
56 + length = 0;
57 + for (let i = 0; i < segments.length; i++) {
58 + data.set(segments[i], length);
59 + length += segments[i].length;
60 + }
61 + display.imageRect(x, y, width, height, "image/jpeg", data);
62 + return true;
63 + }
64 + }
65 +
66 + _parseJPEG(buffer) {
67 + if (this._quantTables.length != 0) {
68 + this._cachedQuantTables = this._quantTables;
69 + }
70 + if (this._huffmanTables.length != 0) {
71 + this._cachedHuffmanTables = this._huffmanTables;
72 + }
73 + this._quantTables = [];
74 + this._huffmanTables = [];
75 + this._segments = [];
76 + let i = 0;
77 + let bufferLength = buffer.length;
78 + while (true) {
79 + let j = i;
80 + if (j + 2 > bufferLength) {
81 + return false;
82 + }
83 + if (buffer[j] != 0xFF) {
84 + throw new Error("Illegal JPEG marker received (byte: " +
85 + buffer[j] + ")");
86 + }
87 + const type = buffer[j+1];
88 + j += 2;
89 + if (type == 0xD9) {
90 + this._jpegLength = j;
91 + this._segments.push(buffer.slice(i, j));
92 + return true;
93 + } else if (type == 0xDA) {
94 + // start of scan
95 + let hasFoundEndOfScan = false;
96 + for (let k = j + 3; k + 1 < bufferLength; k++) {
97 + if (buffer[k] == 0xFF && buffer[k+1] != 0x00 &&
98 + !(buffer[k+1] >= 0xD0 && buffer[k+1] <= 0xD7)) {
99 + j = k;
100 + hasFoundEndOfScan = true;
101 + break;
102 + }
103 + }
104 + if (!hasFoundEndOfScan) {
105 + return false;
106 + }
107 + this._segments.push(buffer.slice(i, j));
108 + i = j;
109 + continue;
110 + } else if (type >= 0xD0 && type < 0xD9 || type == 0x01) {
111 + // No length after marker
112 + this._segments.push(buffer.slice(i, j));
113 + i = j;
114 + continue;
115 + }
116 + if (j + 2 > bufferLength) {
117 + return false;
118 + }
119 + const length = (buffer[j] << 8) + buffer[j+1] - 2;
120 + if (length < 0) {
121 + throw new Error("Illegal JPEG length received (length: " +
122 + length + ")");
123 + }
124 + j += 2;
125 + if (j + length > bufferLength) {
126 + return false;
127 + }
128 + j += length;
129 + const segment = buffer.slice(i, j);
130 + if (type == 0xC4) {
131 + // Huffman tables
132 + this._huffmanTables.push(segment);
133 + } else if (type == 0xDB) {
134 + // Quantization tables
135 + this._quantTables.push(segment);
136 + }
137 + this._segments.push(segment);
138 + i = j;
139 + }
140 + }
141 +}
public/novnc/core/decoders/raw.js
+1 -1
@@ -51,7 +51,7 @@ export default class RawDecoder {
51
52 // Max sure the image is fully opaque
53 for (let i = 0; i < pixels; i++) {
54 - data[i * 4 + 3] = 255;
54 + data[index + i * 4 + 3] = 255;
55 }
56
57 display.blitImage(x, curY, width, currHeight, data, index);
public/novnc/core/decoders/zrle.js new
+185
@@ -0,0 +1,185 @@
1 +/*
2 + * noVNC: HTML5 VNC client
3 + * Copyright (C) 2021 The noVNC Authors
4 + * Licensed under MPL 2.0 (see LICENSE.txt)
5 + *
6 + * See README.md for usage and integration instructions.
7 + *
8 + */
9 +
10 +import Inflate from "../inflator.js";
11 +
12 +const ZRLE_TILE_WIDTH = 64;
13 +const ZRLE_TILE_HEIGHT = 64;
14 +
15 +export default class ZRLEDecoder {
16 + constructor() {
17 + this._length = 0;
18 + this._inflator = new Inflate();
19 +
20 + this._pixelBuffer = new Uint8Array(ZRLE_TILE_WIDTH * ZRLE_TILE_HEIGHT * 4);
21 + this._tileBuffer = new Uint8Array(ZRLE_TILE_WIDTH * ZRLE_TILE_HEIGHT * 4);
22 + }
23 +
24 + decodeRect(x, y, width, height, sock, display, depth) {
25 + if (this._length === 0) {
26 + if (sock.rQwait("ZLib data length", 4)) {
27 + return false;
28 + }
29 + this._length = sock.rQshift32();
30 + }
31 + if (sock.rQwait("Zlib data", this._length)) {
32 + return false;
33 + }
34 +
35 + const data = sock.rQshiftBytes(this._length);
36 +
37 + this._inflator.setInput(data);
38 +
39 + for (let ty = y; ty < y + height; ty += ZRLE_TILE_HEIGHT) {
40 + let th = Math.min(ZRLE_TILE_HEIGHT, y + height - ty);
41 +
42 + for (let tx = x; tx < x + width; tx += ZRLE_TILE_WIDTH) {
43 + let tw = Math.min(ZRLE_TILE_WIDTH, x + width - tx);
44 +
45 + const tileSize = tw * th;
46 + const subencoding = this._inflator.inflate(1)[0];
47 + if (subencoding === 0) {
48 + // raw data
49 + const data = this._readPixels(tileSize);
50 + display.blitImage(tx, ty, tw, th, data, 0, false);
51 + } else if (subencoding === 1) {
52 + // solid
53 + const background = this._readPixels(1);
54 + display.fillRect(tx, ty, tw, th, [background[0], background[1], background[2]]);
55 + } else if (subencoding >= 2 && subencoding <= 16) {
56 + const data = this._decodePaletteTile(subencoding, tileSize, tw, th);
57 + display.blitImage(tx, ty, tw, th, data, 0, false);
58 + } else if (subencoding === 128) {
59 + const data = this._decodeRLETile(tileSize);
60 + display.blitImage(tx, ty, tw, th, data, 0, false);
61 + } else if (subencoding >= 130 && subencoding <= 255) {
62 + const data = this._decodeRLEPaletteTile(subencoding - 128, tileSize);
63 + display.blitImage(tx, ty, tw, th, data, 0, false);
64 + } else {
65 + throw new Error('Unknown subencoding: ' + subencoding);
66 + }
67 + }
68 + }
69 + this._length = 0;
70 + return true;
71 + }
72 +
73 + _getBitsPerPixelInPalette(paletteSize) {
74 + if (paletteSize <= 2) {
75 + return 1;
76 + } else if (paletteSize <= 4) {
77 + return 2;
78 + } else if (paletteSize <= 16) {
79 + return 4;
80 + }
81 + }
82 +
83 + _readPixels(pixels) {
84 + let data = this._pixelBuffer;
85 + const buffer = this._inflator.inflate(3*pixels);
86 + for (let i = 0, j = 0; i < pixels*4; i += 4, j += 3) {
87 + data[i] = buffer[j];
88 + data[i + 1] = buffer[j + 1];
89 + data[i + 2] = buffer[j + 2];
90 + data[i + 3] = 255; // Add the Alpha
91 + }
92 + return data;
93 + }
94 +
95 + _decodePaletteTile(paletteSize, tileSize, tilew, tileh) {
96 + const data = this._tileBuffer;
97 + const palette = this._readPixels(paletteSize);
98 + const bitsPerPixel = this._getBitsPerPixelInPalette(paletteSize);
99 + const mask = (1 << bitsPerPixel) - 1;
100 +
101 + let offset = 0;
102 + let encoded = this._inflator.inflate(1)[0];
103 +
104 + for (let y=0; y<tileh; y++) {
105 + let shift = 8-bitsPerPixel;
106 + for (let x=0; x<tilew; x++) {
107 + if (shift<0) {
108 + shift=8-bitsPerPixel;
109 + encoded = this._inflator.inflate(1)[0];
110 + }
111 + let indexInPalette = (encoded>>shift) & mask;
112 +
113 + data[offset] = palette[indexInPalette * 4];
114 + data[offset + 1] = palette[indexInPalette * 4 + 1];
115 + data[offset + 2] = palette[indexInPalette * 4 + 2];
116 + data[offset + 3] = palette[indexInPalette * 4 + 3];
117 + offset += 4;
118 + shift-=bitsPerPixel;
119 + }
120 + if (shift<8-bitsPerPixel && y<tileh-1) {
121 + encoded = this._inflator.inflate(1)[0];
122 + }
123 + }
124 + return data;
125 + }
126 +
127 + _decodeRLETile(tileSize) {
128 + const data = this._tileBuffer;
129 + let i = 0;
130 + while (i < tileSize) {
131 + const pixel = this._readPixels(1);
132 + const length = this._readRLELength();
133 + for (let j = 0; j < length; j++) {
134 + data[i * 4] = pixel[0];
135 + data[i * 4 + 1] = pixel[1];
136 + data[i * 4 + 2] = pixel[2];
137 + data[i * 4 + 3] = pixel[3];
138 + i++;
139 + }
140 + }
141 + return data;
142 + }
143 +
144 + _decodeRLEPaletteTile(paletteSize, tileSize) {
145 + const data = this._tileBuffer;
146 +
147 + // palette
148 + const palette = this._readPixels(paletteSize);
149 +
150 + let offset = 0;
151 + while (offset < tileSize) {
152 + let indexInPalette = this._inflator.inflate(1)[0];
153 + let length = 1;
154 + if (indexInPalette >= 128) {
155 + indexInPalette -= 128;
156 + length = this._readRLELength();
157 + }
158 + if (indexInPalette > paletteSize) {
159 + throw new Error('Too big index in palette: ' + indexInPalette + ', palette size: ' + paletteSize);
160 + }
161 + if (offset + length > tileSize) {
162 + throw new Error('Too big rle length in palette mode: ' + length + ', allowed length is: ' + (tileSize - offset));
163 + }
164 +
165 + for (let j = 0; j < length; j++) {
166 + data[offset * 4] = palette[indexInPalette * 4];
167 + data[offset * 4 + 1] = palette[indexInPalette * 4 + 1];
168 + data[offset * 4 + 2] = palette[indexInPalette * 4 + 2];
169 + data[offset * 4 + 3] = palette[indexInPalette * 4 + 3];
170 + offset++;
171 + }
172 + }
173 + return data;
174 + }
175 +
176 + _readRLELength() {
177 + let length = 0;
178 + let current = 0;
179 + do {
180 + current = this._inflator.inflate(1)[0];
181 + length += current;
182 + } while (current === 255);
183 + return length + 1;
184 + }
185 +}
public/novnc/core/des.js
+1 -1
@@ -81,7 +81,7 @@
81 const PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3,
82 25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39,
83 50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ],
84 - totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28];
84 + totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28];
85
86 const z = 0x0;
87 let a,b,c,d,e,f;
public/novnc/core/display.js
+12
@@ -224,6 +224,18 @@ export default class Display {
224 this.viewportChangePos(0, 0);
225 }
226
227 + getImageData() {
228 + return this._drawCtx.getImageData(0, 0, this.width, this.height);
229 + }
230 +
231 + toDataURL(type, encoderOptions) {
232 + return this._backbuffer.toDataURL(type, encoderOptions);
233 + }
234 +
235 + toBlob(callback, type, quality) {
236 + return this._backbuffer.toBlob(callback, type, quality);
237 + }
238 +
239 // Track what parts of the visible canvas that need updating
240 _damage(x, y, w, h) {
241 if (x < this._damageBounds.left) {
public/novnc/core/encodings.js
+4
@@ -12,7 +12,9 @@ export const encodings = {
12 encodingRRE: 2,
13 encodingHextile: 5,
14 encodingTight: 7,
15 + encodingZRLE: 16,
16 encodingTightPNG: -260,
17 + encodingJPEG: 21,
18
19 pseudoEncodingQualityLevel9: -23,
20 pseudoEncodingQualityLevel0: -32,
@@ -38,7 +40,9 @@ export function encodingName(num) {
40 case encodings.encodingRRE: return "RRE";
41 case encodings.encodingHextile: return "Hextile";
42 case encodings.encodingTight: return "Tight";
43 + case encodings.encodingZRLE: return "ZRLE";
44 case encodings.encodingTightPNG: return "TightPNG";
45 + case encodings.encodingJPEG: return "JPEG";
46 default: return "[unknown encoding " + num + "]";
47 }
48 }
public/novnc/core/input/keyboard.js
+10
@@ -153,6 +153,16 @@ export default class Keyboard {
153 keysym = this._keyDownList[code];
154 }
155
156 + // macOS doesn't send proper key releases if a key is pressed
157 + // while meta is held down
158 + if ((browser.isMac() || browser.isIOS()) &&
159 + (e.metaKey && code !== 'MetaLeft' && code !== 'MetaRight')) {
160 + this._sendKeyEvent(keysym, code, true);
161 + this._sendKeyEvent(keysym, code, false);
162 + stopEvent(e);
163 + return;
164 + }
165 +
166 // macOS doesn't send proper key events for modifiers, only
167 // state change events. That gets extra confusing for CapsLock
168 // which toggles on each press, but not on release. So pretend
public/novnc/core/ra2.js new
+567
@@ -0,0 +1,567 @@
1 +import Base64 from './base64.js';
2 +import { encodeUTF8 } from './util/strings.js';
3 +import EventTargetMixin from './util/eventtarget.js';
4 +
5 +export class AESEAXCipher {
6 + constructor() {
7 + this._rawKey = null;
8 + this._ctrKey = null;
9 + this._cbcKey = null;
10 + this._zeroBlock = new Uint8Array(16);
11 + this._prefixBlock0 = this._zeroBlock;
12 + this._prefixBlock1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
13 + this._prefixBlock2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]);
14 + }
15 +
16 + async _encryptBlock(block) {
17 + const encrypted = await window.crypto.subtle.encrypt({
18 + name: "AES-CBC",
19 + iv: this._zeroBlock,
20 + }, this._cbcKey, block);
21 + return new Uint8Array(encrypted).slice(0, 16);
22 + }
23 +
24 + async _initCMAC() {
25 + const k1 = await this._encryptBlock(this._zeroBlock);
26 + const k2 = new Uint8Array(16);
27 + const v = k1[0] >>> 6;
28 + for (let i = 0; i < 15; i++) {
29 + k2[i] = (k1[i + 1] >> 6) | (k1[i] << 2);
30 + k1[i] = (k1[i + 1] >> 7) | (k1[i] << 1);
31 + }
32 + const lut = [0x0, 0x87, 0x0e, 0x89];
33 + k2[14] ^= v >>> 1;
34 + k2[15] = (k1[15] << 2) ^ lut[v];
35 + k1[15] = (k1[15] << 1) ^ lut[v >> 1];
36 + this._k1 = k1;
37 + this._k2 = k2;
38 + }
39 +
40 + async _encryptCTR(data, counter) {
41 + const encrypted = await window.crypto.subtle.encrypt({
42 + "name": "AES-CTR",
43 + counter: counter,
44 + length: 128
45 + }, this._ctrKey, data);
46 + return new Uint8Array(encrypted);
47 + }
48 +
49 + async _decryptCTR(data, counter) {
50 + const decrypted = await window.crypto.subtle.decrypt({
51 + "name": "AES-CTR",
52 + counter: counter,
53 + length: 128
54 + }, this._ctrKey, data);
55 + return new Uint8Array(decrypted);
56 + }
57 +
58 + async _computeCMAC(data, prefixBlock) {
59 + if (prefixBlock.length !== 16) {
60 + return null;
61 + }
62 + const n = Math.floor(data.length / 16);
63 + const m = Math.ceil(data.length / 16);
64 + const r = data.length - n * 16;
65 + const cbcData = new Uint8Array((m + 1) * 16);
66 + cbcData.set(prefixBlock);
67 + cbcData.set(data, 16);
68 + if (r === 0) {
69 + for (let i = 0; i < 16; i++) {
70 + cbcData[n * 16 + i] ^= this._k1[i];
71 + }
72 + } else {
73 + cbcData[(n + 1) * 16 + r] = 0x80;
74 + for (let i = 0; i < 16; i++) {
75 + cbcData[(n + 1) * 16 + i] ^= this._k2[i];
76 + }
77 + }
78 + let cbcEncrypted = await window.crypto.subtle.encrypt({
79 + name: "AES-CBC",
80 + iv: this._zeroBlock,
81 + }, this._cbcKey, cbcData);
82 +
83 + cbcEncrypted = new Uint8Array(cbcEncrypted);
84 + const mac = cbcEncrypted.slice(cbcEncrypted.length - 32, cbcEncrypted.length - 16);
85 + return mac;
86 + }
87 +
88 + async setKey(key) {
89 + this._rawKey = key;
90 + this._ctrKey = await window.crypto.subtle.importKey(
91 + "raw", key, {"name": "AES-CTR"}, false, ["encrypt", "decrypt"]);
92 + this._cbcKey = await window.crypto.subtle.importKey(
93 + "raw", key, {"name": "AES-CBC"}, false, ["encrypt", "decrypt"]);
94 + await this._initCMAC();
95 + }
96 +
97 + async encrypt(message, associatedData, nonce) {
98 + const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
99 + const encrypted = await this._encryptCTR(message, nCMAC);
100 + const adCMAC = await this._computeCMAC(associatedData, this._prefixBlock1);
101 + const mac = await this._computeCMAC(encrypted, this._prefixBlock2);
102 + for (let i = 0; i < 16; i++) {
103 + mac[i] ^= nCMAC[i] ^ adCMAC[i];
104 + }
105 + const res = new Uint8Array(16 + encrypted.length);
106 + res.set(encrypted);
107 + res.set(mac, encrypted.length);
108 + return res;
109 + }
110 +
111 + async decrypt(encrypted, associatedData, nonce, mac) {
112 + const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
113 + const adCMAC = await this._computeCMAC(associatedData, this._prefixBlock1);
114 + const computedMac = await this._computeCMAC(encrypted, this._prefixBlock2);
115 + for (let i = 0; i < 16; i++) {
116 + computedMac[i] ^= nCMAC[i] ^ adCMAC[i];
117 + }
118 + if (computedMac.length !== mac.length) {
119 + return null;
120 + }
121 + for (let i = 0; i < mac.length; i++) {
122 + if (computedMac[i] !== mac[i]) {
123 + return null;
124 + }
125 + }
126 + const res = await this._decryptCTR(encrypted, nCMAC);
127 + return res;
128 + }
129 +}
130 +
131 +export class RA2Cipher {
132 + constructor() {
133 + this._cipher = new AESEAXCipher();
134 + this._counter = new Uint8Array(16);
135 + }
136 +
137 + async setKey(key) {
138 + await this._cipher.setKey(key);
139 + }
140 +
141 + async makeMessage(message) {
142 + const ad = new Uint8Array([(message.length & 0xff00) >>> 8, message.length & 0xff]);
143 + const encrypted = await this._cipher.encrypt(message, ad, this._counter);
144 + for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
145 + const res = new Uint8Array(message.length + 2 + 16);
146 + res.set(ad);
147 + res.set(encrypted, 2);
148 + return res;
149 + }
150 +
151 + async receiveMessage(length, encrypted, mac) {
152 + const ad = new Uint8Array([(length & 0xff00) >>> 8, length & 0xff]);
153 + const res = await this._cipher.decrypt(encrypted, ad, this._counter, mac);
154 + for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
155 + return res;
156 + }
157 +}
158 +
159 +export class RSACipher {
160 + constructor(keyLength) {
161 + this._key = null;
162 + this._keyLength = keyLength;
163 + this._keyBytes = Math.ceil(keyLength / 8);
164 + this._n = null;
165 + this._e = null;
166 + this._d = null;
167 + this._nBigInt = null;
168 + this._eBigInt = null;
169 + this._dBigInt = null;
170 + }
171 +
172 + _base64urlDecode(data) {
173 + data = data.replace(/-/g, "+").replace(/_/g, "/");
174 + data = data.padEnd(Math.ceil(data.length / 4) * 4, "=");
175 + return Base64.decode(data);
176 + }
177 +
178 + _u8ArrayToBigInt(arr) {
179 + let hex = '0x';
180 + for (let i = 0; i < arr.length; i++) {
181 + hex += arr[i].toString(16).padStart(2, '0');
182 + }
183 + return BigInt(hex);
184 + }
185 +
186 + _padArray(arr, length) {
187 + const res = new Uint8Array(length);
188 + res.set(arr, length - arr.length);
189 + return res;
190 + }
191 +
192 + _bigIntToU8Array(bigint, padLength=0) {
193 + let hex = bigint.toString(16);
194 + if (padLength === 0) {
195 + padLength = Math.ceil(hex.length / 2) * 2;
196 + }
197 + hex = hex.padStart(padLength * 2, '0');
198 + const length = hex.length / 2;
199 + const arr = new Uint8Array(length);
200 + for (let i = 0; i < length; i++) {
201 + arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
202 + }
203 + return arr;
204 + }
205 +
206 + _modPow(b, e, m) {
207 + if (m === 1n) {
208 + return 0;
209 + }
210 + let r = 1n;
211 + b = b % m;
212 + while (e > 0) {
213 + if (e % 2n === 1n) {
214 + r = (r * b) % m;
215 + }
216 + e = e / 2n;
217 + b = (b * b) % m;
218 + }
219 + return r;
220 + }
221 +
222 + async generateKey() {
223 + this._key = await window.crypto.subtle.generateKey(
224 + {
225 + name: "RSA-OAEP",
226 + modulusLength: this._keyLength,
227 + publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
228 + hash: {name: "SHA-256"},
229 + },
230 + true, ["encrypt", "decrypt"]);
231 + const privateKey = await window.crypto.subtle.exportKey("jwk", this._key.privateKey);
232 + this._n = this._padArray(this._base64urlDecode(privateKey.n), this._keyBytes);
233 + this._nBigInt = this._u8ArrayToBigInt(this._n);
234 + this._e = this._padArray(this._base64urlDecode(privateKey.e), this._keyBytes);
235 + this._eBigInt = this._u8ArrayToBigInt(this._e);
236 + this._d = this._padArray(this._base64urlDecode(privateKey.d), this._keyBytes);
237 + this._dBigInt = this._u8ArrayToBigInt(this._d);
238 + }
239 +
240 + setPublicKey(n, e) {
241 + if (n.length !== this._keyBytes || e.length !== this._keyBytes) {
242 + return;
243 + }
244 + this._n = new Uint8Array(this._keyBytes);
245 + this._e = new Uint8Array(this._keyBytes);
246 + this._n.set(n);
247 + this._e.set(e);
248 + this._nBigInt = this._u8ArrayToBigInt(this._n);
249 + this._eBigInt = this._u8ArrayToBigInt(this._e);
250 + }
251 +
252 + encrypt(message) {
253 + if (message.length > this._keyBytes - 11) {
254 + return null;
255 + }
256 + const ps = new Uint8Array(this._keyBytes - message.length - 3);
257 + window.crypto.getRandomValues(ps);
258 + for (let i = 0; i < ps.length; i++) {
259 + ps[i] = Math.floor(ps[i] * 254 / 255 + 1);
260 + }
261 + const em = new Uint8Array(this._keyBytes);
262 + em[1] = 0x02;
263 + em.set(ps, 2);
264 + em.set(message, ps.length + 3);
265 + const emBigInt = this._u8ArrayToBigInt(em);
266 + const c = this._modPow(emBigInt, this._eBigInt, this._nBigInt);
267 + return this._bigIntToU8Array(c, this._keyBytes);
268 + }
269 +
270 + decrypt(message) {
271 + if (message.length !== this._keyBytes) {
272 + return null;
273 + }
274 + const msgBigInt = this._u8ArrayToBigInt(message);
275 + const emBigInt = this._modPow(msgBigInt, this._dBigInt, this._nBigInt);
276 + const em = this._bigIntToU8Array(emBigInt, this._keyBytes);
277 + if (em[0] !== 0x00 || em[1] !== 0x02) {
278 + return null;
279 + }
280 + let i = 2;
281 + for (; i < em.length; i++) {
282 + if (em[i] === 0x00) {
283 + break;
284 + }
285 + }
286 + if (i === em.length) {
287 + return null;
288 + }
289 + return em.slice(i + 1, em.length);
290 + }
291 +
292 + get keyLength() {
293 + return this._keyLength;
294 + }
295 +
296 + get n() {
297 + return this._n;
298 + }
299 +
300 + get e() {
301 + return this._e;
302 + }
303 +
304 + get d() {
305 + return this._d;
306 + }
307 +}
308 +
309 +export default class RSAAESAuthenticationState extends EventTargetMixin {
310 + constructor(sock, getCredentials) {
311 + super();
312 + this._hasStarted = false;
313 + this._checkSock = null;
314 + this._checkCredentials = null;
315 + this._approveServerResolve = null;
316 + this._sockReject = null;
317 + this._credentialsReject = null;
318 + this._approveServerReject = null;
319 + this._sock = sock;
320 + this._getCredentials = getCredentials;
321 + }
322 +
323 + _waitSockAsync(len) {
324 + return new Promise((resolve, reject) => {
325 + const hasData = () => !this._sock.rQwait('RA2', len);
326 + if (hasData()) {
327 + resolve();
328 + } else {
329 + this._checkSock = () => {
330 + if (hasData()) {
331 + resolve();
332 + this._checkSock = null;
333 + this._sockReject = null;
334 + }
335 + };
336 + this._sockReject = reject;
337 + }
338 + });
339 + }
340 +
341 + _waitApproveKeyAsync() {
342 + return new Promise((resolve, reject) => {
343 + this._approveServerResolve = resolve;
344 + this._approveServerReject = reject;
345 + });
346 + }
347 +
348 + _waitCredentialsAsync(subtype) {
349 + const hasCredentials = () => {
350 + if (subtype === 1 && this._getCredentials().username !== undefined &&
351 + this._getCredentials().password !== undefined) {
352 + return true;
353 + } else if (subtype === 2 && this._getCredentials().password !== undefined) {
354 + return true;
355 + }
356 + return false;
357 + };
358 + return new Promise((resolve, reject) => {
359 + if (hasCredentials()) {
360 + resolve();
361 + } else {
362 + this._checkCredentials = () => {
363 + if (hasCredentials()) {
364 + resolve();
365 + this._checkCredentials = null;
366 + this._credentialsReject = null;
367 + }
368 + };
369 + this._credentialsReject = reject;
370 + }
371 + });
372 + }
373 +
374 + checkInternalEvents() {
375 + if (this._checkSock !== null) {
376 + this._checkSock();
377 + }
378 + if (this._checkCredentials !== null) {
379 + this._checkCredentials();
380 + }
381 + }
382 +
383 + approveServer() {
384 + if (this._approveServerResolve !== null) {
385 + this._approveServerResolve();
386 + this._approveServerResolve = null;
387 + }
388 + }
389 +
390 + disconnect() {
391 + if (this._sockReject !== null) {
392 + this._sockReject(new Error("disconnect normally"));
393 + this._sockReject = null;
394 + }
395 + if (this._credentialsReject !== null) {
396 + this._credentialsReject(new Error("disconnect normally"));
397 + this._credentialsReject = null;
398 + }
399 + if (this._approveServerReject !== null) {
400 + this._approveServerReject(new Error("disconnect normally"));
401 + this._approveServerReject = null;
402 + }
403 + }
404 +
405 + async negotiateRA2neAuthAsync() {
406 + this._hasStarted = true;
407 + // 1: Receive server public key
408 + await this._waitSockAsync(4);
409 + const serverKeyLengthBuffer = this._sock.rQslice(0, 4);
410 + const serverKeyLength = this._sock.rQshift32();
411 + if (serverKeyLength < 1024) {
412 + throw new Error("RA2: server public key is too short: " + serverKeyLength);
413 + } else if (serverKeyLength > 8192) {
414 + throw new Error("RA2: server public key is too long: " + serverKeyLength);
415 + }
416 + const serverKeyBytes = Math.ceil(serverKeyLength / 8);
417 + await this._waitSockAsync(serverKeyBytes * 2);
418 + const serverN = this._sock.rQshiftBytes(serverKeyBytes);
419 + const serverE = this._sock.rQshiftBytes(serverKeyBytes);
420 + const serverRSACipher = new RSACipher(serverKeyLength);
421 + serverRSACipher.setPublicKey(serverN, serverE);
422 + const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2);
423 + serverPublickey.set(serverKeyLengthBuffer);
424 + serverPublickey.set(serverN, 4);
425 + serverPublickey.set(serverE, 4 + serverKeyBytes);
426 +
427 + // verify server public key
428 + this.dispatchEvent(new CustomEvent("serververification", {
429 + detail: { type: "RSA", publickey: serverPublickey }
430 + }));
431 + await this._waitApproveKeyAsync();
432 +
433 + // 2: Send client public key
434 + const clientKeyLength = 2048;
435 + const clientKeyBytes = Math.ceil(clientKeyLength / 8);
436 + const clientRSACipher = new RSACipher(clientKeyLength);
437 + await clientRSACipher.generateKey();
438 + const clientN = clientRSACipher.n;
439 + const clientE = clientRSACipher.e;
440 + const clientPublicKey = new Uint8Array(4 + clientKeyBytes * 2);
441 + clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24;
442 + clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16;
443 + clientPublicKey[2] = (clientKeyLength & 0xff00) >>> 8;
444 + clientPublicKey[3] = clientKeyLength & 0xff;
445 + clientPublicKey.set(clientN, 4);
446 + clientPublicKey.set(clientE, 4 + clientKeyBytes);
447 + this._sock.send(clientPublicKey);
448 +
449 + // 3: Send client random
450 + const clientRandom = new Uint8Array(16);
451 + window.crypto.getRandomValues(clientRandom);
452 + const clientEncryptedRandom = serverRSACipher.encrypt(clientRandom);
453 + const clientRandomMessage = new Uint8Array(2 + serverKeyBytes);
454 + clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8;
455 + clientRandomMessage[1] = serverKeyBytes & 0xff;
456 + clientRandomMessage.set(clientEncryptedRandom, 2);
457 + this._sock.send(clientRandomMessage);
458 +
459 + // 4: Receive server random
460 + await this._waitSockAsync(2);
461 + if (this._sock.rQshift16() !== clientKeyBytes) {
462 + throw new Error("RA2: wrong encrypted message length");
463 + }
464 + const serverEncryptedRandom = this._sock.rQshiftBytes(clientKeyBytes);
465 + const serverRandom = clientRSACipher.decrypt(serverEncryptedRandom);
466 + if (serverRandom === null || serverRandom.length !== 16) {
467 + throw new Error("RA2: corrupted server encrypted random");
468 + }
469 +
470 + // 5: Compute session keys and set ciphers
471 + let clientSessionKey = new Uint8Array(32);
472 + let serverSessionKey = new Uint8Array(32);
473 + clientSessionKey.set(serverRandom);
474 + clientSessionKey.set(clientRandom, 16);
475 + serverSessionKey.set(clientRandom);
476 + serverSessionKey.set(serverRandom, 16);
477 + clientSessionKey = await window.crypto.subtle.digest("SHA-1", clientSessionKey);
478 + clientSessionKey = new Uint8Array(clientSessionKey).slice(0, 16);
479 + serverSessionKey = await window.crypto.subtle.digest("SHA-1", serverSessionKey);
480 + serverSessionKey = new Uint8Array(serverSessionKey).slice(0, 16);
481 + const clientCipher = new RA2Cipher();
482 + await clientCipher.setKey(clientSessionKey);
483 + const serverCipher = new RA2Cipher();
484 + await serverCipher.setKey(serverSessionKey);
485 +
486 + // 6: Compute and exchange hashes
487 + let serverHash = new Uint8Array(8 + serverKeyBytes * 2 + clientKeyBytes * 2);
488 + let clientHash = new Uint8Array(8 + serverKeyBytes * 2 + clientKeyBytes * 2);
489 + serverHash.set(serverPublickey);
490 + serverHash.set(clientPublicKey, 4 + serverKeyBytes * 2);
491 + clientHash.set(clientPublicKey);
492 + clientHash.set(serverPublickey, 4 + clientKeyBytes * 2);
493 + serverHash = await window.crypto.subtle.digest("SHA-1", serverHash);
494 + clientHash = await window.crypto.subtle.digest("SHA-1", clientHash);
495 + serverHash = new Uint8Array(serverHash);
496 + clientHash = new Uint8Array(clientHash);
497 + this._sock.send(await clientCipher.makeMessage(clientHash));
498 + await this._waitSockAsync(2 + 20 + 16);
499 + if (this._sock.rQshift16() !== 20) {
500 + throw new Error("RA2: wrong server hash");
501 + }
502 + const serverHashReceived = await serverCipher.receiveMessage(
503 + 20, this._sock.rQshiftBytes(20), this._sock.rQshiftBytes(16));
504 + if (serverHashReceived === null) {
505 + throw new Error("RA2: failed to authenticate the message");
506 + }
507 + for (let i = 0; i < 20; i++) {
508 + if (serverHashReceived[i] !== serverHash[i]) {
509 + throw new Error("RA2: wrong server hash");
510 + }
511 + }
512 +
513 + // 7: Receive subtype
514 + await this._waitSockAsync(2 + 1 + 16);
515 + if (this._sock.rQshift16() !== 1) {
516 + throw new Error("RA2: wrong subtype");
517 + }
518 + let subtype = (await serverCipher.receiveMessage(
519 + 1, this._sock.rQshiftBytes(1), this._sock.rQshiftBytes(16)));
520 + if (subtype === null) {
521 + throw new Error("RA2: failed to authenticate the message");
522 + }
523 + subtype = subtype[0];
524 + if (subtype === 1) {
525 + if (this._getCredentials().username === undefined ||
526 + this._getCredentials().password === undefined) {
527 + this.dispatchEvent(new CustomEvent(
528 + "credentialsrequired",
529 + { detail: { types: ["username", "password"] } }));
530 + }
531 + } else if (subtype === 2) {
532 + if (this._getCredentials().password === undefined) {
533 + this.dispatchEvent(new CustomEvent(
534 + "credentialsrequired",
535 + { detail: { types: ["password"] } }));
536 + }
537 + } else {
538 + throw new Error("RA2: wrong subtype");
539 + }
540 + await this._waitCredentialsAsync(subtype);
541 + let username;
542 + if (subtype === 1) {
543 + username = encodeUTF8(this._getCredentials().username).slice(0, 255);
544 + } else {
545 + username = "";
546 + }
547 + const password = encodeUTF8(this._getCredentials().password).slice(0, 255);
548 + const credentials = new Uint8Array(username.length + password.length + 2);
549 + credentials[0] = username.length;
550 + credentials[username.length + 1] = password.length;
551 + for (let i = 0; i < username.length; i++) {
552 + credentials[i + 1] = username.charCodeAt(i);
553 + }
554 + for (let i = 0; i < password.length; i++) {
555 + credentials[username.length + 2 + i] = password.charCodeAt(i);
556 + }
557 + this._sock.send(await clientCipher.makeMessage(credentials));
558 + }
559 +
560 + get hasStarted() {
561 + return this._hasStarted;
562 + }
563 +
564 + set hasStarted(s) {
565 + this._hasStarted = s;
566 + }
567 +}
\ No newline at end of file
public/novnc/core/rfb.js
+469 -84
@@ -25,6 +25,8 @@ import DES from "./des.js";
25 import KeyTable from "./input/keysym.js";
26 import XtScancode from "./input/xtscancodes.js";
27 import { encodings } from "./encodings.js";
28 +import RSAAESAuthenticationState from "./ra2.js";
29 +import { MD5 } from "./util/md5.js";
30
31 import RawDecoder from "./decoders/raw.js";
32 import CopyRectDecoder from "./decoders/copyrect.js";
@@ -32,6 +34,8 @@ import RREDecoder from "./decoders/rre.js";
34 import HextileDecoder from "./decoders/hextile.js";
35 import TightDecoder from "./decoders/tight.js";
36 import TightPNGDecoder from "./decoders/tightpng.js";
37 +import ZRLEDecoder from "./decoders/zrle.js";
38 +import JPEGDecoder from "./decoders/jpeg.js";
39
40 // How many seconds to wait for a disconnect to finish
41 const DISCONNECT_TIMEOUT = 3;
@@ -50,6 +54,22 @@ const GESTURE_SCRLSENS = 50;
54 const DOUBLE_TAP_TIMEOUT = 1000;
55 const DOUBLE_TAP_THRESHOLD = 50;
56
57 +// Security types
58 +const securityTypeNone = 1;
59 +const securityTypeVNCAuth = 2;
60 +const securityTypeRA2ne = 6;
61 +const securityTypeTight = 16;
62 +const securityTypeVeNCrypt = 19;
63 +const securityTypeXVP = 22;
64 +const securityTypeARD = 30;
65 +const securityTypeMSLogonII = 113;
66 +
67 +// Special Tight security types
68 +const securityTypeUnixLogon = 129;
69 +
70 +// VeNCrypt security types
71 +const securityTypePlain = 256;
72 +
73 // Extended clipboard pseudo-encoding formats
74 const extendedClipboardFormatText = 1;
75 /*eslint-disable no-unused-vars */
@@ -75,6 +95,12 @@ export default class RFB extends EventTargetMixin {
95 throw new Error("Must specify URL, WebSocket or RTCDataChannel");
96 }
97
98 + // We rely on modern APIs which might not be available in an
99 + // insecure context
100 + if (!window.isSecureContext) {
101 + Log.Error("noVNC requires a secure context (TLS). Expect crashes!");
102 + }
103 +
104 super();
105
106 this._target = target;
@@ -98,6 +124,7 @@ export default class RFB extends EventTargetMixin {
124 this._rfbInitState = '';
125 this._rfbAuthScheme = -1;
126 this._rfbCleanDisconnect = true;
127 + this._rfbRSAAESAuthenticationState = null;
128
129 // Server capabilities
130 this._rfbVersion = 0;
@@ -176,6 +203,8 @@ export default class RFB extends EventTargetMixin {
203 handleMouse: this._handleMouse.bind(this),
204 handleWheel: this._handleWheel.bind(this),
205 handleGesture: this._handleGesture.bind(this),
206 + handleRSAAESCredentialsRequired: this._handleRSAAESCredentialsRequired.bind(this),
207 + handleRSAAESServerVerification: this._handleRSAAESServerVerification.bind(this),
208 };
209
210 // main setup
@@ -218,6 +247,8 @@ export default class RFB extends EventTargetMixin {
247 this._decoders[encodings.encodingHextile] = new HextileDecoder();
248 this._decoders[encodings.encodingTight] = new TightDecoder();
249 this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder();
250 + this._decoders[encodings.encodingZRLE] = new ZRLEDecoder();
251 + this._decoders[encodings.encodingJPEG] = new JPEGDecoder();
252
253 // NB: nothing that needs explicit teardown should be done
254 // before this point, since this can throw an exception
@@ -240,6 +271,8 @@ export default class RFB extends EventTargetMixin {
271 this._sock.on('message', this._handleMessage.bind(this));
272 this._sock.on('error', this._socketError.bind(this));
273
274 + this._expectedClientWidth = null;
275 + this._expectedClientHeight = null;
276 this._resizeObserver = new ResizeObserver(this._eventHandlers.handleResize);
277
278 // All prepared, kick off the connection
@@ -254,6 +287,7 @@ export default class RFB extends EventTargetMixin {
287
288 this._viewOnly = false;
289 this._clipViewport = false;
290 + this._clippingViewport = false;
291 this._scaleViewport = false;
292 this._resizeSession = false;
293
@@ -285,6 +319,16 @@ export default class RFB extends EventTargetMixin {
319
320 get capabilities() { return this._capabilities; }
321
322 + get clippingViewport() { return this._clippingViewport; }
323 + _setClippingViewport(on) {
324 + if (on === this._clippingViewport) {
325 + return;
326 + }
327 + this._clippingViewport = on;
328 + this.dispatchEvent(new CustomEvent("clippingviewport",
329 + { detail: this._clippingViewport }));
330 + }
331 +
332 get touchButton() { return 0; }
333 set touchButton(button) { Log.Warn("Using old API!"); }
334
@@ -372,11 +416,20 @@ export default class RFB extends EventTargetMixin {
416 this._sock.off('error');
417 this._sock.off('message');
418 this._sock.off('open');
419 + if (this._rfbRSAAESAuthenticationState !== null) {
420 + this._rfbRSAAESAuthenticationState.disconnect();
421 + }
422 + }
423 +
424 + approveServer() {
425 + if (this._rfbRSAAESAuthenticationState !== null) {
426 + this._rfbRSAAESAuthenticationState.approveServer();
427 + }
428 }
429
430 sendCredentials(creds) {
431 this._rfbCredentials = creds;
379 - setTimeout(this._initMsg.bind(this), 0);
432 + this._resumeAuthentication();
433 }
434
435 sendCtrlAltDel() {
@@ -432,8 +485,8 @@ export default class RFB extends EventTargetMixin {
485 }
486 }
487
435 - focus() {
436 - this._canvas.focus();
488 + focus(options) {
489 + this._canvas.focus(options);
490 }
491
492 blur() {
@@ -449,16 +502,45 @@ export default class RFB extends EventTargetMixin {
502 this._clipboardText = text;
503 RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);
504 } else {
452 - let data = new Uint8Array(text.length);
453 - for (let i = 0; i < text.length; i++) {
454 - // FIXME: text can have values outside of Latin1/Uint8
455 - data[i] = text.charCodeAt(i);
505 + let length, i;
506 + let data;
507 +
508 + length = 0;
509 + // eslint-disable-next-line no-unused-vars
510 + for (let codePoint of text) {
511 + length++;
512 + }
513 +
514 + data = new Uint8Array(length);
515 +
516 + i = 0;
517 + for (let codePoint of text) {
518 + let code = codePoint.codePointAt(0);
519 +
520 + /* Only ISO 8859-1 is supported */
521 + if (code > 0xff) {
522 + code = 0x3f; // '?'
523 + }
524 +
525 + data[i++] = code;
526 }
527
528 RFB.messages.clientCutText(this._sock, data);
529 }
530 }
531
532 + getImageData() {
533 + return this._display.getImageData();
534 + }
535 +
536 + toDataURL(type, encoderOptions) {
537 + return this._display.toDataURL(type, encoderOptions);
538 + }
539 +
540 + toBlob(callback, type, quality) {
541 + return this._display.toBlob(callback, type, quality);
542 + }
543 +
544 // ===== PRIVATE METHODS =====
545
546 _connect() {
@@ -609,7 +691,7 @@ export default class RFB extends EventTargetMixin {
691 return;
692 }
693
612 - this.focus();
694 + this.focus({ preventScroll: true });
695 }
696
697 _setDesktopName(name) {
@@ -619,7 +701,26 @@ export default class RFB extends EventTargetMixin {
701 { detail: { name: this._fbName } }));
702 }
703
704 + _saveExpectedClientSize() {
705 + this._expectedClientWidth = this._screen.clientWidth;
706 + this._expectedClientHeight = this._screen.clientHeight;
707 + }
708 +
709 + _currentClientSize() {
710 + return [this._screen.clientWidth, this._screen.clientHeight];
711 + }
712 +
713 + _clientHasExpectedSize() {
714 + const [currentWidth, currentHeight] = this._currentClientSize();
715 + return currentWidth == this._expectedClientWidth &&
716 + currentHeight == this._expectedClientHeight;
717 + }
718 +
719 _handleResize() {
720 + // Don't change anything if the client size is already as expected
721 + if (this._clientHasExpectedSize()) {
722 + return;
723 + }
724 // If the window resized then our screen element might have
725 // as well. Update the viewport dimensions.
726 window.requestAnimationFrame(() => {
@@ -659,6 +760,16 @@ export default class RFB extends EventTargetMixin {
760 const size = this._screenSize();
761 this._display.viewportChangeSize(size.w, size.h);
762 this._fixScrollbars();
763 + this._setClippingViewport(size.w < this._display.width ||
764 + size.h < this._display.height);
765 + } else {
766 + this._setClippingViewport(false);
767 + }
768 +
769 + // When changing clipping we might show or hide scrollbars.
770 + // This causes the expected client dimensions to change.
771 + if (curClip !== newClip) {
772 + this._saveExpectedClientSize();
773 }
774 }
775
@@ -684,6 +795,7 @@ export default class RFB extends EventTargetMixin {
795 }
796
797 const size = this._screenSize();
798 +
799 RFB.messages.setDesktopSize(this._sock,
800 Math.floor(size.w), Math.floor(size.h),
801 this._screenID, this._screenFlags);
@@ -699,12 +811,13 @@ export default class RFB extends EventTargetMixin {
811 }
812
813 _fixScrollbars() {
702 - // This is a hack because Chrome screws up the calculation
703 - // for when scrollbars are needed. So to fix it we temporarily
704 - // toggle them off and on.
814 + // This is a hack because Safari on macOS screws up the calculation
815 + // for when scrollbars are needed. We get scrollbars when making the
816 + // browser smaller, despite remote resize being enabled. So to fix it
817 + // we temporarily toggle them off and on.
818 const orig = this._screen.style.overflow;
819 this._screen.style.overflow = 'hidden';
707 - // Force Chrome to recalculate the layout by asking for
820 + // Force Safari to recalculate the layout by asking for
821 // an element's dimensions
822 this._screen.getBoundingClientRect();
823 this._screen.style.overflow = orig;
@@ -869,8 +982,15 @@ export default class RFB extends EventTargetMixin {
982 }
983 }
984 break;
985 + case 'connecting':
986 + while (this._rfbConnectionState === 'connecting') {
987 + if (!this._initMsg()) {
988 + break;
989 + }
990 + }
991 + break;
992 default:
873 - this._initMsg();
993 + Log.Error("Got data while in an invalid state");
994 break;
995 }
996 }
@@ -1242,13 +1362,13 @@ export default class RFB extends EventTargetMixin {
1362 break;
1363 case "003.003":
1364 case "003.006": // UltraVNC
1245 - case "003.889": // Apple Remote Desktop
1365 this._rfbVersion = 3.3;
1366 break;
1367 case "003.007":
1368 this._rfbVersion = 3.7;
1369 break;
1370 case "003.008":
1371 + case "003.889": // Apple Remote Desktop
1372 case "004.000": // Intel AMT KVM
1373 case "004.001": // RealVNC 4.6
1374 case "005.000": // RealVNC 5.3
@@ -1279,6 +1399,22 @@ export default class RFB extends EventTargetMixin {
1399 this._rfbInitState = 'Security';
1400 }
1401
1402 + _isSupportedSecurityType(type) {
1403 + const clientTypes = [
1404 + securityTypeNone,
1405 + securityTypeVNCAuth,
1406 + securityTypeRA2ne,
1407 + securityTypeTight,
1408 + securityTypeVeNCrypt,
1409 + securityTypeXVP,
1410 + securityTypeARD,
1411 + securityTypeMSLogonII,
1412 + securityTypePlain,
1413 + ];
1414 +
1415 + return clientTypes.includes(type);
1416 + }
1417 +
1418 _negotiateSecurity() {
1419 if (this._rfbVersion >= 3.7) {
1420 // Server sends supported list, client decides
@@ -1289,24 +1425,23 @@ export default class RFB extends EventTargetMixin {
1425 this._rfbInitState = "SecurityReason";
1426 this._securityContext = "no security types";
1427 this._securityStatus = 1;
1292 - return this._initMsg();
1428 + return true;
1429 }
1430
1431 const types = this._sock.rQshiftBytes(numTypes);
1432 Log.Debug("Server security types: " + types);
1433
1298 - // Look for each auth in preferred order
1299 - if (types.includes(1)) {
1300 - this._rfbAuthScheme = 1; // None
1301 - } else if (types.includes(22)) {
1302 - this._rfbAuthScheme = 22; // XVP
1303 - } else if (types.includes(16)) {
1304 - this._rfbAuthScheme = 16; // Tight
1305 - } else if (types.includes(2)) {
1306 - this._rfbAuthScheme = 2; // VNC Auth
1307 - } else if (types.includes(19)) {
1308 - this._rfbAuthScheme = 19; // VeNCrypt Auth
1309 - } else {
1434 + // Look for a matching security type in the order that the
1435 + // server prefers
1436 + this._rfbAuthScheme = -1;
1437 + for (let type of types) {
1438 + if (this._isSupportedSecurityType(type)) {
1439 + this._rfbAuthScheme = type;
1440 + break;
1441 + }
1442 + }
1443 +
1444 + if (this._rfbAuthScheme === -1) {
1445 return this._fail("Unsupported security types (types: " + types + ")");
1446 }
1447
@@ -1320,14 +1455,14 @@ export default class RFB extends EventTargetMixin {
1455 this._rfbInitState = "SecurityReason";
1456 this._securityContext = "authentication scheme";
1457 this._securityStatus = 1;
1323 - return this._initMsg();
1458 + return true;
1459 }
1460 }
1461
1462 this._rfbInitState = 'Authentication';
1463 Log.Debug('Authenticating using scheme: ' + this._rfbAuthScheme);
1464
1330 - return this._initMsg(); // jump to authentication
1465 + return true;
1466 }
1467
1468 _handleSecurityReason() {
@@ -1377,7 +1512,7 @@ export default class RFB extends EventTargetMixin {
1512 this._rfbCredentials.username +
1513 this._rfbCredentials.target;
1514 this._sock.sendString(xvpAuthStr);
1380 - this._rfbAuthScheme = 2;
1515 + this._rfbAuthScheme = securityTypeVNCAuth;
1516 return this._negotiateAuthentication();
1517 }
1518
@@ -1435,49 +1570,66 @@ export default class RFB extends EventTargetMixin {
1570 subtypes.push(this._sock.rQshift32());
1571 }
1572
1438 - // 256 = Plain subtype
1439 - if (subtypes.indexOf(256) != -1) {
1440 - // 0x100 = 256
1441 - this._sock.send([0, 0, 1, 0]);
1442 - this._rfbVeNCryptState = 4;
1443 - } else {
1444 - return this._fail("VeNCrypt Plain subtype not offered by server");
1573 + // Look for a matching security type in the order that the
1574 + // server prefers
1575 + this._rfbAuthScheme = -1;
1576 + for (let type of subtypes) {
1577 + // Avoid getting in to a loop
1578 + if (type === securityTypeVeNCrypt) {
1579 + continue;
1580 + }
1581 +
1582 + if (this._isSupportedSecurityType(type)) {
1583 + this._rfbAuthScheme = type;
1584 + break;
1585 + }
1586 }
1446 - }
1587
1448 - // negotiated Plain subtype, server waits for password
1449 - if (this._rfbVeNCryptState == 4) {
1450 - if (this._rfbCredentials.username === undefined ||
1451 - this._rfbCredentials.password === undefined) {
1452 - this.dispatchEvent(new CustomEvent(
1453 - "credentialsrequired",
1454 - { detail: { types: ["username", "password"] } }));
1455 - return false;
1588 + if (this._rfbAuthScheme === -1) {
1589 + return this._fail("Unsupported security types (types: " + subtypes + ")");
1590 }
1591
1458 - const user = encodeUTF8(this._rfbCredentials.username);
1459 - const pass = encodeUTF8(this._rfbCredentials.password);
1460 -
1461 - this._sock.send([
1462 - (user.length >> 24) & 0xFF,
1463 - (user.length >> 16) & 0xFF,
1464 - (user.length >> 8) & 0xFF,
1465 - user.length & 0xFF
1466 - ]);
1467 - this._sock.send([
1468 - (pass.length >> 24) & 0xFF,
1469 - (pass.length >> 16) & 0xFF,
1470 - (pass.length >> 8) & 0xFF,
1471 - pass.length & 0xFF
1472 - ]);
1473 - this._sock.sendString(user);
1474 - this._sock.sendString(pass);
1592 + this._sock.send([this._rfbAuthScheme >> 24,
1593 + this._rfbAuthScheme >> 16,
1594 + this._rfbAuthScheme >> 8,
1595 + this._rfbAuthScheme]);
1596
1476 - this._rfbInitState = "SecurityResult";
1597 + this._rfbVeNCryptState == 4;
1598 return true;
1599 }
1600 }
1601
1602 + _negotiatePlainAuth() {
1603 + if (this._rfbCredentials.username === undefined ||
1604 + this._rfbCredentials.password === undefined) {
1605 + this.dispatchEvent(new CustomEvent(
1606 + "credentialsrequired",
1607 + { detail: { types: ["username", "password"] } }));
1608 + return false;
1609 + }
1610 +
1611 + const user = encodeUTF8(this._rfbCredentials.username);
1612 + const pass = encodeUTF8(this._rfbCredentials.password);
1613 +
1614 + this._sock.send([
1615 + (user.length >> 24) & 0xFF,
1616 + (user.length >> 16) & 0xFF,
1617 + (user.length >> 8) & 0xFF,
1618 + user.length & 0xFF
1619 + ]);
1620 + this._sock.send([
1621 + (pass.length >> 24) & 0xFF,
1622 + (pass.length >> 16) & 0xFF,
1623 + (pass.length >> 8) & 0xFF,
1624 + pass.length & 0xFF
1625 + ]);
1626 + this._sock.sendString(user);
1627 + this._sock.sendString(pass);
1628 +
1629 + this._rfbInitState = "SecurityResult";
1630 + return true;
1631 + }
1632 +
1633 _negotiateStdVNCAuth() {
1634 if (this._sock.rQwait("auth challenge", 16)) { return false; }
1635
@@ -1496,6 +1648,117 @@ export default class RFB extends EventTargetMixin {
1648 return true;
1649 }
1650
1651 + _negotiateARDAuth() {
1652 +
1653 + if (this._rfbCredentials.username === undefined ||
1654 + this._rfbCredentials.password === undefined) {
1655 + this.dispatchEvent(new CustomEvent(
1656 + "credentialsrequired",
1657 + { detail: { types: ["username", "password"] } }));
1658 + return false;
1659 + }
1660 +
1661 + if (this._rfbCredentials.ardPublicKey != undefined &&
1662 + this._rfbCredentials.ardCredentials != undefined) {
1663 + // if the async web crypto is done return the results
1664 + this._sock.send(this._rfbCredentials.ardCredentials);
1665 + this._sock.send(this._rfbCredentials.ardPublicKey);
1666 + this._rfbCredentials.ardCredentials = null;
1667 + this._rfbCredentials.ardPublicKey = null;
1668 + this._rfbInitState = "SecurityResult";
1669 + return true;
1670 + }
1671 +
1672 + if (this._sock.rQwait("read ard", 4)) { return false; }
1673 +
1674 + let generator = this._sock.rQshiftBytes(2); // DH base generator value
1675 +
1676 + let keyLength = this._sock.rQshift16();
1677 +
1678 + if (this._sock.rQwait("read ard keylength", keyLength*2, 4)) { return false; }
1679 +
1680 + // read the server values
1681 + let prime = this._sock.rQshiftBytes(keyLength); // predetermined prime modulus
1682 + let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key
1683 +
1684 + let clientPrivateKey = window.crypto.getRandomValues(new Uint8Array(keyLength));
1685 + let padding = Array.from(window.crypto.getRandomValues(new Uint8Array(64)), byte => String.fromCharCode(65+byte%26)).join('');
1686 +
1687 + this._negotiateARDAuthAsync(generator, keyLength, prime, serverPublicKey, clientPrivateKey, padding);
1688 +
1689 + return false;
1690 + }
1691 +
1692 + _modPow(base, exponent, modulus) {
1693 +
1694 + let baseHex = "0x"+Array.from(base, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
1695 + let exponentHex = "0x"+Array.from(exponent, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
1696 + let modulusHex = "0x"+Array.from(modulus, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
1697 +
1698 + let b = BigInt(baseHex);
1699 + let e = BigInt(exponentHex);
1700 + let m = BigInt(modulusHex);
1701 + let r = 1n;
1702 + b = b % m;
1703 + while (e > 0) {
1704 + if (e % 2n === 1n) {
1705 + r = (r * b) % m;
1706 + }
1707 + e = e / 2n;
1708 + b = (b * b) % m;
1709 + }
1710 + let hexResult = r.toString(16);
1711 +
1712 + while (hexResult.length/2<exponent.length || (hexResult.length%2 != 0)) {
1713 + hexResult = "0"+hexResult;
1714 + }
1715 +
1716 + let bytesResult = [];
1717 + for (let c = 0; c < hexResult.length; c += 2) {
1718 + bytesResult.push(parseInt(hexResult.substr(c, 2), 16));
1719 + }
1720 + return bytesResult;
1721 + }
1722 +
1723 + async _aesEcbEncrypt(string, key) {
1724 + // perform AES-ECB blocks
1725 + let keyString = Array.from(key, byte => String.fromCharCode(byte)).join('');
1726 + let aesKey = await window.crypto.subtle.importKey("raw", MD5(keyString), {name: "AES-CBC"}, false, ["encrypt"]);
1727 + let data = new Uint8Array(string.length);
1728 + for (let i = 0; i < string.length; ++i) {
1729 + data[i] = string.charCodeAt(i);
1730 + }
1731 + let encrypted = new Uint8Array(data.length);
1732 + for (let i=0;i<data.length;i+=16) {
1733 + let block = data.slice(i, i+16);
1734 + let encryptedBlock = await window.crypto.subtle.encrypt({name: "AES-CBC", iv: block},
1735 + aesKey, new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
1736 + );
1737 + encrypted.set((new Uint8Array(encryptedBlock)).slice(0, 16), i);
1738 + }
1739 + return encrypted;
1740 + }
1741 +
1742 + async _negotiateARDAuthAsync(generator, keyLength, prime, serverPublicKey, clientPrivateKey, padding) {
1743 + // calculate the DH keys
1744 + let clientPublicKey = this._modPow(generator, clientPrivateKey, prime);
1745 + let sharedKey = this._modPow(serverPublicKey, clientPrivateKey, prime);
1746 +
1747 + let username = encodeUTF8(this._rfbCredentials.username).substring(0, 63);
1748 + let password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
1749 +
1750 + let paddedUsername = username + '\0' + padding.substring(0, 63);
1751 + let paddedPassword = password + '\0' + padding.substring(0, 63);
1752 + let credentials = paddedUsername.substring(0, 64) + paddedPassword.substring(0, 64);
1753 +
1754 + let encrypted = await this._aesEcbEncrypt(credentials, sharedKey);
1755 +
1756 + this._rfbCredentials.ardCredentials = encrypted;
1757 + this._rfbCredentials.ardPublicKey = clientPublicKey;
1758 +
1759 + this._resumeAuthentication();
1760 + }
1761 +
1762 _negotiateTightUnixAuth() {
1763 if (this._rfbCredentials.username === undefined ||
1764 this._rfbCredentials.password === undefined) {
@@ -1603,12 +1866,12 @@ export default class RFB extends EventTargetMixin {
1866 case 'STDVNOAUTH__': // no auth
1867 this._rfbInitState = 'SecurityResult';
1868 return true;
1606 - case 'STDVVNCAUTH_': // VNC auth
1607 - this._rfbAuthScheme = 2;
1608 - return this._initMsg();
1609 - case 'TGHTULGNAUTH': // UNIX auth
1610 - this._rfbAuthScheme = 129;
1611 - return this._initMsg();
1869 + case 'STDVVNCAUTH_':
1870 + this._rfbAuthScheme = securityTypeVNCAuth;
1871 + return true;
1872 + case 'TGHTULGNAUTH':
1873 + this._rfbAuthScheme = securityTypeUnixLogon;
1874 + return true;
1875 default:
1876 return this._fail("Unsupported tiny auth scheme " +
1877 "(scheme: " + authType + ")");
@@ -1619,31 +1882,133 @@ export default class RFB extends EventTargetMixin {
1882 return this._fail("No supported sub-auth types!");
1883 }
1884
1885 + _handleRSAAESCredentialsRequired(event) {
1886 + this.dispatchEvent(event);
1887 + }
1888 +
1889 + _handleRSAAESServerVerification(event) {
1890 + this.dispatchEvent(event);
1891 + }
1892 +
1893 + _negotiateRA2neAuth() {
1894 + if (this._rfbRSAAESAuthenticationState === null) {
1895 + this._rfbRSAAESAuthenticationState = new RSAAESAuthenticationState(this._sock, () => this._rfbCredentials);
1896 + this._rfbRSAAESAuthenticationState.addEventListener(
1897 + "serververification", this._eventHandlers.handleRSAAESServerVerification);
1898 + this._rfbRSAAESAuthenticationState.addEventListener(
1899 + "credentialsrequired", this._eventHandlers.handleRSAAESCredentialsRequired);
1900 + }
1901 + this._rfbRSAAESAuthenticationState.checkInternalEvents();
1902 + if (!this._rfbRSAAESAuthenticationState.hasStarted) {
1903 + this._rfbRSAAESAuthenticationState.negotiateRA2neAuthAsync()
1904 + .catch((e) => {
1905 + if (e.message !== "disconnect normally") {
1906 + this._fail(e.message);
1907 + }
1908 + }).then(() => {
1909 + this.dispatchEvent(new CustomEvent('securityresult'));
1910 + this._rfbInitState = "SecurityResult";
1911 + return true;
1912 + }).finally(() => {
1913 + this._rfbRSAAESAuthenticationState.removeEventListener(
1914 + "serververification", this._eventHandlers.handleRSAAESServerVerification);
1915 + this._rfbRSAAESAuthenticationState.removeEventListener(
1916 + "credentialsrequired", this._eventHandlers.handleRSAAESCredentialsRequired);
1917 + this._rfbRSAAESAuthenticationState = null;
1918 + });
1919 + }
1920 + return false;
1921 + }
1922 +
1923 + _negotiateMSLogonIIAuth() {
1924 + if (this._sock.rQwait("mslogonii dh param", 24)) { return false; }
1925 +
1926 + if (this._rfbCredentials.username === undefined ||
1927 + this._rfbCredentials.password === undefined) {
1928 + this.dispatchEvent(new CustomEvent(
1929 + "credentialsrequired",
1930 + { detail: { types: ["username", "password"] } }));
1931 + return false;
1932 + }
1933 +
1934 + const g = this._sock.rQshiftBytes(8);
1935 + const p = this._sock.rQshiftBytes(8);
1936 + const A = this._sock.rQshiftBytes(8);
1937 + const b = window.crypto.getRandomValues(new Uint8Array(8));
1938 + const B = new Uint8Array(this._modPow(g, b, p));
1939 + const secret = new Uint8Array(this._modPow(A, b, p));
1940 +
1941 + const des = new DES(secret);
1942 + const username = encodeUTF8(this._rfbCredentials.username).substring(0, 255);
1943 + const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
1944 + const usernameBytes = new Uint8Array(256);
1945 + const passwordBytes = new Uint8Array(64);
1946 + window.crypto.getRandomValues(usernameBytes);
1947 + window.crypto.getRandomValues(passwordBytes);
1948 + for (let i = 0; i < username.length; i++) {
1949 + usernameBytes[i] = username.charCodeAt(i);
1950 + }
1951 + usernameBytes[username.length] = 0;
1952 + for (let i = 0; i < password.length; i++) {
1953 + passwordBytes[i] = password.charCodeAt(i);
1954 + }
1955 + passwordBytes[password.length] = 0;
1956 + let x = new Uint8Array(secret);
1957 + for (let i = 0; i < 32; i++) {
1958 + for (let j = 0; j < 8; j++) {
1959 + x[j] ^= usernameBytes[i * 8 + j];
1960 + }
1961 + x = des.enc8(x);
1962 + usernameBytes.set(x, i * 8);
1963 + }
1964 + x = new Uint8Array(secret);
1965 + for (let i = 0; i < 8; i++) {
1966 + for (let j = 0; j < 8; j++) {
1967 + x[j] ^= passwordBytes[i * 8 + j];
1968 + }
1969 + x = des.enc8(x);
1970 + passwordBytes.set(x, i * 8);
1971 + }
1972 + this._sock.send(B);
1973 + this._sock.send(usernameBytes);
1974 + this._sock.send(passwordBytes);
1975 + this._rfbInitState = "SecurityResult";
1976 + return true;
1977 + }
1978 +
1979 _negotiateAuthentication() {
1980 switch (this._rfbAuthScheme) {
1624 - case 1: // no auth
1625 - if (this._rfbVersion >= 3.8) {
1626 - this._rfbInitState = 'SecurityResult';
1627 - return true;
1628 - }
1629 - this._rfbInitState = 'ClientInitialisation';
1630 - return this._initMsg();
1981 + case securityTypeNone:
1982 + this._rfbInitState = 'SecurityResult';
1983 + return true;
1984
1632 - case 22: // XVP auth
1985 + case securityTypeXVP:
1986 return this._negotiateXvpAuth();
1987
1635 - case 2: // VNC authentication
1988 + case securityTypeARD:
1989 + return this._negotiateARDAuth();
1990 +
1991 + case securityTypeVNCAuth:
1992 return this._negotiateStdVNCAuth();
1993
1638 - case 16: // TightVNC Security Type
1994 + case securityTypeTight:
1995 return this._negotiateTightAuth();
1996
1641 - case 19: // VeNCrypt Security Type
1997 + case securityTypeVeNCrypt:
1998 return this._negotiateVeNCryptAuth();
1999
1644 - case 129: // TightVNC UNIX Security Type
2000 + case securityTypePlain:
2001 + return this._negotiatePlainAuth();
2002 +
2003 + case securityTypeUnixLogon:
2004 return this._negotiateTightUnixAuth();
2005
2006 + case securityTypeRA2ne:
2007 + return this._negotiateRA2neAuth();
2008 +
2009 + case securityTypeMSLogonII:
2010 + return this._negotiateMSLogonIIAuth();
2011 +
2012 default:
2013 return this._fail("Unsupported auth scheme (scheme: " +
2014 this._rfbAuthScheme + ")");
@@ -1651,6 +2016,13 @@ export default class RFB extends EventTargetMixin {
2016 }
2017
2018 _handleSecurityResult() {
2019 + // There is no security choice, and hence no security result
2020 + // until RFB 3.7
2021 + if (this._rfbVersion < 3.7) {
2022 + this._rfbInitState = 'ClientInitialisation';
2023 + return true;
2024 + }
2025 +
2026 if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
2027
2028 const status = this._sock.rQshift32();
@@ -1658,13 +2030,13 @@ export default class RFB extends EventTargetMixin {
2030 if (status === 0) { // OK
2031 this._rfbInitState = 'ClientInitialisation';
2032 Log.Debug('Authentication OK');
1661 - return this._initMsg();
2033 + return true;
2034 } else {
2035 if (this._rfbVersion >= 3.8) {
2036 this._rfbInitState = "SecurityReason";
2037 this._securityContext = "security result";
2038 this._securityStatus = status;
1667 - return this._initMsg();
2039 + return true;
2040 } else {
2041 this.dispatchEvent(new CustomEvent(
2042 "securityfailure",
@@ -1772,6 +2144,8 @@ export default class RFB extends EventTargetMixin {
2144 if (this._fbDepth == 24) {
2145 encs.push(encodings.encodingTight);
2146 encs.push(encodings.encodingTightPNG);
2147 + encs.push(encodings.encodingZRLE);
2148 + encs.push(encodings.encodingJPEG);
2149 encs.push(encodings.encodingHextile);
2150 encs.push(encodings.encodingRRE);
2151 }
@@ -1838,6 +2212,14 @@ export default class RFB extends EventTargetMixin {
2212 }
2213 }
2214
2215 + // Resume authentication handshake after it was paused for some
2216 + // reason, e.g. waiting for a password from the user
2217 + _resumeAuthentication() {
2218 + // We use setTimeout() so it's run in its own context, just like
2219 + // it originally did via the WebSocket's event handler
2220 + setTimeout(this._initMsg.bind(this), 0);
2221 + }
2222 +
2223 _handleSetColourMapMsg() {
2224 Log.Debug("SetColorMapEntries");
2225
@@ -2500,6 +2882,9 @@ export default class RFB extends EventTargetMixin {
2882 this._updateScale();
2883
2884 this._updateContinuousUpdates();
2885 +
2886 + // Keep this size until browser client size changes
2887 + this._saveExpectedClientSize();
2888 }
2889
2890 _xvpOp(ver, op) {
public/novnc/core/util/browser.js
+56 -7
@@ -77,27 +77,76 @@ export const hasScrollbarGutter = _hasScrollbarGutter;
77 * It's better to use feature detection than platform detection.
78 */
79
80 +/* OS */
81 +
82 export function isMac() {
81 - return navigator && !!(/mac/i).exec(navigator.platform);
83 + return !!(/mac/i).exec(navigator.platform);
84 }
85
86 export function isWindows() {
85 - return navigator && !!(/win/i).exec(navigator.platform);
87 + return !!(/win/i).exec(navigator.platform);
88 }
89
90 export function isIOS() {
89 - return navigator &&
90 - (!!(/ipad/i).exec(navigator.platform) ||
91 + return (!!(/ipad/i).exec(navigator.platform) ||
92 !!(/iphone/i).exec(navigator.platform) ||
93 !!(/ipod/i).exec(navigator.platform));
94 }
95
96 +export function isAndroid() {
97 + /* Android sets navigator.platform to Linux :/ */
98 + return !!navigator.userAgent.match('Android ');
99 +}
100 +
101 +export function isChromeOS() {
102 + /* ChromeOS sets navigator.platform to Linux :/ */
103 + return !!navigator.userAgent.match(' CrOS ');
104 +}
105 +
106 +/* Browser */
107 +
108 export function isSafari() {
96 - return navigator && (navigator.userAgent.indexOf('Safari') !== -1 &&
97 - navigator.userAgent.indexOf('Chrome') === -1);
109 + return !!navigator.userAgent.match('Safari/...') &&
110 + !navigator.userAgent.match('Chrome/...') &&
111 + !navigator.userAgent.match('Chromium/...') &&
112 + !navigator.userAgent.match('Epiphany/...');
113 }
114
115 export function isFirefox() {
101 - return navigator && !!(/firefox/i).exec(navigator.userAgent);
116 + return !!navigator.userAgent.match('Firefox/...') &&
117 + !navigator.userAgent.match('Seamonkey/...');
118 +}
119 +
120 +export function isChrome() {
121 + return !!navigator.userAgent.match('Chrome/...') &&
122 + !navigator.userAgent.match('Chromium/...') &&
123 + !navigator.userAgent.match('Edg/...') &&
124 + !navigator.userAgent.match('OPR/...');
125 +}
126 +
127 +export function isChromium() {
128 + return !!navigator.userAgent.match('Chromium/...');
129 }
130
131 +export function isOpera() {
132 + return !!navigator.userAgent.match('OPR/...');
133 +}
134 +
135 +export function isEdge() {
136 + return !!navigator.userAgent.match('Edg/...');
137 +}
138 +
139 +/* Engine */
140 +
141 +export function isGecko() {
142 + return !!navigator.userAgent.match('Gecko/...');
143 +}
144 +
145 +export function isWebKit() {
146 + return !!navigator.userAgent.match('AppleWebKit/...') &&
147 + !navigator.userAgent.match('Chrome/...');
148 +}
149 +
150 +export function isBlink() {
151 + return !!navigator.userAgent.match('Chrome/...');
152 +}
public/novnc/core/util/cursor.js
+4
@@ -18,6 +18,10 @@ export default class Cursor {
18 this._canvas.style.position = 'fixed';
19 this._canvas.style.zIndex = '65535';
20 this._canvas.style.pointerEvents = 'none';
21 + // Safari on iOS can select the cursor image
22 + // https://bugs.webkit.org/show_bug.cgi?id=249223
23 + this._canvas.style.userSelect = 'none';
24 + this._canvas.style.WebkitUserSelect = 'none';
25 // Can't use "display" because of Firefox bug #1445997
26 this._canvas.style.visibility = 'hidden';
27 }
public/novnc/core/util/md5.js new
+79
@@ -0,0 +1,79 @@
1 +/*
2 + * noVNC: HTML5 VNC client
3 + * Copyright (C) 2021 The noVNC Authors
4 + * Licensed under MPL 2.0 (see LICENSE.txt)
5 + *
6 + * See README.md for usage and integration instructions.
7 + */
8 +
9 +/*
10 + * Performs MD5 hashing on a string of binary characters, returns an array of bytes
11 + */
12 +
13 +export function MD5(d) {
14 + let r = M(V(Y(X(d), 8 * d.length)));
15 + return r;
16 +}
17 +
18 +function M(d) {
19 + let f = new Uint8Array(d.length);
20 + for (let i=0;i<d.length;i++) {
21 + f[i] = d.charCodeAt(i);
22 + }
23 + return f;
24 +}
25 +
26 +function X(d) {
27 + let r = Array(d.length >> 2);
28 + for (let m = 0; m < r.length; m++) r[m] = 0;
29 + for (let m = 0; m < 8 * d.length; m += 8) r[m >> 5] |= (255 & d.charCodeAt(m / 8)) << m % 32;
30 + return r;
31 +}
32 +
33 +function V(d) {
34 + let r = "";
35 + for (let m = 0; m < 32 * d.length; m += 8) r += String.fromCharCode(d[m >> 5] >>> m % 32 & 255);
36 + return r;
37 +}
38 +
39 +function Y(d, g) {
40 + d[g >> 5] |= 128 << g % 32, d[14 + (g + 64 >>> 9 << 4)] = g;
41 + let m = 1732584193, f = -271733879, r = -1732584194, i = 271733878;
42 + for (let n = 0; n < d.length; n += 16) {
43 + let h = m,
44 + t = f,
45 + g = r,
46 + e = i;
47 + f = ii(f = ii(f = ii(f = ii(f = hh(f = hh(f = hh(f = hh(f = gg(f = gg(f = gg(f = gg(f = ff(f = ff(f = ff(f = ff(f, r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 0], 7, -680876936), f, r, d[n + 1], 12, -389564586), m, f, d[n + 2], 17, 606105819), i, m, d[n + 3], 22, -1044525330), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 4], 7, -176418897), f, r, d[n + 5], 12, 1200080426), m, f, d[n + 6], 17, -1473231341), i, m, d[n + 7], 22, -45705983), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 8], 7, 1770035416), f, r, d[n + 9], 12, -1958414417), m, f, d[n + 10], 17, -42063), i, m, d[n + 11], 22, -1990404162), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 12], 7, 1804603682), f, r, d[n + 13], 12, -40341101), m, f, d[n + 14], 17, -1502002290), i, m, d[n + 15], 22, 1236535329), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 1], 5, -165796510), f, r, d[n + 6], 9, -1069501632), m, f, d[n + 11], 14, 643717713), i, m, d[n + 0], 20, -373897302), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 5], 5, -701558691), f, r, d[n + 10], 9, 38016083), m, f, d[n + 15], 14, -660478335), i, m, d[n + 4], 20, -405537848), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 9], 5, 568446438), f, r, d[n + 14], 9, -1019803690), m, f, d[n + 3], 14, -187363961), i, m, d[n + 8], 20, 1163531501), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 13], 5, -1444681467), f, r, d[n + 2], 9, -51403784), m, f, d[n + 7], 14, 1735328473), i, m, d[n + 12], 20, -1926607734), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 5], 4, -378558), f, r, d[n + 8], 11, -2022574463), m, f, d[n + 11], 16, 1839030562), i, m, d[n + 14], 23, -35309556), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 1], 4, -1530992060), f, r, d[n + 4], 11, 1272893353), m, f, d[n + 7], 16, -155497632), i, m, d[n + 10], 23, -1094730640), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 13], 4, 681279174), f, r, d[n + 0], 11, -358537222), m, f, d[n + 3], 16, -722521979), i, m, d[n + 6], 23, 76029189), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 9], 4, -640364487), f, r, d[n + 12], 11, -421815835), m, f, d[n + 15], 16, 530742520), i, m, d[n + 2], 23, -995338651), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 0], 6, -198630844), f, r, d[n + 7], 10, 1126891415), m, f, d[n + 14], 15, -1416354905), i, m, d[n + 5], 21, -57434055), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 12], 6, 1700485571), f, r, d[n + 3], 10, -1894986606), m, f, d[n + 10], 15, -1051523), i, m, d[n + 1], 21, -2054922799), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 8], 6, 1873313359), f, r, d[n + 15], 10, -30611744), m, f, d[n + 6], 15, -1560198380), i, m, d[n + 13], 21, 1309151649), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 4], 6, -145523070), f, r, d[n + 11], 10, -1120210379), m, f, d[n + 2], 15, 718787259), i, m, d[n + 9], 21, -343485551), m = add(m, h), f = add(f, t), r = add(r, g), i = add(i, e);
48 + }
49 + return Array(m, f, r, i);
50 +}
51 +
52 +function cmn(d, g, m, f, r, i) {
53 + return add(rol(add(add(g, d), add(f, i)), r), m);
54 +}
55 +
56 +function ff(d, g, m, f, r, i, n) {
57 + return cmn(g & m | ~g & f, d, g, r, i, n);
58 +}
59 +
60 +function gg(d, g, m, f, r, i, n) {
61 + return cmn(g & f | m & ~f, d, g, r, i, n);
62 +}
63 +
64 +function hh(d, g, m, f, r, i, n) {
65 + return cmn(g ^ m ^ f, d, g, r, i, n);
66 +}
67 +
68 +function ii(d, g, m, f, r, i, n) {
69 + return cmn(m ^ (g | ~f), d, g, r, i, n);
70 +}
71 +
72 +function add(d, g) {
73 + let m = (65535 & d) + (65535 & g);
74 + return (d >> 16) + (g >> 16) + (m >> 16) << 16 | 65535 & m;
75 +}
76 +
77 +function rol(d, g) {
78 + return d << g | d >>> 32 - g;
79 +}
\ No newline at end of file
public/novnc/vnc.html
+76 -62
@@ -1,4 +1,4 @@
1 -<!DOCTYPE html>
1 +<!DOCTYPE html>
2 <html lang="en" class="noVNC_loading">
3 <head>
4
@@ -15,53 +15,35 @@
15 -->
16 <title>noVNC</title>
17
18 - <meta charset="utf-8">
19 -
20 - <!-- Icons (see app/images/icons/Makefile for what the sizes are for) -->
21 -<!--
22 - <link rel="icon" sizes="16x16" type="image/png" href="app/images/icons/novnc-16x16.png">
23 - <link rel="icon" sizes="24x24" type="image/png" href="app/images/icons/novnc-24x24.png">
24 - <link rel="icon" sizes="32x32" type="image/png" href="app/images/icons/novnc-32x32.png">
25 - <link rel="icon" sizes="48x48" type="image/png" href="app/images/icons/novnc-48x48.png">
26 - <link rel="icon" sizes="60x60" type="image/png" href="app/images/icons/novnc-60x60.png">
27 - <link rel="icon" sizes="64x64" type="image/png" href="app/images/icons/novnc-64x64.png">
28 - <link rel="icon" sizes="72x72" type="image/png" href="app/images/icons/novnc-72x72.png">
29 - <link rel="icon" sizes="76x76" type="image/png" href="app/images/icons/novnc-76x76.png">
30 - <link rel="icon" sizes="96x96" type="image/png" href="app/images/icons/novnc-96x96.png">
31 - <link rel="icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-120x120.png">
32 - <link rel="icon" sizes="144x144" type="image/png" href="app/images/icons/novnc-144x144.png">
33 - <link rel="icon" sizes="152x152" type="image/png" href="app/images/icons/novnc-152x152.png">
34 - <link rel="icon" sizes="192x192" type="image/png" href="app/images/icons/novnc-192x192.png">
35 --->
36 - <!-- Firefox currently mishandles SVG, see #1419039
37 - <link rel="icon" sizes="any" type="image/svg+xml" href="app/images/icons/novnc-icon.svg">
38 - -->
39 - <!-- Repeated last so that legacy handling will pick this -->
40 -<!--
41 - <link rel="icon" sizes="16x16" type="image/png" href="app/images/icons/novnc-16x16.png">
42 --->
18 + <!-- <link rel="icon" type="image/x-icon" href="app/images/icons/novnc.ico"> -->
19
20 <!-- Apple iOS Safari settings -->
21 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
22 <meta name="apple-mobile-web-app-capable" content="yes">
23 <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
48 - <!-- Home Screen Icons (favourites and bookmarks use the normal icons) -->
49 -<!--
50 - <link rel="apple-touch-icon" sizes="60x60" type="image/png" href="app/images/icons/novnc-60x60.png">
51 - <link rel="apple-touch-icon" sizes="76x76" type="image/png" href="app/images/icons/novnc-76x76.png">
52 - <link rel="apple-touch-icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-120x120.png">
53 - <link rel="apple-touch-icon" sizes="152x152" type="image/png" href="app/images/icons/novnc-152x152.png">
54 --->
24 +
25 +
26 + <!-- <link rel="apple-touch-icon" sizes="40x40" type="image/png" href="app/images/icons/novnc-ios-40.png">
27 + <link rel="apple-touch-icon" sizes="58x58" type="image/png" href="app/images/icons/novnc-ios-58.png">
28 + <link rel="apple-touch-icon" sizes="80x80" type="image/png" href="app/images/icons/novnc-ios-80.png">
29 + <link rel="apple-touch-icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-ios-120.png">
30 + <link rel="apple-touch-icon" sizes="152x152" type="image/png" href="app/images/icons/novnc-ios-152.png">
31 + <link rel="apple-touch-icon" sizes="167x167" type="image/png" href="app/images/icons/novnc-ios-167.png">
32 + <link rel="apple-touch-icon" sizes="60x60" type="image/png" href="app/images/icons/novnc-ios-60.png">
33 + <link rel="apple-touch-icon" sizes="87x87" type="image/png" href="app/images/icons/novnc-ios-87.png">
34 + <link rel="apple-touch-icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-ios-120.png">
35 + <link rel="apple-touch-icon" sizes="180x180" type="image/png" href="app/images/icons/novnc-ios-180.png"> -->
36
37 <!-- Stylesheets -->
38 <link rel="stylesheet" href="app/styles/base.css">
39 + <link rel="stylesheet" href="app/styles/input.css">
40
41 <!-- Images that will later appear via CSS -->
42 <link rel="preload" as="image" href="app/images/info.svg">
43 <link rel="preload" as="image" href="app/images/error.svg">
44 <link rel="preload" as="image" href="app/images/warning.svg">
45
64 - <script src="app/error-handler.js"></script>
46 + <script type="module" crossorigin="anonymous" src="app/error-handler.js"></script>
47 <script type="module" crossorigin="anonymous" src="app/ui.js"></script>
48 </head>
49
@@ -83,7 +65,9 @@
65
66 <div class="noVNC_scroll">
67
86 - <!--<h1 class="noVNC_logo" translate="no"><span>no</span><br>VNC</h1>-->
68 + <!-- <h1 class="noVNC_logo" translate="no"><span>no</span><br>VNC</h1> -->
69 +
70 + <hr>
71
72 <!-- Drag/Pan the viewport -->
73 <input type="image" alt="Drag" src="app/images/drag.svg"
@@ -147,17 +131,17 @@
131 <div class="noVNC_heading">
132 <img alt="" src="app/images/clipboard.svg"> Clipboard
133 </div>
134 + <p class="noVNC_subheading">
135 + Edit clipboard content in the textarea below.
136 + </p>
137 <textarea id="noVNC_clipboard_text" rows=5></textarea>
151 - <br>
152 - <input id="noVNC_clipboard_clear_button" type="button"
153 - value="Clear" class="noVNC_submit">
138 </div>
139 </div>
140
141 <!-- Toggle fullscreen -->
158 - <input type="image" alt="Fullscreen" src="app/images/fullscreen.svg"
142 + <input type="image" alt="Full Screen" src="app/images/fullscreen.svg"
143 id="noVNC_fullscreen_button" class="noVNC_button noVNC_hidden"
160 - title="Fullscreen">
144 + title="Full Screen">
145
146 <!-- Settings -->
147 <input type="image" alt="Settings" src="app/images/settings.svg"
@@ -165,10 +149,10 @@
149 title="Settings">
150 <div class="noVNC_vcenter">
151 <div id="noVNC_settings" class="noVNC_panel">
152 + <div class="noVNC_heading">
153 + <img alt="" src="app/images/settings.svg"> Settings
154 + </div>
155 <ul>
169 - <li class="noVNC_heading">
170 - <img alt="" src="app/images/settings.svg"> Settings
171 - </li>
156 <li>
157 <label><input id="noVNC_setting_shared" type="checkbox"> Shared Mode</label>
158 </li>
@@ -263,39 +247,69 @@
247 </div>
248 </div>
249
266 - <div id="noVNC_control_bar_hint"></div>
267 -
250 </div> <!-- End of noVNC_control_bar -->
251
252 + <div id="noVNC_hint_anchor" class="noVNC_vcenter">
253 + <div id="noVNC_control_bar_hint">
254 + </div>
255 + </div>
256 +
257 <!-- Status Dialog -->
258 <div id="noVNC_status"></div>
259
260 <!-- Connect button -->
261 <div class="noVNC_center">
262 <div id="noVNC_connect_dlg">
276 - <!--<div class="noVNC_logo" translate="no"><span>no</span>VNC</div>-->
277 - <div id="noVNC_connect_button"><div>
278 - <img alt="" src="app/images/connect.svg"> Connect
279 - </div></div>
263 + <!-- <p class="noVNC_logo" translate="no"><span>no</span>VNC</p> -->
264 + <div>
265 + <button id="noVNC_connect_button">
266 + <img alt="" src="app/images/connect.svg"> Connect
267 + </button>
268 + </div>
269 </div>
270 </div>
271
272 + <!-- Server Key Verification Dialog -->
273 + <div class="noVNC_center noVNC_connect_layer">
274 + <div id="noVNC_verify_server_dlg" class="noVNC_panel"><form>
275 + <div class="noVNC_heading">
276 + Server identity
277 + </div>
278 + <div>
279 + The server has provided the following identifying information:
280 + </div>
281 + <div id="noVNC_fingerprint_block">
282 + <b>Fingerprint:</b>
283 + <span id="noVNC_fingerprint"></span>
284 + </div>
285 + <div>
286 + Please verify that the information is correct and press
287 + "Approve". Otherwise press "Reject".
288 + </div>
289 + <div>
290 + <input id="noVNC_approve_server_button" type="submit" value="Approve" class="noVNC_submit">
291 + <input id="noVNC_reject_server_button" type="button" value="Reject" class="noVNC_submit">
292 + </div>
293 + </form></div>
294 + </div>
295 +
296 <!-- Password Dialog -->
297 <div class="noVNC_center noVNC_connect_layer">
298 <div id="noVNC_credentials_dlg" class="noVNC_panel"><form>
286 - <ul>
287 - <li id="noVNC_username_block">
288 - <label>Username:</label>
289 - <input id="noVNC_username_input">
290 - </li>
291 - <li id="noVNC_password_block">
292 - <label>Password:</label>
293 - <input id="noVNC_password_input" type="password">
294 - </li>
295 - <li>
296 - <input id="noVNC_credentials_button" type="submit" value="Send Credentials" class="noVNC_submit">
297 - </li>
298 - </ul>
299 + <div class="noVNC_heading">
300 + Credentials
301 + </div>
302 + <div id="noVNC_username_block">
303 + <label for="noVNC_username_input">Username:</label>
304 + <input id="noVNC_username_input">
305 + </div>
306 + <div id="noVNC_password_block">
307 + <label for="noVNC_password_input">Password:</label>
308 + <input id="noVNC_password_input" type="password">
309 + </div>
310 + <div>
311 + <input id="noVNC_credentials_button" type="submit" value="Send Credentials" class="noVNC_submit">
312 + </div>
313 </form></div>
314 </div>
315