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

com.google.apphosting.runtime.jetty.JettyServletEngineAdapter Maven / Gradle / Ivy

There is a newer version: 2.0.32
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;

import static com.google.apphosting.runtime.AppEngineConstants.HTTP_CONNECTOR_MODE;
import static com.google.apphosting.runtime.AppEngineConstants.IGNORE_RESPONSE_SIZE_LIMIT;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.base.AppVersionKey;
import com.google.apphosting.base.protos.AppinfoPb;
import com.google.apphosting.base.protos.EmptyMessage;
import com.google.apphosting.base.protos.RuntimePb.UPRequest;
import com.google.apphosting.base.protos.RuntimePb.UPResponse;
import com.google.apphosting.runtime.AppEngineConstants;
import com.google.apphosting.runtime.AppVersion;
import com.google.apphosting.runtime.LocalRpcContext;
import com.google.apphosting.runtime.MutableUpResponse;
import com.google.apphosting.runtime.ServletEngineAdapter;
import com.google.apphosting.runtime.anyrpc.EvaluationRuntimeServerInterface;
import com.google.apphosting.runtime.jetty.delegate.DelegateConnector;
import com.google.apphosting.runtime.jetty.delegate.impl.DelegateRpcExchange;
import com.google.apphosting.runtime.jetty.http.JettyHttpHandler;
import com.google.apphosting.runtime.jetty.proxy.JettyHttpProxy;
import com.google.apphosting.utils.config.AppEngineConfigException;
import com.google.apphosting.utils.config.AppYaml;
import com.google.common.flogger.GoogleLogger;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStreamReader;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import org.eclipse.jetty.http.CookieCompliance;
import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.VirtualThreads;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;

/**
 * This is an implementation of ServletEngineAdapter that uses the third-party Jetty servlet engine.
 */
public class JettyServletEngineAdapter implements ServletEngineAdapter {
  private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
  private static final String DEFAULT_APP_YAML_PATH = "/WEB-INF/appengine-generated/app.yaml";
  private static final int MIN_THREAD_POOL_THREADS = 0;
  private static final int MAX_THREAD_POOL_THREADS = 100;
  private static final long MAX_RESPONSE_SIZE = 32 * 1024 * 1024;

  /**
   * If Legacy Mode is turned on, then Jetty is configured to be more forgiving of bad requests and
   * to act more in the style of Jetty-9.3
   */
  public static final boolean LEGACY_MODE =
      Boolean.getBoolean("com.google.apphosting.runtime.jetty94.LEGACY_MODE");

  private AppVersionKey lastAppVersionKey;

  static {
    // Set legacy system property to dummy value because external libraries
    // (google-auth-library-java)
    // test if this value is null to decide whether it is Java 7 runtime.
    System.setProperty("org.eclipse.jetty.util.log.class", "DEPRECATED");
  }

  private Server server;
  private DelegateConnector rpcConnector;
  private AppVersionHandler appVersionHandler;

  public JettyServletEngineAdapter() {}

  private static AppYaml getAppYaml(ServletEngineAdapter.Config runtimeOptions) {
    String applicationPath = runtimeOptions.fixedApplicationPath();
    File appYamlFile = new File(applicationPath + DEFAULT_APP_YAML_PATH);
    AppYaml appYaml = null;
    try {
      appYaml = AppYaml.parse(new InputStreamReader(new FileInputStream(appYamlFile), UTF_8));
    } catch (FileNotFoundException | AppEngineConfigException e) {
      logger.atWarning().log(
          "Failed to load app.yaml file at location %s - %s",
          appYamlFile.getPath(), e.getMessage());
    }
    return appYaml;
  }

