[Box Backup-commit] COMMIT r2428 - in box/trunk: lib/httpserver test/httpserver

boxbackup-dev@boxbackup.org boxbackup-dev@boxbackup.org
Sat, 3 Jan 2009 08:59:48 +0000 (GMT)


Author: chris
Date: 2009-01-03 08:59:47 +0000 (Sat, 03 Jan 2009)
New Revision: 2428

Modified:
   box/trunk/lib/httpserver/HTTPException.txt
   box/trunk/lib/httpserver/HTTPQueryDecoder.cpp
   box/trunk/lib/httpserver/HTTPRequest.cpp
   box/trunk/lib/httpserver/HTTPRequest.h
   box/trunk/lib/httpserver/HTTPResponse.cpp
   box/trunk/lib/httpserver/HTTPResponse.h
   box/trunk/test/httpserver/testhttpserver.cpp
Log:
Add ability to send an HTTPRequest to a socket and to parse an 
HTTPResponse from a socket, to create a simple HTTP client.


Modified: box/trunk/lib/httpserver/HTTPException.txt
===================================================================
--- box/trunk/lib/httpserver/HTTPException.txt	2009-01-03 08:59:08 UTC (rev 2427)
+++ box/trunk/lib/httpserver/HTTPException.txt	2009-01-03 08:59:47 UTC (rev 2428)
@@ -1,12 +1,15 @@
 EXCEPTION HTTP 10
 
 Internal							0
-RequestReadFailed					1
-RequestAlreadyBeenRead				2
+RequestReadFailed						1
+RequestAlreadyBeenRead						2
 BadRequest							3
-UnknownResponseCodeUsed				4
-NoContentTypeSet					5
-POSTContentTooLong					6
-CannotSetRedirectIfReponseHasData	7
-CannotSetNotFoundIfReponseHasData	8
-NotImplemented						9
+UnknownResponseCodeUsed						4
+NoContentTypeSet						5
+POSTContentTooLong						6
+CannotSetRedirectIfReponseHasData				7
+CannotSetNotFoundIfReponseHasData				8
+NotImplemented							9
+RequestNotInitialised						10
+BadResponse							11
+ResponseReadFailed						12

Modified: box/trunk/lib/httpserver/HTTPQueryDecoder.cpp
===================================================================
--- box/trunk/lib/httpserver/HTTPQueryDecoder.cpp	2009-01-03 08:59:08 UTC (rev 2427)
+++ box/trunk/lib/httpserver/HTTPQueryDecoder.cpp	2009-01-03 08:59:47 UTC (rev 2428)
@@ -19,9 +19,10 @@
 // --------------------------------------------------------------------------
 //
 // Function
-//		Name:    HTTPQueryDecoder::HTTPQueryDecoder(HTTPRequest::Query_t &)
-//		Purpose: Constructor. Pass in the query contents you want to decode
-//				 the query string into.
+//		Name:    HTTPQueryDecoder::HTTPQueryDecoder(
+//			 HTTPRequest::Query_t &)
+//		Purpose: Constructor. Pass in the query contents you want
+//			 to decode the query string into.
 //		Created: 26/3/04
 //
 // --------------------------------------------------------------------------

Modified: box/trunk/lib/httpserver/HTTPRequest.cpp
===================================================================
--- box/trunk/lib/httpserver/HTTPRequest.cpp	2009-01-03 08:59:08 UTC (rev 2427)
+++ box/trunk/lib/httpserver/HTTPRequest.cpp	2009-01-03 08:59:47 UTC (rev 2428)
@@ -52,6 +52,28 @@
 // --------------------------------------------------------------------------
 //
 // Function
+//		Name:    HTTPRequest::HTTPRequest(enum Method,
+//			 const std::string&)
+//		Purpose: Alternate constructor for hand-crafted requests
+//		Created: 03/01/09
+//
+// --------------------------------------------------------------------------
+HTTPRequest::HTTPRequest(enum Method method, const std::string& rURI)
+	: mMethod(method),
+	  mRequestURI(rURI),
+	  mHostPort(80),	// default if not specified
+	  mHTTPVersion(HTTPVersion_1_1),
+	  mContentLength(-1),
+	  mpCookies(0),
+	  mClientKeepAliveRequested(false)
+{
+}
+
+
+
+// --------------------------------------------------------------------------
+//
+// Function
 //		Name:    HTTPRequest::~HTTPRequest()
 //		Purpose: Destructor
 //		Created: 26/3/04
