/**
* Copyright 2015 Google Inc. 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 com.google.apphosting.vmruntime.jetty9;
import com.google.appengine.api.memcache.MemcacheSerialization;
import com.google.appengine.spi.ServiceFactoryFactory;
import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.api.ApiProxy.LogRecord;
import com.google.apphosting.runtime.DatastoreSessionStore;
import com.google.apphosting.runtime.DeferredDatastoreSessionStore;
import com.google.apphosting.runtime.MemcacheSessionStore;
import com.google.apphosting.runtime.SessionStore;
import com.google.apphosting.runtime.jetty9.SessionManager;
import com.google.apphosting.runtime.timer.Timer;
import com.google.apphosting.utils.config.AppEngineConfigException;
import com.google.apphosting.utils.config.AppEngineWebXml;
import com.google.apphosting.utils.config.AppEngineWebXmlReader;
import com.google.apphosting.utils.http.HttpRequest;
import com.google.apphosting.utils.http.HttpResponse;
import com.google.apphosting.utils.servlet.HttpServletRequestAdapter;
import com.google.apphosting.utils.servlet.HttpServletResponseAdapter;
import com.google.apphosting.vmruntime.CommitDelayingResponse;
import com.google.apphosting.vmruntime.VmApiProxyDelegate;
import com.google.apphosting.vmruntime.VmApiProxyEnvironment;
import com.google.apphosting.vmruntime.VmEnvironmentFactory;
import com.google.apphosting.vmruntime.VmMetadataCache;
import com.google.apphosting.vmruntime.VmRequestUtils;
import com.google.apphosting.vmruntime.VmRuntimeFileLogHandler;
import com.google.apphosting.vmruntime.VmRuntimeLogHandler;
import com.google.apphosting.vmruntime.VmRuntimeUtils;
import com.google.apphosting.vmruntime.VmTimer;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.session.AbstractSessionManager;
import org.eclipse.jetty.server.session.HashSessionManager;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.util.ArrayUtil;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.webapp.WebAppContext;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;
import javax.servlet.DispatcherType;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* WebAppContext for VM Runtimes. This class extends the "normal" AppEngineWebAppContext with
* functionality that installs a request specific thread local environment on each incoming request.
*/
public class VmRuntimeWebAppContext
extends WebAppContext implements VmRuntimeTrustedAddressChecker {
private static final Logger logger = Logger.getLogger(VmRuntimeWebAppContext.class.getName());
// It's undesirable to have the user app override classes provided by us.
// So we mark them as Jetty system classes, which cannot be overridden.
private static final String[] SYSTEM_CLASSES = {
// The trailing dot means these are all Java packages, not individual classes.
"com.google.appengine.api.",
"com.google.appengine.tools.",
"com.google.apphosting.",
"com.google.cloud.sql.jdbc.",
"com.google.protos.cloud.sql.",
"com.google.storage.onestore.",
};
// constant. If it's much larger than this we may need to
// restructure the code a bit.
protected static final int MAX_RESPONSE_SIZE = 32 * 1024 * 1024;
private final String serverInfo;
private final VmMetadataCache metadataCache;
private final Timer wallclockTimer;
private VmApiProxyEnvironment defaultEnvironment;
// Indicates if the context is running via the Cloud SDK, or the real runtime.
boolean isDevMode;
static {
// Set SPI classloader priority to prefer the WebAppClassloader.
System.setProperty(
ServiceFactoryFactory.USE_THREAD_CONTEXT_CLASSLOADER_PROPERTY, Boolean.TRUE.toString());
// Use thread context class loader for memcache deserialization.
System.setProperty(
MemcacheSerialization.USE_THREAD_CONTEXT_CLASSLOADER_PROPERTY, Boolean.TRUE.toString());
}
// List of Jetty configuration only needed if the quickstart process has been
// executed, so we do not need the webinf, wedxml, fragment and annotation configurations
// because they have been executed via the SDK.
private static final String[] quickstartConfigurationClasses = {
org.eclipse.jetty.quickstart.QuickStartConfiguration.class.getCanonicalName(),
org.eclipse.jetty.plus.webapp.EnvConfiguration.class.getCanonicalName(),
org.eclipse.jetty.plus.webapp.PlusConfiguration.class.getCanonicalName(),
org.eclipse.jetty.webapp.JettyWebXmlConfiguration.class.getCanonicalName()
};
// List of all the standard Jetty configurations that need to be executed when there
// is no quickstart-web.xml.
private static final String[] preconfigurationClasses = {
org.eclipse.jetty.webapp.WebInfConfiguration.class.getCanonicalName(),
org.eclipse.jetty.webapp.WebXmlConfiguration.class.getCanonicalName(),
org.eclipse.jetty.webapp.MetaInfConfiguration.class.getCanonicalName(),
org.eclipse.jetty.webapp.FragmentConfiguration.class.getCanonicalName(),
org.eclipse.jetty.plus.webapp.EnvConfiguration.class.getCanonicalName(),
org.eclipse.jetty.plus.webapp.PlusConfiguration.class.getCanonicalName(),
org.eclipse.jetty.annotations.AnnotationConfiguration.class.getCanonicalName()
};
@Override
protected void doStart() throws Exception {
// unpack and Adjust paths.
Resource base = getBaseResource();
if (base == null) {
base = Resource.newResource(getWar());
}
Resource dir;
if (base.isDirectory()) {
dir = base;
} else {
throw new IllegalArgumentException();
}
Resource qswebxml = dir.addPath("/WEB-INF/quickstart-web.xml");
if (qswebxml.exists()) {
setConfigurationClasses(quickstartConfigurationClasses);
}
super.doStart();
}
/**
* Creates a List of SessionStores based on the configuration in the provided AppEngineWebXml.
*
* @param appEngineWebXml The AppEngineWebXml containing the session configuration.
* @return A List of SessionStores in write order.
*/
private static List<SessionStore> createSessionStores(AppEngineWebXml appEngineWebXml) {
DatastoreSessionStore datastoreSessionStore =
appEngineWebXml.getAsyncSessionPersistence() ? new DeferredDatastoreSessionStore(
appEngineWebXml.getAsyncSessionPersistenceQueueName())
: new DatastoreSessionStore();
// Write session data to the datastore before we write to memcache.
return Arrays.asList(datastoreSessionStore, new MemcacheSessionStore());
}
/**
* Checks if the request was made over HTTPS. If so it modifies the request so that
* {@code HttpServletRequest#isSecure()} returns true, {@code HttpServletRequest#getScheme()}
* returns "https", and {@code HttpServletRequest#getServerPort()} returns 443. Otherwise it sets
* the scheme to "http" and port to 80.
*
* @param request The request to modify.
*/
private void setSchemeAndPort(Request request) {
String https = request.getHeader(VmApiProxyEnvironment.HTTPS_HEADER);
if ("on".equals(https)) {
request.setSecure(true);
request.setScheme(HttpScheme.HTTPS.toString());
request.setAuthority(request.getServerName(), 443);
} else {
request.setSecure(false);
request.setScheme(HttpScheme.HTTP.toString());
request.setAuthority(request.getServerName(), defaultEnvironment.getServerPort());
}
}
/**
* Creates a new VmRuntimeWebAppContext.
*/
public VmRuntimeWebAppContext() {
this.serverInfo = VmRuntimeUtils.getServerInfo();
_scontext = new VmRuntimeServletContext();
// Configure the Jetty SecurityHandler to understand our method of authentication
// (via the UserService). Only the default ConstraintSecurityHandler is supported.
AppEngineAuthentication.configureSecurityHandler(
(ConstraintSecurityHandler) getSecurityHandler(), this);
setMaxFormContentSize(MAX_RESPONSE_SIZE);
setConfigurationClasses(preconfigurationClasses);
// See http://www.eclipse.org/jetty/documentation/current/configuring-webapps.html#webapp-context-attributes
// We also want the Jetty container libs to be scanned for annotations.
setAttribute("org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern", ".*\\.jar");
metadataCache = new VmMetadataCache();
wallclockTimer = new VmTimer();
ApiProxy.setDelegate(new VmApiProxyDelegate());
}
/**
* Initialize the WebAppContext for use by the VmRuntime.
*
* This method initializes the WebAppContext by setting the context path and application folder.
* It will also parse the appengine-web.xml file provided to set System Properties and session
* manager accordingly.
*
* @param appengineWebXmlFile The appengine-web.xml file path (relative to appDir).
* @throws AppEngineConfigException If there was a problem finding or parsing the
* appengine-web.xml configuration.
* @throws IOException If the runtime was unable to find/read appDir.
*/
public void init(String appengineWebXmlFile)
throws AppEngineConfigException, IOException {
String appDir=getBaseResource().getFile().getCanonicalPath();
defaultEnvironment = VmApiProxyEnvironment.createDefaultContext(
System.getenv(), metadataCache, VmRuntimeUtils.getApiServerAddress(), wallclockTimer,
VmRuntimeUtils.ONE_DAY_IN_MILLIS, appDir);
ApiProxy.setEnvironmentForCurrentThread(defaultEnvironment);
if (ApiProxy.getEnvironmentFactory() == null) {
// Need the check above since certain unit tests initialize the context multiple times.
ApiProxy.setEnvironmentFactory(new VmEnvironmentFactory(defaultEnvironment));
}
isDevMode = defaultEnvironment.getPartition().equals("dev");
AppEngineWebXml appEngineWebXml = null;
File appWebXml = new File(appDir, appengineWebXmlFile);
if (appWebXml.exists()) {
AppEngineWebXmlReader appEngineWebXmlReader
= new AppEngineWebXmlReader(appDir, appengineWebXmlFile);
appEngineWebXml = appEngineWebXmlReader.readAppEngineWebXml();
}
VmRuntimeUtils.installSystemProperties(defaultEnvironment, appEngineWebXml);
VmRuntimeLogHandler.init();
VmRuntimeFileLogHandler.init();
for (String systemClass : SYSTEM_CLASSES) {
addSystemClass(systemClass);
}
if (appEngineWebXml == null) {
// No need to configure the session manager.
return;
}
AbstractSessionManager sessionManager;
if (appEngineWebXml.getSessionsEnabled()) {
sessionManager = new SessionManager(createSessionStores(appEngineWebXml));
getSessionHandler().setSessionManager(sessionManager);
}
setProtectedTargets(ArrayUtil.addToArray(getProtectedTargets(), "/app.yaml", String.class));
}
@Override
public boolean isTrustedRemoteAddr(String remoteAddr) {
return VmRequestUtils.isTrustedRemoteAddr(isDevMode, remoteAddr);
}
/**
* Overrides doScope from ScopedHandler.
*
* Configures a thread local environment before the request is forwarded on to be handled by the
* SessionHandler, SecurityHandler, and ServletHandler in turn. The environment is required for
* AppEngine APIs to function. A request specific environment is required since some information
* is encoded in request headers on the request (for example current user).
*/
@Override
public final void doScope(
String target, Request baseRequest, HttpServletRequest httpServletRequest ,
HttpServletResponse httpServletResponse)
throws IOException, ServletException {
HttpRequest request = new HttpServletRequestAdapter(httpServletRequest);
HttpResponse response = new HttpServletResponseAdapter(httpServletResponse);
// For JSP Includes do standard processing, everything else has been done
// in the main request before the include.
if (DispatcherType.INCLUDE.equals(httpServletRequest.getDispatcherType())
|| DispatcherType.FORWARD.equals(httpServletRequest.getDispatcherType())) {
super.doScope(target, baseRequest, httpServletRequest, httpServletResponse);
return;
}
// Install a thread local environment based on request headers of the current request.
VmApiProxyEnvironment requestSpecificEnvironment = VmApiProxyEnvironment.createFromHeaders(
System.getenv(), metadataCache, request, VmRuntimeUtils.getApiServerAddress(),
wallclockTimer, VmRuntimeUtils.ONE_DAY_IN_MILLIS, defaultEnvironment);
CommitDelayingResponse wrappedResponse;
if (httpServletResponse instanceof CommitDelayingResponse) {
wrappedResponse = (CommitDelayingResponse) httpServletResponse;
} else {
wrappedResponse = new CommitDelayingResponse(httpServletResponse);
}
try {
ApiProxy.setEnvironmentForCurrentThread(requestSpecificEnvironment);
// Check for SkipAdminCheck and set attributes accordingly.
VmRuntimeUtils.handleSkipAdminCheck(request);
// Change scheme to HTTPS based on headers set by the appserver.
setSchemeAndPort(baseRequest);
// Forward the request to the rest of the handlers.
super.doScope(target, baseRequest, httpServletRequest, wrappedResponse);
} finally {
try {
// Interrupt any remaining request threads and wait for them to complete.
VmRuntimeUtils.interruptRequestThreads(
requestSpecificEnvironment, VmRuntimeUtils.MAX_REQUEST_THREAD_INTERRUPT_WAIT_TIME_MS);
// Wait for any pending async API requests to complete.
if (!VmRuntimeUtils.waitForAsyncApiCalls(requestSpecificEnvironment,
new HttpServletResponseAdapter(wrappedResponse))) {
logger.warning("Timed out or interrupted while waiting for async API calls to complete.");
}
if (!response.isCommitted()) {
// Flush and set the flush count header so the appserver knows when all logs are in.
VmRuntimeUtils.flushLogsAndAddHeader(response, requestSpecificEnvironment);
} else {
throw new ServletException("Response for request to '" + target
+ "' was already commited (code=" + httpServletResponse.getStatus()
+ "). This might result in lost log messages.'");
}
} finally {
try {
// Complete any pending actions.
wrappedResponse.commit();
} finally {
// Restore the default environment.
ApiProxy.setEnvironmentForCurrentThread(defaultEnvironment);
}
}
}
}
// N.B.(schwardo): 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.
/**
* ServletContext for VmRuntime applications.
*/
public class VmRuntimeServletContext extends Context {
@Override
public ClassLoader getClassLoader() {
return VmRuntimeWebAppContext.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()));
}
@Override
public void log(Exception exception, String msg) {
log(msg, exception);
}
}
}