blob: 52b7a5c932358958d88d7c310154f7b092d5f3dd [file] [log] [blame]
// 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 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.common.io.ByteStreams;
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.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
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 File staticBase;
private final String staticBasePath;
private final boolean refresh;
private final LoadingCache<String, Resource> cache;
@Inject
StaticServlet(@GerritServerConfig Config cfg, SitePaths site) {
File f;
try {
f = site.static_dir.getCanonicalFile();
} catch (IOException e) {
f = site.static_dir.getAbsoluteFile();
}
staticBase = f;
staticBasePath = staticBase.getPath() + File.separator;
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) {
if (name.length() < 1) return true;
if (name.contains("\\")) return true; // no windows/dos style paths
if (name.startsWith("../")) return true; // no "../etc/passwd"
if (name.contains("/../")) return true; // no "foo/../etc/passwd"
if (name.contains("/./")) return true; // "foo/./foo" is insane to ask
if (name.contains("//")) return true; // windows UNC path can be "//..."
return false; // is a reasonable name
}
@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);
final OutputStream out = rsp.getOutputStream();
try {
out.write(tosend);
} finally {
out.close();
}
}
private Resource loadResource(String name) throws IOException {
File p = new File(staticBase, name);
try {
p = p.getCanonicalFile();
} catch (IOException e) {
return Resource.NOT_FOUND;
}
if (!p.getPath().startsWith(staticBasePath)) {
return Resource.NOT_FOUND;
}
long ts = p.lastModified();
FileInputStream in;
try {
in = new FileInputStream(p);
} catch (FileNotFoundException e) {
return Resource.NOT_FOUND;
}
byte[] raw;
try {
raw = ByteStreams.toByteArray(in);
} finally {
in.close();
}
return new Resource(p, ts, contentType(name), raw);
}
static class Resource {
static final Resource NOT_FOUND = new Resource(null, -1, "", new byte[] {});
final File src;
final long lastModified;
final String contentType;
final String etag;
final byte[] raw;
Resource(File 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 != src.lastModified();
}
}
}