From 546c0d068934e8fc5ba53f32bdf61b3a6841c65b Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Sun, 13 Nov 2016 20:50:45 +0000 Subject: [PATCH] Refactor common HTTP header handling Move the common code out of HTTPRequest and HTTPResponse into the new HTTPHeaders class, a container for headers with some sensible HTTP-specific behaviour. --- lib/httpserver/HTTPException.txt | 2 + lib/httpserver/HTTPHeaders.cpp | 311 +++++++++++++++++++++ lib/httpserver/HTTPHeaders.h | 109 ++++++++ lib/httpserver/HTTPQueryDecoder.cpp | 2 +- lib/httpserver/HTTPQueryDecoder.h | 5 +- lib/httpserver/HTTPRequest.cpp | 413 +++++++++++++--------------- lib/httpserver/HTTPRequest.h | 148 +++++----- lib/httpserver/HTTPResponse.cpp | 344 +++++------------------ lib/httpserver/HTTPResponse.h | 118 ++++---- lib/httpserver/S3Client.cpp | 2 +- lib/httpserver/S3Simulator.cpp | 8 +- test/httpserver/testhttpserver.cpp | 348 ++++++++--------------- 12 files changed, 923 insertions(+), 887 deletions(-) create mode 100644 lib/httpserver/HTTPHeaders.cpp create mode 100644 lib/httpserver/HTTPHeaders.h diff --git a/lib/httpserver/HTTPException.txt b/lib/httpserver/HTTPException.txt index c9b3f940b..ba169f215 100644 --- a/lib/httpserver/HTTPException.txt +++ b/lib/httpserver/HTTPException.txt @@ -15,3 +15,5 @@ BadResponse 11 ResponseReadFailed 12 NoStreamConfigured 13 RequestFailedUnexpectedly 14 The request was expected to succeed, but it failed. +ContentLengthAlreadySet 15 Tried to send a request without content, but the Content-Length header is already set. +WrongContentLength 16 There was too much or not enough data in the request content stream. diff --git a/lib/httpserver/HTTPHeaders.cpp b/lib/httpserver/HTTPHeaders.cpp new file mode 100644 index 000000000..fe399e0f1 --- /dev/null +++ b/lib/httpserver/HTTPHeaders.cpp @@ -0,0 +1,311 @@ +// -------------------------------------------------------------------------- +// +// File +// Name: HTTPHeaders +// Purpose: Utility class to decode HTTP headers +// Created: 16/8/2015 +// +// -------------------------------------------------------------------------- + +#include "Box.h" + +#include + +#include "HTTPHeaders.h" +#include "IOStreamGetLine.h" + +#include "MemLeakFindOn.h" + + +// -------------------------------------------------------------------------- +// +// Function +// Name: HTTPHeaders::ReadFromStream(IOStreamGetLine &rGetLine, +// int Timeout); +// Purpose: Read headers from a stream into internal storage. +// Created: 2015-08-22 +// +// -------------------------------------------------------------------------- +void HTTPHeaders::ReadFromStream(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) + { + ParseHeaderLine(header); + + // 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; + } + } +} + +// -------------------------------------------------------------------------- +// +// Function +// Name: HTTPHeaders::ParseHeaderLine +// Purpose: Splits a line into name and value, and adds it to +// this header set. +// Created: 2015-08-22 +// +// -------------------------------------------------------------------------- +void HTTPHeaders::ParseHeaderLine(const std::string& rLine) +{ + // Find where the : is in the line + std::string::size_type colon = rLine.find(':'); + if(colon == std::string::npos || colon == 0 || + colon > rLine.size() - 2) + { + THROW_EXCEPTION_MESSAGE(HTTPException, BadResponse, + "Invalid header line: " << rLine); + } + + std::string name = rLine.substr(0, colon); + std::string value = rLine.substr(colon + 2); + AddHeader(name, value); +} + +// -------------------------------------------------------------------------- +// +// Function +// Name: HTTPHeaders::GetHeader(const std::string& name, +// const std::string* pValueOut) +// Purpose: Get the value of a single header, with the specified +// name, into the std::string pointed to by pValueOut. +// Returns true if the header exists, and false if it +// does not (in which case *pValueOut is not modified). +// Certain headers are stored in specific fields, e.g. +// content-length and host, but this should be done +// transparently to callers of AddHeader and GetHeader. +// Created: 2016-03-12 +// +// -------------------------------------------------------------------------- +bool HTTPHeaders::GetHeader(const std::string& rName, std::string* pValueOut) const +{ + const std::string name = ToLowerCase(rName); + + // Remember to change AddHeader() and GetHeader() together for each + // item in this list! + if(name == "content-length") + { + // Convert number to string. + std::ostringstream out; + out << mContentLength; + *pValueOut = out.str(); + } + else if(name == "content-type") + { + *pValueOut = mContentType; + } + else if(name == "connection") + { + // TODO FIXME: not all values of the Connection header can be + // stored and retrieved at the moment. + *pValueOut = mKeepAlive ? "keep-alive" : "close"; + } + else if (name == "host") + { + std::ostringstream out; + out << mHostName; + if(mHostPort != DEFAULT_PORT) + { + out << ":" << mHostPort; + } + *pValueOut = out.str(); + } + else + { + // All other headers are stored in mExtraHeaders. + + for (std::vector
::const_iterator + i = mExtraHeaders.begin(); + i != mExtraHeaders.end(); i++) + { + if (i->first == name) + { + *pValueOut = i->second; + return true; + } + } + + // Not found in mExtraHeaders. + return false; + } + + // For all except the else case above (searching mExtraHeaders), we must have + // found a value, as there will always be one. + return true; +} + +// -------------------------------------------------------------------------- +// +// Function +// Name: HTTPHeaders::AddHeader(const std::string& name, +// const std::string& value) +// Purpose: Add a single header, with the specified name and +// value, to the internal list of headers. Certain +// headers are stored in specific fields, e.g. +// content-length and host, but this should be done +// transparently to callers of AddHeader and GetHeader. +// Created: 2015-08-22 +// +// -------------------------------------------------------------------------- +void HTTPHeaders::AddHeader(const std::string& rName, const std::string& value) +{ + std::string name = ToLowerCase(rName); + + // Remember to change AddHeader() and GetHeader() together for each + // item in this list! + if(name == "content-length") + { + // Decode number + long len = ::strtol(value.c_str(), NULL, 10); + // returns zero in error case, this is OK + if(len < 0) len = 0; + // Store + mContentLength = len; + } + else if(name == "content-type") + { + // Store rest of string as content type + mContentType = value; + } + else if(name == "connection") + { + // Connection header, what is required? + if(::strcasecmp(value.c_str(), "close") == 0) + { + mKeepAlive = false; + } + else if(::strcasecmp(value.c_str(), "keep-alive") == 0) + { + mKeepAlive = true; + } + // else don't understand, just assume default for protocol version + } + else if (name == "host") + { + // Store host header + mHostName = value; + + // Is there a port number to split off? + std::string::size_type colon = mHostName.find_first_of(':'); + if(colon != std::string::npos) + { + // There's a port in the string... attempt to turn it into an int + mHostPort = ::strtol(mHostName.c_str() + colon + 1, 0, 10); + + // Truncate the string to just the hostname + mHostName = mHostName.substr(0, colon); + } + } + else + { + mExtraHeaders.push_back(Header(name, value)); + } +} + +// -------------------------------------------------------------------------- +// +// Function +// Name: HTTPHeaders::WriteTo(IOStream& rOutput, int Timeout) +// Purpose: Write all headers to the supplied stream. +// Created: 2015-08-22 +// +// -------------------------------------------------------------------------- +void HTTPHeaders::WriteTo(IOStream& rOutput, int Timeout) const +{ + std::ostringstream oss; + + if (mContentLength != -1) + { + oss << "Content-Length: " << mContentLength << "\r\n"; + } + + if (mContentType != "") + { + oss << "Content-Type: " << mContentType << "\r\n"; + } + + if (mHostName != "") + { + oss << "Host: " << GetHostNameWithPort() << "\r\n"; + } + + if (mKeepAlive) + { + oss << "Connection: keep-alive\r\n"; + } + else + { + oss << "Connection: close\r\n"; + } + + for (std::vector
::const_iterator i = mExtraHeaders.begin(); + i != mExtraHeaders.end(); i++) + { + oss << i->first << ": " << i->second << "\r\n"; + } + + rOutput.Write(oss.str(), Timeout); +} + +std::string HTTPHeaders::GetHostNameWithPort() const +{ + + if (mHostPort != 80) + { + std::ostringstream oss; + oss << mHostName << ":" << mHostPort; + return oss.str(); + } + else + { + return mHostName; + } +} + diff --git a/lib/httpserver/HTTPHeaders.h b/lib/httpserver/HTTPHeaders.h new file mode 100644 index 000000000..b965ac465 --- /dev/null +++ b/lib/httpserver/HTTPHeaders.h @@ -0,0 +1,109 @@ +// -------------------------------------------------------------------------- +// +// File +// Name: HTTPHeaders.h +// Purpose: Utility class to decode HTTP headers +// Created: 16/8/2015 +// +// -------------------------------------------------------------------------- + +#ifndef HTTPHEADERS__H +#define HTTPHEADERS__H + +#include +#include + +#include "autogen_HTTPException.h" +#include "IOStream.h" + +class IOStreamGetLine; + +// -------------------------------------------------------------------------- +// +// Class +// Name: HTTPHeaders +// Purpose: Utility class to decode HTTP headers +// Created: 16/8/2015 +// +// -------------------------------------------------------------------------- +class HTTPHeaders +{ +public: + enum { + DEFAULT_PORT = 80, + UNKNOWN_CONTENT_LENGTH = -1, + }; + + HTTPHeaders() + : mKeepAlive(false), + mHostPort(DEFAULT_PORT), // default if not specified: if you change this, + // remember to change GetHeader("host") as well. + mContentLength(UNKNOWN_CONTENT_LENGTH) + { } + virtual ~HTTPHeaders() { } + // copying is fine + + void ReadFromStream(IOStreamGetLine &rGetLine, int Timeout); + void ParseHeaderLine(const std::string& line); + void AddHeader(const std::string& name, const std::string& value); + void WriteTo(IOStream& rOutput, int Timeout) const; + typedef std::pair Header; + bool GetHeader(const std::string& name, std::string* pValueOut) const; + std::string GetHeaderValue(const std::string& name, bool required = true) const + { + std::string value; + if (GetHeader(name, &value)) + { + return value; + } + + if(required) + { + THROW_EXCEPTION_MESSAGE(CommonException, ConfigNoKey, + "Expected header was not present: " << name); + } + else + { + return ""; + } + } + const std::vector
GetExtraHeaders() const { return mExtraHeaders; } + void SetKeepAlive(bool KeepAlive) {mKeepAlive = KeepAlive;} + bool IsKeepAlive() const {return mKeepAlive;} + void SetContentType(const std::string& rContentType) + { + mContentType = rContentType; + } + const std::string& GetContentType() const { return mContentType; } + void SetContentLength(int64_t ContentLength) { mContentLength = ContentLength; } + int64_t GetContentLength() const { return mContentLength; } + const std::string &GetHostName() const {return mHostName;} + const int GetHostPort() const {return mHostPort;} + std::string GetHostNameWithPort() const; + void SetHostName(const std::string& rHostName) + { + AddHeader("host", rHostName); + } + +private: + bool mKeepAlive; + std::string mContentType; + std::string mHostName; + int mHostPort; + int64_t mContentLength; // only used when reading response from stream + std::vector
mExtraHeaders; + + std::string ToLowerCase(const std::string& input) const + { + std::string output = input; + for (std::string::iterator c = output.begin(); + c != output.end(); c++) + { + *c = tolower(*c); + } + return output; + } +}; + +#endif // HTTPHEADERS__H + diff --git a/lib/httpserver/HTTPQueryDecoder.cpp b/lib/httpserver/HTTPQueryDecoder.cpp index c49ac2cef..55da7cda8 100644 --- a/lib/httpserver/HTTPQueryDecoder.cpp +++ b/lib/httpserver/HTTPQueryDecoder.cpp @@ -9,7 +9,7 @@ #include "Box.h" -#include +#include #include "HTTPQueryDecoder.h" diff --git a/lib/httpserver/HTTPQueryDecoder.h b/lib/httpserver/HTTPQueryDecoder.h index ca5afe7e6..d55a70b49 100644 --- a/lib/httpserver/HTTPQueryDecoder.h +++ b/lib/httpserver/HTTPQueryDecoder.h @@ -25,12 +25,13 @@ class HTTPQueryDecoder public: HTTPQueryDecoder(HTTPRequest::Query_t &rDecodeInto); ~HTTPQueryDecoder(); + private: // no copying HTTPQueryDecoder(const HTTPQueryDecoder &); - HTTPQueryDecoder &operator=(const HTTPQueryDecoder &); -public: + HTTPQueryDecoder& operator=(const HTTPQueryDecoder &); +public: void DecodeChunk(const char *pQueryString, int QueryStringSize); void Finish(); diff --git a/lib/httpserver/HTTPRequest.cpp b/lib/httpserver/HTTPRequest.cpp index a9eb0d76c..7fa3dee94 100644 --- a/lib/httpserver/HTTPRequest.cpp +++ b/lib/httpserver/HTTPRequest.cpp @@ -22,6 +22,8 @@ #include "IOStream.h" #include "IOStreamGetLine.h" #include "Logging.h" +#include "PartialReadStream.h" +#include "ReadGatherStream.h" #include "MemLeakFindOn.h" @@ -42,11 +44,8 @@ // -------------------------------------------------------------------------- HTTPRequest::HTTPRequest() : mMethod(Method_UNINITIALISED), - mHostPort(80), // default if not specified mHTTPVersion(0), - mContentLength(-1), mpCookies(0), - mClientKeepAliveRequested(false), mExpectContinue(false), mpStreamToReadFrom(NULL) { @@ -65,11 +64,8 @@ HTTPRequest::HTTPRequest() 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), mExpectContinue(false), mpStreamToReadFrom(NULL) { @@ -115,6 +111,7 @@ std::string HTTPRequest::GetMethodName() const case Method_HEAD: return "HEAD"; case Method_POST: return "POST"; case Method_PUT: return "PUT"; + case Method_DELETE: return "DELETE"; default: std::ostringstream oss; oss << "unknown-" << mMethod; @@ -122,6 +119,11 @@ std::string HTTPRequest::GetMethodName() const }; } +std::string HTTPRequest::GetRequestURI() const +{ + return mRequestURI; +} + // -------------------------------------------------------------------------- // // Function @@ -178,8 +180,14 @@ bool HTTPRequest::Receive(IOStreamGetLine &rGetLine, int Timeout) { mMethod = Method_PUT; } + else if (mHttpVerb == "DELETE") + { + mMethod = Method_DELETE; + } else { + BOX_WARNING("Received HTTP request with unrecognised method: " << + mHttpVerb); mMethod = Method_UNKNOWN; } } @@ -291,7 +299,7 @@ bool HTTPRequest::Receive(IOStreamGetLine &rGetLine, int Timeout) // If HTTP 1.1 or greater, assume keep-alive if(mHTTPVersion >= HTTPVersion_1_1) { - mClientKeepAliveRequested = true; + mHeaders.SetKeepAlive(true); } // Decode query string? @@ -303,7 +311,7 @@ bool HTTPRequest::Receive(IOStreamGetLine &rGetLine, int Timeout) } // Now parse the headers - ParseHeaders(rGetLine, Timeout); + mHeaders.ReadFromStream(rGetLine, Timeout); std::string expected; if(GetHeader("Expect", &expected)) @@ -314,20 +322,36 @@ bool HTTPRequest::Receive(IOStreamGetLine &rGetLine, int Timeout) } } + const std::string& cookies = mHeaders.GetHeaderValue("cookie", false); + // false => not required, returns "" if header is not present. + if(!cookies.empty()) + { + ParseCookies(cookies); + } + // Parse form data? - if(mMethod == Method_POST && mContentLength >= 0) + int64_t contentLength = mHeaders.GetContentLength(); + if(mMethod == Method_POST && contentLength >= 0) { // Too long? Don't allow people to be nasty by sending lots of data - if(mContentLength > MAX_CONTENT_SIZE) + if(contentLength > MAX_CONTENT_SIZE) { - THROW_EXCEPTION(HTTPException, POSTContentTooLong); + THROW_EXCEPTION_MESSAGE(HTTPException, POSTContentTooLong, + "Client tried to upload " << contentLength << " bytes of " + "content, but our maximum supported size is " << + MAX_CONTENT_SIZE); } // Some data in the request to follow, parsing it bit by bit HTTPQueryDecoder decoder(mQuery); + // Don't forget any data left in the GetLine object int fromBuffer = rGetLine.GetSizeOfBufferedData(); - if(fromBuffer > mContentLength) fromBuffer = mContentLength; + if(fromBuffer > contentLength) + { + fromBuffer = contentLength; + } + if(fromBuffer > 0) { BOX_TRACE("Decoding " << fromBuffer << " bytes of " @@ -338,7 +362,7 @@ bool HTTPRequest::Receive(IOStreamGetLine &rGetLine, int Timeout) } // Then read any more data, as required - int bytesToGo = mContentLength - fromBuffer; + int bytesToGo = contentLength - fromBuffer; while(bytesToGo > 0) { char buf[4096]; @@ -358,12 +382,12 @@ bool HTTPRequest::Receive(IOStreamGetLine &rGetLine, int Timeout) // Finish off decoder.Finish(); } - else if (mContentLength > 0) + else { IOStream::pos_type bytesToCopy = rGetLine.GetSizeOfBufferedData(); - if (bytesToCopy > mContentLength) + if (contentLength != -1 && bytesToCopy > contentLength) { - bytesToCopy = mContentLength; + bytesToCopy = contentLength; } Write(rGetLine.GetBufferedData(), bytesToCopy); SetForReading(); @@ -373,24 +397,52 @@ bool HTTPRequest::Receive(IOStreamGetLine &rGetLine, int Timeout) return true; } -void HTTPRequest::ReadContent(IOStream& rStreamToWriteTo) +void HTTPRequest::ReadContent(IOStream& rStreamToWriteTo, int Timeout) { + // TODO FIXME: POST requests (above) do not set mpStreamToReadFrom, would we + // ever want to call ReadContent() on them? I hope not! + ASSERT(mpStreamToReadFrom != NULL); + Seek(0, SeekType_Absolute); - CopyStreamTo(rStreamToWriteTo); + // Copy any data that we've already buffered. + CopyStreamTo(rStreamToWriteTo, Timeout); IOStream::pos_type bytesCopied = GetSize(); - while (bytesCopied < mContentLength) + // Copy the data stream, but only upto the content-length. + int64_t contentLength = mHeaders.GetContentLength(); + if(contentLength == -1) { - char buffer[1024]; - IOStream::pos_type bytesToCopy = sizeof(buffer); - if (bytesToCopy > mContentLength - bytesCopied) + // There is no content-length, so copy all of it. Include the buffered + // data (already copied above) in the final content-length, which we + // update in the HTTPRequest headers. + contentLength = bytesCopied; + + // If there is a stream to read from, then copy its contents too. + if(mpStreamToReadFrom != NULL) { - bytesToCopy = mContentLength - bytesCopied; + contentLength += + mpStreamToReadFrom->CopyStreamTo(rStreamToWriteTo, + Timeout); } - bytesToCopy = mpStreamToReadFrom->Read(buffer, bytesToCopy); - rStreamToWriteTo.Write(buffer, bytesToCopy); - bytesCopied += bytesToCopy; + mHeaders.SetContentLength(contentLength); + } + else + { + // Subtract the amount of data already buffered (and already copied above) + // from the total content-length, to get the amount that we are allowed + // and expected to read from the stream. This will leave the stream + // positioned ready for the next request, or EOF, as the client decides. + PartialReadStream partial(*mpStreamToReadFrom, contentLength - + bytesCopied); + partial.CopyStreamTo(rStreamToWriteTo, Timeout); + + // In case of a timeout or error, PartialReadStream::CopyStreamTo + // should have thrown an exception, so this is just defensive, to + // ensure that the source stream is properly positioned to read + // from again, and the destination received the correct number of + // bytes. + ASSERT(!partial.StreamDataLeft()); } } @@ -398,12 +450,47 @@ void HTTPRequest::ReadContent(IOStream& rStreamToWriteTo) // -------------------------------------------------------------------------- // // Function -// Name: HTTPRequest::Send(IOStream &, int) -// Purpose: Write the request to an IOStream using HTTP. +// Name: HTTPRequest::Send(IOStream &, int, bool) +// Purpose: Write a request with NO CONTENT to an IOStream using +// HTTP. If you want to send a request WITH content, +// such as a PUT or POST request, use SendWithStream() +// instead. // Created: 03/01/09 // // -------------------------------------------------------------------------- -bool HTTPRequest::Send(IOStream &rStream, int Timeout, bool ExpectContinue) + +void HTTPRequest::Send(IOStream &rStream, int Timeout, bool ExpectContinue) +{ + if(mHeaders.GetContentLength() > 0) + { + THROW_EXCEPTION(HTTPException, ContentLengthAlreadySet); + } + + if(GetSize() != 0) + { + THROW_EXCEPTION_MESSAGE(HTTPException, WrongContentLength, + "Tried to send a request without content, but there is data " + "in the request buffer waiting to be sent.") + } + + mHeaders.SetContentLength(0); + SendHeaders(rStream, Timeout, ExpectContinue); +} + + +// -------------------------------------------------------------------------- +// +// Function +// Name: HTTPRequest::SendHeaders(IOStream &, int, bool) +// Purpose: Write the start of a request to an IOStream using +// HTTP. If you want to send a request WITH content, +// but you can't wait for a response, use this followed +// by sending your stream directly to the socket. +// Created: 2015-10-08 +// +// -------------------------------------------------------------------------- + +void HTTPRequest::SendHeaders(IOStream &rStream, int Timeout, bool ExpectContinue) { switch (mMethod) { @@ -411,14 +498,8 @@ bool HTTPRequest::Send(IOStream &rStream, int Timeout, bool ExpectContinue) 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; - case Method_PUT: - rStream.Write("PUT"); break; + default: + rStream.Write(GetMethodName()); } rStream.Write(" "); @@ -435,31 +516,8 @@ bool HTTPRequest::Send(IOStream &rStream, int Timeout, bool ExpectContinue) "Unsupported HTTP version: " << mHTTPVersion); } - 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"; - } - } + rStream.Write("\r\n"); + mHeaders.WriteTo(rStream, Timeout); if (mpCookies) { @@ -467,207 +525,104 @@ bool HTTPRequest::Send(IOStream &rStream, int Timeout, bool ExpectContinue) "Cookie support not implemented yet"); } - if (mClientKeepAliveRequested) - { - oss << "Connection: keep-alive\n"; - } - else - { - oss << "Connection: close\n"; - } - - for (std::vector
::iterator i = mExtraHeaders.begin(); - i != mExtraHeaders.end(); i++) - { - oss << i->first << ": " << i->second << "\n"; - } - if (ExpectContinue) { - oss << "Expect: 100-continue\n"; + rStream.Write("Expect: 100-continue\r\n"); } - rStream.Write(oss.str().c_str()); - rStream.Write("\n"); - - return true; + rStream.Write("\r\n"); } -void HTTPRequest::SendWithStream(IOStream &rStreamToSendTo, int Timeout, - IOStream* pStreamToSend, HTTPResponse& rResponse) -{ - IOStream::pos_type size = pStreamToSend->BytesLeftToRead(); - if (size != IOStream::SizeOfStreamUnknown) - { - mContentLength = size; - } - - Send(rStreamToSendTo, Timeout, true); - - rResponse.Receive(rStreamToSendTo, Timeout); - if (rResponse.GetResponseCode() != 100) - { - // bad response, abort now - return; - } - - pStreamToSend->CopyStreamTo(rStreamToSendTo, Timeout); - - // receive the final response - rResponse.Receive(rStreamToSendTo, Timeout); -} // -------------------------------------------------------------------------- // // Function -// Name: HTTPRequest::ParseHeaders(IOStreamGetLine &, int) -// Purpose: Private. Parse the headers of the request -// Created: 26/3/04 +// Name: HTTPRequest::SendWithStream(IOStream &rStreamToSendTo, +// int Timeout, IOStream* pStreamToSend, +// HTTPResponse& rResponse) +// Purpose: Write a request WITH CONTENT to an IOStream using +// HTTP. If you want to send a request WITHOUT content, +// such as a GET or DELETE request, use Send() instead. +// Because this is interactive (it uses 100 Continue +// responses) it can only be sent to a SocketStream. +// Created: 03/01/09 // // -------------------------------------------------------------------------- -void HTTPRequest::ParseHeaders(IOStreamGetLine &rGetLine, int Timeout) + +IOStream::pos_type HTTPRequest::SendWithStream(SocketStream &rStreamToSendTo, + int Timeout, IOStream* pStreamToSend, HTTPResponse& rResponse) { - std::string header; - bool haveHeader = false; - while(true) + SendHeaders(rStreamToSendTo, Timeout, true); // ExpectContinue + + rResponse.Receive(rStreamToSendTo, Timeout); + if (rResponse.GetResponseCode() != 100) { - if(rGetLine.IsEOF()) - { - // Header terminates unexpectedly - THROW_EXCEPTION(HTTPException, BadRequest) - } + // bad response, abort now + return 0; + } - std::string currentLine; - if(!rGetLine.GetLine(currentLine, false /* no preprocess */, Timeout)) - { - // Timeout - THROW_EXCEPTION(HTTPException, RequestReadFailed) - } + IOStream::pos_type bytes_sent = 0; - // Is this a continuation of the previous line? - bool processHeader = haveHeader; - if(!currentLine.empty() && (currentLine[0] == ' ' || currentLine[0] == '\t')) + if(mHeaders.GetContentLength() == -1) + { + // We don't know how long the stream is, so just send it all. + // Including any data buffered in the HTTPRequest. + CopyStreamTo(rStreamToSendTo, Timeout); + pStreamToSend->CopyStreamTo(rStreamToSendTo, Timeout); + } + else + { + // Check that the length of the stream is correct, and ensure + // that we don't send too much without realising. + ReadGatherStream gather(false); // don't delete anything + + // Send any data buffered in the HTTPRequest first. + gather.AddBlock(gather.AddComponent(this), GetSize()); + + // And the remaining bytes should be read from the supplied stream. + gather.AddBlock(gather.AddComponent(pStreamToSend), + mHeaders.GetContentLength() - GetSize()); + + bytes_sent = gather.CopyStreamTo(rStreamToSendTo, Timeout); + + if(pStreamToSend->StreamDataLeft()) { - // A continuation, don't process anything yet - processHeader = false; + THROW_EXCEPTION_MESSAGE(HTTPException, WrongContentLength, + "Expected to send " << mHeaders.GetContentLength() << + " bytes, but there is still unsent data left in the " + "stream"); } - //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) + if(gather.StreamDataLeft()) { - // 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; - } - - std::string header_name(ToLowerCase(std::string(h, - p))); - - if (header_name == "content-length") - { - // 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 (header_name == "content-type") - { - // Store rest of string as content type - mContentType = h + dataStart; - } - else if (header_name == "host") - { - // Store host header - mHostName = h + dataStart; - - // Is there a port number to split off? - std::string::size_type colon = mHostName.find_first_of(':'); - if(colon != std::string::npos) - { - // There's a port in the string... attempt to turn it into an int - mHostPort = ::strtol(mHostName.c_str() + colon + 1, 0, 10); - - // Truncate the string to just the hostname - mHostName = mHostName.substr(0, colon); - - BOX_TRACE("Host: header, hostname = " << - "'" << mHostName << "', host " - "port = " << mHostPort); - } - } - else if (header_name == "cookie") - { - // Parse cookies - ParseCookies(header, dataStart); - } - else if (header_name == "connection") - { - // Connection header, what is required? - const char *v = h + dataStart; - if(::strcasecmp(v, "close") == 0) - { - mClientKeepAliveRequested = false; - } - else if(::strcasecmp(v, "keep-alive") == 0) - { - mClientKeepAliveRequested = true; - } - // else don't understand, just assume default for protocol version - } - else - { - mExtraHeaders.push_back(Header(header_name, - h + dataStart)); - } - - // Unset have header flag, as it's now been processed - haveHeader = false; + THROW_EXCEPTION_MESSAGE(HTTPException, WrongContentLength, + "Expected to send " << mHeaders.GetContentLength() << + " bytes, but there was not enough data in the stream"); } + } - // Store the chunk of header the for next time round - if(haveHeader) - { - header += currentLine; - } - else - { - header = currentLine; - haveHeader = true; - } + // We don't support keep-alive, so we must shutdown the write side of the stream + // to signal to the other end that we have no more data to send. + ASSERT(!GetClientKeepAliveRequested()); + rStreamToSendTo.Shutdown(false, true); // !read, write - // End of headers? - if(currentLine.empty()) - { - // All done! - break; - } - } + // receive the final response + rResponse.Receive(rStreamToSendTo, Timeout); + return bytes_sent; } // -------------------------------------------------------------------------- // // Function -// Name: HTTPRequest::ParseCookies(const std::string &, int) +// Name: HTTPRequest::ParseCookies(const std::string &) // Purpose: Parse the cookie header // Created: 20/8/04 // // -------------------------------------------------------------------------- -void HTTPRequest::ParseCookies(const std::string &rHeader, int DataStarts) +void HTTPRequest::ParseCookies(const std::string &rCookieString) { - const char *data = rHeader.c_str() + DataStarts; + const char *data = rCookieString.c_str(); const char *pos = data; const char *itemStart = pos; std::string name; diff --git a/lib/httpserver/HTTPRequest.h b/lib/httpserver/HTTPRequest.h index 16c4d16c5..ad8de3f35 100644 --- a/lib/httpserver/HTTPRequest.h +++ b/lib/httpserver/HTTPRequest.h @@ -14,6 +14,8 @@ #include #include "CollectInBufferStream.h" +#include "HTTPHeaders.h" +#include "SocketStream.h" class HTTPResponse; class IOStream; @@ -41,16 +43,47 @@ class HTTPRequest : public CollectInBufferStream Method_GET = 1, Method_HEAD = 2, Method_POST = 3, - Method_PUT = 4 + Method_PUT = 4, + Method_DELETE = 5 }; HTTPRequest(); HTTPRequest(enum Method method, const std::string& rURI); ~HTTPRequest(); -private: - // no copying - HTTPRequest(const HTTPRequest &); - HTTPRequest &operator=(const HTTPRequest &); + + HTTPRequest(const HTTPRequest &to_copy) + : mMethod(to_copy.mMethod), + mRequestURI(to_copy.mRequestURI), + mQueryString(to_copy.mQueryString), + mHTTPVersion(to_copy.mHTTPVersion), + // it's not safe to copy this, as it may be consumed or destroyed: + mpCookies(NULL), + mHeaders(to_copy.mHeaders), + mExpectContinue(to_copy.mExpectContinue), + // it's not safe to copy this, as it may be consumed or destroyed: + mpStreamToReadFrom(NULL), + mHttpVerb(to_copy.mHttpVerb) + // If you ever add members, be sure to update this list too! + { } + + HTTPRequest &operator=(const HTTPRequest &to_copy) + { + mMethod = to_copy.mMethod; + mRequestURI = to_copy.mRequestURI; + mQueryString = to_copy.mQueryString; + mHTTPVersion = to_copy.mHTTPVersion; + // it's not safe to copy this; as it may be modified or destroyed: + mpCookies = NULL; + mHeaders = to_copy.mHeaders; + mExpectContinue = to_copy.mExpectContinue; + // it's not safe to copy this; as it may be consumed or destroyed: + mpStreamToReadFrom = NULL; + mHttpVerb = to_copy.mHttpVerb; + // If you ever add members, be sure to update this list too! + + return *this; + } + public: typedef std::multimap Query_t; typedef Query_t::value_type QueryEn_t; @@ -65,60 +98,42 @@ class HTTPRequest : public CollectInBufferStream }; bool Receive(IOStreamGetLine &rGetLine, int Timeout); - bool Send(IOStream &rStream, int Timeout, bool ExpectContinue = false); - void SendWithStream(IOStream &rStreamToSendTo, int Timeout, + void SendHeaders(IOStream &rStream, int Timeout, bool ExpectContinue = false); + void Send(IOStream &rStream, int Timeout, bool ExpectContinue = false); + IOStream::pos_type SendWithStream(SocketStream &rStreamToSendTo, int Timeout, IOStream* pStreamToSend, HTTPResponse& rResponse); - void ReadContent(IOStream& rStreamToWriteTo); + void ReadContent(IOStream& rStreamToWriteTo, int Timeout); typedef std::map CookieJar_t; - // -------------------------------------------------------------------------- - // - // Function - // Name: HTTPResponse::Get*() - // Purpose: Various Get accessors - // Created: 26/3/04 - // - // -------------------------------------------------------------------------- enum Method GetMethod() const {return mMethod;} std::string GetMethodName() const; - const std::string &GetRequestURI() const {return mRequestURI;} + std::string GetRequestURI() const; - // Note: the HTTPRequest generates and parses the Host: header - // Do not attempt to set one yourself with AddHeader(). - const std::string &GetHostName() const {return mHostName;} + const std::string &GetHostName() const {return mHeaders.GetHostName();} void SetHostName(const std::string& rHostName) { - mHostName = rHostName; + mHeaders.SetHostName(rHostName); } - - const int GetHostPort() const {return mHostPort;} + const int GetHostPort() const {return mHeaders.GetHostPort();} const std::string &GetQueryString() const {return mQueryString;} int GetHTTPVersion() const {return mHTTPVersion;} const Query_t &GetQuery() const {return mQuery;} - int GetContentLength() const {return mContentLength;} - const std::string &GetContentType() const {return mContentType;} + + int GetContentLength() const {return mHeaders.GetContentLength();} + const std::string &GetContentType() const {return mHeaders.GetContentType();} const CookieJar_t *GetCookies() const {return mpCookies;} // WARNING: May return NULL bool GetCookie(const char *CookieName, std::string &rValueOut) const; const std::string &GetCookie(const char *CookieName) const; bool GetHeader(const std::string& rName, std::string* pValueOut) const { - std::string header = ToLowerCase(rName); - - for (std::vector
::const_iterator - i = mExtraHeaders.begin(); - i != mExtraHeaders.end(); i++) - { - if (i->first == header) - { - *pValueOut = i->second; - return true; - } - } - - return false; + return mHeaders.GetHeader(rName, pValueOut); } - std::vector
GetHeaders() { return mExtraHeaders; } + void AddHeader(const std::string& rName, const std::string& rValue) + { + mHeaders.AddHeader(rName, rValue); + } + const HTTPHeaders& GetHeaders() const { return mHeaders; } // -------------------------------------------------------------------------- // @@ -129,65 +144,36 @@ class HTTPRequest : public CollectInBufferStream // Created: 22/12/04 // // -------------------------------------------------------------------------- - bool GetClientKeepAliveRequested() const {return mClientKeepAliveRequested;} + bool GetClientKeepAliveRequested() const {return mHeaders.IsKeepAlive();} void SetClientKeepAliveRequested(bool keepAlive) { - mClientKeepAliveRequested = keepAlive; + mHeaders.SetKeepAlive(keepAlive); } - void AddHeader(const std::string& rName, const std::string& rValue) - { - mExtraHeaders.push_back(Header(ToLowerCase(rName), rValue)); - } bool IsExpectingContinue() const { return mExpectContinue; } - const char* GetVerb() const + + // This is not supposed to be an API, but the S3Simulator needs to be able to + // associate a data stream with an HTTPRequest when handling it in-process. + void SetDataStream(IOStream* pStreamToReadFrom) { - if (!mHttpVerb.empty()) - { - return mHttpVerb.c_str(); - } - switch (mMethod) - { - case Method_UNINITIALISED: return "Uninitialized"; - case Method_UNKNOWN: return "Unknown"; - case Method_GET: return "GET"; - case Method_HEAD: return "HEAD"; - case Method_POST: return "POST"; - case Method_PUT: return "PUT"; - } - return "Bad"; + ASSERT(!mpStreamToReadFrom); + mpStreamToReadFrom = pStreamToReadFrom; } - + private: - void ParseHeaders(IOStreamGetLine &rGetLine, int Timeout); - void ParseCookies(const std::string &rHeader, int DataStarts); + void ParseCookies(const std::string &rCookieString); enum Method mMethod; std::string mRequestURI; - std::string mHostName; - int mHostPort; std::string mQueryString; int mHTTPVersion; Query_t mQuery; - int mContentLength; - std::string mContentType; CookieJar_t *mpCookies; - bool mClientKeepAliveRequested; - std::vector
mExtraHeaders; + HTTPHeaders mHeaders; bool mExpectContinue; IOStream* mpStreamToReadFrom; std::string mHttpVerb; - - std::string ToLowerCase(const std::string& rInput) const - { - std::string output = rInput; - for (std::string::iterator c = output.begin(); - c != output.end(); c++) - { - *c = tolower(*c); - } - return output; - } + // If you ever add members, be sure to update the copy constructor too! }; #endif // HTTPREQUEST__H diff --git a/lib/httpserver/HTTPResponse.cpp b/lib/httpserver/HTTPResponse.cpp index c56f286fb..26492d359 100644 --- a/lib/httpserver/HTTPResponse.cpp +++ b/lib/httpserver/HTTPResponse.cpp @@ -33,8 +33,6 @@ std::string HTTPResponse::msDefaultURIPrefix; HTTPResponse::HTTPResponse(IOStream* pStreamToSendTo) : mResponseCode(HTTPResponse::Code_NoContent), mResponseIsDynamicContent(true), - mKeepAlive(false), - mContentLength(-1), mpStreamToSendTo(pStreamToSendTo) { } @@ -51,13 +49,35 @@ HTTPResponse::HTTPResponse(IOStream* pStreamToSendTo) HTTPResponse::HTTPResponse() : mResponseCode(HTTPResponse::Code_NoContent), mResponseIsDynamicContent(true), - mKeepAlive(false), - mContentLength(-1), mpStreamToSendTo(NULL) { } +// allow copying, but be very careful with the response stream, +// you can only read it once! (this class doesn't police it). +HTTPResponse::HTTPResponse(const HTTPResponse& rOther) +: mResponseCode(rOther.mResponseCode), + mResponseIsDynamicContent(rOther.mResponseIsDynamicContent), + mpStreamToSendTo(rOther.mpStreamToSendTo), + mHeaders(rOther.mHeaders) +{ + Write(rOther.GetBuffer(), rOther.GetSize()); +} + + +HTTPResponse &HTTPResponse::operator=(const HTTPResponse &rOther) +{ + Reset(); + Write(rOther.GetBuffer(), rOther.GetSize()); + mResponseCode = rOther.mResponseCode; + mResponseIsDynamicContent = rOther.mResponseIsDynamicContent; + mHeaders = rOther.mHeaders; + mpStreamToSendTo = rOther.mpStreamToSendTo; + return *this; +} + + // -------------------------------------------------------------------------- // // Function @@ -90,10 +110,11 @@ const char *HTTPResponse::ResponseCodeToString(int ResponseCode) case Code_Found: return "302 Found"; break; case Code_NotModified: return "304 Not Modified"; break; case Code_TemporaryRedirect: return "307 Temporary Redirect"; break; - case Code_MethodNotAllowed: return "400 Method Not Allowed"; break; + case Code_BadRequest: return "400 Bad Request"; break; case Code_Unauthorized: return "401 Unauthorized"; break; case Code_Forbidden: return "403 Forbidden"; break; case Code_NotFound: return "404 Not Found"; break; + case Code_Conflict: return "409 Conflict"; break; case Code_InternalServerError: return "500 Internal Server Error"; break; case Code_NotImplemented: return "501 Not Implemented"; break; default: @@ -119,20 +140,6 @@ void HTTPResponse::SetResponseCode(int Code) } -// -------------------------------------------------------------------------- -// -// Function -// Name: HTTPResponse::SetContentType(const char *) -// Purpose: Set content type -// Created: 26/3/04 -// -// -------------------------------------------------------------------------- -void HTTPResponse::SetContentType(const char *ContentType) -{ - mContentType = ContentType; -} - - // -------------------------------------------------------------------------- // // Function @@ -148,190 +155,49 @@ void HTTPResponse::Send(bool OmitContent) THROW_EXCEPTION(HTTPException, NoStreamConfigured); } - if (GetSize() != 0 && mContentType.empty()) + if (GetSize() != 0 && mHeaders.GetContentType().empty()) { THROW_EXCEPTION(HTTPException, NoContentTypeSet); } // Build and send header { - std::string header("HTTP/1.1 "); - header += ResponseCodeToString(mResponseCode); - header += "\r\nContent-Type: "; - header += mContentType; - header += "\r\nContent-Length: "; - { - char len[32]; - ::sprintf(len, "%d", OmitContent?(0):(GetSize())); - header += len; - } - // Extra headers... - for(std::vector >::const_iterator i(mExtraHeaders.begin()); i != mExtraHeaders.end(); ++i) - { - header += "\r\n"; - header += i->first + ": " + i->second; - } - // NOTE: a line ending must be included here in all cases + std::ostringstream header; + header << "HTTP/1.1 "; + header << ResponseCodeToString(mResponseCode); + header << "\r\n"; + mpStreamToSendTo->Write(header.str()); + // Control whether the response is cached if(mResponseIsDynamicContent) { // dynamic is private and can't be cached - header += "\r\nCache-Control: no-cache, private"; + mHeaders.AddHeader("Cache-Control", "no-cache, private"); } else { // static is allowed to be cached for a day - header += "\r\nCache-Control: max-age=86400"; + mHeaders.AddHeader("Cache-Control", "max-age=86400"); } - if(mKeepAlive) - { - header += "\r\nConnection: keep-alive\r\n\r\n"; - } - else - { - header += "\r\nConnection: close\r\n\r\n"; - } + // Write to stream + mHeaders.WriteTo(*mpStreamToSendTo, IOStream::TimeOutInfinite); // NOTE: header ends with blank line in all cases - - // Write to stream - mpStreamToSendTo->Write(header.c_str(), header.size()); + mpStreamToSendTo->Write(std::string("\r\n")); } // Send content if(!OmitContent) { - mpStreamToSendTo->Write(GetBuffer(), GetSize()); + SetForReading(); + CopyStreamTo(*mpStreamToSendTo); } } void HTTPResponse::SendContinue() { - mpStreamToSendTo->Write("HTTP/1.1 100 Continue\r\n"); -} - -// -------------------------------------------------------------------------- -// -// 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; - } - } + mpStreamToSendTo->Write(std::string("HTTP/1.1 100 Continue\r\n")); } void HTTPResponse::Receive(IOStream& rStream, int Timeout) @@ -353,140 +219,82 @@ void HTTPResponse::Receive(IOStream& rStream, int Timeout) "Failed to get a response from the HTTP server within the timeout"); } - if (statusLine.substr(0, 7) != "HTTP/1." || statusLine[8] != ' ') + if(statusLine.substr(0, 7) != "HTTP/1." || statusLine[8] != ' ') { THROW_EXCEPTION_MESSAGE(HTTPException, BadResponse, "HTTP server sent an invalid HTTP status line: " << statusLine); } - if (statusLine[5] == '1' && statusLine[7] == '1') + if(statusLine[5] == '1' && statusLine[7] == '1') { // HTTP/1.1 default is to keep alive - mKeepAlive = true; + mHeaders.SetKeepAlive(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; + if(status < 0) status = 0; // Store mResponseCode = status; // 100 Continue responses have no headers, terminating newline, or body - if (status == 100) + if(status == 100) { return; } - ParseHeaders(rGetLine, Timeout); + mHeaders.ReadFromStream(rGetLine, Timeout); + int remaining_bytes = mHeaders.GetContentLength(); // push back whatever bytes we have left // rGetLine.DetachFile(); - if (mContentLength > 0) + if(remaining_bytes == -1 || remaining_bytes > 0) { - if (mContentLength < rGetLine.GetSizeOfBufferedData()) + if(remaining_bytes != -1 && + remaining_bytes < rGetLine.GetSizeOfBufferedData()) { // very small response, not good! - THROW_EXCEPTION(HTTPException, NotImplemented); + THROW_EXCEPTION_MESSAGE(HTTPException, BadResponse, + "HTTP server sent a very small response: " << + mHeaders.GetContentLength() << " bytes"); } - mContentLength -= rGetLine.GetSizeOfBufferedData(); + if(remaining_bytes > 0) + { + remaining_bytes -= rGetLine.GetSizeOfBufferedData(); + } Write(rGetLine.GetBufferedData(), rGetLine.GetSizeOfBufferedData()); } - while (mContentLength != 0) // could be -1 as well + while(remaining_bytes != 0) // could be -1 as well { char buffer[4096]; int readSize = sizeof(buffer); - if (mContentLength > 0 && mContentLength < readSize) + + if(remaining_bytes > 0 && remaining_bytes < readSize) { - readSize = mContentLength; + readSize = remaining_bytes; } + readSize = rStream.Read(buffer, readSize, Timeout); - if (readSize == 0) + if(readSize == 0) { break; } - mContentLength -= readSize; + Write(buffer, readSize); + if(remaining_bytes > 0) + { + remaining_bytes -= 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 -// Name: HTTPResponse::AddHeader(const std::string &) -// Purpose: Add header, given entire line -// Created: 26/3/04 -// -// -------------------------------------------------------------------------- -/* -void HTTPResponse::AddHeader(const std::string &rEntireHeaderLine) -{ - mExtraHeaders.push_back(rEntireHeaderLine); -} -*/ - -// -------------------------------------------------------------------------- -// -// Function -// Name: HTTPResponse::AddHeader(const char *, const char *) -// Purpose: Add header, given header name and it's value -// Created: 26/3/04 -// -// -------------------------------------------------------------------------- -void HTTPResponse::AddHeader(const char *pHeader, const char *pValue) -{ - mExtraHeaders.push_back(Header(pHeader, pValue)); -} - - -// -------------------------------------------------------------------------- -// -// Function -// Name: HTTPResponse::AddHeader(const char *, const std::string &) -// Purpose: Add header, given header name and it's value -// Created: 26/3/04 -// -// -------------------------------------------------------------------------- -void HTTPResponse::AddHeader(const char *pHeader, const std::string &rValue) -{ - mExtraHeaders.push_back(Header(pHeader, rValue)); -} - - -// -------------------------------------------------------------------------- -// -// Function -// Name: HTTPResponse::AddHeader(const std::string &, const std::string &) -// Purpose: Add header, given header name and it's value -// Created: 26/3/04 -// -// -------------------------------------------------------------------------- -void HTTPResponse::AddHeader(const std::string &rHeader, const std::string &rValue) -{ - mExtraHeaders.push_back(Header(rHeader, rValue)); -} - // -------------------------------------------------------------------------- // @@ -520,7 +328,7 @@ void HTTPResponse::SetCookie(const char *Name, const char *Value, const char *Pa h += "; Version=1; Path="; h += Path; - mExtraHeaders.push_back(Header("Set-Cookie", h)); + AddHeader("Set-Cookie", h); } @@ -536,7 +344,7 @@ void HTTPResponse::SetCookie(const char *Name, const char *Value, const char *Pa void HTTPResponse::SetAsRedirect(const char *RedirectTo, bool IsLocalURI) { if(mResponseCode != HTTPResponse::Code_NoContent - || !mContentType.empty() + || !mHeaders.GetContentType().empty() || GetSize() != 0) { THROW_EXCEPTION(HTTPException, CannotSetRedirectIfReponseHasData) @@ -549,10 +357,10 @@ void HTTPResponse::SetAsRedirect(const char *RedirectTo, bool IsLocalURI) std::string header; if(IsLocalURI) header += msDefaultURIPrefix; header += RedirectTo; - mExtraHeaders.push_back(Header("Location", header)); + mHeaders.AddHeader("location", header); // Set up some default content - mContentType = "text/html"; + mHeaders.SetContentType("text/html"); #define REDIRECT_HTML_1 "Redirection\n

