diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/RESTExceptionMapper.java b/guacamole/src/main/java/org/apache/guacamole/rest/RESTExceptionMapper.java new file mode 100644 index 000000000..91179473a --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/RESTExceptionMapper.java @@ -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 { + + /** + * 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(); + + } + +} \ No newline at end of file diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/RESTExceptionWrapper.java b/guacamole/src/main/java/org/apache/guacamole/rest/RESTExceptionWrapper.java deleted file mode 100644 index a0c756ebe..000000000 --- a/guacamole/src/main/java/org/apache/guacamole/rest/RESTExceptionWrapper.java +++ /dev/null @@ -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 - * @QueryParam or @FormParam 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)); - - } - - } - -} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/RESTServiceModule.java b/guacamole/src/main/java/org/apache/guacamole/rest/RESTServiceModule.java index b326fa534..4efab0ff5 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/RESTServiceModule.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/RESTServiceModule.java @@ -85,9 +85,7 @@ public class RESTServiceModule extends ServletModule { bind(DecorationService.class); // Automatically translate GuacamoleExceptions for REST methods - MethodInterceptor interceptor = new RESTExceptionWrapper(); - requestInjection(interceptor); - bindInterceptor(Matchers.any(), new RESTMethodMatcher(), interceptor); + bind(RESTExceptionMapper.class); // Set up the API endpoints bind(ExtensionRESTService.class);