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

com.google.apphosting.runtime.jetty94.RpcConnection Maven / Gradle / Ivy

/*
 * 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.jetty94;

import com.google.apphosting.base.AppVersionKey;
import com.google.apphosting.base.protos.HttpPb.HttpRequest;
import com.google.apphosting.base.protos.HttpPb.ParsedHttpHeader;
import com.google.apphosting.base.protos.RuntimePb.UPRequest;
import com.google.apphosting.base.protos.RuntimePb.UPResponse;
import com.google.apphosting.runtime.MutableUpResponse;
import com.google.apphosting.runtime.jetty9.JettyConstants;
import com.google.apphosting.runtime.jetty9.RpcEndPoint;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ascii;
import com.google.protobuf.ByteString;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.HttpInput.Content;
import org.eclipse.jetty.server.HttpTransport;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.webapp.WebAppContext;

/**
 * A custom version of HttpConnection that uses UPRequestParser and
 * UPResponseGenerator instead of the standard HTTP stream parser and
 * generator.
 */
public class RpcConnection implements Connection, HttpTransport {

  // This should be kept in sync with HTTPProto::X_GOOGLE_INTERNAL_SKIPADMINCHECK.
  private static final String X_GOOGLE_INTERNAL_SKIPADMINCHECK = "X-Google-Internal-SkipAdminCheck";

  // Keep in sync with com.google.apphosting.utils.jetty.AppEngineAuthentication.
  private static final String SKIP_ADMIN_CHECK_ATTR =
      "com.google.apphosting.internal.SkipAdminCheck";

  static final String ASYNC_ENABLE_PPROPERTY = "com.google.appengine.enable_async";
  static final boolean NORMALIZE_INET_ADDR =
      Boolean.parseBoolean(
          System.getProperty(
              "com.google.appengine.nomalize_inet_addr",
              Boolean.toString(!"java8".equals(System.getenv("GAE_RUNTIME")))));

  private final List listeners = new CopyOnWriteArrayList<>();
  private final RpcConnector connector;
  private final RpcEndPoint endPoint;
  private final MutableUpResponse upResponse;
  private ByteString aggregate = ByteString.EMPTY;
  private volatile Throwable abortedError;

  public RpcConnection(RpcConnector connector, RpcEndPoint endPoint) {
    this.connector = connector;
    this.endPoint = endPoint;
    this.upResponse = endPoint.getUpResponse();
  }

  @Override
  public void addListener(Listener listener) {
    listeners.add(listener);
  }

  @Override
  public void onOpen() {
    for (Listener listener : listeners) {
      listener.onOpened(this);
    }
  }

  @Override
  public void onClose() {
    for (Listener listener : listeners) {
      listener.onClosed(this);
    }
  }

  @Override
  public EndPoint getEndPoint() {
    return endPoint;
  }

  @Override
  public void close() {
    endPoint.close();
  }

  @Override
  public long getMessagesIn() {
    return 1;
  }

  @Override
  public long getMessagesOut() {
    return 1;
  }

  @Override
  public long getBytesIn() {
    return 0;
  }

  @Override
  public long getBytesOut() {
    return 0;
  }

  @Override
  public long getCreatedTimeStamp() {
    return endPoint.getCreatedTimeStamp();
  }

