Files
noVNC/core/websock.js
T

339 lines
10 KiB
JavaScript
Raw Normal View History

2011-01-13 00:30:08 -06:00
/*
2021-02-18 08:42:34 +00:00
* Websock: high-performance buffering wrapper
2019-09-30 15:35:33 +02:00
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
2011-01-13 00:30:08 -06:00
*
2021-02-18 08:42:34 +00:00
* Websock is similar to the standard WebSocket / RTCDataChannel object
* but with extra buffer handling.
2011-01-13 00:30:08 -06:00
*
* Websock has built-in receive queue buffering; the message event
* does not contain actual data but is simply a notification that
* there is new data available. Several rQ* methods are available to
* read binary data off of the receive queue.
*/
2017-02-04 21:26:00 -05:00
import * as Log from './util/logging.js';
2016-09-14 13:52:53 -04:00
2017-02-04 21:26:00 -05:00
// this has performance issues in some versions Chromium, and
// doesn't gain a tremendous amount of performance increase in Firefox
// at the moment. It may be valuable to turn it on in the future.
2018-05-24 00:27:09 +03:00
const MAX_RQ_GROW_SIZE = 40 * 1024 * 1024; // 40 MiB
2017-02-04 21:26:00 -05:00
2021-02-18 08:42:34 +00:00
// Constants pulled from RTCDataChannelState enum
// https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/readyState#RTCDataChannelState_enum
const DataChannel = {
CONNECTING: "connecting",
OPEN: "open",
CLOSING: "closing",
CLOSED: "closed"
};
const ReadyStates = {
CONNECTING: [WebSocket.CONNECTING, DataChannel.CONNECTING],
OPEN: [WebSocket.OPEN, DataChannel.OPEN],
CLOSING: [WebSocket.CLOSING, DataChannel.CLOSING],
CLOSED: [WebSocket.CLOSED, DataChannel.CLOSED],
};
// Properties a raw channel must have, WebSocket and RTCDataChannel are two examples
const rawChannelProps = [
"send",
"close",
"binaryType",
"onerror",
"onmessage",
"onopen",
"protocol",
"readyState",
];
2018-07-05 21:31:56 +02:00
export default class Websock {
constructor() {
2021-02-18 08:42:34 +00:00
this._websocket = null; // WebSocket or RTCDataChannel object
2018-07-05 21:31:56 +02:00
this._rQi = 0; // Receive queue index
this._rQlen = 0; // Next write position in the receive queue
this._rQbufferSize = 1024 * 1024 * 4; // Receive queue buffer size (4 MiB)
// called in init: this._rQ = new Uint8Array(this._rQbufferSize);
this._rQ = null; // Receive queue
this._sQbufferSize = 1024 * 10; // 10 KiB
// called in init: this._sQ = new Uint8Array(this._sQbufferSize);
this._sQlen = 0;
this._sQ = null; // Send queue
this._eventHandlers = {
message: () => {},
open: () => {},
close: () => {},
error: () => {}
};
}
2017-02-04 21:26:00 -05:00
// Getters and Setters
2018-12-08 17:31:20 +02:00
get sQ() {
2017-02-04 21:26:00 -05:00
return this._sQ;
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
2018-12-08 17:31:20 +02:00
get rQ() {
2017-02-04 21:26:00 -05:00
return this._rQ;
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
2018-12-08 17:31:20 +02:00
get rQi() {
2017-02-04 21:26:00 -05:00
return this._rQi;
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
2018-12-08 17:31:20 +02:00
set rQi(val) {
2017-02-04 21:26:00 -05:00
this._rQi = val;
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
// Receive Queue
2018-12-08 17:31:20 +02:00
get rQlen() {
2017-02-04 21:26:00 -05:00
return this._rQlen - this._rQi;
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
2018-07-05 21:31:56 +02:00
rQpeek8() {
2017-02-04 21:26:00 -05:00
return this._rQ[this._rQi];
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
2018-12-08 17:31:20 +02:00
rQskipBytes(bytes) {
this._rQi += bytes;
}
2018-07-05 21:31:56 +02:00
rQshift8() {
2018-12-08 17:32:00 +02:00
return this._rQshift(1);
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
2018-07-05 21:31:56 +02:00
rQshift16() {
2018-12-08 17:32:00 +02:00
return this._rQshift(2);
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
2018-07-05 21:31:56 +02:00
rQshift32() {
2018-12-08 17:32:00 +02:00
return this._rQshift(4);
}
// TODO(directxman12): test performance with these vs a DataView
_rQshift(bytes) {
let res = 0;
for (let byte = bytes - 1; byte >= 0; byte--) {
res += this._rQ[this._rQi++] << (byte * 8);
}
return res;
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
2018-07-05 21:31:56 +02:00
rQshiftStr(len) {
2018-12-08 17:31:20 +02:00
if (typeof(len) === 'undefined') { len = this.rQlen; }
2018-06-14 16:52:36 +02:00
let str = "";
// Handle large arrays in steps to avoid long strings on the stack
for (let i = 0; i < len; i += 4096) {
2018-06-15 12:00:43 +02:00
let part = this.rQshiftBytes(Math.min(4096, len - i));
str += String.fromCharCode.apply(null, part);
2018-06-14 16:52:36 +02:00
}
return str;
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
2018-07-05 21:31:56 +02:00
rQshiftBytes(len) {
2018-12-08 17:31:20 +02:00
if (typeof(len) === 'undefined') { len = this.rQlen; }
2017-02-04 21:26:00 -05:00
this._rQi += len;
return new Uint8Array(this._rQ.buffer, this._rQi - len, len);
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
2018-07-05 21:31:56 +02:00
rQshiftTo(target, len) {
2018-12-08 17:31:20 +02:00
if (len === undefined) { len = this.rQlen; }
2017-02-04 21:26:00 -05:00
// TODO: make this just use set with views when using a ArrayBuffer to store the rQ
target.set(new Uint8Array(this._rQ.buffer, this._rQi, len));
this._rQi += len;
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
2018-12-08 17:31:20 +02:00
rQslice(start, end = this.rQlen) {
return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start);
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
// Check to see if we must wait for 'num' bytes (default to FBU.bytes)
// to be available in the receive queue. Return true if we need to
// wait (and possibly print a debug message), otherwise false.
2018-07-05 21:31:56 +02:00
rQwait(msg, num, goback) {
2018-12-08 17:31:20 +02:00
if (this.rQlen < num) {
2017-02-04 21:26:00 -05:00
if (goback) {
if (this._rQi < goback) {
throw new Error("rQwait cannot backup " + goback + " bytes");
2014-05-22 16:57:55 -04:00
}
2017-02-04 21:26:00 -05:00
this._rQi -= goback;
2014-05-22 16:57:55 -04:00
}
2017-02-04 21:26:00 -05:00
return true; // true means need more data
}
return false;
2018-07-05 21:31:56 +02:00
}
2011-01-13 00:30:08 -06:00
2017-02-04 21:26:00 -05:00
// Send Queue
2011-01-13 00:30:08 -06:00
2018-07-05 21:31:56 +02:00
flush() {
2021-02-18 08:42:34 +00:00
if (this._sQlen > 0 && ReadyStates.OPEN.indexOf(this._websocket.readyState) >= 0) {
2020-05-31 23:05:42 +02:00
this._websocket.send(this._encodeMessage());
2017-02-04 21:26:00 -05:00
this._sQlen = 0;
}
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
2018-07-05 21:31:56 +02:00
send(arr) {
2017-02-04 21:26:00 -05:00
this._sQ.set(arr, this._sQlen);
this._sQlen += arr.length;
this.flush();
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
2020-05-31 23:05:42 +02:00
sendString(str) {
this.send(str.split('').map(chr => chr.charCodeAt(0)));
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
// Event Handlers
2018-07-05 21:31:56 +02:00
off(evt) {
this._eventHandlers[evt] = () => {};
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
2018-07-05 21:31:56 +02:00
on(evt, handler) {
2017-02-04 21:26:00 -05:00
this._eventHandlers[evt] = handler;
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
2020-05-31 23:05:42 +02:00
_allocateBuffers() {
2017-02-04 21:26:00 -05:00
this._rQ = new Uint8Array(this._rQbufferSize);
this._sQ = new Uint8Array(this._sQbufferSize);
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
2018-07-05 21:31:56 +02:00
init() {
2020-05-31 23:05:42 +02:00
this._allocateBuffers();
2017-02-04 21:26:00 -05:00
this._rQi = 0;
this._websocket = null;
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
2018-07-05 21:31:56 +02:00
open(uri, protocols) {
2021-02-18 08:42:34 +00:00
this.attach(new WebSocket(uri, protocols));
}
attach(rawChannel) {
2017-02-04 21:26:00 -05:00
this.init();
2021-02-18 08:42:34 +00:00
// Must get object and class methods to be compatible with the tests.
const channelProps = [...Object.keys(rawChannel), ...Object.getOwnPropertyNames(Object.getPrototypeOf(rawChannel))];
for (let i = 0; i < rawChannelProps.length; i++) {
const prop = rawChannelProps[i];
if (channelProps.indexOf(prop) < 0) {
throw new Error('Raw channel missing property: ' + prop);
}
}
2017-02-04 21:26:00 -05:00
2021-02-18 08:42:34 +00:00
this._websocket = rawChannel;
this._websocket.binaryType = "arraybuffer";
2020-05-31 23:05:42 +02:00
this._websocket.onmessage = this._recvMessage.bind(this);
2021-02-18 08:42:34 +00:00
const onOpen = () => {
2017-02-04 21:26:00 -05:00
Log.Debug('>> WebSock.onopen');
if (this._websocket.protocol) {
Log.Info("Server choose sub-protocol: " + this._websocket.protocol);
2014-05-22 16:57:55 -04:00
}
2017-02-04 21:26:00 -05:00
this._eventHandlers.open();
Log.Debug("<< WebSock.onopen");
};
2021-02-18 08:42:34 +00:00
// If the readyState cannot be found this defaults to assuming it's not open.
const isOpen = ReadyStates.OPEN.indexOf(this._websocket.readyState) >= 0;
if (isOpen) {
onOpen();
} else {
this._websocket.onopen = onOpen;
}
this._websocket.onclose = (e) => {
2017-02-04 21:26:00 -05:00
Log.Debug(">> WebSock.onclose");
this._eventHandlers.close(e);
Log.Debug("<< WebSock.onclose");
};
2021-02-18 08:42:34 +00:00
this._websocket.onerror = (e) => {
2017-02-04 21:26:00 -05:00
Log.Debug(">> WebSock.onerror: " + e);
this._eventHandlers.error(e);
Log.Debug("<< WebSock.onerror: " + e);
};
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
2018-07-05 21:31:56 +02:00
close() {
2017-02-04 21:26:00 -05:00
if (this._websocket) {
2021-02-18 08:42:34 +00:00
if (ReadyStates.CONNECTING.indexOf(this._websocket.readyState) >= 0 ||
ReadyStates.OPEN.indexOf(this._websocket.readyState) >= 0) {
2017-02-04 21:26:00 -05:00
Log.Info("Closing WebSocket connection");
this._websocket.close();
}
2017-02-04 21:26:00 -05:00
this._websocket.onmessage = () => {};
2017-02-04 21:26:00 -05:00
}
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
// private methods
2020-05-31 23:05:42 +02:00
_encodeMessage() {
2017-02-04 21:26:00 -05:00
// Put in a binary arraybuffer
// according to the spec, you can send ArrayBufferViews with the send method
return new Uint8Array(this._sQ.buffer, 0, this._sQlen);
2018-07-05 21:31:56 +02:00
}
2017-02-04 21:26:00 -05:00
// We want to move all the unread data to the start of the queue,
// e.g. compacting.
// The function also expands the receive que if needed, and for
// performance reasons we combine these two actions to avoid
// unneccessary copying.
2020-05-31 23:05:42 +02:00
_expandCompactRQ(minFit) {
// if we're using less than 1/8th of the buffer even with the incoming bytes, compact in place
// instead of resizing
2020-05-31 23:05:42 +02:00
const requiredBufferSize = (this._rQlen - this._rQi + minFit) * 8;
const resizeNeeded = this._rQbufferSize < requiredBufferSize;
2017-02-04 21:26:00 -05:00
if (resizeNeeded) {
// Make sure we always *at least* double the buffer size, and have at least space for 8x
// the current amount of data
2020-05-31 23:05:42 +02:00
this._rQbufferSize = Math.max(this._rQbufferSize * 2, requiredBufferSize);
2017-02-04 21:26:00 -05:00
}
2015-12-03 21:30:47 -05:00
2017-02-04 21:26:00 -05:00
// we don't want to grow unboundedly
if (this._rQbufferSize > MAX_RQ_GROW_SIZE) {
this._rQbufferSize = MAX_RQ_GROW_SIZE;
2020-05-31 23:05:42 +02:00
if (this._rQbufferSize - this.rQlen < minFit) {
2018-05-24 00:25:44 +03:00
throw new Error("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit");
2015-12-03 21:30:47 -05:00
}
2017-02-04 21:26:00 -05:00
}
2015-12-03 21:30:47 -05:00
2017-02-04 21:26:00 -05:00
if (resizeNeeded) {
2020-05-31 23:05:42 +02:00
const oldRQbuffer = this._rQ.buffer;
2017-02-04 21:26:00 -05:00
this._rQ = new Uint8Array(this._rQbufferSize);
2020-05-31 23:05:42 +02:00
this._rQ.set(new Uint8Array(oldRQbuffer, this._rQi, this._rQlen - this._rQi));
2017-02-04 21:26:00 -05:00
} else {
this._rQ.copyWithin(0, this._rQi, this._rQlen);
2017-02-04 21:26:00 -05:00
}
2015-12-03 21:30:47 -05:00
2017-02-04 21:26:00 -05:00
this._rQlen = this._rQlen - this._rQi;
this._rQi = 0;
2018-07-05 21:31:56 +02:00
}
2015-12-03 21:30:47 -05:00
// push arraybuffer values onto the end of the receive que
2020-05-31 23:05:42 +02:00
_DecodeMessage(data) {
2018-05-24 00:27:09 +03:00
const u8 = new Uint8Array(data);
2017-02-04 21:26:00 -05:00
if (u8.length > this._rQbufferSize - this._rQlen) {
2020-05-31 23:05:42 +02:00
this._expandCompactRQ(u8.length);
2017-02-04 21:26:00 -05:00
}
this._rQ.set(u8, this._rQlen);
this._rQlen += u8.length;
2018-07-05 21:31:56 +02:00
}
2020-05-31 23:05:42 +02:00
_recvMessage(e) {
this._DecodeMessage(e.data);
2018-12-08 17:31:20 +02:00
if (this.rQlen > 0) {
2017-09-07 16:44:01 +02:00
this._eventHandlers.message();
if (this._rQlen == this._rQi) {
// All data has now been processed, this means we
// can reset the receive queue.
2017-09-07 16:44:01 +02:00
this._rQlen = 0;
this._rQi = 0;
2014-05-22 16:57:55 -04:00
}
2017-09-07 16:44:01 +02:00
} else {
Log.Debug("Ignoring empty message");
2011-01-13 00:30:08 -06:00
}
2017-02-04 21:26:00 -05:00
}
2018-07-05 21:31:56 +02:00
}