  @Override
  public void start(String serverInfo, ServletEngineAdapter.Config runtimeOptions) {
    QueuedThreadPool threadPool =
        new QueuedThreadPool(MAX_THREAD_POOL_THREADS, MIN_THREAD_POOL_THREADS);
    // Try to enable virtual threads if requested and on java21:
    if (Boolean.getBoolean("appengine.use.virtualthreads")
        && "java21".equals(System.getenv("GAE_RUNTIME"))) {
      threadPool.setVirtualThreadsExecutor(VirtualThreads.getDefaultVirtualThreadsExecutor());
      logger.atInfo().log("Configuring Appengine web server virtual threads.");
    }

    server =
        new Server(threadPool) {
          @Override
          public InvocationType getInvocationType() {
            return InvocationType.BLOCKING;
          }
        };
    rpcConnector =
        new DelegateConnector(server, "RPC") {
          @Override
          public void run(Runnable runnable) {
            // Override this so that it does the initial run in the same thread.
            // Currently, we block until completion in serviceRequest() so no point starting new
            // thread.
            runnable.run();
          }
        };
    server.addConnector(rpcConnector);
    AppVersionHandlerFactory appVersionHandlerFactory =
        AppVersionHandlerFactory.newInstance(server, serverInfo);
    appVersionHandler = new AppVersionHandler(appVersionHandlerFactory);
    if (!Boolean.getBoolean(IGNORE_RESPONSE_SIZE_LIMIT)) {
      CoreSizeLimitHandler sizeLimitHandler = new CoreSizeLimitHandler(-1, MAX_RESPONSE_SIZE);
      sizeLimitHandler.setHandler(appVersionHandler);
      server.setHandler(sizeLimitHandler);
    } else {
      server.setHandler(appVersionHandler);
    }
    boolean startJettyHttpProxy = false;
    if (runtimeOptions.useJettyHttpProxy()) {
      AppInfoFactory appInfoFactory;
      AppVersionKey appVersionKey;
      /* The init actions are not done in the constructor as they are not used when testing */
      try {
        String appRoot = runtimeOptions.applicationRoot();
        String appPath = runtimeOptions.fixedApplicationPath();
        appInfoFactory = new AppInfoFactory(System.getenv());
        AppinfoPb.AppInfo appinfo = appInfoFactory.getAppInfoFromFile(appRoot, appPath);
        // TODO Should we also call ApplyCloneSettings()?
        LocalRpcContext context = new LocalRpcContext<>(EmptyMessage.class);
        EvaluationRuntimeServerInterface evaluationRuntimeServerInterface =
            Objects.requireNonNull(runtimeOptions.evaluationRuntimeServerInterface());
        evaluationRuntimeServerInterface.addAppVersion(context, appinfo);
        context.getResponse();
        appVersionKey = AppVersionKey.fromAppInfo(appinfo);
      } catch (Exception e) {
        throw new IllegalStateException(e);
      }
      if (Boolean.getBoolean(HTTP_CONNECTOR_MODE)) {
        logger.atInfo().log("Using HTTP_CONNECTOR_MODE to bypass RPC");
        JettyHttpProxy.insertHandlers(server);
        server.insertHandler(
            new JettyHttpHandler(
                runtimeOptions, appVersionHandler.getAppVersion(), appVersionKey, appInfoFactory));
        ServerConnector connector = JettyHttpProxy.newConnector(server, runtimeOptions);
        server.addConnector(connector);
      } else {
        server.setAttribute(
            "com.google.apphosting.runtime.jetty.appYaml",
            JettyServletEngineAdapter.getAppYaml(runtimeOptions));
        // Delay start of JettyHttpProxy until after the main server and application is started.
        startJettyHttpProxy = true;
      }
    }
    try {
      server.start();
      if (startJettyHttpProxy) {
        JettyHttpProxy.startServer(runtimeOptions);
      }
    } catch (Exception ex) {
      // TODO: Should we have a wrapper exception for this
      // type of thing in ServletEngineAdapter?
      throw new RuntimeException(ex);
    }
  }

  @Override
  public void stop() {
    try {
      server.stop();
    } catch (Exception ex) {
      throw new RuntimeException(ex);
    }
  }

  @Override
  public void addAppVersion(AppVersion appVersion) throws FileNotFoundException {
    appVersionHandler.addAppVersion(appVersion);
  }

  @Override
  public void deleteAppVersion(AppVersion appVersion) {
    appVersionHandler.removeAppVersion(appVersion.getKey());
  }

  @Override
  public void setSessionStoreFactory(com.google.apphosting.runtime.SessionStoreFactory factory) {
    // No op with the new Jetty Session management.
  }

  @Override
  public void serviceRequest(UPRequest upRequest, MutableUpResponse upResponse) throws Exception {
    if (upRequest.getHandler().getType() != AppinfoPb.Handler.HANDLERTYPE.CGI_BIN_VALUE) {
      upResponse.setError(UPResponse.ERROR.UNKNOWN_HANDLER_VALUE);
      upResponse.setErrorMessage("Unsupported handler type: " + upRequest.getHandler().getType());
      return;
    }
    // Optimise this adaptor assuming one deployed appVersionKey, so use the last one if it matches
    // and only check the handler is available if we see a new/different key.
    AppVersionKey appVersionKey = AppVersionKey.fromUpRequest(upRequest);
    AppVersionKey lastVersionKey = lastAppVersionKey;
    if (lastVersionKey != null) {
      // We already have created the handler on the previous request, so no need to do another
      // getHandler().
      // The two AppVersionKeys must be the same as we only support one app version.
      if (!Objects.equals(appVersionKey, lastVersionKey)) {
        upResponse.setError(UPResponse.ERROR.UNKNOWN_APP_VALUE);
        upResponse.setErrorMessage("Unknown app: " + appVersionKey);
        return;
      }
    } else {
      if (!appVersionHandler.ensureHandler(appVersionKey)) {
        upResponse.setError(UPResponse.ERROR.UNKNOWN_APP_VALUE);
        upResponse.setErrorMessage("Unknown app: " + appVersionKey);
        return;
      }
      lastAppVersionKey = appVersionKey;
    }
    // TODO: lots of compliance modes to handle.
    HttpConfiguration httpConfiguration = rpcConnector.getHttpConfiguration();
    httpConfiguration.setSendDateHeader(false);
    httpConfiguration.setSendServerVersion(false);
    httpConfiguration.setSendXPoweredBy(false);
    if (LEGACY_MODE) {
      httpConfiguration.setRequestCookieCompliance(CookieCompliance.RFC2965);
      httpConfiguration.setResponseCookieCompliance(CookieCompliance.RFC2965);
      httpConfiguration.setUriCompliance(UriCompliance.LEGACY);
    }
    DelegateRpcExchange rpcExchange = new DelegateRpcExchange(upRequest, upResponse);
    rpcExchange.setAttribute(AppEngineConstants.APP_VERSION_KEY_REQUEST_ATTR, appVersionKey);
    rpcExchange.setAttribute(AppEngineConstants.ENVIRONMENT_ATTR, ApiProxy.getCurrentEnvironment());
    rpcConnector.service(rpcExchange);
    try {
      rpcExchange.awaitResponse();
    } catch (Throwable t) {
      Throwable error = t;
      if (error instanceof ExecutionException) {
        error = error.getCause();
      }
      upResponse.setError(UPResponse.ERROR.UNEXPECTED_ERROR_VALUE);
      upResponse.setErrorMessage("Unexpected Error: " + error);
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy