GUACAMOLE-896: Avoid XHR-related memory limitations by using Fetch API.

The Fetch API allows us to read HTTP responses as true streams, without
building an in-memory string. We can the buffer things ourselves as we
see fit, including as a Blob that can dynamically leverage disk storage
for larger data.
This commit is contained in:
Michael Jumper
2022-02-11 16:44:44 -08:00
parent f559f5ca70
commit 2c8bc58402

View File

@@ -1346,13 +1346,14 @@ Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain, extraTu
var tunnel = this;
/**
* The current, in-progress HTTP request. If no request is currently in
* progress, this will be null.
* AbortController instance which allows the current, in-progress HTTP
* request to be aborted. If no request is currently in progress, this will
* be null.
*
* @private
* @type {XMLHttpRequest}
* @type {AbortController}
*/
var xhr = null;
var abortController = null;
/**
* Additional headers to be sent in tunnel requests. This dictionary can be
@@ -1364,23 +1365,6 @@ Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain, extraTu
*/
var extraHeaders = extraTunnelHeaders || {};
/**
* Adds the configured additional headers to the given request.
*
* @param {!XMLHttpRequest} request
* The request where the configured extra headers will be added.
*
* @param {!object} headers
* The headers to be added to the request.
*
* @private
*/
function addExtraHeaders(request, headers) {
for (var name in headers) {
request.setRequestHeader(name, headers[name]);
}
}
this.sendMessage = function sendMessage(elements) {
// Do nothing
};
@@ -1393,18 +1377,10 @@ Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain, extraTu
// Connection is now starting
tunnel.setState(Guacamole.Tunnel.State.CONNECTING);
// Start a new connection
xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.withCredentials = !!crossDomain;
addExtraHeaders(xhr, extraHeaders);
xhr.responseType = 'text';
xhr.send(null);
var offset = 0;
// Create Guacamole protocol parser specifically for this connection
// Create Guacamole protocol and UTF-8 parsers specifically for this
// connection
var parser = new Guacamole.Parser();
var utf8Parser = new Guacamole.UTF8Parser();
// Invoke tunnel's oninstruction handler for each parsed instruction
parser.oninstruction = function instructionReceived(opcode, args) {
@@ -1412,51 +1388,62 @@ Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain, extraTu
tunnel.oninstruction(opcode, args);
};
// Continuously parse received data
xhr.onreadystatechange = function readyStateChanged() {
// Allow new request to be aborted
abortController = new AbortController();
// Parse while data is being received
if (xhr.readyState === 3 || xhr.readyState === 4) {
// Stream using the Fetch API
fetch(url, {
headers : extraHeaders,
credentials : crossDomain ? 'include' : 'same-origin',
signal : abortController.signal
})
.then(function gotResponse(response) {
// Reset state and close upon error
if (!response.ok) {
if (tunnel.onerror)
tunnel.onerror(new Guacamole.Status(
Guacamole.Status.Code.fromHTTPCode(response.status), response.statusText));
tunnel.disconnect();
return;
}
// Connection is open
tunnel.setState(Guacamole.Tunnel.State.OPEN);
var buffer = xhr.responseText;
var length = buffer.length;
// Parse only the portion of data which is newly received
if (offset < length) {
parser.receive(buffer.substring(offset));
offset = length;
}
}
var reader = response.body.getReader();
var processReceivedText = function processReceivedText(result) {
// Clean up and close when done
if (xhr.readyState === 4)
if (result.done) {
tunnel.disconnect();
return;
}
// Parse only the portion of data which is newly received
parser.receive(utf8Parser.decode(result.value));
// Continue parsing when next chunk is received
reader.read().then(processReceivedText);
};
// Reset state and close upon error
xhr.onerror = function httpError() {
// Schedule parse of first chunk
reader.read().then(processReceivedText);
// Fail if file could not be downloaded via HTTP
if (tunnel.onerror)
tunnel.onerror(new Guacamole.Status(
Guacamole.Status.Code.fromHTTPCode(xhr.status), xhr.statusText));
tunnel.disconnect();
};
});
};
this.disconnect = function disconnect() {
// Abort and dispose of XHR if a request is in progress
if (xhr) {
xhr.abort();
xhr = null;
// Abort any in-progress request
if (abortController) {
abortController.abort();
abortController = null;
}
// Connection is now closed