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,30 @@ 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, try to use a default implementation + if (!audioPlayer) + audioPlayer = Guacamole.AudioPlayer.getInstance(stream, mimetype); - // Send success response - guac_client.sendAck(stream_index, "OK", 0x0000); + // If we have successfully retrieved an audio player, 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 +1117,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 diff --git a/guacamole-common-js/src/main/webapp/modules/Keyboard.js b/guacamole-common-js/src/main/webapp/modules/Keyboard.js index 42617d6d5..1f57a3b70 100644 --- a/guacamole-common-js/src/main/webapp/modules/Keyboard.js +++ b/guacamole-common-js/src/main/webapp/modules/Keyboard.js @@ -325,6 +325,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 +334,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,8 +599,8 @@ 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 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"; } + + }; + } 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); + + } + +} 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 b48ff9d37..044fa5ae9 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 @@ -46,11 +46,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 2d122cd01..258d81962 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.net.basic.rest.history.APIConnectionRecord; @@ -106,7 +105,6 @@ public class ConnectionRESTService { */ @GET @Path("/{connectionID}") - @AuthProviderRESTExposure public APIConnection getConnection(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionID") String connectionID) @@ -142,7 +140,6 @@ public class ConnectionRESTService { */ @GET @Path("/{connectionID}/parameters") - @AuthProviderRESTExposure public Map getConnectionParameters(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionID") String connectionID) @@ -196,7 +193,6 @@ public class ConnectionRESTService { */ @GET @Path("/{connectionID}/history") - @AuthProviderRESTExposure public List getConnectionHistory(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionID") String connectionID) @@ -236,7 +232,6 @@ public class ConnectionRESTService { */ @DELETE @Path("/{connectionID}") - @AuthProviderRESTExposure public void deleteConnection(@QueryParam("token") String authToken, @PathParam("dataSource") String authProviderIdentifier, @PathParam("connectionID") String connectionID) @@ -276,7 +271,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 { @@ -321,7 +315,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 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(); })(); 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 @@ - -
- - - + +
+
+ + + +
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); };