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

com.google.apphosting.runtime.jetty.ee10.AppEngineWebAppContext 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.ee10;

import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.api.ApiProxy.LogRecord;
import com.google.apphosting.runtime.jetty.EE10AppEngineAuthentication;
import com.google.apphosting.utils.servlet.ee10.DeferredTaskServlet;
import com.google.apphosting.utils.servlet.ee10.JdbcMySqlConnectionCleanupFilter;
import com.google.apphosting.utils.servlet.ee10.SessionCleanupServlet;
import com.google.apphosting.utils.servlet.ee10.SnapshotServlet;
import com.google.apphosting.utils.servlet.ee10.WarmupServlet;
import com.google.common.collect.ImmutableSet;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;
import jakarta.servlet.Servlet;
import org.eclipse.jetty.ee10.servlet.FilterHolder;
import org.eclipse.jetty.ee10.servlet.FilterMapping;
import org.eclipse.jetty.ee10.servlet.Holder;
import org.eclipse.jetty.ee10.servlet.ListenerHolder;
import org.eclipse.jetty.ee10.servlet.ServletHandler;
import org.eclipse.jetty.ee10.servlet.ServletHolder;
import org.eclipse.jetty.ee10.servlet.ServletMapping;
import org.eclipse.jetty.ee10.servlet.security.ConstraintMapping;
import org.eclipse.jetty.ee10.servlet.security.ConstraintSecurityHandler;
import org.eclipse.jetty.ee10.webapp.WebAppContext;
import org.eclipse.jetty.security.Constraint;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.EventListener;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;

import static com.google.common.base.StandardSystemProperty.JAVA_IO_TMPDIR;
import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * {@code AppEngineWebAppContext} is a customization of Jetty's {@link WebAppContext} that is aware
 * of the {@link ApiProxy} and can provide custom logging and authentication.
 */
// This class is different than the one for Jetty 9.3 as it the new way we want to use only
// for Jetty 9.4 to define the default servlets and filters, outside of webdefault.xml. Doing so
// will allow to enable Servlet Async capabilities later, controlled programmatically instead of
// declaratively in webdefault.xml.
public class AppEngineWebAppContext extends WebAppContext {

  // TODO: This should be some sort of Prometheus-wide
  // constant.  If it's much larger than this we may need to
  // restructure the code a bit.
  private static final int MAX_RESPONSE_SIZE = 32 * 1024 * 1024;
  private static final String ASYNC_ENABLE_PROPERTY = "enable_async_PROPERTY"; // TODO
  private static final boolean APP_IS_ASYNC = Boolean.getBoolean(ASYNC_ENABLE_PROPERTY);

  private static final String JETTY_PACKAGE = "org.eclipse.jetty.";

  // The optional file path that contains AppIds that need to ignore content length for response.
  private static final String IGNORE_CONTENT_LENGTH =
      "/base/java8_runtime/appengine.ignore-content-length";

  private final String serverInfo;
  private final List requestListeners = new CopyOnWriteArrayList<>();
  private final boolean ignoreContentLength;

  // These are deprecated filters and servlets
  private static final ImmutableSet DEPRECATED_SERVLETS_FILTERS =
      ImmutableSet.of(
          // Remove unused filters that may still be instantiated by
          // deprecated webdefault.xml in old SDKs
          new HolderMatcher(
              "AbandonedTransactionDetector",
              "com.google.apphosting.utils.servlet.TransactionCleanupFilter"),
          new HolderMatcher(
              "SaveSessionFilter", "com.google.apphosting.runtime.jetty.SaveSessionFilter"),
          new HolderMatcher(
              "_ah_ParseBlobUploadFilter",
              "com.google.apphosting.utils.servlet.ParseBlobUploadFilter"),
          new HolderMatcher(
              "_ah_default", "com.google.apphosting.runtime.jetty.ResourceFileServlet"),
          new HolderMatcher(
              "default", "com.google.apphosting.runtime.jetty.ee10.NamedDefaultServlet"),
          new HolderMatcher("jsp", "com.google.apphosting.runtime.jetty.NoJspSerlvet"),

          // remove application filters and servlets that are known to only be applicable to
          // the java 7 runtime
          new HolderMatcher(null, "com.google.appengine.tools.appstats.AppstatsFilter"),
          new HolderMatcher(null, "com.google.appengine.tools.appstats.AppstatsServlet"));

  @Override
  public boolean checkAlias(String path, Resource resource) {
    return true;
  }

