diff --git a/guacamole/pom.xml b/guacamole/pom.xml index 0693e6aae..3e8941da7 100644 --- a/guacamole/pom.xml +++ b/guacamole/pom.xml @@ -254,6 +254,11 @@ slf4j-api 1.7.7 + + org.slf4j + jul-to-slf4j + 1.7.7 + ch.qos.logback logback-classic @@ -435,34 +440,39 @@ com.google.inject guice - 3.0 + 4.2.3 com.google.inject.extensions guice-assistedinject - 3.0 + 4.2.3 com.google.inject.extensions guice-servlet - 3.0 + 4.2.3 - com.sun.jersey - jersey-server - 1.17.1 + org.glassfish.jersey.containers + jersey-container-servlet-core + 2.31 + + + org.glassfish.jersey.inject + jersey-hk2 + 2.31 - + - com.sun.jersey.contribs - jersey-guice - 1.17.1 - + org.glassfish.hk2 + guice-bridge + 2.6.1 + @@ -473,9 +483,9 @@ - com.sun.jersey - jersey-json - 1.17.1 + org.glassfish.jersey.media + jersey-media-json-jackson + 2.31 diff --git a/guacamole/src/main/java/org/apache/guacamole/GuacamoleApplication.java b/guacamole/src/main/java/org/apache/guacamole/GuacamoleApplication.java new file mode 100644 index 000000000..7f3e4603c --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/GuacamoleApplication.java @@ -0,0 +1,76 @@ +/* + * 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; + +import com.google.inject.Injector; +import javax.inject.Inject; +import javax.servlet.ServletContext; +import javax.ws.rs.ApplicationPath; +import org.glassfish.hk2.api.ServiceLocator; +import org.glassfish.jersey.jackson.JacksonFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.jvnet.hk2.guice.bridge.api.GuiceBridge; +import org.jvnet.hk2.guice.bridge.api.GuiceIntoHK2Bridge; +import org.slf4j.bridge.SLF4JBridgeHandler; + +/** + * JAX-RS Application which serves as the root definition of the Guacamole + * REST API. The HK2 dependency injection used by Jersey is automatically + * bridged to Guice, allowing injections managed by Guice to be injected within + * classes served by Jersey. + */ +@ApplicationPath("/*") +public class GuacamoleApplication extends ResourceConfig { + + /** + * Creates a new GuacamoleApplication which defines the Guacamole REST API, + * automatically configuring Jersey's HK2 dependency injection to + * additionally pull services from a Guice injector. + * + * @param servletContext + * The ServletContext which has already associated with a Guice + * injector via a GuacamoleServletContextListener. + * + * @param serviceLocator + * The HK2 service locator (injector). + */ + @Inject + public GuacamoleApplication(ServletContext servletContext, + ServiceLocator serviceLocator) { + + // Bridge Jersey logging (java.util.logging) to SLF4J + SLF4JBridgeHandler.removeHandlersForRootLogger(); + SLF4JBridgeHandler.install(); + + // Bridge HK2 service locator with Guice injector + Injector guiceInjector = (Injector) servletContext.getAttribute(GuacamoleServletContextListener.GUICE_INJECTOR); + GuiceBridge.getGuiceBridge().initializeGuiceBridge(serviceLocator); + GuiceIntoHK2Bridge bridge = serviceLocator.getService(GuiceIntoHK2Bridge.class); + bridge.bridgeGuiceInjector(guiceInjector); + + // Automatically scan for REST resources + packages("org.apache.guacamole.rest"); + + // Use Jackson for JSON + register(JacksonFeature.class); + + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/GuacamoleServletContextListener.java b/guacamole/src/main/java/org/apache/guacamole/GuacamoleServletContextListener.java index 38d5b7c01..f793575e6 100644 --- a/guacamole/src/main/java/org/apache/guacamole/GuacamoleServletContextListener.java +++ b/guacamole/src/main/java/org/apache/guacamole/GuacamoleServletContextListener.java @@ -25,6 +25,7 @@ import com.google.inject.Injector; import com.google.inject.Stage; import com.google.inject.servlet.GuiceServletContextListener; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import javax.inject.Inject; import javax.servlet.ServletContextEvent; import org.apache.guacamole.environment.Environment; @@ -41,9 +42,45 @@ import org.slf4j.LoggerFactory; /** * A ServletContextListener to listen for initialization of the servlet context * in order to set up dependency injection. + * + * NOTE: Guacamole's REST API uses Jersey 2.x which does not natively support + * dependency injection using Guice. It DOES support dependency injection using + * HK2, which supports bi-directional bridging with Guice. + * + * The overall process is thus: + * + * 1. Application initialization proceeds using GuacamoleServletContextListener, + * a subclass of GuiceServletContextListener, with all HTTP requests being + * routed through GuiceFilter which serves as the absolute root. + * + * 2. GuacamoleServletContextListener prepares the Guice injector, storing the + * injector within the ServletContext such that it can later be bridged with + * HK2. + * + * 3. Several of the modules used to prepare the Guice injector are + * ServletModule subclasses, which define HTTP request paths that GuiceFilter + * should route to specific servlets. One of these paths is "/api/*" (the + * root of the REST API) which is routed to Jersey's ServletContainer servlet + * (the root of Jersey's JAX-RS implementation). + * + * 4. Configuration information passed to Jersey's ServletContainer tells Jersey + * to use the GuacamoleApplication class (a subclass of ResourceConfig) to + * define the rest of the resources and any other configuration. + * + * 5. When Jersey creates its instance of GuacamoleApplication, the + * initialization process of GuacamoleApplication pulls the Guice injector + * from the ServletContext, completes the HK2 bridging, and configures Jersey + * to automatically locate and inject all REST services. */ public class GuacamoleServletContextListener extends GuiceServletContextListener { + /** + * The name of the ServletContext attribute which will contain a reference + * to the Guice injector once the contextInitialized() event has been + * handled. + */ + public static final String GUICE_INJECTOR = "GUAC_GUICE_INJECTOR"; + /** * Logger for this class. */ @@ -65,6 +102,12 @@ public class GuacamoleServletContextListener extends GuiceServletContextListener @Inject private List authProviders; + /** + * Internal reference to the Guice injector that was lazily created when + * getInjector() was first invoked. + */ + private final AtomicReference guiceInjector = new AtomicReference<>(); + @Override public void contextInitialized(ServletContextEvent servletContextEvent) { @@ -78,33 +121,47 @@ public class GuacamoleServletContextListener extends GuiceServletContextListener throw new RuntimeException(e); } + // NOTE: The superclass implementation of contextInitialized() is + // expected to invoke getInjector(), hence the need to call AFTER + // setting up the environment and session map super.contextInitialized(servletContextEvent); + // Inject any annotated members of this class + Injector injector = getInjector(); + injector.injectMembers(this); + + // Store reference to injector for use by Jersey and HK2 bridge + servletContextEvent.getServletContext().setAttribute(GUICE_INJECTOR, injector); + } @Override protected Injector getInjector() { + return guiceInjector.updateAndGet((current) -> { - // Create injector - Injector injector = Guice.createInjector(Stage.PRODUCTION, - new EnvironmentModule(environment), - new LogModule(environment), - new ExtensionModule(environment), - new RESTServiceModule(sessionMap), - new TunnelModule() - ); + // Use existing injector if already created + if (current != null) + return current; - // Inject any annotated members of this class - injector.injectMembers(this); + // Create new injector if necessary + Injector injector = Guice.createInjector(Stage.PRODUCTION, + new EnvironmentModule(environment), + new LogModule(environment), + new ExtensionModule(environment), + new RESTServiceModule(sessionMap), + new TunnelModule() + ); - return injector; + return injector; + }); } @Override public void contextDestroyed(ServletContextEvent servletContextEvent) { - super.contextDestroyed(servletContextEvent); + // Clean up reference to Guice injector + servletContextEvent.getServletContext().removeAttribute(GUICE_INJECTOR); // Shutdown TokenSessionMap if (sessionMap != null) @@ -116,6 +173,9 @@ public class GuacamoleServletContextListener extends GuiceServletContextListener authProvider.shutdown(); } + // Continue any Guice-specific cleanup + super.contextDestroyed(servletContextEvent); + } } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/PATCH.java b/guacamole/src/main/java/org/apache/guacamole/rest/PATCH.java deleted file mode 100644 index 171719eed..000000000 --- a/guacamole/src/main/java/org/apache/guacamole/rest/PATCH.java +++ /dev/null @@ -1,35 +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 java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import javax.ws.rs.HttpMethod; - -/** - * An annotation for using the HTTP PATCH method in the REST endpoints. - */ -@Target({ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -@HttpMethod("PATCH") -public @interface PATCH {} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/RESTMethodMatcher.java b/guacamole/src/main/java/org/apache/guacamole/rest/RESTMethodMatcher.java deleted file mode 100644 index 875f4161c..000000000 --- a/guacamole/src/main/java/org/apache/guacamole/rest/RESTMethodMatcher.java +++ /dev/null @@ -1,128 +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.matcher.AbstractMatcher; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import javax.ws.rs.HttpMethod; -import javax.ws.rs.Path; -import org.apache.guacamole.GuacamoleException; - -/** - * A Guice Matcher which matches only methods which throw GuacamoleException - * (or a subclass thereof) and are explicitly annotated as with an HTTP method - * annotation like @GET or @POST. Any method which - * throws GuacamoleException and is annotated with an annotation that is - * annotated with @HttpMethod will match. - */ -public class RESTMethodMatcher extends AbstractMatcher { - - /** - * Returns whether the given method throws the specified exception type, - * including any subclasses of that type. - * - * @param method - * The method to test. - * - * @param exceptionType - * The exception type to test for. - * - * @return - * true if the given method throws an exception of the specified type, - * false otherwise. - */ - private boolean methodThrowsException(Method method, - Class exceptionType) { - - // Check whether the method throws an exception of the specified type - for (Class thrownType : method.getExceptionTypes()) { - if (exceptionType.isAssignableFrom(thrownType)) - return true; - } - - // No such exception is declared to be thrown - return false; - - } - - /** - * Returns whether the given method is annotated as a REST method. A REST - * method is annotated with an annotation which is annotated with - * @HttpMethod or @Path. - * - * @param method - * The method to test. - * - * @return - * true if the given method is annotated as a REST method, false - * otherwise. - */ - private boolean isRESTMethod(Method method) { - - // Check whether the required REST annotations are present - for (Annotation annotation : method.getAnnotations()) { - - // A method is a REST method if it is annotated with @HttpMethod - Class annotationType = annotation.annotationType(); - if (annotationType.isAnnotationPresent(HttpMethod.class)) - return true; - - // A method is a REST method if it is annotated with @Path - if (Path.class.isAssignableFrom(annotationType)) - return true; - - } - - // A method is also REST method if it overrides a REST method within - // the superclass - Class superclass = method.getDeclaringClass().getSuperclass(); - if (superclass != null) { - - // Recheck against identical method within superclass - try { - return isRESTMethod(superclass.getMethod(method.getName(), - method.getParameterTypes())); - } - - // If there is no such method, then this method cannot possibly be - // a REST method - catch (NoSuchMethodException e) { - return false; - } - - } - - // Lacking a superclass, the search stops here - it's not a REST method - return false; - - } - - @Override - public boolean matches(Method method) { - - // Guacamole REST methods are REST methods which throw - // GuacamoleExceptions - return isRESTMethod(method) - && methodThrowsException(method, GuacamoleException.class); - - } - -} 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 9fc1045b2..32878b1ee 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/RESTServiceModule.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/RESTServiceModule.java @@ -19,19 +19,14 @@ package org.apache.guacamole.rest; -import org.apache.guacamole.rest.event.ListenerService; -import org.apache.guacamole.rest.session.UserContextResourceFactory; -import org.apache.guacamole.rest.session.SessionRESTService; import com.google.inject.Scopes; import com.google.inject.assistedinject.FactoryModuleBuilder; import com.google.inject.servlet.ServletModule; -import com.sun.jersey.api.core.ResourceConfig; -import com.sun.jersey.guice.spi.container.servlet.GuiceContainer; -import java.util.HashMap; -import java.util.Map; +import java.util.Collections; +import org.apache.guacamole.rest.event.ListenerService; +import org.apache.guacamole.rest.session.UserContextResourceFactory; +import org.apache.guacamole.GuacamoleApplication; import org.apache.guacamole.rest.activeconnection.ActiveConnectionModule; -import org.codehaus.jackson.jaxrs.JacksonJsonProvider; -import org.apache.guacamole.rest.auth.TokenRESTService; import org.apache.guacamole.rest.auth.AuthTokenGenerator; import org.apache.guacamole.rest.auth.AuthenticationService; import org.apache.guacamole.rest.auth.DecorationService; @@ -39,15 +34,14 @@ import org.apache.guacamole.rest.auth.SecureRandomAuthTokenGenerator; import org.apache.guacamole.rest.auth.TokenSessionMap; import org.apache.guacamole.rest.connection.ConnectionModule; import org.apache.guacamole.rest.connectiongroup.ConnectionGroupModule; -import org.apache.guacamole.rest.extension.ExtensionRESTService; -import org.apache.guacamole.rest.language.LanguageRESTService; -import org.apache.guacamole.rest.patch.PatchRESTService; import org.apache.guacamole.rest.session.SessionResourceFactory; import org.apache.guacamole.rest.sharingprofile.SharingProfileModule; import org.apache.guacamole.rest.tunnel.TunnelCollectionResourceFactory; import org.apache.guacamole.rest.tunnel.TunnelResourceFactory; import org.apache.guacamole.rest.user.UserModule; import org.apache.guacamole.rest.usergroup.UserGroupModule; +import org.glassfish.jersey.servlet.ServletContainer; +import org.glassfish.jersey.servlet.ServletProperties; import org.webjars.servlet.WebjarsServlet; /** @@ -76,8 +70,6 @@ public class RESTServiceModule extends ServletModule { @Override protected void configureServlets() { - Map containerParams = new HashMap<>(); - // Bind session map bind(TokenSessionMap.class).toInstance(tokenSessionMap); @@ -87,21 +79,7 @@ public class RESTServiceModule extends ServletModule { bind(AuthTokenGenerator.class).to(SecureRandomAuthTokenGenerator.class); bind(DecorationService.class); - // Automatically translate GuacamoleExceptions for REST methods - bind(RESTExceptionMapper.class); - - // Restrict API requests by entity size - containerParams.put(ResourceConfig.PROPERTY_CONTAINER_REQUEST_FILTERS, RequestSizeFilter.class.getName()); - bind(RequestSizeFilter.class).in(Scopes.SINGLETON); - - // Set up the API endpoints - bind(ExtensionRESTService.class); - bind(LanguageRESTService.class); - bind(PatchRESTService.class); - bind(TokenRESTService.class); - // Root-level resources - bind(SessionRESTService.class); install(new FactoryModuleBuilder().build(SessionResourceFactory.class)); install(new FactoryModuleBuilder().build(TunnelCollectionResourceFactory.class)); install(new FactoryModuleBuilder().build(TunnelResourceFactory.class)); @@ -115,10 +93,12 @@ public class RESTServiceModule extends ServletModule { install(new UserModule()); install(new UserGroupModule()); - // Set up the servlet and JSON mappings - bind(GuiceContainer.class); - bind(JacksonJsonProvider.class).in(Scopes.SINGLETON); - serve("/api/*").with(GuiceContainer.class, containerParams); + // Serve REST services using Jersey 2.x + bind(ServletContainer.class).in(Scopes.SINGLETON); + serve("/api/*").with(ServletContainer.class, Collections.singletonMap( + ServletProperties.JAXRS_APPLICATION_CLASS, + GuacamoleApplication.class.getName() + )); // Serve Webjar JavaScript dependencies bind(WebjarsServlet.class).in(Scopes.SINGLETON); diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/RequestSizeFilter.java b/guacamole/src/main/java/org/apache/guacamole/rest/RequestSizeFilter.java index 10f0a1cb0..cce8a2970 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/RequestSizeFilter.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/RequestSizeFilter.java @@ -19,11 +19,12 @@ package org.apache.guacamole.rest; -import com.sun.jersey.spi.container.ContainerRequest; -import com.sun.jersey.spi.container.ContainerRequestFilter; -import com.sun.jersey.spi.resource.Singleton; +import java.io.IOException; import java.io.InputStream; import javax.inject.Inject; +import javax.inject.Singleton; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; import javax.ws.rs.ext.Provider; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.environment.Environment; @@ -61,7 +62,7 @@ public class RequestSizeFilter implements ContainerRequestFilter { private Environment environment; @Override - public ContainerRequest filter(ContainerRequest request) { + public void filter(ContainerRequestContext context) throws IOException { // Retrieve configured request size limits final long maxRequestSize; @@ -74,15 +75,13 @@ public class RequestSizeFilter implements ContainerRequestFilter { // Ignore request size if limit is disabled if (maxRequestSize == 0) - return request; + return; // Restrict maximum size of requests which have an input stream // available to be limited - InputStream stream = request.getEntityInputStream(); + InputStream stream = context.getEntityStream(); if (stream != null) - request.setEntityInputStream(new LimitedRequestInputStream(stream, maxRequestSize)); - - return request; + context.setEntityStream(new LimitedRequestInputStream(stream, maxRequestSize)); } diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java index ce9cb8371..e9b4b225b 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/directory/DirectoryResource.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import javax.ws.rs.Consumes; import javax.ws.rs.GET; +import javax.ws.rs.PATCH; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; @@ -44,7 +45,6 @@ import org.apache.guacamole.net.auth.permission.ObjectPermissionSet; import org.apache.guacamole.net.auth.permission.SystemPermission; import org.apache.guacamole.net.auth.permission.SystemPermissionSet; import org.apache.guacamole.rest.APIPatch; -import org.apache.guacamole.rest.PATCH; /** * A REST resource which abstracts the operations available on all Guacamole diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/identifier/RelatedObjectSetResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/identifier/RelatedObjectSetResource.java index 446b0453e..77b0b4091 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/identifier/RelatedObjectSetResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/identifier/RelatedObjectSetResource.java @@ -23,13 +23,13 @@ import java.util.List; import java.util.Set; import javax.ws.rs.Consumes; import javax.ws.rs.GET; +import javax.ws.rs.PATCH; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.apache.guacamole.GuacamoleClientException; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.net.auth.RelatedObjectSet; import org.apache.guacamole.rest.APIPatch; -import org.apache.guacamole.rest.PATCH; /** * A REST resource which abstracts the operations available on arbitrary sets diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/permission/PermissionSetResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/permission/PermissionSetResource.java index 739a39c5e..38b337e0e 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/permission/PermissionSetResource.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/permission/PermissionSetResource.java @@ -22,6 +22,7 @@ package org.apache.guacamole.rest.permission; import java.util.List; import javax.ws.rs.Consumes; import javax.ws.rs.GET; +import javax.ws.rs.PATCH; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.apache.guacamole.GuacamoleClientException; @@ -31,7 +32,6 @@ import org.apache.guacamole.net.auth.permission.ObjectPermission; import org.apache.guacamole.net.auth.permission.Permission; import org.apache.guacamole.net.auth.permission.SystemPermission; import org.apache.guacamole.rest.APIPatch; -import org.apache.guacamole.rest.PATCH; /** * A REST resource which abstracts the operations available on the permissions diff --git a/guacamole/src/main/webapp/WEB-INF/web.xml b/guacamole/src/main/webapp/WEB-INF/web.xml index bc067c704..52a52a624 100644 --- a/guacamole/src/main/webapp/WEB-INF/web.xml +++ b/guacamole/src/main/webapp/WEB-INF/web.xml @@ -28,7 +28,7 @@ index.html - + guiceFilter com.google.inject.servlet.GuiceFilter @@ -37,7 +37,6 @@ guiceFilter /* - org.apache.guacamole.GuacamoleServletContextListener