2
* Copyright (C) 2004-2010 See the AUTHORS file for details.
4
* This program is free software; you can redistribute it and/or modify it
5
* under the terms of the GNU General Public License version 2 as published
6
* by the Free Software Foundation.
9
#include "WebModules.h"
14
/// @todo Do we want to make this a configure option?
15
#define _SKINDIR_ _DATADIR_ "/webskins"
17
// Sessions are valid for a day, (24h, ...)
18
CWebSessionMap CWebSock::m_mspSessions(24 * 60 * 60 * 1000);
20
CZNCTagHandler::CZNCTagHandler(CWebSock& WebSock) : CTemplateTagHandler(), m_WebSock(WebSock) {
23
bool CZNCTagHandler::HandleTag(CTemplate& Tmpl, const CString& sName, const CString& sArgs, CString& sOutput) {
24
if (sName.Equals("URLPARAM")) {
25
//sOutput = CZNC::Get()
26
sOutput = m_WebSock.GetParam(sArgs.Token(0), false);
33
CWebSession::CWebSession(const CString& sId) : m_sId(sId) {
37
bool CWebSession::IsAdmin() const { return IsLoggedIn() && m_pUser->IsAdmin(); }
39
CWebAuth::CWebAuth(CWebSock* pWebSock, const CString& sUsername, const CString& sPassword)
40
: CAuthBase(sUsername, sPassword, pWebSock) {
41
m_pWebSock = pWebSock;
44
void CWebSession::ClearMessageLoops() {
45
m_vsErrorMsgs.clear();
46
m_vsSuccessMsgs.clear();
49
void CWebSession::FillMessageLoops(CTemplate& Tmpl) {
50
for (unsigned int a = 0; a < m_vsErrorMsgs.size(); a++) {
51
CTemplate& Row = Tmpl.AddRow("ErrorLoop");
52
Row["Message"] = m_vsErrorMsgs[a];
55
for (unsigned int b = 0; b < m_vsSuccessMsgs.size(); b++) {
56
CTemplate& Row = Tmpl.AddRow("SuccessLoop");
57
Row["Message"] = m_vsSuccessMsgs[b];
61
size_t CWebSession::AddError(const CString& sMessage) {
62
m_vsErrorMsgs.push_back(sMessage);
63
return m_vsErrorMsgs.size();
66
size_t CWebSession::AddSuccess(const CString& sMessage) {
67
m_vsSuccessMsgs.push_back(sMessage);
68
return m_vsSuccessMsgs.size();
71
void CWebSessionMap::FinishUserSessions(const CUser& User) {
72
iterator it = m_mItems.begin();
74
while (it != m_mItems.end()) {
75
if (it->second.second->GetUser() == &User) {
83
void CWebAuth::AcceptedLogin(CUser& User) {
85
CSmartPtr<CWebSession> spSession = m_pWebSock->GetSession();
87
spSession->SetUser(&User);
89
m_pWebSock->SetLoggedIn(true);
90
m_pWebSock->UnPauseRead();
91
m_pWebSock->Redirect("/?cookie_check=true");
93
DEBUG("Successful login attempt ==> USER [" + User.GetUserName() + "] ==> SESSION [" + spSession->GetId() + "]");
97
void CWebAuth::RefusedLogin(const CString& sReason) {
99
CSmartPtr<CWebSession> spSession = m_pWebSock->GetSession();
101
spSession->AddError("Invalid login!");
102
spSession->SetUser(NULL);
104
m_pWebSock->SetLoggedIn(false);
105
m_pWebSock->UnPauseRead();
106
m_pWebSock->Redirect("/?cookie_check=true");
108
DEBUG("UNSUCCESSFUL login attempt ==> REASON [" + sReason + "] ==> SESSION [" + spSession->GetId() + "]");
112
void CWebAuth::Invalidate() {
113
CAuthBase::Invalidate();
117
CWebSock::CWebSock(CModule* pModule) : CHTTPSock(pModule) {
121
m_Template.AddTagHandler(new CZNCTagHandler(*this));
124
CWebSock::CWebSock(CModule* pModule, const CString& sHostname, unsigned short uPort, int iTimeout)
125
: CHTTPSock(pModule, sHostname, uPort, iTimeout) {
129
m_Template.AddTagHandler(new CZNCTagHandler(*this));
132
CWebSock::~CWebSock() {
133
if (!m_spAuth.IsNull()) {
134
m_spAuth->Invalidate();
137
CUser *pUser = GetSession()->GetUser();
139
pUser->AddBytesWritten(GetBytesWritten());
140
pUser->AddBytesRead(GetBytesRead());
142
CZNC::Get().AddBytesWritten(GetBytesWritten());
143
CZNC::Get().AddBytesRead(GetBytesRead());
146
// bytes have been accounted for, so make sure they don't get again:
150
// If the module IsFake() then it was created as a dummy and needs to be deleted
151
if (m_pModule && m_pModule->IsFake()) {
152
m_pModule->UnlinkSocket(this);
158
void CWebSock::ParsePath() {
159
// The URI looks like:
160
// /[user:][module][/page][?arg1=val1&arg2=val2...]
162
m_sForceUser.clear();
164
m_sPath = GetPath().TrimLeft_n("/");
166
m_sPath.TrimPrefix("mods/");
167
m_sPath.TrimPrefix("modfiles/");
169
m_sModName = m_sPath.Token(0, false, "/");
170
m_sPage = m_sPath.Token(1, true, "/");
172
if (m_sModName.find(":") != CString::npos) {
173
m_sForceUser = m_sModName.Token(0, false, ":");
174
m_sModName = m_sModName.Token(1, false, ":");
177
if (m_sPage.empty()) {
181
DEBUG("Path [" + m_sPath + "], User [" + m_sForceUser + "], Module [" + m_sModName + "], Page [" + m_sPage + "]");
184
CModule* CWebSock::ResolveModule() {
186
CModule* pModRet = NULL;
188
// Dot means static file, not module
189
if (m_sModName.find(".") != CString::npos) {
193
// First look for forced user-mods
194
if (!m_sForceUser.empty()) {
195
CUser* pUser = CZNC::Get().FindUser(m_sForceUser);
198
pModRet = pUser->GetModules().FindModule(m_sModName);
200
DEBUG("User not found while trying to handle web request for [" + m_sPage + "]");
203
// This could be user level or global level, check both
204
pModRet = CZNC::Get().GetModules().FindModule(m_sModName);
207
// It's not a loaded global module and it has no forced username so we
208
// have to force a login to try a module loaded by the current user
213
pModRet = GetSession()->GetUser()->GetModules().FindModule(m_sModName);
218
DEBUG("Module not found");
219
} else if (pModRet->IsFake()) {
220
DEBUG("Fake module found, ignoring");
227
size_t CWebSock::GetAvailSkins(vector<CFile>& vRet) {
230
CString sRoot(GetSkinPath("_default_"));
232
sRoot.TrimRight("/");
233
sRoot.TrimRight("_default_");
234
sRoot.TrimRight("/");
236
if (!sRoot.empty()) {
240
if (!sRoot.empty() && CFile::IsDir(sRoot)) {
243
for (unsigned int d = 0; d < Dir.size(); d++) {
244
const CFile& SubDir = *Dir[d];
246
if (SubDir.IsDir() && SubDir.GetShortName() == "_default_") {
247
vRet.push_back(SubDir);
251
for (unsigned int e = 0; e < Dir.size(); e++) {
252
const CFile& SubDir = *Dir[e];
254
if (SubDir.IsDir() && SubDir.GetShortName() != "_default_" && SubDir.GetShortName() != ".svn") {
255
vRet.push_back(SubDir);
263
void CWebSock::SetPaths(CModule* pModule, bool bIsTemplate) {
264
m_Template.ClearPaths();
266
CString sHomeSkinsDir(CZNC::Get().GetZNCPath() + "/webskins/");
267
CString sSkinName(GetSkinName());
269
// Module specific paths
272
const CString& sModName(pModule->GetModName());
274
// 1. ~/.znc/webskins/<user_skin_setting>/mods/<mod_name>/
276
if (!sSkinName.empty()) {
277
m_Template.AppendPath(GetSkinPath(sSkinName) + "/mods/" + sModName + "/");
280
// 2. ~/.znc/webskins/_default_/mods/<mod_name>/
282
m_Template.AppendPath(GetSkinPath("_default_") + "/mods/" + sModName + "/");
284
// 3. ./modules/<mod_name>/tmpl/
286
m_Template.AppendPath(pModule->GetModDataDir() + "/tmpl/");
288
// 4. ~/.znc/webskins/<user_skin_setting>/mods/<mod_name>/
290
if (!sSkinName.empty()) {
291
m_Template.AppendPath(GetSkinPath(sSkinName) + "/mods/" + sModName + "/");
294
// 5. ~/.znc/webskins/_default_/mods/<mod_name>/
296
m_Template.AppendPath(GetSkinPath("_default_") + "/mods/" + sModName + "/");
299
// 6. ~/.znc/webskins/<user_skin_setting>/
301
if (!sSkinName.empty()) {
302
m_Template.AppendPath(GetSkinPath(sSkinName) + CString(bIsTemplate ? "/tmpl/" : "/"), (0 && pModule != NULL));
305
// 7. ~/.znc/webskins/_default_/
307
m_Template.AppendPath(GetSkinPath("_default_") + CString(bIsTemplate ? "/tmpl/" : "/"), (0 && pModule != NULL));
312
void CWebSock::SetVars() {
313
m_Template["SessionUser"] = GetUser();
314
m_Template["SessionIP"] = GetRemoteIP();
315
m_Template["Tag"] = CZNC::GetTag(GetSession()->GetUser() != NULL);
316
m_Template["SkinName"] = GetSkinName();
317
m_Template["_CSRF_Check"] = GetCSRFCheck();
319
if (GetSession()->IsAdmin()) {
320
m_Template["IsAdmin"] = "true";
323
GetSession()->FillMessageLoops(m_Template);
324
GetSession()->ClearMessageLoops();
327
CGlobalModules& vgMods = CZNC::Get().GetModules();
328
for (unsigned int a = 0; a < vgMods.size(); a++) {
329
AddModLoop("GlobalModLoop", *vgMods[a]);
334
CModules& vMods = GetSession()->GetUser()->GetModules();
336
for (unsigned int a = 0; a < vMods.size(); a++) {
337
AddModLoop("UserModLoop", *vMods[a]);
342
m_Template["LoggedIn"] = "true";
346
bool CWebSock::AddModLoop(const CString& sLoopName, CModule& Module) {
347
CString sTitle(Module.GetWebMenuTitle());
349
if (!sTitle.empty() && (IsLoggedIn() || (!Module.WebRequiresLogin() && !Module.WebRequiresAdmin())) && (GetSession()->IsAdmin() || !Module.WebRequiresAdmin())) {
350
CTemplate& Row = m_Template.AddRow(sLoopName);
352
Row["ModName"] = Module.GetModName();
353
Row["Title"] = sTitle;
355
if (m_sModName == Module.GetModName()) {
356
Row["Active"] = "true";
359
if (Module.GetUser()) {
360
Row["Username"] = Module.GetUser()->GetUserName();
363
VWebSubPages& vSubPages = Module.GetSubPages();
365
for (unsigned int a = 0; a < vSubPages.size(); a++) {
366
TWebSubPage& SubPage = vSubPages[a];
368
// bActive is whether or not the current url matches this subpage (params will be checked below)
369
bool bActive = (m_sModName == Module.GetModName() && m_sPage == SubPage->GetName());
371
if (SubPage->RequiresAdmin() && !GetSession()->IsAdmin()) {
372
continue; // Don't add admin-only subpages to requests from non-admin users
375
CTemplate& SubRow = Row.AddRow("SubPageLoop");
377
SubRow["ModName"] = Module.GetModName();
378
SubRow["PageName"] = SubPage->GetName();
379
SubRow["Title"] = SubPage->GetTitle().empty() ? SubPage->GetName() : SubPage->GetTitle();
381
CString& sParams = SubRow["Params"];
383
const VPair& vParams = SubPage->GetParams();
384
for (size_t b = 0; b < vParams.size(); b++) {
385
pair<CString, CString> ssNV = vParams[b];
387
if (!sParams.empty()) {
391
if (!ssNV.first.empty()) {
392
if (!ssNV.second.empty()) {
393
sParams += ssNV.first.Escape_n(CString::EURL);
395
sParams += ssNV.second.Escape_n(CString::EURL);
398
if (bActive && GetParam(ssNV.first, false) != ssNV.second) {
405
SubRow["Active"] = "true";
415
CWebSock::EPageReqResult CWebSock::PrintStaticFile(const CString& sPath, CString& sPageRet, CModule* pModule) {
417
CString sFile = m_Template.ExpandFile(sPath.TrimLeft_n("/"));
418
DEBUG("About to print [" + sFile+ "]");
419
// Either PrintFile() fails and sends an error page or it suceeds and
420
// sends a result. In both cases we don't have anything more to do.
425
CWebSock::EPageReqResult CWebSock::PrintTemplate(const CString& sPageName, CString& sPageRet, CModule* pModule) {
427
m_Template["PageName"] = sPageName;
430
CUser* pUser = pModule->GetUser();
431
m_Template["ModUser"] = pUser ? pUser->GetUserName() : "";
432
m_Template["ModName"] = pModule->GetModName();
434
if (m_Template.find("Title") == m_Template.end()) {
435
m_Template["Title"] = pModule->GetWebMenuTitle();
440
SetPaths(pModule, true);
443
if (m_Template.GetFileName().empty() && !m_Template.SetFile(sPageName + ".tmpl")) {
444
return PAGE_NOTFOUND;
447
if (m_Template.PrintString(sPageRet)) {
450
return PAGE_NOTFOUND;
454
CString CWebSock::GetSkinPath(const CString& sSkinName) const {
455
CString sRet = CZNC::Get().GetZNCPath() + "/webskins/" + sSkinName;
457
if (!CFile::IsDir(sRet)) {
458
sRet = CZNC::Get().GetCurPath() + "/webskins/" + sSkinName;
460
if (!CFile::IsDir(sRet)) {
461
sRet = CString(_SKINDIR_) + "/" + sSkinName;
468
bool CWebSock::ForceLogin() {
469
if (GetSession()->IsLoggedIn()) {
473
GetSession()->AddError("You must login to view that page");
478
CString CWebSock::GetRequestCookie(const CString& sKey) const {
481
if (!m_sModName.empty()) {
482
sRet = CHTTPSock::GetRequestCookie("Mod::" + m_sModName + "::" + sKey);
486
return CHTTPSock::GetRequestCookie(sKey);
491
bool CWebSock::SendCookie(const CString& sKey, const CString& sValue) {
492
if (!m_sModName.empty()) {
493
return CHTTPSock::SendCookie("Mod::" + m_sModName + "::" + sKey, sValue);
496
return CHTTPSock::SendCookie(sKey, sValue);
499
void CWebSock::OnPageRequest(const CString& sURI) {
501
EPageReqResult eRet = OnPageRequestInternal(sURI, sPageRet);
507
// Something else will later call Close()
510
// Redirect or something like that, it's done, just make sure
511
// the connection will be closed
512
Close(CLT_AFTERWRITE);
520
CWebSock::EPageReqResult CWebSock::OnPageRequestInternal(const CString& sURI, CString& sPageRet) {
521
// Check that they really POSTed from one our forms by checking if they
522
// know the "secret" CSRF check value. Don't do this for login since
523
// CSRF against the login form makes no sense and the login form does a
524
// cookies-enabled check which would break otherwise.
525
if (IsPost() && GetParam("_CSRF_Check") != GetCSRFCheck() && sURI != "/login") {
526
sPageRet = GetErrorPage(403, "Access denied", "POST requests need to send "
527
"a secret token to prevent cross-site request forgery attacks.");
531
SendCookie("SessionId", GetSession()->GetId());
533
if (GetSession()->IsLoggedIn()) {
534
m_sUser = GetSession()->GetUser()->GetUserName();
538
// Handle the static pages that don't require a login
540
if(!m_bLoggedIn && GetParam("cookie_check", false).ToBool() && GetRequestCookie("SessionId").empty()) {
541
GetSession()->AddError("Your browser does not have cookies enabled for this site!");
543
return PrintTemplate("index", sPageRet);
544
} else if (sURI == "/favicon.ico") {
545
return PrintStaticFile("/pub/favicon.ico", sPageRet);
546
} else if (sURI == "/robots.txt") {
547
return PrintStaticFile("/pub/robots.txt", sPageRet);
548
} else if (sURI == "/logout") {
549
GetSession()->SetUser(NULL);
553
// We already sent a reply
555
} else if (sURI == "/login") {
556
if (GetParam("submitted").ToBool()) {
557
m_sUser = GetParam("user");
558
m_sPass = GetParam("pass");
559
m_bLoggedIn = OnLogin(m_sUser, m_sPass);
561
// AcceptedLogin()/RefusedLogin() will call Redirect()
562
return PAGE_DEFERRED;
565
Redirect("/"); // the login form is here
567
} else if (sURI.Left(5) == "/pub/") {
568
return PrintStaticFile(sURI, sPageRet);
569
} else if (sURI.Left(11) == "/skinfiles/") {
570
CString sSkinName = sURI.substr(11);
571
CString::size_type uPathStart = sSkinName.find("/");
572
if (uPathStart != CString::npos) {
573
CString sFilePath = sSkinName.substr(uPathStart + 1);
574
sSkinName.erase(uPathStart);
576
m_Template.ClearPaths();
577
m_Template.AppendPath(GetSkinPath(sSkinName) + "pub");
579
if (PrintFile(m_Template.ExpandFile(sFilePath))) {
582
return PAGE_NOTFOUND;
585
return PAGE_NOTFOUND;
586
} else if (sURI.Left(6) == "/mods/" || sURI.Left(10) == "/modfiles/") {
588
// Make sure modules are treated as directories
589
if (sURI.Right(1) != "/" && sURI.find(".") == CString::npos && sURI.TrimLeft_n("/mods/").TrimLeft_n("/").find("/") == CString::npos) {
590
Redirect(sURI + "/");
594
if (m_sModName.empty()) {
595
return PrintTemplate("modlist", sPageRet);
598
DEBUG("FindModule(" + m_sModName + ", " + m_sForceUser + ")");
599
CModule* pModule = CZNC::Get().FindModule(m_sModName, m_sForceUser);
601
if (!pModule && m_sForceUser.empty()) {
606
pModule = CZNC::Get().FindModule(m_sModName, GetSession()->GetUser());
610
return PAGE_NOTFOUND;
611
} else if (pModule->WebRequiresLogin() && !ForceLogin()) {
613
} else if (pModule->WebRequiresAdmin() && !GetSession()->IsAdmin()) {
614
sPageRet = GetErrorPage(403, "Forbidden", "You need to be an admin to access this module");
616
} else if (!pModule->IsGlobal() && pModule->GetUser() != GetSession()->GetUser()) {
617
sPageRet = GetErrorPage(403, "Forbidden", "You must login as " + pModule->GetUser()->GetUserName() + " in order to view this page");
619
} else if (pModule->OnWebPreRequest(*this, m_sPage)) {
620
return PAGE_DEFERRED;
623
VWebSubPages& vSubPages = pModule->GetSubPages();
625
for (unsigned int a = 0; a < vSubPages.size(); a++) {
626
TWebSubPage& SubPage = vSubPages[a];
628
bool bActive = (m_sModName == pModule->GetModName() && m_sPage == SubPage->GetName());
630
if (bActive && SubPage->RequiresAdmin() && !GetSession()->IsAdmin()) {
631
sPageRet = GetErrorPage(403, "Forbidden", "You need to be an admin to access this page");
636
if (pModule && !pModule->IsGlobal() && (!IsLoggedIn() || pModule->GetUser() != GetSession()->GetUser())) {
637
AddModLoop("UserModLoop", *pModule);
640
if (sURI.Left(10) == "/modfiles/") {
641
m_Template.AppendPath(GetSkinPath(GetSkinName()) + "/mods/" + m_sModName + "/files/");
642
m_Template.AppendPath(pModule->GetModDataDir() + "/files/");
644
if (PrintFile(m_Template.ExpandFile(m_sPage.TrimLeft_n("/")))) {
647
return PAGE_NOTFOUND;
650
SetPaths(pModule, true);
652
/* if a module returns false from OnWebRequest, it does not
653
want the template to be printed, usually because it did a redirect. */
654
if (pModule->OnWebRequest(*this, m_sPage, m_Template)) {
655
// If they already sent a reply, let's assume
656
// they did what they wanted to do.
660
return PrintTemplate(m_sPage, sPageRet, pModule);
664
sPageRet = GetErrorPage(404, "Not Implemented", "The requested module does not acknowledge web requests");
671
CString sPage(sURI.Trim_n("/"));
672
if (sPage.length() < 32) {
673
for (unsigned int a = 0; a < sPage.length(); a++) {
674
unsigned char c = sPage[a];
676
if ((c < '0' || c > '9') && (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && c != '_') {
677
return PAGE_NOTFOUND;
681
return PrintTemplate(sPage, sPageRet);
685
return PAGE_NOTFOUND;
688
void CWebSock::PrintErrorPage(const CString& sMessage) {
689
m_Template.SetFile("Error.tmpl");
691
m_Template["Action"] = "error";
692
m_Template["Title"] = "Error";
693
m_Template["Error"] = sMessage;
696
CSmartPtr<CWebSession> CWebSock::GetSession() {
697
if (!m_spSession.IsNull()) {
701
const CString sCookieSessionId = GetRequestCookie("SessionId");
702
CSmartPtr<CWebSession> *pSession = m_mspSessions.GetItem(sCookieSessionId);
704
if (pSession != NULL) {
705
// Refresh the timeout
706
m_mspSessions.AddItem((*pSession)->GetId(), *pSession);
707
m_spSession = *pSession;
708
DEBUG("Found existing session from cookie: [" + sCookieSessionId + "] IsLoggedIn(" + CString((*pSession)->IsLoggedIn() ? "true" : "false") + ")");
714
sSessionID = CString::RandomString(32);
715
sSessionID += ":" + GetRemoteIP() + ":" + CString(GetRemotePort());
716
sSessionID += ":" + GetLocalIP() + ":" + CString(GetLocalPort());
717
sSessionID += ":" + CString(time(NULL));
718
sSessionID = sSessionID.SHA256();
720
DEBUG("Auto generated session: [" + sSessionID + "]");
721
} while (m_mspSessions.HasItem(sSessionID));
723
CSmartPtr<CWebSession> spSession(new CWebSession(sSessionID));
724
m_mspSessions.AddItem(spSession->GetId(), spSession);
726
m_spSession = spSession;
731
CString CWebSock::GetCSRFCheck() {
732
CSmartPtr<CWebSession> pSession = GetSession();
733
return pSession->GetId().MD5();
736
bool CWebSock::OnLogin(const CString& sUser, const CString& sPass) {
737
DEBUG("=================== CWebSock::OnLogin()");
738
m_spAuth = new CWebAuth(this, sUser, sPass);
740
// Some authentication module could need some time, block this socket
741
// until then. CWebAuth will UnPauseRead().
743
CZNC::Get().AuthUser(m_spAuth);
745
// If CWebAuth already set this, don't change it.
749
Csock* CWebSock::GetSockObj(const CString& sHost, unsigned short uPort) {
750
CWebSock* pSock = new CWebSock(GetModule(), sHost, uPort);
751
pSock->SetSockName("Web::Client");
752
pSock->SetTimeout(120);
757
CString CWebSock::GetSkinName() {
758
CSmartPtr<CWebSession> spSession = GetSession();
760
if (spSession->IsLoggedIn() && !spSession->GetUser()->GetSkinName().empty()) {
761
return spSession->GetUser()->GetSkinName();
764
return CZNC::Get().GetSkinName();