#include "Globals.h" // NOTE: MSVC stupidness requires this to be the same across all modules #include "WebAdmin.h" #include "Bindings/WebPlugin.h" #include "Bindings/PluginManager.h" #include "Bindings/Plugin.h" #include "World.h" #include "Entities/Player.h" #include "Server.h" #include "Root.h" #include "HTTP/HTTPServerConnection.h" #include "HTTP/HTTPFormParser.h" static const char DEFAULT_WEBADMIN_PORTS[] = "8080"; //////////////////////////////////////////////////////////////////////////////// // cPlayerAccum: /** Helper class - appends all player names together in an HTML list */ class cPlayerAccum : public cPlayerListCallback { virtual bool Item(cPlayer * a_Player) override { m_Contents.append("
  • "); m_Contents.append(a_Player->GetName()); m_Contents.append("
  • "); return false; } public: AString m_Contents; } ; //////////////////////////////////////////////////////////////////////////////// // cWebadminRequestData /** The form parser callbacks for requests in the "/webadmin" and "/~webadmin" paths */ class cWebadminRequestData : public cHTTPFormParser::cCallbacks, public cHTTPIncomingRequest::cUserData { public: cHTTPFormParser m_Form; cWebadminRequestData(const cHTTPIncomingRequest & a_Request): m_Form(a_Request, *this) { } // cHTTPFormParser::cCallbacks overrides. Files are ignored: virtual void OnFileStart(cHTTPFormParser &, const AString & a_FileName) override { UNUSED(a_FileName); } virtual void OnFileData(cHTTPFormParser &, const char * a_Data, size_t a_Size) override { UNUSED(a_Data); UNUSED(a_Size); } virtual void OnFileEnd(cHTTPFormParser &) override {} } ; //////////////////////////////////////////////////////////////////////////////// // cWebAdmin: cWebAdmin::cWebAdmin(void) : m_IsInitialized(false), m_IsRunning(false), m_TemplateScript("") { } cWebAdmin::~cWebAdmin() { ASSERT(!m_IsRunning); // Was the HTTP server stopped properly? } void cWebAdmin::AddPlugin(cWebPlugin * a_Plugin) { m_Plugins.remove(a_Plugin); m_Plugins.push_back(a_Plugin); } void cWebAdmin::RemovePlugin(cWebPlugin * a_Plugin) { m_Plugins.remove(a_Plugin); } bool cWebAdmin::Init(void) { if (!m_IniFile.ReadFile("webadmin.ini")) { LOGWARN("Regenerating webadmin.ini, all settings will be reset"); m_IniFile.AddHeaderComment(" This file controls the webadmin feature of Cuberite"); m_IniFile.AddHeaderComment(" Username format: [User:*username*]"); m_IniFile.AddHeaderComment(" Password format: Password=*password*; for example:"); m_IniFile.AddHeaderComment(" [User:admin]"); m_IniFile.AddHeaderComment(" Password=admin"); m_IniFile.SetValue("WebAdmin", "Ports", DEFAULT_WEBADMIN_PORTS); m_IniFile.WriteFile("webadmin.ini"); } if (!m_IniFile.GetValueSetB("WebAdmin", "Enabled", true)) { // WebAdmin is disabled, bail out faking a success return true; } LOGD("Initialising WebAdmin..."); // Initialize the WebAdmin template script and load the file m_TemplateScript.Create(); m_TemplateScript.RegisterAPILibs(); if (!m_TemplateScript.LoadFile(FILE_IO_PREFIX "webadmin/template.lua")) { LOGWARN("Could not load WebAdmin template \"%s\". WebAdmin disabled!", FILE_IO_PREFIX "webadmin/template.lua"); m_TemplateScript.Close(); m_HTTPServer.Stop(); return false; } // Load the login template, provide a fallback default if not found: if (!LoadLoginTemplate()) { LOGWARN("Could not load WebAdmin login template \"%s\", using fallback template.", FILE_IO_PREFIX "webadmin/login_template.html"); // Sets the fallback template: m_LoginTemplate = \ "

    Cuberite WebAdmin

    " \ "
    " \ "
    " \ "" \ "
    " \ "
    "; } // Read the ports to be used: // Note that historically the ports were stored in the "Port" and "PortsIPv6" values m_Ports = ReadUpgradeIniPorts(m_IniFile, "WebAdmin", "Ports", "Port", "PortsIPv6", DEFAULT_WEBADMIN_PORTS); if (!m_HTTPServer.Initialize()) { return false; } m_IsInitialized = true; m_IniFile.WriteFile("webadmin.ini"); return true; } bool cWebAdmin::Start(void) { if (!m_IsInitialized) { // Not initialized return false; } LOGD("Starting WebAdmin..."); m_IsRunning = m_HTTPServer.Start(*this, m_Ports); return m_IsRunning; } void cWebAdmin::Stop(void) { if (!m_IsRunning) { return; } LOGD("Stopping WebAdmin..."); m_HTTPServer.Stop(); m_IsRunning = false; } bool cWebAdmin::LoadLoginTemplate(void) { cFile File(FILE_IO_PREFIX "webadmin/login_template.html", cFile::fmRead); if (!File.IsOpen()) { return false; } AString TemplateContent; if (File.ReadRestOfFile(TemplateContent) == -1) { return false; } m_LoginTemplate = TemplateContent; return true; } void cWebAdmin::HandleWebadminRequest(cHTTPServerConnection & a_Connection, cHTTPIncomingRequest & a_Request) { if (!a_Request.HasAuth()) { a_Connection.SendNeedAuth("Cuberite WebAdmin"); return; } // Check auth: AString UserPassword = m_IniFile.GetValue("User:" + a_Request.GetAuthUsername(), "Password", ""); if ((UserPassword == "") || (a_Request.GetAuthPassword() != UserPassword)) { a_Connection.SendNeedAuth("Cuberite WebAdmin - bad username or password"); return; } // Check if the contents should be wrapped in the template: auto BareURL = a_Request.GetURLPath(); ASSERT(BareURL.length() > 0); bool ShouldWrapInTemplate = ((BareURL.length() > 1) && (BareURL[1] != '~')); // Retrieve the request data: auto Data = std::static_pointer_cast(a_Request.GetUserData()); if (Data == nullptr) { a_Connection.SendStatusAndReason(500, "Bad UserData"); return; } // Wrap it all up for the Lua call: AString Template; HTTPTemplateRequest TemplateRequest; TemplateRequest.Request.URL = a_Request.GetURL(); TemplateRequest.Request.Username = a_Request.GetAuthUsername(); TemplateRequest.Request.Method = a_Request.GetMethod(); TemplateRequest.Request.Path = BareURL.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; } // for itr - Data->m_Form[] // Parse the URL into individual params: const AString & URL = a_Request.GetURL(); size_t idxQM = URL.find('?'); if (idxQM != AString::npos) { cHTTPFormParser URLParams(cHTTPFormParser::fpkURL, URL.c_str() + idxQM + 1, URL.length() - idxQM - 1, *Data); URLParams.Finish(); for (cHTTPFormParser::const_iterator itr = URLParams.begin(), end = URLParams.end(); itr != end; ++itr) { TemplateRequest.Request.Params[itr->first] = itr->second; } // for itr - URLParams[] } } // 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(BareURL); 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 > NameList = WebPlugin->GetTabNames(); for (std::list< std::pair >::iterator Names = NameList.begin(); Names != NameList.end(); ++Names) { Menu += "
  • " + (*Names).first + "
  • "; } } 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(); } int MemUsageKiB = cRoot::GetPhysicalRAMUsage(); if (MemUsageKiB > 0) { ReplaceString(Template, "{MEM}", Printf("%.02f", static_cast(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}", "Cuberite"); 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()); a_Connection.FinishResponse(); } void cWebAdmin::HandleRootRequest(cHTTPServerConnection & a_Connection, cHTTPIncomingRequest & a_Request) { UNUSED(a_Request); cHTTPResponse Resp; Resp.SetContentType("text/html"); a_Connection.Send(Resp); a_Connection.Send(m_LoginTemplate); a_Connection.FinishResponse(); } void cWebAdmin::HandleFileRequest(cHTTPServerConnection & a_Connection, cHTTPIncomingRequest & a_Request) { AString FileURL = a_Request.GetURL(); std::replace(FileURL.begin(), FileURL.end(), '\\', '/'); // Remove all leading backslashes: if (FileURL[0] == '/') { size_t FirstCharToRead = FileURL.find_first_not_of('/'); if (FirstCharToRead != AString::npos) { FileURL = FileURL.substr(FirstCharToRead); } } // Remove all "../" strings: ReplaceString(FileURL, "../", ""); bool LoadedSuccessfull = false; AString Content = "

    404 Not Found

    "; AString Path = Printf(FILE_IO_PREFIX "webadmin/files/%s", FileURL.c_str()); if (cFile::IsFile(Path)) { cFile File(Path, cFile::fmRead); AString FileContent; if (File.IsOpen() && (File.ReadRestOfFile(FileContent) != -1)) { LoadedSuccessfull = true; Content = FileContent; } } // Find content type (The currently method is very bad. We should change it later) AString ContentType = "text/html"; size_t LastPointPosition = Path.find_last_of('.'); if (LoadedSuccessfull && (LastPointPosition != AString::npos) && (LastPointPosition < Path.length())) { AString FileExtension = Path.substr(LastPointPosition + 1); ContentType = GetContentTypeFromFileExt(FileExtension); } // Send the response: cHTTPResponse Resp; Resp.SetContentType(ContentType); a_Connection.Send(Resp); a_Connection.Send(Content); a_Connection.FinishResponse(); } AString cWebAdmin::GetContentTypeFromFileExt(const AString & a_FileExtension) { static bool IsInitialized = false; static std::map ContentTypeMap; if (!IsInitialized) { // Initialize the ContentTypeMap: ContentTypeMap["png"] = "image/png"; ContentTypeMap["fif"] = "image/fif"; ContentTypeMap["gif"] = "image/gif"; ContentTypeMap["jpeg"] = "image/jpeg"; ContentTypeMap["jpg"] = "image/jpeg"; ContentTypeMap["jpe"] = "image/jpeg"; ContentTypeMap["tiff"] = "image/tiff"; ContentTypeMap["ico"] = "image/ico"; ContentTypeMap["csv"] = "image/comma-separated-values"; ContentTypeMap["css"] = "text/css"; ContentTypeMap["js"] = "text/javascript"; ContentTypeMap["txt"] = "text/plain"; ContentTypeMap["rtx"] = "text/richtext"; ContentTypeMap["xml"] = "text/xml"; } AString FileExtension = StrToLower(a_FileExtension); if (ContentTypeMap.find(a_FileExtension) == ContentTypeMap.end()) { return "text/html"; } return ContentTypeMap[FileExtension]; } sWebAdminPage cWebAdmin::GetPage(const HTTPRequest & a_Request) { sWebAdminPage Page; AStringVector Split = StringSplit(a_Request.Path, "/"); // Find the plugin that corresponds to the requested path AString FoundPlugin; if (Split.size() > 1) { for (PluginList::iterator itr = m_Plugins.begin(); itr != m_Plugins.end(); ++itr) { if ((*itr)->GetWebTitle() == Split[1]) { Page.Content = (*itr)->HandleWebRequest(a_Request); cWebPlugin * WebPlugin = *itr; FoundPlugin = WebPlugin->GetWebTitle(); AString TabName = WebPlugin->GetTabNameForRequest(a_Request).first; Page.PluginName = FoundPlugin; Page.TabName = TabName; break; } } } // Return the page contents return Page; } AString cWebAdmin::GetDefaultPage(void) { AString Content; Content += "

    Server Name:

    "; Content += "

    " + AString( cRoot::Get()->GetServer()->GetServerID()) + "

    "; // Display a list of all plugins: Content += "

    Plugins:

      "; struct cPluginCallback: public cPluginManager::cPluginCallback { AString & m_Content; cPluginCallback(AString & a_Content): m_Content(a_Content) { } virtual bool Item(cPlugin * a_Plugin) override { if (a_Plugin->IsLoaded()) { AppendPrintf(m_Content, "
    • %s V.%i
    • ", a_Plugin->GetName().c_str(), a_Plugin->GetVersion()); } return false; } } Callback(Content); cPluginManager::Get()->ForEachPlugin(Callback); Content += "
    "; // Display a list of all players: Content += "

    Players:

      "; cPlayerAccum PlayerAccum; cWorld * World = cRoot::Get()->GetDefaultWorld(); // TODO - Create a list of worlds and players if (World != nullptr) { World->ForEachPlayer(PlayerAccum); Content.append(PlayerAccum.m_Contents); } Content += "

    "; return Content; } AString cWebAdmin::GetBaseURL(const AString & a_URL) { return GetBaseURL(StringSplit(a_URL, "/")); } AString cWebAdmin::GetHTMLEscapedString(const AString & a_Input) { AString dst; dst.reserve(a_Input.length()); // Loop over input and substitute HTML characters for their alternatives: size_t len = a_Input.length(); for (size_t i = 0; i < len; i++) { switch (a_Input[i]) { case '&': dst.append("&"); break; case '\'': dst.append("'"); break; case '"': dst.append("""); break; case '<': dst.append("<"); break; case '>': dst.append(">"); break; default: { dst.push_back(a_Input[i]); break; } } // switch (a_Input[i]) } // for i - a_Input[] return dst; } AString cWebAdmin::GetURLEncodedString(const AString & a_Input) { // Translation table from nibble to hex: static const char Hex[] = "0123456789abcdef"; // Preallocate the output to match input: AString dst; size_t len = a_Input.length(); dst.reserve(len); // Loop over input and substitute whatever is needed: for (size_t i = 0; i < len; i++) { char ch = a_Input[i]; if (isalnum(ch) || (ch == '-') || (ch == '_') || (ch == '.') || (ch == '~')) { dst.push_back(ch); } else { dst.push_back('%'); dst.push_back(Hex[(ch >> 4) & 0x0f]); dst.push_back(Hex[ch & 0x0f]); } } // for i - a_Input[] return dst; } AString cWebAdmin::GetBaseURL(const AStringVector & a_URLSplit) { AString BaseURL = "./"; if (a_URLSplit.size() > 1) { for (unsigned int i = 0; i < a_URLSplit.size(); i++) { BaseURL += "../"; } BaseURL += "webadmin/"; } return BaseURL; } void cWebAdmin::OnRequestBegun(cHTTPServerConnection & a_Connection, cHTTPIncomingRequest & a_Request) { UNUSED(a_Connection); const AString & URL = a_Request.GetURL(); if ( (strncmp(URL.c_str(), "/webadmin", 9) == 0) || (strncmp(URL.c_str(), "/~webadmin", 10) == 0) ) { a_Request.SetUserData(std::make_shared(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(cHTTPServerConnection & a_Connection, cHTTPIncomingRequest & a_Request, const char * a_Data, size_t a_Size) { UNUSED(a_Connection); auto Data = std::static_pointer_cast(a_Request.GetUserData()); if (Data == nullptr) { return; } Data->m_Form.Parse(a_Data, a_Size); } void cWebAdmin::OnRequestFinished(cHTTPServerConnection & a_Connection, cHTTPIncomingRequest & 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 { HandleFileRequest(a_Connection, a_Request); } }