Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / development / DevAppServerFactory.java
blob8b7aad93af0a99842f21a850001be587fa9b58b4
1 // Copyright 2008 Google Inc. All Rights Reserved.
3 package com.google.appengine.tools.development;
5 import com.google.appengine.tools.development.agent.AppEngineDevAgent;
6 import com.google.apphosting.utils.security.SecurityManagerInstaller;
8 import sun.security.util.SecurityConstants;
10 import java.io.File;
11 import java.lang.reflect.Constructor;
12 import java.lang.reflect.InvocationTargetException;
13 import java.net.SocketPermission;
14 import java.net.URL;
15 import java.security.Permission;
16 import java.util.Collection;
17 import java.util.HashMap;
18 import java.util.Map;
19 import java.util.PropertyPermission;
21 /**
22 * Creates new {@link DevAppServer DevAppServers} which can be used to launch
23 * web applications.
24 * TODO(user): Describe the difference between standalone and testing servers.
27 public class DevAppServerFactory {
29 static final String DEV_APP_SERVER_CLASS =
30 "com.google.appengine.tools.development.DevAppServerImpl";
32 private static final Class<?>[] DEV_APPSERVER_CTOR_ARG_TYPES = {File.class, File.class,
33 File.class, File.class, String.class, Integer.TYPE, Boolean.TYPE, Map.class};
35 private static final String USER_CODE_CLASSPATH_MANAGER_PROP =
36 "devappserver.userCodeClasspathManager";
37 private static final String USER_CODE_CLASSPATH = USER_CODE_CLASSPATH_MANAGER_PROP + ".classpath";
38 private static final String USER_CODE_REQUIRES_WEB_INF =
39 USER_CODE_CLASSPATH_MANAGER_PROP + ".requiresWebInf";
41 /**
42 * Creates a new {@link DevAppServer} ready to start serving
44 * @param appDir The top-level directory of the web application to be run
45 * @param address Address to bind to
46 * @param port Port to bind to
48 * @return a {@code DevAppServer}
50 public DevAppServer createDevAppServer(File appDir, String address, int port) {
51 return createDevAppServer(appDir, null, address, port);
54 /**
55 * Creates a new {@link DevAppServer} ready to start serving
57 * @param appDir The top-level directory of the web application to be run
58 * @param externalResourceDir If not {@code null}, a resource directory external to the appDir.
59 * This will be searched before appDir when looking for resources.
60 * @param address Address to bind to
61 * @param port Port to bind to
63 * @return a {@code DevAppServer}
65 public DevAppServer createDevAppServer(File appDir, File externalResourceDir, String address,
66 int port) {
67 return createDevAppServer(appDir, externalResourceDir, null, null, address, port, true,
68 true, new HashMap<String, Object>());
71 /**
72 * Creates a new {@link DevAppServer} with a custom classpath for the web
73 * app..
75 * @param appDir The top-level directory of the web application to be run
76 * @param webXmlLocation The location of a file whose format complies with
77 * http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd. If {@code null},
78 * defaults to <appDir>/WEB-INF/web.xml
79 * @param appEngineWebXmlLocation The name of the app engine config file. If
80 * {@code null}, defaults to <appDir>/WEB-INF/appengine-web.xml.
81 * @param address Address to bind to
82 * @param port Port to bind to
83 * @param useCustomStreamHandler If {@code true}, install
84 * {@link StreamHandlerFactory}. This is "normal" behavior for the dev
85 * app server but tests may want to disable this since there are some
86 * compatibility issues with our custom handler and Selenium.
87 * @param installSecurityManager Whether or not to install the dev appserver
88 * security manager. It is strongly recommended you pass {@code true} unless
89 * there is something in your test environment that prevents you from
90 * installing a security manager.
91 * @param classpath The classpath of the test and all its dependencies
92 * (possibly the entire app)..
94 * @return a {@code DevAppServer}
96 public DevAppServer createDevAppServer(File appDir, File webXmlLocation,
97 File appEngineWebXmlLocation, String address, int port,
98 boolean useCustomStreamHandler, boolean installSecurityManager, Collection<URL> classpath) {
99 Map<String, Object> containerConfigProps = newContainerConfigPropertiesForTest(classpath);
100 return createDevAppServer(appDir, null, webXmlLocation, appEngineWebXmlLocation,
101 address, port, useCustomStreamHandler, installSecurityManager, containerConfigProps);
105 * Creates a new {@link DevAppServer} ready to start serving. Only exposed
106 * to clients that can access it via reflection to keep it out of the public
107 * api.
109 * @param appDir The top-level directory of the web application to be run
110 * @param webXmlLocation The location of a file whose format complies with
111 * http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd. If {@code null},
112 * defaults to <appDir>/WEB-INF/web.xml
113 * @param appEngineWebXmlLocation The location of the app engine config file. If
114 * {@code null}, defaults to <appDir>/WEB-INF/appengine-web.xml.
115 * @param address Address to bind to
116 * @param port Port to bind to
117 * @param useCustomStreamHandler If {@code true}, install
118 * {@link StreamHandlerFactory}. This is "normal" behavior for the dev
119 * app server but tests may want to disable this since there are some
120 * compatibility issues with our custom handler and Selenium.
122 * @return a {@code DevAppServer}
124 @SuppressWarnings("unused")
125 private DevAppServer createDevAppServer(File appDir,File webXmlLocation,
126 File appEngineWebXmlLocation, String address, int port, boolean useCustomStreamHandler) {
127 return createDevAppServer(appDir, null, webXmlLocation, appEngineWebXmlLocation,
128 address, port, useCustomStreamHandler, true, new HashMap<String, Object>());
131 @SuppressWarnings("unused")
132 private DevAppServer createDevAppServer(File appDir, File webXmlLocation,
133 File appEngineWebXmlLocation, String address, int port, boolean useCustomStreamHandler,
134 boolean installSecurityManager, Map<String, Object> containerConfigProperties) {
135 return createDevAppServer(appDir, null, webXmlLocation, appEngineWebXmlLocation, address, port,
136 useCustomStreamHandler,installSecurityManager, containerConfigProperties);
139 private DevAppServer createDevAppServer(File appDir, File externalResourceDir,
140 File webXmlLocation, File appEngineWebXmlLocation, String address, int port,
141 boolean useCustomStreamHandler, boolean installSecurityManager,
142 Map<String, Object> containerConfigProperties) {
143 if (installSecurityManager) {
144 SecurityManagerInstaller.install();
147 DevAppServerClassLoader loader = DevAppServerClassLoader.newClassLoader(
148 DevAppServerFactory.class.getClassLoader());
150 testAgentIsInstalled();
152 DevAppServer devAppServer;
153 try {
154 Class<?> devAppServerClass = Class.forName(DEV_APP_SERVER_CLASS, true, loader);
155 Constructor<?> cons = devAppServerClass.getConstructor(DEV_APPSERVER_CTOR_ARG_TYPES);
156 cons.setAccessible(true);
157 devAppServer = (DevAppServer) cons.newInstance(
158 appDir, externalResourceDir, webXmlLocation, appEngineWebXmlLocation, address, port,
159 useCustomStreamHandler, containerConfigProperties);
160 } catch (Exception e) {
161 Throwable t = e;
162 if (e instanceof InvocationTargetException) {
163 t = e.getCause();
165 throw new RuntimeException("Unable to create a DevAppServer", t);
167 if (installSecurityManager) {
168 System.setSecurityManager(new CustomSecurityManager(devAppServer));
170 return devAppServer;
174 * Build a {@link Map} that contains settings that will allow us to inject
175 * our own classpath and to not require a WEB-INF directory. This map will
176 * travel across classloader boundaries so all values in the map must be jre
177 * classes.
179 private Map<String, Object> newContainerConfigPropertiesForTest(Collection<URL> classpath) {
180 Map<String, Object> containerConfigProps = new HashMap<String, Object>();
181 Map<String, Object> userCodeClasspathManagerProps = new HashMap<String, Object>();
182 userCodeClasspathManagerProps.put(USER_CODE_CLASSPATH, classpath);
183 userCodeClasspathManagerProps.put(USER_CODE_REQUIRES_WEB_INF, false);
184 containerConfigProps.put(USER_CODE_CLASSPATH_MANAGER_PROP, userCodeClasspathManagerProps);
185 return containerConfigProps;
188 private void testAgentIsInstalled() {
189 try {
190 AppEngineDevAgent.getAgent();
191 } catch (Throwable t) {
192 String msg = "Unable to locate the App Engine agent. Please use dev_appserver, KickStart, "
193 + " or set the jvm flag: \"-javaagent:<sdk_root>/lib/agent/appengine-agent.jar\"";
194 throw new RuntimeException(msg, t);
199 * Implements custom security behavior. This SecurityManager only applies
200 * checks when code is running in the context of a DevAppServer thread
201 * handling an http request.
203 static class CustomSecurityManager extends SecurityManager {
205 private static final RuntimePermission PERMISSION_MODIFY_THREAD_GROUP =
206 new RuntimePermission("modifyThreadGroup");
208 private static final RuntimePermission PERMISSION_MODIFY_THREAD =
209 new RuntimePermission("modifyThread");
211 private static final String KEYCHAIN_JNILIB = "/libkeychain.jnilib";
213 private static final Object PERMISSION_LOCK = new Object();
215 private final DevAppServer devAppServer;
217 public CustomSecurityManager(DevAppServer devAppServer) {
218 this.devAppServer = devAppServer;
221 boolean appHasPermissionNonThreadCallerFrame(StackTraceElement frame) {
222 if ("sun.security.ssl.SSLSocketImpl$NotifyHandshakeThread".equals(frame.getClassName()) &&
223 "<init>".equals(frame.getMethodName())) {
224 return true;
226 if (("com.mysql.jdbc.AbandonedConnectionCleanupThread".equals(frame.getClassName()) &&
227 "<init>".equals(frame.getMethodName())) ||
228 frame.getClassName().startsWith("com.mysql.jdbc.NonRegisteringDriver")) {
229 return true;
231 return false;
234 private synchronized boolean appHasPermission(Permission perm) {
235 synchronized (PERMISSION_LOCK) {
236 AppContext context = devAppServer.getCurrentAppContext();
237 if (context.getUserPermissions().implies(perm) ||
238 context.getApplicationPermissions().implies(perm)) {
239 return true;
243 if (PERMISSION_MODIFY_THREAD_GROUP.equals(perm) || PERMISSION_MODIFY_THREAD.equals(perm)) {
244 StackTraceAnalyzer stack = new StackTraceAnalyzer();
245 if (PERMISSION_MODIFY_THREAD.equals(perm)) {
246 StackTraceElement frame = stack.getCallerFrame();
247 if ("java.util.concurrent.ThreadPoolExecutor".equals(frame.getClassName()) ||
248 ("java.lang.Thread".equals(frame.getClassName()) &&
249 "interrupt".equals(frame.getMethodName())) ||
250 ("java.lang.Thread".equals(frame.getClassName()) &&
251 "setUncaughtExceptionHandler".equals(frame.getMethodName()))) {
252 return true;
255 if (this.appHasPermissionNonThreadCallerFrame(stack.getNonThreadCallerFrame())) {
256 return true;
260 if (perm instanceof SocketPermission) {
261 return true;
264 return SecurityConstants.FILE_READ_ACTION.equals(perm.getActions()) &&
265 perm.getName().endsWith(KEYCHAIN_JNILIB);
268 @Override
269 public void checkPermission(Permission perm) {
270 if (perm instanceof PropertyPermission) {
271 return;
274 if (isDevAppServerThread()) {
275 if (appHasPermission(perm)) {
276 return;
279 super.checkPermission(perm);
283 @Override
284 public void checkPermission(Permission perm, Object context) {
285 if (isDevAppServerThread()) {
286 if (appHasPermission(perm)) {
287 return;
289 super.checkPermission(perm, context);
294 * Don't allow user code permission to muck with Threads.
295 * Normally the JDK only enforces this for the root ThreadGroup, but
296 * we enforce it at all times.
298 @Override
299 public void checkAccess(ThreadGroup g) {
300 if (g == null) {
301 throw new NullPointerException("thread group can't be null");
304 checkPermission(PERMISSION_MODIFY_THREAD_GROUP);
308 * Enforces the same thread policy as {@link #checkAccess(ThreadGroup)}.
310 @Override
311 public void checkAccess(Thread t) {
312 if (t == null) {
313 throw new NullPointerException("thread can't be null");
316 checkPermission(PERMISSION_MODIFY_THREAD);
319 private boolean isDevAppServerThread() {
321 return (Boolean.getBoolean("devappserver-thread-" + Thread.currentThread().getName())
322 && devAppServer.getCurrentAppContext() != null);
326 * Performs various tests on the current stack trace.
328 private class StackTraceAnalyzer {
329 private final StackTraceElement[] frames = Thread.currentThread().getStackTrace();
331 private boolean isThisOrOuterClass(StackTraceElement frame) {
332 return CustomSecurityManager.this.getClass().getName().equals(frame.getClassName()) ||
333 getClass().getName().equals(frame.getClassName());
337 * Find the first {@link StackTraceElement} on the current thread
338 * which does not come from this class or from a method named
339 * {@code checkAccess} (e.g. {@link Thread#checkAccess}).
341 StackTraceElement getCallerFrame() {
342 for (int i = 1; i < frames.length; i++) {
343 if ("checkAccess".equals(frames[i].getMethodName())) {
344 } else if (!isThisOrOuterClass(frames[i])) {
345 return frames[i];
348 throw new IllegalStateException("Unable to determine calling frame.");
352 * Find the first {@link StackTraceElement} on the current thread
353 * which does not come from this class or from a method named
354 * {@code checkAccess} (e.g. {@link Thread#checkAccess}).
356 StackTraceElement getNonThreadCallerFrame() {
357 for (int i = 1; i < frames.length; i++) {
358 if ("java.lang.ThreadGroup".equals(frames[i].getClassName()) ||
359 "java.lang.Thread".equals(frames[i].getClassName())) {
360 } else if (!isThisOrOuterClass(frames[i])) {
361 return frames[i];
364 throw new IllegalStateException("Unable to determine calling frame.");