- add sources.
[platform/framework/web/crosswalk.git] / src / net / test / android / javatests / src / org / chromium / net / test / util / TestWebServer.java
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 package org.chromium.net.test.util;
6
7 import android.util.Base64;
8 import android.util.Log;
9 import android.util.Pair;
10
11 import org.apache.http.HttpException;
12 import org.apache.http.HttpRequest;
13 import org.apache.http.HttpResponse;
14 import org.apache.http.HttpStatus;
15 import org.apache.http.HttpVersion;
16 import org.apache.http.RequestLine;
17 import org.apache.http.StatusLine;
18 import org.apache.http.entity.ByteArrayEntity;
19 import org.apache.http.impl.DefaultHttpServerConnection;
20 import org.apache.http.impl.cookie.DateUtils;
21 import org.apache.http.message.BasicHttpResponse;
22 import org.apache.http.params.BasicHttpParams;
23 import org.apache.http.params.CoreProtocolPNames;
24 import org.apache.http.params.HttpParams;
25
26 import java.io.ByteArrayInputStream;
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.net.MalformedURLException;
30 import java.net.ServerSocket;
31 import java.net.Socket;
32 import java.net.URI;
33 import java.net.URL;
34 import java.net.URLConnection;
35 import java.security.KeyManagementException;
36 import java.security.KeyStore;
37 import java.security.NoSuchAlgorithmException;
38 import java.security.cert.X509Certificate;
39 import java.util.ArrayList;
40 import java.util.Date;
41 import java.util.HashMap;
42 import java.util.Hashtable;
43 import java.util.List;
44 import java.util.Map;
45
46 import javax.net.ssl.HostnameVerifier;
47 import javax.net.ssl.HttpsURLConnection;
48 import javax.net.ssl.KeyManager;
49 import javax.net.ssl.KeyManagerFactory;
50 import javax.net.ssl.SSLContext;
51 import javax.net.ssl.SSLSession;
52 import javax.net.ssl.X509TrustManager;
53
54 /**
55  * Simple http test server for testing.
56  *
57  * This server runs in a thread in the current process, so it is convenient
58  * for loopback testing without the need to setup tcp forwarding to the
59  * host computer.
60  *
61  * Based heavily on the CTSWebServer in Android.
62  */
63 public class TestWebServer {
64     private static final String TAG = "TestWebServer";
65     private static final int SERVER_PORT = 4444;
66     private static final int SSL_SERVER_PORT = 4445;
67
68     public static final String SHUTDOWN_PREFIX = "/shutdown";
69
70     private static TestWebServer sInstance;
71     private static Hashtable<Integer, String> sReasons;
72
73     private final ServerThread mServerThread;
74     private String mServerUri;
75     private final boolean mSsl;
76
77     private static class Response {
78         final byte[] mResponseData;
79         final List<Pair<String, String>> mResponseHeaders;
80         final boolean mIsRedirect;
81         final Runnable mResponseAction;
82
83         Response(byte[] resposneData, List<Pair<String, String>> responseHeaders,
84                 boolean isRedirect, Runnable responseAction) {
85             mIsRedirect = isRedirect;
86             mResponseData = resposneData;
87             mResponseHeaders = responseHeaders == null ?
88                     new ArrayList<Pair<String, String>>() : responseHeaders;
89             mResponseAction = responseAction;
90         }
91     }
92
93     // The Maps below are modified on both the client thread and the internal server thread, so
94     // need to use a lock when accessing them.
95     private final Object mLock = new Object();
96     private final Map<String, Response> mResponseMap = new HashMap<String, Response>();
97     private final Map<String, Integer> mResponseCountMap = new HashMap<String, Integer>();
98     private final Map<String, HttpRequest> mLastRequestMap = new HashMap<String, HttpRequest>();
99
100     /**
101      * Create and start a local HTTP server instance.
102      * @param ssl True if the server should be using secure sockets.
103      * @throws Exception
104      */
105     public TestWebServer(boolean ssl) throws Exception {
106         if (sInstance != null) {
107             // attempt to start a new instance while one is still running
108             // shut down the old instance first
109             sInstance.shutdown();
110         }
111         setStaticInstance(this);
112         mSsl = ssl;
113         if (mSsl) {
114             mServerUri = "https://localhost:" + SSL_SERVER_PORT;
115         } else {
116             mServerUri = "http://localhost:" + SERVER_PORT;
117         }
118         mServerThread = new ServerThread(this, mSsl);
119         mServerThread.start();
120     }
121
122     private static void setStaticInstance(TestWebServer instance) {
123         sInstance = instance;
124     }
125
126     /**
127      * Terminate the http server.
128      */
129     public void shutdown() {
130         try {
131             // Avoid a deadlock between two threads where one is trying to call
132             // close() and the other one is calling accept() by sending a GET
133             // request for shutdown and having the server's one thread
134             // sequentially call accept() and close().
135             URL url = new URL(mServerUri + SHUTDOWN_PREFIX);
136             URLConnection connection = openConnection(url);
137             connection.connect();
138
139             // Read the input from the stream to send the request.
140             InputStream is = connection.getInputStream();
141             is.close();
142
143             // Block until the server thread is done shutting down.
144             mServerThread.join();
145
146         } catch (MalformedURLException e) {
147             throw new IllegalStateException(e);
148         } catch (InterruptedException e) {
149             throw new RuntimeException(e);
150         } catch (IOException e) {
151             throw new RuntimeException(e);
152         } catch (NoSuchAlgorithmException e) {
153             throw new IllegalStateException(e);
154         } catch (KeyManagementException e) {
155             throw new IllegalStateException(e);
156         }
157
158         setStaticInstance(null);
159     }
160
161     private final static int RESPONSE_STATUS_NORMAL = 0;
162     private final static int RESPONSE_STATUS_MOVED_TEMPORARILY = 1;
163
164     private String setResponseInternal(
165             String requestPath, byte[] responseData,
166             List<Pair<String, String>> responseHeaders, Runnable responseAction,
167             int status) {
168         final boolean isRedirect = (status == RESPONSE_STATUS_MOVED_TEMPORARILY);
169
170         synchronized (mLock) {
171             mResponseMap.put(requestPath, new Response(
172                     responseData, responseHeaders, isRedirect, responseAction));
173             mResponseCountMap.put(requestPath, Integer.valueOf(0));
174             mLastRequestMap.put(requestPath, null);
175         }
176         return getResponseUrl(requestPath);
177     }
178
179     /**
180      * Gets the URL on the server under which a particular request path will be accessible.
181      *
182      * This only gets the URL, you still need to set the response if you intend to access it.
183      *
184      * @param requestPath The path to respond to.
185      * @return The full URL including the requestPath.
186      */
187     public String getResponseUrl(String requestPath) {
188         return mServerUri + requestPath;
189     }
190
191     /**
192      * Sets a response to be returned when a particular request path is passed
193      * in (with the option to specify additional headers).
194      *
195      * @param requestPath The path to respond to.
196      * @param responseString The response body that will be returned.
197      * @param responseHeaders Any additional headers that should be returned along with the
198      *                        response (null is acceptable).
199      * @return The full URL including the path that should be requested to get the expected
200      *         response.
201      */
202     public String setResponse(
203             String requestPath, String responseString,
204             List<Pair<String, String>> responseHeaders) {
205         return setResponseInternal(requestPath, responseString.getBytes(), responseHeaders, null,
206                 RESPONSE_STATUS_NORMAL);
207     }
208
209     /**
210      * Sets a response to be returned when a particular request path is passed
211      * in with the option to specify additional headers as well as an arbitrary action to be
212      * executed on each request.
213      *
214      * @param requestPath The path to respond to.
215      * @param responseString The response body that will be returned.
216      * @param responseHeaders Any additional headers that should be returned along with the
217      *                        response (null is acceptable).
218      * @param responseAction The action to be performed when fetching the response.  This action
219      *                       will be executed for each request and will be handled on a background
220      *                       thread.
221      * @return The full URL including the path that should be requested to get the expected
222      *         response.
223      */
224     public String setResponseWithRunnableAction(
225             String requestPath, String responseString, List<Pair<String, String>> responseHeaders,
226             Runnable responseAction) {
227         return setResponseInternal(
228                 requestPath, responseString.getBytes(), responseHeaders, responseAction,
229                 RESPONSE_STATUS_NORMAL);
230     }
231
232     /**
233      * Sets a redirect.
234      *
235      * @param requestPath The path to respond to.
236      * @param targetPath The path to redirect to.
237      * @return The full URL including the path that should be requested to get the expected
238      *         response.
239      */
240     public String setRedirect(
241             String requestPath, String targetPath) {
242         List<Pair<String, String>> responseHeaders = new ArrayList<Pair<String, String>>();
243         responseHeaders.add(Pair.create("Location", targetPath));
244
245         return setResponseInternal(requestPath, targetPath.getBytes(), responseHeaders, null,
246                 RESPONSE_STATUS_MOVED_TEMPORARILY);
247     }
248
249     /**
250      * Sets a base64 encoded response to be returned when a particular request path is passed
251      * in (with the option to specify additional headers).
252      *
253      * @param requestPath The path to respond to.
254      * @param base64EncodedResponse The response body that is base64 encoded. The actual server
255      *                              response will the decoded binary form.
256      * @param responseHeaders Any additional headers that should be returned along with the
257      *                        response (null is acceptable).
258      * @return The full URL including the path that should be requested to get the expected
259      *         response.
260      */
261     public String setResponseBase64(
262             String requestPath, String base64EncodedResponse,
263             List<Pair<String, String>> responseHeaders) {
264         return setResponseInternal(
265                 requestPath, Base64.decode(base64EncodedResponse, Base64.DEFAULT),
266                 responseHeaders, null, RESPONSE_STATUS_NORMAL);
267     }
268
269     /**
270      * Get the number of requests was made at this path since it was last set.
271      */
272     public int getRequestCount(String requestPath) {
273         Integer count = null;
274         synchronized (mLock) {
275             count = mResponseCountMap.get(requestPath);
276         }
277         if (count == null) throw new IllegalArgumentException("Path not set: " + requestPath);
278         return count.intValue();
279     }
280
281     /**
282      * Returns the last HttpRequest at this path. Can return null if it is never requested.
283      */
284     public HttpRequest getLastRequest(String requestPath) {
285         synchronized (mLock) {
286             if (!mLastRequestMap.containsKey(requestPath))
287                 throw new IllegalArgumentException("Path not set: " + requestPath);
288             return mLastRequestMap.get(requestPath);
289         }
290     }
291
292     public String getBaseUrl() {
293         return mServerUri + "/";
294     }
295
296     private URLConnection openConnection(URL url)
297             throws IOException, NoSuchAlgorithmException, KeyManagementException {
298         if (mSsl) {
299             // Install hostname verifiers and trust managers that don't do
300             // anything in order to get around the client not trusting
301             // the test server due to a lack of certificates.
302
303             HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
304             connection.setHostnameVerifier(new TestHostnameVerifier());
305
306             SSLContext context = SSLContext.getInstance("TLS");
307             TestTrustManager trustManager = new TestTrustManager();
308             context.init(null, new TestTrustManager[] {trustManager}, null);
309             connection.setSSLSocketFactory(context.getSocketFactory());
310
311             return connection;
312         } else {
313             return url.openConnection();
314         }
315     }
316
317     /**
318      * {@link X509TrustManager} that trusts everybody. This is used so that
319      * the client calling {@link TestWebServer#shutdown()} can issue a request
320      * for shutdown by blindly trusting the {@link TestWebServer}'s
321      * credentials.
322      */
323     private static class TestTrustManager implements X509TrustManager {
324         @Override
325         public void checkClientTrusted(X509Certificate[] chain, String authType) {
326             // Trust the TestWebServer...
327         }
328
329         @Override
330         public void checkServerTrusted(X509Certificate[] chain, String authType) {
331             // Trust the TestWebServer...
332         }
333
334         @Override
335         public X509Certificate[] getAcceptedIssuers() {
336             return null;
337         }
338     }
339
340     /**
341      * {@link HostnameVerifier} that verifies everybody. This permits
342      * the client to trust the web server and call
343      * {@link TestWebServer#shutdown()}.
344      */
345     private static class TestHostnameVerifier implements HostnameVerifier {
346         @Override
347         public boolean verify(String hostname, SSLSession session) {
348             return true;
349         }
350     }
351
352     private void servedResponseFor(String path, HttpRequest request) {
353         synchronized (mLock) {
354             mResponseCountMap.put(path, Integer.valueOf(
355                     mResponseCountMap.get(path).intValue() + 1));
356             mLastRequestMap.put(path, request);
357         }
358     }
359
360     /**
361      * Generate a response to the given request.
362      *
363      * <p>Always executed on the background server thread.
364      *
365      * <p>If there is an action associated with the response, it will be executed inside of
366      * this function.
367      *
368      * @throws InterruptedException
369      */
370     private HttpResponse getResponse(HttpRequest request) throws InterruptedException {
371         assert Thread.currentThread() == mServerThread
372                 : "getResponse called from non-server thread";
373
374         RequestLine requestLine = request.getRequestLine();
375         HttpResponse httpResponse = null;
376         Log.i(TAG, requestLine.getMethod() + ": " + requestLine.getUri());
377         String uriString = requestLine.getUri();
378         URI uri = URI.create(uriString);
379         String path = uri.getPath();
380
381         Response response = null;
382         synchronized (mLock) {
383             response = mResponseMap.get(path);
384         }
385         if (path.equals(SHUTDOWN_PREFIX)) {
386             httpResponse = createResponse(HttpStatus.SC_OK);
387         } else if (response == null) {
388             httpResponse = createResponse(HttpStatus.SC_NOT_FOUND);
389         } else if (response.mIsRedirect) {
390             httpResponse = createResponse(HttpStatus.SC_MOVED_TEMPORARILY);
391             for (Pair<String, String> header : response.mResponseHeaders) {
392                 httpResponse.addHeader(header.first, header.second);
393             }
394             servedResponseFor(path, request);
395         } else {
396             if (response.mResponseAction != null) response.mResponseAction.run();
397
398             httpResponse = createResponse(HttpStatus.SC_OK);
399             httpResponse.setEntity(createEntity(response.mResponseData));
400             for (Pair<String, String> header : response.mResponseHeaders) {
401                 httpResponse.addHeader(header.first, header.second);
402             }
403             servedResponseFor(path, request);
404         }
405         StatusLine sl = httpResponse.getStatusLine();
406         Log.i(TAG, sl.getStatusCode() + "(" + sl.getReasonPhrase() + ")");
407         setDateHeaders(httpResponse);
408         return httpResponse;
409     }
410
411     private void setDateHeaders(HttpResponse response) {
412         response.addHeader("Date", DateUtils.formatDate(new Date(), DateUtils.PATTERN_RFC1123));
413     }
414
415     /**
416      * Create an empty response with the given status.
417      */
418     private HttpResponse createResponse(int status) {
419         HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_0, status, null);
420         String reason = null;
421
422         // This synchronized silences findbugs.
423         synchronized (TestWebServer.class) {
424             if (sReasons == null) {
425                 sReasons = new Hashtable<Integer, String>();
426                 sReasons.put(HttpStatus.SC_UNAUTHORIZED, "Unauthorized");
427                 sReasons.put(HttpStatus.SC_NOT_FOUND, "Not Found");
428                 sReasons.put(HttpStatus.SC_FORBIDDEN, "Forbidden");
429                 sReasons.put(HttpStatus.SC_MOVED_TEMPORARILY, "Moved Temporarily");
430             }
431             // Fill in error reason. Avoid use of the ReasonPhraseCatalog, which is
432             // Locale-dependent.
433             reason = sReasons.get(status);
434         }
435
436         if (reason != null) {
437             StringBuffer buf = new StringBuffer("<html><head><title>");
438             buf.append(reason);
439             buf.append("</title></head><body>");
440             buf.append(reason);
441             buf.append("</body></html>");
442             response.setEntity(createEntity(buf.toString().getBytes()));
443         }
444         return response;
445     }
446
447     /**
448      * Create a string entity for the given content.
449      */
450     private ByteArrayEntity createEntity(byte[] data) {
451         ByteArrayEntity entity = new ByteArrayEntity(data);
452         entity.setContentType("text/html");
453         return entity;
454     }
455
456     private static class ServerThread extends Thread {
457         private TestWebServer mServer;
458         private ServerSocket mSocket;
459         private boolean mIsSsl;
460         private boolean mIsCancelled;
461         private SSLContext mSslContext;
462
463         /**
464          * Defines the keystore contents for the server, BKS version. Holds just a
465          * single self-generated key. The subject name is "Test Server".
466          */
467         private static final String SERVER_KEYS_BKS =
468             "AAAAAQAAABQDkebzoP1XwqyWKRCJEpn/t8dqIQAABDkEAAVteWtleQAAARpYl20nAAAAAQAFWC41" +
469             "MDkAAAJNMIICSTCCAbKgAwIBAgIESEfU1jANBgkqhkiG9w0BAQUFADBpMQswCQYDVQQGEwJVUzET" +
470             "MBEGA1UECBMKQ2FsaWZvcm5pYTEMMAoGA1UEBxMDTVRWMQ8wDQYDVQQKEwZHb29nbGUxEDAOBgNV" +
471             "BAsTB0FuZHJvaWQxFDASBgNVBAMTC1Rlc3QgU2VydmVyMB4XDTA4MDYwNTExNTgxNFoXDTA4MDkw" +
472             "MzExNTgxNFowaTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExDDAKBgNVBAcTA01U" +
473             "VjEPMA0GA1UEChMGR29vZ2xlMRAwDgYDVQQLEwdBbmRyb2lkMRQwEgYDVQQDEwtUZXN0IFNlcnZl" +
474             "cjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0LIdKaIr9/vsTq8BZlA3R+NFWRaH4lGsTAQy" +
475             "DPMF9ZqEDOaL6DJuu0colSBBBQ85hQTPa9m9nyJoN3pEi1hgamqOvQIWcXBk+SOpUGRZZFXwniJV" +
476             "zDKU5nE9MYgn2B9AoiH3CSuMz6HRqgVaqtppIe1jhukMc/kHVJvlKRNy9XMCAwEAATANBgkqhkiG" +
477             "9w0BAQUFAAOBgQC7yBmJ9O/eWDGtSH9BH0R3dh2NdST3W9hNZ8hIa8U8klhNHbUCSSktZmZkvbPU" +
478             "hse5LI3dh6RyNDuqDrbYwcqzKbFJaq/jX9kCoeb3vgbQElMRX8D2ID1vRjxwlALFISrtaN4VpWzV" +
479             "yeoHPW4xldeZmoVtjn8zXNzQhLuBqX2MmAAAAqwAAAAUvkUScfw9yCSmALruURNmtBai7kQAAAZx" +
480             "4Jmijxs/l8EBaleaUru6EOPioWkUAEVWCxjM/TxbGHOi2VMsQWqRr/DZ3wsDmtQgw3QTrUK666sR" +
481             "MBnbqdnyCyvM1J2V1xxLXPUeRBmR2CXorYGF9Dye7NkgVdfA+9g9L/0Au6Ugn+2Cj5leoIgkgApN" +
482             "vuEcZegFlNOUPVEs3SlBgUF1BY6OBM0UBHTPwGGxFBBcetcuMRbUnu65vyDG0pslT59qpaR0TMVs" +
483             "P+tcheEzhyjbfM32/vwhnL9dBEgM8qMt0sqF6itNOQU/F4WGkK2Cm2v4CYEyKYw325fEhzTXosck" +
484             "MhbqmcyLab8EPceWF3dweoUT76+jEZx8lV2dapR+CmczQI43tV9btsd1xiBbBHAKvymm9Ep9bPzM" +
485             "J0MQi+OtURL9Lxke/70/MRueqbPeUlOaGvANTmXQD2OnW7PISwJ9lpeLfTG0LcqkoqkbtLKQLYHI" +
486             "rQfV5j0j+wmvmpMxzjN3uvNajLa4zQ8l0Eok9SFaRr2RL0gN8Q2JegfOL4pUiHPsh64WWya2NB7f" +
487             "V+1s65eA5ospXYsShRjo046QhGTmymwXXzdzuxu8IlnTEont6P4+J+GsWk6cldGbl20hctuUKzyx" +
488             "OptjEPOKejV60iDCYGmHbCWAzQ8h5MILV82IclzNViZmzAapeeCnexhpXhWTs+xDEYSKEiG/camt" +
489             "bhmZc3BcyVJrW23PktSfpBQ6D8ZxoMfF0L7V2GQMaUg+3r7ucrx82kpqotjv0xHghNIm95aBr1Qw" +
490             "1gaEjsC/0wGmmBDg1dTDH+F1p9TInzr3EFuYD0YiQ7YlAHq3cPuyGoLXJ5dXYuSBfhDXJSeddUkl" +
491             "k1ufZyOOcskeInQge7jzaRfmKg3U94r+spMEvb0AzDQVOKvjjo1ivxMSgFRZaDb/4qw=";
492
493         private static final String PASSWORD = "android";
494
495         /**
496          * Loads a keystore from a base64-encoded String. Returns the KeyManager[]
497          * for the result.
498          */
499         private KeyManager[] getKeyManagers() throws Exception {
500             byte[] bytes = Base64.decode(SERVER_KEYS_BKS, Base64.DEFAULT);
501             InputStream inputStream = new ByteArrayInputStream(bytes);
502
503             KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
504             keyStore.load(inputStream, PASSWORD.toCharArray());
505             inputStream.close();
506
507             String algorithm = KeyManagerFactory.getDefaultAlgorithm();
508             KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(algorithm);
509             keyManagerFactory.init(keyStore, PASSWORD.toCharArray());
510
511             return keyManagerFactory.getKeyManagers();
512         }
513
514
515         public ServerThread(TestWebServer server, boolean ssl) throws Exception {
516             super("ServerThread");
517             mServer = server;
518             mIsSsl = ssl;
519             int retry = 3;
520             while (true) {
521                 try {
522                     if (mIsSsl) {
523                         mSslContext = SSLContext.getInstance("TLS");
524                         mSslContext.init(getKeyManagers(), null, null);
525                         mSocket = mSslContext.getServerSocketFactory().createServerSocket(
526                                 SSL_SERVER_PORT);
527                     } else {
528                         mSocket = new ServerSocket(SERVER_PORT);
529                     }
530                     return;
531                 } catch (IOException e) {
532                     Log.w(TAG, e);
533                     if (--retry == 0) {
534                         throw e;
535                     }
536                     // sleep in case server socket is still being closed
537                     Thread.sleep(1000);
538                 }
539             }
540         }
541
542         @Override
543         public void run() {
544             HttpParams params = new BasicHttpParams();
545             params.setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_0);
546             while (!mIsCancelled) {
547                 try {
548                     Socket socket = mSocket.accept();
549                     DefaultHttpServerConnection conn = new DefaultHttpServerConnection();
550                     conn.bind(socket, params);
551
552                     // Determine whether we need to shutdown early before
553                     // parsing the response since conn.close() will crash
554                     // for SSL requests due to UnsupportedOperationException.
555                     HttpRequest request = conn.receiveRequestHeader();
556                     if (isShutdownRequest(request)) {
557                         mIsCancelled = true;
558                     }
559
560                     HttpResponse response = mServer.getResponse(request);
561                     conn.sendResponseHeader(response);
562                     conn.sendResponseEntity(response);
563                     conn.close();
564
565                 } catch (IOException e) {
566                     // normal during shutdown, ignore
567                     Log.w(TAG, e);
568                 } catch (HttpException e) {
569                     Log.w(TAG, e);
570                 } catch (InterruptedException e) {
571                     Log.w(TAG, e);
572                 } catch (UnsupportedOperationException e) {
573                     // DefaultHttpServerConnection's close() throws an
574                     // UnsupportedOperationException.
575                     Log.w(TAG, e);
576                 }
577             }
578             try {
579                 mSocket.close();
580             } catch (IOException ignored) {
581                 // safe to ignore
582             }
583         }
584
585         private boolean isShutdownRequest(HttpRequest request) {
586             RequestLine requestLine = request.getRequestLine();
587             String uriString = requestLine.getUri();
588             URI uri = URI.create(uriString);
589             String path = uri.getPath();
590             return path.equals(SHUTDOWN_PREFIX);
591         }
592     }
593 }