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);
|
||||
}
|
||||
|
||||
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,
|
||||
String type, long age, TimeUnit unit, boolean revalidate) {
|
||||
res.setHeader("Cache-Control", String.format(
|
||||
|
@@ -12,7 +12,9 @@ java_library(
|
||||
'//gerrit-common:annotations',
|
||||
'//gerrit-common:server',
|
||||
'//gerrit-extension-api:api',
|
||||
'//gerrit-gwtexpui:linker_server',
|
||||
'//gerrit-gwtexpui:server',
|
||||
'//gerrit-launcher:launcher',
|
||||
'//gerrit-patch-jgit:server',
|
||||
'//gerrit-prettify: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.RobotsServlet;
|
||||
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.rpc.access.AccessRestApiServlet;
|
||||
import com.google.gerrit.httpd.rpc.account.AccountsRestApiServlet;
|
||||
@@ -77,7 +77,6 @@ class UrlModule extends ServletModule {
|
||||
serve("/signout").with(HttpLogoutServlet.class);
|
||||
}
|
||||
serve("/ssh_info").with(SshInfoServlet.class);
|
||||
serve("/static/*").with(StaticServlet.class);
|
||||
|
||||
serve("/Main.class").with(notFound());
|
||||
serve("/com/google/gerrit/launcher/*").with(notFound());
|
||||
@@ -107,6 +106,8 @@ class UrlModule extends ServletModule {
|
||||
filter("/Documentation/").through(QueryDocumentationFilter.class);
|
||||
|
||||
serve("/robots.txt").with(RobotsServlet.class);
|
||||
|
||||
install(new StaticModule());
|
||||
}
|
||||
|
||||
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 String noCacheName;
|
||||
private final boolean refreshHeaderFooter;
|
||||
private final StaticServlet staticServlet;
|
||||
private final SiteStaticDirectoryServlet staticServlet;
|
||||
private final boolean isNoteDbEnabled;
|
||||
private final Integer pluginsLoadTimeout;
|
||||
private final GetDiffPreferences getDiff;
|
||||
@@ -108,7 +108,7 @@ public class HostPageServlet extends HttpServlet {
|
||||
DynamicSet<WebUiPlugin> webUiPlugins,
|
||||
DynamicSet<MessageOfTheDay> motd,
|
||||
@GerritServerConfig Config cfg,
|
||||
StaticServlet ss,
|
||||
SiteStaticDirectoryServlet ss,
|
||||
NotesMigration migration,
|
||||
GetDiffPreferences diffPref)
|
||||
throws IOException, ServletException {
|
||||
@@ -302,7 +302,7 @@ public class HostPageServlet extends HttpServlet {
|
||||
String src = e.getAttribute("src");
|
||||
if (src != null && src.startsWith("static/")) {
|
||||
String name = src.substring("static/".length());
|
||||
StaticServlet.Resource r = staticServlet.getResource(name);
|
||||
ResourceServlet.Resource r = staticServlet.getResource(name);
|
||||
if (r != null) {
|
||||
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'],
|
||||
visibility = [
|
||||
'//gerrit-acceptance-tests/...',
|
||||
'//gerrit-httpd:',
|
||||
'//gerrit-main:main_lib',
|
||||
'//gerrit-pgm:',
|
||||
],
|
||||
|
@@ -27,12 +27,16 @@ import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.net.JarURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.CodeSource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.SortedMap;
|
||||
@@ -296,6 +300,7 @@ public final class GerritLauncher {
|
||||
}
|
||||
|
||||
private static volatile File myArchive;
|
||||
private static volatile FileSystem myArchiveFs;
|
||||
private static volatile File myHome;
|
||||
|
||||
/**
|
||||
@@ -304,11 +309,29 @@ public final class GerritLauncher {
|
||||
* @return local path of the Gerrit WAR file.
|
||||
* @throws FileNotFoundException if the code cannot guess the location.
|
||||
*/
|
||||
public static File getDistributionArchive() throws FileNotFoundException {
|
||||
if (myArchive == null) {
|
||||
myArchive = locateMyArchive();
|
||||
public static File getDistributionArchive()
|
||||
throws FileNotFoundException, IOException {
|
||||
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 {
|
||||
|
@@ -14,24 +14,15 @@
|
||||
|
||||
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.SECONDS;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
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.launcher.GerritLauncher;
|
||||
import com.google.gerrit.pgm.http.jetty.HttpLog.HttpLogFactory;
|
||||
import com.google.gerrit.reviewdb.client.AuthType;
|
||||
import com.google.gerrit.server.config.GerritServerConfig;
|
||||
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.Injector;
|
||||
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.util.BlockingArrayQueue;
|
||||
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.thread.QueuedThreadPool;
|
||||
import org.eclipse.jetty.util.thread.ThreadPool;
|
||||
import org.eclipse.jgit.lib.Config;
|
||||
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.lang.management.ManagementFactory;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import javax.servlet.DispatcherType;
|
||||
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
|
||||
public class JettyServer {
|
||||
@@ -158,13 +126,9 @@ public class JettyServer {
|
||||
|
||||
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, final HttpLogFactory httpLogFactory)
|
||||
throws MalformedURLException, IOException {
|
||||
final JettyEnv env, final HttpLogFactory httpLogFactory) {
|
||||
this.site = site;
|
||||
|
||||
httpd = new Server(threadPool(cfg));
|
||||
@@ -372,8 +336,7 @@ public class JettyServer {
|
||||
return pool;
|
||||
}
|
||||
|
||||
private Handler makeContext(final JettyEnv env, final Config cfg)
|
||||
throws MalformedURLException, IOException {
|
||||
private Handler makeContext(final JettyEnv env, final Config cfg) {
|
||||
final Set<String> paths = new HashSet<>();
|
||||
for (URI u : listenURLs(cfg)) {
|
||||
String p = u.getPath();
|
||||
@@ -408,7 +371,7 @@ public class JettyServer {
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// This enables the use of sessions in Jetty, feature available
|
||||
@@ -421,12 +384,6 @@ public class JettyServer {
|
||||
//
|
||||
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
|
||||
// reverse-proxy filtering.
|
||||
// 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]);
|
||||
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