/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.core; import android.util.Log; import java.io.*; import java.lang.Thread; import java.net.*; import java.util.*; /** * TestWebServer is a simulated controllable test server that * can respond to requests from HTTP clients. * * The server can be controlled to change how it reacts to any * requests, and can be told to simulate various events (such as * network failure) that would happen in a real environment. */ class TestWebServer implements HttpConstants { /* static class data/methods */ /* The ANDROID_LOG_TAG */ private final static String LOGTAG = "httpsv"; /* Where worker threads stand idle */ Vector threads = new Vector(); /* List of all active worker threads */ Vector activeThreads = new Vector(); /* timeout on client connections */ int timeout = 0; /* max # worker threads */ int workers = 5; /* Default port for this server to listen on */ final static int DEFAULT_PORT = 8080; /* Default socket timeout value */ final static int DEFAULT_TIMEOUT = 5000; /* Version string (configurable) */ protected String HTTP_VERSION_STRING = "HTTP/1.1"; /* Indicator for whether this server is configured as a HTTP/1.1 * or HTTP/1.0 server */ private boolean http11 = true; /* The thread handling new requests from clients */ private AcceptThread acceptT; /* timeout on client connections */ int mTimeout; /* Server port */ int mPort; /* Switch on/off logging */ boolean mLog = false; /* If set, this will keep connections alive after a request has been * processed. */ boolean keepAlive = true; /* If set, this will cause response data to be sent in 'chunked' format */ boolean chunked = false; /* If set, this will indicate a new redirection host */ String redirectHost = null; /* If set, this indicates the reason for redirection */ int redirectCode = -1; /* Set the number of connections the server will accept before shutdown */ int acceptLimit = 100; /* Count of number of accepted connections */ int acceptedConnections = 0; public TestWebServer() { } /** * Initialize a new server with default port and timeout. * @param log Set true if you want trace output */ public void initServer(boolean log) throws Exception { initServer(DEFAULT_PORT, DEFAULT_TIMEOUT, log); } /** * Initialize a new server with default timeout. * @param port Sets the server to listen on this port * @param log Set true if you want trace output */ public void initServer(int port, boolean log) throws Exception { initServer(port, DEFAULT_TIMEOUT, log); } /** * Initialize a new server with default port and timeout. * @param port Sets the server to listen on this port * @param timeout Indicates the period of time to wait until a socket is * closed * @param log Set true if you want trace output */ public void initServer(int port, int timeout, boolean log) throws Exception { mPort = port; mTimeout = timeout; mLog = log; keepAlive = true; if (acceptT == null) { acceptT = new AcceptThread(); acceptT.init(); acceptT.start(); } } /** * Print to the log file (if logging enabled) * @param s String to send to the log */ protected void log(String s) { if (mLog) { Log.d(LOGTAG, s); } } /** * Set the server to be an HTTP/1.0 or HTTP/1.1 server. * This should be called prior to any requests being sent * to the server. * @param set True for the server to be HTTP/1.1, false for HTTP/1.0 */ public void setHttpVersion11(boolean set) { http11 = set; if (set) { HTTP_VERSION_STRING = "HTTP/1.1"; } else { HTTP_VERSION_STRING = "HTTP/1.0"; } } /** * Call this to determine whether server connection should remain open * @param value Set true to keep connections open after a request * completes */ public void setKeepAlive(boolean value) { keepAlive = value; } /** * Call this to indicate whether chunked data should be used * @param value Set true to make server respond with chunk encoded * content data. */ public void setChunked(boolean value) { chunked = value; } /** * Call this to specify the maximum number of sockets to accept * @param limit The number of sockets to accept */ public void setAcceptLimit(int limit) { acceptLimit = limit; } /** * Call this to indicate redirection port requirement. * When this value is set, the server will respond to a request with * a redirect code with the Location response header set to the value * specified. * @param redirect The location to be redirected to * @param redirectCode The code to send when redirecting */ public void setRedirect(String redirect, int code) { redirectHost = redirect; redirectCode = code; log("Server will redirect output to "+redirect+" code "+code); } /** * Cause the thread accepting connections on the server socket to close */ public void close() { /* Stop the Accept thread */ if (acceptT != null) { log("Closing AcceptThread"+acceptT); acceptT.close(); acceptT = null; } } /** * The AcceptThread is responsible for initiating worker threads * to handle incoming requests from clients. */ class AcceptThread extends Thread { ServerSocket ss = null; boolean running = false; public void init() { // Networking code doesn't support ServerSocket(port) yet InetSocketAddress ia = new InetSocketAddress(mPort); while (true) { try { ss = new ServerSocket(); // Socket timeout functionality is not available yet //ss.setSoTimeout(5000); ss.setReuseAddress(true); ss.bind(ia); break; } catch (IOException e) { log("IOException in AcceptThread.init()"); e.printStackTrace(); // wait and retry try { Thread.sleep(1000); } catch (InterruptedException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } } } } /** * Main thread responding to new connections */ public synchronized void run() { running = true; try { while (running) { // Log.d(LOGTAG, "TestWebServer run() calling accept()"); Socket s = ss.accept(); acceptedConnections++; if (acceptedConnections >= acceptLimit) { running = false; } Worker w = null; synchronized (threads) { if (threads.isEmpty()) { Worker ws = new Worker(); ws.setSocket(s); activeThreads.addElement(ws); (new Thread(ws, "additional worker")).start(); } else { w = (Worker) threads.elementAt(0); threads.removeElementAt(0); w.setSocket(s); } } } } catch (SocketException e) { log("SocketException in AcceptThread: probably closed during accept"); running = false; } catch (IOException e) { log("IOException in AcceptThread"); e.printStackTrace(); running = false; } log("AcceptThread terminated" + this); } // Close this socket public void close() { try { running = false; /* Stop server socket from processing further. Currently this does not cause the SocketException from ss.accept therefore the acceptLimit functionality has been added to circumvent this limitation */ ss.close(); // Stop worker threads from continuing for (Enumeration e = activeThreads.elements(); e.hasMoreElements();) { Worker w = (Worker)e.nextElement(); w.close(); } activeThreads.clear(); } catch (IOException e) { /* We are shutting down the server, so we expect * things to die. Don't propagate. */ log("IOException caught by server socket close"); } } } // Size of buffer for reading from the connection final static int BUF_SIZE = 2048; /* End of line byte sequence */ static final byte[] EOL = {(byte)'\r', (byte)'\n' }; /** * The worker thread handles all interactions with a current open * connection. If pipelining is turned on, this will allow this * thread to continuously operate on numerous requests before the * connection is closed. */ class Worker implements HttpConstants, Runnable { /* buffer to use to hold request data */ byte[] buf; /* Socket to client we're handling */ private Socket s; /* Reference to current request method ID */ private int requestMethod; /* Reference to current requests test file/data */ private String testID; /* Reference to test number from testID */ private int testNum; /* Reference to whether new request has been initiated yet */ private boolean readStarted; /* Indicates whether current request has any data content */ private boolean hasContent = false; boolean running = false; /* Request headers are stored here */ private Hashtable headers = new Hashtable(); /* Create a new worker thread */ Worker() { buf = new byte[BUF_SIZE]; s = null; } /** * Called by the AcceptThread to unblock this Worker to process * a request. * @param s The socket on which the connection has been made */ synchronized void setSocket(Socket s) { this.s = s; notify(); } /** * Called by the accept thread when it's closing. Potentially unblocks * the worker thread to terminate properly */ synchronized void close() { running = false; notify(); } /** * Main worker thread. This will wait until a request has * been identified by the accept thread upon which it will * service the thread. */ public synchronized void run() { running = true; while(running) { if (s == null) { /* nothing to do */ try { log(this+" Moving to wait state"); wait(); } catch (InterruptedException e) { /* should not happen */ continue; } if (!running) break; } try { handleClient(); } catch (Exception e) { e.printStackTrace(); } /* go back in wait queue if there's fewer * than numHandler connections. */ s = null; Vector pool = threads; synchronized (pool) { if (pool.size() >= workers) { /* too many threads, exit this one */ activeThreads.remove(this); return; } else { pool.addElement(this); } } } log(this+" terminated"); } /** * Zero out the buffer from last time */ private void clearBuffer() { for (int i = 0; i < BUF_SIZE; i++) { buf[i] = 0; } } /** * Utility method to read a line of data from the input stream * @param is Inputstream to read * @return number of bytes read */ private int readOneLine(InputStream is) { int read = 0; clearBuffer(); try { log("Reading one line: started ="+readStarted+" avail="+is.available()); while ((!readStarted) || (is.available() > 0)) { int data = is.read(); // We shouldn't get EOF but we need tdo check if (data == -1) { log("EOF returned"); return -1; } buf[read] = (byte)data; System.out.print((char)data); readStarted = true; if (buf[read++]==(byte)'\n') { System.out.println(); return read; } } } catch (IOException e) { log("IOException from readOneLine"); e.printStackTrace(); } return read; } /** * Read a chunk of data * @param is Stream from which to read data * @param length Amount of data to read * @return number of bytes read */ private int readData(InputStream is, int length) { int read = 0; int count; // At the moment we're only expecting small data amounts byte[] buf = new byte[length]; try { while (is.available() > 0) { count = is.read(buf, read, length-read); read += count; } } catch (IOException e) { log("IOException from readData"); e.printStackTrace(); } return read; } /** * Read the status line from the input stream extracting method * information. * @param is Inputstream to read * @return number of bytes read */ private int parseStatusLine(InputStream is) { int index; int nread = 0; log("Parse status line"); // Check for status line first nread = readOneLine(is); // Bomb out if stream closes prematurely if (nread == -1) { requestMethod = UNKNOWN_METHOD; return -1; } if (buf[0] == (byte)'G' && buf[1] == (byte)'E' && buf[2] == (byte)'T' && buf[3] == (byte)' ') { requestMethod = GET_METHOD; log("GET request"); index = 4; } else if (buf[0] == (byte)'H' && buf[1] == (byte)'E' && buf[2] == (byte)'A' && buf[3] == (byte)'D' && buf[4] == (byte)' ') { requestMethod = HEAD_METHOD; log("HEAD request"); index = 5; } else if (buf[0] == (byte)'P' && buf[1] == (byte)'O' && buf[2] == (byte)'S' && buf[3] == (byte)'T' && buf[4] == (byte)' ') { requestMethod = POST_METHOD; log("POST request"); index = 5; } else { // Unhandled request requestMethod = UNKNOWN_METHOD; return -1; } // A valid method we understand if (requestMethod > UNKNOWN_METHOD) { // Read file name int i = index; while (buf[i] != (byte)' ') { // There should be HTTP/1.x at the end if ((buf[i] == (byte)'\n') || (buf[i] == (byte)'\r')) { requestMethod = UNKNOWN_METHOD; return -1; } i++; } testID = new String(buf, 0, index, i-index); if (testID.startsWith("/")) { testID = testID.substring(1); } return nread; } return -1; } /** * Read a header from the input stream * @param is Inputstream to read * @return number of bytes read */ private int parseHeader(InputStream is) { int index = 0; int nread = 0; log("Parse a header"); // Check for status line first nread = readOneLine(is); // Bomb out if stream closes prematurely if (nread == -1) { requestMethod = UNKNOWN_METHOD; return -1; } // Read header entry 'Header: data' int i = index; while (buf[i] != (byte)':') { // There should be an entry after the header if ((buf[i] == (byte)'\n') || (buf[i] == (byte)'\r')) { return UNKNOWN_METHOD; } i++; } String headerName = new String(buf, 0, i); i++; // Over ':' while (buf[i] == ' ') { i++; } String headerValue = new String(buf, i, nread-1); headers.put(headerName, headerValue); return nread; } /** * Read all headers from the input stream * @param is Inputstream to read * @return number of bytes read */ private int readHeaders(InputStream is) { int nread = 0; log("Read headers"); // Headers should be terminated by empty CRLF line while (true) { int headerLen = 0; headerLen = parseHeader(is); if (headerLen == -1) return -1; nread += headerLen; if (headerLen <= 2) { return nread; } } } /** * Read content data from the input stream * @param is Inputstream to read * @return number of bytes read */ private int readContent(InputStream is) { int nread = 0; log("Read content"); String lengthString = headers.get(requestHeaders[REQ_CONTENT_LENGTH]); int length = new Integer(lengthString).intValue(); // Read content length = readData(is, length); return length; } /** * The main loop, reading requests. */ void handleClient() throws IOException { InputStream is = new BufferedInputStream(s.getInputStream()); PrintStream ps = new PrintStream(s.getOutputStream()); int nread = 0; /* we will only block in read for this many milliseconds * before we fail with java.io.InterruptedIOException, * at which point we will abandon the connection. */ s.setSoTimeout(mTimeout); s.setTcpNoDelay(true); do { nread = parseStatusLine(is); if (requestMethod != UNKNOWN_METHOD) { // If status line found, read any headers nread = readHeaders(is); // Then read content (if any) // TODO handle chunked encoding from the client if (headers.get(requestHeaders[REQ_CONTENT_LENGTH]) != null) { nread = readContent(is); } } else { if (nread > 0) { /* we don't support this method */ ps.print(HTTP_VERSION_STRING + " " + HTTP_BAD_METHOD + " unsupported method type: "); ps.write(buf, 0, 5); ps.write(EOL); ps.flush(); } else { } if (!keepAlive || nread <= 0) { headers.clear(); readStarted = false; log("SOCKET CLOSED"); s.close(); return; } } // Reset test number prior to outputing data testNum = -1; // Write out the data printStatus(ps); printHeaders(ps); // Write line between headers and body psWriteEOL(ps); // Write the body if (redirectCode == -1) { switch (requestMethod) { case GET_METHOD: if ((testNum < 0) || (testNum > TestWebData.tests.length - 1)) { send404(ps); } else { sendFile(ps); } break; case HEAD_METHOD: // Nothing to do break; case POST_METHOD: // Post method write body data if ((testNum > 0) || (testNum < TestWebData.tests.length - 1)) { sendFile(ps); } break; default: break; } } else { // Redirecting switch (redirectCode) { case 301: // Seems 301 needs a body by neon (although spec // says SHOULD). psPrint(ps, TestWebData.testServerResponse[TestWebData.REDIRECT_301]); break; case 302: // psPrint(ps, TestWebData.testServerResponse[TestWebData.REDIRECT_302]); break; case 303: psPrint(ps, TestWebData.testServerResponse[TestWebData.REDIRECT_303]); break; case 307: psPrint(ps, TestWebData.testServerResponse[TestWebData.REDIRECT_307]); break; default: break; } } ps.flush(); // Reset for next request readStarted = false; headers.clear(); } while (keepAlive); log("SOCKET CLOSED"); s.close(); } // Print string to log and output stream void psPrint(PrintStream ps, String s) throws IOException { log(s); ps.print(s); } // Print bytes to log and output stream void psWrite(PrintStream ps, byte[] bytes, int len) throws IOException { log(new String(bytes)); ps.write(bytes, 0, len); } // Print CRLF to log and output stream void psWriteEOL(PrintStream ps) throws IOException { log("CRLF"); ps.write(EOL); } // Print status to log and output stream void printStatus(PrintStream ps) throws IOException { // Handle redirects first. if (redirectCode != -1) { log("REDIRECTING TO "+redirectHost+" status "+redirectCode); psPrint(ps, HTTP_VERSION_STRING + " " + redirectCode +" Moved permanently"); psWriteEOL(ps); psPrint(ps, "Location: " + redirectHost); psWriteEOL(ps); return; } if (testID.startsWith("test")) { testNum = Integer.valueOf(testID.substring(4))-1; } if ((testNum < 0) || (testNum > TestWebData.tests.length - 1)) { psPrint(ps, HTTP_VERSION_STRING + " " + HTTP_NOT_FOUND + " not found"); psWriteEOL(ps); } else { psPrint(ps, HTTP_VERSION_STRING + " " + HTTP_OK+" OK"); psWriteEOL(ps); } log("Status sent"); } /** * Create the server response and output to the stream * @param ps The PrintStream to output response headers and data to */ void printHeaders(PrintStream ps) throws IOException { psPrint(ps,"Server: TestWebServer"+mPort); psWriteEOL(ps); psPrint(ps, "Date: " + (new Date())); psWriteEOL(ps); psPrint(ps, "Connection: " + ((keepAlive) ? "Keep-Alive" : "Close")); psWriteEOL(ps); // Yuk, if we're not redirecting, we add the file details if (redirectCode == -1) { if (!TestWebData.testParams[testNum].testDir) { if (chunked) { psPrint(ps, "Transfer-Encoding: chunked"); } else { psPrint(ps, "Content-length: "+TestWebData.testParams[testNum].testLength); } psWriteEOL(ps); psPrint(ps,"Last Modified: " + (new Date(TestWebData.testParams[testNum].testLastModified))); psWriteEOL(ps); psPrint(ps, "Content-type: " + TestWebData.testParams[testNum].testType); psWriteEOL(ps); } else { psPrint(ps, "Content-type: text/html"); psWriteEOL(ps); } } else { // Content-length of 301, 302, 303, 307 are the same. psPrint(ps, "Content-length: "+(TestWebData.testServerResponse[TestWebData.REDIRECT_301]).length()); psWriteEOL(ps); psWriteEOL(ps); } log("Headers sent"); } /** * Sends the 404 not found message * @param ps The PrintStream to write to */ void send404(PrintStream ps) throws IOException { ps.println("Not Found\n\n"+ "The requested resource was not found.\n"); } /** * Sends the data associated with the headers * @param ps The PrintStream to write to */ void sendFile(PrintStream ps) throws IOException { // For now just make a chunk with the whole of the test data // It might be worth making this multiple chunks for large // test data to test multiple chunks. int dataSize = TestWebData.tests[testNum].length; if (chunked) { psPrint(ps, Integer.toHexString(dataSize)); psWriteEOL(ps); psWrite(ps, TestWebData.tests[testNum], dataSize); psWriteEOL(ps); psPrint(ps, "0"); psWriteEOL(ps); psWriteEOL(ps); } else { psWrite(ps, TestWebData.tests[testNum], dataSize); } } } }