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.
19 import android.util.Log;
22 import java.lang.Thread;
27 * TestWebServer is a simulated controllable test server that
28 * can respond to requests from HTTP clients.
30 * The server can be controlled to change how it reacts to any
31 * requests, and can be told to simulate various events (such as
32 * network failure) that would happen in a real environment.
34 class TestWebServer implements HttpConstants {
36 /* static class data/methods */
38 /* The ANDROID_LOG_TAG */
39 private final static String LOGTAG = "httpsv";
41 /* Where worker threads stand idle */
42 Vector threads = new Vector();
44 /* List of all active worker threads */
45 Vector activeThreads = new Vector();
47 /* timeout on client connections */
50 /* max # worker threads */
53 /* Default port for this server to listen on */
54 final static int DEFAULT_PORT = 8080;
56 /* Default socket timeout value */
57 final static int DEFAULT_TIMEOUT = 5000;
59 /* Version string (configurable) */
60 protected String HTTP_VERSION_STRING = "HTTP/1.1";
62 /* Indicator for whether this server is configured as a HTTP/1.1
65 private boolean http11 = true;
67 /* The thread handling new requests from clients */
68 private AcceptThread acceptT;
70 /* timeout on client connections */
76 /* Switch on/off logging */
79 /* If set, this will keep connections alive after a request has been
82 boolean keepAlive = true;
84 /* If set, this will cause response data to be sent in 'chunked' format */
85 boolean chunked = false;
87 /* If set, this will indicate a new redirection host */
88 String redirectHost = null;
90 /* If set, this indicates the reason for redirection */
91 int redirectCode = -1;
93 /* Set the number of connections the server will accept before shutdown */
94 int acceptLimit = 100;
96 /* Count of number of accepted connections */
97 int acceptedConnections = 0;
99 public TestWebServer() {
103 * Initialize a new server with default port and timeout.
104 * @param log Set true if you want trace output
106 public void initServer(boolean log) throws Exception {
107 initServer(DEFAULT_PORT, DEFAULT_TIMEOUT, log);
111 * Initialize a new server with default timeout.
112 * @param port Sets the server to listen on this port
113 * @param log Set true if you want trace output
115 public void initServer(int port, boolean log) throws Exception {
116 initServer(port, DEFAULT_TIMEOUT, log);
120 * Initialize a new server with default port and timeout.
121 * @param port Sets the server to listen on this port
122 * @param timeout Indicates the period of time to wait until a socket is
124 * @param log Set true if you want trace output
126 public void initServer(int port, int timeout, boolean log) throws Exception {
132 if (acceptT == null) {
133 acceptT = new AcceptThread();
140 * Print to the log file (if logging enabled)
141 * @param s String to send to the log
143 protected void log(String s) {
150 * Set the server to be an HTTP/1.0 or HTTP/1.1 server.
151 * This should be called prior to any requests being sent
153 * @param set True for the server to be HTTP/1.1, false for HTTP/1.0
155 public void setHttpVersion11(boolean set) {
158 HTTP_VERSION_STRING = "HTTP/1.1";
160 HTTP_VERSION_STRING = "HTTP/1.0";
165 * Call this to determine whether server connection should remain open
166 * @param value Set true to keep connections open after a request
169 public void setKeepAlive(boolean value) {
174 * Call this to indicate whether chunked data should be used
175 * @param value Set true to make server respond with chunk encoded
178 public void setChunked(boolean value) {
183 * Call this to specify the maximum number of sockets to accept
184 * @param limit The number of sockets to accept
186 public void setAcceptLimit(int limit) {
191 * Call this to indicate redirection port requirement.
192 * When this value is set, the server will respond to a request with
193 * a redirect code with the Location response header set to the value
195 * @param redirect The location to be redirected to
196 * @param redirectCode The code to send when redirecting
198 public void setRedirect(String redirect, int code) {
199 redirectHost = redirect;
201 log("Server will redirect output to "+redirect+" code "+code);
205 * Cause the thread accepting connections on the server socket to close
207 public void close() {
208 /* Stop the Accept thread */
209 if (acceptT != null) {
210 log("Closing AcceptThread"+acceptT);
216 * The AcceptThread is responsible for initiating worker threads
217 * to handle incoming requests from clients.
219 class AcceptThread extends Thread {
221 ServerSocket ss = null;
222 boolean running = false;
225 // Networking code doesn't support ServerSocket(port) yet
226 InetSocketAddress ia = new InetSocketAddress(mPort);
229 ss = new ServerSocket();
230 // Socket timeout functionality is not available yet
231 //ss.setSoTimeout(5000);
232 ss.setReuseAddress(true);
235 } catch (IOException e) {
236 log("IOException in AcceptThread.init()");
241 } catch (InterruptedException e1) {
242 // TODO Auto-generated catch block
243 e1.printStackTrace();
250 * Main thread responding to new connections
252 public synchronized void run() {
256 // Log.d(LOGTAG, "TestWebServer run() calling accept()");
257 Socket s = ss.accept();
258 acceptedConnections++;
259 if (acceptedConnections >= acceptLimit) {
264 synchronized (threads) {
265 if (threads.isEmpty()) {
266 Worker ws = new Worker();
268 activeThreads.addElement(ws);
269 (new Thread(ws, "additional worker")).start();
271 w = (Worker) threads.elementAt(0);
272 threads.removeElementAt(0);
277 } catch (SocketException e) {
278 log("SocketException in AcceptThread: probably closed during accept");
280 } catch (IOException e) {
281 log("IOException in AcceptThread");
285 log("AcceptThread terminated" + this);
289 public void close() {
292 /* Stop server socket from processing further. Currently
293 this does not cause the SocketException from ss.accept
294 therefore the acceptLimit functionality has been added
295 to circumvent this limitation */
298 // Stop worker threads from continuing
299 for (Enumeration e = activeThreads.elements(); e.hasMoreElements();) {
300 Worker w = (Worker)e.nextElement();
303 activeThreads.clear();
305 } catch (IOException e) {
306 /* We are shutting down the server, so we expect
307 * things to die. Don't propagate.
309 log("IOException caught by server socket close");
314 // Size of buffer for reading from the connection
315 final static int BUF_SIZE = 2048;
317 /* End of line byte sequence */
318 static final byte[] EOL = {(byte)'\r', (byte)'\n' };
321 * The worker thread handles all interactions with a current open
322 * connection. If pipelining is turned on, this will allow this
323 * thread to continuously operate on numerous requests before the
324 * connection is closed.
326 class Worker implements HttpConstants, Runnable {
328 /* buffer to use to hold request data */
331 /* Socket to client we're handling */
334 /* Reference to current request method ID */
335 private int requestMethod;
337 /* Reference to current requests test file/data */
338 private String testID;
340 /* Reference to test number from testID */
343 /* Reference to whether new request has been initiated yet */
344 private boolean readStarted;
346 /* Indicates whether current request has any data content */
347 private boolean hasContent = false;
349 boolean running = false;
351 /* Request headers are stored here */
352 private Hashtable<String, String> headers = new Hashtable<String, String>();
354 /* Create a new worker thread */
356 buf = new byte[BUF_SIZE];
361 * Called by the AcceptThread to unblock this Worker to process
363 * @param s The socket on which the connection has been made
365 synchronized void setSocket(Socket s) {
371 * Called by the accept thread when it's closing. Potentially unblocks
372 * the worker thread to terminate properly
374 synchronized void close() {
380 * Main worker thread. This will wait until a request has
381 * been identified by the accept thread upon which it will
382 * service the thread.
384 public synchronized void run() {
390 log(this+" Moving to wait state");
392 } catch (InterruptedException e) {
393 /* should not happen */
400 } catch (Exception e) {
403 /* go back in wait queue if there's fewer
404 * than numHandler connections.
407 Vector pool = threads;
408 synchronized (pool) {
409 if (pool.size() >= workers) {
410 /* too many threads, exit this one */
411 activeThreads.remove(this);
414 pool.addElement(this);
418 log(this+" terminated");
422 * Zero out the buffer from last time
424 private void clearBuffer() {
425 for (int i = 0; i < BUF_SIZE; i++) {
431 * Utility method to read a line of data from the input stream
432 * @param is Inputstream to read
433 * @return number of bytes read
435 private int readOneLine(InputStream is) {
441 log("Reading one line: started ="+readStarted+" avail="+is.available());
442 while ((!readStarted) || (is.available() > 0)) {
443 int data = is.read();
444 // We shouldn't get EOF but we need tdo check
450 buf[read] = (byte)data;
452 System.out.print((char)data);
455 if (buf[read++]==(byte)'\n') {
456 System.out.println();
460 } catch (IOException e) {
461 log("IOException from readOneLine");
468 * Read a chunk of data
469 * @param is Stream from which to read data
470 * @param length Amount of data to read
471 * @return number of bytes read
473 private int readData(InputStream is, int length) {
476 // At the moment we're only expecting small data amounts
477 byte[] buf = new byte[length];
480 while (is.available() > 0) {
481 count = is.read(buf, read, length-read);
484 } catch (IOException e) {
485 log("IOException from readData");
492 * Read the status line from the input stream extracting method
494 * @param is Inputstream to read
495 * @return number of bytes read
497 private int parseStatusLine(InputStream is) {
501 log("Parse status line");
502 // Check for status line first
503 nread = readOneLine(is);
504 // Bomb out if stream closes prematurely
506 requestMethod = UNKNOWN_METHOD;
510 if (buf[0] == (byte)'G' &&
511 buf[1] == (byte)'E' &&
512 buf[2] == (byte)'T' &&
513 buf[3] == (byte)' ') {
514 requestMethod = GET_METHOD;
517 } else if (buf[0] == (byte)'H' &&
518 buf[1] == (byte)'E' &&
519 buf[2] == (byte)'A' &&
520 buf[3] == (byte)'D' &&
521 buf[4] == (byte)' ') {
522 requestMethod = HEAD_METHOD;
525 } else if (buf[0] == (byte)'P' &&
526 buf[1] == (byte)'O' &&
527 buf[2] == (byte)'S' &&
528 buf[3] == (byte)'T' &&
529 buf[4] == (byte)' ') {
530 requestMethod = POST_METHOD;
535 requestMethod = UNKNOWN_METHOD;
539 // A valid method we understand
540 if (requestMethod > UNKNOWN_METHOD) {
543 while (buf[i] != (byte)' ') {
544 // There should be HTTP/1.x at the end
545 if ((buf[i] == (byte)'\n') || (buf[i] == (byte)'\r')) {
546 requestMethod = UNKNOWN_METHOD;
552 testID = new String(buf, 0, index, i-index);
553 if (testID.startsWith("/")) {
554 testID = testID.substring(1);
563 * Read a header from the input stream
564 * @param is Inputstream to read
565 * @return number of bytes read
567 private int parseHeader(InputStream is) {
570 log("Parse a header");
571 // Check for status line first
572 nread = readOneLine(is);
573 // Bomb out if stream closes prematurely
575 requestMethod = UNKNOWN_METHOD;
578 // Read header entry 'Header: data'
580 while (buf[i] != (byte)':') {
581 // There should be an entry after the header
583 if ((buf[i] == (byte)'\n') || (buf[i] == (byte)'\r')) {
584 return UNKNOWN_METHOD;
589 String headerName = new String(buf, 0, i);
591 while (buf[i] == ' ') {
594 String headerValue = new String(buf, i, nread-1);
596 headers.put(headerName, headerValue);
601 * Read all headers from the input stream
602 * @param is Inputstream to read
603 * @return number of bytes read
605 private int readHeaders(InputStream is) {
608 // Headers should be terminated by empty CRLF line
611 headerLen = parseHeader(is);
615 if (headerLen <= 2) {
622 * Read content data from the input stream
623 * @param is Inputstream to read
624 * @return number of bytes read
626 private int readContent(InputStream is) {
629 String lengthString = headers.get(requestHeaders[REQ_CONTENT_LENGTH]);
630 int length = new Integer(lengthString).intValue();
633 length = readData(is, length);
638 * The main loop, reading requests.
640 void handleClient() throws IOException {
641 InputStream is = new BufferedInputStream(s.getInputStream());
642 PrintStream ps = new PrintStream(s.getOutputStream());
645 /* we will only block in read for this many milliseconds
646 * before we fail with java.io.InterruptedIOException,
647 * at which point we will abandon the connection.
649 s.setSoTimeout(mTimeout);
650 s.setTcpNoDelay(true);
653 nread = parseStatusLine(is);
654 if (requestMethod != UNKNOWN_METHOD) {
656 // If status line found, read any headers
657 nread = readHeaders(is);
659 // Then read content (if any)
660 // TODO handle chunked encoding from the client
661 if (headers.get(requestHeaders[REQ_CONTENT_LENGTH]) != null) {
662 nread = readContent(is);
666 /* we don't support this method */
667 ps.print(HTTP_VERSION_STRING + " " + HTTP_BAD_METHOD +
668 " unsupported method type: ");
674 if (!keepAlive || nread <= 0) {
678 log("SOCKET CLOSED");
684 // Reset test number prior to outputing data
687 // Write out the data
691 // Write line between headers and body
695 if (redirectCode == -1) {
696 switch (requestMethod) {
698 if ((testNum < 0) || (testNum > TestWebData.tests.length - 1)) {
708 // Post method write body data
709 if ((testNum > 0) || (testNum < TestWebData.tests.length - 1)) {
717 } else { // Redirecting
718 switch (redirectCode) {
720 // Seems 301 needs a body by neon (although spec
722 psPrint(ps, TestWebData.testServerResponse[TestWebData.REDIRECT_301]);
726 psPrint(ps, TestWebData.testServerResponse[TestWebData.REDIRECT_302]);
729 psPrint(ps, TestWebData.testServerResponse[TestWebData.REDIRECT_303]);
732 psPrint(ps, TestWebData.testServerResponse[TestWebData.REDIRECT_307]);
741 // Reset for next request
747 log("SOCKET CLOSED");
751 // Print string to log and output stream
752 void psPrint(PrintStream ps, String s) throws IOException {
757 // Print bytes to log and output stream
758 void psWrite(PrintStream ps, byte[] bytes, int len) throws IOException {
759 log(new String(bytes));
760 ps.write(bytes, 0, len);
763 // Print CRLF to log and output stream
764 void psWriteEOL(PrintStream ps) throws IOException {
770 // Print status to log and output stream
771 void printStatus(PrintStream ps) throws IOException {
772 // Handle redirects first.
773 if (redirectCode != -1) {
774 log("REDIRECTING TO "+redirectHost+" status "+redirectCode);
775 psPrint(ps, HTTP_VERSION_STRING + " " + redirectCode +" Moved permanently");
777 psPrint(ps, "Location: " + redirectHost);
783 if (testID.startsWith("test")) {
784 testNum = Integer.valueOf(testID.substring(4))-1;
787 if ((testNum < 0) || (testNum > TestWebData.tests.length - 1)) {
788 psPrint(ps, HTTP_VERSION_STRING + " " + HTTP_NOT_FOUND + " not found");
791 psPrint(ps, HTTP_VERSION_STRING + " " + HTTP_OK+" OK");
798 * Create the server response and output to the stream
799 * @param ps The PrintStream to output response headers and data to
801 void printHeaders(PrintStream ps) throws IOException {
802 psPrint(ps,"Server: TestWebServer"+mPort);
804 psPrint(ps, "Date: " + (new Date()));
806 psPrint(ps, "Connection: " + ((keepAlive) ? "Keep-Alive" : "Close"));
809 // Yuk, if we're not redirecting, we add the file details
810 if (redirectCode == -1) {
812 if (!TestWebData.testParams[testNum].testDir) {
814 psPrint(ps, "Transfer-Encoding: chunked");
816 psPrint(ps, "Content-length: "+TestWebData.testParams[testNum].testLength);
820 psPrint(ps,"Last Modified: " + (new
821 Date(TestWebData.testParams[testNum].testLastModified)));
824 psPrint(ps, "Content-type: " + TestWebData.testParams[testNum].testType);
827 psPrint(ps, "Content-type: text/html");
831 // Content-length of 301, 302, 303, 307 are the same.
832 psPrint(ps, "Content-length: "+(TestWebData.testServerResponse[TestWebData.REDIRECT_301]).length());
841 * Sends the 404 not found message
842 * @param ps The PrintStream to write to
844 void send404(PrintStream ps) throws IOException {
845 ps.println("Not Found\n\n"+
846 "The requested resource was not found.\n");
850 * Sends the data associated with the headers
851 * @param ps The PrintStream to write to
853 void sendFile(PrintStream ps) throws IOException {
854 // For now just make a chunk with the whole of the test data
855 // It might be worth making this multiple chunks for large
856 // test data to test multiple chunks.
857 int dataSize = TestWebData.tests[testNum].length;
859 psPrint(ps, Integer.toHexString(dataSize));
861 psWrite(ps, TestWebData.tests[testNum], dataSize);
867 psWrite(ps, TestWebData.tests[testNum], dataSize);