]> git.proxmox.com Git - mirror_novnc.git/blame - core/websock.js
feat: add French localization strings
[mirror_novnc.git] / core / websock.js
CommitLineData
72f1348b 1/*
44d384b9 2 * Websock: high-performance buffering wrapper
412d9306 3 * Copyright (C) 2019 The noVNC Authors
1d728ace 4 * Licensed under MPL 2.0 (see LICENSE.txt)
72f1348b 5 *
44d384b9
TS
6 * Websock is similar to the standard WebSocket / RTCDataChannel object
7 * but with extra buffer handling.
72f1348b
JM
8 *
9 * Websock has built-in receive queue buffering; the message event
10 * does not contain actual data but is simply a notification that
11 * there is new data available. Several rQ* methods are available to
12 * read binary data off of the receive queue.
13 */
14
6d6f0db0 15import * as Log from './util/logging.js';
ae510306 16
6d6f0db0
SR
17// this has performance issues in some versions Chromium, and
18// doesn't gain a tremendous amount of performance increase in Firefox
19// at the moment. It may be valuable to turn it on in the future.
2b5f94fa 20const MAX_RQ_GROW_SIZE = 40 * 1024 * 1024; // 40 MiB
6d6f0db0 21
44d384b9
TS
22// Constants pulled from RTCDataChannelState enum
23// https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/readyState#RTCDataChannelState_enum
24const DataChannel = {
25 CONNECTING: "connecting",
26 OPEN: "open",
27 CLOSING: "closing",
28 CLOSED: "closed"
29};
30
31const ReadyStates = {
32 CONNECTING: [WebSocket.CONNECTING, DataChannel.CONNECTING],
33 OPEN: [WebSocket.OPEN, DataChannel.OPEN],
34 CLOSING: [WebSocket.CLOSING, DataChannel.CLOSING],
35 CLOSED: [WebSocket.CLOSED, DataChannel.CLOSED],
36};
37
38// Properties a raw channel must have, WebSocket and RTCDataChannel are two examples
39const rawChannelProps = [
40 "send",
41 "close",
42 "binaryType",
43 "onerror",
44 "onmessage",
45 "onopen",
46 "protocol",
47 "readyState",
48];
49
0e4808bf
JD
50export default class Websock {
51 constructor() {
44d384b9 52 this._websocket = null; // WebSocket or RTCDataChannel object
0e4808bf
JD
53
54 this._rQi = 0; // Receive queue index
55 this._rQlen = 0; // Next write position in the receive queue
56 this._rQbufferSize = 1024 * 1024 * 4; // Receive queue buffer size (4 MiB)
0e4808bf
JD
57 // called in init: this._rQ = new Uint8Array(this._rQbufferSize);
58 this._rQ = null; // Receive queue
59
60 this._sQbufferSize = 1024 * 10; // 10 KiB
61 // called in init: this._sQ = new Uint8Array(this._sQbufferSize);
62 this._sQlen = 0;
63 this._sQ = null; // Send queue
64
65 this._eventHandlers = {
66 message: () => {},
67 open: () => {},
68 close: () => {},
69 error: () => {}
70 };
71 }
72
6d6f0db0 73 // Getters and Setters
b7b7e4e2
PO
74
75 get readyState() {
76 let subState;
77
78 if (this._websocket === null) {
79 return "unused";
80 }
81
82 subState = this._websocket.readyState;
83
84 if (ReadyStates.CONNECTING.includes(subState)) {
85 return "connecting";
86 } else if (ReadyStates.OPEN.includes(subState)) {
87 return "open";
88 } else if (ReadyStates.CLOSING.includes(subState)) {
89 return "closing";
90 } else if (ReadyStates.CLOSED.includes(subState)) {
91 return "closed";
92 }
93
94 return "unknown";
95 }
96
8a189a62 97 get sQ() {
6d6f0db0 98 return this._sQ;
0e4808bf 99 }
6d6f0db0 100
8a189a62 101 get rQ() {
6d6f0db0 102 return this._rQ;
0e4808bf 103 }
6d6f0db0 104
8a189a62 105 get rQi() {
6d6f0db0 106 return this._rQi;
0e4808bf 107 }
6d6f0db0 108
8a189a62 109 set rQi(val) {
6d6f0db0 110 this._rQi = val;
0e4808bf 111 }
6d6f0db0
SR
112
113 // Receive Queue
8a189a62 114 get rQlen() {
6d6f0db0 115 return this._rQlen - this._rQi;
0e4808bf 116 }
6d6f0db0 117
0e4808bf 118 rQpeek8() {
6d6f0db0 119 return this._rQ[this._rQi];
0e4808bf 120 }
6d6f0db0 121
8a189a62
JD
122 rQskipBytes(bytes) {
123 this._rQi += bytes;
124 }
125
0e4808bf 126 rQshift8() {
879e33ab 127 return this._rQshift(1);
0e4808bf 128 }
6d6f0db0 129
0e4808bf 130 rQshift16() {
879e33ab 131 return this._rQshift(2);
0e4808bf 132 }
6d6f0db0 133
0e4808bf 134 rQshift32() {
879e33ab
JD
135 return this._rQshift(4);
136 }
137
138 // TODO(directxman12): test performance with these vs a DataView
139 _rQshift(bytes) {
140 let res = 0;
141 for (let byte = bytes - 1; byte >= 0; byte--) {
142 res += this._rQ[this._rQi++] << (byte * 8);
143 }
144 return res;
0e4808bf 145 }
6d6f0db0 146
0e4808bf 147 rQshiftStr(len) {
8a189a62 148 if (typeof(len) === 'undefined') { len = this.rQlen; }
db9daa98
SM
149 let str = "";
150 // Handle large arrays in steps to avoid long strings on the stack
151 for (let i = 0; i < len; i += 4096) {
f90c2a6d 152 let part = this.rQshiftBytes(Math.min(4096, len - i));
d9814c06 153 str += String.fromCharCode.apply(null, part);
db9daa98
SM
154 }
155 return str;
0e4808bf 156 }
6d6f0db0 157
0e4808bf 158 rQshiftBytes(len) {
8a189a62 159 if (typeof(len) === 'undefined') { len = this.rQlen; }
6d6f0db0
SR
160 this._rQi += len;
161 return new Uint8Array(this._rQ.buffer, this._rQi - len, len);
0e4808bf 162 }
6d6f0db0 163
0e4808bf 164 rQshiftTo(target, len) {
8a189a62 165 if (len === undefined) { len = this.rQlen; }
6d6f0db0
SR
166 // TODO: make this just use set with views when using a ArrayBuffer to store the rQ
167 target.set(new Uint8Array(this._rQ.buffer, this._rQi, len));
168 this._rQi += len;
0e4808bf 169 }
6d6f0db0 170
8a189a62
JD
171 rQslice(start, end = this.rQlen) {
172 return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start);
0e4808bf 173 }
6d6f0db0
SR
174
175 // Check to see if we must wait for 'num' bytes (default to FBU.bytes)
176 // to be available in the receive queue. Return true if we need to
177 // wait (and possibly print a debug message), otherwise false.
0e4808bf 178 rQwait(msg, num, goback) {
8a189a62 179 if (this.rQlen < num) {
6d6f0db0
SR
180 if (goback) {
181 if (this._rQi < goback) {
182 throw new Error("rQwait cannot backup " + goback + " bytes");
2cccf753 183 }
6d6f0db0 184 this._rQi -= goback;
2cccf753 185 }
6d6f0db0
SR
186 return true; // true means need more data
187 }
188 return false;
0e4808bf 189 }
72f1348b 190
6d6f0db0 191 // Send Queue
72f1348b 192
0e4808bf 193 flush() {
b7b7e4e2 194 if (this._sQlen > 0 && this.readyState === 'open') {
ea858bfa 195 this._websocket.send(this._encodeMessage());
6d6f0db0
SR
196 this._sQlen = 0;
197 }
0e4808bf 198 }
6d6f0db0 199
0e4808bf 200 send(arr) {
6d6f0db0
SR
201 this._sQ.set(arr, this._sQlen);
202 this._sQlen += arr.length;
203 this.flush();
0e4808bf 204 }
6d6f0db0 205
ea858bfa 206 sendString(str) {
651c23ec 207 this.send(str.split('').map(chr => chr.charCodeAt(0)));
0e4808bf 208 }
6d6f0db0
SR
209
210 // Event Handlers
0e4808bf 211 off(evt) {
651c23ec 212 this._eventHandlers[evt] = () => {};
0e4808bf 213 }
6d6f0db0 214
0e4808bf 215 on(evt, handler) {
6d6f0db0 216 this._eventHandlers[evt] = handler;
0e4808bf 217 }
6d6f0db0 218
ea858bfa 219 _allocateBuffers() {
6d6f0db0
SR
220 this._rQ = new Uint8Array(this._rQbufferSize);
221 this._sQ = new Uint8Array(this._sQbufferSize);
0e4808bf 222 }
6d6f0db0 223
0e4808bf 224 init() {
ea858bfa 225 this._allocateBuffers();
6d6f0db0
SR
226 this._rQi = 0;
227 this._websocket = null;
0e4808bf 228 }
6d6f0db0 229
0e4808bf 230 open(uri, protocols) {
44d384b9
TS
231 this.attach(new WebSocket(uri, protocols));
232 }
233
234 attach(rawChannel) {
6d6f0db0
SR
235 this.init();
236
44d384b9
TS
237 // Must get object and class methods to be compatible with the tests.
238 const channelProps = [...Object.keys(rawChannel), ...Object.getOwnPropertyNames(Object.getPrototypeOf(rawChannel))];
239 for (let i = 0; i < rawChannelProps.length; i++) {
240 const prop = rawChannelProps[i];
241 if (channelProps.indexOf(prop) < 0) {
242 throw new Error('Raw channel missing property: ' + prop);
243 }
244 }
6d6f0db0 245
44d384b9
TS
246 this._websocket = rawChannel;
247 this._websocket.binaryType = "arraybuffer";
ea858bfa 248 this._websocket.onmessage = this._recvMessage.bind(this);
44d384b9 249
de9fc950 250 this._websocket.onopen = () => {
6d6f0db0
SR
251 Log.Debug('>> WebSock.onopen');
252 if (this._websocket.protocol) {
253 Log.Info("Server choose sub-protocol: " + this._websocket.protocol);
2cccf753 254 }
2cccf753 255
6d6f0db0
SR
256 this._eventHandlers.open();
257 Log.Debug("<< WebSock.onopen");
651c23ec 258 };
44d384b9 259
651c23ec 260 this._websocket.onclose = (e) => {
6d6f0db0
SR
261 Log.Debug(">> WebSock.onclose");
262 this._eventHandlers.close(e);
263 Log.Debug("<< WebSock.onclose");
651c23ec 264 };
44d384b9 265
651c23ec 266 this._websocket.onerror = (e) => {
6d6f0db0
SR
267 Log.Debug(">> WebSock.onerror: " + e);
268 this._eventHandlers.error(e);
269 Log.Debug("<< WebSock.onerror: " + e);
651c23ec 270 };
0e4808bf 271 }
6d6f0db0 272
0e4808bf 273 close() {
6d6f0db0 274 if (this._websocket) {
b7b7e4e2
PO
275 if (this.readyState === 'connecting' ||
276 this.readyState === 'open') {
6d6f0db0
SR
277 Log.Info("Closing WebSocket connection");
278 this._websocket.close();
fcff386b 279 }
6d6f0db0 280
651c23ec 281 this._websocket.onmessage = () => {};
6d6f0db0 282 }
0e4808bf 283 }
6d6f0db0
SR
284
285 // private methods
ea858bfa 286 _encodeMessage() {
6d6f0db0
SR
287 // Put in a binary arraybuffer
288 // according to the spec, you can send ArrayBufferViews with the send method
289 return new Uint8Array(this._sQ.buffer, 0, this._sQlen);
0e4808bf 290 }
6d6f0db0 291
e8614e20
SM
292 // We want to move all the unread data to the start of the queue,
293 // e.g. compacting.
294 // The function also expands the receive que if needed, and for
295 // performance reasons we combine these two actions to avoid
296 // unneccessary copying.
ea858bfa 297 _expandCompactRQ(minFit) {
7d755d10
JAD
298 // if we're using less than 1/8th of the buffer even with the incoming bytes, compact in place
299 // instead of resizing
ea858bfa
SM
300 const requiredBufferSize = (this._rQlen - this._rQi + minFit) * 8;
301 const resizeNeeded = this._rQbufferSize < requiredBufferSize;
7d755d10 302
6d6f0db0 303 if (resizeNeeded) {
7d755d10
JAD
304 // Make sure we always *at least* double the buffer size, and have at least space for 8x
305 // the current amount of data
ea858bfa 306 this._rQbufferSize = Math.max(this._rQbufferSize * 2, requiredBufferSize);
6d6f0db0 307 }
40037b6a 308
6d6f0db0
SR
309 // we don't want to grow unboundedly
310 if (this._rQbufferSize > MAX_RQ_GROW_SIZE) {
311 this._rQbufferSize = MAX_RQ_GROW_SIZE;
ea858bfa 312 if (this._rQbufferSize - this.rQlen < minFit) {
8727f598 313 throw new Error("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit");
40037b6a 314 }
6d6f0db0 315 }
40037b6a 316
6d6f0db0 317 if (resizeNeeded) {
ea858bfa 318 const oldRQbuffer = this._rQ.buffer;
6d6f0db0 319 this._rQ = new Uint8Array(this._rQbufferSize);
ea858bfa 320 this._rQ.set(new Uint8Array(oldRQbuffer, this._rQi, this._rQlen - this._rQi));
6d6f0db0 321 } else {
5b5b7474 322 this._rQ.copyWithin(0, this._rQi, this._rQlen);
6d6f0db0 323 }
40037b6a 324
6d6f0db0
SR
325 this._rQlen = this._rQlen - this._rQi;
326 this._rQi = 0;
0e4808bf 327 }
40037b6a 328
e8614e20 329 // push arraybuffer values onto the end of the receive que
ea858bfa 330 _DecodeMessage(data) {
2b5f94fa 331 const u8 = new Uint8Array(data);
6d6f0db0 332 if (u8.length > this._rQbufferSize - this._rQlen) {
ea858bfa 333 this._expandCompactRQ(u8.length);
6d6f0db0
SR
334 }
335 this._rQ.set(u8, this._rQlen);
336 this._rQlen += u8.length;
0e4808bf 337 }
fcff386b 338
ea858bfa
SM
339 _recvMessage(e) {
340 this._DecodeMessage(e.data);
8a189a62 341 if (this.rQlen > 0) {
1678bf86 342 this._eventHandlers.message();
1678bf86 343 if (this._rQlen == this._rQi) {
e8614e20
SM
344 // All data has now been processed, this means we
345 // can reset the receive queue.
1678bf86
PO
346 this._rQlen = 0;
347 this._rQi = 0;
2cccf753 348 }
1678bf86
PO
349 } else {
350 Log.Debug("Ignoring empty message");
72f1348b 351 }
6d6f0db0 352 }
0e4808bf 353}