diff --git a/VC2008/MCServer.vcproj b/VC2008/MCServer.vcproj index 1e17cc722..6cc83b4db 100644 --- a/VC2008/MCServer.vcproj +++ b/VC2008/MCServer.vcproj @@ -2719,6 +2719,14 @@ + + + + @@ -2751,6 +2759,22 @@ RelativePath="..\source\HTTPServer\HTTPServer.h" > + + + + + + + + 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) + if (m_CurrentRequest == NULL) { - return; + m_CurrentRequest = new cHTTPRequest; } - m_CurrentRequest = new cHTTPRequest; - if (!m_CurrentRequest->ParseHeaders(m_IncomingHeaderData.c_str(), idxEnd + 2)) + + int BytesConsumed = m_CurrentRequest->ParseHeaders(a_Data, a_Size); + if (BytesConsumed < 0) { delete m_CurrentRequest; m_CurrentRequest = NULL; @@ -131,20 +122,29 @@ void cHTTPConnection::DataReceived(const char * a_Data, int a_Size) 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 (m_IncomingHeaderData.size() > idxEnd + 4) + if (a_Size > BytesConsumed) { - m_IncomingHeaderData.erase(0, idxEnd + 4); - DataReceived(m_IncomingHeaderData.c_str(), m_IncomingHeaderData.size()); - m_IncomingHeaderData.clear(); + DataReceived(a_Data + BytesConsumed, a_Size - BytesConsumed); } else { - m_IncomingHeaderData.clear(); DataReceived("", 0); // If the request has zero body length, let it be processed right-away } break; diff --git a/source/HTTPServer/HTTPConnection.h b/source/HTTPServer/HTTPConnection.h index 46c36a8a2..9e05d342b 100644 --- a/source/HTTPServer/HTTPConnection.h +++ b/source/HTTPServer/HTTPConnection.h @@ -31,7 +31,7 @@ public: enum eState { - wcsRecvHeaders, ///< Receiving request headers (m_CurrentRequest == NULL) + 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) diff --git a/source/HTTPServer/HTTPFormParser.cpp b/source/HTTPServer/HTTPFormParser.cpp index 631424391..85a789f7d 100644 --- a/source/HTTPServer/HTTPFormParser.cpp +++ b/source/HTTPServer/HTTPFormParser.cpp @@ -6,19 +6,15 @@ #include "Globals.h" #include "HTTPFormParser.h" #include "HTTPMessage.h" +#include "MultipartParser.h" +#include "NameValueParser.h" -AString cHTTPFormParser::m_FormURLEncoded("application/x-www-form-urlencoded"); -AString cHTTPFormParser::m_MultipartFormData("multipart/form-data"); - - - - - -cHTTPFormParser::cHTTPFormParser(cHTTPRequest & a_Request) : +cHTTPFormParser::cHTTPFormParser(cHTTPRequest & a_Request, cCallbacks & a_Callbacks) : + m_Callbacks(a_Callbacks), m_IsValid(true) { if (a_Request.GetMethod() == "GET") @@ -36,14 +32,15 @@ cHTTPFormParser::cHTTPFormParser(cHTTPRequest & a_Request) : } if ((a_Request.GetMethod() == "POST") || (a_Request.GetMethod() == "PUT")) { - if (a_Request.GetContentType() == m_FormURLEncoded) + if (a_Request.GetContentType() == "application/x-www-form-urlencoded") { m_Kind = fpkFormUrlEncoded; return; } - if (a_Request.GetContentType().substr(0, m_MultipartFormData.length()) == m_MultipartFormData) + if (strncmp(a_Request.GetContentType().c_str(), "multipart/form-data", 19) == 0) { m_Kind = fpkMultipart; + BeginMultipart(a_Request); return; } } @@ -56,18 +53,24 @@ cHTTPFormParser::cHTTPFormParser(cHTTPRequest & a_Request) : void cHTTPFormParser::Parse(const char * a_Data, int a_Size) { - m_IncomingData.append(a_Data, 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: { - ParseMultipart(); + ASSERT(m_MultipartParser.get() != NULL); + m_MultipartParser->Parse(a_Data, a_Size); break; } default: @@ -105,8 +108,8 @@ bool cHTTPFormParser::HasFormData(const cHTTPRequest & a_Request) { const AString & ContentType = a_Request.GetContentType(); return ( - (ContentType == m_FormURLEncoded) || - (ContentType.substr(0, m_MultipartFormData.length()) == m_MultipartFormData) || + (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) @@ -119,6 +122,16 @@ bool cHTTPFormParser::HasFormData(const cHTTPRequest & a_Request) +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() @@ -156,9 +169,107 @@ void cHTTPFormParser::ParseFormUrlEncoded(void) -void cHTTPFormParser::ParseMultipart(void) +void cHTTPFormParser::OnPartStart(void) { - // TODO + 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/source/HTTPServer/HTTPFormParser.h b/source/HTTPServer/HTTPFormParser.h index 01446e865..b92ef9d3c 100644 --- a/source/HTTPServer/HTTPFormParser.h +++ b/source/HTTPServer/HTTPFormParser.h @@ -8,6 +8,8 @@ #pragma once +#include "MultipartParser.h" + @@ -20,10 +22,25 @@ class cHTTPRequest; class cHTTPFormParser : - public std::map + public std::map, + public cMultipartParser::cCallbacks { public: - cHTTPFormParser(cHTTPRequest & a_Request); + 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; + } ; + + + cHTTPFormParser(cHTTPRequest & a_Request, cCallbacks & a_Callbacks); /// Adds more data into the parser, as the request body is received void Parse(const char * a_Data, int a_Size); @@ -41,26 +58,48 @@ protected: { 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/*". Currently unsupported + fpkMultipart, ///< The form has been POSTed or PUT, with Content-Type of "multipart/form-data" }; + + /// 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; - /// Simple static objects to hold the various strings for comparison with request's content-type - static AString m_FormURLEncoded; - static AString m_MultipartFormData; - + /// The parser for the multipart data, if used + std::auto_ptr 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); - /// Parses m_IncomingData as multipart data (fpkMultipart kind) - void ParseMultipart(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/source/HTTPServer/HTTPMessage.cpp b/source/HTTPServer/HTTPMessage.cpp index 72c603295..ed5c87e84 100644 --- a/source/HTTPServer/HTTPMessage.cpp +++ b/source/HTTPServer/HTTPMessage.cpp @@ -10,11 +10,22 @@ +// 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_Kind(a_Kind), + m_ContentLength(-1) { } @@ -24,10 +35,12 @@ cHTTPMessage::cHTTPMessage(eKind a_Kind) : void cHTTPMessage::AddHeader(const AString & a_Key, const AString & a_Value) { - cNameValueMap::iterator itr = m_Headers.find(a_Key); + AString Key = a_Key; + StrToLower(Key); + cNameValueMap::iterator itr = m_Headers.find(Key); if (itr == m_Headers.end()) { - m_Headers[a_Key] = a_Value; + m_Headers[Key] = a_Value; } else { @@ -37,13 +50,13 @@ void cHTTPMessage::AddHeader(const AString & a_Key, const AString & a_Value) } // Special processing for well-known headers: - if (a_Key == "Content-Type") + if (Key == "content-type") { - m_ContentType = m_Headers["Content-Type"]; + m_ContentType = m_Headers[Key]; } - else if (a_Key == "Content-Length") + else if (Key == "content-length") { - m_ContentLength = atoi(m_Headers["Content-Length"].c_str()); + m_ContentLength = atoi(m_Headers[Key].c_str()); } } @@ -56,6 +69,8 @@ void cHTTPMessage::AddHeader(const AString & a_Key, const AString & a_Value) cHTTPRequest::cHTTPRequest(void) : super(mkRequest), + m_EnvelopeParser(*this), + m_IsValid(true), m_UserData(NULL) { } @@ -64,66 +79,75 @@ cHTTPRequest::cHTTPRequest(void) : -bool cHTTPRequest::ParseHeaders(const char * a_IncomingData, size_t a_IdxEnd) +int cHTTPRequest::ParseHeaders(const char * a_Data, int a_Size) { - // The first line contains the method and the URL: - size_t Next = ParseRequestLine(a_IncomingData, a_IdxEnd); - if (Next == AString::npos) + if (!m_IsValid) { - return false; + return -1; } - // The following lines contain headers: - AString Key; - const char * Data = a_IncomingData + Next; - size_t End = a_IdxEnd - Next; - while (End > 0) + if (m_Method.empty()) { - Next = ParseHeaderField(Data, End, Key); - if (Next == AString::npos) + // The first line hasn't been processed yet + int res = ParseRequestLine(a_Data, a_Size); + if ((res < 0) || (res == a_Size)) { - return false; + return res; } - ASSERT(End >= Next); - Data += Next; - End -= Next; + int res2 = m_EnvelopeParser.Parse(a_Data + res, a_Size - res); + if (res2 < 0) + { + m_IsValid = false; + return res2; + } + return res2 + res; } - if (!HasReceivedContentLength()) + if (m_EnvelopeParser.IsInHeaders()) { - SetContentLength(0); + int res = m_EnvelopeParser.Parse(a_Data, a_Size); + if (res < 0) + { + m_IsValid = false; + } + return res; } - return true; + return 0; } -size_t cHTTPRequest::ParseRequestLine(const char * a_Data, size_t a_IdxEnd) +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 < a_IdxEnd) && + (LineStart < IdxEnd) && ( - (a_Data[LineStart] == '\r') || - (a_Data[LineStart] == '\n') + (m_IncomingHeaderData[LineStart] == '\r') || + (m_IncomingHeaderData[LineStart] == '\n') ) ) { LineStart++; } - if (LineStart >= a_IdxEnd) + if (LineStart >= IdxEnd) { - return AString::npos; + m_IsValid = false; + return -1; } - size_t Last = LineStart; int NumSpaces = 0; - for (size_t i = LineStart; i < a_IdxEnd; i++) + size_t MethodEnd = 0; + size_t URLEnd = 0; + for (size_t i = LineStart; i < IdxEnd; i++) { - switch (a_Data[i]) + switch (m_IncomingHeaderData[i]) { case ' ': { @@ -131,124 +155,56 @@ size_t cHTTPRequest::ParseRequestLine(const char * a_Data, size_t a_IdxEnd) { case 0: { - m_Method.assign(a_Data, Last, i - Last); + MethodEnd = i; break; } case 1: { - m_URL.assign(a_Data, Last, i - Last); + URLEnd = i; break; } default: { // Too many spaces in the request - return AString::npos; + m_IsValid = false; + return -1; } } - Last = i + 1; NumSpaces += 1; break; } case '\n': { - if ((i == 0) || (a_Data[i - 1] != '\r') || (NumSpaces != 2) || (i < Last + 7)) + 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 - return AString::npos; + m_IsValid = false; + return -1; } // Check that there's HTTP/version at the end - if (strncmp(a_Data + Last, "HTTP/1.", 7) != 0) + if (strncmp(a_Data + URLEnd + 1, "HTTP/1.", 7) != 0) { - return AString::npos; + 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 (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) + } // switch (m_IncomingHeaderData[i]) } // for i - m_IncomingHeaderData[] - // No header found, return the end-of-data index: - return a_IdxEnd; + + // CRLF hasn't been encountered yet, consider all data consumed + return a_Size; } -size_t cHTTPRequest::ParseHeaderFieldContinuation(const char * a_Data, size_t a_IdxEnd, AString & a_Key) +void cHTTPRequest::OnHeaderLine(const AString & a_Key, const AString & a_Value) { - 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; + AddHeader(a_Key, a_Value); } diff --git a/source/HTTPServer/HTTPMessage.h b/source/HTTPServer/HTTPMessage.h index 1c2514739..ef8e12ca4 100644 --- a/source/HTTPServer/HTTPMessage.h +++ b/source/HTTPServer/HTTPMessage.h @@ -9,6 +9,8 @@ #pragma once +#include "EnvelopeParser.h" + @@ -58,15 +60,18 @@ protected: class cHTTPRequest : - public cHTTPMessage + public cHTTPMessage, + protected cEnvelopeParser::cCallbacks { 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); + /** 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); } @@ -83,7 +88,19 @@ public: /// 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(); } + 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; @@ -94,21 +111,13 @@ protected: void * m_UserData; - /** Parses the RequestLine out of a_Data, up to index a_IdxEnd - Returns the index to the next line, or npos if invalid request + /** Parses the incoming data for the first line (RequestLine) + Returns the number of bytes consumed, or -1 for an error */ - size_t ParseRequestLine(const char * a_Data, size_t a_IdxEnd); + int ParseRequestLine(const char * a_Data, int a_Size); - /** 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); + // cEnvelopeParser::cCallbacks overrides: + virtual void OnHeaderLine(const AString & a_Key, const AString & a_Value) override; } ; diff --git a/source/HTTPServer/HTTPServer.cpp b/source/HTTPServer/HTTPServer.cpp index ac21acb24..4102d1047 100644 --- a/source/HTTPServer/HTTPServer.cpp +++ b/source/HTTPServer/HTTPServer.cpp @@ -24,13 +24,14 @@ class cDebugCallbacks : - public cHTTPServer::cCallbacks + 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)); + a_Request.SetUserData(new cHTTPFormParser(a_Request, *this)); } } @@ -79,6 +80,23 @@ class cDebugCallbacks : } + 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;