diff options
author | worktycho <work.tycho@gmail.com> | 2013-12-09 18:51:12 +0100 |
---|---|---|
committer | worktycho <work.tycho@gmail.com> | 2013-12-09 18:51:12 +0100 |
commit | 843605d59ebc128be0a578dc6f45ef8c05da6e79 (patch) | |
tree | 3ffebc6ba27baf7a9e1d4bc51501ffeea9b14226 /src/HTTPServer | |
parent | merged makefile changes (diff) | |
parent | Fix Undefined behavior at Bindings/LuaWindow line 32 (diff) | |
download | cuberite-843605d59ebc128be0a578dc6f45ef8c05da6e79.tar cuberite-843605d59ebc128be0a578dc6f45ef8c05da6e79.tar.gz cuberite-843605d59ebc128be0a578dc6f45ef8c05da6e79.tar.bz2 cuberite-843605d59ebc128be0a578dc6f45ef8c05da6e79.tar.lz cuberite-843605d59ebc128be0a578dc6f45ef8c05da6e79.tar.xz cuberite-843605d59ebc128be0a578dc6f45ef8c05da6e79.tar.zst cuberite-843605d59ebc128be0a578dc6f45ef8c05da6e79.zip |
Diffstat (limited to 'src/HTTPServer')
-rw-r--r-- | src/HTTPServer/EnvelopeParser.cpp | 132 | ||||
-rw-r--r-- | src/HTTPServer/EnvelopeParser.h | 69 | ||||
-rw-r--r-- | src/HTTPServer/HTTPConnection.cpp | 247 | ||||
-rw-r--r-- | src/HTTPServer/HTTPConnection.h | 101 | ||||
-rw-r--r-- | src/HTTPServer/HTTPFormParser.cpp | 290 | ||||
-rw-r--r-- | src/HTTPServer/HTTPFormParser.h | 112 | ||||
-rw-r--r-- | src/HTTPServer/HTTPMessage.cpp | 279 | ||||
-rw-r--r-- | src/HTTPServer/HTTPMessage.h | 164 | ||||
-rw-r--r-- | src/HTTPServer/HTTPServer.cpp | 258 | ||||
-rw-r--r-- | src/HTTPServer/HTTPServer.h | 101 | ||||
-rw-r--r-- | src/HTTPServer/MultipartParser.cpp | 256 | ||||
-rw-r--r-- | src/HTTPServer/MultipartParser.h | 76 | ||||
-rw-r--r-- | src/HTTPServer/NameValueParser.cpp | 412 | ||||
-rw-r--r-- | src/HTTPServer/NameValueParser.h | 70 |
14 files changed, 2567 insertions, 0 deletions
diff --git a/src/HTTPServer/EnvelopeParser.cpp b/src/HTTPServer/EnvelopeParser.cpp new file mode 100644 index 000000000..8dbe05f14 --- /dev/null +++ b/src/HTTPServer/EnvelopeParser.cpp @@ -0,0 +1,132 @@ + +// EnvelopeParser.cpp + +// Implements the cEnvelopeParser class representing a parser for RFC-822 envelope headers, used both in HTTP and in MIME + +#include "Globals.h" +#include "EnvelopeParser.h" + + + + + +cEnvelopeParser::cEnvelopeParser(cCallbacks & a_Callbacks) : + m_Callbacks(a_Callbacks), + m_IsInHeaders(true) +{ +} + + + + + +int cEnvelopeParser::Parse(const char * a_Data, int a_Size) +{ + if (!m_IsInHeaders) + { + return 0; + } + + // Start searching 1 char from the end of the already received data, if available: + size_t SearchStart = m_IncomingData.size(); + SearchStart = (SearchStart > 1) ? SearchStart - 1 : 0; + + m_IncomingData.append(a_Data, a_Size); + + size_t idxCRLF = m_IncomingData.find("\r\n", SearchStart); + if (idxCRLF == AString::npos) + { + // Not a complete line yet, all input consumed: + return a_Size; + } + + // Parse as many lines as found: + size_t Last = 0; + do + { + if (idxCRLF == Last) + { + // This was the last line of the data. Finish whatever value has been cached and return: + NotifyLast(); + m_IsInHeaders = false; + return a_Size - (m_IncomingData.size() - idxCRLF) + 2; + } + if (!ParseLine(m_IncomingData.c_str() + Last, idxCRLF - Last)) + { + // An error has occurred + m_IsInHeaders = false; + return -1; + } + Last = idxCRLF + 2; + idxCRLF = m_IncomingData.find("\r\n", idxCRLF + 2); + } while (idxCRLF != AString::npos); + m_IncomingData.erase(0, Last); + + // Parsed all lines and still expecting more + return a_Size; +} + + + + + +void cEnvelopeParser::Reset(void) +{ + m_IsInHeaders = true; + m_IncomingData.clear(); + m_LastKey.clear(); + m_LastValue.clear(); +} + + + + + +void cEnvelopeParser::NotifyLast(void) +{ + if (!m_LastKey.empty()) + { + m_Callbacks.OnHeaderLine(m_LastKey, m_LastValue); + m_LastKey.clear(); + } + m_LastValue.clear(); +} + + + + + +bool cEnvelopeParser::ParseLine(const char * a_Data, size_t a_Size) +{ + ASSERT(a_Size > 0); + if (a_Data[0] <= ' ') + { + // This line is a continuation for the previous line + if (m_LastKey.empty()) + { + return false; + } + // Append, including the whitespace in a_Data[0] + m_LastValue.append(a_Data, a_Size); + return true; + } + + // This is a line with a new key: + NotifyLast(); + for (size_t i = 0; i < a_Size; i++) + { + if (a_Data[i] == ':') + { + m_LastKey.assign(a_Data, i); + m_LastValue.assign(a_Data + i + 2, a_Size - i - 2); + return true; + } + } // for i - a_Data[] + + // No colon was found, key-less header?? + return false; +} + + + + diff --git a/src/HTTPServer/EnvelopeParser.h b/src/HTTPServer/EnvelopeParser.h new file mode 100644 index 000000000..6430fbebf --- /dev/null +++ b/src/HTTPServer/EnvelopeParser.h @@ -0,0 +1,69 @@ + +// EnvelopeParser.h + +// Declares the cEnvelopeParser class representing a parser for RFC-822 envelope headers, used both in HTTP and in MIME + + + + + +#pragma once + + + + + +class cEnvelopeParser +{ +public: + class cCallbacks + { + public: + /// Called when a full header line is parsed + virtual void OnHeaderLine(const AString & a_Key, const AString & a_Value) = 0; + } ; + + + cEnvelopeParser(cCallbacks & a_Callbacks); + + /** Parses the incoming data. + Returns the number of bytes consumed from the input. The bytes not consumed are not part of the envelope header + */ + int Parse(const char * a_Data, int a_Size); + + /// Makes the parser forget everything parsed so far, so that it can be reused for parsing another datastream + void Reset(void); + + /// Returns true if more input is expected for the envelope header + bool IsInHeaders(void) const { return m_IsInHeaders; } + + /// Sets the IsInHeaders flag; used by cMultipartParser to simplify the parser initial conditions + void SetIsInHeaders(bool a_IsInHeaders) { m_IsInHeaders = a_IsInHeaders; } + +public: + /// Callbacks to call for the various events + cCallbacks & m_Callbacks; + + /// Set to true while the parser is still parsing the envelope headers. Once set to true, the parser will not consume any more data. + bool m_IsInHeaders; + + /// Buffer for the incoming data until it is parsed + AString m_IncomingData; + + /// Holds the last parsed key; used for line-wrapped values + AString m_LastKey; + + /// Holds the last parsed value; used for line-wrapped values + AString m_LastValue; + + + /// Notifies the callback of the key/value stored in m_LastKey/m_LastValue, then erases them + void NotifyLast(void); + + /// Parses one line of header data. Returns true if successful + bool ParseLine(const char * a_Data, size_t a_Size); +} ; + + + + diff --git a/src/HTTPServer/HTTPConnection.cpp b/src/HTTPServer/HTTPConnection.cpp new file mode 100644 index 000000000..68afdfc11 --- /dev/null +++ b/src/HTTPServer/HTTPConnection.cpp @@ -0,0 +1,247 @@ + +// HTTPConnection.cpp + +// Implements the cHTTPConnection class representing a single persistent connection in the HTTP server. + +#include "Globals.h" +#include "HTTPConnection.h" +#include "HTTPMessage.h" +#include "HTTPServer.h" + + + + + +cHTTPConnection::cHTTPConnection(cHTTPServer & a_HTTPServer) : + m_HTTPServer(a_HTTPServer), + m_State(wcsRecvHeaders), + m_CurrentRequest(NULL) +{ + // LOGD("HTTP: New connection at %p", this); +} + + + + + +cHTTPConnection::~cHTTPConnection() +{ + // LOGD("HTTP: Del connection at %p", this); +} + + + + + +void cHTTPConnection::SendStatusAndReason(int a_StatusCode, const AString & a_Response) +{ + AppendPrintf(m_OutgoingData, "%d %s\r\nContent-Length: 0\r\n\r\n", a_StatusCode, a_Response.c_str()); + m_HTTPServer.NotifyConnectionWrite(*this); + m_State = wcsRecvHeaders; +} + + + + + +void cHTTPConnection::SendNeedAuth(const AString & a_Realm) +{ + AppendPrintf(m_OutgoingData, "HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Basic realm=\"%s\"\r\nContent-Length: 0\r\n\r\n", a_Realm.c_str()); + m_HTTPServer.NotifyConnectionWrite(*this); + m_State = wcsRecvHeaders; +} + + + + + +void cHTTPConnection::Send(const cHTTPResponse & a_Response) +{ + ASSERT(m_State = wcsRecvIdle); + a_Response.AppendToData(m_OutgoingData); + m_State = wcsSendingResp; + m_HTTPServer.NotifyConnectionWrite(*this); +} + + + + + +void cHTTPConnection::Send(const void * a_Data, int a_Size) +{ + ASSERT(m_State == wcsSendingResp); + AppendPrintf(m_OutgoingData, "%x\r\n", a_Size); + m_OutgoingData.append((const char *)a_Data, a_Size); + m_OutgoingData.append("\r\n"); + m_HTTPServer.NotifyConnectionWrite(*this); +} + + + + + +void cHTTPConnection::FinishResponse(void) +{ + ASSERT(m_State == wcsSendingResp); + m_OutgoingData.append("0\r\n\r\n"); + m_State = wcsRecvHeaders; + m_HTTPServer.NotifyConnectionWrite(*this); +} + + + + + +void cHTTPConnection::AwaitNextRequest(void) +{ + switch (m_State) + { + case wcsRecvHeaders: + { + // Nothing has been received yet, or a special response was given (SendStatusAndReason() or SendNeedAuth() ) + break; + } + + case wcsRecvIdle: + { + // The client is waiting for a response, send an "Internal server error": + m_OutgoingData.append("HTTP/1.1 500 Internal Server Error\r\n\r\n"); + m_HTTPServer.NotifyConnectionWrite(*this); + m_State = wcsRecvHeaders; + break; + } + + case wcsSendingResp: + { + // The response headers have been sent, we need to terminate the response body: + m_OutgoingData.append("0\r\n\r\n"); + m_State = wcsRecvHeaders; + break; + } + + default: + { + ASSERT(!"Unhandled state recovery"); + break; + } + } +} + + + + + +void cHTTPConnection::Terminate(void) +{ + if (m_CurrentRequest != NULL) + { + m_HTTPServer.RequestFinished(*this, *m_CurrentRequest); + } + m_HTTPServer.CloseConnection(*this); +} + + + + + +void cHTTPConnection::DataReceived(const char * a_Data, int a_Size) +{ + switch (m_State) + { + case wcsRecvHeaders: + { + if (m_CurrentRequest == NULL) + { + m_CurrentRequest = new cHTTPRequest; + } + + int BytesConsumed = m_CurrentRequest->ParseHeaders(a_Data, a_Size); + if (BytesConsumed < 0) + { + delete m_CurrentRequest; + m_CurrentRequest = NULL; + m_State = wcsInvalid; + m_HTTPServer.CloseConnection(*this); + return; + } + if (m_CurrentRequest->IsInHeaders()) + { + // The request headers are not yet complete + return; + } + + // The request has finished parsing its headers successfully, notify of it: + m_State = wcsRecvBody; + m_HTTPServer.NewRequest(*this, *m_CurrentRequest); + m_CurrentRequestBodyRemaining = m_CurrentRequest->GetContentLength(); + if (m_CurrentRequestBodyRemaining < 0) + { + // The body length was not specified in the request, assume zero + m_CurrentRequestBodyRemaining = 0; + } + + // Process the rest of the incoming data into the request body: + if (a_Size > BytesConsumed) + { + DataReceived(a_Data + BytesConsumed, a_Size - BytesConsumed); + } + else + { + DataReceived("", 0); // If the request has zero body length, let it be processed right-away + } + break; + } + + case wcsRecvBody: + { + ASSERT(m_CurrentRequest != NULL); + if (m_CurrentRequestBodyRemaining > 0) + { + int BytesToConsume = std::min(m_CurrentRequestBodyRemaining, a_Size); + m_HTTPServer.RequestBody(*this, *m_CurrentRequest, a_Data, BytesToConsume); + m_CurrentRequestBodyRemaining -= BytesToConsume; + } + if (m_CurrentRequestBodyRemaining == 0) + { + m_State = wcsRecvIdle; + m_HTTPServer.RequestFinished(*this, *m_CurrentRequest); + delete m_CurrentRequest; + m_CurrentRequest = NULL; + } + break; + } + + default: + { + // TODO: Should we be receiving data in this state? + break; + } + } +} + + + + + +void cHTTPConnection::GetOutgoingData(AString & a_Data) +{ + std::swap(a_Data, m_OutgoingData); +} + + + + + +void cHTTPConnection::SocketClosed(void) +{ + if (m_CurrentRequest != NULL) + { + m_HTTPServer.RequestFinished(*this, *m_CurrentRequest); + } + m_HTTPServer.CloseConnection(*this); +} + + + + + diff --git a/src/HTTPServer/HTTPConnection.h b/src/HTTPServer/HTTPConnection.h new file mode 100644 index 000000000..14603bb70 --- /dev/null +++ b/src/HTTPServer/HTTPConnection.h @@ -0,0 +1,101 @@ + +// HTTPConnection.h + +// Declares the cHTTPConnection class representing a single persistent connection in the HTTP server. + + + + + +#pragma once + +#include "../OSSupport/SocketThreads.h" + + + + + +// fwd: +class cHTTPServer; +class cHTTPResponse; +class cHTTPRequest; + + + + + +class cHTTPConnection : + public cSocketThreads::cCallback +{ +public: + + enum eState + { + wcsRecvHeaders, ///< Receiving request headers (m_CurrentRequest is created if NULL) + wcsRecvBody, ///< Receiving request body (m_CurrentRequest is valid) + wcsRecvIdle, ///< Has received the entire body, waiting to send the response (m_CurrentRequest == NULL) + wcsSendingResp, ///< Sending response body (m_CurrentRequest == NULL) + wcsInvalid, ///< The request was malformed, the connection is closing + } ; + + cHTTPConnection(cHTTPServer & a_HTTPServer); + ~cHTTPConnection(); + + /// Sends HTTP status code together with a_Reason (used for HTTP errors) + void SendStatusAndReason(int a_StatusCode, const AString & a_Reason); + + /// Sends the "401 unauthorized" reply together with instructions on authorizing, using the specified realm + void SendNeedAuth(const AString & a_Realm); + + /// Sends the headers contained in a_Response + void Send(const cHTTPResponse & a_Response); + + /// Sends the data as the response (may be called multiple times) + void Send(const void * a_Data, int a_Size); + + /// Sends the data as the response (may be called multiple times) + void Send(const AString & a_Data) { Send(a_Data.data(), a_Data.size()); } + + /// Indicates that the current response is finished, gets ready for receiving another request (HTTP 1.1 keepalive) + void FinishResponse(void); + + /// Resets the connection for a new request. Depending on the state, this will send an "InternalServerError" status or a "ResponseEnd" + void AwaitNextRequest(void); + + /// Terminates the connection; finishes any request being currently processed + void Terminate(void); + +protected: + typedef std::map<AString, AString> cNameValueMap; + + /// The parent webserver that is to be notified of events on this connection + cHTTPServer & m_HTTPServer; + + /// All the incoming data until the entire request header is parsed + AString m_IncomingHeaderData; + + /// Status in which the request currently is + eState m_State; + + /// Data that is queued for sending, once the socket becomes writable + AString m_OutgoingData; + + /// The request being currently received (valid only between having parsed the headers and finishing receiving the body) + cHTTPRequest * m_CurrentRequest; + + /// Number of bytes that remain to read for the complete body of the message to be received. Valid only in wcsRecvBody + int m_CurrentRequestBodyRemaining; + + + // cSocketThreads::cCallback overrides: + virtual void DataReceived (const char * a_Data, int a_Size) override; // Data is received from the client + virtual void GetOutgoingData(AString & a_Data) override; // Data can be sent to client + virtual void SocketClosed (void) override; // The socket has been closed for any reason +} ; + +typedef std::vector<cHTTPConnection *> cHTTPConnections; + + + + + diff --git a/src/HTTPServer/HTTPFormParser.cpp b/src/HTTPServer/HTTPFormParser.cpp new file mode 100644 index 000000000..596db424e --- /dev/null +++ b/src/HTTPServer/HTTPFormParser.cpp @@ -0,0 +1,290 @@ + +// HTTPFormParser.cpp + +// Implements the cHTTPFormParser class representing a parser for forms sent over HTTP + +#include "Globals.h" +#include "HTTPFormParser.h" +#include "HTTPMessage.h" +#include "MultipartParser.h" +#include "NameValueParser.h" + + + + + +cHTTPFormParser::cHTTPFormParser(cHTTPRequest & a_Request, cCallbacks & a_Callbacks) : + m_Callbacks(a_Callbacks), + m_IsValid(true) +{ + if (a_Request.GetMethod() == "GET") + { + m_Kind = fpkURL; + + // Directly parse the URL in the request: + const AString & URL = a_Request.GetURL(); + size_t idxQM = URL.find('?'); + if (idxQM != AString::npos) + { + Parse(URL.c_str() + idxQM + 1, URL.size() - idxQM - 1); + } + return; + } + if ((a_Request.GetMethod() == "POST") || (a_Request.GetMethod() == "PUT")) + { + if (strncmp(a_Request.GetContentType().c_str(), "application/x-www-form-urlencoded", 33) == 0) + { + m_Kind = fpkFormUrlEncoded; + return; + } + if (strncmp(a_Request.GetContentType().c_str(), "multipart/form-data", 19) == 0) + { + m_Kind = fpkMultipart; + BeginMultipart(a_Request); + return; + } + } + // Invalid method / content type combination, this is not a HTTP form + m_IsValid = false; +} + + + + + +cHTTPFormParser::cHTTPFormParser(eKind a_Kind, const char * a_Data, int a_Size, cCallbacks & a_Callbacks) : + m_Callbacks(a_Callbacks), + m_Kind(a_Kind), + m_IsValid(true) +{ + Parse(a_Data, a_Size); +} + + + + + +void cHTTPFormParser::Parse(const char * a_Data, int a_Size) +{ + if (!m_IsValid) + { + return; + } + + switch (m_Kind) + { + case fpkURL: + case fpkFormUrlEncoded: + { + // This format is used for smaller forms (not file uploads), so we can delay parsing it until Finish() + m_IncomingData.append(a_Data, a_Size); + break; + } + case fpkMultipart: + { + ASSERT(m_MultipartParser.get() != NULL); + m_MultipartParser->Parse(a_Data, a_Size); + break; + } + default: + { + ASSERT(!"Unhandled form kind"); + break; + } + } +} + + + + + +bool cHTTPFormParser::Finish(void) +{ + switch (m_Kind) + { + case fpkURL: + case fpkFormUrlEncoded: + { + // m_IncomingData has all the form data, parse it now: + ParseFormUrlEncoded(); + break; + } + } + return (m_IsValid && m_IncomingData.empty()); +} + + + + + +bool cHTTPFormParser::HasFormData(const cHTTPRequest & a_Request) +{ + const AString & ContentType = a_Request.GetContentType(); + return ( + (ContentType == "application/x-www-form-urlencoded") || + (strncmp(ContentType.c_str(), "multipart/form-data", 19) == 0) || + ( + (a_Request.GetMethod() == "GET") && + (a_Request.GetURL().find('?') != AString::npos) + ) + ); + return false; +} + + + + + +void cHTTPFormParser::BeginMultipart(const cHTTPRequest & a_Request) +{ + ASSERT(m_MultipartParser.get() == NULL); + m_MultipartParser.reset(new cMultipartParser(a_Request.GetContentType(), *this)); +} + + + + + +void cHTTPFormParser::ParseFormUrlEncoded(void) +{ + // Parse m_IncomingData for all the variables; no more data is incoming, since this is called from Finish() + // This may not be the most performant version, but we don't care, the form data is small enough and we're not a full-fledged web server anyway + AStringVector Lines = StringSplit(m_IncomingData, "&"); + for (AStringVector::iterator itr = Lines.begin(), end = Lines.end(); itr != end; ++itr) + { + AStringVector Components = StringSplit(*itr, "="); + switch (Components.size()) + { + default: + { + // Neither name nor value, or too many "="s, mark this as invalid form: + m_IsValid = false; + return; + } + case 1: + { + // Only name present + (*this)[URLDecode(ReplaceAllCharOccurrences(Components[0], '+', ' '))] = ""; + break; + } + case 2: + { + // name=value format: + (*this)[URLDecode(ReplaceAllCharOccurrences(Components[0], '+', ' '))] = URLDecode(ReplaceAllCharOccurrences(Components[1], '+', ' ')); + break; + } + } + } // for itr - Lines[] + m_IncomingData.clear(); +} + + + + + +void cHTTPFormParser::OnPartStart(void) +{ + m_CurrentPartFileName.clear(); + m_CurrentPartName.clear(); + m_IsCurrentPartFile = false; + m_FileHasBeenAnnounced = false; +} + + + + + +void cHTTPFormParser::OnPartHeader(const AString & a_Key, const AString & a_Value) +{ + if (NoCaseCompare(a_Key, "Content-Disposition") == 0) + { + size_t len = a_Value.size(); + size_t ParamsStart = AString::npos; + for (size_t i = 0; i < len; ++i) + { + if (a_Value[i] > ' ') + { + if (strncmp(a_Value.c_str() + i, "form-data", 9) != 0) + { + // Content disposition is not "form-data", mark the whole form invalid + m_IsValid = false; + return; + } + ParamsStart = a_Value.find(';', i + 9); + break; + } + } + if (ParamsStart == AString::npos) + { + // There is data missing in the Content-Disposition field, mark the whole form invalid: + m_IsValid = false; + return; + } + + // Parse the field name and optional filename from this header: + cNameValueParser Parser(a_Value.data() + ParamsStart, a_Value.size() - ParamsStart); + Parser.Finish(); + m_CurrentPartName = Parser["name"]; + if (!Parser.IsValid() || m_CurrentPartName.empty()) + { + // The required parameter "name" is missing, mark the whole form invalid: + m_IsValid = false; + return; + } + m_CurrentPartFileName = Parser["filename"]; + } +} + + + + + +void cHTTPFormParser::OnPartData(const char * a_Data, int a_Size) +{ + if (m_CurrentPartName.empty()) + { + // Prologue, epilogue or invalid part + return; + } + if (m_CurrentPartFileName.empty()) + { + // This is a variable, store it in the map + iterator itr = find(m_CurrentPartName); + if (itr == end()) + { + (*this)[m_CurrentPartName] = AString(a_Data, a_Size); + } + else + { + itr->second.append(a_Data, a_Size); + } + } + else + { + // This is a file, pass it on through the callbacks + if (!m_FileHasBeenAnnounced) + { + m_Callbacks.OnFileStart(*this, m_CurrentPartFileName); + m_FileHasBeenAnnounced = true; + } + m_Callbacks.OnFileData(*this, a_Data, a_Size); + } +} + + + + + +void cHTTPFormParser::OnPartEnd(void) +{ + if (m_FileHasBeenAnnounced) + { + m_Callbacks.OnFileEnd(*this); + } + m_CurrentPartName.clear(); + m_CurrentPartFileName.clear(); +} + + + + diff --git a/src/HTTPServer/HTTPFormParser.h b/src/HTTPServer/HTTPFormParser.h new file mode 100644 index 000000000..a554ca5a4 --- /dev/null +++ b/src/HTTPServer/HTTPFormParser.h @@ -0,0 +1,112 @@ + +// HTTPFormParser.h + +// Declares the cHTTPFormParser class representing a parser for forms sent over HTTP + + + + +#pragma once + +#include "MultipartParser.h" + + + + + +// fwd: +class cHTTPRequest; + + + + + +class cHTTPFormParser : + public std::map<AString, AString>, + public cMultipartParser::cCallbacks +{ +public: + enum eKind + { + fpkURL, ///< The form has been transmitted as parameters to a GET request + fpkFormUrlEncoded, ///< The form has been POSTed or PUT, with Content-Type of "application/x-www-form-urlencoded" + fpkMultipart, ///< The form has been POSTed or PUT, with Content-Type of "multipart/form-data" + } ; + + class cCallbacks + { + public: + /// Called when a new file part is encountered in the form data + virtual void OnFileStart(cHTTPFormParser & a_Parser, const AString & a_FileName) = 0; + + /// Called when more file data has come for the current file in the form data + virtual void OnFileData(cHTTPFormParser & a_Parser, const char * a_Data, int a_Size) = 0; + + /// Called when the current file part has ended in the form data + virtual void OnFileEnd(cHTTPFormParser & a_Parser) = 0; + } ; + + + /// Creates a parser that is tied to a request and notifies of various events using a callback mechanism + cHTTPFormParser(cHTTPRequest & a_Request, cCallbacks & a_Callbacks); + + /// Creates a parser with the specified content type that reads data from a string + cHTTPFormParser(eKind a_Kind, const char * a_Data, int a_Size, cCallbacks & a_Callbacks); + + /// Adds more data into the parser, as the request body is received + void Parse(const char * a_Data, int a_Size); + + /** Notifies that there's no more data incoming and the parser should finish its parsing. + Returns true if parsing successful + */ + bool Finish(void); + + /// Returns true if the headers suggest the request has form data parseable by this class + static bool HasFormData(const cHTTPRequest & a_Request); + +protected: + + /// The callbacks to call for incoming file data + cCallbacks & m_Callbacks; + + /// The kind of the parser (decided in the constructor, used in Parse() + eKind m_Kind; + + /// Buffer for the incoming data until it's parsed + AString m_IncomingData; + + /// True if the information received so far is a valid form; set to false on first problem. Further parsing is skipped when false. + bool m_IsValid; + + /// The parser for the multipart data, if used + std::auto_ptr<cMultipartParser> m_MultipartParser; + + /// Name of the currently parsed part in multipart data + AString m_CurrentPartName; + + /// True if the currently parsed part in multipart data is a file + bool m_IsCurrentPartFile; + + /// Filename of the current parsed part in multipart data (for file uploads) + AString m_CurrentPartFileName; + + /// Set to true after m_Callbacks.OnFileStart() has been called, reset to false on PartEnd + bool m_FileHasBeenAnnounced; + + + /// Sets up the object for parsing a fpkMultipart request + void BeginMultipart(const cHTTPRequest & a_Request); + + /// Parses m_IncomingData as form-urlencoded data (fpkURL or fpkFormUrlEncoded kinds) + void ParseFormUrlEncoded(void); + + // cMultipartParser::cCallbacks overrides: + virtual void OnPartStart (void) override; + virtual void OnPartHeader(const AString & a_Key, const AString & a_Value) override; + virtual void OnPartData (const char * a_Data, int a_Size) override; + virtual void OnPartEnd (void) override; +} ; + + + + diff --git a/src/HTTPServer/HTTPMessage.cpp b/src/HTTPServer/HTTPMessage.cpp new file mode 100644 index 000000000..ab23866e6 --- /dev/null +++ b/src/HTTPServer/HTTPMessage.cpp @@ -0,0 +1,279 @@ + +// HTTPMessage.cpp + +// Declares the cHTTPMessage class representing the common ancestor for HTTP request and response classes + +#include "Globals.h" +#include "HTTPMessage.h" + + + + + +// Disable MSVC warnings: +#if defined(_MSC_VER) + #pragma warning(push) + #pragma warning(disable:4355) // 'this' : used in base member initializer list +#endif + + + + + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// cHTTPMessage: + +cHTTPMessage::cHTTPMessage(eKind a_Kind) : + m_Kind(a_Kind), + m_ContentLength(-1) +{ +} + + + + + +void cHTTPMessage::AddHeader(const AString & a_Key, const AString & a_Value) +{ + AString Key = a_Key; + StrToLower(Key); + cNameValueMap::iterator itr = m_Headers.find(Key); + if (itr == m_Headers.end()) + { + m_Headers[Key] = a_Value; + } + else + { + // The header-field key is specified multiple times, combine into comma-separated list (RFC 2616 @ 4.2) + itr->second.append(", "); + itr->second.append(a_Value); + } + + // Special processing for well-known headers: + if (Key == "content-type") + { + m_ContentType = m_Headers[Key]; + } + else if (Key == "content-length") + { + m_ContentLength = atoi(m_Headers[Key].c_str()); + } +} + + + + + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// cHTTPRequest: + +cHTTPRequest::cHTTPRequest(void) : + super(mkRequest), + m_EnvelopeParser(*this), + m_IsValid(true), + m_UserData(NULL), + m_HasAuth(false) +{ +} + + + + + +int cHTTPRequest::ParseHeaders(const char * a_Data, int a_Size) +{ + if (!m_IsValid) + { + return -1; + } + + if (m_Method.empty()) + { + // The first line hasn't been processed yet + int res = ParseRequestLine(a_Data, a_Size); + if ((res < 0) || (res == a_Size)) + { + return res; + } + int res2 = m_EnvelopeParser.Parse(a_Data + res, a_Size - res); + if (res2 < 0) + { + m_IsValid = false; + return res2; + } + return res2 + res; + } + + if (m_EnvelopeParser.IsInHeaders()) + { + int res = m_EnvelopeParser.Parse(a_Data, a_Size); + if (res < 0) + { + m_IsValid = false; + } + return res; + } + return 0; +} + + + + + +AString cHTTPRequest::GetBareURL(void) const +{ + size_t idxQM = m_URL.find('?'); + if (idxQM != AString::npos) + { + return m_URL.substr(0, idxQM); + } + else + { + return m_URL; + } +} + + + + + +int cHTTPRequest::ParseRequestLine(const char * a_Data, int a_Size) +{ + m_IncomingHeaderData.append(a_Data, a_Size); + size_t IdxEnd = m_IncomingHeaderData.size(); + + // Ignore the initial CRLFs (HTTP spec's "should") + size_t LineStart = 0; + while ( + (LineStart < IdxEnd) && + ( + (m_IncomingHeaderData[LineStart] == '\r') || + (m_IncomingHeaderData[LineStart] == '\n') + ) + ) + { + LineStart++; + } + if (LineStart >= IdxEnd) + { + m_IsValid = false; + return -1; + } + + int NumSpaces = 0; + size_t MethodEnd = 0; + size_t URLEnd = 0; + for (size_t i = LineStart; i < IdxEnd; i++) + { + switch (m_IncomingHeaderData[i]) + { + case ' ': + { + switch (NumSpaces) + { + case 0: + { + MethodEnd = i; + break; + } + case 1: + { + URLEnd = i; + break; + } + default: + { + // Too many spaces in the request + m_IsValid = false; + return -1; + } + } + NumSpaces += 1; + break; + } + case '\n': + { + if ((i == 0) || (m_IncomingHeaderData[i - 1] != '\r') || (NumSpaces != 2) || (i < URLEnd + 7)) + { + // LF too early, without a CR, without two preceeding spaces or too soon after the second space + m_IsValid = false; + return -1; + } + // Check that there's HTTP/version at the end + if (strncmp(a_Data + URLEnd + 1, "HTTP/1.", 7) != 0) + { + m_IsValid = false; + return -1; + } + m_Method = m_IncomingHeaderData.substr(LineStart, MethodEnd - LineStart); + m_URL = m_IncomingHeaderData.substr(MethodEnd + 1, URLEnd - MethodEnd - 1); + return i + 1; + } + } // switch (m_IncomingHeaderData[i]) + } // for i - m_IncomingHeaderData[] + + // CRLF hasn't been encountered yet, consider all data consumed + return a_Size; +} + + + + + +void cHTTPRequest::OnHeaderLine(const AString & a_Key, const AString & a_Value) +{ + if ( + (NoCaseCompare(a_Key, "Authorization") == 0) && + (strncmp(a_Value.c_str(), "Basic ", 6) == 0) + ) + { + AString UserPass = Base64Decode(a_Value.substr(6)); + size_t idxCol = UserPass.find(':'); + if (idxCol != AString::npos) + { + m_AuthUsername = UserPass.substr(0, idxCol); + m_AuthPassword = UserPass.substr(idxCol + 1); + m_HasAuth = true; + } + } + AddHeader(a_Key, a_Value); +} + + + + + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// cHTTPResponse: + +cHTTPResponse::cHTTPResponse(void) : + super(mkResponse) +{ +} + + + + + +void cHTTPResponse::AppendToData(AString & a_DataStream) const +{ + a_DataStream.append("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nContent-Type: "); + a_DataStream.append(m_ContentType); + a_DataStream.append("\r\n"); + for (cNameValueMap::const_iterator itr = m_Headers.begin(), end = m_Headers.end(); itr != end; ++itr) + { + if ((itr->first == "Content-Type") || (itr->first == "Content-Length")) + { + continue; + } + a_DataStream.append(itr->first); + a_DataStream.append(": "); + a_DataStream.append(itr->second); + a_DataStream.append("\r\n"); + } // for itr - m_Headers[] + a_DataStream.append("\r\n"); +} + + + + diff --git a/src/HTTPServer/HTTPMessage.h b/src/HTTPServer/HTTPMessage.h new file mode 100644 index 000000000..f5284c535 --- /dev/null +++ b/src/HTTPServer/HTTPMessage.h @@ -0,0 +1,164 @@ + +// HTTPMessage.h + +// Declares the cHTTPMessage class representing the common ancestor for HTTP request and response classes + + + + + +#pragma once + +#include "EnvelopeParser.h" + + + + + +class cHTTPMessage +{ +public: + enum + { + HTTP_OK = 200, + HTTP_BAD_REQUEST = 400, + } ; + + enum eKind + { + mkRequest, + mkResponse, + } ; + + cHTTPMessage(eKind a_Kind); + + /// Adds a header into the internal map of headers. Recognizes special headers: Content-Type and Content-Length + void AddHeader(const AString & a_Key, const AString & a_Value); + + void SetContentType (const AString & a_ContentType) { m_ContentType = a_ContentType; } + void SetContentLength(int a_ContentLength) { m_ContentLength = a_ContentLength; } + + const AString & GetContentType (void) const { return m_ContentType; } + int GetContentLength(void) const { return m_ContentLength; } + +protected: + typedef std::map<AString, AString> cNameValueMap; + + eKind m_Kind; + + cNameValueMap m_Headers; + + /// Type of the content; parsed by AddHeader(), set directly by SetContentLength() + AString m_ContentType; + + /// Length of the content that is to be received. -1 when the object is created, parsed by AddHeader() or set directly by SetContentLength() + int m_ContentLength; +} ; + + + + + +class cHTTPRequest : + public cHTTPMessage, + protected cEnvelopeParser::cCallbacks +{ + typedef cHTTPMessage super; + +public: + cHTTPRequest(void); + + /** Parses the request line and then headers from the received data. + Returns the number of bytes consumed or a negative number for error + */ + int ParseHeaders(const char * a_Data, int a_Size); + + /// Returns true if the request did contain a Content-Length header + bool HasReceivedContentLength(void) const { return (m_ContentLength >= 0); } + + /// Returns the method used in the request + const AString & GetMethod(void) const { return m_Method; } + + /// Returns the URL used in the request + const AString & GetURL(void) const { return m_URL; } + + /// Returns the URL used in the request, without any parameters + AString GetBareURL(void) const; + + /// Sets the UserData pointer that is stored within this request. The request doesn't touch this data (doesn't delete it)! + void SetUserData(void * a_UserData) { m_UserData = a_UserData; } + + /// Retrieves the UserData pointer that has been stored within this request. + void * GetUserData(void) const { return m_UserData; } + + /// Returns true if more data is expected for the request headers + bool IsInHeaders(void) const { return m_EnvelopeParser.IsInHeaders(); } + + /// Returns true if the request did present auth data that was understood by the parser + bool HasAuth(void) const { return m_HasAuth; } + + /// Returns the username that the request presented. Only valid if HasAuth() is true + const AString & GetAuthUsername(void) const { return m_AuthUsername; } + + /// Returns the password that the request presented. Only valid if HasAuth() is true + const AString & GetAuthPassword(void) const { return m_AuthPassword; } + +protected: + /// Parser for the envelope data + cEnvelopeParser m_EnvelopeParser; + + /// True if the data received so far is parsed successfully. When false, all further parsing is skipped + bool m_IsValid; + + /// Bufferred incoming data, while parsing for the request line + AString m_IncomingHeaderData; + + /// Method of the request (GET / PUT / POST / ...) + AString m_Method; + + /// Full URL of the request + AString m_URL; + + /// Data that the HTTPServer callbacks are allowed to store. + void * m_UserData; + + /// Set to true if the request contains auth data that was understood by the parser + bool m_HasAuth; + + /// The username used for auth + AString m_AuthUsername; + + /// The password used for auth + AString m_AuthPassword; + + + /** Parses the incoming data for the first line (RequestLine) + Returns the number of bytes consumed, or -1 for an error + */ + int ParseRequestLine(const char * a_Data, int a_Size); + + // cEnvelopeParser::cCallbacks overrides: + virtual void OnHeaderLine(const AString & a_Key, const AString & a_Value) override; +} ; + + + + + +class cHTTPResponse : + public cHTTPMessage +{ + typedef cHTTPMessage super; + +public: + cHTTPResponse(void); + + /** Appends the response to the specified datastream - response line and headers. + The body will be sent later directly through cConnection::Send() + */ + void AppendToData(AString & a_DataStream) const; +} ; + + + + diff --git a/src/HTTPServer/HTTPServer.cpp b/src/HTTPServer/HTTPServer.cpp new file mode 100644 index 000000000..f6f5b0f8b --- /dev/null +++ b/src/HTTPServer/HTTPServer.cpp @@ -0,0 +1,258 @@ + +// HTTPServer.cpp + +// Implements the cHTTPServer class representing a HTTP webserver that uses cListenThread and cSocketThreads for processing + +#include "Globals.h" +#include "HTTPServer.h" +#include "HTTPMessage.h" +#include "HTTPConnection.h" +#include "HTTPFormParser.h" + + + + + +// Disable MSVC warnings: +#if defined(_MSC_VER) + #pragma warning(push) + #pragma warning(disable:4355) // 'this' : used in base member initializer list +#endif + + + + + +class cDebugCallbacks : + public cHTTPServer::cCallbacks, + protected cHTTPFormParser::cCallbacks +{ + virtual void OnRequestBegun(cHTTPConnection & a_Connection, cHTTPRequest & a_Request) override + { + if (cHTTPFormParser::HasFormData(a_Request)) + { + a_Request.SetUserData(new cHTTPFormParser(a_Request, *this)); + } + } + + + virtual void OnRequestBody(cHTTPConnection & a_Connection, cHTTPRequest & a_Request, const char * a_Data, int a_Size) override + { + cHTTPFormParser * FormParser = (cHTTPFormParser *)(a_Request.GetUserData()); + if (FormParser != NULL) + { + FormParser->Parse(a_Data, a_Size); + } + } + + + virtual void OnRequestFinished(cHTTPConnection & a_Connection, cHTTPRequest & a_Request) override + { + cHTTPFormParser * FormParser = (cHTTPFormParser *)(a_Request.GetUserData()); + if (FormParser != NULL) + { + if (FormParser->Finish()) + { + cHTTPResponse Resp; + Resp.SetContentType("text/html"); + a_Connection.Send(Resp); + a_Connection.Send("<html><body><table border=1 cellspacing=0><tr><th>Name</th><th>Value</th></tr>\r\n"); + for (cHTTPFormParser::iterator itr = FormParser->begin(), end = FormParser->end(); itr != end; ++itr) + { + a_Connection.Send(Printf("<tr><td valign=\"top\"><pre>%s</pre></td><td valign=\"top\"><pre>%s</pre></td></tr>\r\n", itr->first.c_str(), itr->second.c_str())); + } // for itr - FormParser[] + a_Connection.Send("</table></body></html>"); + return; + } + + // Parsing failed: + cHTTPResponse Resp; + Resp.SetContentType("text/plain"); + a_Connection.Send(Resp); + a_Connection.Send("Form parsing failed"); + return; + } + + // Test the auth failure and success: + if (a_Request.GetURL() == "/auth") + { + if (!a_Request.HasAuth() || (a_Request.GetAuthUsername() != "a") || (a_Request.GetAuthPassword() != "b")) + { + a_Connection.SendNeedAuth("MCServer WebAdmin"); + return; + } + } + + cHTTPResponse Resp; + Resp.SetContentType("text/plain"); + a_Connection.Send(Resp); + a_Connection.Send("Hello, world"); + } + + + virtual void OnFileStart(cHTTPFormParser & a_Parser, const AString & a_FileName) override + { + // TODO + } + + + virtual void OnFileData(cHTTPFormParser & a_Parser, const char * a_Data, int a_Size) override + { + // TODO + } + + + virtual void OnFileEnd(cHTTPFormParser & a_Parser) override + { + // TODO + } + +} g_DebugCallbacks; + + + + + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// cHTTPServer: + +cHTTPServer::cHTTPServer(void) : + m_ListenThreadIPv4(*this, cSocket::IPv4, "WebServer IPv4"), + m_ListenThreadIPv6(*this, cSocket::IPv6, "WebServer IPv6"), + m_Callbacks(NULL) +{ +} + + + + + +cHTTPServer::~cHTTPServer() +{ + Stop(); +} + + + + + +bool cHTTPServer::Initialize(const AString & a_PortsIPv4, const AString & a_PortsIPv6) +{ + bool HasAnyPort; + HasAnyPort = m_ListenThreadIPv4.Initialize(a_PortsIPv4); + HasAnyPort = m_ListenThreadIPv6.Initialize(a_PortsIPv6) || HasAnyPort; + if (!HasAnyPort) + { + return false; + } + + return true; +} + + + + + +bool cHTTPServer::Start(cCallbacks & a_Callbacks) +{ + m_Callbacks = &a_Callbacks; + if (!m_ListenThreadIPv4.Start()) + { + return false; + } + if (!m_ListenThreadIPv6.Start()) + { + m_ListenThreadIPv4.Stop(); + return false; + } + return true; +} + + + + + +void cHTTPServer::Stop(void) +{ + m_ListenThreadIPv4.Stop(); + m_ListenThreadIPv6.Stop(); + + // Drop all current connections: + cCSLock Lock(m_CSConnections); + while (!m_Connections.empty()) + { + m_Connections.front()->Terminate(); + } // for itr - m_Connections[] +} + + + + + +void cHTTPServer::OnConnectionAccepted(cSocket & a_Socket) +{ + cHTTPConnection * Connection = new cHTTPConnection(*this); + m_SocketThreads.AddClient(a_Socket, Connection); + cCSLock Lock(m_CSConnections); + m_Connections.push_back(Connection); +} + + + + + +void cHTTPServer::CloseConnection(cHTTPConnection & a_Connection) +{ + m_SocketThreads.RemoveClient(&a_Connection); + cCSLock Lock(m_CSConnections); + for (cHTTPConnections::iterator itr = m_Connections.begin(), end = m_Connections.end(); itr != end; ++itr) + { + if (*itr == &a_Connection) + { + m_Connections.erase(itr); + break; + } + } + delete &a_Connection; +} + + + + + +void cHTTPServer::NotifyConnectionWrite(cHTTPConnection & a_Connection) +{ + m_SocketThreads.NotifyWrite(&a_Connection); +} + + + + + +void cHTTPServer::NewRequest(cHTTPConnection & a_Connection, cHTTPRequest & a_Request) +{ + m_Callbacks->OnRequestBegun(a_Connection, a_Request); +} + + + + + +void cHTTPServer::RequestBody(cHTTPConnection & a_Connection, cHTTPRequest & a_Request, const char * a_Data, int a_Size) +{ + m_Callbacks->OnRequestBody(a_Connection, a_Request, a_Data, a_Size); +} + + + + + +void cHTTPServer::RequestFinished(cHTTPConnection & a_Connection, cHTTPRequest & a_Request) +{ + m_Callbacks->OnRequestFinished(a_Connection, a_Request); + a_Connection.AwaitNextRequest(); +} + + + + diff --git a/src/HTTPServer/HTTPServer.h b/src/HTTPServer/HTTPServer.h new file mode 100644 index 000000000..24baf8c95 --- /dev/null +++ b/src/HTTPServer/HTTPServer.h @@ -0,0 +1,101 @@ + +// HTTPServer.h + +// Declares the cHTTPServer class representing a HTTP webserver that uses cListenThread and cSocketThreads for processing + + + + + +#pragma once + +#include "../OSSupport/ListenThread.h" +#include "../OSSupport/SocketThreads.h" +#include "inifile/iniFile.h" + + + + + +// fwd: +class cHTTPMessage; +class cHTTPRequest; +class cHTTPResponse; +class cHTTPConnection; + +typedef std::vector<cHTTPConnection *> cHTTPConnections; + + + + + + +class cHTTPServer : + public cListenThread::cCallback +{ +public: + class cCallbacks + { + public: + /** Called when a new request arrives over a connection and its headers have been parsed. + The request body needn't have arrived yet. + */ + virtual void OnRequestBegun(cHTTPConnection & a_Connection, cHTTPRequest & a_Request) = 0; + + /// Called when another part of request body has arrived. + virtual void OnRequestBody(cHTTPConnection & a_Connection, cHTTPRequest & a_Request, const char * a_Data, int a_Size) = 0; + + /// Called when the request body has been fully received in previous calls to OnRequestBody() + virtual void OnRequestFinished(cHTTPConnection & a_Connection, cHTTPRequest & a_Request) = 0; + } ; + + cHTTPServer(void); + ~cHTTPServer(); + + /// Initializes the server on the specified ports + bool Initialize(const AString & a_PortsIPv4, const AString & a_PortsIPv6); + + /// Starts the server and assigns the callbacks to use for incoming requests + bool Start(cCallbacks & a_Callbacks); + + /// Stops the server, drops all current connections + void Stop(void); + +protected: + friend class cHTTPConnection; + + cListenThread m_ListenThreadIPv4; + cListenThread m_ListenThreadIPv6; + + cSocketThreads m_SocketThreads; + + cCriticalSection m_CSConnections; + cHTTPConnections m_Connections; ///< All the connections that are currently being serviced + + /// The callbacks to call for various events + cCallbacks * m_Callbacks; + + + // cListenThread::cCallback overrides: + virtual void OnConnectionAccepted(cSocket & a_Socket) override; + + /// Called by cHTTPConnection to close the connection (presumably due to an error) + void CloseConnection(cHTTPConnection & a_Connection); + + /// Called by cHTTPConnection to notify SocketThreads that there's data to be sent for the connection + void NotifyConnectionWrite(cHTTPConnection & a_Connection); + + /// Called by cHTTPConnection when it finishes parsing the request header + void NewRequest(cHTTPConnection & a_Connection, cHTTPRequest & a_Request); + + /// Called by cHTTPConenction when it receives more data for the request body + void RequestBody(cHTTPConnection & a_Connection, cHTTPRequest & a_Request, const char * a_Data, int a_Size); + + /// Called by cHTTPConnection when it detects that the request has finished (all of its body has been received) + void RequestFinished(cHTTPConnection & a_Connection, cHTTPRequest & a_Request); +} ; + + + + + diff --git a/src/HTTPServer/MultipartParser.cpp b/src/HTTPServer/MultipartParser.cpp new file mode 100644 index 000000000..b49f6ec07 --- /dev/null +++ b/src/HTTPServer/MultipartParser.cpp @@ -0,0 +1,256 @@ + +// MultipartParser.cpp + +// Implements the cMultipartParser class that parses messages in "multipart/*" encoding into the separate parts + +#include "Globals.h" +#include "MultipartParser.h" +#include "NameValueParser.h" + + + + + +// Disable MSVC warnings: +#if defined(_MSC_VER) + #pragma warning(push) + #pragma warning(disable:4355) // 'this' : used in base member initializer list +#endif + + + + + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// self-test: + +#if 0 + +class cMultipartParserTest : + public cMultipartParser::cCallbacks +{ +public: + cMultipartParserTest(void) + { + cMultipartParser Parser("multipart/mixed; boundary=\"MyBoundaryString\"; foo=bar", *this); + const char Data[] = +"ThisIsIgnoredPrologue\r\n\ +--MyBoundaryString\r\n\ +\r\n\ +Body with confusing strings\r\n\ +--NotABoundary\r\n\ +--MyBoundaryStringWithPostfix\r\n\ +--\r\n\ +--MyBoundaryString\r\n\ +content-disposition: inline\r\n\ +\r\n\ +This is body\r\n\ +--MyBoundaryString\r\n\ +\r\n\ +Headerless body with trailing CRLF\r\n\ +\r\n\ +--MyBoundaryString--\r\n\ +ThisIsIgnoredEpilogue"; + printf("Multipart parsing test commencing.\n"); + Parser.Parse(Data, sizeof(Data) - 1); + // DEBUG: Check if the onscreen output corresponds with the data above + printf("Multipart parsing test finished\n"); + } + + virtual void OnPartStart(void) override + { + printf("Starting a new part\n"); + } + + + virtual void OnPartHeader(const AString & a_Key, const AString & a_Value) override + { + printf(" Hdr: \"%s\"=\"%s\"\n", a_Key.c_str(), a_Value.c_str()); + } + + + virtual void OnPartData(const char * a_Data, int a_Size) override + { + printf(" Data: %d bytes, \"%.*s\"\n", a_Size, a_Size, a_Data); + } + + + virtual void OnPartEnd(void) override + { + printf("Part end\n"); + } +} g_Test; + +#endif + + + + + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// cMultipartParser: + + +cMultipartParser::cMultipartParser(const AString & a_ContentType, cCallbacks & a_Callbacks) : + m_Callbacks(a_Callbacks), + m_IsValid(true), + m_EnvelopeParser(*this), + m_HasHadData(false) +{ + static AString s_Multipart = "multipart/"; + + // Check that the content type is multipart: + AString ContentType(a_ContentType); + if (strncmp(ContentType.c_str(), "multipart/", 10) != 0) + { + m_IsValid = false; + return; + } + size_t idxSC = ContentType.find(';', 10); + if (idxSC == AString::npos) + { + m_IsValid = false; + return; + } + + // Find the multipart boundary: + ContentType.erase(0, idxSC + 1); + cNameValueParser CTParser(ContentType.c_str(), ContentType.size()); + CTParser.Finish(); + if (!CTParser.IsValid()) + { + m_IsValid = false; + return; + } + m_Boundary = CTParser["boundary"]; + m_IsValid = !m_Boundary.empty(); + if (!m_IsValid) + { + return; + } + + // Set the envelope parser for parsing the body, so that our Parse() function parses the ignored prefix data as a body + m_EnvelopeParser.SetIsInHeaders(false); + + // Append an initial CRLF to the incoming data, so that a body starting with the boundary line will get caught + m_IncomingData.assign("\r\n"); + + /* + m_Boundary = AString("\r\n--") + m_Boundary + m_BoundaryEnd = m_Boundary + "--\r\n"; + m_Boundary = m_Boundary + "\r\n"; + */ +} + + + + + +void cMultipartParser::Parse(const char * a_Data, int a_Size) +{ + // Skip parsing if invalid + if (!m_IsValid) + { + return; + } + + // Append to buffer, then parse it: + m_IncomingData.append(a_Data, a_Size); + while (true) + { + if (m_EnvelopeParser.IsInHeaders()) + { + int BytesConsumed = m_EnvelopeParser.Parse(m_IncomingData.data(), m_IncomingData.size()); + if (BytesConsumed < 0) + { + m_IsValid = false; + return; + } + if ((BytesConsumed == a_Size) && m_EnvelopeParser.IsInHeaders()) + { + // All the incoming data has been consumed and still waiting for more + return; + } + m_IncomingData.erase(0, BytesConsumed); + } + + // Search for boundary / boundary end: + size_t idxBoundary = m_IncomingData.find("\r\n--"); + if (idxBoundary == AString::npos) + { + // Boundary string start not present, present as much data to the part callback as possible + if (m_IncomingData.size() > m_Boundary.size() + 8) + { + size_t BytesToReport = m_IncomingData.size() - m_Boundary.size() - 8; + m_Callbacks.OnPartData(m_IncomingData.data(), BytesToReport); + m_IncomingData.erase(0, BytesToReport); + } + return; + } + if (idxBoundary > 0) + { + m_Callbacks.OnPartData(m_IncomingData.data(), idxBoundary); + m_IncomingData.erase(0, idxBoundary); + } + idxBoundary = 4; + size_t LineEnd = m_IncomingData.find("\r\n", idxBoundary); + if (LineEnd == AString::npos) + { + // Not a complete line yet, present as much data to the part callback as possible + if (m_IncomingData.size() > m_Boundary.size() + 8) + { + size_t BytesToReport = m_IncomingData.size() - m_Boundary.size() - 8; + m_Callbacks.OnPartData(m_IncomingData.data(), BytesToReport); + m_IncomingData.erase(0, BytesToReport); + } + return; + } + if ( + (LineEnd - idxBoundary != m_Boundary.size()) && // Line length not equal to boundary + (LineEnd - idxBoundary != m_Boundary.size() + 2) // Line length not equal to boundary end + ) + { + // Got a line, but it's not a boundary, report it as data: + m_Callbacks.OnPartData(m_IncomingData.data(), LineEnd); + m_IncomingData.erase(0, LineEnd); + continue; + } + + if (strncmp(m_IncomingData.c_str() + idxBoundary, m_Boundary.c_str(), m_Boundary.size()) == 0) + { + // Boundary or BoundaryEnd found: + m_Callbacks.OnPartEnd(); + size_t idxSlash = idxBoundary + m_Boundary.size(); + if ((m_IncomingData[idxSlash] == '-') && (m_IncomingData[idxSlash + 1] == '-')) + { + // This was the last part + m_Callbacks.OnPartData(m_IncomingData.data() + idxSlash + 4, m_IncomingData.size() - idxSlash - 4); + m_IncomingData.clear(); + return; + } + m_Callbacks.OnPartStart(); + m_IncomingData.erase(0, LineEnd + 2); + + // Keep parsing for the headers that may have come with this data: + m_EnvelopeParser.Reset(); + continue; + } + + // It's a line, but not a boundary. It can be fully sent to the data receiver, since a boundary cannot cross lines + m_Callbacks.OnPartData(m_IncomingData.c_str(), LineEnd); + m_IncomingData.erase(0, LineEnd); + } // while (true) +} + + + + + +void cMultipartParser::OnHeaderLine(const AString & a_Key, const AString & a_Value) +{ + m_Callbacks.OnPartHeader(a_Key, a_Value); +} + + + + diff --git a/src/HTTPServer/MultipartParser.h b/src/HTTPServer/MultipartParser.h new file mode 100644 index 000000000..d853929ed --- /dev/null +++ b/src/HTTPServer/MultipartParser.h @@ -0,0 +1,76 @@ + +// MultipartParser.h + +// Declares the cMultipartParser class that parses messages in "multipart/*" encoding into the separate parts + + + + + +#pragma once + +#include "EnvelopeParser.h" + + + + + +class cMultipartParser : + protected cEnvelopeParser::cCallbacks +{ +public: + class cCallbacks + { + public: + /// Called when a new part starts + virtual void OnPartStart(void) = 0; + + /// Called when a complete header line is received for a part + virtual void OnPartHeader(const AString & a_Key, const AString & a_Value) = 0; + + /// Called when body for a part is received + virtual void OnPartData(const char * a_Data, int a_Size) = 0; + + /// Called when the current part ends + virtual void OnPartEnd(void) = 0; + } ; + + /// Creates the parser, expects to find the boundary in a_ContentType + cMultipartParser(const AString & a_ContentType, cCallbacks & a_Callbacks); + + /// Parses more incoming data + void Parse(const char * a_Data, int a_Size); + +protected: + /// The callbacks to call for various parsing events + cCallbacks & m_Callbacks; + + /// True if the data parsed so far is valid; if false, further parsing is skipped + bool m_IsValid; + + /// Parser for each part's envelope + cEnvelopeParser m_EnvelopeParser; + + /// Buffer for the incoming data until it is parsed + AString m_IncomingData; + + /// The boundary, excluding both the initial "--" and the terminating CRLF + AString m_Boundary; + + /// Set to true if some data for the current part has already been signalized to m_Callbacks. Used for proper CRLF inserting. + bool m_HasHadData; + + + /// Parse one line of incoming data. The CRLF has already been stripped from a_Data / a_Size + void ParseLine(const char * a_Data, int a_Size); + + /// Parse one line of incoming data in the headers section of a part. The CRLF has already been stripped from a_Data / a_Size + void ParseHeaderLine(const char * a_Data, int a_Size); + + // cEnvelopeParser overrides: + virtual void OnHeaderLine(const AString & a_Key, const AString & a_Value) override; +} ; + + + + diff --git a/src/HTTPServer/NameValueParser.cpp b/src/HTTPServer/NameValueParser.cpp new file mode 100644 index 000000000..a27f07d19 --- /dev/null +++ b/src/HTTPServer/NameValueParser.cpp @@ -0,0 +1,412 @@ + +// NameValueParser.cpp + +// Implements the cNameValueParser class that parses strings in the "name=value;name2=value2" format into a stringmap + +#include "Globals.h" +#include "NameValueParser.h" + + + + + + +// DEBUG: Self-test + +#if 0 + +class cNameValueParserTest +{ +public: + cNameValueParserTest(void) + { + const char Data[] = " Name1=Value1;Name2 = Value 2; Name3 =\"Value 3\"; Name4 =\'Value 4\'; Name5=\"Confusing; isn\'t it?\""; + + // Now try parsing char-by-char, to debug transitions across datachunk boundaries: + cNameValueParser Parser2; + for (int i = 0; i < sizeof(Data) - 1; i++) + { + Parser2.Parse(Data + i, 1); + } + Parser2.Finish(); + + // Parse as a single chunk of data: + cNameValueParser Parser(Data, sizeof(Data) - 1); + + // Use the debugger to inspect the Parser variable + + // Check that the two parsers have the same content: + for (cNameValueParser::const_iterator itr = Parser.begin(), end = Parser.end(); itr != end; ++itr) + { + ASSERT(Parser2[itr->first] == itr->second); + } // for itr - Parser[] + + // Try parsing in 2-char chunks: + cNameValueParser Parser3; + for (int i = 0; i < sizeof(Data) - 2; i += 2) + { + Parser3.Parse(Data + i, 2); + } + if ((sizeof(Data) % 2) == 0) // There are even number of chars, including the NUL, so the data has an odd length. Parse one more char + { + Parser3.Parse(Data + sizeof(Data) - 2, 1); + } + Parser3.Finish(); + + // Check that the third parser has the same content: + for (cNameValueParser::const_iterator itr = Parser.begin(), end = Parser.end(); itr != end; ++itr) + { + ASSERT(Parser3[itr->first] == itr->second); + } // for itr - Parser[] + + printf("cNameValueParserTest done"); + } +} g_Test; + +#endif + + + + + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// cNameValueParser: + +cNameValueParser::cNameValueParser(bool a_AllowsKeyOnly) : + m_State(psKeySpace), + m_AllowsKeyOnly(a_AllowsKeyOnly) +{ +} + + + + + +cNameValueParser::cNameValueParser(const char * a_Data, int a_Size, bool a_AllowsKeyOnly) : + m_State(psKeySpace), + m_AllowsKeyOnly(a_AllowsKeyOnly) +{ + Parse(a_Data, a_Size); +} + + + + + +void cNameValueParser::Parse(const char * a_Data, int a_Size) +{ + ASSERT(m_State != psFinished); // Calling Parse() after Finish() is wrong! + + if ((m_State == psInvalid) || (m_State == psFinished)) + { + return; + } + int Last = 0; + for (int i = 0; i < a_Size;) + { + switch (m_State) + { + case psKeySpace: + { + // Skip whitespace until a non-whitespace is found, then start the key: + while ((i < a_Size) && (a_Data[i] <= ' ')) + { + i++; + } + if ((i < a_Size) && (a_Data[i] > ' ')) + { + m_State = psKey; + Last = i; + } + break; + } + + case psKey: + { + // Read the key until whitespace or an equal sign: + while (i < a_Size) + { + if (a_Data[i] == '=') + { + m_CurrentKey.append(a_Data + Last, i - Last); + i++; + Last = i; + m_State = psEqual; + break; + } + else if (a_Data[i] <= ' ') + { + m_CurrentKey.append(a_Data + Last, i - Last); + i++; + Last = i; + m_State = psEqualSpace; + break; + } + else if (a_Data[i] == ';') + { + if (!m_AllowsKeyOnly) + { + m_State = psInvalid; + return; + } + m_CurrentKey.append(a_Data + Last, i - Last); + i++; + Last = i; + (*this)[m_CurrentKey] = ""; + m_CurrentKey.clear(); + m_State = psKeySpace; + break; + } + else if ((a_Data[i] == '\"') || (a_Data[i] == '\'')) + { + m_State = psInvalid; + return; + } + i++; + } // while (i < a_Size) + if (i == a_Size) + { + // Still the key, ran out of data to parse, store the part of the key parsed so far: + m_CurrentKey.append(a_Data + Last, a_Size - Last); + return; + } + break; + } + + case psEqualSpace: + { + // The space before the expected equal sign; the current key is already assigned + while (i < a_Size) + { + if (a_Data[i] == '=') + { + m_State = psEqual; + i++; + Last = i; + break; + } + else if (a_Data[i] == ';') + { + // Key-only + if (!m_AllowsKeyOnly) + { + m_State = psInvalid; + return; + } + i++; + Last = i; + (*this)[m_CurrentKey] = ""; + m_CurrentKey.clear(); + m_State = psKeySpace; + break; + } + else if (a_Data[i] > ' ') + { + m_State = psInvalid; + return; + } + i++; + } // while (i < a_Size) + break; + } // case psEqualSpace + + case psEqual: + { + // just parsed the equal-sign + while (i < a_Size) + { + if (a_Data[i] == ';') + { + if (!m_AllowsKeyOnly) + { + m_State = psInvalid; + return; + } + i++; + Last = i; + (*this)[m_CurrentKey] = ""; + m_CurrentKey.clear(); + m_State = psKeySpace; + break; + } + else if (a_Data[i] == '\"') + { + i++; + Last = i; + m_State = psValueInDQuotes; + break; + } + else if (a_Data[i] == '\'') + { + i++; + Last = i; + m_State = psValueInSQuotes; + break; + } + else + { + m_CurrentValue.push_back(a_Data[i]); + i++; + Last = i; + m_State = psValueRaw; + break; + } + i++; + } // while (i < a_Size) + break; + } // case psEqual + + case psValueInDQuotes: + { + while (i < a_Size) + { + if (a_Data[i] == '\"') + { + m_CurrentValue.append(a_Data + Last, i - Last); + (*this)[m_CurrentKey] = m_CurrentValue; + m_CurrentKey.clear(); + m_CurrentValue.clear(); + m_State = psAfterValue; + i++; + Last = i; + break; + } + i++; + } // while (i < a_Size) + if (i == a_Size) + { + m_CurrentValue.append(a_Data + Last, a_Size - Last); + } + break; + } // case psValueInDQuotes + + case psValueInSQuotes: + { + while (i < a_Size) + { + if (a_Data[i] == '\'') + { + m_CurrentValue.append(a_Data + Last, i - Last); + (*this)[m_CurrentKey] = m_CurrentValue; + m_CurrentKey.clear(); + m_CurrentValue.clear(); + m_State = psAfterValue; + i++; + Last = i; + break; + } + i++; + } // while (i < a_Size) + if (i == a_Size) + { + m_CurrentValue.append(a_Data + Last, a_Size - Last); + } + break; + } // case psValueInSQuotes + + case psValueRaw: + { + while (i < a_Size) + { + if (a_Data[i] == ';') + { + m_CurrentValue.append(a_Data + Last, i - Last); + (*this)[m_CurrentKey] = m_CurrentValue; + m_CurrentKey.clear(); + m_CurrentValue.clear(); + m_State = psKeySpace; + i++; + Last = i; + break; + } + i++; + } + if (i == a_Size) + { + m_CurrentValue.append(a_Data + Last, a_Size - Last); + } + break; + } // case psValueRaw + + case psAfterValue: + { + // Between the closing DQuote or SQuote and the terminating semicolon + while (i < a_Size) + { + if (a_Data[i] == ';') + { + m_State = psKeySpace; + i++; + Last = i; + break; + } + else if (a_Data[i] < ' ') + { + i++; + continue; + } + m_State = psInvalid; + return; + } // while (i < a_Size) + break; + } + } // switch (m_State) + } // for i - a_Data[] +} + + + + + +bool cNameValueParser::Finish(void) +{ + switch (m_State) + { + case psInvalid: + { + return false; + } + case psFinished: + { + return true; + } + case psKey: + case psEqualSpace: + case psEqual: + { + if ((m_AllowsKeyOnly) && !m_CurrentKey.empty()) + { + (*this)[m_CurrentKey] = ""; + m_State = psFinished; + return true; + } + m_State = psInvalid; + return false; + } + case psValueRaw: + { + (*this)[m_CurrentKey] = m_CurrentValue; + m_State = psFinished; + return true; + } + case psValueInDQuotes: + case psValueInSQuotes: + { + // Missing the terminating quotes, this is an error + m_State = psInvalid; + return false; + } + case psKeySpace: + case psAfterValue: + { + m_State = psFinished; + return true; + } + } + ASSERT(!"Unhandled parser state!"); + return false; +} + + + + diff --git a/src/HTTPServer/NameValueParser.h b/src/HTTPServer/NameValueParser.h new file mode 100644 index 000000000..07dc0b942 --- /dev/null +++ b/src/HTTPServer/NameValueParser.h @@ -0,0 +1,70 @@ + +// NameValueParser.h + +// Declares the cNameValueParser class that parses strings in the "name=value;name2=value2" format into a stringmap + + + + + +#pragma once + + + + + +class cNameValueParser : + public std::map<AString, AString> +{ +public: + /// Creates an empty parser + cNameValueParser(bool a_AllowsKeyOnly = true); + + /// Creates an empty parser, then parses the data given. Doesn't call Finish(), so more data can be parsed later + cNameValueParser(const char * a_Data, int a_Size, bool a_AllowsKeyOnly = true); + + /// Parses the data given + void Parse(const char * a_Data, int a_Size); + + /// Notifies the parser that no more data will be coming. Returns true if the parser state is valid + bool Finish(void); + + /// Returns true if the data parsed so far was valid + bool IsValid(void) const { return (m_State != psInvalid); } + + /// Returns true if the parser expects no more data + bool IsFinished(void) const { return ((m_State == psInvalid) || (m_State == psFinished)); } + +protected: + enum eState + { + psKeySpace, ///< Parsing the space in front of the next key + psKey, ///< Currently adding more chars to the key in m_CurrentKey + psEqualSpace, ///< Space after m_CurrentKey + psEqual, ///< Just parsed the = sign after a name + psValueInSQuotes, ///< Just parsed a Single-quote sign after the Equal sign + psValueInDQuotes, ///< Just parsed a Double-quote sign after the Equal sign + psValueRaw, ///< Just parsed a raw value without a quote + psAfterValue, ///< Just finished parsing the value, waiting for semicolon or data end + psInvalid, ///< The parser has encountered an invalid input; further parsing is skipped + psFinished, ///< The parser has already been instructed to finish and doesn't expect any more data + } ; + + /// The current state of the parser + eState m_State; + + /// If true, the parser will accept keys without an equal sign and the value + bool m_AllowsKeyOnly; + + /// Buffer for the current Key + AString m_CurrentKey; + + /// Buffer for the current Value; + AString m_CurrentValue; + + +} ; + + + + |