mirror of
https://github.com/znc/znc.git
synced 2026-03-28 17:42:41 +01:00
1024 lines
33 KiB
C++
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();
|
|
}
|