@@ -72,9 +94,10 @@
 //
 // Function
 //		Name:    HTTPRequest::Read(IOStreamGetLine &, int)
-//		Purpose: Read the request from an IOStreamGetLine (and attached stream)
-//				 Returns false if there was no valid request, probably due to 
-//				 a kept-alive connection closing.
+//		Purpose: Read the request from an IOStreamGetLine (and
+//			 attached stream).
+//			 Returns false if there was no valid request,
+//			 probably due to a kept-alive connection closing.
 //		Created: 26/3/04
 //
 // --------------------------------------------------------------------------
@@ -289,6 +312,89 @@
 // --------------------------------------------------------------------------
 //
 // Function
+//		Name:    HTTPRequest::Write(IOStream &, int)
+//		Purpose: Write the request to an IOStream using HTTP.
+//		Created: 03/01/09
+//
+// --------------------------------------------------------------------------
+bool HTTPRequest::Write(IOStream &rStream, int Timeout)
+{
+	switch (mMethod)
+	{
+	case Method_UNINITIALISED:
+		THROW_EXCEPTION(HTTPException, RequestNotInitialised); break;
+	case Method_UNKNOWN:
+		THROW_EXCEPTION(HTTPException, BadRequest); break;
+	case Method_GET:
+		rStream.Write("GET"); break;
+	case Method_HEAD:
+		rStream.Write("HEAD"); break;
+	case Method_POST:
+		rStream.Write("POST"); break;
+	}
+
+	rStream.Write(" ");
+	rStream.Write(mRequestURI.c_str());
+	rStream.Write(" ");
+
+	switch (mHTTPVersion)
+	{
+	case HTTPVersion_0_9: rStream.Write("HTTP/0.9"); break;
+	case HTTPVersion_1_0: rStream.Write("HTTP/1.0"); break;
+	case HTTPVersion_1_1: rStream.Write("HTTP/1.1"); break;
+	default:
+		THROW_EXCEPTION(HTTPException, NotImplemented);
+	}
+
+	rStream.Write("\n");
+	std::ostringstream oss;
+
+	if (mContentLength != -1)
+	{
+		oss << "Content-Length: " << mContentLength << "\n";
+	}
+
+	if (mContentType != "")
+	{
+		oss << "Content-Type: " << mContentType << "\n";
+	}
+
+	if (mHostName != "")
+	{
+		if (mHostPort != 80)
+		{
+			oss << "Host: " << mHostName << ":" << mHostPort <<
+				"\n";
+		}
+		else
+		{
+			oss << "Host: " << mHostName << "\n";
+		}
+	}
+
+	if (mpCookies)
+	{
+		THROW_EXCEPTION(HTTPException, NotImplemented);
+	}
+
+	if (mClientKeepAliveRequested)
+	{
+		oss << "Connection: keep-alive\n";
+	}
+	else
+	{
+		oss << "Connection: close\n";
+	}
+
+	rStream.Write(oss.str().c_str());
+	rStream.Write("\n");
+
+	return true;
+}
+
+// --------------------------------------------------------------------------
+//
+// Function
 //		Name:    HTTPRequest::ParseHeaders(IOStreamGetLine &, int)
 //		Purpose: Private. Parse the headers of the request
 //		Created: 26/3/04

