// Copyright (C) 2014 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.googlesource.gerrit.plugins.gitblit; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import java.util.regex.Pattern; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jgit.util.IO; import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.mime.MimeUtilFileTypeRegistry; import com.google.inject.Inject; import com.google.inject.Singleton; import eu.medsea.mimeutil.MimeType; @Singleton public class StaticResourcesServlet extends HttpServlet { private static final long serialVersionUID = 5262736289985705065L; /** * The resource must either be in one of the allowed subdirectories, or must match the filename pattern. If neither is true, we return a 404. * There's a whole lot of other stuff there that we don't want to expose. */ private static final Set<String> ALLOWED_SUBDIRECTORIES = ImmutableSet.of("bootstrap", "flotr2", "fontawesome", "octicons"); private static final Pattern ALLOWED_FILE_NAMES = Pattern.compile("^(?:gitblit\\.properties|.*\\.(?:png|css|js|swf))$"); private final MimeUtilFileTypeRegistry mimeDetector; private final File gerritPluginDirectory; private final AtomicLong lastModified = new AtomicLong(-1L); @Inject public StaticResourcesServlet(final MimeUtilFileTypeRegistry mimeDetector, final SitePaths sitePaths) { super(); this.mimeDetector = mimeDetector; this.gerritPluginDirectory = sitePaths.plugins_dir.toFile(); } @Override protected long getLastModified(HttpServletRequest request) { long result = lastModified.get(); if (result < 0) { // Simply return the time the plugin was put into Gerrit's plugin directory. File[] plugins = gerritPluginDirectory.listFiles(new FilenameFilter() { @Override public boolean accept(File directory, String fileName) { return fileName != null && fileName.startsWith("gitblit") && fileName.endsWith(".jar"); } }); result = 0L; if (plugins != null && plugins.length > 0) { for (File f : plugins) { result = Math.max(result, f.lastModified()); } } lastModified.set(result); } return result; } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // Extract the filename from the request String resourcePath = request.getPathInfo(); // Is already relative to /static! if (request.getRequestURI().endsWith("/clippy.swf")) { resourcePath = "/clippy.swf"; } if (Strings.isNullOrEmpty(resourcePath)) { response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } // Must not contain any navigation String[] segments = resourcePath.substring(1).split("/"); for (String segment : segments) { if (segment.equals("..")) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid path"); return; } } String fileName = segments[segments.length - 1]; if (Strings.isNullOrEmpty(fileName)) { response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } // Restrict to the few known subdirectories. if (segments.length > 1 && !ALLOWED_SUBDIRECTORIES.contains(segments[0])) { response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } else if (segments.length == 1 && !ALLOWED_FILE_NAMES.matcher(fileName).matches()) { // Only allow the known filetypes: we have only png, css, js, swf, and gitblit.properties. response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } // We just happen to know that all our static data files are small, so it's OK to read them fully into memory byte[] bytes = null; try (InputStream data = getClass().getResourceAsStream(resourcePath)) { if (data == null) { response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } bytes = IO.readWholeStream(data, 0).array(); } if (bytes == null) { // Should not occur since we don't catch the possible IOException above. Nevertheless, let's be paranoid. bytes = new byte[0]; } String contentType = null; // Compare https://gerrit-review.googlesource.com/#/c/67000/ if (fileName.toLowerCase().endsWith(".js")) { contentType = "application/javascript"; } else if (fileName.toLowerCase().endsWith(".css")) { contentType = "text/css"; } else { MimeType mimeType = mimeDetector.getMimeType(fileName, bytes); contentType = mimeType != null ? mimeType.toString() : "application/octet-stream"; } response.setContentType(contentType); long lastModified = getLastModified(request); if (lastModified > 0) { response.setDateHeader("Last-Modified", lastModified); } response.setHeader("Content-Length", Integer.toString(bytes.length)); try (OutputStream out = response.getOutputStream()) { out.write(bytes, 0, bytes.length); } } }