  public AppEngineWebAppContext(File appDir, String serverInfo) {
    this(appDir, serverInfo, /*extractWar=*/ true);
  }

  public AppEngineWebAppContext(File appDir, String serverInfo, boolean extractWar) {
    // We set the contextPath to / for all applications.
    super(appDir.getPath(), "/");

    // If the application fails to start, we throw so the JVM can exit.
    setThrowUnavailableOnStartupException(true);

    if (extractWar) {
      Resource webApp;
      try {
        ResourceFactory resourceFactory = ResourceFactory.of(this);
        webApp = resourceFactory.newResource(appDir.getAbsolutePath());

        if (appDir.isDirectory()) {
          setWar(appDir.getPath());
          setBaseResource(webApp);
        } else {
          // Real war file, not exploded , so we explode it in tmp area.
          createTempDirectory();
          File extractedWebAppDir = getTempDirectory();
          Resource jarWebWpp = resourceFactory.newJarFileResource(webApp.getURI());
          jarWebWpp.copyTo(extractedWebAppDir.toPath());
          setBaseResource(resourceFactory.newResource(extractedWebAppDir.getAbsolutePath()));
          setWar(extractedWebAppDir.getPath());
        }
      } catch (Exception e) {
        throw new IllegalStateException("cannot create AppEngineWebAppContext:", e);
      }
    } else {
      // Let Jetty serve directly from the war file (or directory, if it's already extracted):
      setWar(appDir.getPath());
    }

    this.serverInfo = serverInfo;

    // Configure the Jetty SecurityHandler to understand our method of
    // authentication (via the UserService).
    setSecurityHandler(EE10AppEngineAuthentication.newSecurityHandler());

    setMaxFormContentSize(MAX_RESPONSE_SIZE);

    // TODO: Can we change to a jetty-core handler? what to do on ASYNC?
    addFilter(new ParseBlobUploadFilter(), "/*", EnumSet.of(DispatcherType.REQUEST));
    ignoreContentLength = isAppIdForNonContentLength();
  }

  @Override
  protected ClassLoader configureClassLoader(ClassLoader loader) {
    // Avoid wrapping the provided classloader with WebAppClassLoader.
    return loader;
  }

  @Override
  public ServletContextApi newServletContextApi() {
    /* TODO only does this for logging?
    // Override the default HttpServletContext implementation.
    // TODO: maybe not needed when there is no securrity manager.
    // see
    // https://github.com/GoogleCloudPlatform/appengine-java-vm-runtime/commit/43c37fd039fb619608cfffdc5461ecddb4d90ebc
    _scontext = new AppEngineServletContext();
    */

    return super.newServletContextApi();
  }

  private static boolean isAppIdForNonContentLength() {
    String projectId = System.getenv("GOOGLE_CLOUD_PROJECT");
    if (projectId == null) {
      return false;
    }
    try (Scanner s = new Scanner(new File(IGNORE_CONTENT_LENGTH), UTF_8.name())) {
      while (s.hasNext()) {
        if (projectId.equals(s.next())) {
          return true;
        }
      }
    } catch (FileNotFoundException ignore) {
      return false;
    }
    return false;
  }

  @Override
  public boolean addEventListener(EventListener listener) {
    if (super.addEventListener(listener)) {
        if (listener instanceof RequestListener) {
            requestListeners.add((RequestListener)listener);
        }
        return true;
    }
    return false;
  }

  @Override
  public boolean removeEventListener(EventListener listener) {
    if (super.removeEventListener(listener)) {
        if (listener instanceof RequestListener) {
            requestListeners.remove((RequestListener)listener);
        }
        return true;
    }
    return false;
  }

  @Override
  public void doStart() throws Exception {
    super.doStart();
    addEventListener(new TransactionCleanupListener(getClassLoader()));
  }

