summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/pgm/http/jetty/JettyServer.java')
-rw-r--r--java/com/google/gerrit/pgm/http/jetty/JettyServer.java459
1 files changed, 459 insertions, 0 deletions
diff --git a/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
new file mode 100644
index 0000000000..25a28a4cfd
--- /dev/null
+++ b/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -0,0 +1,459 @@
+// Copyright (C) 2009 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.pgm.http.jetty;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.client.AuthType;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.pgm.http.jetty.HttpLog.HttpLogFactory;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.config.ThreadSettingsConfig;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Singleton;
+import com.google.inject.servlet.GuiceFilter;
+import com.google.inject.servlet.GuiceServletContextListener;
+import java.lang.management.ManagementFactory;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import javax.servlet.DispatcherType;
+import javax.servlet.Filter;
+import org.eclipse.jetty.http.HttpScheme;
+import org.eclipse.jetty.jmx.MBeanContainer;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.ForwardedRequestCustomizer;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
+import org.eclipse.jetty.server.handler.RequestLogHandler;
+import org.eclipse.jetty.server.handler.StatisticsHandler;
+import org.eclipse.jetty.server.session.SessionHandler;
+import org.eclipse.jetty.servlet.DefaultServlet;
+import org.eclipse.jetty.servlet.FilterHolder;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.util.BlockingArrayQueue;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.util.thread.ThreadPool;
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class JettyServer {
+ static class Lifecycle implements LifecycleListener {
+ private final JettyServer server;
+ private final Config cfg;
+
+ @Inject
+ Lifecycle(JettyServer server, @GerritServerConfig Config cfg) {
+ this.server = server;
+ this.cfg = cfg;
+ }
+
+ @Override
+ public void start() {
+ try {
+ String origUrl = cfg.getString("httpd", null, "listenUrl");
+ boolean rewrite = !Strings.isNullOrEmpty(origUrl) && origUrl.endsWith(":0/");
+ server.httpd.start();
+ if (rewrite) {
+ Connector con = server.httpd.getConnectors()[0];
+ if (con instanceof ServerConnector) {
+ @SuppressWarnings("resource")
+ ServerConnector serverCon = (ServerConnector) con;
+ String host = serverCon.getHost();
+ int port = serverCon.getLocalPort();
+ String url = String.format("http://%s:%d", host, port);
+ cfg.setString("gerrit", null, "canonicalWebUrl", url);
+ cfg.setString("httpd", null, "listenUrl", url);
+ }
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException("Cannot start HTTP daemon", e);
+ }
+ }
+
+ @Override
+ public void stop() {
+ try {
+ server.httpd.stop();
+ server.httpd.join();
+ } catch (Exception e) {
+ throw new IllegalStateException("Cannot stop HTTP daemon", e);
+ }
+ }
+ }
+
+ private final SitePaths site;
+ private final Server httpd;
+
+ private boolean reverseProxy;
+
+ @Inject
+ JettyServer(
+ @GerritServerConfig Config cfg,
+ ThreadSettingsConfig threadSettingsConfig,
+ SitePaths site,
+ JettyEnv env,
+ HttpLogFactory httpLogFactory) {
+ this.site = site;
+
+ httpd = new Server(threadPool(cfg, threadSettingsConfig));
+ httpd.setConnectors(listen(httpd, cfg));
+
+ Handler app = makeContext(env, cfg);
+ if (cfg.getBoolean("httpd", "requestLog", !reverseProxy)) {
+ RequestLogHandler handler = new RequestLogHandler();
+ handler.setRequestLog(httpLogFactory.get());
+ handler.setHandler(app);
+ app = handler;
+ }
+ if (cfg.getBoolean("httpd", "registerMBeans", false)) {
+ MBeanContainer mbean = new MBeanContainer(ManagementFactory.getPlatformMBeanServer());
+ httpd.addEventListener(mbean);
+ httpd.addBean(Log.getRootLogger());
+ httpd.addBean(mbean);
+ }
+
+ long gracefulStopTimeout =
+ cfg.getTimeUnit("httpd", null, "gracefulStopTimeout", 0L, TimeUnit.MILLISECONDS);
+ if (gracefulStopTimeout > 0) {
+ StatisticsHandler statsHandler = new StatisticsHandler();
+ statsHandler.setHandler(app);
+ app = statsHandler;
+ httpd.setStopTimeout(gracefulStopTimeout);
+ }
+
+ httpd.setHandler(app);
+ httpd.setStopAtShutdown(false);
+ }
+
+ private Connector[] listen(Server server, Config cfg) {
+ // OpenID and certain web-based single-sign-on products can cause
+ // some very long headers, especially in the Referer header. We
+ // need to use a larger default header size to ensure we have
+ // the space required.
+ //
+ final int requestHeaderSize = cfg.getInt("httpd", "requestheadersize", 16386);
+ final URI[] listenUrls = listenURLs(cfg);
+ final boolean reuseAddress = cfg.getBoolean("httpd", "reuseaddress", true);
+ final int acceptors = cfg.getInt("httpd", "acceptorThreads", 2);
+ final AuthType authType = cfg.getEnum("auth", null, "type", AuthType.OPENID);
+
+ reverseProxy = isReverseProxied(listenUrls);
+ final Connector[] connectors = new Connector[listenUrls.length];
+ for (int idx = 0; idx < listenUrls.length; idx++) {
+ final URI u = listenUrls[idx];
+ final int defaultPort;
+ final ServerConnector c;
+ HttpConfiguration config = defaultConfig(requestHeaderSize);
+
+ if (AuthType.CLIENT_SSL_CERT_LDAP.equals(authType) && !"https".equals(u.getScheme())) {
+ throw new IllegalArgumentException(
+ "Protocol '"
+ + u.getScheme()
+ + "' "
+ + " not supported in httpd.listenurl '"
+ + u
+ + "' when auth.type = '"
+ + AuthType.CLIENT_SSL_CERT_LDAP.name()
+ + "'; only 'https' is supported");
+ }
+
+ if ("http".equals(u.getScheme())) {
+ defaultPort = 80;
+ c = newServerConnector(server, acceptors, config);
+
+ } else if ("https".equals(u.getScheme())) {
+ SslContextFactory ssl = new SslContextFactory();
+ final Path keystore = getFile(cfg, "sslkeystore", "etc/keystore");
+ String password = cfg.getString("httpd", null, "sslkeypassword");
+ if (password == null) {
+ password = "gerrit";
+ }
+ ssl.setKeyStorePath(keystore.toAbsolutePath().toString());
+ ssl.setTrustStorePath(keystore.toAbsolutePath().toString());
+ ssl.setKeyStorePassword(password);
+ ssl.setTrustStorePassword(password);
+
+ if (AuthType.CLIENT_SSL_CERT_LDAP.equals(authType)) {
+ ssl.setNeedClientAuth(true);
+
+ Path crl = getFile(cfg, "sslCrl", "etc/crl.pem");
+ if (Files.exists(crl)) {
+ ssl.setCrlPath(crl.toAbsolutePath().toString());
+ ssl.setValidatePeerCerts(true);
+ }
+ }
+
+ defaultPort = 443;
+
+ config.addCustomizer(new SecureRequestCustomizer());
+ c =
+ new ServerConnector(
+ server,
+ null,
+ null,
+ null,
+ 0,
+ acceptors,
+ new SslConnectionFactory(ssl, "http/1.1"),
+ new HttpConnectionFactory(config));
+
+ } else if ("proxy-http".equals(u.getScheme())) {
+ defaultPort = 8080;
+ config.addCustomizer(new ForwardedRequestCustomizer());
+ c = newServerConnector(server, acceptors, config);
+
+ } else if ("proxy-https".equals(u.getScheme())) {
+ defaultPort = 8080;
+ config.addCustomizer(new ForwardedRequestCustomizer());
+ config.addCustomizer(
+ new HttpConfiguration.Customizer() {
+ @Override
+ public void customize(
+ Connector connector, HttpConfiguration channelConfig, Request request) {
+ request.setScheme(HttpScheme.HTTPS.asString());
+ request.setSecure(true);
+ }
+ });
+ c = newServerConnector(server, acceptors, config);
+
+ } else {
+ throw new IllegalArgumentException(
+ "Protocol '"
+ + u.getScheme()
+ + "' "
+ + " not supported in httpd.listenurl '"
+ + u
+ + "';"
+ + " only 'http', 'https', 'proxy-http, 'proxy-https'"
+ + " are supported");
+ }
+
+ try {
+ if (u.getHost() == null
+ && (u.getAuthority().equals("*") //
+ || u.getAuthority().startsWith("*:"))) {
+ // Bind to all local addresses. Port wasn't parsed right by URI
+ // due to the illegal host of "*" so replace with a legal name
+ // and parse the URI.
+ //
+ final URI r = new URI(u.toString().replace('*', 'A')).parseServerAuthority();
+ c.setHost(null);
+ c.setPort(0 < r.getPort() ? r.getPort() : defaultPort);
+ } else {
+ final URI r = u.parseServerAuthority();
+ c.setHost(r.getHost());
+ c.setPort(0 <= r.getPort() ? r.getPort() : defaultPort);
+ }
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException("Invalid httpd.listenurl " + u, e);
+ }
+ c.setInheritChannel(cfg.getBoolean("httpd", "inheritChannel", false));
+ c.setReuseAddress(reuseAddress);
+ c.setIdleTimeout(cfg.getTimeUnit("httpd", null, "idleTimeout", 30000L, MILLISECONDS));
+ connectors[idx] = c;
+ }
+ return connectors;
+ }
+
+ private static ServerConnector newServerConnector(
+ Server server, int acceptors, HttpConfiguration config) {
+ return new ServerConnector(
+ server, null, null, null, 0, acceptors, new HttpConnectionFactory(config));
+ }
+
+ private HttpConfiguration defaultConfig(int requestHeaderSize) {
+ HttpConfiguration config = new HttpConfiguration();
+ config.setRequestHeaderSize(requestHeaderSize);
+ config.setSendServerVersion(false);
+ config.setSendDateHeader(true);
+ return config;
+ }
+
+ static boolean isReverseProxied(URI[] listenUrls) {
+ for (URI u : listenUrls) {
+ if ("http".equals(u.getScheme()) || "https".equals(u.getScheme())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ static URI[] listenURLs(Config cfg) {
+ String[] urls = cfg.getStringList("httpd", null, "listenurl");
+ if (urls.length == 0) {
+ urls = new String[] {"http://*:8080/"};
+ }
+
+ final URI[] r = new URI[urls.length];
+ for (int i = 0; i < r.length; i++) {
+ final String s = urls[i];
+ try {
+ r[i] = new URI(s);
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException("Invalid httpd.listenurl " + s, e);
+ }
+ }
+ return r;
+ }
+
+ private Path getFile(Config cfg, String name, String def) {
+ String path = cfg.getString("httpd", null, name);
+ if (path == null || path.length() == 0) {
+ path = def;
+ }
+ return site.resolve(path);
+ }
+
+ private ThreadPool threadPool(Config cfg, ThreadSettingsConfig threadSettingsConfig) {
+ int maxThreads = threadSettingsConfig.getHttpdMaxThreads();
+ int minThreads = cfg.getInt("httpd", null, "minthreads", 5);
+ int maxQueued = cfg.getInt("httpd", null, "maxqueued", 200);
+ int idleTimeout = (int) MILLISECONDS.convert(60, SECONDS);
+ int maxCapacity = maxQueued == 0 ? Integer.MAX_VALUE : Math.max(minThreads, maxQueued);
+ QueuedThreadPool pool =
+ new QueuedThreadPool(
+ maxThreads,
+ minThreads,
+ idleTimeout,
+ new BlockingArrayQueue<Runnable>(
+ minThreads, // capacity,
+ minThreads, // growBy,
+ maxCapacity // maxCapacity
+ ));
+ pool.setName("HTTP");
+ return pool;
+ }
+
+ private Handler makeContext(JettyEnv env, Config cfg) {
+ final Set<String> paths = new HashSet<>();
+ for (URI u : listenURLs(cfg)) {
+ String p = u.getPath();
+ if (p == null || p.isEmpty()) {
+ p = "/";
+ }
+ while (1 < p.length() && p.endsWith("/")) {
+ p = p.substring(0, p.length() - 1);
+ }
+ paths.add(p);
+ }
+
+ final List<ContextHandler> all = new ArrayList<>();
+ for (String path : paths) {
+ all.add(makeContext(path, env, cfg));
+ }
+
+ if (all.size() == 1) {
+ // If we only have one context path in our web space, return it
+ // without any wrapping so Jetty has less work to do per-request.
+ //
+ return all.get(0);
+ }
+ // We have more than one path served out of this container so
+ // combine them in a handler which supports dispatching to the
+ // individual contexts.
+ //
+ final ContextHandlerCollection r = new ContextHandlerCollection();
+ r.setHandlers(all.toArray(new Handler[0]));
+ return r;
+ }
+
+ private ContextHandler makeContext(final String contextPath, JettyEnv env, Config cfg) {
+ final ServletContextHandler app = new ServletContextHandler();
+
+ // This enables the use of sessions in Jetty, feature available
+ // for Gerrit plug-ins to enable user-level sessions.
+ //
+ app.setSessionHandler(new SessionHandler());
+ app.setErrorHandler(new HiddenErrorHandler());
+
+ // This is the path we are accessed by clients within our domain.
+ //
+ app.setContextPath(contextPath);
+
+ // HTTP front-end filters to be used as surrogate of Apache HTTP
+ // reverse-proxy filtering.
+ // It is meant to be used as simpler tiny deployment of custom-made
+ // security enforcement (Security tokens, IP-based security filtering, others)
+ String[] filterClassNames = cfg.getStringList("httpd", null, "filterClass");
+ for (String filterClassName : filterClassNames) {
+ try {
+ @SuppressWarnings("unchecked")
+ Class<? extends Filter> filterClass =
+ (Class<? extends Filter>) Class.forName(filterClassName);
+ Filter filter = env.webInjector.getInstance(filterClass);
+ app.addFilter(
+ new FilterHolder(filter),
+ "/*",
+ EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC));
+ } catch (Throwable e) {
+ throw new IllegalArgumentException(
+ "Unable to instantiate front-end HTTP Filter " + filterClassName, e);
+ }
+ }
+
+ // Perform the same binding as our web.xml would do, but instead
+ // of using the listener to create the injector pass the one we
+ // already have built.
+ //
+ GuiceFilter filter = env.webInjector.getInstance(GuiceFilter.class);
+ app.addFilter(
+ new FilterHolder(filter), "/*", EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC));
+ app.addEventListener(
+ new GuiceServletContextListener() {
+ @Override
+ protected Injector getInjector() {
+ return env.webInjector;
+ }
+ });
+
+ // Jetty requires at least one servlet be bound before it will
+ // bother running the filter above. Since the filter has all
+ // of our URLs except the static resources, the only servlet
+ // we need to bind is the default static resource servlet from
+ // the Jetty container.
+ //
+ final ServletHolder ds = app.addServlet(DefaultServlet.class, "/");
+ ds.setInitParameter("dirAllowed", "false");
+ ds.setInitParameter("redirectWelcome", "false");
+ ds.setInitParameter("useFileMappedBuffer", "false");
+ ds.setInitParameter("gzip", "true");
+
+ app.setWelcomeFiles(new String[0]);
+ return app;
+ }
+}