565 lines
19 KiB
Java
565 lines
19 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.io.ByteStreams;
|
|
import com.google.gerrit.extensions.events.LifecycleListener;
|
|
import com.google.gerrit.launcher.GerritLauncher;
|
|
import com.google.gerrit.reviewdb.client.AuthType;
|
|
import com.google.gerrit.server.config.ConfigUtil;
|
|
import com.google.gerrit.server.config.GerritServerConfig;
|
|
import com.google.gerrit.server.config.SitePaths;
|
|
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 org.eclipse.jetty.io.EndPoint;
|
|
import org.eclipse.jetty.server.Connector;
|
|
import org.eclipse.jetty.server.Handler;
|
|
import org.eclipse.jetty.server.Request;
|
|
import org.eclipse.jetty.server.Server;
|
|
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.nio.SelectChannelConnector;
|
|
import org.eclipse.jetty.server.session.SessionHandler;
|
|
import org.eclipse.jetty.server.ssl.SslSelectChannelConnector;
|
|
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.resource.Resource;
|
|
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;
|
|
|
|
import java.io.File;
|
|
import java.io.FileNotFoundException;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.InterruptedIOException;
|
|
import java.net.MalformedURLException;
|
|
import java.net.URI;
|
|
import java.net.URISyntaxException;
|
|
import java.net.URL;
|
|
import java.util.ArrayList;
|
|
import java.util.EnumSet;
|
|
import java.util.Enumeration;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
import java.util.zip.ZipEntry;
|
|
import java.util.zip.ZipFile;
|
|
|
|
import javax.servlet.DispatcherType;
|
|
|
|
@Singleton
|
|
public class JettyServer {
|
|
private static final Logger log = LoggerFactory.getLogger(JettyServer.class);
|
|
|
|
static class Lifecycle implements LifecycleListener {
|
|
private final JettyServer server;
|
|
|
|
@Inject
|
|
Lifecycle(final JettyServer server) {
|
|
this.server = server;
|
|
}
|
|
|
|
@Override
|
|
public void start() {
|
|
try {
|
|
server.httpd.start();
|
|
} 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;
|
|
|
|
/** Location on disk where our WAR file was unpacked to. */
|
|
private Resource baseResource;
|
|
|
|
@Inject
|
|
JettyServer(@GerritServerConfig final Config cfg, final SitePaths site,
|
|
final JettyEnv env)
|
|
throws MalformedURLException, IOException {
|
|
this.site = site;
|
|
|
|
httpd = new Server();
|
|
httpd.setConnectors(listen(cfg));
|
|
httpd.setThreadPool(threadPool(cfg));
|
|
|
|
Handler app = makeContext(env, cfg);
|
|
if (cfg.getBoolean("httpd", "requestlog", !reverseProxy)) {
|
|
RequestLogHandler handler = new RequestLogHandler();
|
|
handler.setRequestLog(new HttpLog(site, cfg));
|
|
handler.setHandler(app);
|
|
app = handler;
|
|
}
|
|
httpd.setHandler(app);
|
|
|
|
httpd.setStopAtShutdown(false);
|
|
httpd.setSendDateHeader(true);
|
|
httpd.setSendServerVersion(false);
|
|
httpd.setGracefulShutdown((int) MILLISECONDS.convert(1, SECONDS));
|
|
}
|
|
|
|
private Connector[] listen(final 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 = ConfigUtil.getEnum(cfg, "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 SelectChannelConnector c;
|
|
|
|
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 = new SelectChannelConnector();
|
|
} else if ("https".equals(u.getScheme())) {
|
|
SslContextFactory ssl = new SslContextFactory();
|
|
final File keystore = getFile(cfg, "sslkeystore", "etc/keystore");
|
|
String password = cfg.getString("httpd", null, "sslkeypassword");
|
|
if (password == null) {
|
|
password = "gerrit";
|
|
}
|
|
ssl.setKeyStorePath(keystore.getAbsolutePath());
|
|
ssl.setTrustStore(keystore.getAbsolutePath());
|
|
ssl.setKeyStorePassword(password);
|
|
ssl.setTrustStorePassword(password);
|
|
|
|
if (AuthType.CLIENT_SSL_CERT_LDAP.equals(authType)) {
|
|
ssl.setNeedClientAuth(true);
|
|
}
|
|
|
|
defaultPort = 443;
|
|
c = new SslSelectChannelConnector(ssl);
|
|
|
|
} else if ("proxy-http".equals(u.getScheme())) {
|
|
defaultPort = 8080;
|
|
c = new SelectChannelConnector();
|
|
c.setForwarded(true);
|
|
|
|
} else if ("proxy-https".equals(u.getScheme())) {
|
|
defaultPort = 8080;
|
|
c = new SelectChannelConnector() {
|
|
@Override
|
|
public void customize(EndPoint endpoint, Request request)
|
|
throws IOException {
|
|
request.setScheme("https");
|
|
super.customize(endpoint, request);
|
|
}
|
|
};
|
|
c.setForwarded(true);
|
|
|
|
} 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.setRequestHeaderSize(requestHeaderSize);
|
|
c.setAcceptors(acceptors);
|
|
c.setReuseAddress(reuseAddress);
|
|
c.setStatsOn(false);
|
|
|
|
connectors[idx] = c;
|
|
}
|
|
return connectors;
|
|
}
|
|
|
|
static boolean isReverseProxied(final URI[] listenUrls) {
|
|
for (URI u : listenUrls) {
|
|
if ("http".equals(u.getScheme()) || "https".equals(u.getScheme())) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static URI[] listenURLs(final 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 File getFile(final Config cfg, final String name, final 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) {
|
|
final QueuedThreadPool pool = new QueuedThreadPool();
|
|
pool.setName("HTTP");
|
|
pool.setMinThreads(cfg.getInt("httpd", null, "minthreads", 5));
|
|
pool.setMaxThreads(cfg.getInt("httpd", null, "maxthreads", 25));
|
|
pool.setMaxQueued(cfg.getInt("httpd", null, "maxqueued", 50));
|
|
return pool;
|
|
}
|
|
|
|
private Handler makeContext(final JettyEnv env, final Config cfg)
|
|
throws MalformedURLException, IOException {
|
|
final Set<String> paths = new HashSet<String>();
|
|
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<ContextHandler>();
|
|
for (String path : paths) {
|
|
all.add(makeContext(path, env));
|
|
}
|
|
|
|
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);
|
|
} else {
|
|
// 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,
|
|
final JettyEnv env) throws MalformedURLException, IOException {
|
|
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());
|
|
|
|
// This is the path we are accessed by clients within our domain.
|
|
//
|
|
app.setContextPath(contextPath);
|
|
|
|
// Serve static resources directly from our JAR. This way we don't
|
|
// need to unpack them into yet another temporary directory prior to
|
|
// serving to clients.
|
|
//
|
|
app.setBaseResource(getBaseResource());
|
|
|
|
// 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;
|
|
}
|
|
|
|
private Resource getBaseResource() throws IOException {
|
|
if (baseResource == null) {
|
|
try {
|
|
baseResource = unpackWar(GerritLauncher.getDistributionArchive());
|
|
} catch (FileNotFoundException err) {
|
|
if (err.getMessage() == GerritLauncher.NOT_ARCHIVED) {
|
|
baseResource = useDeveloperBuild();
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
return baseResource;
|
|
}
|
|
|
|
private Resource unpackWar(File srcwar) throws IOException {
|
|
// Obtain our local temporary directory, but it comes back as a file
|
|
// so we have to switch it to be a directory post creation.
|
|
//
|
|
File dstwar = GerritLauncher.createTempFile("gerrit_", "war");
|
|
if (!dstwar.delete() || !dstwar.mkdir()) {
|
|
throw new IOException("Cannot mkdir " + dstwar.getAbsolutePath());
|
|
}
|
|
|
|
// Jetty normally refuses to serve out of a symlinked directory, as
|
|
// a security feature. Try to resolve out any symlinks in the path.
|
|
//
|
|
try {
|
|
dstwar = dstwar.getCanonicalFile();
|
|
} catch (IOException e) {
|
|
dstwar = dstwar.getAbsoluteFile();
|
|
}
|
|
|
|
final ZipFile zf = new ZipFile(srcwar);
|
|
try {
|
|
final Enumeration<? extends ZipEntry> e = zf.entries();
|
|
while (e.hasMoreElements()) {
|
|
final ZipEntry ze = e.nextElement();
|
|
final String name = ze.getName();
|
|
|
|
if (ze.isDirectory()) continue;
|
|
if (name.startsWith("WEB-INF/")) continue;
|
|
if (name.startsWith("META-INF/")) continue;
|
|
if (name.startsWith("com/google/gerrit/launcher/")) continue;
|
|
if (name.equals("Main.class")) continue;
|
|
|
|
final File rawtmp = new File(dstwar, name);
|
|
mkdir(rawtmp.getParentFile());
|
|
rawtmp.deleteOnExit();
|
|
|
|
final FileOutputStream rawout = new FileOutputStream(rawtmp);
|
|
try {
|
|
final InputStream in = zf.getInputStream(ze);
|
|
try {
|
|
final byte[] buf = new byte[4096];
|
|
int n;
|
|
while ((n = in.read(buf, 0, buf.length)) > 0) {
|
|
rawout.write(buf, 0, n);
|
|
}
|
|
} finally {
|
|
in.close();
|
|
}
|
|
} finally {
|
|
rawout.close();
|
|
}
|
|
}
|
|
} finally {
|
|
zf.close();
|
|
}
|
|
|
|
return Resource.newResource(dstwar.toURI());
|
|
}
|
|
|
|
private void mkdir(final File dir) throws IOException {
|
|
if (!dir.isDirectory()) {
|
|
mkdir(dir.getParentFile());
|
|
if (!dir.mkdir())
|
|
throw new IOException("Cannot mkdir " + dir.getAbsolutePath());
|
|
dir.deleteOnExit();
|
|
}
|
|
}
|
|
|
|
private Resource useDeveloperBuild() throws IOException {
|
|
// Find ourselves in the CLASSPATH. We should be a loose class file.
|
|
//
|
|
URL u = getClass().getResource(getClass().getSimpleName() + ".class");
|
|
if (u == null) {
|
|
throw new FileNotFoundException("Cannot find web application root");
|
|
}
|
|
if (!"file".equals(u.getProtocol())) {
|
|
throw new FileNotFoundException("Cannot find web root from " + u);
|
|
}
|
|
|
|
// Pop up to the top level classes folder that contains us.
|
|
//
|
|
File dir = new File(u.getPath());
|
|
String myName = getClass().getName();
|
|
for (;;) {
|
|
int dot = myName.lastIndexOf('.');
|
|
if (dot < 0) {
|
|
dir = dir.getParentFile();
|
|
break;
|
|
}
|
|
myName = myName.substring(0, dot);
|
|
dir = dir.getParentFile();
|
|
}
|
|
|
|
if (!dir.getName().equals("classes")) {
|
|
throw new FileNotFoundException("Cannot find web root from " + u);
|
|
}
|
|
dir = dir.getParentFile(); // pop classes
|
|
|
|
if ("buck-out".equals(dir.getName())) {
|
|
String pkg = "gerrit-gwtui";
|
|
String target = targetForBrowser(System.getProperty("gerrit.browser"));
|
|
File gen = new File(dir, "gen");
|
|
String out = new File(new File(gen, pkg), target).getAbsolutePath();
|
|
build(dir.getParentFile(), out + ".rebuild", "//" + pkg + ":" + target);
|
|
return unpackWar(new File(out + ".zip"));
|
|
} else if ("target".equals(dir.getName())) {
|
|
return useMavenDeveloperBuild(dir);
|
|
} else {
|
|
throw new FileNotFoundException("Cannot find web root from " + u);
|
|
}
|
|
}
|
|
|
|
private static String targetForBrowser(String browser) {
|
|
if (browser == null || browser.isEmpty()) {
|
|
return "ui_dbg";
|
|
} else if (browser.startsWith("ui_")) {
|
|
return browser;
|
|
} else {
|
|
return "ui_" + browser;
|
|
}
|
|
}
|
|
|
|
private static void build(File root, String cmd, String target)
|
|
throws IOException {
|
|
long start = System.currentTimeMillis();
|
|
log.info("buck build " + target);
|
|
Process rebuild = new ProcessBuilder(cmd, target)
|
|
.directory(root)
|
|
.redirectErrorStream(true)
|
|
.start();
|
|
|
|
byte[] out;
|
|
InputStream in = rebuild.getInputStream();
|
|
try {
|
|
out = ByteStreams.toByteArray(in);
|
|
} finally {
|
|
rebuild.getOutputStream().close();
|
|
in.close();
|
|
}
|
|
|
|
int status;
|
|
try {
|
|
status = rebuild.waitFor();
|
|
} catch (InterruptedException e) {
|
|
throw new InterruptedIOException("interrupted waiting for " + cmd);
|
|
}
|
|
if (status != 0) {
|
|
System.err.write(out);
|
|
System.err.println();
|
|
System.exit(status);
|
|
}
|
|
|
|
long time = System.currentTimeMillis() - start;
|
|
log.info(String.format("UPDATED %s in %.3fs", target, time / 1000.0));
|
|
}
|
|
|
|
private Resource useMavenDeveloperBuild(File dir) throws IOException {
|
|
dir = dir.getParentFile(); // pop target
|
|
dir = dir.getParentFile(); // pop the module we are in
|
|
|
|
// Drop down into gerrit-gwtui to find the WAR assets we need.
|
|
//
|
|
dir = new File(new File(dir, "gerrit-gwtui"), "target");
|
|
final File[] entries = dir.listFiles();
|
|
if (entries == null) {
|
|
throw new FileNotFoundException("No " + dir);
|
|
}
|
|
for (File e : entries) {
|
|
if (e.isDirectory() /* must be a directory */
|
|
&& e.getName().startsWith("gerrit-gwtui-")
|
|
&& new File(e, "gerrit_ui/gerrit_ui.nocache.js").isFile()) {
|
|
return Resource.newResource(e.toURI());
|
|
}
|
|
}
|
|
throw new FileNotFoundException("No " + dir + "/gerrit-gwtui-*");
|
|
}
|
|
}
|