All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.apphosting.runtime.jetty.ee8.ResourceFileServlet Maven / Gradle / Ivy

There is a newer version: 2.0.31
Show newest version
/*
 * Copyright 2021 Google LLC
 *
 * 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
 *
 *     https://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.apphosting.runtime.jetty.ee8;

import com.google.apphosting.runtime.AppVersion;
import com.google.apphosting.runtime.AppEngineConstants;
import com.google.apphosting.utils.config.AppYaml;
import com.google.common.base.Ascii;
import com.google.common.flogger.GoogleLogger;
import java.io.IOException;
import java.util.Objects;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.ee8.nested.ContextHandler;
import org.eclipse.jetty.ee8.servlet.ServletHandler;
import org.eclipse.jetty.http.pathmap.MappedResource;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;

/**
 * {@code ResourceFileServlet} is a copy of {@code org.mortbay.jetty.servlet.DefaultServlet} that
 * has been trimmed down to only support the subset of features that we want to take advantage of
 * (e.g. no gzipping, no chunked encoding, no buffering, etc.). A number of Jetty-specific
 * optimizations and assumptions have also been removed (e.g. use of custom header manipulation
 * API's, use of {@code ByteArrayBuffer} instead of Strings, etc.).
 *
 * 

A few remaining Jetty-centric details remain, such as use of the {@link * ContextHandler.APIContext} class, and Jetty-specific request attributes, but these are specific * cases where there is no servlet-engine-neutral API available. This class also uses Jetty's {@link * Resource} class as a convenience, but could be converted to use {@link * ServletContext#getResource(String)} instead. * */ public class ResourceFileServlet extends HttpServlet { private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); private Resource resourceBase; private String[] welcomeFiles; private FileSender fSender; ContextHandler chandler; ServletContext context; /** * Initialize the servlet by extracting some useful configuration data from the current {@link * ServletContext}. */ @Override public void init() throws ServletException { context = getServletContext(); AppVersion appVersion = (AppVersion) context.getAttribute(AppEngineConstants.APP_VERSION_CONTEXT_ATTR); chandler = ContextHandler.getContextHandler(context); AppYaml appYaml = (AppYaml) chandler.getServer().getAttribute(AppEngineConstants.APP_YAML_ATTRIBUTE_TARGET); fSender = new FileSender(appYaml); // AFAICT, there is no real API to retrieve this information, so // we access Jetty's internal state. welcomeFiles = chandler.getWelcomeFiles(); try { // TODO: review use of root factory. resourceBase = ResourceFactory.root().newResource(context.getResource("/" + appVersion.getPublicRoot())); } catch (Exception ex) { throw new ServletException(ex); } } /** Retrieve the static resource file indicated. */ @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String servletPath; String pathInfo; boolean included = request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null; if (included) { servletPath = (String) request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); pathInfo = (String) request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO); if (servletPath == null) { servletPath = request.getServletPath(); pathInfo = request.getPathInfo(); } } else { included = Boolean.FALSE; servletPath = request.getServletPath(); pathInfo = request.getPathInfo(); } boolean forwarded = request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI) != null; String pathInContext = URIUtil.addPaths(servletPath, pathInfo); // The servlet spec says "No file contained in the WEB-INF // directory may be served directly a client by the container. // However, ... may be exposed using the RequestDispatcher calls." // Thus, we only allow these requests for includes and forwards. // // TODO: I suspect we should allow error handlers here somehow. if (isProtectedPath(pathInContext) && !included && !forwarded) { response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } if (maybeServeWelcomeFile(pathInContext, included, request, response)) { // We served a welcome file (either via redirecting, forwarding, or including). return; } if (pathInContext.endsWith("/")) { // N.B.: Resource.addPath() trims off trailing // slashes, which may result in us serving files for strange // paths (e.g. "/index.html/"). Since we already took care of // welcome files above, we just return a 404 now if the path // ends with a slash. response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } // RFC 2396 specifies which characters are allowed in URIs: // // http://tools.ietf.org/html/rfc2396#section-2.4.3 // // See also RFC 3986, which specifically mentions handling %00, // which would allow security checks to be bypassed. for (int i = 0; i < pathInContext.length(); i++) { int c = pathInContext.charAt(i); if (c < 0x20 || c == 0x7F) { response.sendError(HttpServletResponse.SC_BAD_REQUEST); logger.atWarning().log( "Attempted to access file containing control character, returning 400."); return; } } // Find the resource Resource resource = null; try { resource = getResource(pathInContext); if (resource == null) { response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } if (StringUtil.endsWithIgnoreCase(resource.getName(), ".jsp")) { // General paranoia: don't ever serve raw .jsp files. response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } // Handle resource if (resource.isDirectory()) { if (included || !fSender.checkIfUnmodified(request, response, resource)) { response.sendError(HttpServletResponse.SC_FORBIDDEN); } } else { if (resource == null || !resource.exists()) { logger.atWarning().log("Non existent resource: %s = %s", pathInContext, resource); response.sendError(HttpServletResponse.SC_NOT_FOUND); } else { if (included || !fSender.checkIfUnmodified(request, response, resource)) { fSender.sendData(context, response, included, resource, request.getRequestURI()); } } } } finally { if (resource != null) { // TODO: do we need to release. // resource.release(); } } } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } @Override protected void doTrace(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } protected boolean isProtectedPath(String target) { target = Ascii.toLowerCase(target); return target.contains("/web-inf/") || target.contains("/meta-inf/"); } /** * Get Resource to serve. * * @param pathInContext The path to find a resource for. * @return The resource to serve. */ private Resource getResource(String pathInContext) { try { if (resourceBase != null) { return resourceBase.resolve(pathInContext); } } catch (Exception ex) { logger.atWarning().withCause(ex).log("Could not find: %s", pathInContext); } return null; } /** * Finds a matching welcome file for the supplied path and, if found, serves it to the user. This * will be the first entry in the list of configured {@link #welcomeFiles welcome files} that * exists within the directory referenced by the path. If the resource is not a directory, or no * matching file is found, then null is returned. The list of welcome files is read * from the {@link ContextHandler} for this servlet, or "index.jsp" , "index.html" if * that is null. * * @return true if a welcome file was served, false otherwise */ private boolean maybeServeWelcomeFile( String path, boolean included, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { if (welcomeFiles == null) { System.err.println("No welcome files"); return false; } // Add a slash for matching purposes. If we needed this slash, we // are not doing an include, and we're not going to redirect // somewhere else we'll redirect the user to add it later. if (!path.endsWith("/")) { path += "/"; } AppVersion appVersion = (AppVersion) getServletContext().getAttribute(AppEngineConstants.APP_VERSION_CONTEXT_ATTR); ServletHandler handler = chandler.getChildHandlerByClass(ServletHandler.class); MappedResource defaultEntry = handler.getHolderEntry("/"); for (String welcomeName : welcomeFiles) { String welcomePath = path + welcomeName; String relativePath = welcomePath.substring(1); if (!Objects.equals(handler.getHolderEntry(welcomePath), defaultEntry)) { // It's a path mapped to a servlet. Forward to it. RequestDispatcher dispatcher = request.getRequestDispatcher(path + welcomeName); return serveWelcomeFileAsForward(dispatcher, included, request, response); } if (appVersion.isResourceFile(relativePath)) { // It's a resource file. Forward to it. RequestDispatcher dispatcher = request.getRequestDispatcher(path + welcomeName); return serveWelcomeFileAsForward(dispatcher, included, request, response); } if (appVersion.isStaticFile(relativePath)) { // It's a static file (served from blobstore). Redirect to it return serveWelcomeFileAsRedirect(path + welcomeName, included, request, response); } } return false; } private boolean serveWelcomeFileAsRedirect( String path, boolean included, HttpServletRequest request, HttpServletResponse response) throws IOException { if (included) { // This is an error. We don't have the file so we can't // include it in the request. return false; } // Even if the trailing slash is missing, don't bother trying to // add it. We're going to redirect to a full file anyway. response.setContentLength(0); String q = request.getQueryString(); if (q != null && q.length() != 0) { response.sendRedirect(path + "?" + q); } else { response.sendRedirect(path); } return true; } private boolean serveWelcomeFileAsForward( RequestDispatcher dispatcher, boolean included, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { // If the user didn't specify a slash but we know we want a // welcome file, redirect them to add the slash now. if (!included && !request.getRequestURI().endsWith("/")) { redirectToAddSlash(request, response); return true; } if (dispatcher != null) { if (included) { dispatcher.include(request, response); } else { dispatcher.forward(request, response); } return true; } return false; } private void redirectToAddSlash(HttpServletRequest request, HttpServletResponse response) throws IOException { StringBuffer buf = request.getRequestURL(); int param = buf.lastIndexOf(";"); if (param < 0) { buf.append('/'); } else { buf.insert(param, '/'); } String q = request.getQueryString(); if (q != null && q.length() != 0) { buf.append('?'); buf.append(q); } response.setContentLength(0); response.sendRedirect(response.encodeRedirectURL(buf.toString())); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy