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