Merge 0.9.14 changes back to master.
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/form/CASTicketField.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/form/CASTicketField.java
index 2fb96b0..eb0f68e 100644
--- a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/form/CASTicketField.java
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/form/CASTicketField.java
@@ -41,6 +41,12 @@
     public static final String PARAMETER_NAME = "ticket";
 
     /**
+     * The standard URI name for the CAS login resource.
+     */
+    private static final String CAS_LOGIN_URI = "login";
+
+
+    /**
      * The full URI which the field should link to.
      */
     private final String authorizationURI;
@@ -57,11 +63,6 @@
      *     The full URL of the endpoint accepting CAS authentication
      *     requests.
      *
-     * @param clientID
-     *     The ID of the CAS client. This is normally determined ahead of
-     *     time by the CAS service through some manual credential request
-     *     procedure.
-     *
      * @param redirectURI
      *     The URI that the CAS service should redirect to upon successful
      *     authentication.
@@ -73,8 +74,16 @@
 
         // Build authorization URI from given values
         try {
-            this.authorizationURI = authorizationEndpoint
-                    + "?service=" + URLEncoder.encode(redirectURI, "UTF-8");
+            final StringBuilder sb = new StringBuilder();
+            sb.append(authorizationEndpoint);
+            // user might configure the endpoint with a trailing slash
+            if (sb.charAt(sb.length() - 1) != '/') {
+                sb.append('/');
+            }
+            sb.append(CAS_LOGIN_URI);
+            sb.append("?service=");
+            sb.append(URLEncoder.encode(redirectURI, "UTF-8"));
+            this.authorizationURI = sb.toString();
         }
 
         // Java is required to provide UTF-8 support
diff --git a/guacamole-common-js/src/main/webapp/modules/Tunnel.js b/guacamole-common-js/src/main/webapp/modules/Tunnel.js
index 508aa4c..c8f8502 100644
--- a/guacamole-common-js/src/main/webapp/modules/Tunnel.js
+++ b/guacamole-common-js/src/main/webapp/modules/Tunnel.js
@@ -55,6 +55,25 @@
     this.sendMessage = function(elements) {};
 
     /**
+     * Changes the stored numeric state of this tunnel, firing the onstatechange
+     * event if the new state is different and a handler has been defined.
+     *
+     * @private
+     * @param {Number} state
+     *     The new state of this tunnel.
+     */
+    this.setState = function(state) {
+
+        // Notify only if state changes
+        if (state !== this.state) {
+            this.state = state;
+            if (this.onstatechange)
+                this.onstatechange(state);
+        }
+
+    };
+
+    /**
      * The current state of this tunnel.
      * 
      * @type {Number}
@@ -163,8 +182,13 @@
  *     Whether tunnel requests will be cross-domain, and thus must use CORS
  *     mechanisms and headers. By default, it is assumed that tunnel requests
  *     will be made to the same domain.
+ *
+ * @param {Object} [extraTunnelHeaders={}]
+ *     Key value pairs containing the header names and values of any additional
+ *     headers to be sent in tunnel requests. By default, no extra headers will
+ *     be added.
  */
