diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java new file mode 100644 index 0000000000..1de330fc00 --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java @@ -0,0 +1,30 @@ +// Copyright (C) 2012 The Android Open Source Project +// +// Licensed 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 com.google.gerrit.httpd.plugins; + +import com.google.gerrit.server.plugins.StartPluginListener; +import com.google.inject.internal.UniqueAnnotations; +import com.google.inject.servlet.ServletModule; + +public class HttpPluginModule extends ServletModule { + @Override + protected void configureServlets() { + serve("/plugins/*").with(HttpPluginServlet.class); + + bind(StartPluginListener.class) + .annotatedWith(UniqueAnnotations.create()) + .to(HttpPluginServlet.class); + } +} diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java new file mode 100644 index 0000000000..b73d6e6b1e --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java @@ -0,0 +1,166 @@ +// Copyright (C) 2012 The Android Open Source Project +// +// Licensed 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 com.google.gerrit.httpd.plugins; + +import com.google.common.base.Strings; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.gerrit.server.plugins.Plugin; +import com.google.gerrit.server.plugins.RegistrationHandle; +import com.google.gerrit.server.plugins.StartPluginListener; +import com.google.inject.Singleton; +import com.google.inject.servlet.GuiceFilter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.ConcurrentMap; + +import javax.servlet.FilterChain; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; + +@Singleton +class HttpPluginServlet extends HttpServlet + implements StartPluginListener { + private static final long serialVersionUID = 1L; + private static final Logger log + = LoggerFactory.getLogger(HttpPluginServlet.class); + + private List pending = Lists.newArrayList(); + private String base; + private final ConcurrentMap plugins + = Maps.newConcurrentMap(); + + @Override + public synchronized void init(ServletConfig config) throws ServletException { + super.init(config); + + String path = config.getServletContext().getContextPath(); + base = Strings.nullToEmpty(path) + "/plugins/"; + for (Plugin plugin : pending) { + start(plugin); + } + pending = null; + } + + @Override + public synchronized void onStartPlugin(Plugin plugin) { + if (pending != null) { + pending.add(plugin); + } else { + start(plugin); + } + } + + private void start(Plugin plugin) { + if (plugin.getHttpInjector() != null) { + final String name = plugin.getName(); + final GuiceFilter filter; + try { + filter = plugin.getHttpInjector().getInstance(GuiceFilter.class); + } catch (RuntimeException e) { + log.warn(String.format("Plugin %s cannot load GuiceFilter", name), e); + return; + } + + try { + WrappedContext ctx = new WrappedContext(plugin, base + name); + filter.init(new WrappedFilterConfig(ctx)); + } catch (ServletException e) { + log.warn(String.format("Plugin %s failed to initialize HTTP", name), e); + return; + } + + plugin.add(new RegistrationHandle() { + @Override + public void remove() { + try { + filter.destroy(); + } finally { + plugins.remove(name, filter); + } + } + }); + plugins.put(name, filter); + } + } + + @Override + public void service(HttpServletRequest req, HttpServletResponse res) + throws IOException, ServletException { + String name = extractName(req); + GuiceFilter filter = plugins.get(name); + if (filter == null) { + noCache(res); + res.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + filter.doFilter(new WrappedRequest(req, base + name), res, + new FilterChain() { + @Override + public void doFilter(ServletRequest req, ServletResponse response) + throws IOException, ServletException { + HttpServletResponse res = (HttpServletResponse) response; + noCache(res); + res.sendError(HttpServletResponse.SC_NOT_FOUND); + } + }); + } + + private static String extractName(HttpServletRequest req) { + String path = req.getPathInfo(); + if (Strings.isNullOrEmpty(path) || "/".equals(path)) { + return ""; + } + int s = path.indexOf('/', 1); + return 0 <= s ? path.substring(1, s) : path.substring(1); + } + + private static void noCache(HttpServletResponse res) { + res.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT"); + res.setHeader("Pragma", "no-cache"); + res.setHeader("Cache-Control", "no-cache, must-revalidate"); + res.setHeader("Content-Disposition", "attachment"); + } + + private static class WrappedRequest extends HttpServletRequestWrapper { + private final String contextPath; + + WrappedRequest(HttpServletRequest req, String contextPath) { + super(req); + this.contextPath = contextPath; + } + + @Override + public String getContextPath() { + return contextPath; + } + + @Override + public String getServletPath() { + return ((HttpServletRequest) getRequest()).getRequestURI(); + } + } +} diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedContext.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedContext.java new file mode 100644 index 0000000000..daeb6ff87d --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedContext.java @@ -0,0 +1,178 @@ +// Copyright (C) 2012 The Android Open Source Project +// +// Licensed 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 com.google.gerrit.httpd.plugins; + +import com.google.common.collect.Maps; +import com.google.gerrit.common.Version; +import com.google.gerrit.server.plugins.Plugin; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; + +import javax.servlet.RequestDispatcher; +import javax.servlet.Servlet; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; + +class WrappedContext implements ServletContext { + private static final Logger log = LoggerFactory.getLogger("plugin"); + private final Plugin plugin; + private final String contextPath; + private final ConcurrentMap attributes; + + WrappedContext(Plugin plugin, String contextPath) { + this.plugin = plugin; + this.contextPath = contextPath; + this.attributes = Maps.newConcurrentMap(); + } + + @Override + public String getContextPath() { + return contextPath; + } + + @Override + public String getInitParameter(String name) { + return null; + } + + @SuppressWarnings("rawtypes") + @Override + public Enumeration getInitParameterNames() { + return Collections.enumeration(Collections.emptyList()); + } + + @Override + public ServletContext getContext(String name) { + return null; + } + + @Override + public RequestDispatcher getNamedDispatcher(String name) { + return null; + } + + @Override + public RequestDispatcher getRequestDispatcher(String name) { + return null; + } + + @Override + public URL getResource(String name) throws MalformedURLException { + return null; + } + + @Override + public InputStream getResourceAsStream(String name) { + return null; + } + + @SuppressWarnings("rawtypes") + @Override + public Set getResourcePaths(String name) { + return null; + } + + @Override + public Servlet getServlet(String name) throws ServletException { + return null; + } + + @Override + public String getRealPath(String name) { + return null; + } + + @Override + public String getServletContextName() { + return plugin.getName(); + } + + @SuppressWarnings("rawtypes") + @Override + public Enumeration getServletNames() { + return Collections.enumeration(Collections.emptyList()); + } + + @SuppressWarnings("rawtypes") + @Override + public Enumeration getServlets() { + return Collections.enumeration(Collections.emptyList()); + } + + @Override + public void log(Exception reason, String msg) { + log(msg, reason); + } + + @Override + public void log(String msg) { + log(msg, null); + } + + @Override + public void log(String msg, Throwable reason) { + log.warn(String.format("[plugin %s] %s", plugin.getName(), msg), reason); + } + + @Override + public Object getAttribute(String name) { + return attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(attributes.keySet()); + } + + @Override + public void setAttribute(String name, Object value) { + attributes.put(name, value); + } + + @Override + public void removeAttribute(String name) { + attributes.remove(name); + } + + @Override + public String getMimeType(String file) { + return null; + } + + @Override + public int getMajorVersion() { + return 2; + } + + @Override + public int getMinorVersion() { + return 5; + } + + @Override + public String getServerInfo() { + String v = Version.getVersion(); + return "Gerrit Code Review/" + (v != null ? v : "dev"); + } +} diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java new file mode 100644 index 0000000000..c9107dc8d2 --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java @@ -0,0 +1,52 @@ +// Copyright (C) 2012 The Android Open Source Project +// +// Licensed 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 com.google.gerrit.httpd.plugins; + +import com.google.inject.servlet.GuiceFilter; + +import java.util.Collections; +import java.util.Enumeration; + +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; + +class WrappedFilterConfig implements FilterConfig { + private final WrappedContext context; + + WrappedFilterConfig(WrappedContext context) { + this.context = context; + } + + @Override + public String getFilterName() { + return GuiceFilter.class.getName(); + } + + @Override + public String getInitParameter(String name) { + return null; + } + + @SuppressWarnings("rawtypes") + @Override + public Enumeration getInitParameterNames() { + return Collections.enumeration(Collections.emptyList()); + } + + @Override + public ServletContext getServletContext() { + return context; + } +} diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java index 85b10126c0..bbff5cbae8 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java @@ -24,6 +24,7 @@ import com.google.gerrit.httpd.HttpCanonicalWebUrlProvider; import com.google.gerrit.httpd.WebModule; import com.google.gerrit.httpd.WebSshGlueModule; import com.google.gerrit.httpd.auth.openid.OpenIdModule; +import com.google.gerrit.httpd.plugins.HttpPluginModule; import com.google.gerrit.lifecycle.LifecycleManager; import com.google.gerrit.pgm.http.jetty.GetUserFilter; import com.google.gerrit.pgm.http.jetty.JettyEnv; @@ -259,6 +260,9 @@ public class Daemon extends SiteProgram { private void initHttpd() { webInjector = createWebInjector(); + sysInjector.getInstance(PluginGuiceEnvironment.class) + .setHttpInjector(webInjector); + sysInjector.getInstance(HttpCanonicalWebUrlProvider.class) .setHttpServletRequest( webInjector.getProvider(HttpServletRequest.class)); @@ -273,6 +277,7 @@ public class Daemon extends SiteProgram { modules.add(HttpContactStoreConnection.module()); modules.add(sysInjector.getInstance(GitOverHttpModule.class)); modules.add(sysInjector.getInstance(WebModule.class)); + modules.add(new HttpPluginModule()); if (sshd) { modules.add(sshInjector.getInstance(WebSshGlueModule.class)); modules.add(new ProjectQoSFilter.Module()); diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java index fa5ef5918d..a57de3cb8a 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java @@ -40,6 +40,7 @@ import org.eclipse.jetty.server.handler.RequestLogHandler; import org.eclipse.jetty.server.nio.SelectChannelConnector; import org.eclipse.jetty.server.ssl.SslSelectChannelConnector; import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.FilterMapping; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; @@ -328,7 +329,8 @@ public class JettyServer { // of using the listener to create the injector pass the one we // already have built. // - app.addFilter(GuiceFilter.class, "/*", FilterMapping.DEFAULT); + GuiceFilter filter = env.webInjector.getInstance(GuiceFilter.class); + app.addFilter(new FilterHolder(filter), "/*", FilterMapping.DEFAULT); app.addEventListener(new GuiceServletContextListener() { @Override protected Injector getInjector() { diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml index e0c1514368..5c4ca34492 100644 --- a/gerrit-plugin-api/pom.xml +++ b/gerrit-plugin-api/pom.xml @@ -38,6 +38,17 @@ limitations under the License. gerrit-sshd ${project.version} + + + com.google.gerrit + gerrit-httpd + ${project.version} + + + + org.apache.tomcat + servlet-api + diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java index 0c1ab0fdaf..e9a63085c1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java @@ -20,6 +20,7 @@ import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Module; +import com.google.inject.servlet.GuiceFilter; import org.eclipse.jgit.storage.file.FileSnapshot; @@ -30,15 +31,24 @@ import java.util.jar.Manifest; import javax.annotation.Nullable; public class Plugin { + static { + // Guice logs warnings about multiple injectors being created. + // Silence this in case HTTP plugins are used. + java.util.logging.Logger.getLogger(GuiceFilter.class.getName()) + .setLevel(java.util.logging.Level.OFF); + } + private final String name; private final File jar; private final Manifest manifest; private final FileSnapshot snapshot; private Class sysModule; private Class sshModule; + private Class httpModule; private Injector sysInjector; private Injector sshInjector; + private Injector httpInjector; private LifecycleManager manager; public Plugin(String name, @@ -46,13 +56,15 @@ public class Plugin { Manifest manifest, FileSnapshot snapshot, @Nullable Class sysModule, - @Nullable Class sshModule) { + @Nullable Class sshModule, + @Nullable Class httpModule) { this.name = name; this.jar = jar; this.manifest = manifest; this.snapshot = snapshot; this.sysModule = sysModule; this.sshModule = sshModule; + this.httpModule = httpModule; } File getJar() { @@ -90,6 +102,13 @@ public class Plugin { manager.add(sshInjector); } + if (httpModule != null && env.hasHttpModule()) { + httpInjector = sysInjector.createChildInjector( + env.getHttpModule(), + sysInjector.getInstance(httpModule)); + manager.add(httpInjector); + } + manager.start(); env.onStartPlugin(this); } @@ -113,6 +132,7 @@ public class Plugin { manager = null; sysInjector = null; sshInjector = null; + httpInjector = null; } } @@ -121,6 +141,11 @@ public class Plugin { return sshInjector; } + @Nullable + public Injector getHttpInjector() { + return httpInjector; + } + public void add(final RegistrationHandle handle) { add(new LifecycleListener() { @Override diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java index 418fbf29b4..4b6f49710c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java @@ -45,6 +45,7 @@ public class PluginGuiceEnvironment { private final List listeners; private Module sysModule; private Module sshModule; + private Module httpModule; @Inject PluginGuiceEnvironment(Injector sysInjector, CopyConfigModule ccm) { @@ -84,6 +85,19 @@ public class PluginGuiceEnvironment { return sshModule; } + public void setHttpInjector(Injector httpInjector) { + httpModule = copy(httpInjector); + listeners.addAll(getListeners(httpInjector)); + } + + boolean hasHttpModule() { + return httpModule != null; + } + + Module getHttpModule() { + return httpModule; + } + void onStartPlugin(Plugin plugin) { for (StartPluginListener l : listeners) { l.onStartPlugin(plugin); @@ -126,15 +140,74 @@ public class PluginGuiceEnvironment { private static boolean shouldCopy(Key key) { Class type = key.getTypeLiteral().getRawType(); - if (type == LifecycleListener.class) { + if (LifecycleListener.class.isAssignableFrom(type)) { return false; } - if (type == StartPluginListener.class) { + if (StartPluginListener.class.isAssignableFrom(type)) { return false; } - if ("org.apache.sshd.server.Command".equals(type.getName())) { + + if (type.getName().startsWith("com.google.inject.")) { + return false; + } + + if (is("org.apache.sshd.server.Command", type)) { + return false; + } + + if (is("javax.servlet.Filter", type)) { + return false; + } + if (is("javax.servlet.ServletContext", type)) { + return false; + } + if (is("javax.servlet.ServletRequest", type)) { + return false; + } + if (is("javax.servlet.ServletResponse", type)) { + return false; + } + if (is("javax.servlet.http.HttpServlet", type)) { + return false; + } + if (is("javax.servlet.http.HttpServletRequest", type)) { + return false; + } + if (is("javax.servlet.http.HttpServletResponse", type)) { + return false; + } + if (is("javax.servlet.http.HttpSession", type)) { + return false; + } + if (Map.class.isAssignableFrom(type) + && key.getAnnotationType() != null + && "com.google.inject.servlet.RequestParameters" + .equals(key.getAnnotationType().getName())) { + return false; + } + if (type.getName().startsWith("com.google.gerrit.httpd.GitOverHttpServlet$")) { return false; } return true; } + + private static boolean is(String name, Class type) { + Class p = type; + while (p != null) { + if (name.equals(p.getName())) { + return true; + } + p = p.getSuperclass(); + } + + Class[] interfaces = type.getInterfaces(); + if (interfaces != null) { + for (Class i : interfaces) { + if (is(name, i)) { + return true; + } + } + } + return false; + } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java index 44b2f12aee..2ee6b04f81 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java @@ -245,6 +245,7 @@ public class PluginLoader implements LifecycleListener { Attributes main = manifest.getMainAttributes(); String sysName = main.getValue("Gerrit-Module"); String sshName = main.getValue("Gerrit-SshModule"); + String httpName = main.getValue("Gerrit-HttpModule"); URL[] urls = {jarFile.toURI().toURL()}; ClassLoader parentLoader = PluginLoader.class.getClassLoader(); @@ -252,7 +253,9 @@ public class PluginLoader implements LifecycleListener { Class sysModule = load(sysName, pluginLoader); Class sshModule = load(sshName, pluginLoader); - return new Plugin(name, jarFile, manifest, snapshot, sysModule, sshModule); + Class httpModule = load(httpName, pluginLoader); + return new Plugin(name, jarFile, manifest, snapshot, + sysModule, sshModule, httpModule); } private Class load(String name, ClassLoader pluginLoader) diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java index 0c15dfdd00..8db75e267d 100644 --- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java +++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java @@ -20,6 +20,7 @@ import static com.google.inject.Stage.PRODUCTION; import com.google.gerrit.common.ChangeHookRunner; import com.google.gerrit.ehcache.EhcachePoolImpl; import com.google.gerrit.httpd.auth.openid.OpenIdModule; +import com.google.gerrit.httpd.plugins.HttpPluginModule; import com.google.gerrit.lifecycle.LifecycleManager; import com.google.gerrit.lifecycle.LifecycleModule; import com.google.gerrit.reviewdb.client.AuthType; @@ -117,6 +118,7 @@ public class WebAppInitializer extends GuiceServletContextListener { PluginGuiceEnvironment env = sysInjector.getInstance(PluginGuiceEnvironment.class); env.setCfgInjector(cfgInjector); env.setSshInjector(sshInjector); + env.setHttpInjector(webInjector); // Push the Provider down into the canonical // URL provider. Its optional for that provider, but since we can @@ -228,6 +230,7 @@ public class WebAppInitializer extends GuiceServletContextListener { modules.add(sshInjector.getInstance(WebSshGlueModule.class)); modules.add(CacheBasedWebSession.module()); modules.add(HttpContactStoreConnection.module()); + modules.add(new HttpPluginModule()); AuthConfig authConfig = cfgInjector.getInstance(AuthConfig.class); if (authConfig.getAuthType() == AuthType.OPENID) {