| // 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(); |
| } |
| } |
| } |