  public void handle(final AppVersionKey appVersionKey) throws ServletException, IOException {

    HttpRequest rpc = endPoint.getUpRequest().getRequest();
    final byte[] postdata = rpc.getPostdata().toByteArray();
    final CountDownLatch blockEndRequest = new CountDownLatch(1);

    HttpChannel channel =
        new HttpChannel(connector, connector.getHttpConfiguration(), endPoint, this) {
          @Override
          protected void handleException(Throwable th) {
            boolean requestWasCommitted = isCommitted();
            super.handleException(th);
            if (requestWasCommitted) {
              // The response was already committed before Jetty handled the exception.
              // In order to preserve Jetty 6 behavior we clear out the
              // attribute that holds the exception to avoid rethrowing it further.
              getRequest().removeAttribute(RequestDispatcher.ERROR_EXCEPTION);
            }
            blockEndRequest.countDown();
          }

          @Override
          protected String formatAddrOrHost(String addr) {
            return NORMALIZE_INET_ADDR ? super.formatAddrOrHost(addr) : addr;
          }

          @Override
          public void onCompleted() {
            super.onCompleted();
            blockEndRequest.countDown();
          }
        };

    Request request = channel.getRequest();

    // Enable async via a property
    request.setAsyncSupported(Boolean.getBoolean(ASYNC_ENABLE_PPROPERTY), null);

    // pretend to parse the request line

    // LEGACY_MODE is case insensitive for known methods
    HttpMethod method = RpcConnector.LEGACY_MODE
            ? HttpMethod.INSENSITIVE_CACHE.get(rpc.getProtocol())
            : HttpMethod.CACHE.get(rpc.getProtocol());
    String methodS = method != null ? method.asString() : rpc.getProtocol();

    try {
      String url = rpc.getUrl();
      HttpURI uri = new HttpURI(url);

      HttpVersion version = HttpVersion.CACHE.getBest(rpc.getHttpVersion());
      MetaData.Request requestData =
          new MetaData.Request(
              methodS, uri, version, new HttpFields(), postdata == null ? -1 : postdata.length);

      // pretend to parse the header fields
      boolean contentLength = false;

      for (ParsedHttpHeader header : rpc.getHeadersList()) {
        HttpField field = getField(header);

        // Handle LegacyMode Headers
        if (RpcConnector.LEGACY_MODE && field.getHeader() != null) {
          switch (field.getHeader()) {
            case CONTENT_ENCODING:
              continue;
            case CONTENT_LENGTH:
              if (contentLength) {
                throw new BadMessageException("Duplicate Content-Length");
              }
              contentLength = true;
              break;
            default:
              break;
          }
        }

        requestData.getFields().add(field);
      }
      // end of headers. This should return true to indicate that we are good to continue handling
      channel.onRequest(requestData);
      // is this SSL
      if (rpc.getIsHttps()) {
        // the following code has to be done after the channel.onRequest(requestData) call
        // to avoid NPE.
        request.setScheme(HttpScheme.HTTPS.asString());
        request.setSecure(true);
      }

      // signal the end of the request
      channel.onRequestComplete();

      // Give the input any post content.
      if (postdata != null) {
        channel.getRequest().getHttpInput().addContent(new Content(BufferUtil.toBuffer(postdata)));
      }
    } catch (Exception t) {
      // Any exception at this stage is most likely due to a bad message
      // We cannot use response.sendError as it needs a validly initiated channel to work.
      upResponse.setHttpResponseCodeAndResponse(400, "");
      channel.getResponse().setStatus(400);
      return;
    }

    // Tell AppVersionHandlerMap which app version should handle this
    // request.
    request.setAttribute(JettyConstants.APP_VERSION_KEY_REQUEST_ATTR, appVersionKey);

    final boolean skipAdmin = hasSkipAdminCheck(endPoint.getUpRequest());
    // Translate the X-Google-Internal-SkipAdminCheck to a servlet attribute.
    if (skipAdmin) {
      request.setAttribute(SKIP_ADMIN_CHECK_ATTR, true);

      // N.B.: If SkipAdminCheck is set, we're actually lying
      // to Jetty here to tell it that HTTPS is in use when it may not
      // be.  This is useful because we want to bypass Jetty's
      // transport-guarantee checks (to match Python, which bypasses
      // handler_security: for these requests), but unlike
      // authentication SecurityHandler does not provide an easy way to
      // plug in custom logic here.  I do not believe that our lie is
      // user-visible (ServletRequest.getProtocol() is unchanged).
      request.setSecure(true);
    }

    Throwable exception = null;
    try {
      // This will invoke a servlet and mutate upResponse before returning.
      channel.handle();
      waitforAsyncDone(blockEndRequest);

      // If an exception occurred while running GenericServlet.service,
      // this attribute will either be thrown or set as an attribute for the WebAppContext's
      // ErrorHandler to be invoked.
      exception = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
    } catch (Exception ex) {
      exception = ex;
    }

    // TODO(b/263341977) this is a correct behavior, but customers depend on this bug, so we
    // enable it only for non java8 runtimes.
    if ((exception == null)
        && (abortedError != null)
        && !"java8".equals(System.getenv("GAE_RUNTIME"))) {
        exception = abortedError;
      }


    if (exception != null) {
      Throwable cause = unwrap(exception);
      if (cause instanceof BadMessageException) {
        // Jetty bad messages exceptions are handled here to prevent
        // 4xx client issues being signalled as 5xx server issues
        BadMessageException bme = (BadMessageException) cause;
        upResponse.clearHttpResponse();
        upResponse.setError(UPResponse.ERROR.OK_VALUE);
        upResponse.setHttpResponseCode(bme.getCode());
        upResponse.setErrorMessage(bme.getReason());
      } else if (!hasExceptionHandledByErrorPage(request)) {
        // We will most likely have set something here, but the
        // AppServer will only do the right thing (print stack traces
        // for admins, generic Prometheus error message others) if this
        // is completely unset.
        upResponse.clearHttpResponse();

        if (exception instanceof ServletException) {
          throw (ServletException) exception;
        } else {
          throw new ServletException(exception);
        }
      }
    }
  }

