From d6a01c28e56edb0b0d3bab4459b1f1a75414e999 Mon Sep 17 00:00:00 2001 From: Mike Jumper Date: Wed, 26 Apr 2023 16:11:37 -0700 Subject: [PATCH] GUACAMOLE-615: Migrate tunnel implementations to common parser. --- .../src/main/webapp/modules/Parser.js | 89 ++++-- .../src/main/webapp/modules/Tunnel.js | 281 +++++------------- 2 files changed, 148 insertions(+), 222 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Parser.js b/guacamole-common-js/src/main/webapp/modules/Parser.js index a30e7851b..a2afa3d17 100644 --- a/guacamole-common-js/src/main/webapp/modules/Parser.js +++ b/guacamole-common-js/src/main/webapp/modules/Parser.js @@ -92,30 +92,45 @@ Guacamole.Parser = function Parser() { * * @param {!string} packet * The instruction data to receive. + * + * @param {!boolean} [isBuffer=false] + * Whether the provided data should be treated as an instruction buffer + * that grows continuously. If true, the data provided to receive() + * MUST always start with the data provided to the previous call. If + * false (the default), only the new data should be provided to + * receive(), and previously-received data will automatically be + * buffered by the parser as needed. */ - this.receive = function receive(packet) { + this.receive = function receive(packet, isBuffer) { - // Truncate buffer as necessary - if (startIndex > 4096 && elementEnd >= startIndex) { + if (isBuffer) + buffer = packet; - buffer = buffer.substring(startIndex); + else { - // Reset parse relative to truncation - elementEnd -= startIndex; - startIndex = 0; + // Truncate buffer as necessary + if (startIndex > 4096 && elementEnd >= startIndex) { + + buffer = buffer.substring(startIndex); + + // Reset parse relative to truncation + elementEnd -= startIndex; + startIndex = 0; + + } + + // Append data to buffer ONLY if there is outstanding data present. It + // is otherwise much faster to simply parse the received buffer as-is, + // and tunnel implementations can take advantage of this by preferring + // to send only complete instructions. Both the HTTP and WebSocket + // tunnel implementations included with Guacamole already do this. + if (buffer.length) + buffer += packet; + else + buffer = packet; } - // Append data to buffer ONLY if there is outstanding data present. It - // is otherwise much faster to simply parse the received buffer as-is, - // and tunnel implementations can take advantage of this by preferring - // to send only complete instructions. Both the HTTP and WebSocket - // tunnel implementations included with Guacamole already do this. - if (buffer.length) - buffer += packet; - else - buffer = packet; - // While search is within currently received data while (elementEnd < buffer.length) { @@ -257,3 +272,43 @@ Guacamole.Parser.codePointCount = function codePointCount(str, start, end) { var surrogatePairs = str.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g); return str.length - (surrogatePairs ? surrogatePairs.length : 0); }; + +/** + * Converts each of the values within the given array to strings, formatting + * those strings as length-prefixed elements of a complete Guacamole + * instruction. + * + * @param {!*[]} elements + * The values that should be encoded as the elements of a Guacamole + * instruction. Order of these elements is preserved. This array MUST have + * at least one element. + * + * @returns {!string} + * A complete Guacamole instruction consisting of each of the provided + * element values, in order. + */ +Guacamole.Parser.toInstruction = function toInstruction(elements) { + + /** + * Converts the given value to a length/string pair for use as an + * element in a Guacamole instruction. + * + * @private + * @param {*} value + * The value to convert. + * + * @return {!string} + * The converted value. + */ + var toElement = function toElement(value) { + var str = '' + value; + return Guacamole.Parser.codePointCount(str) + "." + str; + }; + + var instr = toElement(elements[0]); + for (var i = 1; i < elements.length; i++) + instr += ',' + toElement(elements[i]); + + return instr + ';'; + +}; diff --git a/guacamole-common-js/src/main/webapp/modules/Tunnel.js b/guacamole-common-js/src/main/webapp/modules/Tunnel.js index 5f3b79f3c..7f2ee0fa5 100644 --- a/guacamole-common-js/src/main/webapp/modules/Tunnel.js +++ b/guacamole-common-js/src/main/webapp/modules/Tunnel.js @@ -436,37 +436,11 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) { return; // Do not attempt to send empty messages - if (arguments.length === 0) + if (!arguments.length) return; - /** - * Converts the given value to a length/string pair for use as an - * element in a Guacamole instruction. - * - * @private - * @param value - * The value to convert. - * - * @return {!string} - * The converted value. - */ - function getElement(value) { - var string = new String(value); - return string.length + "." + string; - } - - // Initialized message with first element - var message = getElement(arguments[0]); - - // Append remaining elements - for (var i=1; i= startIndex) { - - // We now have enough data for the element. Parse. - var element = current.substring(startIndex, elementEnd); - var terminator = current.substring(elementEnd, elementEnd+1); - - // Add element to array - elements.push(element); - - // If last element, handle instruction - if (terminator === ";") { - - // Get opcode - var opcode = elements.shift(); - - // Call instruction handler. - if (tunnel.oninstruction) - tunnel.oninstruction(opcode, elements); - - // Clear elements - elements.length = 0; - - } - - // Start searching for length at character after - // element terminator - startIndex = elementEnd + 1; - - } - - // Search for end of length - var lengthEnd = current.indexOf(".", startIndex); - if (lengthEnd !== -1) { - - // Parse length - var length = parseInt(current.substring(elementEnd+1, lengthEnd)); - - // If we're done parsing, handle the next response. - if (length === 0) { - - // Clean up interval if polling - if (interval) - clearInterval(interval); - - // Clean up object - xmlhttprequest.onreadystatechange = null; - xmlhttprequest.abort(); - - // Start handling next request - if (nextRequest) - handleResponse(nextRequest); - - // Done parsing - break; - - } - - // Calculate start of element - startIndex = lengthEnd + 1; - - // Calculate location of element terminator - elementEnd = startIndex + length; - - } - - // If no period yet, continue search when more data - // is received - else { - startIndex = current.length; - break; - } - - } // end parse loop + try { + parser.receive(current, true); + } + catch (e) { + close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, e.message)); + return; + } } @@ -823,6 +749,16 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { */ var tunnel = this; + /** + * The parser that this tunnel will use to parse received Guacamole + * instructions. The parser is created when the tunnel is (re-)connected. + * Initially, this will be null. + * + * @private + * @type {Guacamole.Parser} + */ + var parser = null; + /** * The WebSocket used by this tunnel. * @@ -1016,36 +952,10 @@ Guacamole.WebSocketTunnel = function(tunnelURL) { return; // Do not attempt to send empty messages - if (arguments.length === 0) + if (!arguments.length) return; - /** - * Converts the given value to a length/string pair for use as an - * element in a Guacamole instruction. - * - * @private - * @param {*} value - * The value to convert. - * - * @return {!string} - * The converted value. - */ - function getElement(value) { - var string = new String(value); - return string.length + "." + string; - } - - // Initialized message with first element - var message = getElement(arguments[0]); - - // Append remaining elements - for (var i=1; i