mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-07 13:41:21 +00:00
GUACAMOLE-566: Merge changes migrating from method interceptors to exception mappers.
This commit is contained in:
@@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.guacamole.rest;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.ws.rs.core.Context;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.ws.rs.ext.ExceptionMapper;
|
||||||
|
import javax.ws.rs.ext.Provider;
|
||||||
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
import org.apache.guacamole.GuacamoleUnauthorizedException;
|
||||||
|
import org.apache.guacamole.rest.auth.AuthenticationService;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that maps GuacamoleExceptions in a way that returns a
|
||||||
|
* custom response to the user via JSON rather than allowing the default
|
||||||
|
* web application error handling to take place.
|
||||||
|
*/
|
||||||
|
@Provider
|
||||||
|
@Singleton
|
||||||
|
public class RESTExceptionMapper implements ExceptionMapper<Throwable> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The logger for this class.
|
||||||
|
*/
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(RESTExceptionMapper.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The HttpServletRequest for the Throwable being intercepted. Despite this
|
||||||
|
* class being a Singleton, this object will always be scoped with the
|
||||||
|
* current request for the Throwable that is being processed by this class.
|
||||||
|
*/
|
||||||
|
@Context
|
||||||
|
private HttpServletRequest request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The authentication service associated with the currently active session.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
private AuthenticationService authenticationService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the authentication token that is in use in the current session,
|
||||||
|
* if present, or null if otherwise.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The authentication token for the current session, or null if no
|
||||||
|
* token is present.
|
||||||
|
*/
|
||||||
|
private String getAuthenticationToken() {
|
||||||
|
|
||||||
|
String token = request.getParameter("token");
|
||||||
|
if (token != null && !token.isEmpty())
|
||||||
|
return token;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response toResponse(Throwable t) {
|
||||||
|
|
||||||
|
// Ensure any associated session is invalidated if unauthorized
|
||||||
|
if (t instanceof GuacamoleUnauthorizedException) {
|
||||||
|
String token = getAuthenticationToken();
|
||||||
|
|
||||||
|
if (authenticationService.destroyGuacamoleSession(token))
|
||||||
|
logger.debug("Implicitly invalidated session for token \"{}\"", token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate GuacamoleException subclasses to HTTP error codes
|
||||||
|
if (t instanceof GuacamoleException)
|
||||||
|
return Response
|
||||||
|
.status(((GuacamoleException) t).getHttpStatusCode())
|
||||||
|
.entity(new APIError((GuacamoleException) t))
|
||||||
|
.type(MediaType.APPLICATION_JSON)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Rethrow unchecked exceptions such that they are properly wrapped
|
||||||
|
String message = t.getMessage();
|
||||||
|
if (message != null)
|
||||||
|
logger.error("Unexpected internal error: {}", message);
|
||||||
|
else
|
||||||
|
logger.error("An internal error occurred, but did not contain "
|
||||||
|
+ "an error message. Enable debug-level logging for "
|
||||||
|
+ "details.");
|
||||||
|
|
||||||
|
logger.debug("Unexpected error in REST endpoint.", t);
|
||||||
|
|
||||||
|
return Response
|
||||||
|
.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(new APIError(
|
||||||
|
new GuacamoleException("Unexpected internal error", t)))
|
||||||
|
.type(MediaType.APPLICATION_JSON)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,203 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.apache.guacamole.rest;
|
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
|
||||||
import java.lang.annotation.Annotation;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import javax.ws.rs.FormParam;
|
|
||||||
import javax.ws.rs.QueryParam;
|
|
||||||
import javax.ws.rs.WebApplicationException;
|
|
||||||
import javax.ws.rs.core.Response;
|
|
||||||
import org.aopalliance.intercept.MethodInterceptor;
|
|
||||||
import org.aopalliance.intercept.MethodInvocation;
|
|
||||||
import org.apache.guacamole.GuacamoleClientException;
|
|
||||||
import org.apache.guacamole.GuacamoleException;
|
|
||||||
import org.apache.guacamole.GuacamoleResourceNotFoundException;
|
|
||||||
import org.apache.guacamole.GuacamoleSecurityException;
|
|
||||||
import org.apache.guacamole.GuacamoleUnauthorizedException;
|
|
||||||
import org.apache.guacamole.rest.auth.AuthenticationService;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
public class RESTExceptionWrapper implements MethodInterceptor {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logger for this class.
|
|
||||||
*/
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(RESTExceptionWrapper.class);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service for authenticating users and managing their Guacamole sessions.
|
|
||||||
*/
|
|
||||||
@Inject
|
|
||||||
private AuthenticationService authenticationService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines whether the given set of annotations describes an HTTP
|
|
||||||
* request parameter of the given name. For a parameter to be associated
|
|
||||||
* with an HTTP request parameter, it must be annotated with either the
|
|
||||||
* <code>@QueryParam</code> or <code>@FormParam</code> annotations.
|
|
||||||
*
|
|
||||||
* @param annotations
|
|
||||||
* The annotations associated with the Java parameter being checked.
|
|
||||||
*
|
|
||||||
* @param name
|
|
||||||
* The name of the HTTP request parameter.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
* true if the given set of annotations describes an HTTP request
|
|
||||||
* parameter having the given name, false otherwise.
|
|
||||||
*/
|
|
||||||
private boolean isRequestParameter(Annotation[] annotations, String name) {
|
|
||||||
|
|
||||||
// Search annotations for associated HTTP parameters
|
|
||||||
for (Annotation annotation : annotations) {
|
|
||||||
|
|
||||||
// Check if parameter is associated with the HTTP query string
|
|
||||||
if (annotation instanceof QueryParam && name.equals(((QueryParam) annotation).value()))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
// Failing that, check whether the parameter is associated with the
|
|
||||||
// HTTP request body
|
|
||||||
if (annotation instanceof FormParam && name.equals(((FormParam) annotation).value()))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// No parameter annotations are present
|
|
||||||
return false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the authentication token that was passed in the given method
|
|
||||||
* invocation. If the given method invocation is not associated with an
|
|
||||||
* HTTP request (it lacks the appropriate JAX-RS annotations) or there is
|
|
||||||
* no authentication token, null is returned.
|
|
||||||
*
|
|
||||||
* @param invocation
|
|
||||||
* The method invocation whose corresponding authentication token
|
|
||||||
* should be determined.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
* The authentication token passed in the given method invocation, or
|
|
||||||
* null if there is no such token.
|
|
||||||
*/
|
|
||||||
private String getAuthenticationToken(MethodInvocation invocation) {
|
|
||||||
|
|
||||||
Method method = invocation.getMethod();
|
|
||||||
|
|
||||||
// Get the types and annotations associated with each parameter
|
|
||||||
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
|
|
||||||
Class<?>[] parameterTypes = method.getParameterTypes();
|
|
||||||
|
|
||||||
// The Java standards require these to be parallel arrays
|
|
||||||
assert(parameterAnnotations.length == parameterTypes.length);
|
|
||||||
|
|
||||||
// Iterate through all parameters, looking for the authentication token
|
|
||||||
for (int i = 0; i < parameterTypes.length; i++) {
|
|
||||||
|
|
||||||
// Only inspect String parameters
|
|
||||||
Class<?> parameterType = parameterTypes[i];
|
|
||||||
if (parameterType != String.class)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Parameter must be declared as a REST service parameter
|
|
||||||
Annotation[] annotations = parameterAnnotations[i];
|
|
||||||
if (!isRequestParameter(annotations, "token"))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// The token parameter has been found - return its value
|
|
||||||
Object[] args = invocation.getArguments();
|
|
||||||
return (String) args[i];
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// No token parameter is defined
|
|
||||||
return null;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object invoke(MethodInvocation invocation) throws WebApplicationException {
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
// Invoke wrapped method
|
|
||||||
try {
|
|
||||||
return invocation.proceed();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure any associated session is invalidated if unauthorized
|
|
||||||
catch (GuacamoleUnauthorizedException e) {
|
|
||||||
|
|
||||||
// Pull authentication token from request
|
|
||||||
String token = getAuthenticationToken(invocation);
|
|
||||||
|
|
||||||
// If there is an associated auth token, invalidate it
|
|
||||||
if (authenticationService.destroyGuacamoleSession(token))
|
|
||||||
logger.debug("Implicitly invalidated session for token \"{}\".", token);
|
|
||||||
|
|
||||||
// Continue with exception processing
|
|
||||||
throw e;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Translate GuacamoleException subclasses to HTTP error codes
|
|
||||||
catch (GuacamoleException e) {
|
|
||||||
throw new APIException(
|
|
||||||
Response.Status.fromStatusCode(e.getHttpStatusCode()),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rethrow unchecked exceptions such that they are properly wrapped
|
|
||||||
catch (Throwable t) {
|
|
||||||
|
|
||||||
// Log all reasonable details of error
|
|
||||||
String message = t.getMessage();
|
|
||||||
if (message != null)
|
|
||||||
logger.error("Unexpected internal error: {}", message);
|
|
||||||
else
|
|
||||||
logger.error("An internal error occurred, but did not contain "
|
|
||||||
+ "an error message. Enable debug-level logging for "
|
|
||||||
+ "details.");
|
|
||||||
|
|
||||||
// Ensure internal errors are fully logged at the debug level
|
|
||||||
logger.debug("Unexpected error in REST endpoint.", t);
|
|
||||||
|
|
||||||
throw new APIException(Response.Status.INTERNAL_SERVER_ERROR,
|
|
||||||
new GuacamoleException("Unexpected internal error.", t));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -85,9 +85,7 @@ public class RESTServiceModule extends ServletModule {
|
|||||||
bind(DecorationService.class);
|
bind(DecorationService.class);
|
||||||
|
|
||||||
// Automatically translate GuacamoleExceptions for REST methods
|
// Automatically translate GuacamoleExceptions for REST methods
|
||||||
MethodInterceptor interceptor = new RESTExceptionWrapper();
|
bind(RESTExceptionMapper.class);
|
||||||
requestInjection(interceptor);
|
|
||||||
bindInterceptor(Matchers.any(), new RESTMethodMatcher(), interceptor);
|
|
||||||
|
|
||||||
// Set up the API endpoints
|
// Set up the API endpoints
|
||||||
bind(ExtensionRESTService.class);
|
bind(ExtensionRESTService.class);
|
||||||
|
Reference in New Issue
Block a user