Modified: box/trunk/lib/httpserver/HTTPRequest.h
===================================================================
--- box/trunk/lib/httpserver/HTTPRequest.h	2009-01-03 08:59:08 UTC (rev 2427)
+++ box/trunk/lib/httpserver/HTTPRequest.h	2009-01-03 08:59:47 UTC (rev 2428)
@@ -27,7 +27,17 @@
 class HTTPRequest
 {
 public:
+	enum Method
+	{
+		Method_UNINITIALISED = -1,
+		Method_UNKNOWN = 0,
+		Method_GET = 1,
+		Method_HEAD = 2,
+		Method_POST = 3
+	};
+	
 	HTTPRequest();
+	HTTPRequest(enum Method method, const std::string& rURI);
 	~HTTPRequest();
 private:
 	// no copying
@@ -40,15 +50,6 @@
 
 	enum
 	{
-		Method_UNINITIALISED = -1,
-		Method_UNKNOWN = 0,
-		Method_GET = 1,
-		Method_HEAD = 2,
-		Method_POST = 3
-	};
-	
-	enum
-	{
 		HTTPVersion__MajorMultiplier = 1000,
 		HTTPVersion_0_9 = 9,
 		HTTPVersion_1_0 = 1000,
@@ -56,6 +57,7 @@
 	};
 
 	bool Read(IOStreamGetLine &rGetLine, int Timeout);
+	bool Write(IOStream &rStream, int Timeout);
 
 	typedef std::map<std::string, std::string> CookieJar_t;
 	
@@ -67,7 +69,7 @@
 	//		Created: 26/3/04
 	//
 	// --------------------------------------------------------------------------
-	int GetMethod() const {return mMethod;}
+	enum Method GetMethod() const {return mMethod;}
 	const std::string &GetRequestURI() const {return mRequestURI;}
 	const std::string &GetHostName() const {return mHostName;}	// note: request does splitting of Host: header
 	const int GetHostPort() const {return mHostPort;}  // into host name and port number
@@ -91,13 +93,17 @@
 	//
 	// --------------------------------------------------------------------------
 	bool GetClientKeepAliveRequested() const {return mClientKeepAliveRequested;}
+	void SetClientKeepAliveRequested(bool keepAlive)
+	{
+		mClientKeepAliveRequested = keepAlive;
+	}
 
 private:
 	void ParseHeaders(IOStreamGetLine &rGetLine, int Timeout);
 	void ParseCookies(const std::string &rHeader, int DataStarts);
 
 private:
-	int mMethod;
+	enum Method mMethod;
 	std::string mRequestURI;
 	std::string mHostName;
 	int mHostPort;

Modified: box/trunk/lib/httpserver/HTTPResponse.cpp
===================================================================
--- box/trunk/lib/httpserver/HTTPResponse.cpp	2009-01-03 08:59:08 UTC (rev 2427)
+++ box/trunk/lib/httpserver/HTTPResponse.cpp	2009-01-03 08:59:47 UTC (rev 2428)
@@ -13,6 +13,7 @@
 #include <string.h>
 
 #include "HTTPResponse.h"
+#include "IOStreamGetLine.h"
 #include "autogen_HTTPException.h"
 
 #include "MemLeakFindOn.h"
@@ -32,7 +33,8 @@
 HTTPResponse::HTTPResponse()
 	: mResponseCode(HTTPResponse::Code_NoContent),
 	  mResponseIsDynamicContent(true),
-	  mKeepAlive(false)
+	  mKeepAlive(false),
+	  mContentLength(-1)
 {
 }
 
@@ -54,7 +56,8 @@
 //
 // Function
 //		Name:    HTTPResponse::ResponseCodeToString(int)
-//		Purpose: Return string equivalent of the response code, suitable for Status: headers
+//		Purpose: Return string equivalent of the response code,
+//			 suitable for Status: headers
 //		Created: 26/3/04
 //
 // --------------------------------------------------------------------------
@@ -114,8 +117,8 @@
 //
 // Function
 //		Name:    HTTPResponse::Send(IOStream &, bool)
-//		Purpose: Build the response, and send via the stream. Optionally omitting
-//				 the content.
+//		Purpose: Build the response, and send via the stream.
+//			 Optionally omitting the content.
 //		Created: 26/3/04
 //
 // --------------------------------------------------------------------------
@@ -139,10 +142,10 @@
 			header += len;
 		}
 		// Extra headers...
-		for(std::vector<std::string>::const_iterator i(mExtraHeaders.begin()); i != mExtraHeaders.end(); ++i)
+		for(std::vector<std::pair<std::string, std::string> >::const_iterator i(mExtraHeaders.begin()); i != mExtraHeaders.end(); ++i)
 		{
 			header += "\r\n";
-			header += *i;
+			header += i->first + ": " + i->second;
 		}
 		// NOTE: a line ending must be included here in all cases
 		// Control whether the response is cached
