// Copyright 2016 The Nomulus Authors. All Rights Reserved. // // 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 google.registry.server; import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Verify.verify; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import com.google.common.base.Optional; import com.google.common.collect.ImmutableMap; import com.google.common.net.MediaType; import com.google.common.primitives.Ints; import google.registry.util.FormattingLogger; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import javax.annotation.PostConstruct; import javax.servlet.ServletConfig; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.mortbay.jetty.servlet.ServletHolder; /** * Servlet for serving static resources on a Jetty development server path prefix. * *

This servlet can serve either a single file or the contents of a directory. * *

Note: This code should never be used in production. It's for testing purposes only. */ public final class StaticResourceServlet extends HttpServlet { private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass(); private static final MediaType DEFAULT_MIME_TYPE = MediaType.OCTET_STREAM; private static final ImmutableMap MIMES_BY_EXTENSION = new ImmutableMap.Builder() .put("css", MediaType.CSS_UTF_8) .put("csv", MediaType.CSV_UTF_8) .put("gif", MediaType.GIF) .put("html", MediaType.HTML_UTF_8) .put("jpeg", MediaType.JPEG) .put("jpg", MediaType.JPEG) .put("js", MediaType.JAVASCRIPT_UTF_8) .put("json", MediaType.JSON_UTF_8) .put("png", MediaType.PNG) .put("txt", MediaType.PLAIN_TEXT_UTF_8) .put("xml", MediaType.XML_UTF_8) .build(); /** * Creates a servlet holder for this servlet so it can be used with Jetty. * * @param prefix servlet path starting with a slash and ending with {@code "/*"} if {@code root} * is a directory * @param root file or root directory to serve */ public static ServletHolder create(String prefix, Path root) { root = root.toAbsolutePath(); checkArgument(Files.exists(root), "Root must exist: %s", root); checkArgument(prefix.startsWith("/"), "Prefix must start with a slash: %s", prefix); ServletHolder holder = new ServletHolder(StaticResourceServlet.class); holder.setInitParameter("root", root.toString()); if (Files.isDirectory(root)) { checkArgument(prefix.endsWith("/*"), "Prefix (%s) must end with /* since root (%s) is a directory", prefix, root); holder.setInitParameter("prefix", prefix.substring(0, prefix.length() - 1)); } else { holder.setInitParameter("prefix", prefix); } return holder; } private Optional fileServer = Optional.absent(); @Override @PostConstruct public void init(ServletConfig config) { Path root = Paths.get(config.getInitParameter("root")); String prefix = config.getInitParameter("prefix"); verify(prefix.startsWith("/")); boolean isDirectory = Files.isDirectory(root); verify(!isDirectory || isDirectory && prefix.endsWith("/")); fileServer = Optional.of(new FileServer(root, prefix)); } @Override public void doHead(HttpServletRequest req, HttpServletResponse rsp) throws IOException { fileServer.get().doHead(req, rsp); } @Override public void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { fileServer.get().doGet(req, rsp); } private static final class FileServer { private final Path root; private final String prefix; FileServer(Path root, String prefix) { this.root = root; this.prefix = prefix; } Optional doHead(HttpServletRequest req, HttpServletResponse rsp) throws IOException { verify(req.getRequestURI().startsWith(prefix)); Path file = root.resolve(req.getRequestURI().substring(prefix.length())).normalize(); if (!file.startsWith(root)) { logger.infofmt("Evil request: %s (%s) (%s + %s)", req.getRequestURI(), file, root, prefix); rsp.sendError(SC_BAD_REQUEST, "Naughty naughty!"); return Optional.absent(); } if (!Files.exists(file)) { logger.infofmt("Not found: %s (%s)", req.getRequestURI(), file); rsp.sendError(SC_NOT_FOUND, "Not found"); return Optional.absent(); } if (Files.isDirectory(file)) { logger.infofmt("Directory listing forbidden: %s (%s)", req.getRequestURI(), file); rsp.sendError(SC_FORBIDDEN, "No directory listing"); return Optional.absent(); } rsp.setContentType( firstNonNull( MIMES_BY_EXTENSION.get(getExtension(file.getFileName().toString())), DEFAULT_MIME_TYPE) .toString()); rsp.setContentLength(Ints.checkedCast(Files.size(file))); return Optional.of(file); } void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { for (Path file : doHead(req, rsp).asSet()) { rsp.getOutputStream().write(Files.readAllBytes(file)); } } } private static String getExtension(String filename) { int dot = filename.lastIndexOf('.'); return dot == -1 ? "" : filename.substring(dot + 1); } }