  private static Throwable unwrap(Throwable th) {
    while (th instanceof ServletException && th.getCause() != null) {
      th = th.getCause();
    }
    return th;
  }

  private static void waitforAsyncDone(CountDownLatch blockEndRequest) {
    if (Boolean.getBoolean(ASYNC_ENABLE_PPROPERTY)) {
      try {
        /**
         * Note: such a wait means that the container is not really running in asynchronous mode as
         * a thread will be held while waiting.
         *
         * 

If the application is using the async APIs for async IO, this will not be a big issue * as the request is fully available before handle and the response is entirely buffered. * Thus an application will never actually block on IO as data will always be available and * writes will always be possible. * *

However, if the application is going async to wait for a backend * server/database/webservice/etc. then the intention of using async is so that the thread * assigned can be used for other useful work. This wait will prevent this. Thus an feature * request could be opened on the RPC layer to provide a real async invocation mode, so that * this wait is not necessary. */ blockEndRequest.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } /** * Returns true if the X-Google-Internal-SkipAdminCheck header is * present. This header is passed via the set of protected headers * that is made available to the runtime but not to user code. Note * that like the AppServer code, we only check if the header is * present and not the value itself. */ private static boolean hasSkipAdminCheck(UPRequest upRequest) { for (ParsedHttpHeader header : upRequest.getRuntimeHeadersList()) { if (Ascii.equalsIgnoreCase(X_GOOGLE_INTERNAL_SKIPADMINCHECK, header.getKey())) { return true; } } return false; } // Transform ParsedHttpHeader to HttpField. @VisibleForTesting static HttpField getField(ParsedHttpHeader header) { return new HttpField(header.getKey(), header.getValue()); } /** * Check if the exception has been explicitly handled by an "error" page of the webapp. * * @return true iff the exception has already been handled by the "current" error page. */ private static boolean hasExceptionHandledByErrorPage(Request servletRequest) { Object errorPage = servletRequest.getAttribute(WebAppContext.ERROR_PAGE); Object errorPageHandled = servletRequest.getAttribute(AppVersionHandlerFactory.ERROR_PAGE_HANDLED); return errorPage != null && errorPage.equals(errorPageHandled); } @Override public void send( MetaData.Response info, boolean head, ByteBuffer content, boolean lastContent, Callback callback) { if (info != null) { upResponse.setHttpResponseCode(info.getStatus()); for (HttpField field : info.getFields()) { upResponse.addHttpOutputHeaders(ParsedHttpHeader.newBuilder() .setKey(field.getName()) .setValue(field.getValue())); } } if (BufferUtil.hasContent(content)) { aggregate = aggregate.concat(ByteString.copyFrom(content)); } callback.succeeded(); } @Override public void onCompleted() { upResponse.setHttpResponseResponse(aggregate); aggregate = ByteString.EMPTY; } @Override public void abort(Throwable t) { abortedError = t; endPoint.close(); } @Override public boolean isPushSupported() { return false; } @Override public void push(MetaData.Request rqst) { throw new UnsupportedOperationException("Not supported yet."); } @Override public boolean isOptimizedForDirectBuffers() { return false; } @Override public boolean onIdleExpired() { return false; } @Override public void removeListener(Listener ll) {} }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy