GUAC-442: Restrict access to WebSocket tunnel using filter (rather than RestrictedHttpServlet like the rest of guac).

This commit is contained in:
Michael Jumper
2014-10-09 15:29:14 -07:00
parent 16b0f047ea
commit 68f7afb8c9
7 changed files with 132 additions and 357 deletions

View File

@@ -0,0 +1,93 @@
/*
* 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;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.glyptodon.guacamole.net.auth.UserContext;
import org.glyptodon.guacamole.protocol.GuacamoleStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Filter which enforces authentication. If no user context is associated with
* the current HTTP session, or no HTTP session exists, the request is denied.
*
* @author Michael Jumper
*/
public class RestrictedFilter implements Filter {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(RestrictedFilter.class);
@Override
public void init(FilterConfig config) throws ServletException {
// No configuration
}
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
// Pull user context from session
UserContext context = null;
HttpSession session = request.getSession(false);
if (session != null)
context = AuthenticatingFilter.getUserContext(session);
// If authenticated, proceed with rest of chain
if (context != null)
chain.doFilter(req, resp);
// Otherwise, deny entire request
else {
final GuacamoleStatus status = GuacamoleStatus.CLIENT_UNAUTHORIZED;
final String message = "Not authenticated";
logger.warn("Client request rejected: {}", message);
response.addHeader("Guacamole-Status-Code", Integer.toString(status.getGuacamoleStatusCode()));
response.addHeader("Guacamole-Error-Message", message);
response.sendError(status.getHttpStatusCode());
}
}
@Override
public void destroy() {
// No destruction needed
}
}

View File

@@ -1,147 +0,0 @@
/*
* Copyright (C) 2013 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.websocket.jetty;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.GuacamoleServerException;
import org.glyptodon.guacamole.net.auth.UserContext;
import org.glyptodon.guacamole.net.basic.RestrictedHttpServlet;
import org.eclipse.jetty.websocket.WebSocket;
import org.eclipse.jetty.websocket.WebSocketServlet;
import org.glyptodon.guacamole.net.basic.AuthenticatingFilter;
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 final Logger logger = LoggerFactory.getLogger(AuthenticatingWebSocketServlet.class);
/**
* Wrapped authenticating servlet.
*/
private final RestrictedHttpServlet auth_servlet = new RestrictedHttpServlet() {
@Override
protected void restrictedService(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
public void init() throws ServletException {
super.init();
auth_servlet.init();
}
@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 = AuthenticatingFilter.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);
}
/**
* 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.
* @return A connected WebSocket.
*/
protected abstract WebSocket authenticatedConnect(
UserContext context,
HttpServletRequest request, String protocol);
}

View File

@@ -25,22 +25,14 @@ package org.glyptodon.guacamole.net.basic.websocket.jetty;
import javax.servlet.http.HttpServletRequest;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.net.GuacamoleTunnel;
import org.glyptodon.guacamole.net.auth.UserContext;
import org.eclipse.jetty.websocket.WebSocket;
import org.glyptodon.guacamole.net.basic.BasicTunnelRequestUtility;
/**
* Authenticating tunnel servlet implementation which uses WebSocket as a
* tunnel backend, rather than HTTP.
* Tunnel servlet implementation which uses WebSocket as a tunnel backend,
* rather than HTTP, properly parsing connection IDs included in the connection
* request.
*/
public class BasicGuacamoleWebSocketTunnelServlet extends AuthenticatingWebSocketServlet {
/**
* Wrapped GuacamoleHTTPTunnelServlet which will handle all authenticated
* requests.
*/
private GuacamoleWebSocketTunnelServlet tunnelServlet =
new GuacamoleWebSocketTunnelServlet() {
public class BasicGuacamoleWebSocketTunnelServlet extends GuacamoleWebSocketTunnelServlet {
@Override
protected GuacamoleTunnel doConnect(HttpServletRequest request)
@@ -48,13 +40,4 @@ public class BasicGuacamoleWebSocketTunnelServlet extends AuthenticatingWebSocke
return BasicTunnelRequestUtility.createTunnel(request);
}
};
@Override
protected WebSocket authenticatedConnect(UserContext context,
HttpServletRequest request, String protocol) {
return tunnelServlet.doWebSocketConnect(request, protocol);
}
}