@@ -181,17 +184,215 @@
 // --------------------------------------------------------------------------
 //
 // Function
+//		Name:    HTTPResponse::ParseHeaders(IOStreamGetLine &, int)
+//		Purpose: Private. Parse the headers of the response
+//		Created: 26/3/04
+//
+// --------------------------------------------------------------------------
+void HTTPResponse::ParseHeaders(IOStreamGetLine &rGetLine, int Timeout)
+{
+	std::string header;
+	bool haveHeader = false;
+	while(true)
+	{
+		if(rGetLine.IsEOF())
+		{
+			// Header terminates unexpectedly
+			THROW_EXCEPTION(HTTPException, BadRequest)		
+		}
+
+		std::string currentLine;	
+		if(!rGetLine.GetLine(currentLine, false /* no preprocess */, Timeout))
+		{
+			// Timeout
+			THROW_EXCEPTION(HTTPException, RequestReadFailed)
+		}
+		
+		// Is this a continuation of the previous line?
+		bool processHeader = haveHeader;
+		if(!currentLine.empty() && (currentLine[0] == ' ' || currentLine[0] == '\t'))
+		{
+			// A continuation, don't process anything yet
+			processHeader = false;
+		}
+		//TRACE3("%d:%d:%s\n", processHeader, haveHeader, currentLine.c_str());
+		
+		// Parse the header -- this will actually process the header
+		// from the previous run around the loop.
+		if(processHeader)
+		{
+			// Find where the : is in the line
+			const char *h = header.c_str();
+			int p = 0;
+			while(h[p] != '\0' && h[p] != ':')
+			{
+				++p;
+			}
+			// Skip white space
+			int dataStart = p + 1;
+			while(h[dataStart] == ' ' || h[dataStart] == '\t')
+			{
+				++dataStart;
+			}
+		
+			if(p == sizeof("Content-Length")-1
+				&& ::strncasecmp(h, "Content-Length", sizeof("Content-Length")-1) == 0)
+			{
+				// Decode number
+				long len = ::strtol(h + dataStart, NULL, 10);	// returns zero in error case, this is OK
+				if(len < 0) len = 0;
+				// Store
+				mContentLength = len;
+			}
+			else if(p == sizeof("Content-Type")-1
+				&& ::strncasecmp(h, "Content-Type", sizeof("Content-Type")-1) == 0)
+			{
+				// Store rest of string as content type
+				mContentType = h + dataStart;
+			}
+			else if(p == sizeof("Cookie")-1
+				&& ::strncasecmp(h, "Cookie", sizeof("Cookie")-1) == 0)
+			{
+				THROW_EXCEPTION(HTTPException, NotImplemented);
+				/*
+				// Parse cookies
+				ParseCookies(header, dataStart);
+				*/
+			}
+			else if(p == sizeof("Connection")-1
+				&& ::strncasecmp(h, "Connection", sizeof("Connection")-1) == 0)
+			{
+				// Connection header, what is required?
+				const char *v = h + dataStart;
+				if(::strcasecmp(v, "close") == 0)
+				{
+					mKeepAlive = false;
+				}
+				else if(::strcasecmp(v, "keep-alive") == 0)
+				{
+					mKeepAlive = true;
+				}
+				// else don't understand, just assume default for protocol version
+			}
+			else
+			{
+				std::string headerName = header.substr(0, p);
+				AddHeader(headerName, h + dataStart);
+			}
+			
+			// Unset have header flag, as it's now been processed
+			haveHeader = false;
+		}
+
+		// Store the chunk of header the for next time round
+		if(haveHeader)
+		{
+			header += currentLine;
+		}
+		else
+		{
+			header = currentLine;
+			haveHeader = true;
+		}
+
+		// End of headers?
+		if(currentLine.empty())
+		{
+			// All done!
+			break;
+		}		
+	}
+}
+
+void HTTPResponse::Receive(IOStream& rStream, int Timeout)
+{
+	IOStreamGetLine rGetLine(rStream);
+
+	if(rGetLine.IsEOF())
+	{
+		// Connection terminated unexpectedly
+		THROW_EXCEPTION(HTTPException, BadResponse)		
+	}
+
+	std::string statusLine;	
+	if(!rGetLine.GetLine(statusLine, false /* no preprocess */, Timeout))
+	{
+		// Timeout
+		THROW_EXCEPTION(HTTPException, ResponseReadFailed)
+	}
+
+	if (statusLine.substr(0, 7) != "HTTP/1." ||
+		statusLine[8] != ' ')
+	{
+		// Status line terminated unexpectedly
+		BOX_ERROR("Bad response status line: " << statusLine);
+		THROW_EXCEPTION(HTTPException, BadResponse)		
+	}
+
+	if (statusLine[5] == '1' && statusLine[7] == '1')
+	{
+		// HTTP/1.1 default is to keep alive
+		mKeepAlive = true;
+	}
+		
+	// Decode the status code
+	long status = ::strtol(statusLine.substr(9, 3).c_str(), NULL, 10);
+	// returns zero in error case, this is OK
+	if(status < 0) status = 0;
+	// Store
+	mResponseCode = status;
+
+	ParseHeaders(rGetLine, Timeout);
+
+	// push back whatever bytes we have left
+	// rGetLine.DetachFile();
+	if (mContentLength > 0)
+	{
+		if (mContentLength < rGetLine.GetSizeOfBufferedData())
+		{
+			// very small response, not good!
+			THROW_EXCEPTION(HTTPException, NotImplemented);
+		}
+
+		Write(rGetLine.GetBufferedData(),
+			rGetLine.GetSizeOfBufferedData());
+	}
+
+	while (mContentLength != 0) // could be -1 as well
+	{
+		char buffer[4096];
+		int readSize = sizeof(buffer);
+		if (mContentLength > 0 && mContentLength < readSize)
+		{
+			readSize = mContentLength;
+		}
+		readSize = rStream.Read(buffer, readSize, Timeout);
+		if (readSize == 0)
+		{
+			break;
+		}
+		mContentLength -= readSize;
+		Write(buffer, readSize);
+	}
+
+	SetForReading();
+}
+
+// --------------------------------------------------------------------------
+//
+// Function
 //		Name:    HTTPResponse::AddHeader(const char *)
 //		Purpose: Add header, given entire line
 //		Created: 26/3/04
 //
 // --------------------------------------------------------------------------
