2 * Copyright (C) 2007 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package tests.support;
20 import java.lang.Thread;
22 import java.text.SimpleDateFormat;
24 import java.util.concurrent.ConcurrentHashMap;
25 import java.util.logging.Logger;
28 * TestWebServer is a simulated controllable test server that
29 * can respond to requests from HTTP clients.
31 * The server can be controlled to change how it reacts to any
32 * requests, and can be told to simulate various events (such as
33 * network failure) that would happen in a real environment.
35 public class Support_TestWebServer implements Support_HttpConstants {
37 /* static class data/methods */
39 /* The ANDROID_LOG_TAG */
40 private final static String LOGTAG = "httpsv";
42 /** maps the recently requested URLs to the full request snapshot */
43 private final Map<String, Request> pathToRequest
44 = new ConcurrentHashMap<String, Request>();
46 /* timeout on client connections */
49 /* Default socket timeout value */
50 final static int DEFAULT_TIMEOUT = 5000;
52 /* Version string (configurable) */
53 protected String HTTP_VERSION_STRING = "HTTP/1.1";
55 /* Indicator for whether this server is configured as a HTTP/1.1
58 private boolean http11 = true;
60 /* The thread handling new requests from clients */
61 private AcceptThread acceptT;
63 /* timeout on client connections */
69 /* Switch on/off logging */
72 /* If set, this will keep connections alive after a request has been
75 boolean keepAlive = true;
77 /* If set, this will cause response data to be sent in 'chunked' format */
78 boolean chunked = false;
79 int maxChunkSize = 1024;
81 /* If set, this will indicate a new redirection host */
82 String redirectHost = null;
84 /* If set, this indicates the reason for redirection */
85 int redirectCode = -1;
87 /* Set the number of connections the server will accept before shutdown */
88 int acceptLimit = 100;
90 /* Count of number of accepted connections */
91 int acceptedConnections = 0;
93 public Support_TestWebServer() {
97 * Initialize a new server with default port and timeout.
98 * @param log Set true if you want trace output
100 public int initServer(boolean log) throws Exception {
101 return initServer(0, DEFAULT_TIMEOUT, log);
105 * Initialize a new server with default timeout.
106 * @param port Sets the server to listen on this port, or 0 to let the OS choose.
107 * Hard-coding ports is evil, so always pass 0.
108 * @param log Set true if you want trace output
110 public int initServer(int port, boolean log) throws Exception {
111 return initServer(port, DEFAULT_TIMEOUT, log);
115 * Initialize a new server with default timeout and disabled log.
116 * @param port Sets the server to listen on this port, or 0 to let the OS choose.
117 * Hard-coding ports is evil, so always pass 0.
118 * @param servePath the path to the dynamic web test data
119 * @param contentType the type of the dynamic web test data
121 public int initServer(int port, String servePath, String contentType)
123 Support_TestWebData.initDynamicTestWebData(servePath, contentType);
124 return initServer(port, DEFAULT_TIMEOUT, false);
128 * Initialize a new server with default port and timeout.
129 * @param port Sets the server to listen on this port, or 0 to let the OS choose.
130 * Hard-coding ports is evil, so always pass 0.
131 * @param timeout Indicates the period of time to wait until a socket is
133 * @param log Set true if you want trace output
135 public int initServer(int port, int timeout, boolean log) throws Exception {
139 if (acceptT == null) {
140 acceptT = new AcceptThread();
141 mPort = acceptT.init(port);
148 * Print to the log file (if logging enabled)
149 * @param s String to send to the log
151 protected void log(String s) {
153 Logger.global.fine(s);
158 * Set the server to be an HTTP/1.0 or HTTP/1.1 server.
159 * This should be called prior to any requests being sent
161 * @param set True for the server to be HTTP/1.1, false for HTTP/1.0
163 public void setHttpVersion11(boolean set) {
166 HTTP_VERSION_STRING = "HTTP/1.1";
168 HTTP_VERSION_STRING = "HTTP/1.0";
173 * Call this to determine whether server connection should remain open
174 * @param value Set true to keep connections open after a request
177 public void setKeepAlive(boolean value) {
182 * Call this to indicate whether chunked data should be used
183 * @param value Set true to make server respond with chunk encoded
186 public void setChunked(boolean value) {
191 * Sets the maximum byte count of any chunk if the server is using
192 * the "chunked" transfer encoding.
194 public void setMaxChunkSize(int maxChunkSize) {
195 this.maxChunkSize = maxChunkSize;
199 * Call this to specify the maximum number of sockets to accept
200 * @param limit The number of sockets to accept
202 public void setAcceptLimit(int limit) {
207 * Call this to indicate redirection port requirement.
208 * When this value is set, the server will respond to a request with
209 * a redirect code with the Location response header set to the value
211 * @param redirect The location to be redirected to
212 * @param code The code to send when redirecting
214 public void setRedirect(String redirect, int code) {
215 redirectHost = redirect;
217 log("Server will redirect output to "+redirect+" code "+code);
221 * Returns a map from recently-requested paths (like "/index.html") to a
222 * snapshot of the request data.
224 public Map<String, Request> pathToRequest() {
225 return pathToRequest;
229 * Cause the thread accepting connections on the server socket to close
231 public void close() {
232 /* Stop the Accept thread */
233 if (acceptT != null) {
234 log("Closing AcceptThread"+acceptT);
240 * The AcceptThread is responsible for initiating worker threads
241 * to handle incoming requests from clients.
243 class AcceptThread extends Thread {
245 ServerSocket ss = null;
246 boolean running = false;
249 * @param port the port to use, or 0 to let the OS choose.
250 * Hard-coding ports is evil, so always pass 0!
252 public int init(int port) throws IOException {
253 ss = new ServerSocket(port);
254 ss.setSoTimeout(5000);
255 ss.setReuseAddress(true);
256 return ss.getLocalPort();
260 * Main thread responding to new connections
262 public synchronized void run() {
266 // Log.d(LOGTAG, "TestWebServer run() calling accept()");
267 Socket s = ss.accept();
268 acceptedConnections++;
269 if (acceptedConnections >= acceptLimit) {
273 new Thread(new Worker(s), "additional worker").start();
275 } catch (SocketException e) {
276 log("SocketException in AcceptThread: probably closed during accept");
278 } catch (IOException e) {
279 log("IOException in AcceptThread");
282 log("AcceptThread terminated" + this);
286 public void close() {
289 /* Stop server socket from processing further. Currently
290 this does not cause the SocketException from ss.accept
291 therefore the acceptLimit functionality has been added
292 to circumvent this limitation */
294 } catch (IOException e) {
295 /* We are shutting down the server, so we expect
296 * things to die. Don't propagate.
298 log("IOException caught by server socket close");
303 // Size of buffer for reading from the connection
304 final static int BUF_SIZE = 2048;
306 /* End of line byte sequence */
307 static final byte[] EOL = {(byte)'\r', (byte)'\n' };
310 * An immutable snapshot of an HTTP request.
312 public static class Request {
313 private final String path;
314 private final Map<String, String> headers;
315 // TODO: include posted content?
317 public Request(String path, Map<String, String> headers) {
319 this.headers = new LinkedHashMap<String, String>(headers);
322 public String getPath() {
326 public Map<String, String> getHeaders() {
332 * The worker thread handles all interactions with a current open
333 * connection. If pipelining is turned on, this will allow this
334 * thread to continuously operate on numerous requests before the
335 * connection is closed.
337 class Worker implements Support_HttpConstants, Runnable {
339 /* buffer to use to hold request data */
342 /* Socket to client we're handling */
345 /* Reference to current request method ID */
346 private int requestMethod;
348 /* Reference to current requests test file/data */
349 private String testID;
351 /* The requested path, such as "/test1" */
354 /* Reference to test number from testID */
357 /* Reference to whether new request has been initiated yet */
358 private boolean readStarted;
360 /* Indicates whether current request has any data content */
361 private boolean hasContent = false;
363 /* Request headers are stored here */
364 private Map<String, String> headers = new LinkedHashMap<String, String>();
366 /* Create a new worker thread */
368 this.buf = new byte[BUF_SIZE];
372 public synchronized void run() {
375 } catch (Exception e) {
376 log("Exception during handleClient in the TestWebServer: " + e.getMessage());
378 log(this+" terminated");
382 * Zero out the buffer from last time
384 private void clearBuffer() {
385 for (int i = 0; i < BUF_SIZE; i++) {
391 * Utility method to read a line of data from the input stream
392 * @param is Inputstream to read
393 * @return number of bytes read
395 private int readOneLine(InputStream is) {
401 log("Reading one line: started ="+readStarted+" avail="+is.available());
402 StringBuilder log = new StringBuilder();
403 while ((!readStarted) || (is.available() > 0)) {
404 int data = is.read();
405 // We shouldn't get EOF but we need tdo check
411 buf[read] = (byte)data;
413 log.append((char)data);
416 if (buf[read++]==(byte)'\n') {
421 } catch (IOException e) {
422 log("IOException from readOneLine");
428 * Read a chunk of data
429 * @param is Stream from which to read data
430 * @param length Amount of data to read
431 * @return number of bytes read
433 private int readData(InputStream is, int length) {
436 // At the moment we're only expecting small data amounts
437 byte[] buf = new byte[length];
440 while (is.available() > 0) {
441 count = is.read(buf, read, length-read);
444 } catch (IOException e) {
445 log("IOException from readData");
451 * Read the status line from the input stream extracting method
453 * @param is Inputstream to read
454 * @return number of bytes read
456 private int parseStatusLine(InputStream is) {
460 log("Parse status line");
461 // Check for status line first
462 nread = readOneLine(is);
463 // Bomb out if stream closes prematurely
465 requestMethod = UNKNOWN_METHOD;
469 if (buf[0] == (byte)'G' &&
470 buf[1] == (byte)'E' &&
471 buf[2] == (byte)'T' &&
472 buf[3] == (byte)' ') {
473 requestMethod = GET_METHOD;
476 } else if (buf[0] == (byte)'H' &&
477 buf[1] == (byte)'E' &&
478 buf[2] == (byte)'A' &&
479 buf[3] == (byte)'D' &&
480 buf[4] == (byte)' ') {
481 requestMethod = HEAD_METHOD;
484 } else if (buf[0] == (byte)'P' &&
485 buf[1] == (byte)'O' &&
486 buf[2] == (byte)'S' &&
487 buf[3] == (byte)'T' &&
488 buf[4] == (byte)' ') {
489 requestMethod = POST_METHOD;
494 requestMethod = UNKNOWN_METHOD;
498 // A valid method we understand
499 if (requestMethod > UNKNOWN_METHOD) {
502 while (buf[i] != (byte)' ') {
503 // There should be HTTP/1.x at the end
504 if ((buf[i] == (byte)'\n') || (buf[i] == (byte)'\r')) {
505 requestMethod = UNKNOWN_METHOD;
511 path = new String(buf, 0, index, i-index);
512 testID = path.substring(1);
520 * Read a header from the input stream
521 * @param is Inputstream to read
522 * @return number of bytes read
524 private int parseHeader(InputStream is) {
527 log("Parse a header");
528 // Check for status line first
529 nread = readOneLine(is);
530 // Bomb out if stream closes prematurely
532 requestMethod = UNKNOWN_METHOD;
535 // Read header entry 'Header: data'
537 while (buf[i] != (byte)':') {
538 // There should be an entry after the header
540 if ((buf[i] == (byte)'\n') || (buf[i] == (byte)'\r')) {
541 return UNKNOWN_METHOD;
546 String headerName = new String(buf, 0, i);
548 while (buf[i] == ' ') {
551 String headerValue = new String(buf, i, nread - i - 2); // drop \r\n
553 headers.put(headerName, headerValue);
558 * Read all headers from the input stream
559 * @param is Inputstream to read
560 * @return number of bytes read
562 private int readHeaders(InputStream is) {
565 // Headers should be terminated by empty CRLF line
568 headerLen = parseHeader(is);
572 if (headerLen <= 2) {
579 * Read content data from the input stream
580 * @param is Inputstream to read
581 * @return number of bytes read
583 private int readContent(InputStream is) {
586 String lengthString = headers.get(requestHeaders[REQ_CONTENT_LENGTH]);
587 int length = new Integer(lengthString).intValue();
590 length = readData(is, length);
595 * The main loop, reading requests.
597 void handleClient() throws IOException {
598 InputStream is = new BufferedInputStream(s.getInputStream());
599 PrintStream ps = new PrintStream(s.getOutputStream());
602 /* we will only block in read for this many milliseconds
603 * before we fail with java.io.InterruptedIOException,
604 * at which point we will abandon the connection.
606 s.setSoTimeout(mTimeout);
607 s.setTcpNoDelay(true);
610 nread = parseStatusLine(is);
611 if (requestMethod != UNKNOWN_METHOD) {
613 // If status line found, read any headers
614 nread = readHeaders(is);
616 pathToRequest().put(path, new Request(path, headers));
618 // Then read content (if any)
619 // TODO handle chunked encoding from the client
620 if (headers.get(requestHeaders[REQ_CONTENT_LENGTH]) != null) {
621 nread = readContent(is);
625 /* we don't support this method */
626 ps.print(HTTP_VERSION_STRING + " " + HTTP_BAD_METHOD +
627 " unsupported method type: ");
633 if (!keepAlive || nread <= 0) {
637 log("SOCKET CLOSED");
643 // Reset test number prior to outputing data
646 // Write out the data
650 // Write line between headers and body
654 if (redirectCode == -1) {
655 switch (requestMethod) {
657 if ((testNum < -1) || (testNum > Support_TestWebData.tests.length - 1)) {
667 // Post method write body data
668 if ((testNum > 0) || (testNum < Support_TestWebData.tests.length - 1)) {
676 } else { // Redirecting
677 switch (redirectCode) {
679 // Seems 301 needs a body by neon (although spec
681 psPrint(ps, Support_TestWebData.testServerResponse[Support_TestWebData.REDIRECT_301]);
685 psPrint(ps, Support_TestWebData.testServerResponse[Support_TestWebData.REDIRECT_302]);
688 psPrint(ps, Support_TestWebData.testServerResponse[Support_TestWebData.REDIRECT_303]);
691 psPrint(ps, Support_TestWebData.testServerResponse[Support_TestWebData.REDIRECT_307]);
700 // Reset for next request
706 log("SOCKET CLOSED");
710 // Print string to log and output stream
711 void psPrint(PrintStream ps, String s) throws IOException {
716 // Print bytes to log and output stream
717 void psWrite(PrintStream ps, byte[] bytes, int offset, int count) throws IOException {
718 log(new String(bytes));
719 ps.write(bytes, offset, count);
722 // Print CRLF to log and output stream
723 void psWriteEOL(PrintStream ps) throws IOException {
729 // Print status to log and output stream
730 void printStatus(PrintStream ps) throws IOException {
731 // Handle redirects first.
732 if (redirectCode != -1) {
733 log("REDIRECTING TO "+redirectHost+" status "+redirectCode);
734 psPrint(ps, HTTP_VERSION_STRING + " " + redirectCode +" Moved permanently");
736 psPrint(ps, "Location: " + redirectHost);
742 if (testID.startsWith("test")) {
743 testNum = Integer.valueOf(testID.substring(4))-1;
746 if ((testNum < -1) || (testNum > Support_TestWebData.tests.length - 1)) {
747 psPrint(ps, HTTP_VERSION_STRING + " " + HTTP_NOT_FOUND + " not found");
750 psPrint(ps, HTTP_VERSION_STRING + " " + HTTP_OK+" OK");
757 * Create the server response and output to the stream
758 * @param ps The PrintStream to output response headers and data to
760 void printHeaders(PrintStream ps) throws IOException {
761 if ((testNum < -1) || (testNum > Support_TestWebData.tests.length - 1)) {
762 // 404 status already sent
765 SimpleDateFormat df = new SimpleDateFormat("EE, dd MMM yyyy HH:mm:ss");
767 psPrint(ps,"Server: TestWebServer"+mPort);
769 psPrint(ps, "Date: " + df.format(new Date()));
771 psPrint(ps, "Connection: " + ((keepAlive) ? "Keep-Alive" : "Close"));
774 // Yuk, if we're not redirecting, we add the file details
775 if (redirectCode == -1) {
778 if (!Support_TestWebData.test0DataAvailable) {
779 log("testdata was not initilaized");
783 psPrint(ps, "Transfer-Encoding: chunked");
785 psPrint(ps, "Content-length: "
786 + Support_TestWebData.test0Data.length);
790 psPrint(ps, "Last Modified: " + (new Date(
791 Support_TestWebData.test0Params.testLastModified)));
794 psPrint(ps, "Content-type: "
795 + Support_TestWebData.test0Params.testType);
798 if (Support_TestWebData.testParams[testNum].testExp > 0) {
800 exp = Support_TestWebData.testParams[testNum].testExp;
801 psPrint(ps, "expires: "
802 + df.format(exp) + " GMT");
805 } else if (!Support_TestWebData.testParams[testNum].testDir) {
807 psPrint(ps, "Transfer-Encoding: chunked");
809 psPrint(ps, "Content-length: "+Support_TestWebData.testParams[testNum].testLength);
813 psPrint(ps,"Last Modified: " + (new
814 Date(Support_TestWebData.testParams[testNum].testLastModified)));
817 psPrint(ps, "Content-type: " + Support_TestWebData.testParams[testNum].testType);
820 if (Support_TestWebData.testParams[testNum].testExp > 0) {
822 exp = Support_TestWebData.testParams[testNum].testExp;
823 psPrint(ps, "expires: "
824 + df.format(exp) + " GMT");
828 psPrint(ps, "Content-type: text/html");
832 // Content-length of 301, 302, 303, 307 are the same.
833 psPrint(ps, "Content-length: "+(Support_TestWebData.testServerResponse[Support_TestWebData.REDIRECT_301]).length());
842 * Sends the 404 not found message
843 * @param ps The PrintStream to write to
845 void send404(PrintStream ps) throws IOException {
846 ps.println("Not Found\n\n"+
847 "The requested resource was not found.\n");
851 * Sends the data associated with the headers
852 * @param ps The PrintStream to write to
854 void sendFile(PrintStream ps) throws IOException {
856 if (!Support_TestWebData.test0DataAvailable) {
857 log("test data was not initialized");
860 sendFile(ps, Support_TestWebData.test0Data);
862 sendFile(ps, Support_TestWebData.tests[testNum]);
866 void sendFile(PrintStream ps, byte[] bytes) throws IOException {
869 while (offset < bytes.length) {
870 int chunkSize = Math.min(bytes.length - offset, maxChunkSize);
871 psPrint(ps, Integer.toHexString(chunkSize));
873 psWrite(ps, bytes, offset, chunkSize);
881 psWrite(ps, bytes, 0, bytes.length);