1
0

Merge pull request #208 from mc-server/NewWebServer

New web server
This commit is contained in:
Mattes D
2013-10-07 13:19:14 -07:00
43 changed files with 3212 additions and 2619 deletions

View File

@@ -1,6 +1,6 @@
/*
** Lua binding: AllToLua
** Generated automatically by tolua++-1.0.92 on 10/04/13 08:36:37.
** Generated automatically by tolua++-1.0.92 on 10/06/13 15:12:14.
*/
#ifndef __cplusplus
@@ -240,18 +240,19 @@ static void tolua_reg_types (lua_State* tolua_S)
tolua_usertype(tolua_S,"cCraftingRecipe");
tolua_usertype(tolua_S,"cPlugin");
tolua_usertype(tolua_S,"cItemGrid");
tolua_usertype(tolua_S,"cBlockArea");
tolua_usertype(tolua_S,"cHTTPServer::cCallbacks");
tolua_usertype(tolua_S,"cLuaWindow");
tolua_usertype(tolua_S,"cInventory");
tolua_usertype(tolua_S,"cBoundingBox");
tolua_usertype(tolua_S,"cBlockEntityWithItems");
tolua_usertype(tolua_S,"HTTPFormData");
tolua_usertype(tolua_S,"cTracer");
tolua_usertype(tolua_S,"HTTPFormData");
tolua_usertype(tolua_S,"cWindow");
tolua_usertype(tolua_S,"cArrowEntity");
tolua_usertype(tolua_S,"cDropSpenserEntity");
tolua_usertype(tolua_S,"cWindow");
tolua_usertype(tolua_S,"Vector3i");
tolua_usertype(tolua_S,"cBlockArea");
tolua_usertype(tolua_S,"cCraftingGrid");
tolua_usertype(tolua_S,"Vector3i");
tolua_usertype(tolua_S,"cGroup");
tolua_usertype(tolua_S,"cStringMap");
tolua_usertype(tolua_S,"cBlockEntity");
@@ -18794,38 +18795,6 @@ static int tolua_AllToLua_cWebAdmin_GetMemoryUsage00(lua_State* tolua_S)
}
#endif //#ifndef TOLUA_DISABLE
/* method: GetPort of class cWebAdmin */
#ifndef TOLUA_DISABLE_tolua_AllToLua_cWebAdmin_GetPort00
static int tolua_AllToLua_cWebAdmin_GetPort00(lua_State* tolua_S)
{
#ifndef TOLUA_RELEASE
tolua_Error tolua_err;
if (
!tolua_isusertype(tolua_S,1,"cWebAdmin",0,&tolua_err) ||
!tolua_isnoobj(tolua_S,2,&tolua_err)
)
goto tolua_lerror;
else
#endif
{
cWebAdmin* self = (cWebAdmin*) tolua_tousertype(tolua_S,1,0);
#ifndef TOLUA_RELEASE
if (!self) tolua_error(tolua_S,"invalid 'self' in function 'GetPort'", NULL);
#endif
{
int tolua_ret = (int) self->GetPort();
tolua_pushnumber(tolua_S,(lua_Number)tolua_ret);
}
}
return 1;
#ifndef TOLUA_RELEASE
tolua_lerror:
tolua_error(tolua_S,"#ferror in function 'GetPort'.",&tolua_err);
return 0;
#endif
}
#endif //#ifndef TOLUA_DISABLE
/* method: GetPage of class cWebAdmin */
#ifndef TOLUA_DISABLE_tolua_AllToLua_cWebAdmin_GetPage00
static int tolua_AllToLua_cWebAdmin_GetPage00(lua_State* tolua_S)
@@ -18870,6 +18839,38 @@ static int tolua_AllToLua_cWebAdmin_GetPage00(lua_State* tolua_S)
}
#endif //#ifndef TOLUA_DISABLE
/* method: GetDefaultPage of class cWebAdmin */
#ifndef TOLUA_DISABLE_tolua_AllToLua_cWebAdmin_GetDefaultPage00
static int tolua_AllToLua_cWebAdmin_GetDefaultPage00(lua_State* tolua_S)
{
#ifndef TOLUA_RELEASE
tolua_Error tolua_err;
if (
!tolua_isusertype(tolua_S,1,"cWebAdmin",0,&tolua_err) ||
!tolua_isnoobj(tolua_S,2,&tolua_err)
)
goto tolua_lerror;
else
#endif
{
cWebAdmin* self = (cWebAdmin*) tolua_tousertype(tolua_S,1,0);
#ifndef TOLUA_RELEASE
if (!self) tolua_error(tolua_S,"invalid 'self' in function 'GetDefaultPage'", NULL);
#endif
{
AString tolua_ret = (AString) self->GetDefaultPage();
tolua_pushcppstring(tolua_S,(const char*)tolua_ret);
}
}
return 1;
#ifndef TOLUA_RELEASE
tolua_lerror:
tolua_error(tolua_S,"#ferror in function 'GetDefaultPage'.",&tolua_err);
return 0;
#endif
}
#endif //#ifndef TOLUA_DISABLE
/* method: GetBaseURL of class cWebAdmin */
#ifndef TOLUA_DISABLE_tolua_AllToLua_cWebAdmin_GetBaseURL00
static int tolua_AllToLua_cWebAdmin_GetBaseURL00(lua_State* tolua_S)
@@ -30330,11 +30331,11 @@ TOLUA_API int tolua_AllToLua_open (lua_State* tolua_S)
tolua_variable(tolua_S,"PluginName",tolua_get_sWebAdminPage_PluginName,tolua_set_sWebAdminPage_PluginName);
tolua_variable(tolua_S,"TabName",tolua_get_sWebAdminPage_TabName,tolua_set_sWebAdminPage_TabName);
tolua_endmodule(tolua_S);
tolua_cclass(tolua_S,"cWebAdmin","cWebAdmin","",NULL);
tolua_cclass(tolua_S,"cWebAdmin","cWebAdmin","cHTTPServer::cCallbacks",NULL);
tolua_beginmodule(tolua_S,"cWebAdmin");
tolua_function(tolua_S,"GetMemoryUsage",tolua_AllToLua_cWebAdmin_GetMemoryUsage00);
tolua_function(tolua_S,"GetPort",tolua_AllToLua_cWebAdmin_GetPort00);
tolua_function(tolua_S,"GetPage",tolua_AllToLua_cWebAdmin_GetPage00);
tolua_function(tolua_S,"GetDefaultPage",tolua_AllToLua_cWebAdmin_GetDefaultPage00);
tolua_function(tolua_S,"GetBaseURL",tolua_AllToLua_cWebAdmin_GetBaseURL00);
tolua_endmodule(tolua_S);
tolua_cclass(tolua_S,"cWebPlugin","cWebPlugin","",NULL);