+/*
 void HTTPResponse::AddHeader(const char *EntireHeaderLine)
 {
 	mExtraHeaders.push_back(std::string(EntireHeaderLine));
 }
+*/
 
-
 // --------------------------------------------------------------------------
 //
 // Function
@@ -200,12 +401,13 @@
 //		Created: 26/3/04
 //
 // --------------------------------------------------------------------------
+/*
 void HTTPResponse::AddHeader(const std::string &rEntireHeaderLine)
 {
 	mExtraHeaders.push_back(rEntireHeaderLine);
 }
+*/
 
-
 // --------------------------------------------------------------------------
 //
 // Function
@@ -214,12 +416,9 @@
 //		Created: 26/3/04
 //
 // --------------------------------------------------------------------------
-void HTTPResponse::AddHeader(const char *Header, const char *Value)
+void HTTPResponse::AddHeader(const char *pHeader, const char *pValue)
 {
-	std::string h(Header);
-	h += ": ";
-	h += Value;
-	mExtraHeaders.push_back(h);
+	mExtraHeaders.push_back(Header(pHeader, pValue));
 }
 
 
@@ -231,12 +430,9 @@
 //		Created: 26/3/04
 //
 // --------------------------------------------------------------------------
-void HTTPResponse::AddHeader(const char *Header, const std::string &rValue)
+void HTTPResponse::AddHeader(const char *pHeader, const std::string &rValue)
 {
-	std::string h(Header);
-	h += ": ";
-	h += rValue;
-	mExtraHeaders.push_back(h);
+	mExtraHeaders.push_back(Header(pHeader, rValue));
 }
 
 
