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
;
11 import java
.lang
.reflect
.Constructor
;
12 import java
.lang
.reflect
.InvocationTargetException
;
13 import java
.net
.SocketPermission
;
15 import java
.security
.Permission
;
16 import java
.util
.Collection
;
17 import java
.util
.HashMap
;
19 import java
.util
.PropertyPermission
;
22 * Creates new {@link DevAppServer DevAppServers} which can be used to launch
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";
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
);
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
,
67 return createDevAppServer(appDir
, externalResourceDir
, null, null, address
, port
, true,
68 true, new HashMap
<String
, Object
>());
72 * Creates a new {@link DevAppServer} with a custom classpath for the web
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
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
;
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
) {
162 if (e
instanceof InvocationTargetException
) {
165 throw new RuntimeException("Unable to create a DevAppServer", t
);
167 if (installSecurityManager
) {
168 System
.setSecurityManager(new CustomSecurityManager(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
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() {
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())) {
226 if (("com.mysql.jdbc.AbandonedConnectionCleanupThread".equals(frame
.getClassName()) &&
227 "<init>".equals(frame
.getMethodName())) ||
228 frame
.getClassName().startsWith("com.mysql.jdbc.NonRegisteringDriver")) {
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
)) {
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()))) {
255 if (this.appHasPermissionNonThreadCallerFrame(stack
.getNonThreadCallerFrame())) {
260 if (perm
instanceof SocketPermission
) {
264 return SecurityConstants
.FILE_READ_ACTION
.equals(perm
.getActions()) &&
265 perm
.getName().endsWith(KEYCHAIN_JNILIB
);
269 public void checkPermission(Permission perm
) {
270 if (perm
instanceof PropertyPermission
) {
274 if (isDevAppServerThread()) {
275 if (appHasPermission(perm
)) {
279 super.checkPermission(perm
);
284 public void checkPermission(Permission perm
, Object context
) {
285 if (isDevAppServerThread()) {
286 if (appHasPermission(perm
)) {
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.
299 public void checkAccess(ThreadGroup g
) {
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)}.
311 public void checkAccess(Thread t
) {
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
])) {
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
])) {
364 throw new IllegalStateException("Unable to determine calling frame.");