mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 13:17:41 +00:00
Add official WebSocket support, always enabled, for both Jetty and Tomcat.
This commit is contained in:
@@ -134,6 +134,29 @@
|
|||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Jetty servlet API (websocket) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-websocket</artifactId>
|
||||||
|
<version>8.1.1.v20120215</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Tomcat servlet API (websocket) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.tomcat</groupId>
|
||||||
|
<artifactId>tomcat-catalina</artifactId>
|
||||||
|
<version>7.0.37</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.tomcat</groupId>
|
||||||
|
<artifactId>tomcat-coyote</artifactId>
|
||||||
|
<version>7.0.37</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
@@ -0,0 +1,161 @@
|
|||||||
|
|
||||||
|
package net.sourceforge.guacamole.net.basic.websocket.jetty;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Guacamole - Clientless Remote Desktop
|
||||||
|
* Copyright (C) 2010 Michael Jumper
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
import net.sourceforge.guacamole.GuacamoleException;
|
||||||
|
import net.sourceforge.guacamole.GuacamoleServerException;
|
||||||
|
import net.sourceforge.guacamole.net.auth.Credentials;
|
||||||
|
import net.sourceforge.guacamole.net.auth.UserContext;
|
||||||
|
import net.sourceforge.guacamole.net.basic.AuthenticatingHttpServlet;
|
||||||
|
import org.eclipse.jetty.websocket.WebSocket;
|
||||||
|
import org.eclipse.jetty.websocket.WebSocketServlet;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A WebSocket servlet wrapped around an AuthenticatingHttpServlet.
|
||||||
|
*
|
||||||
|
* @author Michael Jumper
|
||||||
|
*/
|
||||||
|
public abstract class AuthenticatingWebSocketServlet extends WebSocketServlet {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logger for this class.
|
||||||
|
*/
|
||||||
|
private Logger logger = LoggerFactory.getLogger(AuthenticatingWebSocketServlet.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapped authenticating servlet.
|
||||||
|
*/
|
||||||
|
private AuthenticatingHttpServlet auth_servlet =
|
||||||
|
new AuthenticatingHttpServlet() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void authenticatedService(UserContext context,
|
||||||
|
HttpServletRequest request, HttpServletResponse response)
|
||||||
|
throws GuacamoleException {
|
||||||
|
|
||||||
|
try {
|
||||||
|
// If authenticated, service request
|
||||||
|
service_websocket_request(request, response);
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
throw new GuacamoleServerException(
|
||||||
|
"Cannot service WebSocket request (I/O error).", e);
|
||||||
|
}
|
||||||
|
catch (ServletException e) {
|
||||||
|
throw new GuacamoleServerException(
|
||||||
|
"Cannot service WebSocket request (internal error).", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void service(HttpServletRequest request,
|
||||||
|
HttpServletResponse response)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
|
||||||
|
// Authenticate all inbound requests
|
||||||
|
auth_servlet.service(request, response);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actually services the given request, bypassing the service() override
|
||||||
|
* and the authentication scheme.
|
||||||
|
*
|
||||||
|
* @param request The HttpServletRequest to service.
|
||||||
|
* @param response The associated HttpServletResponse.
|
||||||
|
* @throws IOException If an I/O error occurs while handling the request.
|
||||||
|
* @throws ServletException If an internal error occurs while handling the
|
||||||
|
* request.
|
||||||
|
*/
|
||||||
|
private void service_websocket_request(HttpServletRequest request,
|
||||||
|
HttpServletResponse response)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
|
||||||
|
// Bypass override and service WebSocket request
|
||||||
|
super.service(request, response);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebSocket doWebSocketConnect(HttpServletRequest request,
|
||||||
|
String protocol) {
|
||||||
|
|
||||||
|
// Get session and user context
|
||||||
|
HttpSession session = request.getSession(true);
|
||||||
|
UserContext context = getUserContext(session);
|
||||||
|
|
||||||
|
// Ensure user logged in
|
||||||
|
if (context == null) {
|
||||||
|
logger.warn("User no longer logged in upon WebSocket connect.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect WebSocket
|
||||||
|
return authenticatedConnect(context, request, protocol);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the credentials associated with the given session.
|
||||||
|
*
|
||||||
|
* @param session The session to retrieve credentials from.
|
||||||
|
* @return The credentials associated with the given session.
|
||||||
|
*/
|
||||||
|
protected Credentials getCredentials(HttpSession session) {
|
||||||
|
return (Credentials) session.getAttribute(
|
||||||
|
AuthenticatingHttpServlet.CREDENTIALS_ATTRIBUTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the UserContext associated with the given session.
|
||||||
|
*
|
||||||
|
* @param session The session to retrieve UserContext from.
|
||||||
|
* @return The UserContext associated with the given session.
|
||||||
|
*/
|
||||||
|
protected UserContext getUserContext(HttpSession session) {
|
||||||
|
return (UserContext) session.getAttribute(
|
||||||
|
AuthenticatingHttpServlet.CONTEXT_ATTRIBUTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called after the credentials given in the request (if any)
|
||||||
|
* are authenticated. If the current session is not associated with
|
||||||
|
* valid credentials, this function will not be called.
|
||||||
|
*
|
||||||
|
* @param context The current UserContext.
|
||||||
|
* @param request The HttpServletRequest being serviced.
|
||||||
|
* @param protocol The protocol being used over the WebSocket connection.
|
||||||
|
*/
|
||||||
|
protected abstract WebSocket authenticatedConnect(
|
||||||
|
UserContext context,
|
||||||
|
HttpServletRequest request, String protocol);
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,243 @@
|
|||||||
|
package net.sourceforge.guacamole.net.basic.websocket.jetty;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Guacamole - Clientless Remote Desktop
|
||||||
|
* Copyright (C) 2010 Michael Jumper
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
import net.sourceforge.guacamole.GuacamoleException;
|
||||||
|
import net.sourceforge.guacamole.GuacamoleSecurityException;
|
||||||
|
import net.sourceforge.guacamole.net.GuacamoleSocket;
|
||||||
|
import net.sourceforge.guacamole.net.GuacamoleTunnel;
|
||||||
|
import net.sourceforge.guacamole.net.auth.Connection;
|
||||||
|
import net.sourceforge.guacamole.net.auth.Credentials;
|
||||||
|
import net.sourceforge.guacamole.net.auth.Directory;
|
||||||
|
import net.sourceforge.guacamole.net.auth.UserContext;
|
||||||
|
import net.sourceforge.guacamole.net.basic.event.SessionListenerCollection;
|
||||||
|
import net.sourceforge.guacamole.net.event.TunnelCloseEvent;
|
||||||
|
import net.sourceforge.guacamole.net.event.TunnelConnectEvent;
|
||||||
|
import net.sourceforge.guacamole.net.event.listener.TunnelCloseListener;
|
||||||
|
import net.sourceforge.guacamole.net.event.listener.TunnelConnectListener;
|
||||||
|
import net.sourceforge.guacamole.protocol.GuacamoleClientInformation;
|
||||||
|
import org.eclipse.jetty.websocket.WebSocket;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticating tunnel servlet implementation which uses WebSocket as a
|
||||||
|
* tunnel backend, rather than HTTP.
|
||||||
|
*/
|
||||||
|
public class BasicGuacamoleWebSocketTunnelServlet extends AuthenticatingWebSocketServlet {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logger for this class.
|
||||||
|
*/
|
||||||
|
private Logger logger = LoggerFactory.getLogger(BasicGuacamoleWebSocketTunnelServlet.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies all listeners in the given collection that a tunnel has been
|
||||||
|
* connected.
|
||||||
|
*
|
||||||
|
* @param listeners A collection of all listeners that should be notified.
|
||||||
|
* @param credentials The credentials associated with the authentication
|
||||||
|
* request that connected the tunnel.
|
||||||
|
* @param tunnel The tunnel being connected.
|
||||||
|
* @return true if all listeners are allowing the tunnel to connect,
|
||||||
|
* or if there are no listeners, and false if any listener is
|
||||||
|
* canceling the connection. Note that once one listener cancels,
|
||||||
|
* no other listeners will run.
|
||||||
|
* @throws GuacamoleException If any listener throws an error while being
|
||||||
|
* notified. Note that if any listener throws an
|
||||||
|
* error, the connect is canceled, and no other
|
||||||
|
* listeners will run.
|
||||||
|
*/
|
||||||
|
private boolean notifyConnect(Collection listeners,
|
||||||
|
Credentials credentials, GuacamoleTunnel tunnel)
|
||||||
|
throws GuacamoleException {
|
||||||
|
|
||||||
|
// Build event for auth success
|
||||||
|
TunnelConnectEvent event = new TunnelConnectEvent(credentials, tunnel);
|
||||||
|
|
||||||
|
// Notify all listeners
|
||||||
|
for (Object listener : listeners) {
|
||||||
|
if (listener instanceof TunnelConnectListener) {
|
||||||
|
|
||||||
|
// Cancel immediately if hook returns false
|
||||||
|
if (!((TunnelConnectListener) listener).tunnelConnected(event))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies all listeners in the given collection that a tunnel has been
|
||||||
|
* closed.
|
||||||
|
*
|
||||||
|
* @param listeners A collection of all listeners that should be notified.
|
||||||
|
* @param credentials The credentials associated with the authentication
|
||||||
|
* request that closed the tunnel.
|
||||||
|
* @param tunnel The tunnel being closed.
|
||||||
|
* @return true if all listeners are allowing the tunnel to close,
|
||||||
|
* or if there are no listeners, and false if any listener is
|
||||||
|
* canceling the close. Note that once one listener cancels,
|
||||||
|
* no other listeners will run.
|
||||||
|
* @throws GuacamoleException If any listener throws an error while being
|
||||||
|
* notified. Note that if any listener throws an
|
||||||
|
* error, the close is canceled, and no other
|
||||||
|
* listeners will run.
|
||||||
|
*/
|
||||||
|
private boolean notifyClose(Collection listeners,
|
||||||
|
Credentials credentials, GuacamoleTunnel tunnel)
|
||||||
|
throws GuacamoleException {
|
||||||
|
|
||||||
|
// Build event for auth success
|
||||||
|
TunnelCloseEvent event = new TunnelCloseEvent(credentials, tunnel);
|
||||||
|
|
||||||
|
// Notify all listeners
|
||||||
|
for (Object listener : listeners) {
|
||||||
|
if (listener instanceof TunnelCloseListener) {
|
||||||
|
|
||||||
|
// Cancel immediately if hook returns false
|
||||||
|
if (!((TunnelCloseListener) listener).tunnelClosed(event))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapped GuacamoleHTTPTunnelServlet which will handle all authenticated
|
||||||
|
* requests.
|
||||||
|
*/
|
||||||
|
private GuacamoleWebSocketTunnelServlet tunnelServlet =
|
||||||
|
new GuacamoleWebSocketTunnelServlet() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected GuacamoleTunnel doConnect(HttpServletRequest request)
|
||||||
|
throws GuacamoleException {
|
||||||
|
|
||||||
|
HttpSession httpSession = request.getSession(true);
|
||||||
|
|
||||||
|
// Get listeners
|
||||||
|
final SessionListenerCollection listeners;
|
||||||
|
try {
|
||||||
|
listeners = new SessionListenerCollection(httpSession);
|
||||||
|
}
|
||||||
|
catch (GuacamoleException e) {
|
||||||
|
logger.error("Failed to retrieve listeners. Authentication canceled.", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ID of connection
|
||||||
|
String id = request.getParameter("id");
|
||||||
|
|
||||||
|
// Get credentials
|
||||||
|
final Credentials credentials = getCredentials(httpSession);
|
||||||
|
|
||||||
|
// Get context
|
||||||
|
UserContext context = getUserContext(httpSession);
|
||||||
|
|
||||||
|
// Get connection directory
|
||||||
|
Directory<String, Connection> directory = context.getConnectionDirectory();
|
||||||
|
|
||||||
|
// If no credentials in session, not authorized
|
||||||
|
if (credentials == null)
|
||||||
|
throw new GuacamoleSecurityException("Cannot connect - user not logged in.");
|
||||||
|
|
||||||
|
// Get authorized connection
|
||||||
|
Connection connection = directory.get(id);
|
||||||
|
if (connection == null) {
|
||||||
|
logger.warn("Connection id={} not found.", id);
|
||||||
|
throw new GuacamoleSecurityException("Requested connection is not authorized.");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Successful connection from {} to \"{}\".", request.getRemoteAddr(), id);
|
||||||
|
|
||||||
|
// Get client information
|
||||||
|
GuacamoleClientInformation info = new GuacamoleClientInformation();
|
||||||
|
|
||||||
|
// Set width if provided
|
||||||
|
String width = request.getParameter("width");
|
||||||
|
if (width != null)
|
||||||
|
info.setOptimalScreenWidth(Integer.parseInt(width));
|
||||||
|
|
||||||
|
// Set height if provided
|
||||||
|
String height = request.getParameter("height");
|
||||||
|
if (height != null)
|
||||||
|
info.setOptimalScreenHeight(Integer.parseInt(height));
|
||||||
|
|
||||||
|
// Add audio mimetypes
|
||||||
|
String[] audio_mimetypes = request.getParameterValues("audio");
|
||||||
|
if (audio_mimetypes != null)
|
||||||
|
info.getAudioMimetypes().addAll(Arrays.asList(audio_mimetypes));
|
||||||
|
|
||||||
|
// Add video mimetypes
|
||||||
|
String[] video_mimetypes = request.getParameterValues("video");
|
||||||
|
if (video_mimetypes != null)
|
||||||
|
info.getVideoMimetypes().addAll(Arrays.asList(video_mimetypes));
|
||||||
|
|
||||||
|
// Connect socket
|
||||||
|
GuacamoleSocket socket = connection.connect(info);
|
||||||
|
|
||||||
|
// Associate socket with tunnel
|
||||||
|
GuacamoleTunnel tunnel = new GuacamoleTunnel(socket) {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws GuacamoleException {
|
||||||
|
|
||||||
|
// Only close if not canceled
|
||||||
|
if (!notifyClose(listeners, credentials, this))
|
||||||
|
throw new GuacamoleException("Tunnel close canceled by listener.");
|
||||||
|
|
||||||
|
// Close if no exception due to listener
|
||||||
|
super.close();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// Notify listeners about connection
|
||||||
|
if (!notifyConnect(listeners, credentials, tunnel)) {
|
||||||
|
logger.info("Connection canceled by listener.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tunnel;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected WebSocket authenticatedConnect(UserContext context,
|
||||||
|
HttpServletRequest request, String protocol) {
|
||||||
|
return tunnelServlet.doWebSocketConnect(request, protocol);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@@ -0,0 +1,133 @@
|
|||||||
|
package net.sourceforge.guacamole.net.basic.websocket.jetty;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Guacamole - Clientless Remote Desktop
|
||||||
|
* Copyright (C) 2010 Michael Jumper
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import net.sourceforge.guacamole.GuacamoleException;
|
||||||
|
import net.sourceforge.guacamole.io.GuacamoleReader;
|
||||||
|
import net.sourceforge.guacamole.io.GuacamoleWriter;
|
||||||
|
import net.sourceforge.guacamole.net.GuacamoleTunnel;
|
||||||
|
import org.eclipse.jetty.websocket.WebSocket;
|
||||||
|
import org.eclipse.jetty.websocket.WebSocket.Connection;
|
||||||
|
import org.eclipse.jetty.websocket.WebSocketServlet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A WebSocketServlet partial re-implementation of GuacamoleTunnelServlet.
|
||||||
|
*
|
||||||
|
* @author Michael Jumper
|
||||||
|
*/
|
||||||
|
public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default, minimum buffer size for instructions.
|
||||||
|
*/
|
||||||
|
private static final int BUFFER_SIZE = 8192;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) {
|
||||||
|
|
||||||
|
// Get tunnel
|
||||||
|
final GuacamoleTunnel tunnel;
|
||||||
|
|
||||||
|
try {
|
||||||
|
tunnel = doConnect(request);
|
||||||
|
}
|
||||||
|
catch (GuacamoleException e) {
|
||||||
|
return null; // FIXME: Can throw exception?
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return new WebSocket which communicates through tunnel
|
||||||
|
return new WebSocket.OnTextMessage() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(String string) {
|
||||||
|
GuacamoleWriter writer = tunnel.acquireWriter();
|
||||||
|
|
||||||
|
// Write message received
|
||||||
|
try {
|
||||||
|
writer.write(string.toCharArray());
|
||||||
|
}
|
||||||
|
catch (GuacamoleException e) {
|
||||||
|
// FIXME: Handle exception
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnel.releaseWriter();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOpen(final Connection connection) {
|
||||||
|
|
||||||
|
Thread readThread = new Thread() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
|
||||||
|
StringBuilder buffer = new StringBuilder(BUFFER_SIZE);
|
||||||
|
GuacamoleReader reader = tunnel.acquireReader();
|
||||||
|
char[] readMessage;
|
||||||
|
|
||||||
|
try {
|
||||||
|
while ((readMessage = reader.read()) != null) {
|
||||||
|
|
||||||
|
// Buffer message
|
||||||
|
buffer.append(readMessage);
|
||||||
|
|
||||||
|
// Flush if we expect to wait or buffer is getting full
|
||||||
|
if (!reader.available() || buffer.length() >= BUFFER_SIZE) {
|
||||||
|
connection.sendMessage(buffer.toString());
|
||||||
|
buffer.setLength(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
// FIXME: Handle exception
|
||||||
|
}
|
||||||
|
catch (GuacamoleException e) {
|
||||||
|
// FIXME: Handle exception
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
readThread.start();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClose(int i, String string) {
|
||||||
|
try {
|
||||||
|
tunnel.close();
|
||||||
|
}
|
||||||
|
catch (GuacamoleException e) {
|
||||||
|
// FIXME: Handle exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract GuacamoleTunnel doConnect(HttpServletRequest request)
|
||||||
|
throws GuacamoleException;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Jetty WebSocket tunnel implementation. The classes here require at least
|
||||||
|
* Jetty 8, and may change significantly as there is no common WebSocket
|
||||||
|
* API for Java yet.
|
||||||
|
*/
|
||||||
|
package net.sourceforge.guacamole.net.basic.websocket.jetty;
|
||||||
|
|
@@ -0,0 +1,161 @@
|
|||||||
|
|
||||||
|
package net.sourceforge.guacamole.net.basic.websocket.tomcat;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Guacamole - Clientless Remote Desktop
|
||||||
|
* Copyright (C) 2010 Michael Jumper
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
import net.sourceforge.guacamole.GuacamoleException;
|
||||||
|
import net.sourceforge.guacamole.GuacamoleServerException;
|
||||||
|
import net.sourceforge.guacamole.net.auth.Credentials;
|
||||||
|
import net.sourceforge.guacamole.net.auth.UserContext;
|
||||||
|
import net.sourceforge.guacamole.net.basic.AuthenticatingHttpServlet;
|
||||||
|
import org.apache.catalina.websocket.StreamInbound;
|
||||||
|
import org.apache.catalina.websocket.WebSocketServlet;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A WebSocket servlet wrapped around an AuthenticatingHttpServlet.
|
||||||
|
*
|
||||||
|
* @author Michael Jumper
|
||||||
|
*/
|
||||||
|
public abstract class AuthenticatingWebSocketServlet extends WebSocketServlet {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logger for this class.
|
||||||
|
*/
|
||||||
|
private Logger logger = LoggerFactory.getLogger(AuthenticatingWebSocketServlet.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapped authenticating servlet.
|
||||||
|
*/
|
||||||
|
private AuthenticatingHttpServlet auth_servlet =
|
||||||
|
new AuthenticatingHttpServlet() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void authenticatedService(UserContext context,
|
||||||
|
HttpServletRequest request, HttpServletResponse response)
|
||||||
|
throws GuacamoleException {
|
||||||
|
|
||||||
|
try {
|
||||||
|
// If authenticated, service request
|
||||||
|
service_websocket_request(request, response);
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
throw new GuacamoleServerException(
|
||||||
|
"Cannot service WebSocket request (I/O error).", e);
|
||||||
|
}
|
||||||
|
catch (ServletException e) {
|
||||||
|
throw new GuacamoleServerException(
|
||||||
|
"Cannot service WebSocket request (internal error).", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void service(HttpServletRequest request,
|
||||||
|
HttpServletResponse response)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
|
||||||
|
// Authenticate all inbound requests
|
||||||
|
auth_servlet.service(request, response);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actually services the given request, bypassing the service() override
|
||||||
|
* and the authentication scheme.
|
||||||
|
*
|
||||||
|
* @param request The HttpServletRequest to service.
|
||||||
|
* @param response The associated HttpServletResponse.
|
||||||
|
* @throws IOException If an I/O error occurs while handling the request.
|
||||||
|
* @throws ServletException If an internal error occurs while handling the
|
||||||
|
* request.
|
||||||
|
*/
|
||||||
|
private void service_websocket_request(HttpServletRequest request,
|
||||||
|
HttpServletResponse response)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
|
||||||
|
// Bypass override and service WebSocket request
|
||||||
|
super.service(request, response);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StreamInbound createWebSocketInbound(String protocol,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
|
||||||
|
// Get session and user context
|
||||||
|
HttpSession session = request.getSession(true);
|
||||||
|
UserContext context = getUserContext(session);
|
||||||
|
|
||||||
|
// Ensure user logged in
|
||||||
|
if (context == null) {
|
||||||
|
logger.warn("User no longer logged in upon WebSocket connect.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect WebSocket
|
||||||
|
return authenticatedConnect(context, request, protocol);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the credentials associated with the given session.
|
||||||
|
*
|
||||||
|
* @param session The session to retrieve credentials from.
|
||||||
|
* @return The credentials associated with the given session.
|
||||||
|
*/
|
||||||
|
protected Credentials getCredentials(HttpSession session) {
|
||||||
|
return (Credentials) session.getAttribute(
|
||||||
|
AuthenticatingHttpServlet.CREDENTIALS_ATTRIBUTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the UserContext associated with the given session.
|
||||||
|
*
|
||||||
|
* @param session The session to retrieve UserContext from.
|
||||||
|
* @return The UserContext associated with the given session.
|
||||||
|
*/
|
||||||
|
protected UserContext getUserContext(HttpSession session) {
|
||||||
|
return (UserContext) session.getAttribute(
|
||||||
|
AuthenticatingHttpServlet.CONTEXT_ATTRIBUTE);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called after the credentials given in the request (if any)
|
||||||
|
* are authenticated. If the current session is not associated with
|
||||||
|
* valid credentials, this function will not be called.
|
||||||
|
*
|
||||||
|
* @param context The current UserContext.
|
||||||
|
* @param request The HttpServletRequest being serviced.
|
||||||
|
* @param protocol The protocol being used over the WebSocket connection.
|
||||||
|
*/
|
||||||
|
protected abstract StreamInbound authenticatedConnect(
|
||||||
|
UserContext context,
|
||||||
|
HttpServletRequest request, String protocol);
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,243 @@
|
|||||||
|
package net.sourceforge.guacamole.net.basic.websocket.tomcat;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Guacamole - Clientless Remote Desktop
|
||||||
|
* Copyright (C) 2010 Michael Jumper
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
import net.sourceforge.guacamole.GuacamoleException;
|
||||||
|
import net.sourceforge.guacamole.GuacamoleSecurityException;
|
||||||
|
import net.sourceforge.guacamole.net.GuacamoleSocket;
|
||||||
|
import net.sourceforge.guacamole.net.GuacamoleTunnel;
|
||||||
|
import net.sourceforge.guacamole.net.auth.Connection;
|
||||||
|
import net.sourceforge.guacamole.net.auth.Credentials;
|
||||||
|
import net.sourceforge.guacamole.net.auth.Directory;
|
||||||
|
import net.sourceforge.guacamole.net.auth.UserContext;
|
||||||
|
import net.sourceforge.guacamole.net.basic.event.SessionListenerCollection;
|
||||||
|
import net.sourceforge.guacamole.net.event.TunnelCloseEvent;
|
||||||
|
import net.sourceforge.guacamole.net.event.TunnelConnectEvent;
|
||||||
|
import net.sourceforge.guacamole.net.event.listener.TunnelCloseListener;
|
||||||
|
import net.sourceforge.guacamole.net.event.listener.TunnelConnectListener;
|
||||||
|
import net.sourceforge.guacamole.protocol.GuacamoleClientInformation;
|
||||||
|
import org.apache.catalina.websocket.StreamInbound;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticating tunnel servlet implementation which uses WebSocket as a
|
||||||
|
* tunnel backend, rather than HTTP.
|
||||||
|
*/
|
||||||
|
public class BasicGuacamoleWebSocketTunnelServlet extends AuthenticatingWebSocketServlet {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logger for this class.
|
||||||
|
*/
|
||||||
|
private Logger logger = LoggerFactory.getLogger(BasicGuacamoleWebSocketTunnelServlet.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies all listeners in the given collection that a tunnel has been
|
||||||
|
* connected.
|
||||||
|
*
|
||||||
|
* @param listeners A collection of all listeners that should be notified.
|
||||||
|
* @param credentials The credentials associated with the authentication
|
||||||
|
* request that connected the tunnel.
|
||||||
|
* @param tunnel The tunnel being connected.
|
||||||
|
* @return true if all listeners are allowing the tunnel to connect,
|
||||||
|
* or if there are no listeners, and false if any listener is
|
||||||
|
* canceling the connection. Note that once one listener cancels,
|
||||||
|
* no other listeners will run.
|
||||||
|
* @throws GuacamoleException If any listener throws an error while being
|
||||||
|
* notified. Note that if any listener throws an
|
||||||
|
* error, the connect is canceled, and no other
|
||||||
|
* listeners will run.
|
||||||
|
*/
|
||||||
|
private boolean notifyConnect(Collection listeners,
|
||||||
|
Credentials credentials, GuacamoleTunnel tunnel)
|
||||||
|
throws GuacamoleException {
|
||||||
|
|
||||||
|
// Build event for auth success
|
||||||
|
TunnelConnectEvent event = new TunnelConnectEvent(credentials, tunnel);
|
||||||
|
|
||||||
|
// Notify all listeners
|
||||||
|
for (Object listener : listeners) {
|
||||||
|
if (listener instanceof TunnelConnectListener) {
|
||||||
|
|
||||||
|
// Cancel immediately if hook returns false
|
||||||
|
if (!((TunnelConnectListener) listener).tunnelConnected(event))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies all listeners in the given collection that a tunnel has been
|
||||||
|
* closed.
|
||||||
|
*
|
||||||
|
* @param listeners A collection of all listeners that should be notified.
|
||||||
|
* @param credentials The credentials associated with the authentication
|
||||||
|
* request that closed the tunnel.
|
||||||
|
* @param tunnel The tunnel being closed.
|
||||||
|
* @return true if all listeners are allowing the tunnel to close,
|
||||||
|
* or if there are no listeners, and false if any listener is
|
||||||
|
* canceling the close. Note that once one listener cancels,
|
||||||
|
* no other listeners will run.
|
||||||
|
* @throws GuacamoleException If any listener throws an error while being
|
||||||
|
* notified. Note that if any listener throws an
|
||||||
|
* error, the close is canceled, and no other
|
||||||
|
* listeners will run.
|
||||||
|
*/
|
||||||
|
private boolean notifyClose(Collection listeners,
|
||||||
|
Credentials credentials, GuacamoleTunnel tunnel)
|
||||||
|
throws GuacamoleException {
|
||||||
|
|
||||||
|
// Build event for auth success
|
||||||
|
TunnelCloseEvent event = new TunnelCloseEvent(credentials, tunnel);
|
||||||
|
|
||||||
|
// Notify all listeners
|
||||||
|
for (Object listener : listeners) {
|
||||||
|
if (listener instanceof TunnelCloseListener) {
|
||||||
|
|
||||||
|
// Cancel immediately if hook returns false
|
||||||
|
if (!((TunnelCloseListener) listener).tunnelClosed(event))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapped GuacamoleHTTPTunnelServlet which will handle all authenticated
|
||||||
|
* requests.
|
||||||
|
*/
|
||||||
|
private GuacamoleWebSocketTunnelServlet tunnelServlet =
|
||||||
|
new GuacamoleWebSocketTunnelServlet() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected GuacamoleTunnel doConnect(HttpServletRequest request)
|
||||||
|
throws GuacamoleException {
|
||||||
|
|
||||||
|
HttpSession httpSession = request.getSession(true);
|
||||||
|
|
||||||
|
// Get listeners
|
||||||
|
final SessionListenerCollection listeners;
|
||||||
|
try {
|
||||||
|
listeners = new SessionListenerCollection(httpSession);
|
||||||
|
}
|
||||||
|
catch (GuacamoleException e) {
|
||||||
|
logger.error("Failed to retrieve listeners. Authentication canceled.", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get ID of connection
|
||||||
|
String id = request.getParameter("id");
|
||||||
|
|
||||||
|
// Get credentials
|
||||||
|
final Credentials credentials = getCredentials(httpSession);
|
||||||
|
|
||||||
|
// Get context
|
||||||
|
UserContext context = getUserContext(httpSession);
|
||||||
|
|
||||||
|
// Get connection directory
|
||||||
|
Directory<String, Connection> directory = context.getConnectionDirectory();
|
||||||
|
|
||||||
|
// If no credentials in session, not authorized
|
||||||
|
if (credentials == null)
|
||||||
|
throw new GuacamoleSecurityException("Cannot connect - user not logged in.");
|
||||||
|
|
||||||
|
// Get authorized connection
|
||||||
|
Connection connection = directory.get(id);
|
||||||
|
if (connection == null) {
|
||||||
|
logger.warn("Connection id={} not found.", id);
|
||||||
|
throw new GuacamoleSecurityException("Requested connection is not authorized.");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Successful connection from {} to \"{}\".", request.getRemoteAddr(), id);
|
||||||
|
|
||||||
|
// Get client information
|
||||||
|
GuacamoleClientInformation info = new GuacamoleClientInformation();
|
||||||
|
|
||||||
|
// Set width if provided
|
||||||
|
String width = request.getParameter("width");
|
||||||
|
if (width != null)
|
||||||
|
info.setOptimalScreenWidth(Integer.parseInt(width));
|
||||||
|
|
||||||
|
// Set height if provided
|
||||||
|
String height = request.getParameter("height");
|
||||||
|
if (height != null)
|
||||||
|
info.setOptimalScreenHeight(Integer.parseInt(height));
|
||||||
|
|
||||||
|
// Add audio mimetypes
|
||||||
|
String[] audio_mimetypes = request.getParameterValues("audio");
|
||||||
|
if (audio_mimetypes != null)
|
||||||
|
info.getAudioMimetypes().addAll(Arrays.asList(audio_mimetypes));
|
||||||
|
|
||||||
|
// Add video mimetypes
|
||||||
|
String[] video_mimetypes = request.getParameterValues("video");
|
||||||
|
if (video_mimetypes != null)
|
||||||
|
info.getVideoMimetypes().addAll(Arrays.asList(video_mimetypes));
|
||||||
|
|
||||||
|
// Connect socket
|
||||||
|
GuacamoleSocket socket = connection.connect(info);
|
||||||
|
|
||||||
|
// Associate socket with tunnel
|
||||||
|
GuacamoleTunnel tunnel = new GuacamoleTunnel(socket) {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws GuacamoleException {
|
||||||
|
|
||||||
|
// Only close if not canceled
|
||||||
|
if (!notifyClose(listeners, credentials, this))
|
||||||
|
throw new GuacamoleException("Tunnel close canceled by listener.");
|
||||||
|
|
||||||
|
// Close if no exception due to listener
|
||||||
|
super.close();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// Notify listeners about connection
|
||||||
|
if (!notifyConnect(listeners, credentials, tunnel)) {
|
||||||
|
logger.info("Connection canceled by listener.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tunnel;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected StreamInbound authenticatedConnect(UserContext context,
|
||||||
|
HttpServletRequest request, String protocol) {
|
||||||
|
return tunnelServlet.createWebSocketInbound(protocol, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@@ -0,0 +1,166 @@
|
|||||||
|
package net.sourceforge.guacamole.net.basic.websocket.tomcat;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Guacamole - Clientless Remote Desktop
|
||||||
|
* Copyright (C) 2010 Michael Jumper
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.nio.CharBuffer;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import net.sourceforge.guacamole.GuacamoleException;
|
||||||
|
import net.sourceforge.guacamole.io.GuacamoleReader;
|
||||||
|
import net.sourceforge.guacamole.io.GuacamoleWriter;
|
||||||
|
import net.sourceforge.guacamole.net.GuacamoleTunnel;
|
||||||
|
import org.apache.catalina.websocket.StreamInbound;
|
||||||
|
import org.apache.catalina.websocket.WebSocketServlet;
|
||||||
|
import org.apache.catalina.websocket.WsOutbound;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A WebSocketServlet partial re-implementation of GuacamoleTunnelServlet.
|
||||||
|
*
|
||||||
|
* @author Michael Jumper
|
||||||
|
*/
|
||||||
|
public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default, minimum buffer size for instructions.
|
||||||
|
*/
|
||||||
|
private static final int BUFFER_SIZE = 8192;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logger for this class.
|
||||||
|
*/
|
||||||
|
private Logger logger = LoggerFactory.getLogger(GuacamoleWebSocketTunnelServlet.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StreamInbound createWebSocketInbound(String protocol, HttpServletRequest request) {
|
||||||
|
|
||||||
|
// Get tunnel
|
||||||
|
final GuacamoleTunnel tunnel;
|
||||||
|
|
||||||
|
try {
|
||||||
|
tunnel = doConnect(request);
|
||||||
|
}
|
||||||
|
catch (GuacamoleException e) {
|
||||||
|
logger.error("Error connecting WebSocket tunnel.", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return new WebSocket which communicates through tunnel
|
||||||
|
return new StreamInbound() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onTextData(Reader reader) throws IOException {
|
||||||
|
|
||||||
|
GuacamoleWriter writer = tunnel.acquireWriter();
|
||||||
|
|
||||||
|
// Write all available data
|
||||||
|
try {
|
||||||
|
|
||||||
|
char[] buffer = new char[BUFFER_SIZE];
|
||||||
|
|
||||||
|
int num_read;
|
||||||
|
while ((num_read = reader.read(buffer)) > 0)
|
||||||
|
writer.write(buffer, 0, num_read);
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (GuacamoleException e) {
|
||||||
|
// FIXME: Handle exception
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnel.releaseWriter();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOpen(final WsOutbound outbound) {
|
||||||
|
|
||||||
|
Thread readThread = new Thread() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
|
||||||
|
CharBuffer charBuffer = CharBuffer.allocate(BUFFER_SIZE);
|
||||||
|
StringBuilder buffer = new StringBuilder(BUFFER_SIZE);
|
||||||
|
GuacamoleReader reader = tunnel.acquireReader();
|
||||||
|
char[] readMessage;
|
||||||
|
|
||||||
|
try {
|
||||||
|
while ((readMessage = reader.read()) != null) {
|
||||||
|
|
||||||
|
// Buffer message
|
||||||
|
buffer.append(readMessage);
|
||||||
|
|
||||||
|
// Flush if we expect to wait or buffer is getting full
|
||||||
|
if (!reader.available() || buffer.length() >= BUFFER_SIZE) {
|
||||||
|
|
||||||
|
// Reallocate buffer if necessary
|
||||||
|
if (buffer.length() > charBuffer.length())
|
||||||
|
charBuffer = CharBuffer.allocate(buffer.length());
|
||||||
|
else
|
||||||
|
charBuffer.clear();
|
||||||
|
|
||||||
|
charBuffer.put(buffer.toString().toCharArray());
|
||||||
|
charBuffer.flip();
|
||||||
|
|
||||||
|
outbound.writeTextMessage(charBuffer);
|
||||||
|
buffer.setLength(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
// FIXME: Handle exception
|
||||||
|
}
|
||||||
|
catch (GuacamoleException e) {
|
||||||
|
// FIXME: Handle exception
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
readThread.start();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClose(int i) {
|
||||||
|
try {
|
||||||
|
tunnel.close();
|
||||||
|
}
|
||||||
|
catch (GuacamoleException e) {
|
||||||
|
// FIXME: Handle exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onBinaryData(InputStream in) throws IOException {
|
||||||
|
throw new UnsupportedOperationException("Not supported yet.");
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract GuacamoleTunnel doConnect(HttpServletRequest request) throws GuacamoleException;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Tomcat WebSocket tunnel implementation. The classes here require at least
|
||||||
|
* Tomcat 7.0, and may change significantly as there is no common WebSocket
|
||||||
|
* API for Java yet.
|
||||||
|
*/
|
||||||
|
package net.sourceforge.guacamole.net.basic.websocket.tomcat;
|
||||||
|
|
@@ -70,12 +70,12 @@ public abstract class AuthenticatingHttpServlet extends HttpServlet {
|
|||||||
/**
|
/**
|
||||||
* The session attribute holding the current UserContext.
|
* The session attribute holding the current UserContext.
|
||||||
*/
|
*/
|
||||||
private static final String CONTEXT_ATTRIBUTE = "GUAC_CONTEXT";
|
public static final String CONTEXT_ATTRIBUTE = "GUAC_CONTEXT";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The session attribute holding the credentials authorizing this session.
|
* The session attribute holding the credentials authorizing this session.
|
||||||
*/
|
*/
|
||||||
private static final String CREDENTIALS_ATTRIBUTE = "GUAC_CREDS";
|
public static final String CREDENTIALS_ATTRIBUTE = "GUAC_CREDS";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The AuthenticationProvider to use to authenticate all requests.
|
* The AuthenticationProvider to use to authenticate all requests.
|
||||||
|
@@ -46,19 +46,25 @@ public class WebSocketSupportLoader implements ServletContextListener {
|
|||||||
*/
|
*/
|
||||||
private Logger logger = LoggerFactory.getLogger(WebSocketSupportLoader.class);
|
private Logger logger = LoggerFactory.getLogger(WebSocketSupportLoader.class);
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
public void contextDestroyed(ServletContextEvent sce) {
|
* Classname of the Jetty-specific WebSocket tunnel implementation.
|
||||||
}
|
*/
|
||||||
|
private static final String JETTY_WEBSOCKET =
|
||||||
|
"net.sourceforge.guacamole.net.basic.websocket.jetty.BasicGuacamoleWebSocketTunnelServlet";
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
public void contextInitialized(ServletContextEvent sce) {
|
* Classname of the Tomcat-specific WebSocket tunnel implementation.
|
||||||
|
*/
|
||||||
|
private static final String TOMCAT_WEBSOCKET =
|
||||||
|
"net.sourceforge.guacamole.net.basic.websocket.tomcat.BasicGuacamoleWebSocketTunnelServlet";
|
||||||
|
|
||||||
|
private boolean loadWebSocketTunnel(ServletContext context, String classname) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// Attempt to find WebSocket servlet
|
// Attempt to find WebSocket servlet
|
||||||
Class<Servlet> servlet = (Class<Servlet>) GuacamoleClassLoader.getInstance().findClass(
|
Class<Servlet> servlet = (Class<Servlet>)
|
||||||
"org.glyptodon.guacamole.net.basic.BasicGuacamoleWebSocketTunnelServlet"
|
GuacamoleClassLoader.getInstance().findClass(classname);
|
||||||
);
|
|
||||||
|
|
||||||
// Dynamically add servlet IF SERVLET 3.0 API AVAILABLE!
|
// Dynamically add servlet IF SERVLET 3.0 API AVAILABLE!
|
||||||
try {
|
try {
|
||||||
@@ -67,8 +73,9 @@ public class WebSocketSupportLoader implements ServletContextListener {
|
|||||||
Class regClass = Class.forName("javax.servlet.ServletRegistration");
|
Class regClass = Class.forName("javax.servlet.ServletRegistration");
|
||||||
|
|
||||||
// Get and invoke addServlet()
|
// Get and invoke addServlet()
|
||||||
Method addServlet = ServletContext.class.getMethod("addServlet", String.class, Class.class);
|
Method addServlet = ServletContext.class.getMethod("addServlet",
|
||||||
Object reg = addServlet.invoke(sce.getServletContext(), "WebSocketTunnel", servlet);
|
String.class, Class.class);
|
||||||
|
Object reg = addServlet.invoke(context, "WebSocketTunnel", servlet);
|
||||||
|
|
||||||
// Get and invoke addMapping()
|
// Get and invoke addMapping()
|
||||||
Method addMapping = regClass.getMethod("addMapping", String[].class);
|
Method addMapping = regClass.getMethod("addMapping", String[].class);
|
||||||
@@ -77,6 +84,7 @@ public class WebSocketSupportLoader implements ServletContextListener {
|
|||||||
// If we succesfully load and register the WebSocket tunnel servlet,
|
// If we succesfully load and register the WebSocket tunnel servlet,
|
||||||
// WebSocket is supported.
|
// WebSocket is supported.
|
||||||
logger.info("WebSocket support found and loaded.");
|
logger.info("WebSocket support found and loaded.");
|
||||||
|
return true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +108,10 @@ public class WebSocketSupportLoader implements ServletContextListener {
|
|||||||
|
|
||||||
// If no such servlet class, WebSocket support not present
|
// If no such servlet class, WebSocket support not present
|
||||||
catch (ClassNotFoundException e) {
|
catch (ClassNotFoundException e) {
|
||||||
logger.info("WebSocket support not found.");
|
logger.info("WebSocket support not found.", e);
|
||||||
|
}
|
||||||
|
catch (NoClassDefFoundError e) {
|
||||||
|
logger.info("WebSocket support not found.", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log all GuacamoleExceptions
|
// Log all GuacamoleExceptions
|
||||||
@@ -108,6 +119,31 @@ public class WebSocketSupportLoader implements ServletContextListener {
|
|||||||
logger.error("Unable to load/detect WebSocket support.", e);
|
logger.error("Unable to load/detect WebSocket support.", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load attempt failed
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void contextDestroyed(ServletContextEvent sce) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void contextInitialized(ServletContextEvent sce) {
|
||||||
|
|
||||||
|
// Try to load websocket support for Jetty
|
||||||
|
logger.info("Attempting to load Jetty-specific WebSocket support...");
|
||||||
|
if (loadWebSocketTunnel(sce.getServletContext(), JETTY_WEBSOCKET))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Try to load websocket support for Tomcat
|
||||||
|
logger.info("Attempting to load Tomcat-specific WebSocket support...");
|
||||||
|
if (loadWebSocketTunnel(sce.getServletContext(), TOMCAT_WEBSOCKET))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Inform of lack of support
|
||||||
|
logger.info("No WebSocket support could be loaded. Only HTTP will be used.");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user