-Guacamole.HTTPTunnel = function(tunnelURL, crossDomain) {
+Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {
 
     /**
      * Reference to this HTTP tunnel.
@@ -196,6 +220,32 @@
     var receive_timeout = null;
 
     /**
+     * Additional headers to be sent in tunnel requests. This dictionary can be
+     * populated with key/value header pairs to pass information such as authentication
+     * tokens, etc.
+     *
+     * @private
+     */
+    var extraHeaders = extraTunnelHeaders || {};
+
+    /**
+     * Adds the configured additional headers to the given request.
+     *
+     * @param {XMLHttpRequest} request
+     *     The request where the configured extra headers will be added.
+     *
+     * @param {Object} headers
+     *     The headers to be added to the request.
+     *
+     * @private
+     */
+    function addExtraHeaders(request, headers) {
+        for (var name in headers) {
+            request.setRequestHeader(name, headers[name]);
+        }
+    }
+
+    /**
      * Initiates a timeout which, if data is not received, causes the tunnel
      * to close with an error.
      * 
@@ -239,14 +289,11 @@
 
         }
 
-        // Mark as closed
-        tunnel.state = Guacamole.Tunnel.State.CLOSED;
-
         // Reset output message buffer
         sendingMessages = false;
 
-        if (tunnel.onstatechange)
-            tunnel.onstatechange(tunnel.state);
+        // Mark as closed
+        tunnel.setState(Guacamole.Tunnel.State.CLOSED);
 
     }
 
@@ -306,6 +353,7 @@
             var message_xmlhttprequest = new XMLHttpRequest();
             message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel.uuid);
             message_xmlhttprequest.withCredentials = withCredentials;
+            addExtraHeaders(message_xmlhttprequest, extraHeaders);
             message_xmlhttprequest.setRequestHeader("Content-type", "application/octet-stream");
 
             // Once response received, send next queued event.
@@ -537,6 +585,7 @@
         var xmlhttprequest = new XMLHttpRequest();
         xmlhttprequest.open("GET", TUNNEL_READ + tunnel.uuid + ":" + (request_id++));
         xmlhttprequest.withCredentials = withCredentials;
+        addExtraHeaders(xmlhttprequest, extraHeaders);
         xmlhttprequest.send(null);
 
         return xmlhttprequest;
@@ -548,6 +597,9 @@
         // Start waiting for connect
         reset_timeout();
 
+        // Mark the tunnel as connecting
+        tunnel.setState(Guacamole.Tunnel.State.CONNECTING);
+
         // Start tunnel and connect
         var connect_xmlhttprequest = new XMLHttpRequest();
         connect_xmlhttprequest.onreadystatechange = function() {
@@ -566,9 +618,8 @@
             // Get UUID from response
             tunnel.uuid = connect_xmlhttprequest.responseText;
 
-            tunnel.state = Guacamole.Tunnel.State.OPEN;
-            if (tunnel.onstatechange)
-                tunnel.onstatechange(tunnel.state);
+            // Mark as open
+            tunnel.setState(Guacamole.Tunnel.State.OPEN);
 
             // Start reading data
             handleResponse(makeRequest());
@@ -577,6 +628,7 @@
 
         connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, true);
         connect_xmlhttprequest.withCredentials = withCredentials;
+        addExtraHeaders(connect_xmlhttprequest, extraHeaders);
         connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
         connect_xmlhttprequest.send(data);
 
@@ -698,9 +750,7 @@
             tunnel.onerror(status);
 
         // Mark as closed
-        tunnel.state = Guacamole.Tunnel.State.CLOSED;
-        if (tunnel.onstatechange)
-            tunnel.onstatechange(tunnel.state);
+        tunnel.setState(Guacamole.Tunnel.State.CLOSED);
 
         socket.close();
 
@@ -747,6 +797,9 @@
 
         reset_timeout();
 
+        // Mark the tunnel as connecting
+        tunnel.setState(Guacamole.Tunnel.State.CONNECTING);
+
         // Connect socket
         socket = new WebSocket(tunnelURL + "?" + data, "guacamole");
 
@@ -814,9 +867,7 @@
                             tunnel.uuid = elements[0];
 
                         // Tunnel is now open and UUID is available
-                        tunnel.state = Guacamole.Tunnel.State.OPEN;
-                        if (tunnel.onstatechange)
-                            tunnel.onstatechange(tunnel.state);
+                        tunnel.setState(Guacamole.Tunnel.State.OPEN);
 
                     }
 
@@ -1040,8 +1091,13 @@
  *     Whether tunnel requests will be cross-domain, and thus must use CORS
  *     mechanisms and headers. By default, it is assumed that tunnel requests
  *     will be made to the same domain.
+ *
+ * @param {Object} [extraTunnelHeaders={}]
+ *     Key value pairs containing the header names and values of any additional
+ *     headers to be sent in tunnel requests. By default, no extra headers will
+ *     be added.
  */
-Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain) {
+Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain, extraTunnelHeaders) {
 
     /**
      * Reference to this Guacamole.StaticHTTPTunnel.
@@ -1060,23 +1116,30 @@
     var xhr = null;
 
     /**
-     * Changes the stored numeric state of this tunnel, firing the onstatechange
-     * event if the new state is different and a handler has been defined.
+     * Additional headers to be sent in tunnel requests. This dictionary can be
+     * populated with key/value header pairs to pass information such as authentication
+     * tokens, etc.
      *
      * @private
-     * @param {Number} state
-     *     The new state of this tunnel.
      */
-    var setState = function setState(state) {
+    var extraHeaders = extraTunnelHeaders || {};
 
-        // Notify only if state changes
-        if (state !== tunnel.state) {
-            tunnel.state = state;
-            if (tunnel.onstatechange)
-                tunnel.onstatechange(state);
+    /**
+     * Adds the configured additional headers to the given request.
+     *
+     * @param {XMLHttpRequest} request
+     *     The request where the configured extra headers will be added.
+     *
+     * @param {Object} headers
+     *     The headers to be added to the request.
+     *
+     * @private
+     */
+    function addExtraHeaders(request, headers) {
+        for (var name in headers) {
+            request.setRequestHeader(name, headers[name]);
         }
-
-    };
+    }
 
     /**
      * Returns the Guacamole protocol status code which most closely
@@ -1133,12 +1196,13 @@
         tunnel.disconnect();
 
         // Connection is now starting
-        setState(Guacamole.Tunnel.State.CONNECTING);
+        tunnel.setState(Guacamole.Tunnel.State.CONNECTING);
 
         // Start a new connection
         xhr = new XMLHttpRequest();
         xhr.open('GET', url);
         xhr.withCredentials = !!crossDomain;
+        addExtraHeaders(xhr, extraHeaders);
         xhr.responseType = 'text';
         xhr.send(null);
 
@@ -1160,7 +1224,7 @@
             if (xhr.readyState === 3 || xhr.readyState === 4) {
 
                 // Connection is open
-                setState(Guacamole.Tunnel.State.OPEN);
+                tunnel.setState(Guacamole.Tunnel.State.OPEN);
 
                 var buffer = xhr.responseText;
                 var length = buffer.length;
@@ -1200,7 +1264,7 @@
         }
 
         // Connection is now closed
-        setState(Guacamole.Tunnel.State.CLOSED);
+        tunnel.setState(Guacamole.Tunnel.State.CLOSED);
 
     };