  @Override
  protected void startWebapp() throws Exception {
    // startWebapp is called after the web.xml metadata has been resolved, so we can
    // clean configuration here:
    //  - Removed deprecated filters and servlets
    //  - Ensure known runtime filters/servlets are instantiated from this classloader
    //  - Ensure known runtime mappings exist.
    ServletHandler servletHandler = getServletHandler();
    TrimmedFilters trimmedFilters =
            new TrimmedFilters(
                    servletHandler.getFilters(),
                    servletHandler.getFilterMappings(),
                    DEPRECATED_SERVLETS_FILTERS);
    trimmedFilters.ensure(
            "CloudSqlConnectionCleanupFilter", JdbcMySqlConnectionCleanupFilter.class, "/*");

    TrimmedServlets trimmedServlets =
            new TrimmedServlets(
                    servletHandler.getServlets(),
                    servletHandler.getServletMappings(),
                    DEPRECATED_SERVLETS_FILTERS);
    trimmedServlets.ensure("_ah_warmup", WarmupServlet.class, "/_ah/warmup");
    trimmedServlets.ensure(
            "_ah_sessioncleanup", SessionCleanupServlet.class, "/_ah/sessioncleanup");
    trimmedServlets.ensure(
            "_ah_queue_deferred", DeferredTaskServlet.class, "/_ah/queue/__deferred__");
    trimmedServlets.ensure("_ah_snapshot", SnapshotServlet.class, "/_ah/snapshot");
    trimmedServlets.ensure("_ah_default", ResourceFileServlet.class, "/");
    trimmedServlets.ensure("default", NamedDefaultServlet.class);
    trimmedServlets.ensure("jsp", NamedJspServlet.class);

    trimmedServlets.instantiateJettyServlets();
    trimmedFilters.instantiateJettyFilters();
    instantiateJettyListeners();

    servletHandler.setFilters(trimmedFilters.getHolders());
    servletHandler.setFilterMappings(trimmedFilters.getMappings());
    servletHandler.setServlets(trimmedServlets.getHolders());
    servletHandler.setServletMappings(trimmedServlets.getMappings());
    servletHandler.setAllowDuplicateMappings(true);

    // Protect deferred task queue with constraint
    ConstraintSecurityHandler security = (ConstraintSecurityHandler) getSecurityHandler();
    ConstraintMapping cm = new ConstraintMapping();
    cm.setConstraint(
            Constraint.from("deferred_queue", Constraint.Authorization.SPECIFIC_ROLE, "admin"));
    cm.setPathSpec("/_ah/queue/__deferred__");
    security.addConstraintMapping(cm);

    // continue starting the webapp
    super.startWebapp();
  }

  @Override
  public boolean handle(Request request, Response response, Callback callback) throws Exception {
    ListIterator iter = requestListeners.listIterator();
    while (iter.hasNext()) {
      iter.next().requestReceived(this, request);
    }
    try {
      if (ignoreContentLength) {
        response = new IgnoreContentLengthResponseWrapper(request, response);
      }

      return super.handle(request, response, callback);
    } finally {
      // TODO: this finally approach is ok until async request handling is supported
      while (iter.hasPrevious()) {
        iter.previous().requestComplete(this, request);
      }
    }
  }

  @Override
  protected ServletHandler newServletHandler() {
    ServletHandler handler = new ServletHandler();
    handler.setAllowDuplicateMappings(true);
    return handler;
  }

  /* Instantiate any jetty listeners from the container classloader */
  private void instantiateJettyListeners() throws ReflectiveOperationException {
    ListenerHolder[] listeners = getServletHandler().getListeners();
    if (listeners != null) {
      for (ListenerHolder h : listeners) {
        if (h.getClassName().startsWith(JETTY_PACKAGE)) {
          Class listener =
              ServletHandler.class
                  .getClassLoader()
                  .loadClass(h.getClassName())
                  .asSubclass(EventListener.class);
          h.setListener(listener.getConstructor().newInstance());
        }
      }
    }
  }

  @Override
  protected void createTempDirectory() {
    File tempDir = getTempDirectory();
    if (tempDir != null) {
      // Someone has already set the temp directory.
      super.createTempDirectory();
      return;
    }

    File baseDir = new File(Objects.requireNonNull(JAVA_IO_TMPDIR.value()));
    String baseName = System.currentTimeMillis() + "-";

    for (int counter = 0; counter < 10; counter++) {
      tempDir = new File(baseDir, baseName + counter);
      if (tempDir.mkdir()) {
        if (!isTempDirectoryPersistent()) {
          tempDir.deleteOnExit();
        }

        setTempDirectory(tempDir);
        return;
      }
    }
    throw new IllegalStateException("Failed to create directory ");
  }

  // N.B.: Yuck.  Jetty hardcodes all of this logic into an
  // inner class of ContextHandler.  We need to subclass WebAppContext
  // (which extends ContextHandler) and then subclass the SContext
  // inner class to modify its behavior.

  /** A context that uses our logs API to log messages. */
  public class AppEngineServletContext extends ServletContextApi {

