/* * Copyright (C) 2004-2025 ZNC, see the NOTICE file for details. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include #include #include #include #include using std::pair; using std::vector; /// @todo Do we want to make this a configure option? #define _SKINDIR_ _DATADIR_ "/webskins" const unsigned int CWebSock::m_uiMaxSessions = 5; // We need this class to make sure the contained maps and their content is // destroyed in the order that we want. struct CSessionManager { // Sessions are valid for a day, (24h, ...) CSessionManager() : m_mspSessions(24 * 60 * 60 * 1000), m_mIPSessions() {} ~CSessionManager() { // Make sure all sessions are destroyed before any of our maps // are destroyed m_mspSessions.Clear(); } CWebSessionMap m_mspSessions; std::multimap m_mIPSessions; }; typedef std::multimap::iterator mIPSessionsIterator; static CSessionManager Sessions; class CWebAuth : public CAuthBase { public: CWebAuth(CWebSock* pWebSock, const CString& sUsername, const CString& sPassword, bool bBasic); ~CWebAuth() override {} CWebAuth(const CWebAuth&) = delete; CWebAuth& operator=(const CWebAuth&) = delete; void SetWebSock(CWebSock* pWebSock) { m_pWebSock = pWebSock; } void AcceptedLogin(CUser& User) override; void RefusedLogin(const CString& sReason) override; void Invalidate() override; private: protected: CWebSock* m_pWebSock; bool m_bBasic; }; void CWebSock::FinishUserSessions(const CUser& User) { Sessions.m_mspSessions.FinishUserSessions(User); } CWebSession::~CWebSession() { // Find our entry in mIPSessions pair p = Sessions.m_mIPSessions.equal_range(m_sIP); mIPSessionsIterator it = p.first; mIPSessionsIterator end = p.second; while (it != end) { if (it->second == this) { Sessions.m_mIPSessions.erase(it++); } else { ++it; } } } CZNCTagHandler::CZNCTagHandler(CWebSock& WebSock) : CTemplateTagHandler(), m_WebSock(WebSock) {} bool CZNCTagHandler::HandleTag(CTemplate& Tmpl, const CString& sName, const CString& sArgs, CString& sOutput) { if (sName.Equals("URLPARAM")) { // sOutput = CZNC::Get() sOutput = m_WebSock.GetParam(sArgs.Token(0), false); return true; } return false; } CWebSession::CWebSession(const CString& sId, const CString& sIP) : m_sId(sId), m_sIP(sIP), m_pUser(nullptr), m_vsErrorMsgs(), m_vsSuccessMsgs(), m_tmLastActive() { Sessions.m_mIPSessions.insert(make_pair(sIP, this)); UpdateLastActive(); } void CWebSession::UpdateLastActive() { time(&m_tmLastActive); } bool CWebSession::IsAdmin() const { return IsLoggedIn() && m_pUser->IsAdmin(); } CWebAuth::CWebAuth(CWebSock* pWebSock, const CString& sUsername, const CString& sPassword, bool bBasic) : CAuthBase(sUsername, sPassword, pWebSock), m_pWebSock(pWebSock), m_bBasic(bBasic) {} void CWebSession::ClearMessageLoops() { m_vsErrorMsgs.clear(); m_vsSuccessMsgs.clear(); } void CWebSession::FillMessageLoops(CTemplate& Tmpl) { for (const CString& sMessage : m_vsErrorMsgs) { CTemplate& Row = Tmpl.AddRow("ErrorLoop"); Row["Message"] = sMessage; } for (const CString& sMessage : m_vsSuccessMsgs) { CTemplate& Row = Tmpl.AddRow("SuccessLoop"); Row["Message"] = sMessage; } } size_t CWebSession::AddError(const CString& sMessage) { m_vsErrorMsgs.push_back(sMessage); return m_vsErrorMsgs.size(); } size_t CWebSession::AddSuccess(const CString& sMessage) { m_vsSuccessMsgs.push_back(sMessage); return m_vsSuccessMsgs.size(); } void CWebSessionMap::FinishUserSessions(const CUser& User) { iterator it = m_mItems.begin(); while (it != m_mItems.end()) { if (it->second.second->GetUser() == &User) { m_mItems.erase(it++); } else { ++it; } } } void CWebAuth::AcceptedLogin(CUser& User) { if (m_pWebSock) { std::shared_ptr spSession = m_pWebSock->GetSession(); spSession->SetUser(&User); m_pWebSock->SetLoggedIn(true); m_pWebSock->UnPauseRead(); if (m_bBasic) { m_pWebSock->ReadLine(""); } else { m_pWebSock->Redirect("/?cookie_check=true"); } DEBUG("Successful login attempt ==> USER [" + User.GetUsername() + "] ==> SESSION [" + spSession->GetId() + "]"); } } void CWebAuth::RefusedLogin(const CString& sReason) { if (m_pWebSock) { std::shared_ptr spSession = m_pWebSock->GetSession(); spSession->AddError("Invalid login!"); spSession->SetUser(nullptr); m_pWebSock->SetLoggedIn(false); m_pWebSock->UnPauseRead(); if (m_bBasic) { m_pWebSock->AddHeader("WWW-Authenticate", "Basic realm=\"ZNC\""); m_pWebSock->CHTTPSock::PrintErrorPage( 401, "Unauthorized", "HTTP Basic authentication attempted with invalid credentials"); // Why CWebSock makes this function protected?.. } else { m_pWebSock->Redirect("/?cookie_check=true"); } DEBUG("UNSUCCESSFUL login attempt ==> REASON [" + sReason + "] ==> SESSION [" + spSession->GetId() + "]"); } } void CWebAuth::Invalidate() { CAuthBase::Invalidate(); m_pWebSock = nullptr; } CWebSock::CWebSock(const CString& sURIPrefix) : CHTTPSock(nullptr, sURIPrefix), m_bPathsSet(false), m_Template(), m_spAuth(), m_sModName(""), m_sPath(""), m_sPage(""), m_spSession() { m_Template.AddTagHandler(std::make_shared(*this)); } CWebSock::~CWebSock() { if (m_spAuth) { m_spAuth->Invalidate(); } // we have to account for traffic here because CSocket does // not have a valid CModule* pointer. CUser* pUser = GetSession()->GetUser(); if (pUser) { pUser->AddBytesWritten(GetBytesWritten()); pUser->AddBytesRead(GetBytesRead()); } else { CZNC::Get().AddBytesWritten(GetBytesWritten()); CZNC::Get().AddBytesRead(GetBytesRead()); } // bytes have been accounted for, so make sure they don't get again: ResetBytesWritten(); ResetBytesRead(); } void CWebSock::GetAvailSkins(VCString& vRet) const { vRet.clear(); CString sRoot(GetSkinPath("_default_")); sRoot.TrimRight("/"); sRoot.TrimRight("_default_"); sRoot.TrimRight("/"); if (!sRoot.empty()) { sRoot += "/"; } if (!sRoot.empty() && CFile::IsDir(sRoot)) { CDir Dir(sRoot); for (const CFile* pSubDir : Dir) { if (pSubDir->IsDir() && pSubDir->GetShortName() == "_default_") { vRet.push_back(pSubDir->GetShortName()); break; } } for (const CFile* pSubDir : Dir) { if (pSubDir->IsDir() && pSubDir->GetShortName() != "_default_" && pSubDir->GetShortName() != ".svn") { vRet.push_back(pSubDir->GetShortName()); } } } } VCString CWebSock::GetDirs(CModule* pModule, bool bIsTemplate) { CString sHomeSkinsDir(CZNC::Get().GetZNCPath() + "/webskins/"); CString sSkinName(GetSkinName()); VCString vsResult; // Module specific paths if (pModule) { const CString& sModName(pModule->GetModName()); // 1. ~/.znc/webskins//mods// // if (!sSkinName.empty()) { vsResult.push_back(GetSkinPath(sSkinName) + "/mods/" + sModName + "/"); } // 2. ~/.znc/webskins/_default_/mods// // vsResult.push_back(GetSkinPath("_default_") + "/mods/" + sModName + "/"); // 3. ./modules//tmpl/ // vsResult.push_back(pModule->GetModDataDir() + "/tmpl/"); // 4. ~/.znc/webskins//mods// // if (!sSkinName.empty()) { vsResult.push_back(GetSkinPath(sSkinName) + "/mods/" + sModName + "/"); } // 5. ~/.znc/webskins/_default_/mods// // vsResult.push_back(GetSkinPath("_default_") + "/mods/" + sModName + "/"); } // 6. ~/.znc/webskins// // if (!sSkinName.empty()) { vsResult.push_back(GetSkinPath(sSkinName) + CString(bIsTemplate ? "/tmpl/" : "/")); } // 7. ~/.znc/webskins/_default_/ // vsResult.push_back(GetSkinPath("_default_") + CString(bIsTemplate ? "/tmpl/" : "/")); return vsResult; } CString CWebSock::FindTmpl(CModule* pModule, const CString& sName) { VCString vsDirs = GetDirs(pModule, true); CString sFile = pModule->GetModName() + "_" + sName; for (const CString& sDir : vsDirs) { if (CFile::Exists(CDir::ChangeDir(sDir, sFile))) { m_Template.AppendPath(sDir); return sFile; } } return sName; } void CWebSock::SetPaths(CModule* pModule, bool bIsTemplate) { m_Template.ClearPaths(); VCString vsDirs = GetDirs(pModule, bIsTemplate); for (const CString& sDir : vsDirs) { m_Template.AppendPath(sDir); } m_bPathsSet = true; } void CWebSock::SetVars() { m_Template["SessionUser"] = GetUser(); m_Template["SessionIP"] = GetRemoteIP(); m_Template["Tag"] = CZNC::GetTag(GetSession()->GetUser() != nullptr, true); m_Template["Version"] = CZNC::GetVersion(); m_Template["SkinName"] = GetSkinName(); m_Template["_CSRF_Check"] = GetCSRFCheck(); m_Template["URIPrefix"] = GetURIPrefix(); if (GetSession()->IsAdmin()) { m_Template["IsAdmin"] = "true"; } GetSession()->FillMessageLoops(m_Template); GetSession()->ClearMessageLoops(); // Global Mods CModules& vgMods = CZNC::Get().GetModules(); for (CModule* pgMod : vgMods) { AddModLoop("GlobalModLoop", *pgMod); } // User Mods if (IsLoggedIn()) { CModules& vMods = GetSession()->GetUser()->GetModules(); for (CModule* pMod : vMods) { AddModLoop("UserModLoop", *pMod); } vector vNetworks = GetSession()->GetUser()->GetNetworks(); for (CIRCNetwork* pNetwork : vNetworks) { CModules& vnMods = pNetwork->GetModules(); CTemplate& Row = m_Template.AddRow("NetworkModLoop"); Row["NetworkName"] = pNetwork->GetName(); for (CModule* pnMod : vnMods) { AddModLoop("ModLoop", *pnMod, &Row); } } } if (IsLoggedIn()) { m_Template["LoggedIn"] = "true"; } } bool CWebSock::AddModLoop(const CString& sLoopName, CModule& Module, CTemplate* pTemplate) { if (!pTemplate) { pTemplate = &m_Template; } CString sTitle(Module.GetWebMenuTitle()); if (!sTitle.empty() && (IsLoggedIn() || (!Module.WebRequiresLogin() && !Module.WebRequiresAdmin())) && (GetSession()->IsAdmin() || !Module.WebRequiresAdmin())) { CTemplate& Row = pTemplate->AddRow(sLoopName); bool bActiveModule = false; Row["ModName"] = Module.GetModName(); Row["ModPath"] = Module.GetWebPath(); Row["Title"] = sTitle; if (m_sModName == Module.GetModName()) { CString sModuleType = GetPath().Token(1, false, "/"); if (sModuleType == "global" && Module.GetType() == CModInfo::GlobalModule) { bActiveModule = true; } else if (sModuleType == "user" && Module.GetType() == CModInfo::UserModule) { bActiveModule = true; } else if (sModuleType == "network" && Module.GetType() == CModInfo::NetworkModule) { CIRCNetwork* Network = Module.GetNetwork(); if (Network) { CString sNetworkName = GetPath().Token(2, false, "/"); if (sNetworkName == Network->GetName()) { bActiveModule = true; } } else { bActiveModule = true; } } } if (bActiveModule) { Row["Active"] = "true"; } if (Module.GetUser()) { Row["Username"] = Module.GetUser()->GetUsername(); } VWebSubPages& vSubPages = Module.GetSubPages(); for (TWebSubPage& SubPage : vSubPages) { // bActive is whether or not the current url matches this subpage // (params will be checked below) bool bActive = (m_sModName == Module.GetModName() && m_sPage == SubPage->GetName() && bActiveModule); if (SubPage->RequiresAdmin() && !GetSession()->IsAdmin()) { // Don't add admin-only subpages to requests from non-admin // users continue; } CTemplate& SubRow = Row.AddRow("SubPageLoop"); SubRow["ModName"] = Module.GetModName(); SubRow["ModPath"] = Module.GetWebPath(); SubRow["PageName"] = SubPage->GetName(); SubRow["Title"] = SubPage->GetTitle().empty() ? SubPage->GetName() : SubPage->GetTitle(); CString& sParams = SubRow["Params"]; const VPair& vParams = SubPage->GetParams(); for (const pair& ssNV : vParams) { if (!sParams.empty()) { sParams += "&"; } if (!ssNV.first.empty()) { if (!ssNV.second.empty()) { sParams += ssNV.first.Escape_n(CString::EURL); sParams += "="; sParams += ssNV.second.Escape_n(CString::EURL); } if (bActive && GetParam(ssNV.first, false) != ssNV.second) { bActive = false; } } } if (bActive) { SubRow["Active"] = "true"; } } return true; } return false; } CWebSock::EPageReqResult CWebSock::PrintStaticFile(const CString& sPath, CString& sPageRet, CModule* pModule) { SetPaths(pModule); CString sFile = m_Template.ExpandFile(sPath.TrimLeft_n("/")); DEBUG("About to print [" + sFile + "]"); // Either PrintFile() fails and sends an error page or it succeeds and // sends a result. In both cases we don't have anything more to do. PrintFile(sFile); return PAGE_DONE; } CWebSock::EPageReqResult CWebSock::PrintTemplate(const CString& sPageName, CString& sPageRet, CModule* pModule) { SetVars(); m_Template["PageName"] = sPageName; if (pModule) { m_Template["ModName"] = pModule->GetModName(); if (m_Template.find("Title") == m_Template.end()) { m_Template["Title"] = pModule->GetWebMenuTitle(); } std::vector* breadcrumbs = m_Template.GetLoop("BreadCrumbs"); if (breadcrumbs->size() == 1 && m_Template["Title"] != pModule->GetModName()) { // Module didn't add its own breadcrumbs, so add a generic one... // But it'll be useless if it's the same as module name CTemplate& bread = m_Template.AddRow("BreadCrumbs"); bread["Text"] = m_Template["Title"]; } } if (!m_bPathsSet) { SetPaths(pModule, true); } if (m_Template.GetFileName().empty() && !m_Template.SetFile(sPageName + ".tmpl")) { return PAGE_NOTFOUND; } if (m_Template.PrintString(sPageRet)) { return PAGE_PRINT; } else { return PAGE_NOTFOUND; } } CString CWebSock::GetSkinPath(const CString& sSkinName) { const CString sSkin = sSkinName.Replace_n("/", "_").Replace_n(".", "_"); CString sRet = CZNC::Get().GetZNCPath() + "/webskins/" + sSkin; if (!CFile::IsDir(sRet)) { sRet = CZNC::Get().GetCurPath() + "/webskins/" + sSkin; if (!CFile::IsDir(sRet)) { sRet = CString(_SKINDIR_) + "/" + sSkin; } } return sRet + "/"; } bool CWebSock::ForceLogin() { if (GetSession()->IsLoggedIn()) { return true; } GetSession()->AddError("You must login to view that page"); Redirect("/"); return false; } CString CWebSock::GetRequestCookie(const CString& sKey) { const CString sPrefixedKey = CString(GetLocalPort()) + "-" + sKey; CString sRet; if (!m_sModName.empty()) { sRet = CHTTPSock::GetRequestCookie("Mod-" + m_sModName + "-" + sPrefixedKey); } if (sRet.empty()) { return CHTTPSock::GetRequestCookie(sPrefixedKey); } return sRet; } bool CWebSock::SendCookie(const CString& sKey, const CString& sValue) { const CString sPrefixedKey = CString(GetLocalPort()) + "-" + sKey; if (!m_sModName.empty()) { return CHTTPSock::SendCookie("Mod-" + m_sModName + "-" + sPrefixedKey, sValue); } return CHTTPSock::SendCookie(sPrefixedKey, sValue); } void CWebSock::OnPageRequest(const CString& sURI) { CString sPageRet; EPageReqResult eRet = OnPageRequestInternal(sURI, sPageRet); switch (eRet) { case PAGE_PRINT: PrintPage(sPageRet); break; case PAGE_DEFERRED: // Something else will later call Close() break; case PAGE_DONE: // Redirect or something like that, it's done, just make sure // the connection will be closed Close(CLT_AFTERWRITE); break; case PAGE_NOTFOUND: default: PrintNotFound(); break; } } CWebSock::EPageReqResult CWebSock::OnPageRequestInternal(const CString& sURI, CString& sPageRet) { // Check that their session really belongs to their IP address. IP-based // authentication is bad, but here it's just an extra layer that makes // stealing cookies harder to pull off. // // When their IP is wrong, we give them an invalid cookie. This makes // sure that they will get a new cookie on their next request. if (CZNC::Get().GetProtectWebSessions() && GetSession()->GetIP() != GetRemoteIP()) { DEBUG("Expected IP: " << GetSession()->GetIP()); DEBUG("Remote IP: " << GetRemoteIP()); SendCookie("SessionId", "WRONG_IP_FOR_SESSION"); PrintErrorPage(403, "Access denied", "This session does not belong to your IP."); return PAGE_DONE; } // For pages *not provided* by modules, a CSRF check is performed which involves: // Ensure that they really POSTed from one our forms by checking if they // know the "secret" CSRF check value. Don't do this for login since // CSRF against the login form makes no sense and the login form does a // cookies-enabled check which would break otherwise. // Don't do this, if user authenticated using http-basic auth, because: // 1. they obviously know the password, // 2. it's easier to automate some tasks e.g. user creation, without need to // care about cookies and CSRF if (IsPost() && !m_bBasicAuth && !sURI.StartsWith("/mods/") && !ValidateCSRFCheck(sURI)) { DEBUG("Expected _CSRF_Check: " << GetCSRFCheck()); DEBUG("Actual _CSRF_Check: " << GetParam("_CSRF_Check")); PrintErrorPage( 403, "Access denied", "POST requests need to send " "a secret token to prevent cross-site request forgery attacks."); return PAGE_DONE; } SendCookie("SessionId", GetSession()->GetId()); if (GetSession()->IsLoggedIn()) { m_sUser = GetSession()->GetUser()->GetUsername(); m_bLoggedIn = true; } CLanguageScope user_language( m_bLoggedIn ? GetSession()->GetUser()->GetLanguage() : ""); // Handle the static pages that don't require a login if (sURI == "/") { if (!m_bLoggedIn && GetParam("cookie_check", false).ToBool() && GetRequestCookie("SessionId").empty()) { GetSession()->AddError( "Your browser does not have cookies enabled for this site!"); } return PrintTemplate("index", sPageRet); } else if (sURI == "/favicon.ico") { return PrintStaticFile("/pub/favicon.ico", sPageRet); } else if (sURI == "/robots.txt") { return PrintStaticFile("/pub/robots.txt", sPageRet); } else if (sURI == "/logout") { GetSession()->SetUser(nullptr); SetLoggedIn(false); Redirect("/"); // We already sent a reply return PAGE_DONE; } else if (sURI == "/login") { if (GetParam("submitted").ToBool()) { m_sUser = GetParam("user"); m_sPass = GetParam("pass"); m_bLoggedIn = OnLogin(m_sUser, m_sPass, false); // AcceptedLogin()/RefusedLogin() will call Redirect() return PAGE_DEFERRED; } Redirect("/"); // the login form is here return PAGE_DONE; } else if (sURI.StartsWith("/pub/")) { return PrintStaticFile(sURI, sPageRet); } else if (sURI.StartsWith("/skinfiles/")) { CString sSkinName = sURI.substr(11); CString::size_type uPathStart = sSkinName.find("/"); if (uPathStart != CString::npos) { CString sFilePath = sSkinName.substr(uPathStart + 1); sSkinName.erase(uPathStart); m_Template.ClearPaths(); m_Template.AppendPath(GetSkinPath(sSkinName) + "pub"); if (PrintFile(m_Template.ExpandFile(sFilePath))) { return PAGE_DONE; } else { return PAGE_NOTFOUND; } } return PAGE_NOTFOUND; } else if (sURI.StartsWith("/mods/") || sURI.StartsWith("/modfiles/")) { // Make sure modules are treated as directories if (!sURI.EndsWith("/") && !sURI.Contains(".") && !sURI.TrimLeft_n("/mods/").TrimLeft_n("/").Contains("/")) { Redirect(sURI + "/"); return PAGE_DONE; } // The URI looks like: // /mods/[type]/([network]/)?[module][/page][?arg1=val1&arg2=val2...] m_sPath = GetPath().TrimLeft_n("/"); m_sPath.TrimPrefix("mods/"); m_sPath.TrimPrefix("modfiles/"); CString sType = m_sPath.Token(0, false, "/"); m_sPath = m_sPath.Token(1, true, "/"); CModInfo::EModuleType eModType; if (sType.Equals("global")) { eModType = CModInfo::GlobalModule; } else if (sType.Equals("user")) { eModType = CModInfo::UserModule; } else if (sType.Equals("network")) { eModType = CModInfo::NetworkModule; } else { PrintErrorPage(403, "Forbidden", "Unknown module type [" + sType + "]"); return PAGE_DONE; } if ((eModType != CModInfo::GlobalModule) && !ForceLogin()) { // Make sure we have a valid user return PAGE_DONE; } CIRCNetwork* pNetwork = nullptr; if (eModType == CModInfo::NetworkModule) { CString sNetwork = m_sPath.Token(0, false, "/"); m_sPath = m_sPath.Token(1, true, "/"); pNetwork = GetSession()->GetUser()->FindNetwork(sNetwork); if (!pNetwork) { PrintErrorPage(404, "Not Found", "Network [" + sNetwork + "] not found."); return PAGE_DONE; } } m_sModName = m_sPath.Token(0, false, "/"); m_sPage = m_sPath.Token(1, true, "/"); if (m_sPage.empty()) { m_sPage = "index"; } DEBUG("Path [" + m_sPath + "], Module [" + m_sModName + "], Page [" + m_sPage + "]"); CModule* pModule = nullptr; switch (eModType) { case CModInfo::GlobalModule: pModule = CZNC::Get().GetModules().FindModule(m_sModName); break; case CModInfo::UserModule: pModule = GetSession()->GetUser()->GetModules().FindModule( m_sModName); break; case CModInfo::NetworkModule: pModule = pNetwork->GetModules().FindModule(m_sModName); break; } if (!pModule) return PAGE_NOTFOUND; // Pass CSRF check to module. // Note that the normal CSRF checks are not applied to /mods/ URLs. if (IsPost() && !m_bBasicAuth && !pModule->ValidateWebRequestCSRFCheck(*this, m_sPage)) { DEBUG("Expected _CSRF_Check: " << GetCSRFCheck()); DEBUG("Actual _CSRF_Check: " << GetParam("_CSRF_Check")); PrintErrorPage( 403, "Access denied", "POST requests need to send " "a secret token to prevent cross-site request forgery attacks."); return PAGE_DONE; } m_Template["ModPath"] = pModule->GetWebPath(); m_Template["ModFilesPath"] = pModule->GetWebFilesPath(); if (pModule->WebRequiresLogin() && !ForceLogin()) { return PAGE_PRINT; } else if (pModule->WebRequiresAdmin() && !GetSession()->IsAdmin()) { PrintErrorPage(403, "Forbidden", "You need to be an admin to access this module"); return PAGE_DONE; } else if (pModule->GetType() != CModInfo::GlobalModule && pModule->GetUser() != GetSession()->GetUser()) { PrintErrorPage(403, "Forbidden", "You must login as " + pModule->GetUser()->GetUsername() + " in order to view this page"); return PAGE_DONE; } else if (pModule->OnWebPreRequest(*this, m_sPage)) { return PAGE_DEFERRED; } VWebSubPages& vSubPages = pModule->GetSubPages(); for (TWebSubPage& SubPage : vSubPages) { bool bActive = (m_sModName == pModule->GetModName() && m_sPage == SubPage->GetName()); if (bActive && SubPage->RequiresAdmin() && !GetSession()->IsAdmin()) { PrintErrorPage(403, "Forbidden", "You need to be an admin to access this page"); return PAGE_DONE; } } if (pModule && pModule->GetType() != CModInfo::GlobalModule && (!IsLoggedIn() || pModule->GetUser() != GetSession()->GetUser())) { AddModLoop("UserModLoop", *pModule); } if (sURI.StartsWith("/modfiles/")) { m_Template.AppendPath(GetSkinPath(GetSkinName()) + "/mods/" + m_sModName + "/files/"); m_Template.AppendPath(pModule->GetModDataDir() + "/files/"); if (PrintFile(m_Template.ExpandFile(m_sPage.TrimLeft_n("/")))) { return PAGE_PRINT; } else { return PAGE_NOTFOUND; } } else { SetPaths(pModule, true); CTemplate& breadModule = m_Template.AddRow("BreadCrumbs"); breadModule["Text"] = pModule->GetModName(); breadModule["URL"] = pModule->GetWebPath(); /* if a module returns false from OnWebRequest, it does not want the template to be printed, usually because it did a redirect. */ if (pModule->OnWebRequest(*this, m_sPage, m_Template)) { // If they already sent a reply, let's assume // they did what they wanted to do. if (SentHeader()) { return PAGE_DONE; } return PrintTemplate(m_sPage, sPageRet, pModule); } if (!SentHeader()) { PrintErrorPage( 404, "Not Implemented", "The requested module does not acknowledge web requests"); } return PAGE_DONE; } } else { CString sPage(sURI.Trim_n("/")); if (sPage.length() < 32) { for (unsigned int a = 0; a < sPage.length(); a++) { unsigned char c = sPage[a]; if ((c < '0' || c > '9') && (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && c != '_') { return PAGE_NOTFOUND; } } return PrintTemplate(sPage, sPageRet); } } return PAGE_NOTFOUND; } void CWebSock::PrintErrorPage(const CString& sMessage) { m_Template.SetFile("Error.tmpl"); m_Template["Action"] = "error"; m_Template["Title"] = "Error"; m_Template["Error"] = sMessage; } static inline bool compareLastActive( const std::pair& first, const std::pair& second) { return first.second->GetLastActive() < second.second->GetLastActive(); } std::shared_ptr CWebSock::GetSession() { if (m_spSession) { return m_spSession; } const CString sCookieSessionId = GetRequestCookie("SessionId"); std::shared_ptr* pSession = Sessions.m_mspSessions.GetItem(sCookieSessionId); if (pSession != nullptr) { // Refresh the timeout Sessions.m_mspSessions.AddItem((*pSession)->GetId(), *pSession); (*pSession)->UpdateLastActive(); m_spSession = *pSession; DEBUG("Found existing session from cookie: [" + sCookieSessionId + "] IsLoggedIn(" + CString((*pSession)->IsLoggedIn() ? "true, " + ((*pSession)->GetUser()->GetUsername()) : "false") + ")"); return *pSession; } if (Sessions.m_mIPSessions.count(GetRemoteIP()) > m_uiMaxSessions) { pair p = Sessions.m_mIPSessions.equal_range(GetRemoteIP()); mIPSessionsIterator it = std::min_element(p.first, p.second, compareLastActive); DEBUG("Remote IP: " << GetRemoteIP() << "; discarding session [" << it->second->GetId() << "]"); Sessions.m_mspSessions.RemItem(it->second->GetId()); } CString sSessionID; do { sSessionID = CString::RandomString(32); sSessionID += ":" + GetRemoteIP() + ":" + CString(GetRemotePort()); sSessionID += ":" + GetLocalIP() + ":" + CString(GetLocalPort()); sSessionID += ":" + CString(time(nullptr)); sSessionID = sSessionID.SHA256(); DEBUG("Auto generated session: [" + sSessionID + "]"); } while (Sessions.m_mspSessions.HasItem(sSessionID)); std::shared_ptr spSession( new CWebSession(sSessionID, GetRemoteIP())); Sessions.m_mspSessions.AddItem(spSession->GetId(), spSession); m_spSession = spSession; return spSession; } CString CWebSock::GetCSRFCheck() { std::shared_ptr pSession = GetSession(); return pSession->GetId().MD5(); } bool CWebSock::ValidateCSRFCheck(const CString& sURI) { return sURI == "/login" || GetParam("_CSRF_Check") == GetCSRFCheck(); } bool CWebSock::OnLogin(const CString& sUser, const CString& sPass, bool bBasic) { DEBUG("=================== CWebSock::OnLogin(), basic auth? " << std::boolalpha << bBasic); m_spAuth = std::make_shared(this, sUser, sPass, bBasic); // Some authentication module could need some time, block this socket // until then. CWebAuth will UnPauseRead(). PauseRead(); CZNC::Get().AuthUser(m_spAuth); // If CWebAuth already set this, don't change it. return IsLoggedIn(); } Csock* CWebSock::GetSockObj(const CString& sHost, unsigned short uPort) { // All listening is done by CListener, thus CWebSock should never have // to listen, but since GetSockObj() is pure virtual... DEBUG("CWebSock::GetSockObj() called - this should never happen!"); return nullptr; } CString CWebSock::GetSkinName() { std::shared_ptr spSession = GetSession(); if (spSession->IsLoggedIn() && !spSession->GetUser()->GetSkinName().empty()) { return spSession->GetUser()->GetSkinName(); } return CZNC::Get().GetSkinName(); }