@novnc/novnc 1.6.0-g71d0bfa → 1.6.0-g8e1ebdf
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/clipboard.js +72 -0
- package/core/crypto/des.js +4 -4
- package/core/decoders/zrle.js +1 -1
- package/core/display.js +1 -1
- package/core/rfb.js +23 -8
- package/core/util/browser.js +33 -0
- package/docs/API.md +0 -5
- package/package.json +2 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* noVNC: HTML5 VNC client
|
|
3
|
+
* Copyright (c) 2025 The noVNC authors
|
|
4
|
+
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as Log from './util/logging.js';
|
|
8
|
+
import { browserAsyncClipboardSupport } from './util/browser.js';
|
|
9
|
+
|
|
10
|
+
export default class AsyncClipboard {
|
|
11
|
+
constructor(target) {
|
|
12
|
+
this._target = target || null;
|
|
13
|
+
|
|
14
|
+
this._isAvailable = null;
|
|
15
|
+
|
|
16
|
+
this._eventHandlers = {
|
|
17
|
+
'focus': this._handleFocus.bind(this),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// ===== EVENT HANDLERS =====
|
|
21
|
+
|
|
22
|
+
this.onpaste = () => {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ===== PRIVATE METHODS =====
|
|
26
|
+
|
|
27
|
+
async _ensureAvailable() {
|
|
28
|
+
if (this._isAvailable !== null) return this._isAvailable;
|
|
29
|
+
try {
|
|
30
|
+
const status = await browserAsyncClipboardSupport();
|
|
31
|
+
this._isAvailable = (status === 'available');
|
|
32
|
+
} catch {
|
|
33
|
+
this._isAvailable = false;
|
|
34
|
+
}
|
|
35
|
+
return this._isAvailable;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async _handleFocus(event) {
|
|
39
|
+
if (!(await this._ensureAvailable())) return;
|
|
40
|
+
try {
|
|
41
|
+
const text = await navigator.clipboard.readText();
|
|
42
|
+
this.onpaste(text);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
Log.Error("Clipboard read failed: ", error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ===== PUBLIC METHODS =====
|
|
49
|
+
|
|
50
|
+
writeClipboard(text) {
|
|
51
|
+
// Can lazily check cached availability
|
|
52
|
+
if (!this._isAvailable) return false;
|
|
53
|
+
navigator.clipboard.writeText(text)
|
|
54
|
+
.catch(error => Log.Error("Clipboard write failed: ", error));
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
grab() {
|
|
59
|
+
if (!this._target) return;
|
|
60
|
+
this._ensureAvailable()
|
|
61
|
+
.then((isAvailable) => {
|
|
62
|
+
if (isAvailable) {
|
|
63
|
+
this._target.addEventListener('focus', this._eventHandlers.focus);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
ungrab() {
|
|
69
|
+
if (!this._target) return;
|
|
70
|
+
this._target.removeEventListener('focus', this._eventHandlers.focus);
|
|
71
|
+
}
|
|
72
|
+
}
|
package/core/crypto/des.js
CHANGED
|
@@ -181,11 +181,11 @@ class DES {
|
|
|
181
181
|
// Encrypt 8 bytes of text
|
|
182
182
|
enc8(text) {
|
|
183
183
|
const b = text.slice();
|
|
184
|
-
let
|
|
184
|
+
let l, r, x; // left, right, accumulator
|
|
185
185
|
|
|
186
186
|
// Squash 8 bytes to 2 ints
|
|
187
|
-
l = b[
|
|
188
|
-
r = b[
|
|
187
|
+
l = b[0]<<24 | b[1]<<16 | b[2]<<8 | b[3];
|
|
188
|
+
r = b[4]<<24 | b[5]<<16 | b[6]<<8 | b[7];
|
|
189
189
|
|
|
190
190
|
x = ((l >>> 4) ^ r) & 0x0f0f0f0f;
|
|
191
191
|
r ^= x;
|
|
@@ -252,7 +252,7 @@ class DES {
|
|
|
252
252
|
|
|
253
253
|
// Spread ints to bytes
|
|
254
254
|
x = [r, l];
|
|
255
|
-
for (i = 0; i < 8; i++) {
|
|
255
|
+
for (let i = 0; i < 8; i++) {
|
|
256
256
|
b[i] = (x[i>>>2] >>> (8 * (3 - (i % 4)))) % 256;
|
|
257
257
|
if (b[i] < 0) { b[i] += 256; } // unsigned
|
|
258
258
|
}
|
package/core/decoders/zrle.js
CHANGED
package/core/display.js
CHANGED
|
@@ -14,7 +14,7 @@ export default class Display {
|
|
|
14
14
|
constructor(target) {
|
|
15
15
|
this._drawCtx = null;
|
|
16
16
|
|
|
17
|
-
this._renderQ = []; // queue drawing actions for in-
|
|
17
|
+
this._renderQ = []; // queue drawing actions for in-order rendering
|
|
18
18
|
this._flushPromise = null;
|
|
19
19
|
|
|
20
20
|
// the full frame buffer (logical canvas) size
|
package/core/rfb.js
CHANGED
|
@@ -15,6 +15,7 @@ import { clientToElement } from './util/element.js';
|
|
|
15
15
|
import { setCapture } from './util/events.js';
|
|
16
16
|
import EventTargetMixin from './util/eventtarget.js';
|
|
17
17
|
import Display from "./display.js";
|
|
18
|
+
import AsyncClipboard from "./clipboard.js";
|
|
18
19
|
import Inflator from "./inflator.js";
|
|
19
20
|
import Deflator from "./deflator.js";
|
|
20
21
|
import Keyboard from "./input/keyboard.js";
|
|
@@ -164,6 +165,7 @@ export default class RFB extends EventTargetMixin {
|
|
|
164
165
|
this._sock = null; // Websock object
|
|
165
166
|
this._display = null; // Display object
|
|
166
167
|
this._flushing = false; // Display flushing state
|
|
168
|
+
this._asyncClipboard = null; // Async clipboard object
|
|
167
169
|
this._keyboard = null; // Keyboard input handler object
|
|
168
170
|
this._gestures = null; // Gesture input handler object
|
|
169
171
|
this._resizeObserver = null; // Resize observer object
|
|
@@ -266,6 +268,9 @@ export default class RFB extends EventTargetMixin {
|
|
|
266
268
|
throw exc;
|
|
267
269
|
}
|
|
268
270
|
|
|
271
|
+
this._asyncClipboard = new AsyncClipboard(this._canvas);
|
|
272
|
+
this._asyncClipboard.onpaste = this.clipboardPasteFrom.bind(this);
|
|
273
|
+
|
|
269
274
|
this._keyboard = new Keyboard(this._canvas);
|
|
270
275
|
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
|
|
271
276
|
this._remoteCapsLock = null; // Null indicates unknown or irrelevant
|
|
@@ -315,8 +320,10 @@ export default class RFB extends EventTargetMixin {
|
|
|
315
320
|
this._rfbConnectionState === "connected") {
|
|
316
321
|
if (viewOnly) {
|
|
317
322
|
this._keyboard.ungrab();
|
|
323
|
+
this._asyncClipboard.ungrab();
|
|
318
324
|
} else {
|
|
319
325
|
this._keyboard.grab();
|
|
326
|
+
this._asyncClipboard.grab();
|
|
320
327
|
}
|
|
321
328
|
}
|
|
322
329
|
}
|
|
@@ -2208,7 +2215,10 @@ export default class RFB extends EventTargetMixin {
|
|
|
2208
2215
|
this._setDesktopName(name);
|
|
2209
2216
|
this._resize(width, height);
|
|
2210
2217
|
|
|
2211
|
-
if (!this._viewOnly) {
|
|
2218
|
+
if (!this._viewOnly) {
|
|
2219
|
+
this._keyboard.grab();
|
|
2220
|
+
this._asyncClipboard.grab();
|
|
2221
|
+
}
|
|
2212
2222
|
|
|
2213
2223
|
this._fbDepth = 24;
|
|
2214
2224
|
|
|
@@ -2323,6 +2333,15 @@ export default class RFB extends EventTargetMixin {
|
|
|
2323
2333
|
return this._fail("Unexpected SetColorMapEntries message");
|
|
2324
2334
|
}
|
|
2325
2335
|
|
|
2336
|
+
_writeClipboard(text) {
|
|
2337
|
+
if (this._viewOnly) return;
|
|
2338
|
+
if (this._asyncClipboard.writeClipboard(text)) return;
|
|
2339
|
+
// Fallback clipboard
|
|
2340
|
+
this.dispatchEvent(
|
|
2341
|
+
new CustomEvent("clipboard", {detail: {text: text}})
|
|
2342
|
+
);
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2326
2345
|
_handleServerCutText() {
|
|
2327
2346
|
Log.Debug("ServerCutText");
|
|
2328
2347
|
|
|
@@ -2342,9 +2361,7 @@ export default class RFB extends EventTargetMixin {
|
|
|
2342
2361
|
return true;
|
|
2343
2362
|
}
|
|
2344
2363
|
|
|
2345
|
-
this.
|
|
2346
|
-
"clipboard",
|
|
2347
|
-
{ detail: { text: text } }));
|
|
2364
|
+
this._writeClipboard(text);
|
|
2348
2365
|
|
|
2349
2366
|
} else {
|
|
2350
2367
|
//Extended msg.
|
|
@@ -2480,9 +2497,7 @@ export default class RFB extends EventTargetMixin {
|
|
|
2480
2497
|
|
|
2481
2498
|
textData = textData.replaceAll("\r\n", "\n");
|
|
2482
2499
|
|
|
2483
|
-
this.
|
|
2484
|
-
"clipboard",
|
|
2485
|
-
{ detail: { text: textData } }));
|
|
2500
|
+
this._writeClipboard(textData);
|
|
2486
2501
|
}
|
|
2487
2502
|
} else {
|
|
2488
2503
|
return this._fail("Unexpected action in extended clipboard message: " + actions);
|
|
@@ -2934,7 +2949,7 @@ export default class RFB extends EventTargetMixin {
|
|
|
2934
2949
|
|
|
2935
2950
|
// We need to handle errors when we requested the resize.
|
|
2936
2951
|
if (this._FBU.x === 1 && this._FBU.y !== 0) {
|
|
2937
|
-
let msg
|
|
2952
|
+
let msg;
|
|
2938
2953
|
// The y-position indicates the status code from the server
|
|
2939
2954
|
switch (this._FBU.y) {
|
|
2940
2955
|
case 1:
|
package/core/util/browser.js
CHANGED
|
@@ -11,6 +11,39 @@
|
|
|
11
11
|
import * as Log from './logging.js';
|
|
12
12
|
import Base64 from '../base64.js';
|
|
13
13
|
|
|
14
|
+
// Async clipboard detection
|
|
15
|
+
|
|
16
|
+
/* Evaluates if there is browser support for the async clipboard API and
|
|
17
|
+
* relevant clipboard permissions. Returns 'unsupported' if permission states
|
|
18
|
+
* cannot be resolved. On the other hand, detecting 'granted' or 'prompt'
|
|
19
|
+
* permission states for both read and write indicates full API support with no
|
|
20
|
+
* imposed native browser paste prompt. Conversely, detecting 'denied' indicates
|
|
21
|
+
* the user elected to disable clipboard.
|
|
22
|
+
*/
|
|
23
|
+
export async function browserAsyncClipboardSupport() {
|
|
24
|
+
if (!(navigator?.permissions?.query &&
|
|
25
|
+
navigator?.clipboard?.writeText &&
|
|
26
|
+
navigator?.clipboard?.readText)) {
|
|
27
|
+
return 'unsupported';
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const writePerm = await navigator.permissions.query(
|
|
31
|
+
{name: "clipboard-write", allowWithoutGesture: true});
|
|
32
|
+
const readPerm = await navigator.permissions.query(
|
|
33
|
+
{name: "clipboard-read", allowWithoutGesture: false});
|
|
34
|
+
if (writePerm.state === "denied" || readPerm.state === "denied") {
|
|
35
|
+
return 'denied';
|
|
36
|
+
}
|
|
37
|
+
if ((writePerm.state === "granted" || writePerm.state === "prompt") &&
|
|
38
|
+
(readPerm.state === "granted" || readPerm.state === "prompt")) {
|
|
39
|
+
return 'available';
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
return 'unsupported';
|
|
43
|
+
}
|
|
44
|
+
return 'unsupported';
|
|
45
|
+
}
|
|
46
|
+
|
|
14
47
|
// Touch detection
|
|
15
48
|
export let isTouchDevice = ('ontouchstart' in document.documentElement) ||
|
|
16
49
|
// required for Chrome debugger
|
package/docs/API.md
CHANGED
|
@@ -77,11 +77,6 @@ protocol stream.
|
|
|
77
77
|
if the remote session is smaller than its container, or handled
|
|
78
78
|
according to `clipViewport` if it is larger. Disabled by default.
|
|
79
79
|
|
|
80
|
-
`showDotCursor`
|
|
81
|
-
- Is a `boolean` indicating whether a dot cursor should be shown
|
|
82
|
-
instead of a zero-sized or fully-transparent cursor if the server
|
|
83
|
-
sets such invisible cursor. Disabled by default.
|
|
84
|
-
|
|
85
80
|
`viewOnly`
|
|
86
81
|
- Is a `boolean` indicating if any events (e.g. key presses or mouse
|
|
87
82
|
movement) should be prevented from being sent to the server.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@novnc/novnc",
|
|
3
|
-
"version": "1.6.0-
|
|
3
|
+
"version": "1.6.0-g8e1ebdf",
|
|
4
4
|
"description": "An HTML5 VNC client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"chai": "latest",
|
|
40
40
|
"commander": "latest",
|
|
41
41
|
"eslint": "latest",
|
|
42
|
+
"@eslint/js": "latest",
|
|
42
43
|
"fs-extra": "latest",
|
|
43
44
|
"globals": "latest",
|
|
44
45
|
"jsdom": "latest",
|