@@ -250,7 +446,7 @@
 // --------------------------------------------------------------------------
 void HTTPResponse::AddHeader(const std::string &rHeader, const std::string &rValue)
 {
-	mExtraHeaders.push_back(rHeader + ": " + rValue);
+	mExtraHeaders.push_back(Header(rHeader, rValue));
 }
 
 
@@ -279,14 +475,14 @@
 	h += Path;
 	h += "\"";
 */
-	std::string h("Set-Cookie: ");
+	std::string h;
 	h += Name;
 	h += "=";
 	h += Value;
 	h += "; Version=1; Path=";
 	h += Path;
 
-	mExtraHeaders.push_back(h);
+	mExtraHeaders.push_back(Header("Set-Cookie", h));
 }
 
 
@@ -312,10 +508,10 @@
 	mResponseCode = Code_Found;
 
 	// Set location to redirect to
-	std::string header("Location: ");
+	std::string header;
 	if(IsLocalURI) header += msDefaultURIPrefix;
 	header += RedirectTo;
-	mExtraHeaders.push_back(header);
+	mExtraHeaders.push_back(Header("Location", header));
 	
 	// Set up some default content
 	mContentType = "text/html";

Modified: box/trunk/lib/httpserver/HTTPResponse.h
===================================================================
--- box/trunk/lib/httpserver/HTTPResponse.h	2009-01-03 08:59:08 UTC (rev 2427)
+++ box/trunk/lib/httpserver/HTTPResponse.h	2009-01-03 08:59:47 UTC (rev 2428)
@@ -15,6 +15,8 @@
 
 #include "CollectInBufferStream.h"
 
+class IOStreamGetLine;
+
 // --------------------------------------------------------------------------
 //
 // Class
@@ -35,15 +37,18 @@
 public:
 
 	void SetResponseCode(int Code);
+	int GetResponseCode() { return mResponseCode; }
 	void SetContentType(const char *ContentType);
+	const std::string& GetContentType() { return mContentType; }
 
 	void SetAsRedirect(const char *RedirectTo, bool IsLocalURI = true);
 	void SetAsNotFound(const char *URI);
 
 	void Send(IOStream &rStream, bool OmitContent = false);
+	void Receive(IOStream& rStream, int Timeout = IOStream::TimeOutInfinite);
 
-	void AddHeader(const char *EntireHeaderLine);
-	void AddHeader(const std::string &rEntireHeaderLine);
+	// void AddHeader(const char *EntireHeaderLine);
+	// void AddHeader(const std::string &rEntireHeaderLine);
 	void AddHeader(const char *Header, const char *Value);
 	void AddHeader(const char *Header, const std::string &rValue);
 	void AddHeader(const std::string &rHeader, const std::string &rValue);
@@ -106,9 +111,13 @@
 	bool mResponseIsDynamicContent;
 	bool mKeepAlive;
 	std::string mContentType;
-	std::vector<std::string> mExtraHeaders;
+	typedef std::pair<std::string, std::string> Header;
+	std::vector<Header> mExtraHeaders;
+	int mContentLength; // only used when reading response from stream
 	
 	static std::string msDefaultURIPrefix;
+
+	void ParseHeaders(IOStreamGetLine &rGetLine, int Timeout);
 };
 
 #endif // HTTPRESPONSE__H

Modified: box/trunk/test/httpserver/testhttpserver.cpp
===================================================================
--- box/trunk/test/httpserver/testhttpserver.cpp	2009-01-03 08:59:08 UTC (rev 2427)
+++ box/trunk/test/httpserver/testhttpserver.cpp	2009-01-03 08:59:47 UTC (rev 2428)
@@ -16,6 +16,7 @@
 #include "HTTPServer.h"
 #include "HTTPRequest.h"
 #include "HTTPResponse.h"
+#include "IOStreamGetLine.h"
 #include "ServerControl.h"
 
 #include "MemLeakFindOn.h"
@@ -71,6 +72,7 @@
 		case HTTPRequest::Method_GET: m = "GET "; break;
 		case HTTPRequest::Method_HEAD: m = "HEAD"; break;
 		case HTTPRequest::Method_POST: m = "POST"; break;
+		default: m = "UNKNOWN";
 		}
 		rResponse.Write(m, 4);
 	}