    @Override
    public ClassLoader getClassLoader() {
      return AppEngineWebAppContext.this.getClassLoader();
    }

    @Override
    public String getServerInfo() {
      return serverInfo;
    }

    @Override
    public void log(String message) {
      log(message, null);
    }

    /**
     * {@inheritDoc}
     *
     * @param throwable an exception associated with this log message, or {@code null}.
     */
    @Override
    public void log(String message, Throwable throwable) {
      StringWriter writer = new StringWriter();
      writer.append("javax.servlet.ServletContext log: ");
      writer.append(message);

      if (throwable != null) {
        writer.append("\n");
        throwable.printStackTrace(new PrintWriter(writer));
      }

      LogRecord.Level logLevel = throwable == null ? LogRecord.Level.info : LogRecord.Level.error;
      ApiProxy.log(
          new ApiProxy.LogRecord(logLevel, System.currentTimeMillis() * 1000L, writer.toString()));
    }
  }

  /** A class to hold a Holder name and/or className and/or source location for matching. */
  private static class HolderMatcher {
    final String name;
    final String className;

    /**
     * @param name The name of a filter/servlet to match, or null if not matching on name.
     * @param className The class name of a filter/servlet to match, or null if not matching on
     *     className
     */
    HolderMatcher(String name, String className) {
      this.name = name;
      this.className = className;
    }

    /**
     * @param holder The holder to match
     * @return true IFF this matcher matches the holder.
     */
    boolean appliesTo(Holder holder) {
      if (name != null && !name.equals(holder.getName())) {
        return false;
      }

      if (className != null && !className.equals(holder.getClassName())) {
        return false;
      }

      return true;
    }
  }

