1
0
Fork 0

HTTP Server can now parse multipart/form-data forms; better architecture.

This commit is contained in:
madmaxoft 2013-10-04 13:07:57 +02:00
parent 9a33732f6a
commit 1012fd82fd
8 changed files with 342 additions and 185 deletions

View File

@ -2719,6 +2719,14 @@
<Filter
Name="HTTPServer"
>
<File
RelativePath="..\source\HTTPServer\EnvelopeParser.cpp"
>
</File>
<File
RelativePath="..\source\HTTPServer\EnvelopeParser.h"
>
</File>
<File
RelativePath="..\source\HTTPServer\HTTPConnection.cpp"
>
@ -2751,6 +2759,22 @@
RelativePath="..\source\HTTPServer\HTTPServer.h"
>
</File>
<File
RelativePath="..\source\HTTPServer\MultipartParser.cpp"
>
</File>
<File
RelativePath="..\source\HTTPServer\MultipartParser.h"
>
</File>
<File
RelativePath="..\source\HTTPServer\NameValueParser.cpp"
>
</File>
<File
RelativePath="..\source\HTTPServer\NameValueParser.h"
>
</File>
</Filter>
</Filter>
<Filter

View File

@ -108,22 +108,13 @@ void cHTTPConnection::DataReceived(const char * a_Data, int a_Size)
{
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)
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;

View File

@ -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)

View File

@ -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();
}

View File

@ -8,6 +8,8 @@
#pragma once
#include "MultipartParser.h"
@ -20,10 +22,25 @@ class cHTTPRequest;
class cHTTPFormParser :
public std::map<AString, AString>
public std::map<AString, AString>,
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<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);
/// 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;
} ;

View File

@ -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);
}

View File

@ -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;
} ;

View File

@ -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;