From fdb344ff80c4987cbec1ee015f3076766fd38e9d Mon Sep 17 00:00:00 2001 From: Frode Langelo Date: Thu, 3 Sep 2015 15:56:40 +0000 Subject: [PATCH 01/16] GUAC-1314: Added mapping of keycodes for numeric keypad and changed keyidentifier mapping to not map the typed character if it originated from the keypad. This should also make the numeric keypad work as expected when using VNC. Mapped MacOS Clear to NumLock. --- .../src/main/webapp/modules/Keyboard.js | 67 ++++++++++++++----- 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Keyboard.js b/guacamole-common-js/src/main/webapp/modules/Keyboard.js index 9a93864cb..9714e5b80 100644 --- a/guacamole-common-js/src/main/webapp/modules/Keyboard.js +++ b/guacamole-common-js/src/main/webapp/modules/Keyboard.js @@ -38,6 +38,13 @@ Guacamole.Keyboard = function(element) { */ var guac_keyboard = this; + /** + * Flag indicating whether the browser is running on Mac + * + * @type Boolean + */ + var is_mac = (navigator && navigator.platform && navigator.platform.match(/^mac/i)); + /** * Fired whenever the user presses a key with the element associated * with this Guacamole.Keyboard in focus. @@ -187,8 +194,7 @@ Guacamole.Keyboard = function(element) { this.keysym = keysym_from_key_identifier(keyIdentifier, location, guac_keyboard.modifiers.shift); // Determine whether default action for Alt+combinations must be prevented - var prevent_alt = !guac_keyboard.modifiers.ctrl - && !(navigator && navigator.platform && navigator.platform.match(/^mac/i)); + var prevent_alt = !guac_keyboard.modifiers.ctrl && !is_mac; // Determine whether default action for Ctrl+combinations must be prevented var prevent_ctrl = !guac_keyboard.modifiers.alt; @@ -325,6 +331,7 @@ Guacamole.Keyboard = function(element) { var keycodeKeysyms = { 8: [0xFF08], // backspace 9: [0xFF09], // tab + 12: [0xFF0B, 0xFF0B, 0xFF0B, 0xFFB5], // clear / KP 5 13: [0xFF0D], // enter 16: [0xFFE1, 0xFFE1, 0xFFE2], // shift 17: [0xFFE3, 0xFFE3, 0xFFE4], // ctrl @@ -333,19 +340,34 @@ Guacamole.Keyboard = function(element) { 20: [0xFFE5], // caps lock 27: [0xFF1B], // escape 32: [0x0020], // space - 33: [0xFF55], // page up - 34: [0xFF56], // page down - 35: [0xFF57], // end - 36: [0xFF50], // home - 37: [0xFF51], // left arrow - 38: [0xFF52], // up arrow - 39: [0xFF53], // right arrow - 40: [0xFF54], // down arrow - 45: [0xFF63], // insert - 46: [0xFFFF], // delete + 33: [0xFF55, 0xFF55, 0xFF55, 0xFFB9], // page up / KP 9 + 34: [0xFF56, 0xFF56, 0xFF56, 0xFFB3], // page down / KP 3 + 35: [0xFF57, 0xFF57, 0xFF57, 0xFFB1], // end / KP 1 + 36: [0xFF50, 0xFF50, 0xFF50, 0xFFB7], // home / KP 7 + 37: [0xFF51, 0xFF51, 0xFF51, 0xFFB4], // left arrow / KP 4 + 38: [0xFF52, 0xFF52, 0xFF52, 0xFFB8], // up arrow / KP 8 + 39: [0xFF53, 0xFF53, 0xFF53, 0xFFB6], // right arrow / KP 6 + 40: [0xFF54, 0xFF54, 0xFF54, 0xFFB2], // down arrow / KP 2 + 45: [0xFF63, 0xFF63, 0xFF63, 0xFFB0], // insert / KP 0 + 46: [0xFFFF, 0xFFFF, 0xFFFF, 0xFFAE], // delete / KP decimal 91: [0xFFEB], // left window key (hyper_l) 92: [0xFF67], // right window key (menu key?) 93: null, // select key + 96: [0xFFB0], // KP 0 + 97: [0xFFB1], // KP 1 + 98: [0xFFB2], // KP 2 + 99: [0xFFB3], // KP 3 + 100: [0xFFB4], // KP 4 + 101: [0xFFB5], // KP 5 + 102: [0xFFB6], // KP 6 + 103: [0xFFB7], // KP 7 + 104: [0xFFB8], // KP 8 + 105: [0xFFB9], // KP 9 + 106: [0xFFAA], // KP multiply + 107: [0xFFAB], // KP add + 109: [0xFFAD], // KP subtract + 110: [0xFFAE], // KP decimal + 111: [0xFFAF], // KP divide 112: [0xFFBE], // f1 113: [0xFFBF], // f2 114: [0xFFC0], // f3 @@ -583,13 +605,19 @@ Guacamole.Keyboard = function(element) { typedCharacter = String.fromCharCode(parseInt(hex, 16)); } - // If single character, use that as typed character - else if (identifier.length === 1) + // If single character and not keypad, use that as typed character + else if (identifier.length === 1 && location !== 3) typedCharacter = identifier; // Otherwise, look up corresponding keysym - else + else { + // Clear on Mac maps to NumLock + if (identifier === "Clear" && is_mac) { + identifier = "NumLock"; + } + return get_keysym(keyidentifier_keysym[identifier], location); + } // Alter case if necessary if (shifted === true) @@ -625,6 +653,12 @@ Guacamole.Keyboard = function(element) { } function keysym_from_keycode(keyCode, location) { + + // Map Clear on Mac to NumLock + if (keyCode === 12 && is_mac) { + keyCode = 144; + } + return get_keysym(keycodeKeysyms[keyCode], location); } @@ -827,6 +861,9 @@ Guacamole.Keyboard = function(element) { handled_event = interpret_event(); } while (handled_event !== null); + if (!last_event) + return false; + return last_event.defaultPrevented; } From 005d7897fbc167507a8ec8e5b60e4f3006be63bd Mon Sep 17 00:00:00 2001 From: Frode Langelo Date: Thu, 24 Sep 2015 03:21:27 +0000 Subject: [PATCH 02/16] GUAC-1314: Remove mapping of Mac's Clear to Num Lock. --- .../src/main/webapp/modules/Keyboard.js | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Keyboard.js b/guacamole-common-js/src/main/webapp/modules/Keyboard.js index 9714e5b80..e9319835b 100644 --- a/guacamole-common-js/src/main/webapp/modules/Keyboard.js +++ b/guacamole-common-js/src/main/webapp/modules/Keyboard.js @@ -610,14 +610,8 @@ Guacamole.Keyboard = function(element) { typedCharacter = identifier; // Otherwise, look up corresponding keysym - else { - // Clear on Mac maps to NumLock - if (identifier === "Clear" && is_mac) { - identifier = "NumLock"; - } - + else return get_keysym(keyidentifier_keysym[identifier], location); - } // Alter case if necessary if (shifted === true) @@ -653,12 +647,6 @@ Guacamole.Keyboard = function(element) { } function keysym_from_keycode(keyCode, location) { - - // Map Clear on Mac to NumLock - if (keyCode === 12 && is_mac) { - keyCode = 144; - } - return get_keysym(keycodeKeysyms[keyCode], location); } @@ -861,9 +849,6 @@ Guacamole.Keyboard = function(element) { handled_event = interpret_event(); } while (handled_event !== null); - if (!last_event) - return false; - return last_event.defaultPrevented; } From ff255ce977875553e75d5c6d9db1f403e7bc110e Mon Sep 17 00:00:00 2001 From: Frode Langelo Date: Thu, 24 Sep 2015 15:55:54 +0000 Subject: [PATCH 03/16] GUAC-1314: Remove is_mac flag --- .../src/main/webapp/modules/Keyboard.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Keyboard.js b/guacamole-common-js/src/main/webapp/modules/Keyboard.js index e9319835b..27da3afa5 100644 --- a/guacamole-common-js/src/main/webapp/modules/Keyboard.js +++ b/guacamole-common-js/src/main/webapp/modules/Keyboard.js @@ -38,13 +38,6 @@ Guacamole.Keyboard = function(element) { */ var guac_keyboard = this; - /** - * Flag indicating whether the browser is running on Mac - * - * @type Boolean - */ - var is_mac = (navigator && navigator.platform && navigator.platform.match(/^mac/i)); - /** * Fired whenever the user presses a key with the element associated * with this Guacamole.Keyboard in focus. @@ -194,7 +187,8 @@ Guacamole.Keyboard = function(element) { this.keysym = keysym_from_key_identifier(keyIdentifier, location, guac_keyboard.modifiers.shift); // Determine whether default action for Alt+combinations must be prevented - var prevent_alt = !guac_keyboard.modifiers.ctrl && !is_mac; + var prevent_alt = !guac_keyboard.modifiers.ctrl + && !(navigator && navigator.platform && navigator.platform.match(/^mac/i)); // Determine whether default action for Ctrl+combinations must be prevented var prevent_ctrl = !guac_keyboard.modifiers.alt; From ff5687c01ec3d93d4cdd4ed569a26250dfc41ef9 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 28 Sep 2015 12:37:09 -0700 Subject: [PATCH 04/16] GUAC-1354: Refactor Guacamole.AudioChannel to Guacamole.AudioPlayer. --- .../src/main/webapp/modules/AudioChannel.js | 291 ------------ .../src/main/webapp/modules/AudioPlayer.js | 418 ++++++++++++++++++ .../src/main/webapp/modules/Client.js | 88 ++-- 3 files changed, 465 insertions(+), 332 deletions(-) delete mode 100644 guacamole-common-js/src/main/webapp/modules/AudioChannel.js create mode 100644 guacamole-common-js/src/main/webapp/modules/AudioPlayer.js diff --git a/guacamole-common-js/src/main/webapp/modules/AudioChannel.js b/guacamole-common-js/src/main/webapp/modules/AudioChannel.js deleted file mode 100644 index 0f64c4d70..000000000 --- a/guacamole-common-js/src/main/webapp/modules/AudioChannel.js +++ /dev/null @@ -1,291 +0,0 @@ -/* - * Copyright (C) 2015 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -var Guacamole = Guacamole || {}; - -/** - * Abstract audio channel which queues and plays arbitrary audio data. - * - * @constructor - */ -Guacamole.AudioChannel = function AudioChannel() { - - /** - * Reference to this AudioChannel. - * - * @private - * @type Guacamole.AudioChannel - */ - var channel = this; - - /** - * The earliest possible time that the next packet could play without - * overlapping an already-playing packet, in milliseconds. - * - * @private - * @type Number - */ - var nextPacketTime = Guacamole.AudioChannel.getTimestamp(); - - /** - * The last time that sync() was called, in milliseconds. If sync() has - * never been called, this will be the time the Guacamole.AudioChannel - * was created. - * - * @type Number - */ - var lastSync = nextPacketTime; - - /** - * Notifies this Guacamole.AudioChannel that all audio up to the current - * point in time has been given via play(), and that any difference in time - * between queued audio packets and the current time can be considered - * latency. - */ - this.sync = function sync() { - - // Calculate elapsed time since last sync - var now = Guacamole.AudioChannel.getTimestamp(); - var elapsed = now - lastSync; - - // Reschedule future playback time such that playback latency is - // bounded within the duration of the last audio frame - nextPacketTime = Math.min(nextPacketTime, now + elapsed); - - // Record sync time - lastSync = now; - - }; - - /** - * Queues up the given data for playing by this channel once all previously - * queued data has been played. If no data has been queued, the data will - * play immediately. - * - * @param {String} mimetype - * The mimetype of the audio data provided. - * - * @param {Number} duration - * The duration of the data provided, in milliseconds. - * - * @param {Blob} data - * The blob of audio data to play. - */ - this.play = function play(mimetype, duration, data) { - - var packet = new Guacamole.AudioChannel.Packet(mimetype, data); - - // Determine exactly when packet CAN play - var packetTime = Guacamole.AudioChannel.getTimestamp(); - if (nextPacketTime < packetTime) - nextPacketTime = packetTime; - - // Schedule packet - packet.play(nextPacketTime); - - // Update timeline - nextPacketTime += duration; - - }; - -}; - -// Define context if available -if (window.AudioContext) { - try {Guacamole.AudioChannel.context = new AudioContext();} - catch (e){} -} - -// Fallback to Webkit-specific AudioContext implementation -else if (window.webkitAudioContext) { - try {Guacamole.AudioChannel.context = new webkitAudioContext();} - catch (e){} -} - -/** - * Returns a base timestamp which can be used for scheduling future audio - * playback. Scheduling playback for the value returned by this function plus - * N will cause the associated audio to be played back N milliseconds after - * the function is called. - * - * @return {Number} An arbitrary channel-relative timestamp, in milliseconds. - */ -Guacamole.AudioChannel.getTimestamp = function() { - - // If we have an audio context, use its timestamp - if (Guacamole.AudioChannel.context) - return Guacamole.AudioChannel.context.currentTime * 1000; - - // If we have high-resolution timers, use those - if (window.performance) { - - if (window.performance.now) - return window.performance.now(); - - if (window.performance.webkitNow) - return window.performance.webkitNow(); - - } - - // Fallback to millisecond-resolution system time - return new Date().getTime(); - -}; - -/** - * Abstract representation of an audio packet. - * - * @constructor - * - * @param {String} mimetype The mimetype of the data contained by this packet. - * @param {Blob} data The blob of sound data contained by this packet. - */ -Guacamole.AudioChannel.Packet = function(mimetype, data) { - - /** - * Schedules this packet for playback at the given time. - * - * @function - * @param {Number} when The time this packet should be played, in - * milliseconds. - */ - this.play = function(when) { /* NOP */ }; // Defined conditionally depending on support - - // If audio API available, use it. - if (Guacamole.AudioChannel.context) { - - var readyBuffer = null; - - // By default, when decoding finishes, store buffer for future - // playback - var handleReady = function(buffer) { - readyBuffer = buffer; - }; - - // Read data and start decoding - var reader = new FileReader(); - reader.onload = function() { - Guacamole.AudioChannel.context.decodeAudioData( - reader.result, - function(buffer) { handleReady(buffer); } - ); - }; - reader.readAsArrayBuffer(data); - - // Set up buffer source - var source = Guacamole.AudioChannel.context.createBufferSource(); - source.connect(Guacamole.AudioChannel.context.destination); - - // Use noteOn() instead of start() if necessary - if (!source.start) - source.start = source.noteOn; - - var play_when; - - function playDelayed(buffer) { - source.buffer = buffer; - source.start(play_when / 1000); - } - - /** @ignore */ - this.play = function(when) { - - play_when = when; - - // If buffer available, play it NOW - if (readyBuffer) - playDelayed(readyBuffer); - - // Otherwise, play when decoded - else - handleReady = playDelayed; - - }; - - } - - else { - - var play_on_load = false; - - // Create audio element to house and play the data - var audio = null; - try { audio = new Audio(); } - catch (e) {} - - if (audio) { - - // Read data and start decoding - var reader = new FileReader(); - reader.onload = function() { - - var binary = ""; - var bytes = new Uint8Array(reader.result); - - // Produce binary string from bytes in buffer - for (var i=0; i + * @type Object. */ - var audioChannels = {}; + var audioPlayers = {}; // No initial parsers var parsers = []; @@ -440,6 +440,25 @@ Guacamole.Client = function(tunnel) { */ this.onerror = null; + /** + * Fired when a audio stream is created. The stream provided to this event + * handler will contain its own event handlers for received data. + * + * @event + * @param {Guacamole.InputStream} stream + * The stream that will receive audio data from the server. + * + * @param {String} mimetype + * The mimetype of the audio data which will be received. + * + * @return {Guacamole.AudioPlayer} + * An object which implements the Guacamole.AudioPlayer interface and + * has been initialied to play the data in the provided stream, or null + * if the built-in audio players of the Guacamole client should be + * used. + */ + this.onaudio = null; + /** * Fired when the clipboard of the remote client is changing. * @@ -499,27 +518,6 @@ Guacamole.Client = function(tunnel) { */ this.onsync = null; - /** - * Returns the audio channel having the given index, creating a new channel - * if necessary. - * - * @param {Number} index - * The index of the audio channel to retrieve. - * - * @returns {Guacamole.AudioChannel} - * The audio channel having the given index. - */ - var getAudioChannel = function getAudioChannel(index) { - - // Get audio channel, creating it first if necessary - var audio_channel = audioChannels[index]; - if (!audio_channel) - audio_channel = audioChannels[index] = new Guacamole.AudioChannel(); - - return audio_channel; - - }; - /** * Returns the layer with the given index, creating it if necessary. * Positive indices refer to visible layers, an index of zero refers to @@ -626,24 +624,32 @@ Guacamole.Client = function(tunnel) { "audio": function(parameters) { var stream_index = parseInt(parameters[0]); - var channel = getAudioChannel(parseInt(parameters[1])); - var mimetype = parameters[2]; - var duration = parseFloat(parameters[3]); + var mimetype = parameters[1]; // Create stream var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); - // Assemble entire stream as a blob - var blob_reader = new Guacamole.BlobReader(stream, mimetype); + // Get player instance via callback + var audioPlayer = null; + if (guac_client.onaudio) + audioPlayer = guac_client.onaudio(stream, mimetype); - // Play blob as audio - blob_reader.onend = function() { - channel.play(mimetype, duration, blob_reader.getBlob()); - }; + // If unsuccessful, use a default implementation + if (!audioPlayer) { + if (Guacamole.RawAudioPlayer.isSupportedType(mimetype)) + audioPlayer = new Guacamole.RawAudioPlayer(stream, mimetype); + } - // Send success response - guac_client.sendAck(stream_index, "OK", 0x0000); + // If player somehow successfully retrieved, send success response + if (audioPlayer) { + audioPlayers[stream_index] = audioPlayer; + guac_client.sendAck(stream_index, "OK", 0x0000); + } + + // Otherwise, mimetype must be unsupported + else + guac_client.sendAck(stream_index, "BAD TYPE", 0x030F); }, @@ -1113,11 +1119,11 @@ Guacamole.Client = function(tunnel) { // Flush display, send sync when done display.flush(function displaySyncComplete() { - // Synchronize all audio channels - for (var index in audioChannels) { - var audioChannel = audioChannels[index]; - if (audioChannel) - audioChannel.sync(); + // Synchronize all audio players + for (var index in audioPlayers) { + var audioPlayer = audioPlayers[index]; + if (audioPlayer) + audioPlayer.sync(); } // Send sync response to server From df57eac616955478d172c4e81f126faef7fc8883 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 28 Sep 2015 13:23:40 -0700 Subject: [PATCH 05/16] GUAC-1354: Use past audio packet size to determine playback latency threshold for audio. Add missing private annotations. --- .../src/main/webapp/modules/AudioPlayer.js | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js b/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js index 5c0eeeb90..abda411d0 100644 --- a/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js +++ b/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js @@ -86,6 +86,7 @@ Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) { /** * The format of audio this player will decode. * + * @private * @type Guacamole.RawAudioPlayer._Format */ var format = Guacamole.RawAudioPlayer._Format.parse(mimetype); @@ -94,6 +95,7 @@ Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) { * An instance of a Web Audio API AudioContext object, or null if the * Web Audio API is not supported. * + * @private * @type AudioContext */ var context = (function getAudioContext() { @@ -122,6 +124,7 @@ Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) { * N will cause the associated audio to be played back N milliseconds after * the function is called. * + * @private * @return {Number} * An arbitrary relative timestamp, in milliseconds. */ @@ -145,23 +148,25 @@ Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) { */ var nextPacketTime = getTimestamp(); - /** - * The last time that sync() was called, in milliseconds. If sync() has - * never been called, this will be the time the Guacamole.AudioPlayer - * was created. - * - * @type Number - */ - var lastSync = nextPacketTime; - /** * Guacamole.ArrayBufferReader wrapped around the audio input stream * provided with this Guacamole.RawAudioPlayer was created. * + * @private * @type Guacamole.ArrayBufferReader */ var reader = new Guacamole.ArrayBufferReader(stream); + /** + * The maximum amount of latency to allow between the buffered data stream + * and the playback position, in milliseconds. Initially, this is set to + * roughly one third of a second, but it will be recalculated dynamically. + * + * @private + * @type Number + */ + var maxLatency = 300; + // Play each received raw packet of audio immediately reader.ondata = function playReceivedAudio(data) { @@ -171,6 +176,9 @@ Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) { // Calculate overall duration (in milliseconds) var duration = samples * 1000 / format.rate; + // Recalculate latency threshold based on packet size + maxLatency = duration * 2; + // Determine exactly when packet CAN play var packetTime = getTimestamp(); if (nextPacketTime < packetTime) @@ -227,14 +235,10 @@ Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) { // Calculate elapsed time since last sync var now = getTimestamp(); - var elapsed = now - lastSync; // Reschedule future playback time such that playback latency is - // bounded within the duration of the last audio frame - nextPacketTime = Math.min(nextPacketTime, now + elapsed); - - // Record sync time - lastSync = now; + // bounded within a reasonable latency threshold + nextPacketTime = Math.min(nextPacketTime, now + maxLatency); }; From f0e6da86c9b0268b18e224851b2f5da5eee1ef68 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 30 Sep 2015 17:02:18 -0700 Subject: [PATCH 06/16] GUAC-1354: Use Guacamole.AudioPlayer.getInstance(), etc. to abstract away the various implementations. --- .../src/main/webapp/modules/AudioPlayer.js | 64 +++++++++++++++++++ .../src/main/webapp/modules/Client.js | 10 ++- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js b/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js index abda411d0..08bf4c82c 100644 --- a/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js +++ b/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js @@ -66,6 +66,70 @@ Guacamole.AudioPlayer.getTimestamp = function() { }; +/** + * Determines whether the given mimetype is supported by any built-in + * implementation of Guacamole.AudioPlayer, and thus will be properly handled + * by Guacamole.AudioPlayer.getInstance(). + * + * @param {String} mimetype + * The mimetype to check. + * + * @returns {Boolean} + * true if the given mimetype is supported by any built-in + * Guacamole.AudioPlayer, false otherwise. + */ +Guacamole.AudioPlayer.isSupportedType = function isSupportedType(mimetype) { + + return Guacamole.RawAudioPlayer.isSupportedType(mimetype); + +}; + +/** + * Returns a list of all mimetypes supported by any built-in + * Guacamole.AudioPlayer, in rough order of priority. Beware that only the core + * mimetypes themselves will be listed. Any mimetype parameters, even required + * ones, will not be included in the list. For example, "audio/L8" is a + * supported raw audio mimetype that is supported, but it is invalid without + * additional parameters. Something like "audio/L8;rate=44100" would be valid, + * however (see https://tools.ietf.org/html/rfc4856). + * + * @returns {String[]} + * A list of all mimetypes supported by any built-in Guacamole.AudioPlayer, + * excluding any parameters. + */ +Guacamole.AudioPlayer.getSupportedTypes = function getSupportedTypes() { + + return Guacamole.RawAudioPlayer.getSupportedTypes(); + +}; + +/** + * Returns an instance of Guacamole.AudioPlayer providing support for the given + * audio format. If support for the given audio format is not available, null + * is returned. + * + * @param {Guacamole.InputStream} stream + * The Guacamole.InputStream to read audio data from. + * + * @param {String} mimetype + * The mimetype of the audio data in the provided stream. + * + * @return {Guacamole.AudioPlayer} + * A Guacamole.AudioPlayer instance supporting the given mimetype and + * reading from the given stream, or null if support for the given mimetype + * is absent. + */ +Guacamole.AudioPlayer.getInstance = function getInstance(stream, mimetype) { + + // Use raw audio player if possible + if (Guacamole.RawAudioPlayer.isSupportedType(mimetype)) + return new Guacamole.RawAudioPlayer(stream, mimetype); + + // No support for given mimetype + return null; + +}; + /** * Implementation of Guacamole.AudioPlayer providing support for raw PCM format * audio. This player relies only on the Web Audio API and does not require any diff --git a/guacamole-common-js/src/main/webapp/modules/Client.js b/guacamole-common-js/src/main/webapp/modules/Client.js index 8104159d4..894888738 100644 --- a/guacamole-common-js/src/main/webapp/modules/Client.js +++ b/guacamole-common-js/src/main/webapp/modules/Client.js @@ -635,13 +635,11 @@ Guacamole.Client = function(tunnel) { if (guac_client.onaudio) audioPlayer = guac_client.onaudio(stream, mimetype); - // If unsuccessful, use a default implementation - if (!audioPlayer) { - if (Guacamole.RawAudioPlayer.isSupportedType(mimetype)) - audioPlayer = new Guacamole.RawAudioPlayer(stream, mimetype); - } + // If unsuccessful, try to use a default implementation + if (!audioPlayer) + audioPlayer = Guacamole.AudioPlayer.getInstance(stream, mimetype); - // If player somehow successfully retrieved, send success response + // If we have successfully retrieved an audio player, send success response if (audioPlayer) { audioPlayers[stream_index] = audioPlayer; guac_client.sendAck(stream_index, "OK", 0x0000); From 572534c6d336d3e68bdb2a548d0854ac84499b8f Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 30 Sep 2015 17:06:23 -0700 Subject: [PATCH 07/16] GUAC-1354: Use Web Audio API timestamps directly - no need to convert to milliseconds and back. --- .../src/main/webapp/modules/AudioPlayer.js | 63 ++++--------------- 1 file changed, 11 insertions(+), 52 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js b/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js index 08bf4c82c..98a3592aa 100644 --- a/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js +++ b/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js @@ -44,28 +44,6 @@ Guacamole.AudioPlayer = function AudioPlayer() { }; -/** - * Returns a base timestamp which can be used for scheduling future audio - * playback. Scheduling playback for the value returned by this function plus - * N will cause the associated audio to be played back N milliseconds after - * the function is called. - * - * @return {Number} - * An arbitrary relative timestamp, in milliseconds. - */ -Guacamole.AudioPlayer.getTimestamp = function() { - - // If we have high-resolution timers, use those - if (window.performance) { - var now = performance.now || performance.webkitNow(); - return now(); - } - - // Fallback to millisecond-resolution system time - return Date.now(); - -}; - /** * Determines whether the given mimetype is supported by any built-in * implementation of Guacamole.AudioPlayer, and thus will be properly handled @@ -182,35 +160,16 @@ Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) { })(); - /** - * Returns a base timestamp which can be used for scheduling future audio - * playback. Scheduling playback for the value returned by this function plus - * N will cause the associated audio to be played back N milliseconds after - * the function is called. - * - * @private - * @return {Number} - * An arbitrary relative timestamp, in milliseconds. - */ - var getTimestamp = function getTimestamp() { - - // If we have an audio context, use its timestamp - if (context) - return context.currentTime * 1000; - - // Otherwise, use the internal timestamp implementation - return Guacamole.AudioPlayer.getTimestamp(); - - }; - /** * The earliest possible time that the next packet could play without - * overlapping an already-playing packet, in milliseconds. + * overlapping an already-playing packet, in seconds. Note that while this + * value is in seconds, it is not an integer value and has microsecond + * resolution. * * @private * @type Number */ - var nextPacketTime = getTimestamp(); + var nextPacketTime = context.currentTime; /** * Guacamole.ArrayBufferReader wrapped around the audio input stream @@ -223,13 +182,13 @@ Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) { /** * The maximum amount of latency to allow between the buffered data stream - * and the playback position, in milliseconds. Initially, this is set to + * and the playback position, in seconds. Initially, this is set to * roughly one third of a second, but it will be recalculated dynamically. * * @private * @type Number */ - var maxLatency = 300; + var maxLatency = 0.3; // Play each received raw packet of audio immediately reader.ondata = function playReceivedAudio(data) { @@ -237,14 +196,14 @@ Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) { // Calculate total number of samples var samples = data.byteLength / format.channels / format.bytesPerSample; - // Calculate overall duration (in milliseconds) - var duration = samples * 1000 / format.rate; + // Calculate overall duration (in seconds) + var duration = samples / format.rate; // Recalculate latency threshold based on packet size maxLatency = duration * 2; // Determine exactly when packet CAN play - var packetTime = getTimestamp(); + var packetTime = context.currentTime; if (nextPacketTime < packetTime) nextPacketTime = packetTime; @@ -287,7 +246,7 @@ Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) { // Schedule packet source.buffer = audioBuffer; - source.start(nextPacketTime / 1000); + source.start(nextPacketTime); // Update timeline nextPacketTime += duration; @@ -298,7 +257,7 @@ Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) { this.sync = function sync() { // Calculate elapsed time since last sync - var now = getTimestamp(); + var now = context.currentTime; // Reschedule future playback time such that playback latency is // bounded within a reasonable latency threshold From 324c800167765b0fbe09dcbdb5ca3f546892ba53 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 30 Sep 2015 17:07:14 -0700 Subject: [PATCH 08/16] GUAC-1354: Use Guacamole.AudioPlayer.getSupportedTypes() to query available audio mimetypes within webapp. --- .../webapp/app/client/services/guacAudio.js | 77 +------------------ 1 file changed, 3 insertions(+), 74 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/services/guacAudio.js b/guacamole/src/main/webapp/app/client/services/guacAudio.js index 061820584..bacc62842 100644 --- a/guacamole/src/main/webapp/app/client/services/guacAudio.js +++ b/guacamole/src/main/webapp/app/client/services/guacAudio.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Glyptodon LLC + * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -31,82 +31,11 @@ angular.module('client').factory('guacAudio', [function guacAudio() { return new (function() { /** - * Array of codecs to test. + * Array of all supported audio mimetypes. * * @type String[] */ - var codecs = [ - 'audio/ogg; codecs="vorbis"', - 'audio/mp4; codecs="mp4a.40.5"', - 'audio/mpeg; codecs="mp3"', - 'audio/webm; codecs="vorbis"', - 'audio/wav; codecs=1' - ]; - - /** - * Array of all codecs that are reported as "probably" supported. - * - * @type String[] - */ - var probably_supported = []; - - /** - * Array of all codecs that are reported as "maybe" supported. - * - * @type String[] - */ - var maybe_supported = []; - - /** - * Internal audio element for the sake of testing codec support. If - * audio is explicitly not supported by the browser, this will instead - * be null. - * - * @type Audio - */ - var audio = null; - - // Attempt to create audio element - try { - audio = new Audio(); - } - catch (e) { - // If creation fails, allow audio to remain null - } - - /** - * Array of all supported audio mimetypes, ordered by liklihood of - * working. - */ - this.supported = []; - - // Build array of supported audio formats (if audio supported at all) - if (audio) { - codecs.forEach(function(mimetype) { - - var support_level = audio.canPlayType(mimetype); - - // Trim semicolon and trailer - var semicolon = mimetype.indexOf(";"); - if (semicolon !== -1) - mimetype = mimetype.substring(0, semicolon); - - // Partition by probably/maybe - if (support_level === "probably") - probably_supported.push(mimetype); - else if (support_level === "maybe") - maybe_supported.push(mimetype); - - }); - - // Add probably supported types first - Array.prototype.push.apply( - this.supported, probably_supported); - - // Prioritize "maybe" supported types second - Array.prototype.push.apply( - this.supported, maybe_supported); - } + this.supported = Guacamole.AudioPlayer.getSupportedTypes(); })(); From a3dd959dc41c0aafcaeb8f17694741b5f7ab4e48 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 30 Sep 2015 17:11:54 -0700 Subject: [PATCH 09/16] GUAC-1354: Do not recalculate max latency using packet duration. Audio packet duration will ALWAYS be roughly the same due to the max blob size. --- guacamole-common-js/src/main/webapp/modules/AudioPlayer.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js b/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js index 98a3592aa..938c04318 100644 --- a/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js +++ b/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js @@ -183,7 +183,7 @@ Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) { /** * The maximum amount of latency to allow between the buffered data stream * and the playback position, in seconds. Initially, this is set to - * roughly one third of a second, but it will be recalculated dynamically. + * roughly one third of a second. * * @private * @type Number @@ -199,9 +199,6 @@ Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) { // Calculate overall duration (in seconds) var duration = samples / format.rate; - // Recalculate latency threshold based on packet size - maxLatency = duration * 2; - // Determine exactly when packet CAN play var packetTime = context.currentTime; if (nextPacketTime < packetTime) From 079e3dad8c1df3e5ce4c0aba3f6b186461e0ace5 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 2 Oct 2015 16:42:24 -0700 Subject: [PATCH 10/16] GUAC-1354: Dynamically split and reassemble audio packets to minimize clicking. --- .../src/main/webapp/modules/AudioPlayer.js | 254 ++++++++++++++++-- 1 file changed, 232 insertions(+), 22 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js b/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js index 938c04318..98b3189ba 100644 --- a/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js +++ b/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js @@ -180,6 +180,18 @@ Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) { */ var reader = new Guacamole.ArrayBufferReader(stream); + /** + * The minimum size of an audio packet split by splitAudioPacket(), in + * seconds. Audio packets smaller than this will not be split, nor will the + * split result of a larger packet ever be smaller in size than this + * minimum. + * + * @private + * @constant + * @type Number + */ + var MIN_SPLIT_SIZE = 0.02; + /** * The maximum amount of latency to allow between the buffered data stream * and the playback position, in seconds. Initially, this is set to @@ -190,32 +202,209 @@ Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) { */ var maxLatency = 0.3; - // Play each received raw packet of audio immediately - reader.ondata = function playReceivedAudio(data) { + /** + * The type of typed array that will be used to represent each audio packet + * internally. This will be either Int8Array or Int16Array, depending on + * whether the raw audio format is 8-bit or 16-bit. + * + * @private + * @constructor + */ + var SampleArray = (format.bytesPerSample === 1) ? window.Int8Array : window.Int16Array; + + /** + * The maximum absolute value of any sample within a raw audio packet + * received by this audio player. This depends only on the size of each + * sample, and will be 128 for 8-bit audio and 32768 for 16-bit audio. + * + * @private + * @type Number + */ + var maxSampleValue = (format.bytesPerSample === 1) ? 128 : 32768; + + /** + * The queue of all pending audio packets, as an array of sample arrays. + * Audio packets which are pending playback will be added to this queue for + * further manipulation prior to scheduling via the Web Audio API. Once an + * audio packet leaves this queue and is scheduled via the Web Audio API, + * no further modifications can be made to that packet. + * + * @private + * @type SampleArray[] + */ + var packetQueue = []; + + /** + * Given an array of audio packets, returns a single audio packet + * containing the concatenation of those packets. + * + * @private + * @param {SampleArray[]} packets + * The array of audio packets to concatenate. + * + * @returns {SampleArray} + * A single audio packet containing the concatenation of all given + * audio packets. If no packets are provided, this will be undefined. + */ + var joinAudioPackets = function joinAudioPackets(packets) { + + // Do not bother joining if one or fewer packets are in the queue + if (packets.length <= 1) + return packets[0]; + + // Determine total sample length of the entire queue + var totalLength = 0; + packets.forEach(function addPacketLengths(packet) { + totalLength += packet.length; + }); + + // Append each packet within queue + var offset = 0; + var joined = new SampleArray(totalLength); + packets.forEach(function appendPacket(packet) { + joined.set(packet, offset); + offset += packet.length; + }); + + return joined; + + }; + + /** + * Given a single packet of audio data, splits off an arbitrary length of + * audio data from the beginning of that packet, returning the split result + * as an array of two packets. The split location is determined through an + * algorithm intended to minimize the liklihood of audible clicking between + * packets. If no such split location is possible, an array containing only + * the originally-provided audio packet is returned. + * + * @private + * @param {SampleArray} data + * The audio packet to split. + * + * @returns {SampleArray[]} + * An array of audio packets containing the result of splitting the + * provided audio packet. If splitting is possible, this array will + * contain two packets. If splitting is not possible, this array will + * contain only the originally-provided packet. + */ + var splitAudioPacket = function splitAudioPacket(data) { + + var minValue = Number.MAX_VALUE; + var optimalSplitLength = data.length; + + // Calculate number of whole samples in the provided audio packet AND + // in the minimum possible split packet + var samples = Math.floor(data.length / format.channels); + var minSplitSamples = Math.floor(format.rate * MIN_SPLIT_SIZE); + + // Calculate the beginning of the "end" of the audio packet + var start = Math.max( + format.channels * minSplitSamples, + format.channels * (samples - minSplitSamples) + ); + + // For all samples at the end of the given packet, find a point where + // the perceptible volume across all channels is lowest (and thus is + // the optimal point to split) + for (var offset = start; offset < data.length; offset += format.channels) { + + // Calculate the sum of all values across all channels (the result + // will be proportional to the average volume of a sample) + var totalValue = 0; + for (var channel = 0; channel < format.channels; channel++) { + totalValue += Math.abs(data[offset + channel]); + } + + // If this is the smallest average value thus far, set the split + // length such that the first packet ends with the current sample + if (totalValue <= minValue) { + optimalSplitLength = offset + format.channels; + minValue = totalValue; + } + + } + + // If packet is not split, return the supplied packet untouched + if (optimalSplitLength === data.length) + return [data]; + + // Otherwise, split the packet into two new packets according to the + // calculated optimal split length + return [ + data.slice(0, optimalSplitLength), + data.slice(optimalSplitLength) + ]; + + }; + + /** + * Pushes the given packet of audio data onto the playback queue. Unlike + * other private functions within Guacamole.RawAudioPlayer, the type of the + * ArrayBuffer packet of audio data here need not be specific to the type + * of audio (as with SampleArray). The ArrayBuffer type provided by a + * Guacamole.ArrayBufferReader, for example, is sufficient. Any necessary + * conversions will be performed automatically internally. + * + * @private + * @param {ArrayBuffer} data + * A raw packet of audio data that should be pushed onto the audio + * playback queue. + */ + var pushAudioPacket = function pushAudioPacket(data) { + packetQueue.push(new SampleArray(data)); + }; + + /** + * Shifts off and returns a packet of audio data from the beginning of the + * playback queue. The length of this audio packet is determined + * dynamically according to the click-reduction algorithm implemented by + * splitAudioPacket(). + * + * @returns {SampleArray} + * A packet of audio data pulled from the beginning of the playback + * queue. + */ + var shiftAudioPacket = function shiftAudioPacket() { + + // Flatten data in packet queue + var data = joinAudioPackets(packetQueue); + if (!data) + return null; + + // Pull an appropriate amount of data from the front of the queue + packetQueue = splitAudioPacket(data); + data = packetQueue.shift(); + + return data; + + }; + + /** + * Converts the given audio packet into an AudioBuffer, ready for playback + * by the Web Audio API. Unlike the raw audio packets received by this + * audio player, AudioBuffers require floating point samples and are split + * into isolated planes of channel-specific data. + * + * @private + * @param {SampleArray} data + * The raw audio packet that should be converted into a Web Audio API + * AudioBuffer. + * + * @returns {AudioBuffer} + * A new Web Audio API AudioBuffer containing the provided audio data, + * converted to the format used by the Web Audio API. + */ + var toAudioBuffer = function toAudioBuffer(data) { // Calculate total number of samples - var samples = data.byteLength / format.channels / format.bytesPerSample; - - // Calculate overall duration (in seconds) - var duration = samples / format.rate; + var samples = data.length / format.channels; // Determine exactly when packet CAN play var packetTime = context.currentTime; if (nextPacketTime < packetTime) nextPacketTime = packetTime; - // Obtain typed array view based on defined bytes per sample - var maxValue; - var source; - if (format.bytesPerSample === 1) { - source = new Int8Array(data); - maxValue = 128; - } - else { - source = new Int16Array(data); - maxValue = 32768; - } - // Get audio buffer for specified format var audioBuffer = context.createBuffer(format.channels, samples, format.rate); @@ -227,12 +416,33 @@ Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) { // Fill audio buffer with data for channel var offset = channel; for (var i = 0; i < samples; i++) { - audioData[i] = source[offset] / maxValue; + audioData[i] = data[offset] / maxSampleValue; offset += format.channels; } } + return audioBuffer; + + }; + + // Defer playback of received audio packets slightly + reader.ondata = function playReceivedAudio(data) { + + // Push received samples onto queue + pushAudioPacket(new SampleArray(data)); + + // Shift off an arbitrary packet of audio data from the queue (this may + // be different in size from the packet just pushed) + var packet = shiftAudioPacket(); + if (!packet) + return; + + // Determine exactly when packet CAN play + var packetTime = context.currentTime; + if (nextPacketTime < packetTime) + nextPacketTime = packetTime; + // Set up buffer source var source = context.createBufferSource(); source.connect(context.destination); @@ -242,11 +452,11 @@ Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) { source.start = source.noteOn; // Schedule packet - source.buffer = audioBuffer; + source.buffer = toAudioBuffer(packet); source.start(nextPacketTime); - // Update timeline - nextPacketTime += duration; + // Update timeline by duration of scheduled packet + nextPacketTime += packet.length / format.channels / format.rate; }; From d4f4ec0fb297b4c6fc4ec8ea0c8fa810ae255c48 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 2 Oct 2015 16:52:37 -0700 Subject: [PATCH 11/16] GUAC-1354: Use ArrayBuffer.slice - do not call slice directly on typed arrays (not widely supported). --- guacamole-common-js/src/main/webapp/modules/AudioPlayer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js b/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js index 98b3189ba..330881caa 100644 --- a/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js +++ b/guacamole-common-js/src/main/webapp/modules/AudioPlayer.js @@ -332,8 +332,8 @@ Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) { // Otherwise, split the packet into two new packets according to the // calculated optimal split length return [ - data.slice(0, optimalSplitLength), - data.slice(optimalSplitLength) + new SampleArray(data.buffer.slice(0, optimalSplitLength * format.bytesPerSample)), + new SampleArray(data.buffer.slice(optimalSplitLength * format.bytesPerSample)) ]; }; From 27fea9574bd6d5bb920cc20d8f58a5411cc05825 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 12 Oct 2015 10:53:53 -0700 Subject: [PATCH 12/16] GUAC-1349: Ensure file transfer dialog does not overflow viewport bounds. --- .../client/styles/file-transfer-dialog.css | 82 ++++++++++++++++++- .../templates/guacFileTransferManager.html | 20 +++-- 2 files changed, 92 insertions(+), 10 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/styles/file-transfer-dialog.css b/guacamole/src/main/webapp/app/client/styles/file-transfer-dialog.css index 5f7d594fa..666ca7f8d 100644 --- a/guacamole/src/main/webapp/app/client/styles/file-transfer-dialog.css +++ b/guacamole/src/main/webapp/app/client/styles/file-transfer-dialog.css @@ -28,14 +28,94 @@ z-index: 20; font-size: 0.8em; - padding: 0.5em; width: 4in; max-width: 100%; + max-height: 3in; } #file-transfer-dialog .transfer-manager { + + /* IE10 */ + display: -ms-flexbox; + -ms-flex-align: stretch; + -ms-flex-direction: column; + + /* Ancient Mozilla */ + display: -moz-box; + -moz-box-align: stretch; + -moz-box-orient: vertical; + + /* Ancient WebKit */ + display: -webkit-box; + -webkit-box-align: stretch; + -webkit-box-orient: vertical; + + /* Old WebKit */ + display: -webkit-flex; + -webkit-align-items: stretch; + -webkit-flex-direction: column; + + /* W3C */ + display: flex; + align-items: stretch; + flex-direction: column; + + max-width: inherit; + max-height: inherit; + border: 1px solid rgba(0, 0, 0, 0.5); box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.25); + +} + +#file-transfer-dialog .transfer-manager .header { + -ms-flex: 0 0 auto; + -moz-box-flex: 0; + -webkit-box-flex: 0; + -webkit-flex: 0 0 auto; + flex: 0 0 auto; +} + +#file-transfer-dialog .transfer-manager .transfer-manager-body { + + -ms-flex: 1 1 auto; + -moz-box-flex: 1; + -webkit-box-flex: 1; + -webkit-flex: 1 1 auto; + flex: 1 1 auto; + + overflow: auto; + +} + +/* + * Shrink maximum height if viewport is too small for default 3in dialog. + */ +@media all and (max-height: 3in) { + + #file-transfer-dialog { + max-height: 1.5in; + } + +} + +/* + * If viewport is too small for even the 1.5in dialog, fit all available space. + */ +@media all and (max-height: 1.5in) { + + #file-transfer-dialog { + height: 100%; + } + + #file-transfer-dialog .transfer-manager { + position: absolute; + left: 0.5em; + top: 0.5em; + right: 0.5em; + bottom: 0.5em; + } + } diff --git a/guacamole/src/main/webapp/app/client/templates/guacFileTransferManager.html b/guacamole/src/main/webapp/app/client/templates/guacFileTransferManager.html index 7de41078a..fc2a22f8d 100644 --- a/guacamole/src/main/webapp/app/client/templates/guacFileTransferManager.html +++ b/guacamole/src/main/webapp/app/client/templates/guacFileTransferManager.html @@ -27,15 +27,17 @@ - -
- - - + +
+
+ + + +
From c57893f5d340de89376926f628477135e36ba318 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 12 Oct 2015 17:10:46 -0700 Subject: [PATCH 13/16] GUAC-1345: Add simple property type which parses to a set of strings. --- .../basic/properties/StringSetProperty.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/StringSetProperty.java diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/StringSetProperty.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/StringSetProperty.java new file mode 100644 index 000000000..037427f3e --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/StringSetProperty.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.properties; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.properties.GuacamoleProperty; + +/** + * A GuacamoleProperty whose value is a Set of unique Strings. The string value + * parsed to produce this set is a comma-delimited list. Duplicate values are + * ignored, as is any whitespace following delimiters. To maintain + * compatibility with the behavior of Java properties in general, only + * whitespace at the beginning of each value is ignored; trailing whitespace + * becomes part of the value. + * + * @author Michael Jumper + */ +public abstract class StringSetProperty implements GuacamoleProperty> { + + /** + * A pattern which matches against the delimiters between values. This is + * currently simply a comma and any following whitespace. Parts of the + * input string which match this pattern will not be included in the parsed + * result. + */ + private static final Pattern DELIMITER_PATTERN = Pattern.compile(",\\s*"); + + @Override + public Set parseValue(String values) throws GuacamoleException { + + // If no property provided, return null. + if (values == null) + return null; + + // Split string into a set of individual values + List valueList = Arrays.asList(DELIMITER_PATTERN.split(values)); + return new HashSet(valueList); + + } + +} From 35a828bfb6290e19ec07fd1064f198cff8a85df8 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 12 Oct 2015 17:11:46 -0700 Subject: [PATCH 14/16] GUAC-1345: Do not attempt to retrieve translation files for languages which are not explicitly listed as available by the REST API. --- .../app/locale/services/translationLoader.js | 63 +++++++++++++------ 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/guacamole/src/main/webapp/app/locale/services/translationLoader.js b/guacamole/src/main/webapp/app/locale/services/translationLoader.js index ec05ef540..a95f85f1b 100644 --- a/guacamole/src/main/webapp/app/locale/services/translationLoader.js +++ b/guacamole/src/main/webapp/app/locale/services/translationLoader.js @@ -29,9 +29,10 @@ angular.module('locale').factory('translationLoader', ['$injector', function translationLoader($injector) { // Required services - var $http = $injector.get('$http'); - var $q = $injector.get('$q'); - var cacheService = $injector.get('cacheService'); + var $http = $injector.get('$http'); + var $q = $injector.get('$q'); + var cacheService = $injector.get('cacheService'); + var languageService = $injector.get('languageService'); /** * Satisfies a translation request for the given key by searching for the @@ -62,22 +63,48 @@ angular.module('locale').factory('translationLoader', ['$injector', function tra return; } - // Attempt to retrieve language - $http({ - cache : cacheService.languages, - method : 'GET', - url : 'translations/' + encodeURIComponent(currentKey) + '.json' - }) - - // Resolve promise if translation retrieved successfully - .success(function translationFileRetrieved(translation) { - deferred.resolve(translation); - }) - - // Retry with remaining languages if translation file could not be retrieved - .error(function translationFileUnretrievable() { + /** + * Continues trying possible translation files until no possibilities + * exist. + * + * @private + */ + var tryNextTranslation = function tryNextTranslation() { satisfyTranslation(deferred, requestedKey, remainingKeys); - }); + }; + + // Retrieve list of supported languages + languageService.getLanguages() + + // Attempt to retrieve translation if language is supported + .success(function retrievedLanguages(languages) { + + // Skip retrieval if language is not supported + if (!(currentKey in languages)) { + tryNextTranslation(); + return; + } + + // Attempt to retrieve language + $http({ + cache : cacheService.languages, + method : 'GET', + url : 'translations/' + encodeURIComponent(currentKey) + '.json' + }) + + // Resolve promise if translation retrieved successfully + .success(function translationFileRetrieved(translation) { + deferred.resolve(translation); + }) + + // Retry with remaining languages if translation file could not be + // retrieved + .error(tryNextTranslation); + + }) + + // Retry with remaining languages if translation does not exist + .error(tryNextTranslation); }; From fdd23036a224675ac82267698df52be133bc0217 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 12 Oct 2015 17:23:41 -0700 Subject: [PATCH 15/16] GUAC-1345: Add 'allowed-languages' property. Restrict loaded languages to keys explicitly listed, if any. --- .../net/basic/extension/ExtensionModule.java | 3 +- .../extension/LanguageResourceService.java | 70 +++++++++++++++++++ .../properties/BasicGuacamoleProperties.java | 14 ++++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java index 3e8c70af3..b9f070412 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/ExtensionModule.java @@ -100,7 +100,7 @@ public class ExtensionModule extends ServletModule { /** * Service for adding and retrieving language resources. */ - private final LanguageResourceService languageResourceService = new LanguageResourceService(); + private final LanguageResourceService languageResourceService; /** * Returns the classloader that should be used as the parent classloader @@ -139,6 +139,7 @@ public class ExtensionModule extends ServletModule { */ public ExtensionModule(Environment environment) { this.environment = environment; + this.languageResourceService = new LanguageResourceService(environment); } /** diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/LanguageResourceService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/LanguageResourceService.java index 9955b5bf6..dece975e6 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/LanguageResourceService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/extension/LanguageResourceService.java @@ -36,6 +36,9 @@ import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.node.JsonNodeFactory; import org.codehaus.jackson.node.ObjectNode; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.environment.Environment; +import org.glyptodon.guacamole.net.basic.properties.BasicGuacamoleProperties; import org.glyptodon.guacamole.net.basic.resource.ByteArrayResource; import org.glyptodon.guacamole.net.basic.resource.Resource; import org.glyptodon.guacamole.net.basic.resource.WebApplicationResource; @@ -76,6 +79,13 @@ public class LanguageResourceService { */ private static final Pattern LANGUAGE_KEY_PATTERN = Pattern.compile(".*/([a-z]+(_[A-Z]+)?)\\.json"); + /** + * The set of all language keys which are explicitly listed as allowed + * within guacamole.properties, or null if all defined languages should be + * allowed. + */ + private final Set allowedLanguages; + /** * Map of all language resources by language key. Language keys are * language and country code pairs, separated by an underscore, like @@ -86,6 +96,35 @@ public class LanguageResourceService { */ private final Map resources = new HashMap(); + /** + * Creates a new service for tracking and parsing available translations + * which reads its configuration from the given environment. + * + * @param environment + * The environment from which the configuration properties of this + * service should be read. + */ + public LanguageResourceService(Environment environment) { + + Set parsedAllowedLanguages; + + // Parse list of available languages from properties + try { + parsedAllowedLanguages = environment.getProperty(BasicGuacamoleProperties.ALLOWED_LANGUAGES); + logger.debug("Available languages will be restricted to: {}", parsedAllowedLanguages); + } + + // Warn of failure to parse + catch (GuacamoleException e) { + parsedAllowedLanguages = null; + logger.error("Unable to parse list of allowed languages: {}", e.getMessage()); + logger.debug("Error parsing list of allowed languages.", e); + } + + this.allowedLanguages = parsedAllowedLanguages; + + } + /** * Derives a language key from the filename within the given path, if * possible. If the filename is not a valid language key, null is returned. @@ -184,6 +223,31 @@ public class LanguageResourceService { } + /** + * Returns whether a language having the given key should be allowed to be + * loaded. If language availability restrictions are imposed through + * guacamole.properties, this may return false in some cases. By default, + * this function will always return true. Note that just because a language + * key is allowed to be loaded does not imply that the language key is + * valid. + * + * @param languageKey + * The language key of the language to test. + * + * @return + * true if the given language key should be allowed to be loaded, false + * otherwise. + */ + private boolean isLanguageAllowed(String languageKey) { + + // If no list is provided, all languages are implicitly available + if (allowedLanguages == null) + return true; + + return allowedLanguages.contains(languageKey); + + } + /** * Adds or overlays the given language resource, which need not exist in * the ServletContext. If a language resource is already defined for the @@ -202,6 +266,12 @@ public class LanguageResourceService { */ public void addLanguageResource(String key, Resource resource) { + // Skip loading of language if not allowed + if (!isLanguageAllowed(key)) { + logger.debug("OMITTING language: \"{}\"", key); + return; + } + // Merge language resources if already defined Resource existing = resources.get(key); if (existing != null) { diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/BasicGuacamoleProperties.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/BasicGuacamoleProperties.java index a1b2e1336..2bccfdb5e 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/BasicGuacamoleProperties.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/BasicGuacamoleProperties.java @@ -24,6 +24,7 @@ package org.glyptodon.guacamole.net.basic.properties; import org.glyptodon.guacamole.properties.FileGuacamoleProperty; import org.glyptodon.guacamole.properties.IntegerGuacamoleProperty; +import org.glyptodon.guacamole.properties.StringGuacamoleProperty; /** * Properties used by the default Guacamole web application. @@ -73,4 +74,17 @@ public class BasicGuacamoleProperties { }; + /** + * Comma-separated list of all allowed languages, where each language is + * represented by a language key, such as "en" or "en_US". If specified, + * only languages within this list will be listed as available by the REST + * service. + */ + public static final StringSetProperty ALLOWED_LANGUAGES = new StringSetProperty() { + + @Override + public String getName() { return "allowed-languages"; } + + }; + } From 4b26750a3ab890bdf4b8238bd86f297b37fde3a5 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 14 Oct 2015 17:18:08 -0700 Subject: [PATCH 16/16] GUAC-1364: Finally remove the @AuthProviderRESTExposure annotation. --- .../basic/rest/AuthProviderRESTExposure.java | 38 ------ ...Wrapper.java => RESTExceptionWrapper.java} | 15 ++- .../net/basic/rest/RESTMethodMatcher.java | 109 ++++++++++++++++++ .../net/basic/rest/RESTServletModule.java | 6 +- .../ActiveConnectionRESTService.java | 3 - .../net/basic/rest/auth/TokenRESTService.java | 17 +-- .../connection/ConnectionRESTService.java | 7 -- .../ConnectionGroupRESTService.java | 6 - .../basic/rest/schema/SchemaRESTService.java | 5 - .../net/basic/rest/user/UserRESTService.java | 43 +++---- 10 files changed, 145 insertions(+), 104 deletions(-) delete mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/AuthProviderRESTExposure.java rename guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/{AuthProviderRESTExceptionWrapper.java => RESTExceptionWrapper.java} (90%) create mode 100644 guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTMethodMatcher.java diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/AuthProviderRESTExposure.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/AuthProviderRESTExposure.java deleted file mode 100644 index 2a450c2c3..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/AuthProviderRESTExposure.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2014 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package org.glyptodon.guacamole.net.basic.rest; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks that a method exposes functionality from the Guacamole AuthenticationProvider - * using a REST interface. - * - * @author James Muehlner - */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD}) -public @interface AuthProviderRESTExposure {} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/AuthProviderRESTExceptionWrapper.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTExceptionWrapper.java similarity index 90% rename from guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/AuthProviderRESTExceptionWrapper.java rename to guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTExceptionWrapper.java index d73903ef5..9cb0bc807 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/AuthProviderRESTExceptionWrapper.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTExceptionWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Glyptodon LLC + * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -34,13 +34,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * A method interceptor to wrap some custom exception handling around methods - * that expose AuthenticationProvider functionality through the REST interface. - * Translates various types of GuacamoleExceptions into appropriate HTTP responses. - * + * A method interceptor which wraps custom exception handling around methods + * which can throw GuacamoleExceptions and which are exposed through the REST + * interface. The various types of GuacamoleExceptions are automatically + * translated into appropriate HTTP responses, including JSON describing the + * error that occurred. + * * @author James Muehlner + * @author Michael Jumper */ -public class AuthProviderRESTExceptionWrapper implements MethodInterceptor { +public class RESTExceptionWrapper implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTMethodMatcher.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTMethodMatcher.java new file mode 100644 index 000000000..643addcd9 --- /dev/null +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTMethodMatcher.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.net.basic.rest; + +import com.google.inject.matcher.AbstractMatcher; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import javax.ws.rs.HttpMethod; +import org.glyptodon.guacamole.GuacamoleException; + +/** + * A Guice Matcher which matches only methods which throw GuacamoleException + * (or a subclass thereof) and are explicitly annotated as with an HTTP method + * annotation like @GET or @POST. Any method which + * throws GuacamoleException and is annotated with an annotation that is + * annotated with @HttpMethod will match. + * + * @author Michael Jumper + */ +public class RESTMethodMatcher extends AbstractMatcher { + + /** + * Returns whether the given method throws the specified exception type, + * including any subclasses of that type. + * + * @param method + * The method to test. + * + * @param exceptionType + * The exception type to test for. + * + * @return + * true if the given method throws an exception of the specified type, + * false otherwise. + */ + private boolean methodThrowsException(Method method, + Class exceptionType) { + + // Check whether the method throws an exception of the specified type + for (Class thrownType : method.getExceptionTypes()) { + if (exceptionType.isAssignableFrom(thrownType)) + return true; + } + + // No such exception is declared to be thrown + return false; + + } + + /** + * Returns whether the given method is annotated as a REST method. A REST + * method is annotated with an annotation which is annotated with + * @HttpMethod. + * + * @param method + * The method to test. + * + * @return + * true if the given method is annotated as a REST method, false + * otherwise. + */ + private boolean isRESTMethod(Method method) { + + // Check whether the required REST annotations are present + for (Annotation annotation : method.getAnnotations()) { + + // A method is a REST method if it is annotated with @HttpMethod + Class annotationType = annotation.annotationType(); + if (annotationType.isAnnotationPresent(HttpMethod.class)) + return true; + + } + + // The method is not an HTTP method + return false; + + } + + @Override + public boolean matches(Method method) { + + // Guacamole REST methods are REST methods which throw + // GuacamoleExceptions + return isRESTMethod(method) + && methodThrowsException(method, GuacamoleException.class); + + } + +} diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java index 478c2182e..48db9857b 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java @@ -45,11 +45,11 @@ public class RESTServletModule extends ServletModule { @Override protected void configureServlets() { - // Bind @AuthProviderRESTExposure annotation + // Automatically translate GuacamoleExceptions for REST methods bindInterceptor( Matchers.any(), - Matchers.annotatedWith(AuthProviderRESTExposure.class), - new AuthProviderRESTExceptionWrapper() + new RESTMethodMatcher(), + new RESTExceptionWrapper() ); // Bind convenience services used by the REST API diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/ActiveConnectionRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/ActiveConnectionRESTService.java index 42ae1c15f..2f2b69994 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/ActiveConnectionRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/activeconnection/ActiveConnectionRESTService.java @@ -47,7 +47,6 @@ import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; import org.glyptodon.guacamole.net.basic.GuacamoleSession; import org.glyptodon.guacamole.net.basic.rest.APIPatch; -import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.PATCH; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; @@ -107,7 +106,6 @@ public class ActiveConnectionRESTService { * If an error is encountered while retrieving active connections. */ @GET - @AuthProviderRESTExposure public Map getActiveConnections(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @QueryParam("permission") List permissions) @@ -166,7 +164,6 @@ public class ActiveConnectionRESTService { * If an error occurs while deleting the active connections. */ @PATCH - @AuthProviderRESTExposure public void patchTunnels(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, List> patches) throws GuacamoleException { diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenRESTService.java index 998cd5e44..4351b890e 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenRESTService.java @@ -39,6 +39,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.xml.bind.DatatypeConverter; import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.net.auth.AuthenticatedUser; @@ -49,10 +50,7 @@ import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo; import org.glyptodon.guacamole.net.auth.credentials.GuacamoleCredentialsException; import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; import org.glyptodon.guacamole.net.basic.GuacamoleSession; -import org.glyptodon.guacamole.net.basic.rest.APIError; import org.glyptodon.guacamole.net.basic.rest.APIRequest; -import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; -import org.glyptodon.guacamole.net.basic.rest.APIException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -464,7 +462,6 @@ public class TokenRESTService { * If an error prevents successful authentication. */ @POST - @AuthProviderRESTExposure public APIAuthenticationResult createToken(@FormParam("username") String username, @FormParam("password") String password, @FormParam("token") String token, @@ -523,16 +520,20 @@ public class TokenRESTService { * Invalidates a specific auth token, effectively logging out the associated * user. * - * @param authToken The token being invalidated. + * @param authToken + * The token being invalidated. + * + * @throws GuacamoleException + * If the specified token does not exist. */ @DELETE @Path("/{token}") - @AuthProviderRESTExposure - public void invalidateToken(@PathParam("token") String authToken) { + public void invalidateToken(@PathParam("token") String authToken) + throws GuacamoleException { GuacamoleSession session = tokenSessionMap.remove(authToken); if (session == null) - throw new APIException(APIError.Type.NOT_FOUND, "No such token."); + throw new GuacamoleResourceNotFoundException("No such token."); session.invalidate(); diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionRESTService.java index 270e3c302..caa4aa99a 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connection/ConnectionRESTService.java @@ -49,7 +49,6 @@ import org.glyptodon.guacamole.net.auth.permission.ObjectPermissionSet; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; import org.glyptodon.guacamole.net.basic.GuacamoleSession; -import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; @@ -105,7 +104,6 @@ public class ConnectionRESTService { */ @GET @Path("/{connectionID}") - @AuthProviderRESTExposure public APIConnection getConnection(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionID") String connectionID) @@ -141,7 +139,6 @@ public class ConnectionRESTService { */ @GET @Path("/{connectionID}/parameters") - @AuthProviderRESTExposure public Map getConnectionParameters(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionID") String connectionID) @@ -195,7 +192,6 @@ public class ConnectionRESTService { */ @GET @Path("/{connectionID}/history") - @AuthProviderRESTExposure public List getConnectionHistory(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionID") String connectionID) @@ -235,7 +231,6 @@ public class ConnectionRESTService { */ @DELETE @Path("/{connectionID}") - @AuthProviderRESTExposure public void deleteConnection(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionID") String connectionID) @@ -275,7 +270,6 @@ public class ConnectionRESTService { */ @POST @Produces(MediaType.TEXT_PLAIN) - @AuthProviderRESTExposure public String createConnection(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, APIConnection connection) throws GuacamoleException { @@ -320,7 +314,6 @@ public class ConnectionRESTService { */ @PUT @Path("/{connectionID}") - @AuthProviderRESTExposure public void updateConnection(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionID") String connectionID, diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupRESTService.java index 5899d32d7..77bcbdfae 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/connectiongroup/ConnectionGroupRESTService.java @@ -41,7 +41,6 @@ import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.auth.permission.ObjectPermission; import org.glyptodon.guacamole.net.basic.GuacamoleSession; -import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.slf4j.Logger; @@ -96,7 +95,6 @@ public class ConnectionGroupRESTService { */ @GET @Path("/{connectionGroupID}") - @AuthProviderRESTExposure public APIConnectionGroup getConnectionGroup(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionGroupID") String connectionGroupID) @@ -138,7 +136,6 @@ public class ConnectionGroupRESTService { */ @GET @Path("/{connectionGroupID}/tree") - @AuthProviderRESTExposure public APIConnectionGroup getConnectionGroupTree(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionGroupID") String connectionGroupID, @@ -176,7 +173,6 @@ public class ConnectionGroupRESTService { */ @DELETE @Path("/{connectionGroupID}") - @AuthProviderRESTExposure public void deleteConnectionGroup(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionGroupID") String connectionGroupID) @@ -218,7 +214,6 @@ public class ConnectionGroupRESTService { */ @POST @Produces(MediaType.TEXT_PLAIN) - @AuthProviderRESTExposure public String createConnectionGroup(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, APIConnectionGroup connectionGroup) throws GuacamoleException { @@ -263,7 +258,6 @@ public class ConnectionGroupRESTService { */ @PUT @Path("/{connectionGroupID}") - @AuthProviderRESTExposure public void updateConnectionGroup(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionGroupID") String connectionGroupID, diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/schema/SchemaRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/schema/SchemaRESTService.java index 6a09cba32..66a2aa78f 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/schema/SchemaRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/schema/SchemaRESTService.java @@ -38,7 +38,6 @@ import org.glyptodon.guacamole.environment.LocalEnvironment; import org.glyptodon.guacamole.form.Form; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.basic.GuacamoleSession; -import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; import org.glyptodon.guacamole.protocols.ProtocolInfo; @@ -86,7 +85,6 @@ public class SchemaRESTService { */ @GET @Path("/users/attributes") - @AuthProviderRESTExposure public Collection
getUserAttributes(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier) throws GuacamoleException { @@ -118,7 +116,6 @@ public class SchemaRESTService { */ @GET @Path("/connections/attributes") - @AuthProviderRESTExposure public Collection getConnectionAttributes(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier) throws GuacamoleException { @@ -151,7 +148,6 @@ public class SchemaRESTService { */ @GET @Path("/connectionGroups/attributes") - @AuthProviderRESTExposure public Collection getConnectionGroupAttributes(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier) throws GuacamoleException { @@ -186,7 +182,6 @@ public class SchemaRESTService { */ @GET @Path("/protocols") - @AuthProviderRESTExposure public Map getProtocols(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier) throws GuacamoleException { diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java index 687d4ff85..404757b86 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/user/UserRESTService.java @@ -39,8 +39,10 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; +import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleResourceNotFoundException; +import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.Directory; @@ -53,12 +55,9 @@ import org.glyptodon.guacamole.net.auth.permission.Permission; import org.glyptodon.guacamole.net.auth.permission.SystemPermission; import org.glyptodon.guacamole.net.auth.permission.SystemPermissionSet; import org.glyptodon.guacamole.net.basic.GuacamoleSession; -import org.glyptodon.guacamole.net.basic.rest.APIError; import org.glyptodon.guacamole.net.basic.rest.APIPatch; import static org.glyptodon.guacamole.net.basic.rest.APIPatch.Operation.add; import static org.glyptodon.guacamole.net.basic.rest.APIPatch.Operation.remove; -import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; -import org.glyptodon.guacamole.net.basic.rest.APIException; import org.glyptodon.guacamole.net.basic.rest.ObjectRetrievalService; import org.glyptodon.guacamole.net.basic.rest.PATCH; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; @@ -150,7 +149,6 @@ public class UserRESTService { * If an error is encountered while retrieving users. */ @GET - @AuthProviderRESTExposure public List getUsers(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @QueryParam("permission") List permissions) @@ -205,7 +203,6 @@ public class UserRESTService { */ @GET @Path("/{username}") - @AuthProviderRESTExposure public APIUser getUser(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("username") String username) @@ -241,7 +238,6 @@ public class UserRESTService { */ @POST @Produces(MediaType.TEXT_PLAIN) - @AuthProviderRESTExposure public String createUser(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, APIUser user) throws GuacamoleException { @@ -285,7 +281,6 @@ public class UserRESTService { */ @PUT @Path("/{username}") - @AuthProviderRESTExposure public void updateUser(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("username") String username, APIUser user) @@ -299,14 +294,11 @@ public class UserRESTService { // Validate data and path are sane if (!user.getUsername().equals(username)) - throw new APIException(APIError.Type.BAD_REQUEST, - "Username in path does not match username provided JSON data."); + throw new GuacamoleClientException("Username in path does not match username provided JSON data."); // A user may not use this endpoint to modify himself - if (userContext.self().getIdentifier().equals(user.getUsername())) { - throw new APIException(APIError.Type.PERMISSION_DENIED, - "Permission denied."); - } + if (userContext.self().getIdentifier().equals(user.getUsername())) + throw new GuacamoleSecurityException("Permission denied."); // Get the user User existingUser = retrievalService.retrieveUser(userContext, username); @@ -349,7 +341,6 @@ public class UserRESTService { */ @PUT @Path("/{username}/password") - @AuthProviderRESTExposure public void updatePassword(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("username") String username, @@ -369,18 +360,15 @@ public class UserRESTService { // Verify that the old password was correct try { AuthenticationProvider authProvider = userContext.getAuthenticationProvider(); - if (authProvider.authenticateUser(credentials) == null) { - throw new APIException(APIError.Type.PERMISSION_DENIED, - "Permission denied."); - } + if (authProvider.authenticateUser(credentials) == null) + throw new GuacamoleSecurityException("Permission denied."); } // Pass through any credentials exceptions as simple permission denied catch (GuacamoleCredentialsException e) { - throw new APIException(APIError.Type.PERMISSION_DENIED, - "Permission denied."); + throw new GuacamoleSecurityException("Permission denied."); } - + // Get the user directory Directory userDirectory = userContext.getUserDirectory(); @@ -414,7 +402,6 @@ public class UserRESTService { */ @DELETE @Path("/{username}") - @AuthProviderRESTExposure public void deleteUser(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("username") String username) @@ -458,7 +445,6 @@ public class UserRESTService { */ @GET @Path("/{username}/permissions") - @AuthProviderRESTExposure public APIPermissionSet getPermissions(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("username") String username) @@ -499,11 +485,14 @@ public class UserRESTService { * * @param permission * The permission being added or removed from the set. + * + * @throws GuacamoleException + * If the requested patch operation is not supported. */ private void updatePermissionSet( APIPatch.Operation operation, PermissionSetPatch permissionSetPatch, - PermissionType permission) { + PermissionType permission) throws GuacamoleException { // Add or remove permission based on operation switch (operation) { @@ -520,8 +509,7 @@ public class UserRESTService { // Unsupported patch operation default: - throw new APIException(APIError.Type.BAD_REQUEST, - "Unsupported patch operation: \"" + operation + "\""); + throw new GuacamoleClientException("Unsupported patch operation: \"" + operation + "\""); } @@ -553,7 +541,6 @@ public class UserRESTService { */ @PATCH @Path("/{username}/permissions") - @AuthProviderRESTExposure public void patchPermissions(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("username") String username, @@ -645,7 +632,7 @@ public class UserRESTService { // Otherwise, the path is not supported else - throw new APIException(APIError.Type.BAD_REQUEST, "Unsupported patch path: \"" + path + "\""); + throw new GuacamoleClientException("Unsupported patch path: \"" + path + "\""); } // end for each patch operation