View File

@@ -1,161 +0,0 @@
/*
* Copyright (C) 2013 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.websocket.tomcat;
import java.io.IOException;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.GuacamoleServerException;
import org.glyptodon.guacamole.net.auth.UserContext;
import org.glyptodon.guacamole.net.basic.RestrictedHttpServlet;
import org.apache.catalina.websocket.StreamInbound;
import org.apache.catalina.websocket.WebSocketServlet;
import org.glyptodon.guacamole.net.basic.AuthenticatingFilter;
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 final Logger logger = LoggerFactory.getLogger(AuthenticatingWebSocketServlet.class);
/**
* Wrapped authenticating servlet.
*/
private final RestrictedHttpServlet auth_servlet = new RestrictedHttpServlet() {
@Override
protected void restrictedService(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
public void init() throws ServletException {
super.init();
auth_servlet.init();
}
@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
protected String selectSubProtocol(List<String> subProtocols) {
// Search for expected protocol
for (String protocol : subProtocols)
if ("guacamole".equals(protocol))
return "guacamole";
// Otherwise, fail
return null;
}
@Override
public StreamInbound createWebSocketInbound(String protocol,
HttpServletRequest request) {
// Get session and user context
HttpSession session = request.getSession(true);
UserContext context = AuthenticatingFilter.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);
}
/**
* 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.
* @return A completed WebSocket connection.
*/
protected abstract StreamInbound authenticatedConnect(
UserContext context,
HttpServletRequest request, String protocol);
}

View File

@@ -25,36 +25,19 @@ package org.glyptodon.guacamole.net.basic.websocket.tomcat;
import javax.servlet.http.HttpServletRequest;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.net.GuacamoleTunnel;
import org.glyptodon.guacamole.net.auth.UserContext;
import org.apache.catalina.websocket.StreamInbound;
import org.glyptodon.guacamole.net.basic.BasicTunnelRequestUtility;
/**
* Authenticating tunnel servlet implementation which uses WebSocket as a
* tunnel backend, rather than HTTP.
* Tunnel servlet implementation which uses WebSocket as a tunnel backend,
* rather than HTTP, properly parsing connection IDs included in the connection
* request.
*/
public class BasicGuacamoleWebSocketTunnelServlet extends AuthenticatingWebSocketServlet {
/**
* Wrapped GuacamoleHTTPTunnelServlet which will handle all authenticated
* requests.
*/
private GuacamoleWebSocketTunnelServlet tunnelServlet =
new GuacamoleWebSocketTunnelServlet() {
public class BasicGuacamoleWebSocketTunnelServlet extends GuacamoleWebSocketTunnelServlet {
@Override
protected GuacamoleTunnel doConnect(HttpServletRequest request)
throws GuacamoleException {
return BasicTunnelRequestUtility.createTunnel(request);
}
};
@Override
protected StreamInbound authenticatedConnect(UserContext context,
HttpServletRequest request, String protocol) {
return tunnelServlet.createWebSocketInbound(protocol, request);
}
}

View File

@@ -27,6 +27,7 @@ import java.io.InputStream;
import java.io.Reader;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.io.GuacamoleReader;
@@ -74,6 +75,19 @@ public abstract class GuacamoleWebSocketTunnelServlet extends WebSocketServlet {
}
@Override
protected String selectSubProtocol(List<String> subProtocols) {
// Search for expected protocol
for (String protocol : subProtocols)
if ("guacamole".equals(protocol))
return "guacamole";
// Otherwise, fail
return null;
}
@Override
public StreamInbound createWebSocketInbound(String protocol, HttpServletRequest request) {

View File

@@ -47,6 +47,16 @@
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Restrict requests to the WebSocket tunnel -->
<filter>
<filter-name>RestrictedFilter</filter-name>
<filter-class>org.glyptodon.guacamole.net.basic.RestrictedFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>RestrictedFilter</filter-name>
<url-pattern>/websocket-tunnel</url-pattern>
</filter-mapping>
<!-- Basic Login Servlet -->
<servlet>
<description>Login servlet.</description>