  /**
   * TrimmedServlets is in charge of handling web applications that got deployed previously with the
   * previous webdefault.xml content(prior to this CL changing it). We still need to be able to load
   * old apps defined with the previous webdefault.xml file that generated an obsolete
   * quickstart.xml having the servlets defined in webdefault.xml.
   *
   * 

New deployements would not need this processing (no-op), but we need to handle all apps, * deployed now or in the past. */ private static class TrimmedServlets { private final Map holders = new HashMap<>(); private final List mappings = new ArrayList<>(); TrimmedServlets( ServletHolder[] holders, ServletMapping[] mappings, Set deprecations) { for (ServletHolder servletHolder : holders) { boolean deprecated = false; servletHolder.setAsyncSupported(APP_IS_ASYNC); for (HolderMatcher holderMatcher : deprecations) { deprecated |= holderMatcher.appliesTo(servletHolder); } if (!deprecated) { this.holders.put(servletHolder.getName(), servletHolder); } } for (ServletMapping m : mappings) { this.mappings.add(m); } } /** * Ensure the registration of a container provided servlet: * *

    *
  • If any existing servlet registrations are for the passed servlet class, then their * holder is updated with a new instance created on the containers classpath. *
  • If a servlet registration for the passed servlet name does not exist, one is created to * the passed servlet class. *
* * @param name The servlet name * @param servlet The servlet class * @throws ReflectiveOperationException If a new instance of the servlet cannot be instantiated */ void ensure(String name, Class servlet) throws ReflectiveOperationException { // Instantiate any holders referencing this servlet (may be application instances) for (ServletHolder h : holders.values()) { if (servlet.getName().equals(h.getClassName())) { h.setServlet(servlet.getConstructor().newInstance()); h.setAsyncSupported(APP_IS_ASYNC); } } // Look for (or instantiate) our named instance ServletHolder holder = holders.get(name); if (holder == null) { holder = new ServletHolder(servlet.getConstructor().newInstance()); holder.setInitOrder(1); holder.setName(name); holder.setAsyncSupported(APP_IS_ASYNC); holders.put(name, holder); } } /** * Ensure the registration of a container provided servlet: * *
    *
  • If any existing servlet registrations are for the passed servlet class, then their * holder is updated with a new instance created on the containers classpath. *
  • If a servlet registration for the passed servlet name does not exist, one is created to * the passed servlet class. *
  • If a servlet mapping for the passed servlet name and pathSpec does not exist, one is * created. *
* * @param name The servlet name * @param servlet The servlet class * @param pathSpec The servlet pathspec * @throws ReflectiveOperationException If a new instance of the servlet cannot be instantiated */ void ensure(String name, Class servlet, String pathSpec) throws ReflectiveOperationException { // Ensure Servlet ensure(name, servlet); // Ensure mapping if (pathSpec != null) { boolean mapped = false; for (ServletMapping mapping : mappings) { if (mapping.containsPathSpec(pathSpec)) { mapped = true; break; } } if (!mapped) { ServletMapping mapping = new ServletMapping(); mapping.setServletName(name); mapping.setPathSpec(pathSpec); if (pathSpec.equals("/")) { mapping.setFromDefaultDescriptor(true); } mappings.add(mapping); } } } /** * Instantiate any registrations of a jetty provided servlet * * @throws ReflectiveOperationException If a new instance of the servlet cannot be instantiated */ void instantiateJettyServlets() throws ReflectiveOperationException { for (ServletHolder h : holders.values()) { if (h.getClassName() != null && h.getClassName().startsWith(JETTY_PACKAGE)) { Class servlet = ServletHolder.class .getClassLoader() .loadClass(h.getClassName()) .asSubclass(Servlet.class); h.setServlet(servlet.getConstructor().newInstance()); } } } ServletHolder[] getHolders() { return holders.values().toArray(new ServletHolder[0]); } ServletMapping[] getMappings() { List trimmed = new ArrayList<>(mappings.size()); for (ServletMapping m : mappings) { if (this.holders.containsKey(m.getServletName())) { trimmed.add(m); } } return trimmed.toArray(new ServletMapping[0]); } } private static class TrimmedFilters { private final Map holders = new HashMap<>(); private final List mappings = new ArrayList<>(); TrimmedFilters( FilterHolder[] holders, FilterMapping[] mappings, Set deprecations) { for (FilterHolder h : holders) { boolean deprecated = false; h.setAsyncSupported(APP_IS_ASYNC); for (HolderMatcher m : deprecations) { deprecated |= m.appliesTo(h); } if (!deprecated) { this.holders.put(h.getName(), h); } } for (FilterMapping m : mappings) { this.mappings.add(m); } } /** * Ensure the registration of a container provided filter: * *
    *
  • If any existing filter registrations are for the passed filter class, then their holder * is updated with a new instance created on the containers classpath. *
  • If a filter registration for the passed filter name does not exist, one is created to * the passed filter class. *
  • If a filter mapping for the passed filter name and pathSpec does not exist, one is * created. *
* * @param name The filter name * @param filter The filter class * @param pathSpec The servlet pathspec * @throws ReflectiveOperationException If a new instance of the servlet cannot be instantiated */ void ensure(String name, Class filter, String pathSpec) throws Exception { // Instantiate any holders referencing this filter (may be application instances) for (FilterHolder h : holders.values()) { if (filter.getName().equals(h.getClassName())) { h.setFilter(filter.getConstructor().newInstance()); h.setAsyncSupported(APP_IS_ASYNC); } } // Look for (or instantiate) our named instance FilterHolder holder = holders.get(name); if (holder == null) { holder = new FilterHolder(filter.getConstructor().newInstance()); holder.setName(name); holders.put(name, holder); holder.setAsyncSupported(APP_IS_ASYNC); } // Ensure mapping boolean mapped = false; for (FilterMapping mapping : mappings) { for (String ps : mapping.getPathSpecs()) { if (pathSpec.equals(ps) && name.equals(mapping.getFilterName())) { mapped = true; break; } } } if (!mapped) { FilterMapping mapping = new FilterMapping(); mapping.setFilterName(name); mapping.setPathSpec(pathSpec); mapping.setDispatches(FilterMapping.REQUEST); mappings.add(mapping); } } /** * Instantiate any registrations of a jetty provided filter * * @throws ReflectiveOperationException If a new instance of the filter cannot be instantiated */ void instantiateJettyFilters() throws ReflectiveOperationException { for (FilterHolder h : holders.values()) { if (h.getClassName().startsWith(JETTY_PACKAGE)) { Class filter = ServletHolder.class .getClassLoader() .loadClass(h.getClassName()) .asSubclass(Filter.class); h.setFilter(filter.getConstructor().newInstance()); } } } FilterHolder[] getHolders() { return holders.values().toArray(new FilterHolder[0]); } FilterMapping[] getMappings() { List trimmed = new ArrayList<>(mappings.size()); for (FilterMapping m : mappings) { if (this.holders.containsKey(m.getFilterName())) { trimmed.add(m); } } return trimmed.toArray(new FilterMapping[0]); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy