Files
znc/src/WebModules.cpp
2025-02-07 23:23:45 +00:00

1024 lines
33 KiB
C++

/*
* 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 <znc/WebModules.h>
#include <znc/FileUtils.h>
#include <znc/User.h>
#include <znc/IRCNetwork.h>
#include <znc/znc.h>
#include <time.h>
#include <algorithm>
#include <sstream>
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<CString, CWebSession*> m_mIPSessions;
};
typedef std::multimap<CString, CWebSession*>::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<mIPSessionsIterator, mIPSessionsIterator> 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<CWebSession> 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<CWebSession> 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<CZNCTagHandler>(*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/<user_skin_setting>/mods/<mod_name>/
//
if (!sSkinName.empty()) {
vsResult.push_back(GetSkinPath(sSkinName) + "/mods/" + sModName +
"/");
}
// 2. ~/.znc/webskins/_default_/mods/<mod_name>/
//
vsResult.push_back(GetSkinPath("_default_") + "/mods/" + sModName +
"/");
// 3. ./modules/<mod_name>/tmpl/
//
vsResult.push_back(pModule->GetModDataDir() + "/tmpl/");
// 4. ~/.znc/webskins/<user_skin_setting>/mods/<mod_name>/
//
if (!sSkinName.empty()) {
vsResult.push_back(GetSkinPath(sSkinName) + "/mods/" + sModName +
"/");
}
// 5. ~/.znc/webskins/_default_/mods/<mod_name>/
//
vsResult.push_back(GetSkinPath("_default_") + "/mods/" + sModName +
"/");
}
// 6. ~/.znc/webskins/<user_skin_setting>/
//
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<CIRCNetwork*> 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<CString, CString>& 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<CTemplate*>* 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<const CString, CWebSession*>& first,
const std::pair<const CString, CWebSession*>& second) {
return first.second->GetLastActive() < second.second->GetLastActive();
}
std::shared_ptr<CWebSession> CWebSock::GetSession() {
if (m_spSession) {
return m_spSession;
}
const CString sCookieSessionId = GetRequestCookie("SessionId");
std::shared_ptr<CWebSession>* 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<mIPSessionsIterator, mIPSessionsIterator> 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<CWebSession> spSession(
new CWebSession(sSessionID, GetRemoteIP()));
Sessions.m_mspSessions.AddItem(spSession->GetId(), spSession);
m_spSession = spSession;
return spSession;
}
CString CWebSock::GetCSRFCheck() {
std::shared_ptr<CWebSession> 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<CWebAuth>(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<CWebSession> spSession = GetSession();
if (spSession->IsLoggedIn() &&
!spSession->GetUser()->GetSkinName().empty()) {
return spSession->GetUser()->GetSkinName();
}
return CZNC::Get().GetSkinName();
}