Redirect to content

\n" Write(REDIRECT_HTML_1, sizeof(REDIRECT_HTML_1) - 1); @@ -573,8 +381,8 @@ void HTTPResponse::SetAsRedirect(const char *RedirectTo, bool IsLocalURI) void HTTPResponse::SetAsNotFound(const char *URI) { if(mResponseCode != HTTPResponse::Code_NoContent - || mExtraHeaders.size() != 0 - || !mContentType.empty() + || !mHeaders.GetExtraHeaders().empty() + || !mHeaders.GetContentType().empty() || GetSize() != 0) { THROW_EXCEPTION(HTTPException, CannotSetNotFoundIfReponseHasData) @@ -584,7 +392,7 @@ void HTTPResponse::SetAsNotFound(const char *URI) mResponseCode = Code_NotFound; // Set data - mContentType = "text/html"; + mHeaders.SetContentType("text/html"); #define NOT_FOUND_HTML_1 "404 Not Found\n

404 Not Found

\n

The URI " #define NOT_FOUND_HTML_2 " was not found on this server.

\n" Write(NOT_FOUND_HTML_1, sizeof(NOT_FOUND_HTML_1) - 1); diff --git a/lib/httpserver/HTTPResponse.h b/lib/httpserver/HTTPResponse.h index f39825d99..957b90219 100644 --- a/lib/httpserver/HTTPResponse.h +++ b/lib/httpserver/HTTPResponse.h @@ -14,6 +14,7 @@ #include #include "CollectInBufferStream.h" +#include "HTTPHeaders.h" class IOStreamGetLine; @@ -34,39 +35,17 @@ class HTTPResponse : public CollectInBufferStream // allow copying, but be very careful with the response stream, // you can only read it once! (this class doesn't police it). - HTTPResponse(const HTTPResponse& rOther) - : mResponseCode(rOther.mResponseCode), - mResponseIsDynamicContent(rOther.mResponseIsDynamicContent), - mKeepAlive(rOther.mKeepAlive), - mContentType(rOther.mContentType), - mExtraHeaders(rOther.mExtraHeaders), - mContentLength(rOther.mContentLength), - mpStreamToSendTo(rOther.mpStreamToSendTo) - { - Write(rOther.GetBuffer(), rOther.GetSize()); - } - - HTTPResponse &operator=(const HTTPResponse &rOther) - { - Reset(); - Write(rOther.GetBuffer(), rOther.GetSize()); - mResponseCode = rOther.mResponseCode; - mResponseIsDynamicContent = rOther.mResponseIsDynamicContent; - mKeepAlive = rOther.mKeepAlive; - mContentType = rOther.mContentType; - mExtraHeaders = rOther.mExtraHeaders; - mContentLength = rOther.mContentLength; - mpStreamToSendTo = rOther.mpStreamToSendTo; - return *this; - } - - typedef std::pair Header; + HTTPResponse(const HTTPResponse& rOther); + HTTPResponse &operator=(const HTTPResponse &rOther); void SetResponseCode(int Code); int GetResponseCode() const { return mResponseCode; } - void SetContentType(const char *ContentType); - const std::string& GetContentType() { return mContentType; } - int64_t GetContentLength() { return mContentLength; } + void SetContentType(const char *ContentType) + { + mHeaders.SetContentType(ContentType); + } + const std::string& GetContentType() { return mHeaders.GetContentType(); } + int64_t GetContentLength() { return mHeaders.GetContentLength(); } void SetAsRedirect(const char *RedirectTo, bool IsLocalURI = true); void SetAsNotFound(const char *URI); @@ -75,40 +54,28 @@ class HTTPResponse : public CollectInBufferStream void SendContinue(); void Receive(IOStream& rStream, int Timeout = IOStream::TimeOutInfinite); - // 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); - bool GetHeader(const std::string& rName, std::string* pValueOut) const + bool GetHeader(const std::string& name, std::string* pValueOut) const { - for (std::vector
::const_iterator - i = mExtraHeaders.begin(); - i != mExtraHeaders.end(); i++) - { - if (i->first == rName) - { - *pValueOut = i->second; - return true; - } - } - return false; + return mHeaders.GetHeader(name, pValueOut); } - std::string GetHeaderValue(const std::string& rName) + std::string GetHeaderValue(const std::string& name) { - std::string value; - if (!GetHeader(rName, &value)) - { - THROW_EXCEPTION(CommonException, ConfigNoKey); - } - return value; + return mHeaders.GetHeaderValue(name); } + void AddHeader(const std::string& name, const std::string& value) + { + mHeaders.AddHeader(name, value); + } + HTTPHeaders& GetHeaders() { return mHeaders; } // Set dynamic content flag, default is content is dynamic void SetResponseIsDynamicContent(bool IsDynamic) {mResponseIsDynamicContent = IsDynamic;} // Set keep alive control, default is to mark as to be closed - void SetKeepAlive(bool KeepAlive) {mKeepAlive = KeepAlive;} - bool IsKeepAlive() {return mKeepAlive;} + void SetKeepAlive(bool KeepAlive) + { + mHeaders.SetKeepAlive(KeepAlive); + } + bool IsKeepAlive() {return mHeaders.IsKeepAlive();} void SetCookie(const char *Name, const char *Value, const char *Path = "/", int ExpiresAt = 0); @@ -117,18 +84,23 @@ class HTTPResponse : public CollectInBufferStream Code_OK = 200, Code_NoContent = 204, Code_MovedPermanently = 301, - Code_Found = 302, // redirection + Code_Found = 302, // redirection Code_NotModified = 304, Code_TemporaryRedirect = 307, - Code_MethodNotAllowed = 400, + Code_BadRequest = 400, Code_Unauthorized = 401, Code_Forbidden = 403, Code_NotFound = 404, + Code_Conflict = 409, Code_InternalServerError = 500, Code_NotImplemented = 501 }; static const char *ResponseCodeToString(int ResponseCode); + const char *ResponseCodeString() const + { + return ResponseCodeToString(mResponseCode); + } void WriteStringDefang(const char *String, unsigned int StringLen); void WriteStringDefang(const std::string &rString) {WriteStringDefang(rString.c_str(), rString.size());} @@ -159,18 +131,36 @@ class HTTPResponse : public CollectInBufferStream msDefaultURIPrefix = rPrefix; } + // Update Content-Length from current buffer size. + void SetForReading() + { + CollectInBufferStream::SetForReading(); + // If the ContentLength is unknown, set it to the size of the response. + // But if the user has specifically set it to something, then leave it + // alone. + if(mHeaders.GetContentLength() == HTTPHeaders::UNKNOWN_CONTENT_LENGTH) + { + mHeaders.SetContentLength(GetSize()); + } + } + + // Clear all state for reading again + void Reset() + { + CollectInBufferStream::Reset(); + mHeaders = HTTPHeaders(); + mResponseCode = HTTPResponse::Code_NoContent; + mResponseIsDynamicContent = true; + mpStreamToSendTo = NULL; + } + private: int mResponseCode; bool mResponseIsDynamicContent; - bool mKeepAlive; - std::string mContentType; - std::vector
mExtraHeaders; - int64_t mContentLength; // only used when reading response from stream IOStream* mpStreamToSendTo; // nonzero only when constructed with a stream static std::string msDefaultURIPrefix; - - void ParseHeaders(IOStreamGetLine &rGetLine, int Timeout); + HTTPHeaders mHeaders; }; #endif // HTTPRESPONSE__H diff --git a/lib/httpserver/S3Client.cpp b/lib/httpserver/S3Client.cpp index 218140665..3d525844e 100644 --- a/lib/httpserver/S3Client.cpp +++ b/lib/httpserver/S3Client.cpp @@ -145,7 +145,7 @@ HTTPResponse S3Client::FinishAndSendRequest(HTTPRequest::Method Method, } std::ostringstream data; - data << request.GetVerb() << "\n"; + data << request.GetMethodName() << "\n"; data << "\n"; /* Content-MD5 */ data << request.GetContentType() << "\n"; data << date.str() << "\n"; diff --git a/lib/httpserver/S3Simulator.cpp b/lib/httpserver/S3Simulator.cpp index df8910d75..c400e93dc 100644 --- a/lib/httpserver/S3Simulator.cpp +++ b/lib/httpserver/S3Simulator.cpp @@ -118,14 +118,14 @@ void S3Simulator::Handle(HTTPRequest &rRequest, HTTPResponse &rResponse) } std::ostringstream data; - data << rRequest.GetVerb() << "\n"; + data << rRequest.GetMethodName() << "\n"; data << md5 << "\n"; data << rRequest.GetContentType() << "\n"; data << date << "\n"; // header names are already in lower case, i.e. canonical form - std::vector headers = rRequest.GetHeaders(); + std::vector headers = rRequest.GetHeaders().GetExtraHeaders(); std::sort(headers.begin(), headers.end()); for (std::vector::iterator @@ -181,7 +181,7 @@ void S3Simulator::Handle(HTTPRequest &rRequest, HTTPResponse &rResponse) } else { - rResponse.SetResponseCode(HTTPResponse::Code_MethodNotAllowed); + rResponse.SetResponseCode(HTTPResponse::Code_BadRequest); SendInternalErrorResponse("Unsupported Method", rResponse); } @@ -298,7 +298,7 @@ void S3Simulator::HandlePut(HTTPRequest &rRequest, HTTPResponse &rResponse) rResponse.SendContinue(); } - rRequest.ReadContent(*apFile); + rRequest.ReadContent(*apFile, IOStream::TimeOutInfinite); // http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTObjectPUT.html rResponse.AddHeader("x-amz-id-2", "LriYPLdmOdAiIfgSm/F1YsViT1LW94/xUQxMsF7xiEb1a0wiIOIxl+zbwZ163pt7"); diff --git a/test/httpserver/testhttpserver.cpp b/test/httpserver/testhttpserver.cpp index 469fa3837..a628a6f0e 100644 --- a/test/httpserver/testhttpserver.cpp +++ b/test/httpserver/testhttpserver.cpp @@ -18,8 +18,6 @@ #include #endif -#include - #include "autogen_HTTPException.h" #include "HTTPRequest.h" #include "HTTPResponse.h" @@ -35,6 +33,7 @@ #include "MemLeakFindOn.h" #define SHORT_TIMEOUT 5000 +#define LONG_TIMEOUT 300000 class TestWebServer : public HTTPServer { @@ -128,20 +127,106 @@ void TestWebServer::Handle(HTTPRequest &rRequest, HTTPResponse &rResponse) TestWebServer::TestWebServer() {} TestWebServer::~TestWebServer() {} -int test(int argc, const char *argv[]) +bool test_httpserver() { - if(argc >= 2 && ::strcmp(argv[1], "server") == 0) + SETUP(); + + // Test that HTTPRequest can be written to and read from a stream. { - // Run a server - TestWebServer server; - return server.Main("doesnotexist", argc - 1, argv + 1); + HTTPRequest request(HTTPRequest::Method_PUT, "/newfile"); + request.SetHostName("quotes.s3.amazonaws.com"); + // Write headers in lower case. + request.AddHeader("date", "Wed, 01 Mar 2006 12:00:00 GMT"); + request.AddHeader("authorization", + "AWS foo:bar="); + request.AddHeader("Content-Type", "text/plain"); + request.SetClientKeepAliveRequested(true); + + // Stream it to a CollectInBufferStream + CollectInBufferStream request_buffer; + + // Because there isn't an HTTP server to respond to us, we can't use + // SendWithStream, so just send the content after the request. + request.SendHeaders(request_buffer, IOStream::TimeOutInfinite); + FileStream fs("testfiles/photos/puppy.jpg"); + fs.CopyStreamTo(request_buffer); + + request_buffer.SetForReading(); + + IOStreamGetLine getLine(request_buffer); + HTTPRequest request2; + TEST_THAT(request2.Receive(getLine, IOStream::TimeOutInfinite)); + + TEST_EQUAL(HTTPRequest::Method_PUT, request2.GetMethod()); + TEST_EQUAL("PUT", request2.GetMethodName()); + TEST_EQUAL("/newfile", request2.GetRequestURI()); + TEST_EQUAL("quotes.s3.amazonaws.com", request2.GetHostName()); + TEST_EQUAL(80, request2.GetHostPort()); + TEST_EQUAL("", request2.GetQueryString()); + TEST_EQUAL("text/plain", request2.GetContentType()); + // Content-Length was not known when the stream was sent, so it should + // be unknown in the received stream too (certainly before it has all + // been read!) + TEST_EQUAL(-1, request2.GetContentLength()); + const HTTPHeaders& headers(request2.GetHeaders()); + TEST_EQUAL("Wed, 01 Mar 2006 12:00:00 GMT", + headers.GetHeaderValue("Date")); + TEST_EQUAL("AWS foo:bar=", + headers.GetHeaderValue("Authorization")); + TEST_THAT(request2.GetClientKeepAliveRequested()); + + CollectInBufferStream request_data; + request2.ReadContent(request_data, IOStream::TimeOutInfinite); + TEST_EQUAL(fs.GetPosition(), request_data.GetPosition()); + request_data.SetForReading(); + fs.Seek(0, IOStream::SeekType_Absolute); + TEST_THAT(fs.CompareWith(request_data, IOStream::TimeOutInfinite)); } - if(argc >= 2 && ::strcmp(argv[1], "s3server") == 0) + // Test that HTTPResponse can be written to and read from a stream. + // TODO FIXME: we should stream the response instead of buffering it, on both + // sides (send and receive). { - // Run a server - S3Simulator server; - return server.Main("doesnotexist", argc - 1, argv + 1); + // Stream it to a CollectInBufferStream + CollectInBufferStream response_buffer; + + HTTPResponse response(&response_buffer); + FileStream fs("testfiles/photos/puppy.jpg"); + // Write headers in lower case. + response.SetResponseCode(HTTPResponse::Code_OK); + response.AddHeader("date", "Wed, 01 Mar 2006 12:00:00 GMT"); + response.AddHeader("authorization", + "AWS foo:bar="); + response.AddHeader("content-type", "text/perl"); + fs.CopyStreamTo(response); + response.Send(); + response_buffer.SetForReading(); + + HTTPResponse response2; + response2.Receive(response_buffer); + + TEST_EQUAL(200, response2.GetResponseCode()); + TEST_EQUAL("text/perl", response2.GetContentType()); + + // TODO FIXME: Content-Length was not known when the stream was sent, + // so it should be unknown in the received stream too (certainly before + // it has all been read!) This is currently wrong because we read the + // entire response into memory immediately. + TEST_EQUAL(fs.GetPosition(), response2.GetContentLength()); + + HTTPHeaders& headers(response2.GetHeaders()); + TEST_EQUAL("Wed, 01 Mar 2006 12:00:00 GMT", + headers.GetHeaderValue("Date")); + TEST_EQUAL("AWS foo:bar=", + headers.GetHeaderValue("Authorization")); + + CollectInBufferStream response_data; + // request2.ReadContent(request_data, IOStream::TimeOutInfinite); + response2.CopyStreamTo(response_data); + TEST_EQUAL(fs.GetPosition(), response_data.GetPosition()); + response_data.SetForReading(); + fs.Seek(0, IOStream::SeekType_Absolute); + TEST_THAT(fs.CompareWith(response_data, IOStream::TimeOutInfinite)); } #ifndef WIN32 @@ -156,11 +241,11 @@ int test(int argc, const char *argv[]) // Run the request script TEST_THAT(::system("perl testfiles/testrequests.pl") == 0); -#ifdef ENABLE_KEEPALIVE_SUPPORT // incomplete, need chunked encoding support #ifndef WIN32 signal(SIGPIPE, SIG_IGN); #endif +#ifdef ENABLE_KEEPALIVE_SUPPORT // incomplete, need chunked encoding support SocketStream sock; sock.Open(Socket::TypeINET, "localhost", 1080); @@ -257,237 +342,26 @@ int test(int argc, const char *argv[]) // Kill it TEST_THAT(StopDaemon(pid, "testfiles/httpserver.pid", "generic-httpserver.memleaks", true)); + TEARDOWN(); +} - // correct, official signature should succeed, with lower-case header - { - // http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAuthentication.html - HTTPRequest request(HTTPRequest::Method_GET, "/photos/puppy.jpg"); - request.SetHostName("johnsmith.s3.amazonaws.com"); - request.AddHeader("date", "Tue, 27 Mar 2007 19:36:42 +0000"); - request.AddHeader("authorization", - "AWS 0PN5J17HBGZHT7JJ3X82:xXjDGYUmKxnwqr5KXNPGldn5LbA="); - - S3Simulator simulator; - simulator.Configure("testfiles/s3simulator.conf"); - - CollectInBufferStream response_buffer; - HTTPResponse response(&response_buffer); - - simulator.Handle(request, response); - TEST_EQUAL(200, response.GetResponseCode()); - - std::string response_data((const char *)response.GetBuffer(), - response.GetSize()); - TEST_EQUAL("omgpuppies!\n", response_data); - } - - // modified signature should fail - { - // http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAuthentication.html - HTTPRequest request(HTTPRequest::Method_GET, "/photos/puppy.jpg"); - request.SetHostName("johnsmith.s3.amazonaws.com"); - request.AddHeader("date", "Tue, 27 Mar 2007 19:36:42 +0000"); - request.AddHeader("authorization", - "AWS 0PN5J17HBGZHT7JJ3X82:xXjDGYUmKxnwqr5KXNPGldn5LbB="); - - S3Simulator simulator; - simulator.Configure("testfiles/s3simulator.conf"); - - CollectInBufferStream response_buffer; - HTTPResponse response(&response_buffer); - - simulator.Handle(request, response); - TEST_EQUAL(401, response.GetResponseCode()); - - std::string response_data((const char *)response.GetBuffer(), - response.GetSize()); - TEST_EQUAL("" - "Internal Server Error\n" - "

