Merge changes from topic 'resource-servlet'
* changes: Serve GWT UI from ResourceServlet ResourceServlet: Stream large files, bypassing the cache ResourceServlet: Respect existing cache headers Rename StaticServlet to SiteStaticDirectoryServlet Refactor static content serving
This commit is contained in:
@@ -128,6 +128,12 @@ public class CacheHeaders {
|
|||||||
cache(res, "private", age, unit, mustRevalidate);
|
cache(res, "private", age, unit, mustRevalidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean hasCacheHeader(HttpServletResponse res) {
|
||||||
|
return res.getHeader("Cache-Control") != null
|
||||||
|
|| res.getHeader("Expires") != null
|
||||||
|
|| "no-cache".equals(res.getHeader("Pragma"));
|
||||||
|
}
|
||||||
|
|
||||||
private static void cache(HttpServletResponse res,
|
private static void cache(HttpServletResponse res,
|
||||||
String type, long age, TimeUnit unit, boolean revalidate) {
|
String type, long age, TimeUnit unit, boolean revalidate) {
|
||||||
res.setHeader("Cache-Control", String.format(
|
res.setHeader("Cache-Control", String.format(
|
||||||
|
@@ -12,7 +12,9 @@ java_library(
|
|||||||
'//gerrit-common:annotations',
|
'//gerrit-common:annotations',
|
||||||
'//gerrit-common:server',
|
'//gerrit-common:server',
|
||||||
'//gerrit-extension-api:api',
|
'//gerrit-extension-api:api',
|
||||||
|
'//gerrit-gwtexpui:linker_server',
|
||||||
'//gerrit-gwtexpui:server',
|
'//gerrit-gwtexpui:server',
|
||||||
|
'//gerrit-launcher:launcher',
|
||||||
'//gerrit-patch-jgit:server',
|
'//gerrit-patch-jgit:server',
|
||||||
'//gerrit-prettify:server',
|
'//gerrit-prettify:server',
|
||||||
'//gerrit-reviewdb:server',
|
'//gerrit-reviewdb:server',
|
||||||
|
@@ -23,7 +23,7 @@ import com.google.gerrit.httpd.raw.HostPageServlet;
|
|||||||
import com.google.gerrit.httpd.raw.LegacyGerritServlet;
|
import com.google.gerrit.httpd.raw.LegacyGerritServlet;
|
||||||
import com.google.gerrit.httpd.raw.RobotsServlet;
|
import com.google.gerrit.httpd.raw.RobotsServlet;
|
||||||
import com.google.gerrit.httpd.raw.SshInfoServlet;
|
import com.google.gerrit.httpd.raw.SshInfoServlet;
|
||||||
import com.google.gerrit.httpd.raw.StaticServlet;
|
import com.google.gerrit.httpd.raw.StaticModule;
|
||||||
import com.google.gerrit.httpd.raw.ToolServlet;
|
import com.google.gerrit.httpd.raw.ToolServlet;
|
||||||
import com.google.gerrit.httpd.rpc.access.AccessRestApiServlet;
|
import com.google.gerrit.httpd.rpc.access.AccessRestApiServlet;
|
||||||
import com.google.gerrit.httpd.rpc.account.AccountsRestApiServlet;
|
import com.google.gerrit.httpd.rpc.account.AccountsRestApiServlet;
|
||||||
@@ -77,7 +77,6 @@ class UrlModule extends ServletModule {
|
|||||||
serve("/signout").with(HttpLogoutServlet.class);
|
serve("/signout").with(HttpLogoutServlet.class);
|
||||||
}
|
}
|
||||||
serve("/ssh_info").with(SshInfoServlet.class);
|
serve("/ssh_info").with(SshInfoServlet.class);
|
||||||
serve("/static/*").with(StaticServlet.class);
|
|
||||||
|
|
||||||
serve("/Main.class").with(notFound());
|
serve("/Main.class").with(notFound());
|
||||||
serve("/com/google/gerrit/launcher/*").with(notFound());
|
serve("/com/google/gerrit/launcher/*").with(notFound());
|
||||||
@@ -107,6 +106,8 @@ class UrlModule extends ServletModule {
|
|||||||
filter("/Documentation/").through(QueryDocumentationFilter.class);
|
filter("/Documentation/").through(QueryDocumentationFilter.class);
|
||||||
|
|
||||||
serve("/robots.txt").with(RobotsServlet.class);
|
serve("/robots.txt").with(RobotsServlet.class);
|
||||||
|
|
||||||
|
install(new StaticModule());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Key<HttpServlet> notFound() {
|
private Key<HttpServlet> notFound() {
|
||||||
|
@@ -0,0 +1,51 @@
|
|||||||
|
// Copyright (C) 2015 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.raw;
|
||||||
|
|
||||||
|
import com.google.common.cache.Cache;
|
||||||
|
import com.google.gerrit.common.TimeUtil;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.attribute.FileTime;
|
||||||
|
|
||||||
|
class DeveloperGwtUiServlet extends ResourceServlet {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private static final FileTime NOW = FileTime.fromMillis(TimeUtil.nowMs());
|
||||||
|
|
||||||
|
private final Path ui;
|
||||||
|
|
||||||
|
DeveloperGwtUiServlet(Cache<Path, Resource> cache, Path unpackedWar)
|
||||||
|
throws IOException {
|
||||||
|
super(cache, false);
|
||||||
|
ui = unpackedWar.resolve("gerrit_ui");
|
||||||
|
Files.createDirectory(ui);
|
||||||
|
ui.toFile().deleteOnExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Path getResourcePath(String pathInfo) {
|
||||||
|
return ui.resolve(pathInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected FileTime getLastModifiedTime(Path p) {
|
||||||
|
// Return initialization time of this class, since the GWT outputs from the
|
||||||
|
// build process all have mtimes of 1980/1/1.
|
||||||
|
return NOW;
|
||||||
|
}
|
||||||
|
}
|
@@ -92,7 +92,7 @@ public class HostPageServlet extends HttpServlet {
|
|||||||
private final Document template;
|
private final Document template;
|
||||||
private final String noCacheName;
|
private final String noCacheName;
|
||||||
private final boolean refreshHeaderFooter;
|
private final boolean refreshHeaderFooter;
|
||||||
private final StaticServlet staticServlet;
|
private final SiteStaticDirectoryServlet staticServlet;
|
||||||
private final boolean isNoteDbEnabled;
|
private final boolean isNoteDbEnabled;
|
||||||
private final Integer pluginsLoadTimeout;
|
private final Integer pluginsLoadTimeout;
|
||||||
private final GetDiffPreferences getDiff;
|
private final GetDiffPreferences getDiff;
|
||||||
@@ -108,7 +108,7 @@ public class HostPageServlet extends HttpServlet {
|
|||||||
DynamicSet<WebUiPlugin> webUiPlugins,
|
DynamicSet<WebUiPlugin> webUiPlugins,
|
||||||
DynamicSet<MessageOfTheDay> motd,
|
DynamicSet<MessageOfTheDay> motd,
|
||||||
@GerritServerConfig Config cfg,
|
@GerritServerConfig Config cfg,
|
||||||
StaticServlet ss,
|
SiteStaticDirectoryServlet ss,
|
||||||
NotesMigration migration,
|
NotesMigration migration,
|
||||||
GetDiffPreferences diffPref)
|
GetDiffPreferences diffPref)
|
||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
@@ -302,7 +302,7 @@ public class HostPageServlet extends HttpServlet {
|
|||||||
String src = e.getAttribute("src");
|
String src = e.getAttribute("src");
|
||||||
if (src != null && src.startsWith("static/")) {
|
if (src != null && src.startsWith("static/")) {
|
||||||
String name = src.substring("static/".length());
|
String name = src.substring("static/".length());
|
||||||
StaticServlet.Resource r = staticServlet.getResource(name);
|
ResourceServlet.Resource r = staticServlet.getResource(name);
|
||||||
if (r != null) {
|
if (r != null) {
|
||||||
e.setAttribute("src", src + "?e=" + r.etag);
|
e.setAttribute("src", src + "?e=" + r.etag);
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,231 @@
|
|||||||
|
// Copyright (C) 2015 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.raw;
|
||||||
|
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
import com.google.common.base.MoreObjects;
|
||||||
|
import com.google.common.escape.Escaper;
|
||||||
|
import com.google.common.html.HtmlEscapers;
|
||||||
|
import com.google.common.io.ByteStreams;
|
||||||
|
import com.google.gerrit.common.TimeUtil;
|
||||||
|
import com.google.gwtexpui.linker.server.UserAgentRule;
|
||||||
|
import com.google.gwtexpui.server.CacheHeaders;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.util.RawParseUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InterruptedIOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
|
import javax.servlet.Filter;
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.FilterConfig;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.ServletRequest;
|
||||||
|
import javax.servlet.ServletResponse;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
class RecompileGwtUiFilter implements Filter {
|
||||||
|
private static final Logger log =
|
||||||
|
LoggerFactory.getLogger(RecompileGwtUiFilter.class);
|
||||||
|
|
||||||
|
private final boolean gwtuiRecompile =
|
||||||
|
System.getProperty("gerrit.disable-gwtui-recompile") == null;
|
||||||
|
private final UserAgentRule rule = new UserAgentRule();
|
||||||
|
private final Set<String> uaInitialized = new HashSet<>();
|
||||||
|
private final Path unpackedWar;
|
||||||
|
private final Path gen;
|
||||||
|
private final Path root;
|
||||||
|
|
||||||
|
private String lastTarget;
|
||||||
|
private long lastTime;
|
||||||
|
|
||||||
|
RecompileGwtUiFilter(Path buckOut, Path unpackedWar) {
|
||||||
|
this.unpackedWar = unpackedWar;
|
||||||
|
gen = buckOut.resolve("gen");
|
||||||
|
root = buckOut.getParent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFilter(ServletRequest request, ServletResponse res,
|
||||||
|
FilterChain chain) throws IOException, ServletException {
|
||||||
|
String pkg = "gerrit-gwtui";
|
||||||
|
String target = "ui_" + rule.select((HttpServletRequest) request);
|
||||||
|
if (gwtuiRecompile || !uaInitialized.contains(target)) {
|
||||||
|
String rule = "//" + pkg + ":" + target;
|
||||||
|
// TODO(davido): instead of assuming specific Buck's internal
|
||||||
|
// target directory for gwt_binary() artifacts, ask Buck for
|
||||||
|
// the location of user agent permutation GWT zip, e. g.:
|
||||||
|
// $ buck targets --show_output //gerrit-gwtui:ui_safari \
|
||||||
|
// | awk '{print $2}'
|
||||||
|
String child = String.format("%s/__gwt_binary_%s__", pkg, target);
|
||||||
|
File zip = gen.resolve(child).resolve(target + ".zip").toFile();
|
||||||
|
|
||||||
|
synchronized (this) {
|
||||||
|
try {
|
||||||
|
build(root, gen, rule);
|
||||||
|
} catch (BuildFailureException e) {
|
||||||
|
displayFailure(rule, e.why, (HttpServletResponse) res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target.equals(lastTarget) || lastTime != zip.lastModified()) {
|
||||||
|
lastTarget = target;
|
||||||
|
lastTime = zip.lastModified();
|
||||||
|
unpack(zip, unpackedWar.toFile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uaInitialized.add(target);
|
||||||
|
}
|
||||||
|
chain.doFilter(request, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void displayFailure(String rule, byte[] why, HttpServletResponse res)
|
||||||
|
throws IOException {
|
||||||
|
res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||||
|
res.setContentType("text/html");
|
||||||
|
res.setCharacterEncoding(UTF_8.name());
|
||||||
|
CacheHeaders.setNotCacheable(res);
|
||||||
|
|
||||||
|
Escaper html = HtmlEscapers.htmlEscaper();
|
||||||
|
try (PrintWriter w = res.getWriter()) {
|
||||||
|
w.write("<html><title>BUILD FAILED</title><body>");
|
||||||
|
w.format("<h1>%s FAILED</h1>", html.escape(rule));
|
||||||
|
w.write("<pre>");
|
||||||
|
w.write(html.escape(RawParseUtils.decode(why)));
|
||||||
|
w.write("</pre>");
|
||||||
|
w.write("</body></html>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(FilterConfig config) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void unpack(File srcwar, File dstwar) throws IOException {
|
||||||
|
try (ZipFile zf = new ZipFile(srcwar)) {
|
||||||
|
final Enumeration<? extends ZipEntry> e = zf.entries();
|
||||||
|
while (e.hasMoreElements()) {
|
||||||
|
final ZipEntry ze = e.nextElement();
|
||||||
|
final String name = ze.getName();
|
||||||
|
|
||||||
|
if (ze.isDirectory()
|
||||||
|
|| name.startsWith("WEB-INF/")
|
||||||
|
|| name.startsWith("META-INF/")
|
||||||
|
|| name.startsWith("com/google/gerrit/launcher/")
|
||||||
|
|| name.equals("Main.class")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final File rawtmp = new File(dstwar, name);
|
||||||
|
mkdir(rawtmp.getParentFile());
|
||||||
|
rawtmp.deleteOnExit();
|
||||||
|
|
||||||
|
try (FileOutputStream rawout = new FileOutputStream(rawtmp);
|
||||||
|
InputStream in = zf.getInputStream(ze)) {
|
||||||
|
final byte[] buf = new byte[4096];
|
||||||
|
int n;
|
||||||
|
while ((n = in.read(buf, 0, buf.length)) > 0) {
|
||||||
|
rawout.write(buf, 0, n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void build(Path root, Path gen, String target)
|
||||||
|
throws IOException, BuildFailureException {
|
||||||
|
log.info("buck build " + target);
|
||||||
|
Properties properties = loadBuckProperties(gen);
|
||||||
|
String buck = MoreObjects.firstNonNull(properties.getProperty("buck"), "buck");
|
||||||
|
ProcessBuilder proc = new ProcessBuilder(buck, "build", target)
|
||||||
|
.directory(root.toFile())
|
||||||
|
.redirectErrorStream(true);
|
||||||
|
if (properties.containsKey("PATH")) {
|
||||||
|
proc.environment().put("PATH", properties.getProperty("PATH"));
|
||||||
|
}
|
||||||
|
long start = TimeUtil.nowMs();
|
||||||
|
Process rebuild = proc.start();
|
||||||
|
byte[] out;
|
||||||
|
try (InputStream in = rebuild.getInputStream()) {
|
||||||
|
out = ByteStreams.toByteArray(in);
|
||||||
|
} finally {
|
||||||
|
rebuild.getOutputStream().close();
|
||||||
|
}
|
||||||
|
|
||||||
|
int status;
|
||||||
|
try {
|
||||||
|
status = rebuild.waitFor();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new InterruptedIOException("interrupted waiting for " + buck);
|
||||||
|
}
|
||||||
|
if (status != 0) {
|
||||||
|
throw new BuildFailureException(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
long time = TimeUtil.nowMs() - start;
|
||||||
|
log.info(String.format("UPDATED %s in %.3fs", target, time / 1000.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Properties loadBuckProperties(Path gen)
|
||||||
|
throws FileNotFoundException, IOException {
|
||||||
|
Properties properties = new Properties();
|
||||||
|
try (InputStream in = new FileInputStream(
|
||||||
|
gen.resolve(Paths.get("tools/buck/buck.properties")).toFile())) {
|
||||||
|
properties.load(in);
|
||||||
|
}
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
private static class BuildFailureException extends Exception {
|
||||||
|
final byte[] why;
|
||||||
|
|
||||||
|
BuildFailureException(byte[] why) {
|
||||||
|
this.why = why;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void mkdir(File dir) throws IOException {
|
||||||
|
if (!dir.isDirectory()) {
|
||||||
|
mkdir(dir.getParentFile());
|
||||||
|
if (!dir.mkdir()) {
|
||||||
|
throw new IOException("Cannot mkdir " + dir.getAbsolutePath());
|
||||||
|
}
|
||||||
|
dir.deleteOnExit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,314 @@
|
|||||||
|
// Copyright (C) 2015 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.raw;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
import static com.google.common.net.HttpHeaders.CONTENT_ENCODING;
|
||||||
|
import static com.google.common.net.HttpHeaders.ETAG;
|
||||||
|
import static com.google.common.net.HttpHeaders.IF_MODIFIED_SINCE;
|
||||||
|
import static com.google.common.net.HttpHeaders.IF_NONE_MATCH;
|
||||||
|
import static com.google.common.net.HttpHeaders.LAST_MODIFIED;
|
||||||
|
import static java.util.concurrent.TimeUnit.DAYS;
|
||||||
|
import static java.util.concurrent.TimeUnit.MINUTES;
|
||||||
|
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||||
|
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
|
||||||
|
import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
|
||||||
|
|
||||||
|
import com.google.common.base.CharMatcher;
|
||||||
|
import com.google.common.cache.Cache;
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.google.common.hash.Hashing;
|
||||||
|
import com.google.gerrit.common.FileUtil;
|
||||||
|
import com.google.gerrit.common.Nullable;
|
||||||
|
import com.google.gerrit.httpd.HtmlDomUtil;
|
||||||
|
import com.google.gwtexpui.server.CacheHeaders;
|
||||||
|
import com.google.gwtjsonrpc.server.RPCServletUtils;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.NoSuchFileException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.attribute.FileTime;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.zip.GZIPOutputStream;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServlet;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for serving static resources.
|
||||||
|
* <p>
|
||||||
|
* Supports caching, ETags, basic content type detection, and limited gzip
|
||||||
|
* compression.
|
||||||
|
*/
|
||||||
|
public abstract class ResourceServlet extends HttpServlet {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private static final Logger log =
|
||||||
|
LoggerFactory.getLogger(ResourceServlet.class);
|
||||||
|
|
||||||
|
private static final int CACHE_FILE_SIZE_LIMIT_BYTES = 100 << 10;
|
||||||
|
|
||||||
|
private static final String JS = "application/x-javascript";
|
||||||
|
private static final ImmutableMap<String, String> MIME_TYPES =
|
||||||
|
ImmutableMap.<String, String> builder()
|
||||||
|
.put("css", "text/css")
|
||||||
|
.put("gif", "image/gif")
|
||||||
|
.put("htm", "text/html")
|
||||||
|
.put("html", "text/html")
|
||||||
|
.put("jpeg", "image/jpeg")
|
||||||
|
.put("jpg", "image/jpeg")
|
||||||
|
.put("js", JS)
|
||||||
|
.put("pdf", "application/pdf")
|
||||||
|
.put("png", "image/png")
|
||||||
|
.put("rtf", "text/rtf")
|
||||||
|
.put("svg", "image/svg+xml")
|
||||||
|
.put("text", "text/plain")
|
||||||
|
.put("tif", "image/tiff")
|
||||||
|
.put("tiff", "image/tiff")
|
||||||
|
.put("txt", "text/plain")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
protected static String contentType(String name) {
|
||||||
|
int dot = name.lastIndexOf('.');
|
||||||
|
String ext = 0 < dot ? name.substring(dot + 1) : "";
|
||||||
|
String type = MIME_TYPES.get(ext);
|
||||||
|
return type != null ? type : "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Cache<Path, Resource> cache;
|
||||||
|
private final boolean refresh;
|
||||||
|
|
||||||
|
protected ResourceServlet(Cache<Path, Resource> cache, boolean refresh) {
|
||||||
|
this.cache = checkNotNull(cache, "cache");
|
||||||
|
this.refresh = refresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the resource path on the filesystem that should be served for this
|
||||||
|
* request.
|
||||||
|
*
|
||||||
|
* @param pathInfo result of {@link HttpServletRequest#getPathInfo()}.
|
||||||
|
* @return path where static content can be found.
|
||||||
|
*/
|
||||||
|
protected abstract Path getResourcePath(String pathInfo);
|
||||||
|
|
||||||
|
protected FileTime getLastModifiedTime(Path p) throws IOException {
|
||||||
|
return Files.getLastModifiedTime(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doGet(HttpServletRequest req, HttpServletResponse rsp)
|
||||||
|
throws IOException {
|
||||||
|
String name = CharMatcher.is('/').trimFrom(req.getPathInfo());
|
||||||
|
if (isUnreasonableName(name)) {
|
||||||
|
notFound(rsp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Path p = getResourcePath(name);
|
||||||
|
if (p == null) {
|
||||||
|
notFound(rsp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Resource r = cache.getIfPresent(p);
|
||||||
|
if (r == null && maybeStream(p, req, rsp)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r == null) {
|
||||||
|
Callable<Resource> loader = newLoader(p);
|
||||||
|
try {
|
||||||
|
r = cache.get(p, loader);
|
||||||
|
if (refresh && r.isStale(p, this)) {
|
||||||
|
cache.invalidate(p);
|
||||||
|
r = cache.get(p, loader);
|
||||||
|
}
|
||||||
|
} catch (ExecutionException | IOException e) {
|
||||||
|
log.warn("Cannot load static resource " + req.getPathInfo(), e);
|
||||||
|
CacheHeaders.setNotCacheable(rsp);
|
||||||
|
rsp.setStatus(SC_INTERNAL_SERVER_ERROR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r == Resource.NOT_FOUND) {
|
||||||
|
notFound(rsp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String e = req.getParameter("e");
|
||||||
|
if (e != null && !r.etag.equals(e)) {
|
||||||
|
CacheHeaders.setNotCacheable(rsp);
|
||||||
|
rsp.setStatus(SC_NOT_FOUND);
|
||||||
|
return;
|
||||||
|
} else if (r.etag.equals(req.getHeader(IF_NONE_MATCH))) {
|
||||||
|
rsp.setStatus(SC_NOT_MODIFIED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] tosend = r.raw;
|
||||||
|
if (!r.contentType.equals(JS) && RPCServletUtils.acceptsGzipEncoding(req)) {
|
||||||
|
byte[] gz = HtmlDomUtil.compress(tosend);
|
||||||
|
if ((gz.length + 24) < tosend.length) {
|
||||||
|
rsp.setHeader(CONTENT_ENCODING, "gzip");
|
||||||
|
tosend = gz;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!CacheHeaders.hasCacheHeader(rsp)) {
|
||||||
|
if (e != null && r.etag.equals(e)) {
|
||||||
|
CacheHeaders.setCacheable(req, rsp, 360, DAYS, false);
|
||||||
|
} else {
|
||||||
|
CacheHeaders.setCacheable(req, rsp, 15, MINUTES, refresh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rsp.setHeader(ETAG, r.etag);
|
||||||
|
rsp.setContentType(r.contentType);
|
||||||
|
rsp.setContentLength(tosend.length);
|
||||||
|
try (OutputStream out = rsp.getOutputStream()) {
|
||||||
|
out.write(tosend);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
Resource getResource(String name) {
|
||||||
|
try {
|
||||||
|
Path p = getResourcePath(name);
|
||||||
|
return cache.get(p, newLoader(p));
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
log.warn(String.format("Cannot load static resource %s", name), e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void notFound(HttpServletResponse rsp) {
|
||||||
|
rsp.setStatus(SC_NOT_FOUND);
|
||||||
|
CacheHeaders.setNotCacheable(rsp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maybe stream a path to the response, depending on the properties of the
|
||||||
|
* file and cache headers in the request.
|
||||||
|
*
|
||||||
|
* @param p path to stream
|
||||||
|
* @param req HTTP request.
|
||||||
|
* @param rsp HTTP response.
|
||||||
|
* @return true if the response was written (either the file contents or an
|
||||||
|
* error); false if the path is too small to stream and should be cached.
|
||||||
|
*/
|
||||||
|
private boolean maybeStream(Path p, HttpServletRequest req,
|
||||||
|
HttpServletResponse rsp) throws IOException {
|
||||||
|
try {
|
||||||
|
if (Files.size(p) < CACHE_FILE_SIZE_LIMIT_BYTES) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (NoSuchFileException e) {
|
||||||
|
cache.put(p, Resource.NOT_FOUND);
|
||||||
|
notFound(rsp);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
long lastModified = FileUtil.lastModified(p);
|
||||||
|
if (req.getDateHeader(IF_MODIFIED_SINCE) >= lastModified) {
|
||||||
|
rsp.setStatus(SC_NOT_MODIFIED);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastModified > 0) {
|
||||||
|
rsp.setDateHeader(LAST_MODIFIED, lastModified);
|
||||||
|
}
|
||||||
|
if (!CacheHeaders.hasCacheHeader(rsp)) {
|
||||||
|
CacheHeaders.setCacheable(req, rsp, 15, MINUTES, refresh);
|
||||||
|
}
|
||||||
|
rsp.setContentType(contentType(p.toString()));
|
||||||
|
|
||||||
|
OutputStream out = rsp.getOutputStream();
|
||||||
|
GZIPOutputStream gz = null;
|
||||||
|
if (RPCServletUtils.acceptsGzipEncoding(req)) {
|
||||||
|
rsp.setHeader(CONTENT_ENCODING, "gzip");
|
||||||
|
gz = new GZIPOutputStream(out);
|
||||||
|
out = gz;
|
||||||
|
}
|
||||||
|
Files.copy(p, out);
|
||||||
|
if (gz != null) {
|
||||||
|
gz.finish();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static boolean isUnreasonableName(String name) {
|
||||||
|
return name.length() < 1
|
||||||
|
|| name.contains("\\") // no windows/dos style paths
|
||||||
|
|| name.startsWith("../") // no "../etc/passwd"
|
||||||
|
|| name.contains("/../") // no "foo/../etc/passwd"
|
||||||
|
|| name.contains("/./") // "foo/./foo" is insane to ask
|
||||||
|
|| name.contains("//"); // windows UNC path can be "//..."
|
||||||
|
}
|
||||||
|
|
||||||
|
private Callable<Resource> newLoader(final Path p) {
|
||||||
|
return new Callable<Resource>() {
|
||||||
|
@Override
|
||||||
|
public Resource call() throws IOException {
|
||||||
|
try {
|
||||||
|
return new Resource(
|
||||||
|
getLastModifiedTime(p),
|
||||||
|
contentType(p.toString()),
|
||||||
|
Files.readAllBytes(p));
|
||||||
|
} catch (NoSuchFileException e) {
|
||||||
|
return Resource.NOT_FOUND;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Resource {
|
||||||
|
static final Resource NOT_FOUND =
|
||||||
|
new Resource(FileTime.fromMillis(0), "", new byte[] {});
|
||||||
|
|
||||||
|
final FileTime lastModified;
|
||||||
|
final String contentType;
|
||||||
|
final String etag;
|
||||||
|
final byte[] raw;
|
||||||
|
|
||||||
|
Resource(FileTime lastModified, String contentType, byte[] raw) {
|
||||||
|
this.lastModified = checkNotNull(lastModified, "lastModified");
|
||||||
|
this.contentType = checkNotNull(contentType, "contentType");
|
||||||
|
this.raw = checkNotNull(raw, "raw");
|
||||||
|
this.etag = Hashing.md5().hashBytes(raw).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isStale(Path p, ResourceServlet rs) throws IOException {
|
||||||
|
FileTime t = rs.getLastModifiedTime(p);
|
||||||
|
return t.toMillis() == 0
|
||||||
|
|| lastModified.toMillis() == 0
|
||||||
|
|| !lastModified.equals(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Weigher
|
||||||
|
implements com.google.common.cache.Weigher<Path, Resource> {
|
||||||
|
@Override
|
||||||
|
public int weigh(Path p, Resource r) {
|
||||||
|
return 2 * p.toString().length() + r.raw.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright (C) 2008 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.raw;
|
||||||
|
|
||||||
|
import com.google.common.cache.Cache;
|
||||||
|
import com.google.gerrit.server.config.GerritServerConfig;
|
||||||
|
import com.google.gerrit.server.config.SitePaths;
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import com.google.inject.name.Named;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.lib.Config;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
|
||||||
|
/** Sends static content from the site 's {@code static/} subdirectory. */
|
||||||
|
@Singleton
|
||||||
|
public class SiteStaticDirectoryServlet extends ResourceServlet {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final Path staticBase;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SiteStaticDirectoryServlet(
|
||||||
|
SitePaths site,
|
||||||
|
@GerritServerConfig Config cfg,
|
||||||
|
@Named(StaticModule.CACHE) Cache<Path, Resource> cache) {
|
||||||
|
super(cache, cfg.getBoolean("site", "refreshHeaderFooter", true));
|
||||||
|
Path p;
|
||||||
|
try {
|
||||||
|
p = site.static_dir.toRealPath().normalize();
|
||||||
|
} catch (IOException e) {
|
||||||
|
p = site.static_dir.toAbsolutePath().normalize();
|
||||||
|
}
|
||||||
|
staticBase = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Path getResourcePath(String pathInfo) {
|
||||||
|
Path p = staticBase.resolve(pathInfo);
|
||||||
|
try {
|
||||||
|
p = p.toRealPath().normalize();
|
||||||
|
if (!p.startsWith(staticBase)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,139 @@
|
|||||||
|
// Copyright (C) 2015 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.raw;
|
||||||
|
|
||||||
|
import com.google.common.cache.Cache;
|
||||||
|
import com.google.gerrit.httpd.raw.ResourceServlet.Resource;
|
||||||
|
import com.google.gerrit.launcher.GerritLauncher;
|
||||||
|
import com.google.gerrit.server.cache.CacheModule;
|
||||||
|
import com.google.inject.Key;
|
||||||
|
import com.google.inject.Provides;
|
||||||
|
import com.google.inject.ProvisionException;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import com.google.inject.name.Named;
|
||||||
|
import com.google.inject.name.Names;
|
||||||
|
import com.google.inject.servlet.ServletModule;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.FileSystem;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServlet;
|
||||||
|
|
||||||
|
public class StaticModule extends ServletModule {
|
||||||
|
private static final String GWT_UI_SERVLET = "GwtUiServlet";
|
||||||
|
static final String CACHE = "static_content";
|
||||||
|
|
||||||
|
private final FileSystem warFs;
|
||||||
|
private final Path buckOut;
|
||||||
|
private final Path unpackedWar;
|
||||||
|
|
||||||
|
public StaticModule() {
|
||||||
|
warFs = getDistributionArchive();
|
||||||
|
if (warFs == null) {
|
||||||
|
buckOut = getDeveloperBuckOut();
|
||||||
|
unpackedWar = makeWarTempDir();
|
||||||
|
} else {
|
||||||
|
buckOut = null;
|
||||||
|
unpackedWar = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void configureServlets() {
|
||||||
|
serve("/static/*").with(SiteStaticDirectoryServlet.class);
|
||||||
|
serveGwtUi();
|
||||||
|
install(new CacheModule() {
|
||||||
|
@Override
|
||||||
|
protected void configure() {
|
||||||
|
cache(CACHE, Path.class, Resource.class)
|
||||||
|
.maximumWeight(1 << 20)
|
||||||
|
.weigher(ResourceServlet.Weigher.class);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void serveGwtUi() {
|
||||||
|
serve("/gerrit_ui/*")
|
||||||
|
.with(Key.get(HttpServlet.class, Names.named(GWT_UI_SERVLET)));
|
||||||
|
if (warFs == null) {
|
||||||
|
filter("/").through(new RecompileGwtUiFilter(buckOut, unpackedWar));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
@Named(GWT_UI_SERVLET)
|
||||||
|
HttpServlet getGwtUiServlet(@Named(CACHE) Cache<Path, Resource> cache)
|
||||||
|
throws IOException {
|
||||||
|
if (warFs != null) {
|
||||||
|
return new WarGwtUiServlet(cache, warFs);
|
||||||
|
} else {
|
||||||
|
return new DeveloperGwtUiServlet(cache, unpackedWar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FileSystem getDistributionArchive() {
|
||||||
|
try {
|
||||||
|
return GerritLauncher.getDistributionArchiveFileSystem();
|
||||||
|
} catch (IOException e) {
|
||||||
|
if ((e instanceof FileNotFoundException)
|
||||||
|
&& GerritLauncher.NOT_ARCHIVED.equals(e.getMessage())) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
ProvisionException pe =
|
||||||
|
new ProvisionException("Error reading gerrit.war");
|
||||||
|
pe.initCause(e);
|
||||||
|
throw pe;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path getDeveloperBuckOut() {
|
||||||
|
try {
|
||||||
|
return GerritLauncher.getDeveloperBuckOut();
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path makeWarTempDir() {
|
||||||
|
// Obtain our local temporary directory, but it comes back as a file
|
||||||
|
// so we have to switch it to be a directory post creation.
|
||||||
|
//
|
||||||
|
try {
|
||||||
|
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 {
|
||||||
|
return dstwar.getCanonicalFile().toPath();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return dstwar.getAbsoluteFile().toPath();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
ProvisionException pe =
|
||||||
|
new ProvisionException("Cannot create war tempdir");
|
||||||
|
pe.initCause(e);
|
||||||
|
throw pe;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,249 +0,0 @@
|
|||||||
// Copyright (C) 2008 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.raw;
|
|
||||||
|
|
||||||
import static com.google.common.net.HttpHeaders.CONTENT_ENCODING;
|
|
||||||
import static com.google.common.net.HttpHeaders.ETAG;
|
|
||||||
import static com.google.common.net.HttpHeaders.IF_NONE_MATCH;
|
|
||||||
import static com.google.gerrit.common.FileUtil.lastModified;
|
|
||||||
import static java.util.concurrent.TimeUnit.DAYS;
|
|
||||||
import static java.util.concurrent.TimeUnit.MINUTES;
|
|
||||||
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
|
||||||
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
|
|
||||||
import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
|
|
||||||
|
|
||||||
import com.google.common.base.CharMatcher;
|
|
||||||
import com.google.common.cache.CacheBuilder;
|
|
||||||
import com.google.common.cache.CacheLoader;
|
|
||||||
import com.google.common.cache.LoadingCache;
|
|
||||||
import com.google.common.cache.Weigher;
|
|
||||||
import com.google.common.collect.Maps;
|
|
||||||
import com.google.common.hash.Hashing;
|
|
||||||
import com.google.gerrit.common.FileUtil;
|
|
||||||
import com.google.gerrit.common.Nullable;
|
|
||||||
import com.google.gerrit.httpd.HtmlDomUtil;
|
|
||||||
import com.google.gerrit.server.config.GerritServerConfig;
|
|
||||||
import com.google.gerrit.server.config.SitePaths;
|
|
||||||
import com.google.gwtexpui.server.CacheHeaders;
|
|
||||||
import com.google.gwtjsonrpc.server.RPCServletUtils;
|
|
||||||
import com.google.inject.Inject;
|
|
||||||
import com.google.inject.Singleton;
|
|
||||||
|
|
||||||
import org.eclipse.jgit.lib.Config;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.NoSuchFileException;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
|
|
||||||
import javax.servlet.http.HttpServlet;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
|
|
||||||
/** Sends static content from the site 's {@code static/} subdirectory. */
|
|
||||||
@SuppressWarnings("serial")
|
|
||||||
@Singleton
|
|
||||||
public class StaticServlet extends HttpServlet {
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(StaticServlet.class);
|
|
||||||
private static final String JS = "application/x-javascript";
|
|
||||||
private static final Map<String, String> MIME_TYPES = Maps.newHashMap();
|
|
||||||
static {
|
|
||||||
MIME_TYPES.put("html", "text/html");
|
|
||||||
MIME_TYPES.put("htm", "text/html");
|
|
||||||
MIME_TYPES.put("js", JS);
|
|
||||||
MIME_TYPES.put("css", "text/css");
|
|
||||||
MIME_TYPES.put("rtf", "text/rtf");
|
|
||||||
MIME_TYPES.put("txt", "text/plain");
|
|
||||||
MIME_TYPES.put("text", "text/plain");
|
|
||||||
MIME_TYPES.put("pdf", "application/pdf");
|
|
||||||
MIME_TYPES.put("jpeg", "image/jpeg");
|
|
||||||
MIME_TYPES.put("jpg", "image/jpeg");
|
|
||||||
MIME_TYPES.put("gif", "image/gif");
|
|
||||||
MIME_TYPES.put("png", "image/png");
|
|
||||||
MIME_TYPES.put("tiff", "image/tiff");
|
|
||||||
MIME_TYPES.put("tif", "image/tiff");
|
|
||||||
MIME_TYPES.put("svg", "image/svg+xml");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String contentType(final String name) {
|
|
||||||
final int dot = name.lastIndexOf('.');
|
|
||||||
final String ext = 0 < dot ? name.substring(dot + 1) : "";
|
|
||||||
final String type = MIME_TYPES.get(ext);
|
|
||||||
return type != null ? type : "application/octet-stream";
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Path staticBase;
|
|
||||||
private final boolean refresh;
|
|
||||||
private final LoadingCache<String, Resource> cache;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
StaticServlet(@GerritServerConfig Config cfg, SitePaths site) {
|
|
||||||
Path p;
|
|
||||||
try {
|
|
||||||
p = site.static_dir.toRealPath().normalize();
|
|
||||||
} catch (IOException e) {
|
|
||||||
p = site.static_dir.toAbsolutePath().normalize();
|
|
||||||
}
|
|
||||||
staticBase = p;
|
|
||||||
refresh = cfg.getBoolean("site", "refreshHeaderFooter", true);
|
|
||||||
cache = CacheBuilder.newBuilder()
|
|
||||||
.maximumWeight(1 << 20)
|
|
||||||
.weigher(new Weigher<String, Resource>() {
|
|
||||||
@Override
|
|
||||||
public int weigh(String name, Resource r) {
|
|
||||||
return 2 * name.length() + r.raw.length;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.build(new CacheLoader<String, Resource>() {
|
|
||||||
@Override
|
|
||||||
public Resource load(String name) throws Exception {
|
|
||||||
return loadResource(name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
Resource getResource(String name) {
|
|
||||||
try {
|
|
||||||
return cache.get(name);
|
|
||||||
} catch (ExecutionException e) {
|
|
||||||
log.warn(String.format("Cannot load static resource %s", name), e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Resource getResource(HttpServletRequest req)
|
|
||||||
throws ExecutionException {
|
|
||||||
String name = CharMatcher.is('/').trimFrom(req.getPathInfo());
|
|
||||||
if (isUnreasonableName(name)) {
|
|
||||||
return Resource.NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
Resource r = cache.get(name);
|
|
||||||
if (r == Resource.NOT_FOUND) {
|
|
||||||
return Resource.NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (refresh && r.isStale()) {
|
|
||||||
cache.invalidate(name);
|
|
||||||
r = cache.get(name);
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isUnreasonableName(String name) {
|
|
||||||
return name.length() < 1
|
|
||||||
|| name.contains("\\") // no windows/dos style paths
|
|
||||||
|| name.startsWith("../") // no "../etc/passwd"
|
|
||||||
|| name.contains("/../") // no "foo/../etc/passwd"
|
|
||||||
|| name.contains("/./") // "foo/./foo" is insane to ask
|
|
||||||
|| name.contains("//"); // windows UNC path can be "//..."
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doGet(final HttpServletRequest req,
|
|
||||||
final HttpServletResponse rsp) throws IOException {
|
|
||||||
Resource r;
|
|
||||||
try {
|
|
||||||
r = getResource(req);
|
|
||||||
} catch (ExecutionException e) {
|
|
||||||
log.warn(String.format(
|
|
||||||
"Cannot load static resource %s",
|
|
||||||
req.getPathInfo()), e);
|
|
||||||
CacheHeaders.setNotCacheable(rsp);
|
|
||||||
rsp.setStatus(SC_INTERNAL_SERVER_ERROR);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String e = req.getParameter("e");
|
|
||||||
if (r == Resource.NOT_FOUND || (e != null && !r.etag.equals(e))) {
|
|
||||||
CacheHeaders.setNotCacheable(rsp);
|
|
||||||
rsp.setStatus(SC_NOT_FOUND);
|
|
||||||
return;
|
|
||||||
} else if (r.etag.equals(req.getHeader(IF_NONE_MATCH))) {
|
|
||||||
rsp.setStatus(SC_NOT_MODIFIED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] tosend = r.raw;
|
|
||||||
if (!r.contentType.equals(JS) && RPCServletUtils.acceptsGzipEncoding(req)) {
|
|
||||||
byte[] gz = HtmlDomUtil.compress(tosend);
|
|
||||||
if ((gz.length + 24) < tosend.length) {
|
|
||||||
rsp.setHeader(CONTENT_ENCODING, "gzip");
|
|
||||||
tosend = gz;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (e != null && r.etag.equals(e)) {
|
|
||||||
CacheHeaders.setCacheable(req, rsp, 360, DAYS, false);
|
|
||||||
} else {
|
|
||||||
CacheHeaders.setCacheable(req, rsp, 15, MINUTES, refresh);
|
|
||||||
}
|
|
||||||
rsp.setHeader(ETAG, r.etag);
|
|
||||||
rsp.setContentType(r.contentType);
|
|
||||||
rsp.setContentLength(tosend.length);
|
|
||||||
try (OutputStream out = rsp.getOutputStream()) {
|
|
||||||
out.write(tosend);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Resource loadResource(String name) throws IOException {
|
|
||||||
Path p = staticBase.resolve(name);
|
|
||||||
try {
|
|
||||||
p = p.toRealPath().normalize();
|
|
||||||
} catch (IOException e) {
|
|
||||||
return Resource.NOT_FOUND;
|
|
||||||
}
|
|
||||||
if (!p.startsWith(staticBase)) {
|
|
||||||
return Resource.NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
long ts = FileUtil.lastModified(p);
|
|
||||||
byte[] raw;
|
|
||||||
try {
|
|
||||||
raw = Files.readAllBytes(p);
|
|
||||||
} catch (NoSuchFileException e) {
|
|
||||||
return Resource.NOT_FOUND;
|
|
||||||
}
|
|
||||||
return new Resource(p, ts, contentType(name), raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
static class Resource {
|
|
||||||
static final Resource NOT_FOUND = new Resource(null, -1, "", new byte[] {});
|
|
||||||
|
|
||||||
final Path src;
|
|
||||||
final long lastModified;
|
|
||||||
final String contentType;
|
|
||||||
final String etag;
|
|
||||||
final byte[] raw;
|
|
||||||
|
|
||||||
Resource(Path src, long lastModified, String contentType, byte[] raw) {
|
|
||||||
this.src = src;
|
|
||||||
this.lastModified = lastModified;
|
|
||||||
this.contentType = contentType;
|
|
||||||
this.etag = Hashing.md5().hashBytes(raw).toString();
|
|
||||||
this.raw = raw;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isStale() {
|
|
||||||
return lastModified != lastModified(src);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (C) 2015 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.raw;
|
||||||
|
|
||||||
|
import com.google.common.cache.Cache;
|
||||||
|
import com.google.gerrit.common.TimeUtil;
|
||||||
|
|
||||||
|
import java.nio.file.FileSystem;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.attribute.FileTime;
|
||||||
|
|
||||||
|
class WarGwtUiServlet extends ResourceServlet {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private static final FileTime NOW = FileTime.fromMillis(TimeUtil.nowMs());
|
||||||
|
|
||||||
|
private final FileSystem warFs;
|
||||||
|
|
||||||
|
WarGwtUiServlet(Cache<Path, Resource> cache, FileSystem warFs) {
|
||||||
|
super(cache, false);
|
||||||
|
this.warFs = warFs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Path getResourcePath(String pathInfo) {
|
||||||
|
return warFs.getPath("/gerrit_ui/" + pathInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected FileTime getLastModifiedTime(Path p) {
|
||||||
|
// Return initialization time of this class, since the GWT outputs from the
|
||||||
|
// build process all have mtimes of 1980/1/1.
|
||||||
|
return NOW;
|
||||||
|
}
|
||||||
|
}
|
@@ -5,6 +5,7 @@ java_library(
|
|||||||
srcs = ['src/main/java/com/google/gerrit/launcher/GerritLauncher.java'],
|
srcs = ['src/main/java/com/google/gerrit/launcher/GerritLauncher.java'],
|
||||||
visibility = [
|
visibility = [
|
||||||
'//gerrit-acceptance-tests/...',
|
'//gerrit-acceptance-tests/...',
|
||||||
|
'//gerrit-httpd:',
|
||||||
'//gerrit-main:main_lib',
|
'//gerrit-main:main_lib',
|
||||||
'//gerrit-pgm:',
|
'//gerrit-pgm:',
|
||||||
],
|
],
|
||||||
|
@@ -27,12 +27,16 @@ import java.lang.reflect.Method;
|
|||||||
import java.lang.reflect.Modifier;
|
import java.lang.reflect.Modifier;
|
||||||
import java.net.JarURLConnection;
|
import java.net.JarURLConnection;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URI;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLClassLoader;
|
import java.net.URLClassLoader;
|
||||||
|
import java.nio.file.FileSystem;
|
||||||
|
import java.nio.file.FileSystems;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.security.CodeSource;
|
import java.security.CodeSource;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.SortedMap;
|
import java.util.SortedMap;
|
||||||
@@ -296,6 +300,7 @@ public final class GerritLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static volatile File myArchive;
|
private static volatile File myArchive;
|
||||||
|
private static volatile FileSystem myArchiveFs;
|
||||||
private static volatile File myHome;
|
private static volatile File myHome;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -304,11 +309,29 @@ public final class GerritLauncher {
|
|||||||
* @return local path of the Gerrit WAR file.
|
* @return local path of the Gerrit WAR file.
|
||||||
* @throws FileNotFoundException if the code cannot guess the location.
|
* @throws FileNotFoundException if the code cannot guess the location.
|
||||||
*/
|
*/
|
||||||
public static File getDistributionArchive() throws FileNotFoundException {
|
public static File getDistributionArchive()
|
||||||
if (myArchive == null) {
|
throws FileNotFoundException, IOException {
|
||||||
myArchive = locateMyArchive();
|
File result = myArchive;
|
||||||
|
if (result == null) {
|
||||||
|
synchronized (GerritLauncher.class) {
|
||||||
|
result = myArchive;
|
||||||
|
if (result != null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result = locateMyArchive();
|
||||||
|
myArchiveFs = FileSystems.newFileSystem(
|
||||||
|
URI.create("jar:" + result.toPath().toUri()),
|
||||||
|
Collections.<String, String> emptyMap());
|
||||||
|
myArchive = result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return myArchive;
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FileSystem getDistributionArchiveFileSystem()
|
||||||
|
throws FileNotFoundException, IOException {
|
||||||
|
getDistributionArchive();
|
||||||
|
return myArchiveFs;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static File locateMyArchive() throws FileNotFoundException {
|
private static File locateMyArchive() throws FileNotFoundException {
|
||||||
|
@@ -14,24 +14,15 @@
|
|||||||
|
|
||||||
package com.google.gerrit.pgm.http.jetty;
|
package com.google.gerrit.pgm.http.jetty;
|
||||||
|
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
|
||||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||||
|
|
||||||
import com.google.common.base.MoreObjects;
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.escape.Escaper;
|
|
||||||
import com.google.common.html.HtmlEscapers;
|
|
||||||
import com.google.common.io.ByteStreams;
|
|
||||||
import com.google.gerrit.common.TimeUtil;
|
|
||||||
import com.google.gerrit.extensions.events.LifecycleListener;
|
import com.google.gerrit.extensions.events.LifecycleListener;
|
||||||
import com.google.gerrit.launcher.GerritLauncher;
|
|
||||||
import com.google.gerrit.pgm.http.jetty.HttpLog.HttpLogFactory;
|
import com.google.gerrit.pgm.http.jetty.HttpLog.HttpLogFactory;
|
||||||
import com.google.gerrit.reviewdb.client.AuthType;
|
import com.google.gerrit.reviewdb.client.AuthType;
|
||||||
import com.google.gerrit.server.config.GerritServerConfig;
|
import com.google.gerrit.server.config.GerritServerConfig;
|
||||||
import com.google.gerrit.server.config.SitePaths;
|
import com.google.gerrit.server.config.SitePaths;
|
||||||
import com.google.gwtexpui.linker.server.UserAgentRule;
|
|
||||||
import com.google.gwtexpui.server.CacheHeaders;
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Injector;
|
import com.google.inject.Injector;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
@@ -60,49 +51,26 @@ import org.eclipse.jetty.servlet.ServletContextHandler;
|
|||||||
import org.eclipse.jetty.servlet.ServletHolder;
|
import org.eclipse.jetty.servlet.ServletHolder;
|
||||||
import org.eclipse.jetty.util.BlockingArrayQueue;
|
import org.eclipse.jetty.util.BlockingArrayQueue;
|
||||||
import org.eclipse.jetty.util.log.Log;
|
import org.eclipse.jetty.util.log.Log;
|
||||||
import org.eclipse.jetty.util.resource.Resource;
|
|
||||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||||
import org.eclipse.jetty.util.thread.ThreadPool;
|
import org.eclipse.jetty.util.thread.ThreadPool;
|
||||||
import org.eclipse.jgit.lib.Config;
|
import org.eclipse.jgit.lib.Config;
|
||||||
import org.eclipse.jgit.util.RawParseUtils;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InterruptedIOException;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.lang.management.ManagementFactory;
|
import java.lang.management.ManagementFactory;
|
||||||
import java.net.MalformedURLException;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.Enumeration;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Properties;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.zip.ZipEntry;
|
|
||||||
import java.util.zip.ZipFile;
|
|
||||||
|
|
||||||
import javax.servlet.DispatcherType;
|
import javax.servlet.DispatcherType;
|
||||||
import javax.servlet.Filter;
|
import javax.servlet.Filter;
|
||||||
import javax.servlet.FilterChain;
|
|
||||||
import javax.servlet.FilterConfig;
|
|
||||||
import javax.servlet.ServletException;
|
|
||||||
import javax.servlet.ServletRequest;
|
|
||||||
import javax.servlet.ServletResponse;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class JettyServer {
|
public class JettyServer {
|
||||||
@@ -158,13 +126,9 @@ public class JettyServer {
|
|||||||
|
|
||||||
private boolean reverseProxy;
|
private boolean reverseProxy;
|
||||||
|
|
||||||
/** Location on disk where our WAR file was unpacked to. */
|
|
||||||
private Resource baseResource;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
JettyServer(@GerritServerConfig final Config cfg, final SitePaths site,
|
JettyServer(@GerritServerConfig final Config cfg, final SitePaths site,
|
||||||
final JettyEnv env, final HttpLogFactory httpLogFactory)
|
final JettyEnv env, final HttpLogFactory httpLogFactory) {
|
||||||
throws MalformedURLException, IOException {
|
|
||||||
this.site = site;
|
this.site = site;
|
||||||
|
|
||||||
httpd = new Server(threadPool(cfg));
|
httpd = new Server(threadPool(cfg));
|
||||||
@@ -372,8 +336,7 @@ public class JettyServer {
|
|||||||
return pool;
|
return pool;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Handler makeContext(final JettyEnv env, final Config cfg)
|
private Handler makeContext(final JettyEnv env, final Config cfg) {
|
||||||
throws MalformedURLException, IOException {
|
|
||||||
final Set<String> paths = new HashSet<>();
|
final Set<String> paths = new HashSet<>();
|
||||||
for (URI u : listenURLs(cfg)) {
|
for (URI u : listenURLs(cfg)) {
|
||||||
String p = u.getPath();
|
String p = u.getPath();
|
||||||
@@ -408,7 +371,7 @@ public class JettyServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ContextHandler makeContext(final String contextPath,
|
private ContextHandler makeContext(final String contextPath,
|
||||||
final JettyEnv env, final Config cfg) throws MalformedURLException, IOException {
|
final JettyEnv env, final Config cfg) {
|
||||||
final ServletContextHandler app = new ServletContextHandler();
|
final ServletContextHandler app = new ServletContextHandler();
|
||||||
|
|
||||||
// This enables the use of sessions in Jetty, feature available
|
// This enables the use of sessions in Jetty, feature available
|
||||||
@@ -421,12 +384,6 @@ public class JettyServer {
|
|||||||
//
|
//
|
||||||
app.setContextPath(contextPath);
|
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(app));
|
|
||||||
|
|
||||||
// HTTP front-end filter to be used as surrogate of Apache HTTP
|
// HTTP front-end filter to be used as surrogate of Apache HTTP
|
||||||
// reverse-proxy filtering.
|
// reverse-proxy filtering.
|
||||||
// It is meant to be used as simpler tiny deployment of custom-made
|
// It is meant to be used as simpler tiny deployment of custom-made
|
||||||
@@ -478,222 +435,4 @@ public class JettyServer {
|
|||||||
app.setWelcomeFiles(new String[0]);
|
app.setWelcomeFiles(new String[0]);
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Resource getBaseResource(ServletContextHandler app)
|
|
||||||
throws IOException {
|
|
||||||
if (baseResource == null) {
|
|
||||||
try {
|
|
||||||
baseResource = unpackWar(GerritLauncher.getDistributionArchive());
|
|
||||||
} catch (FileNotFoundException err) {
|
|
||||||
if (GerritLauncher.NOT_ARCHIVED.equals(err.getMessage())) {
|
|
||||||
baseResource = useDeveloperBuild(app);
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return baseResource;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Resource unpackWar(File srcwar) throws IOException {
|
|
||||||
File dstwar = makeWarTempDir();
|
|
||||||
unpack(srcwar, dstwar);
|
|
||||||
return Resource.newResource(dstwar.toURI());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static File makeWarTempDir() 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 {
|
|
||||||
return dstwar.getCanonicalFile();
|
|
||||||
} catch (IOException e) {
|
|
||||||
return dstwar.getAbsoluteFile();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void unpack(File srcwar, File dstwar) throws IOException {
|
|
||||||
try (ZipFile zf = new ZipFile(srcwar)) {
|
|
||||||
final Enumeration<? extends ZipEntry> e = zf.entries();
|
|
||||||
while (e.hasMoreElements()) {
|
|
||||||
final ZipEntry ze = e.nextElement();
|
|
||||||
final String name = ze.getName();
|
|
||||||
|
|
||||||
if (ze.isDirectory()
|
|
||||||
|| name.startsWith("WEB-INF/")
|
|
||||||
|| name.startsWith("META-INF/")
|
|
||||||
|| name.startsWith("com/google/gerrit/launcher/")
|
|
||||||
|| name.equals("Main.class")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
final File rawtmp = new File(dstwar, name);
|
|
||||||
mkdir(rawtmp.getParentFile());
|
|
||||||
rawtmp.deleteOnExit();
|
|
||||||
|
|
||||||
try (FileOutputStream rawout = new FileOutputStream(rawtmp);
|
|
||||||
InputStream in = zf.getInputStream(ze)) {
|
|
||||||
final byte[] buf = new byte[4096];
|
|
||||||
int n;
|
|
||||||
while ((n = in.read(buf, 0, buf.length)) > 0) {
|
|
||||||
rawout.write(buf, 0, n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void mkdir(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(ServletContextHandler app)
|
|
||||||
throws IOException {
|
|
||||||
final Path dir = GerritLauncher.getDeveloperBuckOut();
|
|
||||||
final Path gen = dir.resolve("gen");
|
|
||||||
final Path root = dir.getParent();
|
|
||||||
final File dstwar = makeWarTempDir();
|
|
||||||
File ui = new File(dstwar, "gerrit_ui");
|
|
||||||
File p = new File(ui, "permutations");
|
|
||||||
mkdir(ui);
|
|
||||||
p.createNewFile();
|
|
||||||
p.deleteOnExit();
|
|
||||||
|
|
||||||
app.addFilter(new FilterHolder(new Filter() {
|
|
||||||
private final boolean gwtuiRecompile =
|
|
||||||
System.getProperty("gerrit.disable-gwtui-recompile") == null;
|
|
||||||
private final UserAgentRule rule = new UserAgentRule();
|
|
||||||
private final Set<String> uaInitialized = new HashSet<>();
|
|
||||||
private String lastTarget;
|
|
||||||
private long lastTime;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doFilter(ServletRequest request, ServletResponse res,
|
|
||||||
FilterChain chain) throws IOException, ServletException {
|
|
||||||
String pkg = "gerrit-gwtui";
|
|
||||||
String target = "ui_" + rule.select((HttpServletRequest) request);
|
|
||||||
if (gwtuiRecompile || !uaInitialized.contains(target)) {
|
|
||||||
String rule = "//" + pkg + ":" + target;
|
|
||||||
// TODO(davido): instead of assuming specific Buck's internal
|
|
||||||
// target directory for gwt_binary() artifacts, ask Buck for
|
|
||||||
// the location of user agent permutation GWT zip, e. g.:
|
|
||||||
// $ buck targets --show_output //gerrit-gwtui:ui_safari \
|
|
||||||
// | awk '{print $2}'
|
|
||||||
String child = String.format("%s/__gwt_binary_%s__", pkg, target);
|
|
||||||
File zip = gen.resolve(child).resolve(target + ".zip").toFile();
|
|
||||||
|
|
||||||
synchronized (this) {
|
|
||||||
try {
|
|
||||||
build(root, gen, rule);
|
|
||||||
} catch (BuildFailureException e) {
|
|
||||||
displayFailure(rule, e.why, (HttpServletResponse) res);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!target.equals(lastTarget) || lastTime != zip.lastModified()) {
|
|
||||||
lastTarget = target;
|
|
||||||
lastTime = zip.lastModified();
|
|
||||||
unpack(zip, dstwar);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
uaInitialized.add(target);
|
|
||||||
}
|
|
||||||
chain.doFilter(request, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void displayFailure(String rule, byte[] why, HttpServletResponse res)
|
|
||||||
throws IOException {
|
|
||||||
res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
|
||||||
res.setContentType("text/html");
|
|
||||||
res.setCharacterEncoding(UTF_8.name());
|
|
||||||
CacheHeaders.setNotCacheable(res);
|
|
||||||
|
|
||||||
Escaper html = HtmlEscapers.htmlEscaper();
|
|
||||||
try (PrintWriter w = res.getWriter()) {
|
|
||||||
w.write("<html><title>BUILD FAILED</title><body>");
|
|
||||||
w.format("<h1>%s FAILED</h1>", html.escape(rule));
|
|
||||||
w.write("<pre>");
|
|
||||||
w.write(html.escape(RawParseUtils.decode(why)));
|
|
||||||
w.write("</pre>");
|
|
||||||
w.write("</body></html>");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(FilterConfig config) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroy() {
|
|
||||||
}
|
|
||||||
}), "/", EnumSet.of(DispatcherType.REQUEST));
|
|
||||||
return Resource.newResource(dstwar.toURI());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void build(Path root, Path gen, String target)
|
|
||||||
throws IOException, BuildFailureException {
|
|
||||||
log.info("buck build " + target);
|
|
||||||
Properties properties = loadBuckProperties(gen);
|
|
||||||
String buck = MoreObjects.firstNonNull(properties.getProperty("buck"), "buck");
|
|
||||||
ProcessBuilder proc = new ProcessBuilder(buck, "build", target)
|
|
||||||
.directory(root.toFile())
|
|
||||||
.redirectErrorStream(true);
|
|
||||||
if (properties.containsKey("PATH")) {
|
|
||||||
proc.environment().put("PATH", properties.getProperty("PATH"));
|
|
||||||
}
|
|
||||||
long start = TimeUtil.nowMs();
|
|
||||||
Process rebuild = proc.start();
|
|
||||||
byte[] out;
|
|
||||||
try (InputStream in = rebuild.getInputStream()) {
|
|
||||||
out = ByteStreams.toByteArray(in);
|
|
||||||
} finally {
|
|
||||||
rebuild.getOutputStream().close();
|
|
||||||
}
|
|
||||||
|
|
||||||
int status;
|
|
||||||
try {
|
|
||||||
status = rebuild.waitFor();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
throw new InterruptedIOException("interrupted waiting for " + buck);
|
|
||||||
}
|
|
||||||
if (status != 0) {
|
|
||||||
throw new BuildFailureException(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
long time = TimeUtil.nowMs() - start;
|
|
||||||
log.info(String.format("UPDATED %s in %.3fs", target, time / 1000.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Properties loadBuckProperties(Path gen)
|
|
||||||
throws FileNotFoundException, IOException {
|
|
||||||
Properties properties = new Properties();
|
|
||||||
try (InputStream in = new FileInputStream(
|
|
||||||
gen.resolve(Paths.get("tools/buck/buck.properties")).toFile())) {
|
|
||||||
properties.load(in);
|
|
||||||
}
|
|
||||||
return properties;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("serial")
|
|
||||||
private static class BuildFailureException extends Exception {
|
|
||||||
final byte[] why;
|
|
||||||
|
|
||||||
BuildFailureException(byte[] why) {
|
|
||||||
this.why = why;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user