diff options
Diffstat (limited to 'support/src')
3 files changed, 1296 insertions, 0 deletions
diff --git a/support/src/test/java/tests/support/Support_HttpConstants.java b/support/src/test/java/tests/support/Support_HttpConstants.java new file mode 100644 index 0000000..e99bdc1 --- /dev/null +++ b/support/src/test/java/tests/support/Support_HttpConstants.java @@ -0,0 +1,157 @@ +/* + * 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 tests.support; + +interface Support_HttpConstants { + /** 2XX: generally "OK" */ + public static final int HTTP_OK = 200; + public static final int HTTP_CREATED = 201; + public static final int HTTP_ACCEPTED = 202; + public static final int HTTP_NOT_AUTHORITATIVE = 203; + public static final int HTTP_NO_CONTENT = 204; + public static final int HTTP_RESET = 205; + public static final int HTTP_PARTIAL = 206; + + /** 3XX: relocation/redirect */ + public static final int HTTP_MULT_CHOICE = 300; + public static final int HTTP_MOVED_PERM = 301; + public static final int HTTP_MOVED_TEMP = 302; + public static final int HTTP_SEE_OTHER = 303; + public static final int HTTP_NOT_MODIFIED = 304; + public static final int HTTP_USE_PROXY = 305; + + /** 4XX: client error */ + public static final int HTTP_BAD_REQUEST = 400; + public static final int HTTP_UNAUTHORIZED = 401; + public static final int HTTP_PAYMENT_REQUIRED = 402; + public static final int HTTP_FORBIDDEN = 403; + public static final int HTTP_NOT_FOUND = 404; + public static final int HTTP_BAD_METHOD = 405; + public static final int HTTP_NOT_ACCEPTABLE = 406; + public static final int HTTP_PROXY_AUTH = 407; + public static final int HTTP_CLIENT_TIMEOUT = 408; + public static final int HTTP_CONFLICT = 409; + public static final int HTTP_GONE = 410; + public static final int HTTP_LENGTH_REQUIRED = 411; + public static final int HTTP_PRECON_FAILED = 412; + public static final int HTTP_ENTITY_TOO_LARGE = 413; + public static final int HTTP_REQ_TOO_LONG = 414; + public static final int HTTP_UNSUPPORTED_TYPE = 415; + + /** 5XX: server error */ + public static final int HTTP_SERVER_ERROR = 500; + public static final int HTTP_INTERNAL_ERROR = 501; + public static final int HTTP_BAD_GATEWAY = 502; + public static final int HTTP_UNAVAILABLE = 503; + public static final int HTTP_GATEWAY_TIMEOUT = 504; + public static final int HTTP_VERSION = 505; + + /** Method IDs */ + public static final int UNKNOWN_METHOD = 0; + public static final int GET_METHOD = 1; + public static final int HEAD_METHOD = 2; + public static final int POST_METHOD = 3; + + public static final String[] requestHeaders = { + "cache-control", + "connection", + "date", + "pragma", + "trailer", + "transfer-encoding", + "upgrade", + "via", + "warning", + "accept", + "accept-charset", + "accept-encoding", + "accept-language", + "authorization", + "expect", + "from", + "host", + "if-match", + "if-modified-since", + "if-none-match", + "if-range", + "if-unmodified-since", + "max-forwards", + "proxy-authentication", + "range", + "referer", + "te", + "user-agent", + "keep-alive", + "allow", + "content-encoding", + "content-language", + "content-length", + "content-location", + "content-md5", + "content-range", + "content-type", + "expires", + "last-modified", + "location", + "server" + + }; + + public static final int REQ_UNKNOWN = -1; + public static final int REQ_CACHE_CONTROL = 0; + public static final int REQ_CONNECTION = 1; + public static final int REQ_DATE = 2; + public static final int REQ_PRAGMA = 3; + public static final int REQ_TRAILER = 4; + public static final int REQ_TRANSFER_ENCODING = 5; + public static final int REQ_UPGRADE = 6; + public static final int REQ_VIA = 7; + public static final int REQ_WARNING = 8; + public static final int REQ_ACCEPT = 9; + public static final int REQ_ACCEPT_CHARSET = 10; + public static final int REQ_ACCEPT_ENCODING = 11; + public static final int REQ_ACCEPT_LANGUAGE = 12; + public static final int REQ_AUTHORIZATION = 13; + public static final int REQ_EXPECT = 14; + public static final int REQ_FROM = 15; + public static final int REQ_HOST = 16; + public static final int REQ_IF_MATCH = 17; + public static final int REQ_IF_MODIFIED_SINCE = 18; + public static final int REQ_IF_NONE_MATCH = 19; + public static final int REQ_IF_RANGE = 20; + public static final int REQ_IF_UNMODIFIED_SINCE = 21; + public static final int REQ_MAX_FORWARDS = 22; + public static final int REQ_PROXY_AUTHENTICATION = 23; + public static final int REQ_RANGE = 24; + public static final int REQ_REFERER = 25; + public static final int REQ_TE = 26; + public static final int REQ_USER_AGENT = 27; + public static final int REQ_KEEP_ALIVE = 28; + public static final int REQ_ALLOW = 29; + public static final int REQ_CONTENT_ENCODING = 30; + public static final int REQ_CONTENT_LANGUAGE = 31; + public static final int REQ_CONTENT_LENGTH = 32; + public static final int REQ_CONTENT_LOCATION = 33; + public static final int REQ_CONTENT_MD5 = 34; + public static final int REQ_CONTENT_RANGE = 35; + public static final int REQ_CONTENT_TYPE = 36; + public static final int REQ_EXPIRES = 37; + public static final int REQ_LAST_MODIFIED = 38; + public static final int REQ_LOCATION = 39; + public static final int REQ_SERVER = 40; + +} diff --git a/support/src/test/java/tests/support/Support_TestWebData.java b/support/src/test/java/tests/support/Support_TestWebData.java new file mode 100644 index 0000000..6f414ea --- /dev/null +++ b/support/src/test/java/tests/support/Support_TestWebData.java @@ -0,0 +1,197 @@ +/* + * 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 tests.support; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Date; + +/** + * Represents test data used by the Request API tests + */ +public class Support_TestWebData { + + /* + * Simple Html body + * <html> + * <body> + * <h1>Hello World!</h1> + * </body> + * </html> + */ + public final static byte[] test1 = { + (byte)0x3c, (byte)0x68, (byte)0x74, (byte)0x6d, + (byte)0x6c, (byte)0x3e, (byte)0x0a, (byte)0x3c, + (byte)0x62, (byte)0x6f, (byte)0x64, (byte)0x79, + (byte)0x3e, (byte)0x0a, (byte)0x3c, (byte)0x68, + (byte)0x31, (byte)0x3e, (byte)0x48, (byte)0x65, + (byte)0x6c, (byte)0x6c, (byte)0x6f, (byte)0x20, + (byte)0x57, (byte)0x6f, (byte)0x72, (byte)0x6c, + (byte)0x64, (byte)0x21, (byte)0x3c, (byte)0x2f, + (byte)0x68, (byte)0x31, (byte)0x3e, (byte)0x0a, + (byte)0x3c, (byte)0x2f, (byte)0x62, (byte)0x6f, + (byte)0x64, (byte)0x79, (byte)0x3e, (byte)0x0a, + (byte)0x3c, (byte)0x2f, (byte)0x68, (byte)0x74, + (byte)0x6d, (byte)0x6c, (byte)0x3e, (byte)0x0a + }; + + /* + * Simple Html body + * <html> + * <body> + * <h1>Hello World!</h1> + * </body> + * </html> + */ + public final static byte[] test2 = { + (byte)0x3c, (byte)0x68, (byte)0x74, (byte)0x6d, + (byte)0x6c, (byte)0x3e, (byte)0x0a, (byte)0x3c, + (byte)0x62, (byte)0x6f, (byte)0x64, (byte)0x79, + (byte)0x3e, (byte)0x0a, (byte)0x3c, (byte)0x68, + (byte)0x31, (byte)0x3e, (byte)0x48, (byte)0x65, + (byte)0x6c, (byte)0x6c, (byte)0x6f, (byte)0x20, + (byte)0x57, (byte)0x6f, (byte)0x72, (byte)0x6c, + (byte)0x64, (byte)0x21, (byte)0x3c, (byte)0x2f, + (byte)0x68, (byte)0x31, (byte)0x3e, (byte)0x0a, + (byte)0x3c, (byte)0x2f, (byte)0x62, (byte)0x6f, + (byte)0x64, (byte)0x79, (byte)0x3e, (byte)0x0a, + (byte)0x3c, (byte)0x2f, (byte)0x68, (byte)0x74, + (byte)0x6d, (byte)0x6c, (byte)0x3e, (byte)0x0a + }; + + // string for test request post body + public final static String postContent = "user=111"; + + // Array of all test data + public final static byte[][] tests = { + test1, + test2 + }; + + /** + * List of static test cases for use with test server + */ + public static Support_TestWebData[] testParams = { + new Support_TestWebData(52, 14000000, "test1", "text/html", false, 0), + new Support_TestWebData(52, 14000002, "test2", "unknown/unknown", false, + new Date().getTime() + 100000) + }; + + /** + * List of response strings for use by the test server + */ + public static String[] testServerResponse = { + "Redirecting 301", + "Redirecting 302", + "Redirecting 303", + "Redirecting 307" + }; + + // Redirection indices into testServerResponse + public final static int REDIRECT_301 = 0; + public final static int REDIRECT_302 = 1; + public final static int REDIRECT_303 = 2; + public final static int REDIRECT_307 = 3; + + /** + * Creates a data package with information used by the server when responding + * to requests + */ + Support_TestWebData(int length, int lastModified, String name, String type, boolean isDir, long expDate) { + testLength = length; + testLastModified = lastModified; + testName = name; + testType = type; + testDir = isDir; + testExp = expDate; + } + + /** + * Creates a data package with information used by the server when responding + * to requests + */ + private Support_TestWebData(String path, String type) { + File file = new File(path); + testLength = file.length(); + testLastModified = file.lastModified(); + testName = file.getName(); + testType = type; + testDir = file.isDirectory(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + FileInputStream in = null; + try { + in = new FileInputStream(file); + while (in.available() > 0) { + out.write(in.read()); + } + in.close(); + out.flush(); + test0Data = out.toByteArray(); + out.close(); + test0DataAvailable = true; + return; + } catch (Exception e) { + // ignore + e.printStackTrace(); + } finally { + try { + if (in != null) { + in.close(); + } + if (out != null) { + out.close(); + } + } catch (IOException e) { + // ignore + } + } + } + + public static void initDynamicTestWebData(String path, String type) { + test0Params = new Support_TestWebData(path, type); + } + + // Length of test entity body + public long testLength; + + // Last modified date value (milliseconds) + public long testLastModified; + + // Test identification name + public String testName; + + // The MIME type to assume for this test + public String testType; + + // The expiration date + public long testExp; + + // Indicates if this is a directory or not + public boolean testDir; + + // Indicate if test0 data has bin initialized + public static boolean test0DataAvailable = false; + + // test0 data + public static byte[] test0Data; + + // test0 parameters + public static Support_TestWebData test0Params; +} diff --git a/support/src/test/java/tests/support/Support_TestWebServer.java b/support/src/test/java/tests/support/Support_TestWebServer.java new file mode 100644 index 0000000..8ee7248 --- /dev/null +++ b/support/src/test/java/tests/support/Support_TestWebServer.java @@ -0,0 +1,942 @@ +/* + * 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 tests.support; + +import java.io.*; +import java.lang.Thread; +import java.net.*; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.logging.Logger; + +/** + * 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. + */ +public class Support_TestWebServer implements Support_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 Support_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 timeout and disabled log. + * @param port Sets the server to listen on this port + * @param servePath the path to the dynamic web test data + * @param contentType the type of the dynamic web test data + */ + public void initServer(int port, String servePath, String contentType) + throws Exception { + Support_TestWebData.initDynamicTestWebData(servePath, contentType); + initServer(port, DEFAULT_TIMEOUT, false); + } + + /** + * 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) { + Logger.global.fine(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()"); + // wait and retry + try { + Thread.sleep(1000); + } catch (InterruptedException e1) { + 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"); + 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 Support_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<String, String> headers = new Hashtable<String, String>(); + + /* 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) { + log("Exception during handleClient in the TestWebServer: " + + e.getMessage()); + } + /* 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()); + StringBuilder log = new StringBuilder(); + 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; + + log.append((char)data); + + readStarted = true; + if (buf[read++]==(byte)'\n') { + log(log.toString()); + return read; + } + } + } catch (IOException e) { + log("IOException from readOneLine"); + } + 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"); + } + 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 < -1) || (testNum > Support_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 < Support_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, Support_TestWebData.testServerResponse[Support_TestWebData.REDIRECT_301]); + break; + case 302: + // + psPrint(ps, Support_TestWebData.testServerResponse[Support_TestWebData.REDIRECT_302]); + break; + case 303: + psPrint(ps, Support_TestWebData.testServerResponse[Support_TestWebData.REDIRECT_303]); + break; + case 307: + psPrint(ps, Support_TestWebData.testServerResponse[Support_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 < -1) || (testNum > Support_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 { + if ((testNum < -1) || (testNum > Support_TestWebData.tests.length - 1)) { + // 404 status already sent + return; + } + SimpleDateFormat df = new SimpleDateFormat("EE, dd MMM yyyy HH:mm:ss"); + + psPrint(ps,"Server: TestWebServer"+mPort); + psWriteEOL(ps); + psPrint(ps, "Date: " + df.format(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 (testNum == -1) { + if (!Support_TestWebData.test0DataAvailable) { + log("testdata was not initilaized"); + return; + } + if (chunked) { + psPrint(ps, "Transfer-Encoding: chunked"); + } else { + psPrint(ps, "Content-length: " + + Support_TestWebData.test0Data.length); + } + psWriteEOL(ps); + + psPrint(ps, "Last Modified: " + (new Date( + Support_TestWebData.test0Params.testLastModified))); + psWriteEOL(ps); + + psPrint(ps, "Content-type: " + + Support_TestWebData.test0Params.testType); + psWriteEOL(ps); + + if (Support_TestWebData.testParams[testNum].testExp > 0) { + long exp; + exp = Support_TestWebData.testParams[testNum].testExp; + psPrint(ps, "expires: " + + df.format(exp) + " GMT"); + psWriteEOL(ps); + } + } else if (!Support_TestWebData.testParams[testNum].testDir) { + if (chunked) { + psPrint(ps, "Transfer-Encoding: chunked"); + } else { + psPrint(ps, "Content-length: "+Support_TestWebData.testParams[testNum].testLength); + } + psWriteEOL(ps); + + psPrint(ps,"Last Modified: " + (new + Date(Support_TestWebData.testParams[testNum].testLastModified))); + psWriteEOL(ps); + + psPrint(ps, "Content-type: " + Support_TestWebData.testParams[testNum].testType); + psWriteEOL(ps); + + if (Support_TestWebData.testParams[testNum].testExp > 0) { + long exp; + exp = Support_TestWebData.testParams[testNum].testExp; + psPrint(ps, "expires: " + + df.format(exp) + " GMT"); + 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: "+(Support_TestWebData.testServerResponse[Support_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. + if (testNum == -1) { + if (!Support_TestWebData.test0DataAvailable) { + log("testdata was not initilaized"); + return; + } + int dataSize = Support_TestWebData.test0Data.length; + if (chunked) { + psPrint(ps, Integer.toHexString(dataSize)); + psWriteEOL(ps); + psWrite(ps, Support_TestWebData.test0Data, dataSize); + psWriteEOL(ps); + psPrint(ps, "0"); + psWriteEOL(ps); + psWriteEOL(ps); + } else { + psWrite(ps, Support_TestWebData.test0Data, dataSize); + } + } else { + int dataSize = Support_TestWebData.tests[testNum].length; + if (chunked) { + psPrint(ps, Integer.toHexString(dataSize)); + psWriteEOL(ps); + psWrite(ps, Support_TestWebData.tests[testNum], dataSize); + psWriteEOL(ps); + psPrint(ps, "0"); + psWriteEOL(ps); + psWriteEOL(ps); + } else { + psWrite(ps, Support_TestWebData.tests[testNum], dataSize); + } + } + } + } +} |