blob: 4d6b0d14f98dc630294fe014755dcf5b9a4065ee [file] [log] [blame]
/*
* 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.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
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";
/** maps the recently requested URLs to the full request snapshot */
private final Map<String, Request> pathToRequest
= new ConcurrentHashMap<String, Request>();
/* timeout on client connections */
int timeout = 0;
/* 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;
int maxChunkSize = 1024;
/* 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 int initServer(boolean log) throws Exception {
return initServer(0, DEFAULT_TIMEOUT, log);
}
/**
* Initialize a new server with default timeout.
* @param port Sets the server to listen on this port, or 0 to let the OS choose.
* Hard-coding ports is evil, so always pass 0.
* @param log Set true if you want trace output
*/
public int initServer(int port, boolean log) throws Exception {
return 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, or 0 to let the OS choose.
* Hard-coding ports is evil, so always pass 0.
* @param servePath the path to the dynamic web test data
* @param contentType the type of the dynamic web test data
*/
public int initServer(int port, String servePath, String contentType)
throws Exception {
Support_TestWebData.initDynamicTestWebData(servePath, contentType);
return initServer(port, DEFAULT_TIMEOUT, false);
}
/**
* Initialize a new server with default port and timeout.
* @param port Sets the server to listen on this port, or 0 to let the OS choose.
* Hard-coding ports is evil, so always pass 0.
* @param timeout Indicates the period of time to wait until a socket is
* closed
* @param log Set true if you want trace output
*/
public int initServer(int port, int timeout, boolean log) throws Exception {
mTimeout = timeout;
mLog = log;
keepAlive = true;
if (acceptT == null) {
acceptT = new AcceptThread();
mPort = acceptT.init(port);
acceptT.start();
}
return mPort;
}
/**
* 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;
}
/**
* Sets the maximum byte count of any chunk if the server is using
* the "chunked" transfer encoding.
*/
public void setMaxChunkSize(int maxChunkSize) {
this.maxChunkSize = maxChunkSize;
}
/**
* 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 code 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);
}
/**
* Returns a map from recently-requested paths (like "/index.html") to a
* snapshot of the request data.
*/
public Map<String, Request> pathToRequest() {
return pathToRequest;
}
public int getNumAcceptedConnections() {
return acceptedConnections;
}
/**
* 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;
/**
* @param port the port to use, or 0 to let the OS choose.
* Hard-coding ports is evil, so always pass 0!
*/
public int init(int port) throws IOException {
ss = new ServerSocket(port);
ss.setSoTimeout(5000);
ss.setReuseAddress(true);
return ss.getLocalPort();
}
/**
* Main thread responding to new connections
*/
public synchronized void run() {
running = true;
while (running) {
try {
Socket s = ss.accept();
acceptedConnections++;
if (acceptedConnections >= acceptLimit) {
running = false;
}
new Thread(new Worker(s), "additional worker").start();
} catch (SocketException e) {
log(e.getMessage());
} catch (IOException e) {
log(e.getMessage());
}
}
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();
} 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' };
/**
* An immutable snapshot of an HTTP request.
*/
public static class Request {
private final String path;
private final Map<String, String> headers;
// TODO: include posted content?
public Request(String path, Map<String, String> headers) {
this.path = path;
this.headers = new LinkedHashMap<String, String>(headers);
}
public String getPath() {
return path;
}
public Map<String, String> getHeaders() {
return headers;
}
}
/**
* 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;
/* The requested path, such as "/test1" */
private String path;
/* 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;
/* Request headers are stored here */
private Map<String, String> headers = new LinkedHashMap<String, String>();
/* Create a new worker thread */
Worker(Socket s) {
this.buf = new byte[BUF_SIZE];
this.s = s;
}
public synchronized void run() {
try {
handleClient();
} catch (Exception e) {
log("Exception during handleClient in the TestWebServer: " + e.getMessage());
}
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++;
}
path = new String(buf, 0, index, i-index);
testID = path.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 - i - 2); // drop \r\n
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);
pathToRequest().put(path, new Request(path, headers));
// 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 offset, int count) throws IOException {
log(new String(bytes));
ps.write(bytes, offset, count);
}
// 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 {
if (testNum == -1) {
if (!Support_TestWebData.test0DataAvailable) {
log("test data was not initialized");
return;
}
sendFile(ps, Support_TestWebData.test0Data);
} else {
sendFile(ps, Support_TestWebData.tests[testNum]);
}
}
void sendFile(PrintStream ps, byte[] bytes) throws IOException {
if (chunked) {
int offset = 0;
while (offset < bytes.length) {
int chunkSize = Math.min(bytes.length - offset, maxChunkSize);
psPrint(ps, Integer.toHexString(chunkSize));
psWriteEOL(ps);
psWrite(ps, bytes, offset, chunkSize);
psWriteEOL(ps);
offset += chunkSize;
}
psPrint(ps, "0");
psWriteEOL(ps);
psWriteEOL(ps);
} else {
psWrite(ps, bytes, 0, bytes.length);
}
}
}
}