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, File
.class,
33 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 private 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 private synchronized boolean appHasPermission(Permission perm
) {
222 synchronized (PERMISSION_LOCK
) {
223 AppContext context
= devAppServer
.getCurrentAppContext();
224 if (context
.getUserPermissions().implies(perm
) ||
225 context
.getApplicationPermissions().implies(perm
)) {
230 if (PERMISSION_MODIFY_THREAD
.equals(perm
)) {
231 StackTraceElement frame
= getCallerFrame();
232 if ("java.util.concurrent.ThreadPoolExecutor".equals(frame
.getClassName()) ||
233 ("java.lang.Thread".equals(frame
.getClassName()) &&
234 "interrupt".equals(frame
.getMethodName())) ||
235 ("java.lang.Thread".equals(frame
.getClassName()) &&
236 "setUncaughtExceptionHandler".equals(frame
.getMethodName()))) {
241 if (perm
instanceof SocketPermission
) {
245 return SecurityConstants
.FILE_READ_ACTION
.equals(perm
.getActions()) &&
246 perm
.getName().endsWith(KEYCHAIN_JNILIB
);
250 public void checkPermission(Permission perm
) {
251 if (perm
instanceof PropertyPermission
) {
255 if (isDevAppServerThread()) {
256 if (appHasPermission(perm
)) {
260 super.checkPermission(perm
);
265 public void checkPermission(Permission perm
, Object context
) {
266 if (isDevAppServerThread()) {
267 if (appHasPermission(perm
)) {
270 super.checkPermission(perm
, context
);
275 * Don't allow user code permission to muck with Threads.
276 * Normally the JDK only enforces this for the root ThreadGroup, but
277 * we enforce it at all times.
280 public void checkAccess(ThreadGroup g
) {
282 throw new NullPointerException("thread group can't be null");
285 checkPermission(PERMISSION_MODIFY_THREAD_GROUP
);
289 * Enforces the same thread policy as {@link #checkAccess(ThreadGroup)}.
292 public void checkAccess(Thread t
) {
294 throw new NullPointerException("thread can't be null");
297 checkPermission(PERMISSION_MODIFY_THREAD
);
300 private boolean isDevAppServerThread() {
302 AppContext context
= devAppServer
.getCurrentAppContext();
303 return Boolean
.getBoolean("devappserver-thread-" + Thread
.currentThread().getName())
308 * Find the first {@link StackTraceElement} on the current thread
309 * which does not come from this class or from a method named
310 * {@code checkAccess} (e.g. {@link Thread#checkAccess}).
312 private StackTraceElement
getCallerFrame() {
313 StackTraceElement
[] frames
= Thread
.currentThread().getStackTrace();
314 for (int i
= 1; i
< frames
.length
; i
++) {
315 if ("checkAccess".equals(frames
[i
].getMethodName())) {
316 } else if (!getClass().getName().equals(frames
[i
].getClassName())) {
320 throw new IllegalStateException("Unable to determine calling frame.");