View File

@@ -1,6 +1,6 @@
/*
** Lua binding: AllToLua
** Generated automatically by tolua++-1.0.92 on 10/04/13 08:36:38.
** Generated automatically by tolua++-1.0.92 on 10/06/13 15:12:15.
*/
/* Exported function */

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,278 @@
// 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;
}
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();
}

View File

@@ -0,0 +1,107 @@
// 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:
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);
/** 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:
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"
};
/// 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;
} ;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -224,7 +224,10 @@ void cListenThread::Execute(void)
if (itr->IsValid() && FD_ISSET(itr->GetSocket(), &fdRead))
{
cSocket Client = (m_Family == cSocket::IPv4) ? itr->AcceptIPv4() : itr->AcceptIPv6();
m_Callback.OnConnectionAccepted(Client);
if (Client.IsValid())
{
m_Callback.OnConnectionAccepted(Client);
}
}
} // for itr - m_Sockets[]
} // while (!m_ShouldTerminate)

View File

@@ -134,17 +134,9 @@ void cRoot::Start(void)
}
IniFile.WriteFile();
cIniFile WebIniFile("webadmin.ini");
if (!WebIniFile.ReadFile())
{
LOGWARNING("webadmin.ini inaccessible, wabadmin is disabled");
}
if (WebIniFile.GetValueB("WebAdmin", "Enabled", false))
{
LOGD("Creating WebAdmin...");
m_WebAdmin = new cWebAdmin(8080);
}
LOG("Initialising WebAdmin...");
m_WebAdmin = new cWebAdmin();
m_WebAdmin->Init();
LOGD("Loading settings...");
m_GroupManager = new cGroupManager();
@@ -173,6 +165,9 @@ void cRoot::Start(void)
LOGD("Finalising startup...");
m_Server->Start();
LOG("Starting WebAdmin...");
m_WebAdmin->Start();
#if !defined(ANDROID_NDK)
LOGD("Starting InputThread...");

View File

@@ -2,6 +2,7 @@
#pragma once
#include "Authenticator.h"
#include "HTTPServer/HTTPServer.h"
@@ -141,6 +142,7 @@ private:
cWebAdmin * m_WebAdmin;
cPluginManager * m_PluginManager;
cAuthenticator m_Authenticator;
cHTTPServer m_HTTPServer;
cMCLogger * m_Log;

View File

@@ -196,6 +196,23 @@ AString & StrToUpper(AString & s)
AString & StrToLower(AString & s)
{
AString::iterator i = s.begin();
AString::iterator end = s.end();
while (i != end)
{
*i = (char)tolower(*i);
++i;
}
return s;
}
int NoCaseCompare(const AString & s1, const AString & s2)
{
#ifdef _MSC_VER
@@ -658,3 +675,141 @@ AString StripColorCodes(const AString & a_Message)
AString URLDecode(const AString & a_String)
{
AString res;
size_t len = a_String.length();
res.reserve(len);
for (size_t i = 0; i < len; i++)
{
char ch = a_String[i];
if ((ch != '%') || (i > len - 3))
{
res.push_back(ch);
continue;
}
// Decode the hex value:
char hi = a_String[i + 1], lo = a_String[i + 2];
if ((hi >= '0') && (hi <= '9'))
{
hi = hi - '0';
}
else if ((hi >= 'a') && (hi <= 'f'))
{
hi = hi - 'a' + 10;
}
else if ((hi >= 'A') && (hi <= 'F'))
{
hi = hi - 'F' + 10;
}
else
{
res.push_back(ch);
continue;
}
if ((lo >= '0') && (lo <= '9'))
{
lo = lo - '0';
}
else if ((lo >= 'a') && (lo <= 'f'))
{
lo = lo - 'a' + 10;
}
else if ((lo >= 'A') && (lo <= 'F'))
{
lo = lo - 'A' + 10;
}
else
{
res.push_back(ch);
continue;
}
res.push_back((hi << 4) | lo);
i += 2;
} // for i - a_String[]
return res;
}
AString ReplaceAllCharOccurrences(const AString & a_String, char a_From, char a_To)
{
AString res(a_String);
std::replace(res.begin(), res.end(), a_From, a_To);
return res;
}
/// Converts one Hex character in a Base64 encoding into the data value
static inline int UnBase64(char c)
{
if (c >='A' && c <= 'Z')
{
return c - 'A';
}
if (c >='a' && c <= 'z')
{
return c - 'a' + 26;
}
if (c >= '0' && c <= '9')
{
return c - '0' + 52;
}
if (c == '+')
{
return 62;
}
if (c == '/')
{
return 63;
}
if (c == '=')
{
return -1;
}
return -2;
}
AString Base64Decode(const AString & a_Base64String)
{
AString res;
size_t i, len = a_Base64String.size();
int o, c;
res.resize((len * 4) / 3 + 5, 0); // Approximate the upper bound on the result length
for (o = 0, i = 0; i < len; i++)
{
c = UnBase64(a_Base64String[i]);
if (c >= 0)
{
switch (o & 7)
{
case 0: res[o >> 3] |= (c << 2); break;
case 6: res[o >> 3] |= (c >> 4); res[(o >> 3) + 1] |= (c << 4); break;
case 4: res[o >> 3] |= (c >> 2); res[(o >> 3) + 1] |= (c << 6); break;
case 2: res[o >> 3] |= c; break;
}
o += 6;
}
if (c == -1)
{
// Error while decoding, invalid input. Return as much as we've decoded:
res.resize(o >> 3);
return res;
}
}
res.resize(o >> 3);
return res;}

View File

@@ -45,6 +45,9 @@ extern AString TrimString(const AString & str); // tolua_export
/// In-place string conversion to uppercase; returns the same string
extern AString & StrToUpper(AString & s);
/// In-place string conversion to lowercase; returns the same string
extern AString & StrToLower(AString & s);
/// Case-insensitive string comparison; returns 0 if the strings are the same
extern int NoCaseCompare(const AString & s1, const AString & s2); // tolua_export
@@ -72,6 +75,15 @@ extern AString EscapeString(const AString & a_Message); // tolua_export
/// Removes all control codes used by MC for colors and styles
extern AString StripColorCodes(const AString & a_Message); // tolua_export
/// URL-Decodes the given string, replacing all "%HH" into the correct characters. Invalid % sequences are left intact
extern AString URLDecode(const AString & a_String); // Cannot export to Lua automatically - would generated an extra return value
/// Replaces all occurrences of char a_From inside a_String with char a_To.
extern AString ReplaceAllCharOccurrences(const AString & a_String, char a_From, char a_To); // Needn't export to Lua, since Lua doesn't have chars anyway
/// Decodes a Base64-encoded string into the raw data
extern AString Base64Decode(const AString & a_Base64String);
// If you have any other string helper functions, declare them here

View File

@@ -14,7 +14,8 @@
#include "Server.h"
#include "Root.h"
#include "../iniFile/iniFile.h"
#include "HTTPServer/HTTPMessage.h"
#include "HTTPServer/HTTPConnection.h"
#ifdef _WIN32
#include <psapi.h>
@@ -49,37 +50,11 @@ public:
cWebAdmin * WebAdmin = NULL;
cWebAdmin::cWebAdmin( int a_Port /* = 8080 */ ) :
m_Port(a_Port),
m_bConnected(false),
m_TemplateScript("<webadmin_template>")
cWebAdmin::cWebAdmin(void) :
m_IsInitialized(false),
m_TemplateScript("<webadmin_template>"),
m_IniFile("webadmin.ini")
{
WebAdmin = this;
m_Event = new cEvent();
Init( m_Port );
}
cWebAdmin::~cWebAdmin()
{
WebAdmin = 0;
m_WebServer->Stop();
delete m_WebServer;
delete m_IniFile;
m_Event->Wait();
delete m_Event;
}
@@ -105,189 +80,36 @@ void cWebAdmin::RemovePlugin( cWebPlugin * a_Plugin )
void cWebAdmin::Request_Handler(webserver::http_request* r)
bool cWebAdmin::Init(void)
{
if( WebAdmin == 0 ) return;
LOG("Path: %s", r->path_.c_str() );
if (r->path_ == "/")
if (!m_IniFile.ReadFile())
{
r->answer_ += "<h1>MCServer WebAdmin</h1>";
r->answer_ += "<center>";
r->answer_ += "<form method='get' action='webadmin/'>";
r->answer_ += "<input type='submit' value='Log in'>";
r->answer_ += "</form>";
r->answer_ += "</center>";
return;
}
if (r->path_.empty() || r->path_[0] != '/')
{
r->answer_ += "<h1>Bad request</h1>";
r->answer_ += "<p>";
r->answer_ = r->path_; // TODO: Shouldn't we sanitize this? Possible security issue.
r->answer_ += "</p>";
return;
return false;
}
AStringVector Split = StringSplit(r->path_.substr(1), "/");
if (Split.empty() || (Split[0] != "webadmin" && Split[0] != "~webadmin"))
{
r->answer_ += "<h1>Bad request</h1>";
return;
}
AString PortsIPv4 = m_IniFile.GetValue("WebAdmin", "Port", "8080");
AString PortsIPv6 = m_IniFile.GetValue("WebAdmin", "PortsIPv6", "");
if (!r->authentication_given_)
if (!m_HTTPServer.Initialize(PortsIPv4, PortsIPv6))
{
r->answer_ += "no auth";
r->auth_realm_ = "MCServer WebAdmin";
}
bool bDontShowTemplate = false;
if (Split[0] == "~webadmin")
{
bDontShowTemplate = true;
}
AString UserPassword = WebAdmin->m_IniFile->GetValue( "User:"+r->username_, "Password", "");
if ((UserPassword != "") && (r->password_ == UserPassword))
{
AString Template;
HTTPTemplateRequest TemplateRequest;
TemplateRequest.Request.Username = r->username_;
TemplateRequest.Request.Method = r->method_;
TemplateRequest.Request.Params = r->params_;
TemplateRequest.Request.PostParams = r->params_post_;
TemplateRequest.Request.Path = r->path_.substr(1);
for( unsigned int i = 0; i < r->multipart_formdata_.size(); ++i )
{
webserver::formdata& fd = r->multipart_formdata_[i];
HTTPFormData HTTPfd;//( fd.value_ );
HTTPfd.Value = fd.value_;
HTTPfd.Type = fd.content_type_;
HTTPfd.Name = fd.name_;
LOGINFO("Form data name: %s", fd.name_.c_str() );
TemplateRequest.Request.FormData[ fd.name_ ] = HTTPfd;
}
// Try to get the template from the Lua template script
bool bLuaTemplateSuccessful = false;
if (!bDontShowTemplate)
{
bLuaTemplateSuccessful = WebAdmin->m_TemplateScript.Call("ShowPage", WebAdmin, &TemplateRequest, cLuaState::Return, Template);
}
if (!bLuaTemplateSuccessful)
{
AString BaseURL = WebAdmin->GetBaseURL(Split);
AString Menu;
Template = bDontShowTemplate ? "{CONTENT}" : WebAdmin->GetTemplate();
AString FoundPlugin;
for (PluginList::iterator itr = WebAdmin->m_Plugins.begin(); itr != WebAdmin->m_Plugins.end(); ++itr)
{
cWebPlugin* WebPlugin = *itr;
std::list< std::pair<AString, AString> > NameList = WebPlugin->GetTabNames();
for( std::list< std::pair<AString, AString> >::iterator Names = NameList.begin(); Names != NameList.end(); ++Names )
{
Menu += "<li><a href='" + BaseURL + WebPlugin->GetWebTitle().c_str() + "/" + (*Names).second + "'>" + (*Names).first + "</a></li>";
}
}
sWebAdminPage Page = WebAdmin->GetPage(TemplateRequest.Request);
AString Content = Page.Content;
FoundPlugin = Page.PluginName;
if (!Page.TabName.empty())
FoundPlugin += " - " + Page.TabName;
if( FoundPlugin.empty() ) // Default page
{
Content.clear();
FoundPlugin = "Current Game";
Content += "<h4>Server Name:</h4>";
Content += "<p>" + AString( cRoot::Get()->GetServer()->GetServerID() ) + "</p>";
Content += "<h4>Plugins:</h4><ul>";
cPluginManager* PM = cRoot::Get()->GetPluginManager();
if( PM )
{
const cPluginManager::PluginMap & List = PM->GetAllPlugins();
for( cPluginManager::PluginMap::const_iterator itr = List.begin(); itr != List.end(); ++itr )
{
if( itr->second == NULL ) continue;
AString VersionNum;
AppendPrintf(Content, "<li>%s V.%i</li>", itr->second->GetName().c_str(), itr->second->GetVersion());
}
}
Content += "</ul>";
Content += "<h4>Players:</h4><ul>";
cPlayerAccum PlayerAccum;
cWorld * World = cRoot::Get()->GetDefaultWorld(); // TODO - Create a list of worlds and players
if( World != NULL )
{
World->ForEachPlayer(PlayerAccum);
Content.append(PlayerAccum.m_Contents);
}
Content += "</ul><br>";
}
if (!bDontShowTemplate && (Split.size() > 1))
{
Content += "\n<p><a href='" + BaseURL + "'>Go back</a></p>";
}
int MemUsageKiB = GetMemoryUsage();
if (MemUsageKiB > 0)
{
ReplaceString(Template, "{MEM}", Printf("%.02f", (double)MemUsageKiB / 1024));
ReplaceString(Template, "{MEMKIB}", Printf("%d", MemUsageKiB));
}
else
{
ReplaceString(Template, "{MEM}", "unknown");
ReplaceString(Template, "{MEMKIB}", "unknown");
}
ReplaceString(Template, "{USERNAME}", r->username_);
ReplaceString(Template, "{MENU}", Menu);
ReplaceString(Template, "{PLUGIN_NAME}", FoundPlugin);
ReplaceString(Template, "{CONTENT}", Content);
ReplaceString(Template, "{TITLE}", "MCServer");
AString NumChunks;
Printf(NumChunks, "%d", cRoot::Get()->GetTotalChunkCount());
ReplaceString(Template, "{NUMCHUNKS}", NumChunks);
}
r->answer_ = Template;
}
else
{
r->answer_ += "Wrong username/password";
r->auth_realm_ = "MCServer WebAdmin";
return false;
}
m_IsInitialized = true;
return true;
}
bool cWebAdmin::Init(int a_Port)
bool cWebAdmin::Start(void)
{
m_Port = a_Port;
m_IniFile = new cIniFile("webadmin.ini");
if (m_IniFile->ReadFile())
if (!m_IsInitialized)
{
m_Port = m_IniFile->GetValueI("WebAdmin", "Port", 8080);
// Not initialized
return false;
}
// Initialize the WebAdmin template script and load the file
m_TemplateScript.Create();
if (!m_TemplateScript.LoadFile(FILE_IO_PREFIX "webadmin/template.lua"))
@@ -296,46 +118,7 @@ bool cWebAdmin::Init(int a_Port)
m_TemplateScript.Close();
}
LOGINFO("Starting WebAdmin on port %i", m_Port);
#ifdef _WIN32
HANDLE hThread = CreateThread(
NULL, // default security
0, // default stack size
ListenThread, // name of the thread function
this, // thread parameters
0, // default startup flags
NULL);
CloseHandle( hThread ); // Just close the handle immediately
#else
pthread_t LstnThread;
pthread_create( &LstnThread, 0, ListenThread, this);
#endif
return true;
}
#ifdef _WIN32
DWORD WINAPI cWebAdmin::ListenThread(LPVOID lpParam)
#else
void *cWebAdmin::ListenThread( void *lpParam )
#endif
{
cWebAdmin* self = (cWebAdmin*)lpParam;
self->m_WebServer = new webserver(self->m_Port, Request_Handler );
if (!self->m_WebServer->Begin())
{
LOGWARN("WebServer failed to start! WebAdmin is disabled");
}
self->m_Event->Set();
return 0;
return m_HTTPServer.Start(*this);
}
@@ -364,7 +147,156 @@ AString cWebAdmin::GetTemplate()
sWebAdminPage cWebAdmin::GetPage(const HTTPRequest& a_Request)
void cWebAdmin::HandleWebadminRequest(cHTTPConnection & a_Connection, cHTTPRequest & a_Request)
{
if (!a_Request.HasAuth())
{
a_Connection.SendNeedAuth("MCServer WebAdmin");
return;
}
// Check auth:
AString UserPassword = m_IniFile.GetValue("User:" + a_Request.GetAuthUsername(), "Password", "");
if ((UserPassword == "") || (a_Request.GetAuthPassword() != UserPassword))
{
a_Connection.SendNeedAuth("MCServer WebAdmin - bad username or password");
return;
}
// Check if the contents should be wrapped in the template:
AString URL = a_Request.GetBareURL();
ASSERT(URL.length() > 0);
bool ShouldWrapInTemplate = ((URL.length() > 1) && (URL[1] != '~'));
// Retrieve the request data:
cWebadminRequestData * Data = (cWebadminRequestData *)(a_Request.GetUserData());
if (Data == NULL)
{
a_Connection.SendStatusAndReason(500, "Bad UserData");
return;
}
// Wrap it all up for the Lua call:
AString Template;
HTTPTemplateRequest TemplateRequest;
TemplateRequest.Request.Username = a_Request.GetAuthUsername();
TemplateRequest.Request.Method = a_Request.GetMethod();
TemplateRequest.Request.Path = URL.substr(1);
if (Data->m_Form.Finish())
{
for (cHTTPFormParser::const_iterator itr = Data->m_Form.begin(), end = Data->m_Form.end(); itr != end; ++itr)
{
HTTPFormData HTTPfd;
HTTPfd.Value = itr->second;
HTTPfd.Type = "";
HTTPfd.Name = itr->first;
TemplateRequest.Request.FormData[itr->first] = HTTPfd;
TemplateRequest.Request.PostParams[itr->first] = itr->second;
TemplateRequest.Request.Params[itr->first] = itr->second;
} // for itr - Data->m_Form[]
}
// Try to get the template from the Lua template script
if (ShouldWrapInTemplate)
{
if (m_TemplateScript.Call("ShowPage", this, &TemplateRequest, cLuaState::Return, Template))
{
cHTTPResponse Resp;
Resp.SetContentType("text/html");
a_Connection.Send(Resp);
a_Connection.Send(Template.c_str(), Template.length());
return;
}
a_Connection.SendStatusAndReason(500, "m_TemplateScript failed");
return;
}
AString BaseURL = GetBaseURL(URL);
AString Menu;
Template = "{CONTENT}";
AString FoundPlugin;
for (PluginList::iterator itr = m_Plugins.begin(); itr != m_Plugins.end(); ++itr)
{
cWebPlugin * WebPlugin = *itr;
std::list< std::pair<AString, AString> > NameList = WebPlugin->GetTabNames();
for (std::list< std::pair<AString, AString> >::iterator Names = NameList.begin(); Names != NameList.end(); ++Names)
{
Menu += "<li><a href='" + BaseURL + WebPlugin->GetWebTitle().c_str() + "/" + (*Names).second + "'>" + (*Names).first + "</a></li>";
}
}
sWebAdminPage Page = GetPage(TemplateRequest.Request);
AString Content = Page.Content;
FoundPlugin = Page.PluginName;
if (!Page.TabName.empty())
{
FoundPlugin += " - " + Page.TabName;
}
if (FoundPlugin.empty()) // Default page
{
Content = GetDefaultPage();
}
if (ShouldWrapInTemplate && (URL.size() > 1))
{
Content += "\n<p><a href='" + BaseURL + "'>Go back</a></p>";
}
int MemUsageKiB = GetMemoryUsage();
if (MemUsageKiB > 0)
{
ReplaceString(Template, "{MEM}", Printf("%.02f", (double)MemUsageKiB / 1024));
ReplaceString(Template, "{MEMKIB}", Printf("%d", MemUsageKiB));
}
else
{
ReplaceString(Template, "{MEM}", "unknown");
ReplaceString(Template, "{MEMKIB}", "unknown");
}
ReplaceString(Template, "{USERNAME}", a_Request.GetAuthUsername());
ReplaceString(Template, "{MENU}", Menu);
ReplaceString(Template, "{PLUGIN_NAME}", FoundPlugin);
ReplaceString(Template, "{CONTENT}", Content);
ReplaceString(Template, "{TITLE}", "MCServer");
AString NumChunks;
Printf(NumChunks, "%d", cRoot::Get()->GetTotalChunkCount());
ReplaceString(Template, "{NUMCHUNKS}", NumChunks);
cHTTPResponse Resp;
Resp.SetContentType("text/html");
a_Connection.Send(Resp);
a_Connection.Send(Template.c_str(), Template.length());
}
void cWebAdmin::HandleRootRequest(cHTTPConnection & a_Connection, cHTTPRequest & a_Request)
{
static const char LoginForm[] = \
"<h1>MCServer WebAdmin</h1>" \
"<center>" \
"<form method='get' action='webadmin/'>" \
"<input type='submit' value='Log in'>" \
"</form>" \
"</center>";
cHTTPResponse Resp;
Resp.SetContentType("text/html");
a_Connection.Send(Resp);
a_Connection.Send(LoginForm, sizeof(LoginForm) - 1);
a_Connection.FinishResponse();
}
sWebAdminPage cWebAdmin::GetPage(const HTTPRequest & a_Request)
{
sWebAdminPage Page;
AStringVector Split = StringSplit(a_Request.Path, "/");
@@ -373,7 +305,7 @@ sWebAdminPage cWebAdmin::GetPage(const HTTPRequest& a_Request)
AString FoundPlugin;
if (Split.size() > 1)
{
for (PluginList::iterator itr = WebAdmin->m_Plugins.begin(); itr != WebAdmin->m_Plugins.end(); ++itr)
for (PluginList::iterator itr = m_Plugins.begin(); itr != m_Plugins.end(); ++itr)
{
if ((*itr)->GetWebTitle() == Split[1])
{
@@ -396,6 +328,41 @@ sWebAdminPage cWebAdmin::GetPage(const HTTPRequest& a_Request)
AString cWebAdmin::GetDefaultPage(void)
{
AString Content;
Content += "<h4>Server Name:</h4>";
Content += "<p>" + AString( cRoot::Get()->GetServer()->GetServerID() ) + "</p>";
Content += "<h4>Plugins:</h4><ul>";
cPluginManager * PM = cPluginManager::Get();
const cPluginManager::PluginMap & List = PM->GetAllPlugins();
for (cPluginManager::PluginMap::const_iterator itr = List.begin(); itr != List.end(); ++itr)
{
if (itr->second == NULL)
{
continue;
}
AString VersionNum;
AppendPrintf(Content, "<li>%s V.%i</li>", itr->second->GetName().c_str(), itr->second->GetVersion());
}
Content += "</ul>";
Content += "<h4>Players:</h4><ul>";
cPlayerAccum PlayerAccum;
cWorld * World = cRoot::Get()->GetDefaultWorld(); // TODO - Create a list of worlds and players
if( World != NULL )
{
World->ForEachPlayer(PlayerAccum);
Content.append(PlayerAccum.m_Contents);
}
Content += "</ul><br>";
return Content;
}
AString cWebAdmin::GetBaseURL( const AString& a_URL )
{
return GetBaseURL(StringSplit(a_URL, "/"));
@@ -474,3 +441,81 @@ int cWebAdmin::GetMemoryUsage(void)
void cWebAdmin::OnRequestBegun(cHTTPConnection & a_Connection, cHTTPRequest & a_Request)
{
const AString & URL = a_Request.GetURL();
if (
(strncmp(URL.c_str(), "/webadmin", 9) == 0) ||
(strncmp(URL.c_str(), "/~webadmin", 10) == 0)
)
{
a_Request.SetUserData(new cWebadminRequestData(a_Request));
return;
}
if (URL == "/")
{
// The root needs no body handler and is fully handled in the OnRequestFinished() call
return;
}
// TODO: Handle other requests
}
void cWebAdmin::OnRequestBody(cHTTPConnection & a_Connection, cHTTPRequest & a_Request, const char * a_Data, int a_Size)
{
cRequestData * Data = (cRequestData *)(a_Request.GetUserData());
if (Data == NULL)
{
return;
}
Data->OnBody(a_Data, a_Size);
}
void cWebAdmin::OnRequestFinished(cHTTPConnection & a_Connection, cHTTPRequest & a_Request)
{
const AString & URL = a_Request.GetURL();
if (
(strncmp(URL.c_str(), "/webadmin", 9) == 0) ||
(strncmp(URL.c_str(), "/~webadmin", 10) == 0)
)
{
HandleWebadminRequest(a_Connection, a_Request);
}
else if (URL == "/")
{
// The root needs no body handler and is fully handled in the OnRequestFinished() call
HandleRootRequest(a_Connection, a_Request);
}
else
{
// TODO: Handle other requests
}
// Delete any request data assigned to the request:
cRequestData * Data = (cRequestData *)(a_Request.GetUserData());
delete Data;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// cWebAdmin::cWebadminRequestData
void cWebAdmin::cWebadminRequestData::OnBody(const char * a_Data, int a_Size)
{
m_Form.Parse(a_Data, a_Size);
}

View File

@@ -1,8 +1,26 @@
// WebAdmin.h
// Declares the cWebAdmin class representing the admin interface over http protocol, and related services (API)
#pragma once
#include "../WebServer/WebServer.h"
#include "OSSupport/Socket.h"
#include "LuaState.h"
#include "../iniFile/iniFile.h"
#include "HTTPServer/HTTPServer.h"
#include "HTTPServer/HTTPFormParser.h"
// Disable MSVC warnings:
#if defined(_MSC_VER)
#pragma warning(push)
#pragma warning(disable:4355) // 'this' : used in base member initializer list
#endif
@@ -10,7 +28,6 @@
// fwd:
class cStringMap;
class cEvent;
class cIniFile;
class cWebPlugin;
@@ -73,7 +90,8 @@ struct sWebAdminPage
// tolua_begin
class cWebAdmin
class cWebAdmin :
public cHTTPServer::cCallbacks
{
public:
// tolua_end
@@ -81,60 +99,109 @@ public:
typedef std::list< cWebPlugin* > PluginList;
cWebAdmin( int a_Port = 8080 );
~cWebAdmin();
cWebAdmin(void);
bool Init( int a_Port );
/// Initializes the object. Returns true if successfully initialized and ready to start
bool Init(void);
/// Starts the HTTP server taking care of the admin. Returns true if successful
bool Start(void);
void AddPlugin( cWebPlugin* a_Plugin );
void RemovePlugin( cWebPlugin* a_Plugin );
void AddPlugin( cWebPlugin* a_Plugin );
void RemovePlugin( cWebPlugin* a_Plugin );
// TODO: Convert this to the auto-locking callback mechanism used for looping players in worlds and such
PluginList GetPlugins() const { return m_Plugins; } // >> EXPORTED IN MANUALBINDINGS <<
static void Request_Handler(webserver::http_request* r);
// tolua_begin
/// Returns the amount of currently used memory, in KiB, or -1 if it cannot be queried
static int GetMemoryUsage(void);
int GetPort() { return m_Port; }
sWebAdminPage GetPage(const HTTPRequest& a_Request);
/// Returns the contents of the default page - the list of plugins and players
AString GetDefaultPage(void);
AString GetBaseURL(const AString& a_URL);
// tolua_end
AString GetBaseURL(const AStringVector& a_URLSplit);
private:
int m_Port;
protected:
/// Common base class for request body data handlers
class cRequestData
{
public:
virtual ~cRequestData() {} // Force a virtual destructor in all descendants
/// Called when a new chunk of body data is received
virtual void OnBody(const char * a_Data, int a_Size) = 0;
} ;
/// The body handler for requests in the "/webadmin" and "/~webadmin" paths
class cWebadminRequestData :
public cRequestData,
public cHTTPFormParser::cCallbacks
{
public:
cHTTPFormParser m_Form;
cWebadminRequestData(cHTTPRequest & a_Request) :
m_Form(a_Request, *this)
{
}
// cRequestData overrides:
virtual void OnBody(const char * a_Data, int a_Size) override;
bool m_bConnected;
cSocket m_ListenSocket;
// cHTTPFormParser::cCallbacks overrides. Files are ignored:
virtual void OnFileStart(cHTTPFormParser & a_Parser, const AString & a_FileName) override {}
virtual void OnFileData(cHTTPFormParser & a_Parser, const char * a_Data, int a_Size) override {}
virtual void OnFileEnd(cHTTPFormParser & a_Parser) override {}
} ;
/// Set to true if Init() succeeds and the webadmin isn't to be disabled
bool m_IsInitialized;
cIniFile * m_IniFile;
/// The webadmin.ini file, used for the settings and allowed logins
cIniFile m_IniFile;
PluginList m_Plugins;
cEvent * m_Event;
webserver * m_WebServer;
/// The Lua template script to provide templates:
cLuaState m_TemplateScript;
/// The HTTP server which provides the underlying HTTP parsing, serialization and events
cHTTPServer m_HTTPServer;
#ifdef _WIN32
static DWORD WINAPI ListenThread(LPVOID lpParam);
#else
static void * ListenThread(void * lpParam);
#endif
AString GetTemplate();
AString GetTemplate(void);
/// Handles requests coming to the "/webadmin" or "/~webadmin" URLs
void HandleWebadminRequest(cHTTPConnection & a_Connection, cHTTPRequest & a_Request);
/// Handles requests for the root page
void HandleRootRequest(cHTTPConnection & a_Connection, cHTTPRequest & a_Request);
// cHTTPServer::cCallbacks overrides:
virtual void OnRequestBegun (cHTTPConnection & a_Connection, cHTTPRequest & a_Request) override;
virtual void OnRequestBody (cHTTPConnection & a_Connection, cHTTPRequest & a_Request, const char * a_Data, int a_Size) override;
virtual void OnRequestFinished(cHTTPConnection & a_Connection, cHTTPRequest & a_Request) override;
} ; // tolua_export
// Revert MSVC warnings back to orignal state:
#if defined(_MSC_VER)
#pragma warning(pop)
#endif