@@ -125,16 +127,93 @@
 	// Start the server
 	int pid = LaunchServer("./test server testfiles/httpserver.conf", "testfiles/httpserver.pid");
 	TEST_THAT(pid != -1 && pid != 0);
-	if(pid > 0)
+	if(pid <= 0)
 	{
-		// Run the request script
-		TEST_THAT(::system("perl testfiles/testrequests.pl") == 0);
-	
-		// Kill it
-		TEST_THAT(KillServer(pid));
-		TestRemoteProcessMemLeaks("generic-httpserver.memleaks");
+		return 0;
 	}
 
+	// Run the request script
+	TEST_THAT(::system("perl testfiles/testrequests.pl") == 0);
+
+	signal(SIGPIPE, SIG_IGN);
+
+	SocketStream sock;
+	sock.Open(Socket::TypeINET, "localhost", 1080);
+
+	for (int i = 0; i < 4; i++)
+	{
+		HTTPRequest request(HTTPRequest::Method_GET,
+			"/test-one/34/341s/234?p1=vOne&p2=vTwo");
+
+		if (i >= 2)
+		{
+			// first set of passes has keepalive off by default,
+			// so when i == 1 the socket has already been closed
+			// by the server, and we'll get -EPIPE when we try
+			// to send the request.
+			request.SetClientKeepAliveRequested(true);
+		}
+
+		if (i == 1)
+		{
+			TEST_CHECK_THROWS(request.Write(sock,
+				IOStream::TimeOutInfinite),
+				ConnectionException, SocketWriteError);
+			sock.Close();
+			sock.Open(Socket::TypeINET, "localhost", 1080);
+			continue;
+		}
+		else
+		{
+			request.Write(sock, IOStream::TimeOutInfinite);
+		}
+
+		HTTPResponse response;
+		response.Receive(sock);
+		
+		TEST_THAT(response.GetResponseCode() == HTTPResponse::Code_OK);
+		TEST_THAT(response.GetContentType() == "text/html");
+
+		IOStreamGetLine getline(response);
+		std::string line;
+
+		TEST_THAT(getline.GetLine(line));
+		TEST_EQUAL("<html>", line);
+		TEST_THAT(getline.GetLine(line));
+		TEST_EQUAL("<head><title>TEST SERVER RESPONSE</title></head>",
+			line);
+		TEST_THAT(getline.GetLine(line));
+		TEST_EQUAL("<body><h1>Test response</h1>", line);
+		TEST_THAT(getline.GetLine(line));
+		TEST_EQUAL("<p><b>URI:</b> /test-one/34/341s/234</p>", line);
+		TEST_THAT(getline.GetLine(line));
+		TEST_EQUAL("<p><b>Query string:</b> p1=vOne&p2=vTwo</p>", line);
+		TEST_THAT(getline.GetLine(line));
+		TEST_EQUAL("<p><b>Method:</b> GET </p>", line);
+		TEST_THAT(getline.GetLine(line));
+		TEST_EQUAL("<p><b>Decoded query:</b><br>", line);
+		TEST_THAT(getline.GetLine(line));
+		TEST_EQUAL("PARAM:p1=vOne<br>", line);
+		TEST_THAT(getline.GetLine(line));
+		TEST_EQUAL("PARAM:p2=vTwo<br></p>", line);
+		TEST_THAT(getline.GetLine(line));
+		TEST_EQUAL("<p><b>Content type:</b> </p>", line);
+		TEST_THAT(getline.GetLine(line));
+		TEST_EQUAL("<p><b>Content length:</b> -1</p>", line);
+		TEST_THAT(getline.GetLine(line));
+		TEST_EQUAL("<p><b>Cookies:</b><br>", line);
+		TEST_THAT(getline.GetLine(line));
+		TEST_EQUAL("</p>", line);
+		TEST_THAT(getline.GetLine(line));
+		TEST_EQUAL("</body>", line);
+		TEST_THAT(getline.GetLine(line));
+		TEST_EQUAL("</html>", line);
+	}
+	
+	// Kill it
+	TEST_THAT(KillServer(pid));
+	TestRemoteProcessMemLeaks("generic-httpserver.memleaks");
+
 	return 0;
 }