465 lines
17 KiB
Java
465 lines
17 KiB
Java
// 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;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
@Singleton
|
|
public class JettyServer {
|
|
private static final Logger log = LoggerFactory.getLogger(JettyServer.class);
|
|
|
|
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) {
|
|
String errorMessage = "Unable to instantiate front-end HTTP Filter " + filterClassName;
|
|
log.error(errorMessage, e);
|
|
throw new IllegalArgumentException(errorMessage, 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;
|
|
}
|
|
}
|