Internal Server Error

\n" - "

An error, type Authentication Failed occured " - "when processing the request.

" - "

Please try again later.

\n" - "\n", response_data); - } - - // S3Client tests with S3Simulator in-process server for debugging - { - S3Simulator simulator; - simulator.Configure("testfiles/s3simulator.conf"); - S3Client client(&simulator, "johnsmith.s3.amazonaws.com", - "0PN5J17HBGZHT7JJ3X82", - "uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o"); - - HTTPResponse response = client.GetObject("/photos/puppy.jpg"); - TEST_EQUAL(200, response.GetResponseCode()); - std::string response_data((const char *)response.GetBuffer(), - response.GetSize()); - TEST_EQUAL("omgpuppies!\n", response_data); - - // make sure that assigning to HTTPResponse does clear stream - response = client.GetObject("/photos/puppy.jpg"); - TEST_EQUAL(200, response.GetResponseCode()); - response_data = std::string((const char *)response.GetBuffer(), - response.GetSize()); - TEST_EQUAL("omgpuppies!\n", response_data); - - response = client.GetObject("/nonexist"); - TEST_EQUAL(404, response.GetResponseCode()); - - FileStream fs("testfiles/testrequests.pl"); - response = client.PutObject("/newfile", fs); - TEST_EQUAL(200, response.GetResponseCode()); - - response = client.GetObject("/newfile"); - TEST_EQUAL(200, response.GetResponseCode()); - TEST_THAT(fs.CompareWith(response)); - TEST_EQUAL(0, ::unlink("testfiles/newfile")); - } - - { - HTTPRequest request(HTTPRequest::Method_PUT, "/newfile"); - request.SetHostName("quotes.s3.amazonaws.com"); - request.AddHeader("date", "Wed, 01 Mar 2006 12:00:00 GMT"); - request.AddHeader("authorization", - "AWS 0PN5J17HBGZHT7JJ3X82:XtMYZf0hdOo4TdPYQknZk0Lz7rw="); - request.AddHeader("Content-Type", "text/plain"); - - FileStream fs("testfiles/testrequests.pl"); - fs.CopyStreamTo(request); - request.SetForReading(); - - CollectInBufferStream response_buffer; - HTTPResponse response(&response_buffer); - - S3Simulator simulator; - simulator.Configure("testfiles/s3simulator.conf"); - simulator.Handle(request, response); - - TEST_EQUAL(200, response.GetResponseCode()); - TEST_EQUAL("LriYPLdmOdAiIfgSm/F1YsViT1LW94/xUQxMsF7xiEb1a0wiIOIxl+zbwZ163pt7", - response.GetHeaderValue("x-amz-id-2")); - TEST_EQUAL("F2A8CCCA26B4B26D", response.GetHeaderValue("x-amz-request-id")); - TEST_EQUAL("Wed, 01 Mar 2006 12:00:00 GMT", response.GetHeaderValue("Date")); - TEST_EQUAL("Sun, 1 Jan 2006 12:00:00 GMT", response.GetHeaderValue("Last-Modified")); - TEST_EQUAL("\"828ef3fdfa96f00ad9f27c383fc9ac7f\"", response.GetHeaderValue("ETag")); - TEST_EQUAL("", response.GetContentType()); - TEST_EQUAL("AmazonS3", response.GetHeaderValue("Server")); - TEST_EQUAL(0, response.GetSize()); - - FileStream f1("testfiles/testrequests.pl"); - FileStream f2("testfiles/newfile"); - TEST_THAT(f1.CompareWith(f2)); - TEST_EQUAL(0, ::unlink("testfiles/newfile")); - } - - // Start the S3Simulator server - pid = StartDaemon(0, TEST_EXECUTABLE " s3server testfiles/s3simulator.conf", - "testfiles/s3simulator.pid"); - TEST_THAT_OR(pid > 0, return 1); - - { - SocketStream sock; - sock.Open(Socket::TypeINET, "localhost", 1080); - - HTTPRequest request(HTTPRequest::Method_GET, "/nonexist"); - request.SetHostName("quotes.s3.amazonaws.com"); - request.AddHeader("Date", "Wed, 01 Mar 2006 12:00:00 GMT"); - request.AddHeader("Authorization", "AWS 0PN5J17HBGZHT7JJ3X82:0cSX/YPdtXua1aFFpYmH1tc0ajA="); - request.SetClientKeepAliveRequested(true); - request.Send(sock, SHORT_TIMEOUT); - - HTTPResponse response; - response.Receive(sock, SHORT_TIMEOUT); - std::string value; - TEST_EQUAL(404, response.GetResponseCode()); - } - - #ifndef WIN32 // much harder to make files inaccessible on WIN32 - // Make file inaccessible, should cause server to return a 403 error, - // unless of course the test is run as root :) - { - SocketStream sock; - sock.Open(Socket::TypeINET, "localhost", 1080); - - TEST_THAT(chmod("testfiles/testrequests.pl", 0) == 0); - HTTPRequest request(HTTPRequest::Method_GET, - "/testrequests.pl"); - request.SetHostName("quotes.s3.amazonaws.com"); - request.AddHeader("Date", "Wed, 01 Mar 2006 12:00:00 GMT"); - request.AddHeader("Authorization", "AWS 0PN5J17HBGZHT7JJ3X82:qc1e8u8TVl2BpIxwZwsursIb8U8="); - request.SetClientKeepAliveRequested(true); - request.Send(sock, SHORT_TIMEOUT); - - HTTPResponse response; - response.Receive(sock, SHORT_TIMEOUT); - std::string value; - TEST_EQUAL(403, response.GetResponseCode()); - TEST_THAT(chmod("testfiles/testrequests.pl", 0755) == 0); - } - #endif - +int test(int argc, const char *argv[]) +{ + if(argc >= 2 && ::strcmp(argv[1], "server") == 0) { - SocketStream sock; - sock.Open(Socket::TypeINET, "localhost", 1080); - - HTTPRequest request(HTTPRequest::Method_GET, - "/testrequests.pl"); - request.SetHostName("quotes.s3.amazonaws.com"); - request.AddHeader("Date", "Wed, 01 Mar 2006 12:00:00 GMT"); - request.AddHeader("Authorization", "AWS 0PN5J17HBGZHT7JJ3X82:qc1e8u8TVl2BpIxwZwsursIb8U8="); - request.SetClientKeepAliveRequested(true); - request.Send(sock, SHORT_TIMEOUT); - - HTTPResponse response; - response.Receive(sock, SHORT_TIMEOUT); - std::string value; - TEST_EQUAL(200, response.GetResponseCode()); - TEST_EQUAL("qBmKRcEWBBhH6XAqsKU/eg24V3jf/kWKN9dJip1L/FpbYr9FDy7wWFurfdQOEMcY", response.GetHeaderValue("x-amz-id-2")); - TEST_EQUAL("F2A8CCCA26B4B26D", response.GetHeaderValue("x-amz-request-id")); - TEST_EQUAL("Wed, 01 Mar 2006 12:00:00 GMT", response.GetHeaderValue("Date")); - TEST_EQUAL("Sun, 1 Jan 2006 12:00:00 GMT", response.GetHeaderValue("Last-Modified")); - TEST_EQUAL("\"828ef3fdfa96f00ad9f27c383fc9ac7f\"", response.GetHeaderValue("ETag")); - TEST_EQUAL("text/plain", response.GetContentType()); - TEST_EQUAL("AmazonS3", response.GetHeaderValue("Server")); - - FileStream file("testfiles/testrequests.pl"); - TEST_THAT(file.CompareWith(response)); + // Run a server + TestWebServer server; + return server.Main("doesnotexist", argc - 1, argv + 1); } + if(argc >= 2 && ::strcmp(argv[1], "s3server") == 0) { - SocketStream sock; - sock.Open(Socket::TypeINET, "localhost", 1080); - - HTTPRequest request(HTTPRequest::Method_PUT, - "/newfile"); - request.SetHostName("quotes.s3.amazonaws.com"); - request.AddHeader("Date", "Wed, 01 Mar 2006 12:00:00 GMT"); - request.AddHeader("Authorization", "AWS 0PN5J17HBGZHT7JJ3X82:kfY1m6V3zTufRy2kj92FpQGKz4M="); - request.AddHeader("Content-Type", "text/plain"); - FileStream fs("testfiles/testrequests.pl"); - HTTPResponse response; - request.SendWithStream(sock, SHORT_TIMEOUT, &fs, response); - std::string value; - TEST_EQUAL(200, response.GetResponseCode()); - TEST_EQUAL("LriYPLdmOdAiIfgSm/F1YsViT1LW94/xUQxMsF7xiEb1a0wiIOIxl+zbwZ163pt7", response.GetHeaderValue("x-amz-id-2")); - TEST_EQUAL("F2A8CCCA26B4B26D", response.GetHeaderValue("x-amz-request-id")); - TEST_EQUAL("Wed, 01 Mar 2006 12:00:00 GMT", response.GetHeaderValue("Date")); - TEST_EQUAL("Sun, 1 Jan 2006 12:00:00 GMT", response.GetHeaderValue("Last-Modified")); - TEST_EQUAL("\"828ef3fdfa96f00ad9f27c383fc9ac7f\"", response.GetHeaderValue("ETag")); - TEST_EQUAL("", response.GetContentType()); - TEST_EQUAL("AmazonS3", response.GetHeaderValue("Server")); - TEST_EQUAL(0, response.GetSize()); - - FileStream f1("testfiles/testrequests.pl"); - FileStream f2("testfiles/newfile"); - TEST_THAT(f1.CompareWith(f2)); + // Run a server + S3Simulator server; + return server.Main("doesnotexist", argc - 1, argv + 1); } + TEST_THAT(test_httpserver()); - // Kill it - TEST_THAT(StopDaemon(pid, "testfiles/s3simulator.pid", - "s3simulator.memleaks", true)); - - return 0; + return finish_test_suite(); } -