summaryrefslogtreecommitdiff
path: root/lib/httpserver/HTTPRequest.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'lib/httpserver/HTTPRequest.cpp')
-rw-r--r--lib/httpserver/HTTPRequest.cpp783
1 files changed, 783 insertions, 0 deletions
diff --git a/lib/httpserver/HTTPRequest.cpp b/lib/httpserver/HTTPRequest.cpp
new file mode 100644
index 00000000..e146a0a3
--- /dev/null
+++ b/lib/httpserver/HTTPRequest.cpp
@@ -0,0 +1,783 @@
+// --------------------------------------------------------------------------
+//
+// File
+// Name: HTTPRequest.cpp
+// Purpose: Request object for HTTP connections
+// Created: 26/3/04
+//
+// --------------------------------------------------------------------------
+
+#include "Box.h"
+
+#include <string.h>
+#include <strings.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#include "HTTPRequest.h"
+#include "HTTPResponse.h"
+#include "HTTPQueryDecoder.h"
+#include "autogen_HTTPException.h"
+#include "IOStream.h"
+#include "IOStreamGetLine.h"
+#include "Logging.h"
+
+#include "MemLeakFindOn.h"
+
+#define MAX_CONTENT_SIZE (128*1024)
+
+#define ENSURE_COOKIE_JAR_ALLOCATED \
+ if(mpCookies == 0) {mpCookies = new CookieJar_t;}
+
+
+
+// --------------------------------------------------------------------------
+//
+// Function
+// Name: HTTPRequest::HTTPRequest()
+// Purpose: Constructor
+// Created: 26/3/04
+//
+// --------------------------------------------------------------------------
+HTTPRequest::HTTPRequest()
+ : mMethod(Method_UNINITIALISED),
+ mHostPort(80), // default if not specified
+ mHTTPVersion(0),
+ mContentLength(-1),
+ mpCookies(0),
+ mClientKeepAliveRequested(false),
+ mExpectContinue(false),
+ mpStreamToReadFrom(NULL)
+{
+}
+
+
+// --------------------------------------------------------------------------
+//
+// 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),
+ mExpectContinue(false),
+ mpStreamToReadFrom(NULL)
+{
+}
+
+
+
+// --------------------------------------------------------------------------
+//
+// Function
+// Name: HTTPRequest::~HTTPRequest()
+// Purpose: Destructor
+// Created: 26/3/04
+//
+// --------------------------------------------------------------------------
+HTTPRequest::~HTTPRequest()
+{
+ // Clean up any cookies
+ if(mpCookies != 0)
+ {
+ delete mpCookies;
+ mpCookies = 0;
+ }
+}
+
+
+// --------------------------------------------------------------------------
+//
+// Function
+// Name: HTTPRequest::Receive(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.
+// Created: 26/3/04
+//
+// --------------------------------------------------------------------------
+bool HTTPRequest::Receive(IOStreamGetLine &rGetLine, int Timeout)
+{
+ // Check caller's logic
+ if(mMethod != Method_UNINITIALISED)
+ {
+ THROW_EXCEPTION(HTTPException, RequestAlreadyBeenRead)
+ }
+
+ // Read the first line, which is of a different format to the rest of the lines
+ std::string requestLine;
+ if(!rGetLine.GetLine(requestLine, false /* no preprocessing */, Timeout))
+ {
+ // Didn't get the request line, probably end of connection which had been kept alive
+ return false;
+ }
+ BOX_TRACE("Request line: " << requestLine);
+
+ // Check the method
+ size_t p = 0; // current position in string
+ p = requestLine.find(' '); // end of first word
+
+ if (p == std::string::npos)
+ {
+ // No terminating space, looks bad
+ p = requestLine.size();
+ }
+ else
+ {
+ mHttpVerb = requestLine.substr(0, p);
+ if (mHttpVerb == "GET")
+ {
+ mMethod = Method_GET;
+ }
+ else if (mHttpVerb == "HEAD")
+ {
+ mMethod = Method_HEAD;
+ }
+ else if (mHttpVerb == "POST")
+ {
+ mMethod = Method_POST;
+ }
+ else if (mHttpVerb == "PUT")
+ {
+ mMethod = Method_PUT;
+ }
+ else
+ {
+ mMethod = Method_UNKNOWN;
+ }
+ }
+
+ // Skip spaces to find URI
+ const char *requestLinePtr = requestLine.c_str();
+ while(requestLinePtr[p] != '\0' && requestLinePtr[p] == ' ')
+ {
+ ++p;
+ }
+
+ // Check there's a URI following...
+ if(requestLinePtr[p] == '\0')
+ {
+ // Didn't get the request line, probably end of connection which had been kept alive
+ return false;
+ }
+
+ // Read the URI, unescaping any %XX hex codes
+ while(requestLinePtr[p] != ' ' && requestLinePtr[p] != '\0')
+ {
+ // End of URI, on to query string?
+ if(requestLinePtr[p] == '?')
+ {
+ // Put the rest into the query string, without escaping anything
+ ++p;
+ while(requestLinePtr[p] != ' ' && requestLinePtr[p] != '\0')
+ {
+ mQueryString += requestLinePtr[p];
+ ++p;
+ }
+ break;
+ }
+ // Needs unescaping?
+ else if(requestLinePtr[p] == '+')
+ {
+ mRequestURI += ' ';
+ }
+ else if(requestLinePtr[p] == '%')
+ {
+ // Be tolerant about this... bad things are silently accepted,
+ // rather than throwing an error.
+ char code[4] = {0,0,0,0};
+ code[0] = requestLinePtr[++p];
+ if(code[0] != '\0')
+ {
+ code[1] = requestLinePtr[++p];
+ }
+
+ // Convert into a char code
+ long c = ::strtol(code, NULL, 16);
+
+ // Accept it?
+ if(c > 0 && c <= 255)
+ {
+ mRequestURI += (char)c;
+ }
+ }
+ else
+ {
+ // Simple copy of character
+ mRequestURI += requestLinePtr[p];
+ }
+
+ ++p;
+ }
+
+ // End of URL?
+ if(requestLinePtr[p] == '\0')
+ {
+ // Assume HTTP 0.9
+ mHTTPVersion = HTTPVersion_0_9;
+ }
+ else
+ {
+ // Skip any more spaces
+ while(requestLinePtr[p] != '\0' && requestLinePtr[p] == ' ')
+ {
+ ++p;
+ }
+
+ // Check to see if there's the right string next...
+ if(::strncmp(requestLinePtr + p, "HTTP/", 5) == 0)
+ {
+ // Find the version numbers
+ int major, minor;
+ if(::sscanf(requestLinePtr + p + 5, "%d.%d", &major, &minor) != 2)
+ {
+ THROW_EXCEPTION(HTTPException, BadRequest)
+ }
+
+ // Store version
+ mHTTPVersion = (major * HTTPVersion__MajorMultiplier) + minor;
+ }
+ else
+ {
+ // Not good -- wrong string found
+ THROW_EXCEPTION(HTTPException, BadRequest)
+ }
+ }
+
+ BOX_TRACE("HTTPRequest: method=" << mMethod << ", uri=" <<
+ mRequestURI << ", version=" << mHTTPVersion);
+
+ // If HTTP 1.1 or greater, assume keep-alive
+ if(mHTTPVersion >= HTTPVersion_1_1)
+ {
+ mClientKeepAliveRequested = true;
+ }
+
+ // Decode query string?
+ if((mMethod == Method_GET || mMethod == Method_HEAD) && !mQueryString.empty())
+ {
+ HTTPQueryDecoder decoder(mQuery);
+ decoder.DecodeChunk(mQueryString.c_str(), mQueryString.size());
+ decoder.Finish();
+ }
+
+ // Now parse the headers
+ ParseHeaders(rGetLine, Timeout);
+
+ std::string expected;
+ if (GetHeader("Expect", &expected))
+ {
+ if (expected == "100-continue")
+ {
+ mExpectContinue = true;
+ }
+ }
+
+ // Parse form data?
+ if(mMethod == Method_POST && mContentLength >= 0)
+ {
+ // Too long? Don't allow people to be nasty by sending lots of data
+ if(mContentLength > MAX_CONTENT_SIZE)
+ {
+ THROW_EXCEPTION(HTTPException, POSTContentTooLong)
+ }
+
+ // 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 > 0)
+ {
+ BOX_TRACE("Decoding " << fromBuffer << " bytes of "
+ "data from getline buffer");
+ decoder.DecodeChunk((const char *)rGetLine.GetBufferedData(), fromBuffer);
+ // And tell the getline object to ignore the data we just used
+ rGetLine.IgnoreBufferedData(fromBuffer);
+ }
+ // Then read any more data, as required
+ int bytesToGo = mContentLength - fromBuffer;
+ while(bytesToGo > 0)
+ {
+ char buf[4096];
+ int toRead = sizeof(buf);
+ if(toRead > bytesToGo) toRead = bytesToGo;
+ IOStream &rstream(rGetLine.GetUnderlyingStream());
+ int r = rstream.Read(buf, toRead, Timeout);
+ if(r == 0)
+ {
+ // Timeout, just error
+ THROW_EXCEPTION(HTTPException, RequestReadFailed)
+ }
+ decoder.DecodeChunk(buf, r);
+ bytesToGo -= r;
+ }
+ // Finish off
+ decoder.Finish();
+ }
+ else if (mContentLength > 0)
+ {
+ IOStream::pos_type bytesToCopy = rGetLine.GetSizeOfBufferedData();
+ if (bytesToCopy > mContentLength)
+ {
+ bytesToCopy = mContentLength;
+ }
+ Write(rGetLine.GetBufferedData(), bytesToCopy);
+ SetForReading();
+ mpStreamToReadFrom = &(rGetLine.GetUnderlyingStream());
+ }
+
+ return true;
+}
+
+void HTTPRequest::ReadContent(IOStream& rStreamToWriteTo)
+{
+ Seek(0, SeekType_Absolute);
+
+ CopyStreamTo(rStreamToWriteTo);
+ IOStream::pos_type bytesCopied = GetSize();
+
+ while (bytesCopied < mContentLength)
+ {
+ char buffer[1024];
+ IOStream::pos_type bytesToCopy = sizeof(buffer);
+ if (bytesToCopy > mContentLength - bytesCopied)
+ {
+ bytesToCopy = mContentLength - bytesCopied;
+ }
+ bytesToCopy = mpStreamToReadFrom->Read(buffer, bytesToCopy);
+ rStreamToWriteTo.Write(buffer, bytesToCopy);
+ bytesCopied += bytesToCopy;
+ }
+}
+
+// --------------------------------------------------------------------------
+//
+// Function
+// Name: HTTPRequest::Send(IOStream &, int)
+// Purpose: Write the request to an IOStream using HTTP.
+// Created: 03/01/09
+//
+// --------------------------------------------------------------------------
+bool HTTPRequest::Send(IOStream &rStream, int Timeout, bool ExpectContinue)
+{
+ 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;
+ case Method_PUT:
+ rStream.Write("PUT"); 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";
+ }
+
+ for (std::vector<Header>::iterator i = mExtraHeaders.begin();
+ i != mExtraHeaders.end(); i++)
+ {
+ oss << i->first << ": " << i->second << "\n";
+ }
+
+ if (ExpectContinue)
+ {
+ oss << "Expect: 100-continue\n";
+ }
+
+ rStream.Write(oss.str().c_str());
+ rStream.Write("\n");
+
+ return true;
+}
+
+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
+//
+// --------------------------------------------------------------------------
+void HTTPRequest::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("Host")-1
+ && ::strncasecmp(h, "Host", sizeof("Host")-1) == 0)
+ {
+ // 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(p == sizeof("Cookie")-1
+ && ::strncasecmp(h, "Cookie", sizeof("Cookie")-1) == 0)
+ {
+ // 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)
+ {
+ mClientKeepAliveRequested = false;
+ }
+ else if(::strcasecmp(v, "keep-alive") == 0)
+ {
+ mClientKeepAliveRequested = true;
+ }
+ // else don't understand, just assume default for protocol version
+ }
+ else
+ {
+ std::string name = header.substr(0, p);
+ mExtraHeaders.push_back(Header(name,
+ 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;
+ }
+ }
+}
+
+
+// --------------------------------------------------------------------------
+//
+// Function
+// Name: HTTPRequest::ParseCookies(const std::string &, int)
+// Purpose: Parse the cookie header
+// Created: 20/8/04
+//
+// --------------------------------------------------------------------------
+void HTTPRequest::ParseCookies(const std::string &rHeader, int DataStarts)
+{
+ const char *data = rHeader.c_str() + DataStarts;
+ const char *pos = data;
+ const char *itemStart = pos;
+ std::string name;
+
+ enum
+ {
+ s_NAME, s_VALUE, s_VALUE_QUOTED, s_FIND_NEXT_NAME
+ } state = s_NAME;
+
+ do
+ {
+ switch(state)
+ {
+ case s_NAME:
+ {
+ if(*pos == '=')
+ {
+ // Found the name. Store
+ name.assign(itemStart, pos - itemStart);
+ // Looking at values now
+ state = s_VALUE;
+ if((*(pos + 1)) == '"')
+ {
+ // Actually it's a quoted value, skip over that
+ ++pos;
+ state = s_VALUE_QUOTED;
+ }
+ // Record starting point for this item
+ itemStart = pos + 1;
+ }
+ }
+ break;
+
+ case s_VALUE:
+ {
+ if(*pos == ';' || *pos == ',' || *pos == '\0')
+ {
+ // Name ends
+ ENSURE_COOKIE_JAR_ALLOCATED
+ std::string value(itemStart, pos - itemStart);
+ (*mpCookies)[name] = value;
+ // And move to the waiting stage
+ state = s_FIND_NEXT_NAME;
+ }
+ }
+ break;
+
+ case s_VALUE_QUOTED:
+ {
+ if(*pos == '"')
+ {
+ // That'll do nicely, save it
+ ENSURE_COOKIE_JAR_ALLOCATED
+ std::string value(itemStart, pos - itemStart);
+ (*mpCookies)[name] = value;
+ // And move to the waiting stage
+ state = s_FIND_NEXT_NAME;
+ }
+ }
+ break;
+
+ case s_FIND_NEXT_NAME:
+ {
+ // Skip over terminators and white space to get to the next name
+ if(*pos != ';' && *pos != ',' && *pos != ' ' && *pos != '\t')
+ {
+ // Name starts here
+ itemStart = pos;
+ state = s_NAME;
+ }
+ }
+ break;
+
+ default:
+ // Ooops
+ THROW_EXCEPTION(HTTPException, Internal)
+ break;
+ }
+ }
+ while(*(pos++) != 0);
+}
+
+
+// --------------------------------------------------------------------------
+//
+// Function
+// Name: HTTPRequest::GetCookie(const char *, std::string &) const
+// Purpose: Fetch a cookie's value. If cookie not present, returns false
+// and string is unaltered.
+// Created: 20/8/04
+//
+// --------------------------------------------------------------------------
+bool HTTPRequest::GetCookie(const char *CookieName, std::string &rValueOut) const
+{
+ // Got any cookies?
+ if(mpCookies == 0)
+ {
+ return false;
+ }
+
+ // See if it's there
+ CookieJar_t::const_iterator v(mpCookies->find(std::string(CookieName)));
+ if(v != mpCookies->end())
+ {
+ // Return the value
+ rValueOut = v->second;
+ return true;
+ }
+
+ return false;
+}
+
+
+// --------------------------------------------------------------------------
+//
+// Function
+// Name: HTTPRequest::GetCookie(const char *)
+// Purpose: Return a string for the given cookie, or the null string if the
+// cookie has not been recieved.
+// Created: 22/8/04
+//
+// --------------------------------------------------------------------------
+const std::string &HTTPRequest::GetCookie(const char *CookieName) const
+{
+ static const std::string noCookie;
+
+ // Got any cookies?
+ if(mpCookies == 0)
+ {
+ return noCookie;
+ }
+
+ // See if it's there
+ CookieJar_t::const_iterator v(mpCookies->find(std::string(CookieName)));
+ if(v != mpCookies->end())
+ {
+ // Return the value
+ return v->second;
+ }
+
+ return noCookie;
+}
+
+
+