From f4efcb90808603bbfce5a149f5490bd6fceb880f Mon Sep 17 00:00:00 2001 From: madmaxoft Date: Fri, 27 Sep 2013 18:14:26 +0200 Subject: Rewritten HTTPServer to split into cHTTPConnection, cHTTPRequest and cHTTPResponse classes. --- source/HTTPServer/HTTPMessage.cpp | 285 ++++++++++++++++++++++++++++++++++++++ source/HTTPServer/HTTPMessage.h | 121 ++++++++++++++++ source/HTTPServer/HTTPServer.cpp | 267 +++++++++++++++++++++++++++++++++++ source/HTTPServer/HTTPServer.h | 135 ++++++++++++++++++ 4 files changed, 808 insertions(+) create mode 100644 source/HTTPServer/HTTPMessage.cpp create mode 100644 source/HTTPServer/HTTPMessage.h create mode 100644 source/HTTPServer/HTTPServer.cpp create mode 100644 source/HTTPServer/HTTPServer.h (limited to 'source/HTTPServer') diff --git a/source/HTTPServer/HTTPMessage.cpp b/source/HTTPServer/HTTPMessage.cpp new file mode 100644 index 000000000..b784cb941 --- /dev/null +++ b/source/HTTPServer/HTTPMessage.cpp @@ -0,0 +1,285 @@ + +// HTTPMessage.cpp + +// Declares the cHTTPMessage class representing the common ancestor for HTTP request and response classes + +#include "Globals.h" +#include "HTTPMessage.h" + + + + + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// cHTTPMessage: + +cHTTPMessage::cHTTPMessage(eKind a_Kind) : + m_Kind(a_Kind) +{ +} + + + + + +void cHTTPMessage::AddHeader(const AString & a_Key, const AString & a_Value) +{ + cNameValueMap::iterator itr = m_Headers.find(a_Key); + if (itr == m_Headers.end()) + { + m_Headers[a_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 (a_Key == "Content-Type") + { + m_ContentType = m_Headers["Content-Type"]; + } + else if (a_Key == "Content-Length") + { + m_ContentLength = atoi(m_Headers["Content-Length"].c_str()); + } +} + + + + + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// cHTTPRequest: + +cHTTPRequest::cHTTPRequest(void) : + super(mkRequest) +{ +} + + + + + +bool cHTTPRequest::ParseHeaders(const char * a_IncomingData, size_t a_IdxEnd) +{ + // The first line contains the method and the URL: + size_t Next = ParseRequestLine(a_IncomingData, a_IdxEnd); + if (Next == AString::npos) + { + return false; + } + + // The following lines contain headers: + AString Key; + const char * Data = a_IncomingData + Next; + size_t End = a_IdxEnd - Next; + while (End > 0) + { + Next = ParseHeaderField(Data, End, Key); + if (Next == AString::npos) + { + return false; + } + ASSERT(End >= Next); + Data += Next; + End -= Next; + } + + return HasReceivedContentLength(); +} + + + + +size_t cHTTPRequest::ParseRequestLine(const char * a_Data, size_t a_IdxEnd) +{ + // Ignore the initial CRLFs (HTTP spec's "should") + size_t LineStart = 0; + while ( + (LineStart < a_IdxEnd) && + ( + (a_Data[LineStart] == '\r') || + (a_Data[LineStart] == '\n') + ) + ) + { + LineStart++; + } + if (LineStart >= a_IdxEnd) + { + return AString::npos; + } + + size_t Last = LineStart; + int NumSpaces = 0; + for (size_t i = LineStart; i < a_IdxEnd; i++) + { + switch (a_Data[i]) + { + case ' ': + { + switch (NumSpaces) + { + case 0: + { + m_Method.assign(a_Data, Last, i - Last - 1); + break; + } + case 1: + { + m_URL.assign(a_Data, Last, i - Last - 1); + break; + } + default: + { + // Too many spaces in the request + return AString::npos; + } + } + Last = i + 1; + NumSpaces += 1; + break; + } + case '\n': + { + if ((i == 0) || (a_Data[i] != '\r') || (NumSpaces != 2) || (i < Last + 7)) + { + // LF too early, without a CR, without two preceeding spaces or too soon after the second space + return AString::npos; + } + // Check that there's HTTP/version at the end + if (strncmp(a_Data + Last, "HTTP/1.", 7) != 0) + { + return AString::npos; + } + return i; + } + } // switch (a_Data[i]) + } // for i - a_Data[] + return AString::npos; +} + + + + + +size_t cHTTPRequest::ParseHeaderField(const char * a_Data, size_t a_IdxEnd, AString & a_Key) +{ + if (*a_Data <= ' ') + { + size_t res = ParseHeaderFieldContinuation(a_Data + 1, a_IdxEnd - 1, a_Key); + return (res == AString::npos) ? res : (res + 1); + } + size_t ValueIdx = 0; + AString Key; + for (size_t i = 0; i < a_IdxEnd; i++) + { + switch (a_Data[i]) + { + case '\n': + { + if ((ValueIdx == 0) || (i < ValueIdx - 2) || (i == 0) || (a_Data[i - 1] != '\r')) + { + // Invalid header field - no colon or no CR before LF + return AString::npos; + } + AString Value(a_Data, ValueIdx + 1, i - ValueIdx - 2); + AddHeader(Key, Value); + a_Key = Key; + return i + 1; + } + case ':': + { + if (ValueIdx == 0) + { + Key.assign(a_Data, 0, i); + ValueIdx = i; + } + break; + } + case ' ': + case '\t': + { + if (ValueIdx == i - 1) + { + // Value has started in this char, but it is whitespace, so move the start one char further + ValueIdx = i; + } + } + } // switch (char) + } // for i - m_IncomingHeaderData[] + // No header found, return the end-of-data index: + return a_IdxEnd; +} + + + + + +size_t cHTTPRequest::ParseHeaderFieldContinuation(const char * a_Data, size_t a_IdxEnd, AString & a_Key) +{ + size_t Start = 0; + for (size_t i = 0; i < a_IdxEnd; i++) + { + if ((a_Data[i] > ' ') && (Start == 0)) + { + Start = i; + } + else if (a_Data[i] == '\n') + { + if ((i == 0) || (a_Data[i - 1] != '\r')) + { + // There wasn't a CR before this LF + return AString::npos; + } + AString Value(a_Data, 0, i - Start - 1); + AddHeader(a_Key, Value); + return i + 1; + } + } + // LF not found, how? We found it at the header end (CRLFCRLF) + ASSERT(!"LF not found, wtf?"); + return AString::npos; +} + + + + + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// cHTTPResponse: + +cHTTPResponse::cHTTPResponse(void) : + super(mkResponse) +{ +} + + + + + +void cHTTPResponse::AppendToData(AString & a_DataStream) const +{ + a_DataStream.append("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/source/HTTPServer/HTTPMessage.h b/source/HTTPServer/HTTPMessage.h new file mode 100644 index 000000000..a3c4f96d1 --- /dev/null +++ b/source/HTTPServer/HTTPMessage.h @@ -0,0 +1,121 @@ + +// HTTPMessage.h + +// Declares the cHTTPMessage class representing the common ancestor for HTTP request and response classes + + + + + +#pragma once + + + + + +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 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 +{ + typedef cHTTPMessage super; + +public: + cHTTPRequest(void); + + /// Parses the headers information from the received data in the specified string of incoming data. Returns true if successful. + bool ParseHeaders(const char * a_IncomingData, size_t a_idxEnd); + + /// Returns true if the request did contain a Content-Length header + bool HasReceivedContentLength(void) const { return (m_ContentLength >= 0); } + +protected: + /// Method of the request (GET / PUT / POST / ...) + AString m_Method; + + /// Full URL of the request + AString m_URL; + + /// Number of bytes that remain to read for the complete body of the message to be received + int m_BodyRemaining; + + /** Parses the RequestLine out of a_Data, up to index a_IdxEnd + Returns the index to the next line, or npos if invalid request + */ + size_t ParseRequestLine(const char * a_Data, size_t a_IdxEnd); + + /** Parses one header field out of a_Data, up to offset a_IdxEnd. + Returns the index to the next line (relative to a_Data), or npos if invalid request. + a_Key is set to the key that was parsed (used for multi-line headers) + */ + size_t ParseHeaderField(const char * a_Data, size_t a_IdxEnd, AString & a_Key); + + /** Parses one header field that is known to be a continuation of previous header. + Returns the index to the next line, or npos if invalid request. + */ + size_t ParseHeaderFieldContinuation(const char * a_Data, size_t a_IdxEnd, AString & a_Key); +} ; + + + + + +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/source/HTTPServer/HTTPServer.cpp b/source/HTTPServer/HTTPServer.cpp new file mode 100644 index 000000000..e68032bc2 --- /dev/null +++ b/source/HTTPServer/HTTPServer.cpp @@ -0,0 +1,267 @@ + +// 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" + + + + + +// Disable MSVC warnings: +#if defined(_MSC_VER) + #pragma warning(push) + #pragma warning(disable:4355) // 'this' : used in base member initializer list +#endif + + + + + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// cHTTPConnection: + +cHTTPConnection::cHTTPConnection(cHTTPServer & a_HTTPServer) : + m_HTTPServer(a_HTTPServer), + m_State(wcsRecvHeaders), + m_CurrentRequest(NULL) +{ +} + + + + +void cHTTPConnection::SendStatusAndReason(int a_StatusCode, const AString & a_Response) +{ + AppendPrintf(m_OutgoingData, "%d %s\r\n\r\n", a_StatusCode, a_Response.c_str()); +} + + + + + +void cHTTPConnection::Send(const cHTTPResponse & a_Response) +{ + ASSERT(m_State = wcsRecvIdle); + a_Response.AppendToData(m_OutgoingData); + m_State = wcsSendingResp; +} + + + + + +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); +} + + + + + +void cHTTPConnection::FinishResponse(void) +{ + ASSERT(m_State == wcsSendingResp); + m_OutgoingData.append("0\r\n"); + m_State = wcsRecvHeaders; +} + + + + + +void cHTTPConnection::DataReceived(const char * a_Data, int a_Size) +{ + switch (m_State) + { + case wcsRecvHeaders: + { + ASSERT(m_CurrentRequest == NULL); + + // Start searching 3 chars from the end of the already received data, if available: + size_t SearchStart = m_IncomingHeaderData.size(); + SearchStart = (SearchStart > 3) ? SearchStart - 3 : 0; + + m_IncomingHeaderData.append(a_Data, a_Size); + + // Parse the header, if it is complete: + size_t idxEnd = m_IncomingHeaderData.find("\r\n\r\n", SearchStart); + if (idxEnd == AString::npos) + { + return; + } + m_CurrentRequest = new cHTTPRequest; + if (!m_CurrentRequest->ParseHeaders(m_IncomingHeaderData.c_str(), idxEnd + 2)) + { + delete m_CurrentRequest; + m_CurrentRequest = NULL; + m_State = wcsInvalid; + m_HTTPServer.CloseConnection(*this); + return; + } + m_State = wcsRecvBody; + m_HTTPServer.NewRequest(*this, *m_CurrentRequest); + + // Process the rest of the incoming data into the request body: + if (m_IncomingHeaderData.size() > idxEnd + 4) + { + m_IncomingHeaderData.erase(0, idxEnd + 4); + DataReceived(m_IncomingHeaderData.c_str(), m_IncomingHeaderData.size()); + } + break; + } + + case wcsRecvBody: + { + ASSERT(m_CurrentRequest != NULL); + // TODO: Receive the body, and the next request (If HTTP/1.1 keepalive) + 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); + } +} + + + + + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// cHTTPServer: + +cHTTPServer::cHTTPServer(void) : + m_ListenThreadIPv4(*this, cSocket::IPv4, "WebServer IPv4"), + m_ListenThreadIPv6(*this, cSocket::IPv6, "WebServer IPv6"), + m_SocketThreads() +{ +} + + + + + +bool cHTTPServer::Initialize(cIniFile & a_IniFile) +{ + if (!a_IniFile.GetValueSetB("WebAdmin", "Enabled", false)) + { + // The WebAdmin is disabled + return true; + } + bool HasAnyPort; + HasAnyPort = m_ListenThreadIPv4.Initialize(a_IniFile.GetValueSet("WebAdmin", "Port", "8081")); + HasAnyPort = m_ListenThreadIPv6.Initialize(a_IniFile.GetValueSet("WebAdmin", "PortsIPv6", "8082, 3300")) || HasAnyPort; + if (!HasAnyPort) + { + LOG("WebAdmin is disabled"); + return false; + } + if (!m_ListenThreadIPv4.Start()) + { + return false; + } + if (!m_ListenThreadIPv6.Start()) + { + m_ListenThreadIPv4.Stop(); + return false; + } + return true; +} + + + + + +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; + } + } +} + + + + + +void cHTTPServer::NewRequest(cHTTPConnection & a_Connection, cHTTPRequest & a_Request) +{ + // TODO +} + + + + + +void cHTTPServer::RequestBody(cHTTPConnection & a_Connection, cHTTPRequest & a_Request) +{ + // TODO +} + + + + + +void cHTTPServer::RequestFinished(cHTTPConnection & a_Connection, cHTTPRequest & a_Request) +{ + // TODO + + // DEBUG: Send a debug response: + cHTTPResponse Resp; + Resp.SetContentType("text/plain"); + a_Connection.Send(Resp); + a_Connection.Send("Hello"); + a_Connection.FinishResponse(); +} + + + + diff --git a/source/HTTPServer/HTTPServer.h b/source/HTTPServer/HTTPServer.h new file mode 100644 index 000000000..9287a79e8 --- /dev/null +++ b/source/HTTPServer/HTTPServer.h @@ -0,0 +1,135 @@ + +// 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 cHTTPServer; +class cHTTPMessage; +class cHTTPRequest; +class cHTTPResponse; + + + + + +class cHTTPConnection : + public cSocketThreads::cCallback +{ +public: + + enum eState + { + wcsRecvHeaders, ///< Receiving request headers (m_CurrentRequest == 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); + + /// Sends HTTP status code together with a_Reason (used for HTTP errors) + void SendStatusAndReason(int a_StatusCode, const AString & a_Reason); + + /// 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()); } + + /// Finishes sending current response, gets ready for receiving another request (HTTP 1.1 keepalive) + void FinishResponse(void); + +protected: + typedef std::map 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; + + + /// Parses the header in m_IncomingData until the specified end mark + void ParseHeader(size_t a_IdxEnd); + + /// Sends the response status and headers. Transition from wrsRecvBody to wrsSendingResp. + void SendRespHeaders(void); + + // 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 cHTTPConnections; + + + + +class cHTTPServer : + public cListenThread::cCallback +{ +public: + cHTTPServer(void); + + bool Initialize(cIniFile & a_IniFile); + +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 + + // 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 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); + + /// 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); +} ; + + + + + -- cgit v1.2.3