Coverage Report

Created: 2023-09-25 06:17

/src/znc/src/WebModules.cpp
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * Copyright (C) 2004-2023 ZNC, see the NOTICE file for details.
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
17
#include <znc/WebModules.h>
18
#include <znc/FileUtils.h>
19
#include <znc/User.h>
20
#include <znc/IRCNetwork.h>
21
#include <znc/znc.h>
22
#include <time.h>
23
#include <algorithm>
24
#include <sstream>
25
26
using std::pair;
27
using std::vector;
28
29
/// @todo Do we want to make this a configure option?
30
0
#define _SKINDIR_ _DATADIR_ "/webskins"
31
32
const unsigned int CWebSock::m_uiMaxSessions = 5;
33
34
// We need this class to make sure the contained maps and their content is
35
// destroyed in the order that we want.
36
struct CSessionManager {
37
    // Sessions are valid for a day, (24h, ...)
38
2
    CSessionManager() : m_mspSessions(24 * 60 * 60 * 1000), m_mIPSessions() {}
39
0
    ~CSessionManager() {
40
        // Make sure all sessions are destroyed before any of our maps
41
        // are destroyed
42
0
        m_mspSessions.Clear();
43
0
    }
44
45
    CWebSessionMap m_mspSessions;
46
    std::multimap<CString, CWebSession*> m_mIPSessions;
47
};
48
typedef std::multimap<CString, CWebSession*>::iterator mIPSessionsIterator;
49
50
static CSessionManager Sessions;
51
52
class CWebAuth : public CAuthBase {
53
  public:
54
    CWebAuth(CWebSock* pWebSock, const CString& sUsername,
55
             const CString& sPassword, bool bBasic);
56
0
    ~CWebAuth() override {}
57
58
    CWebAuth(const CWebAuth&) = delete;
59
    CWebAuth& operator=(const CWebAuth&) = delete;
60
61
0
    void SetWebSock(CWebSock* pWebSock) { m_pWebSock = pWebSock; }
62
    void AcceptedLogin(CUser& User) override;
63
    void RefusedLogin(const CString& sReason) override;
64
    void Invalidate() override;
65
66
  private:
67
  protected:
68
    CWebSock* m_pWebSock;
69
    bool m_bBasic;
70
};
71
72
0
void CWebSock::FinishUserSessions(const CUser& User) {
73
0
    Sessions.m_mspSessions.FinishUserSessions(User);
74
0
}
75
76
0
CWebSession::~CWebSession() {
77
    // Find our entry in mIPSessions
78
0
    pair<mIPSessionsIterator, mIPSessionsIterator> p =
79
0
        Sessions.m_mIPSessions.equal_range(m_sIP);
80
0
    mIPSessionsIterator it = p.first;
81
0
    mIPSessionsIterator end = p.second;
82
83
0
    while (it != end) {
84
0
        if (it->second == this) {
85
0
            Sessions.m_mIPSessions.erase(it++);
86
0
        } else {
87
0
            ++it;
88
0
        }
89
0
    }
90
0
}
91
92
CZNCTagHandler::CZNCTagHandler(CWebSock& WebSock)
93
0
    : CTemplateTagHandler(), m_WebSock(WebSock) {}
94
95
bool CZNCTagHandler::HandleTag(CTemplate& Tmpl, const CString& sName,
96
0
                               const CString& sArgs, CString& sOutput) {
97
0
    if (sName.Equals("URLPARAM")) {
98
        // sOutput = CZNC::Get()
99
0
        sOutput = m_WebSock.GetParam(sArgs.Token(0), false);
100
0
        return true;
101
0
    }
102
103
0
    return false;
104
0
}
105
106
CWebSession::CWebSession(const CString& sId, const CString& sIP)
107
    : m_sId(sId),
108
      m_sIP(sIP),
109
      m_pUser(nullptr),
110
      m_vsErrorMsgs(),
111
      m_vsSuccessMsgs(),
112
0
      m_tmLastActive() {
113
0
    Sessions.m_mIPSessions.insert(make_pair(sIP, this));
114
0
    UpdateLastActive();
115
0
}
116
117
0
void CWebSession::UpdateLastActive() { time(&m_tmLastActive); }
118
119
0
bool CWebSession::IsAdmin() const { return IsLoggedIn() && m_pUser->IsAdmin(); }
120
121
CWebAuth::CWebAuth(CWebSock* pWebSock, const CString& sUsername,
122
                   const CString& sPassword, bool bBasic)
123
    : CAuthBase(sUsername, sPassword, pWebSock),
124
      m_pWebSock(pWebSock),
125
0
      m_bBasic(bBasic) {}
126
127
0
void CWebSession::ClearMessageLoops() {
128
0
    m_vsErrorMsgs.clear();
129
0
    m_vsSuccessMsgs.clear();
130
0
}
131
132
0
void CWebSession::FillMessageLoops(CTemplate& Tmpl) {
133
0
    for (const CString& sMessage : m_vsErrorMsgs) {
134
0
        CTemplate& Row = Tmpl.AddRow("ErrorLoop");
135
0
        Row["Message"] = sMessage;
136
0
    }
137
138
0
    for (const CString& sMessage : m_vsSuccessMsgs) {
139
0
        CTemplate& Row = Tmpl.AddRow("SuccessLoop");
140
0
        Row["Message"] = sMessage;
141
0
    }
142
0
}
143
144
0
size_t CWebSession::AddError(const CString& sMessage) {
145
0
    m_vsErrorMsgs.push_back(sMessage);
146
0
    return m_vsErrorMsgs.size();
147
0
}
148
149
0
size_t CWebSession::AddSuccess(const CString& sMessage) {
150
0
    m_vsSuccessMsgs.push_back(sMessage);
151
0
    return m_vsSuccessMsgs.size();
152
0
}
153
154
0
void CWebSessionMap::FinishUserSessions(const CUser& User) {
155
0
    iterator it = m_mItems.begin();
156
157
0
    while (it != m_mItems.end()) {
158
0
        if (it->second.second->GetUser() == &User) {
159
0
            m_mItems.erase(it++);
160
0
        } else {
161
0
            ++it;
162
0
        }
163
0
    }
164
0
}
165
166
0
void CWebAuth::AcceptedLogin(CUser& User) {
167
0
    if (m_pWebSock) {
168
0
        std::shared_ptr<CWebSession> spSession = m_pWebSock->GetSession();
169
170
0
        spSession->SetUser(&User);
171
172
0
        m_pWebSock->SetLoggedIn(true);
173
0
        m_pWebSock->UnPauseRead();
174
0
        if (m_bBasic) {
175
0
            m_pWebSock->ReadLine("");
176
0
        } else {
177
0
            m_pWebSock->Redirect("/?cookie_check=true");
178
0
        }
179
180
0
        DEBUG("Successful login attempt ==> USER [" + User.GetUsername() +
181
0
              "] ==> SESSION [" + spSession->GetId() + "]");
182
0
    }
183
0
}
184
185
0
void CWebAuth::RefusedLogin(const CString& sReason) {
186
0
    if (m_pWebSock) {
187
0
        std::shared_ptr<CWebSession> spSession = m_pWebSock->GetSession();
188
189
0
        spSession->AddError("Invalid login!");
190
0
        spSession->SetUser(nullptr);
191
192
0
        m_pWebSock->SetLoggedIn(false);
193
0
        m_pWebSock->UnPauseRead();
194
0
        if (m_bBasic) {
195
0
            m_pWebSock->AddHeader("WWW-Authenticate", "Basic realm=\"ZNC\"");
196
0
            m_pWebSock->CHTTPSock::PrintErrorPage(
197
0
                401, "Unauthorized",
198
0
                "HTTP Basic authentication attempted with invalid credentials");
199
            // Why CWebSock makes this function protected?..
200
0
        } else {
201
0
            m_pWebSock->Redirect("/?cookie_check=true");
202
0
        }
203
204
0
        DEBUG("UNSUCCESSFUL login attempt ==> REASON [" + sReason +
205
0
              "] ==> SESSION [" + spSession->GetId() + "]");
206
0
    }
207
0
}
208
209
0
void CWebAuth::Invalidate() {
210
0
    CAuthBase::Invalidate();
211
0
    m_pWebSock = nullptr;
212
0
}
213
214
CWebSock::CWebSock(const CString& sURIPrefix)
215
    : CHTTPSock(nullptr, sURIPrefix),
216
      m_bPathsSet(false),
217
      m_Template(),
218
      m_spAuth(),
219
      m_sModName(""),
220
      m_sPath(""),
221
      m_sPage(""),
222
0
      m_spSession() {
223
0
    m_Template.AddTagHandler(std::make_shared<CZNCTagHandler>(*this));
224
0
}
225
226
0
CWebSock::~CWebSock() {
227
0
    if (m_spAuth) {
228
0
        m_spAuth->Invalidate();
229
0
    }
230
231
    // we have to account for traffic here because CSocket does
232
    // not have a valid CModule* pointer.
233
0
    CUser* pUser = GetSession()->GetUser();
234
0
    if (pUser) {
235
0
        pUser->AddBytesWritten(GetBytesWritten());
236
0
        pUser->AddBytesRead(GetBytesRead());
237
0
    } else {
238
0
        CZNC::Get().AddBytesWritten(GetBytesWritten());
239
0
        CZNC::Get().AddBytesRead(GetBytesRead());
240
0
    }
241
242
    // bytes have been accounted for, so make sure they don't get again:
243
0
    ResetBytesWritten();
244
0
    ResetBytesRead();
245
0
}
246
247
0
void CWebSock::GetAvailSkins(VCString& vRet) const {
248
0
    vRet.clear();
249
250
0
    CString sRoot(GetSkinPath("_default_"));
251
252
0
    sRoot.TrimRight("/");
253
0
    sRoot.TrimRight("_default_");
254
0
    sRoot.TrimRight("/");
255
256
0
    if (!sRoot.empty()) {
257
0
        sRoot += "/";
258
0
    }
259
260
0
    if (!sRoot.empty() && CFile::IsDir(sRoot)) {
261
0
        CDir Dir(sRoot);
262
263
0
        for (const CFile* pSubDir : Dir) {
264
0
            if (pSubDir->IsDir() && pSubDir->GetShortName() == "_default_") {
265
0
                vRet.push_back(pSubDir->GetShortName());
266
0
                break;
267
0
            }
268
0
        }
269
270
0
        for (const CFile* pSubDir : Dir) {
271
0
            if (pSubDir->IsDir() && pSubDir->GetShortName() != "_default_" &&
272
0
                pSubDir->GetShortName() != ".svn") {
273
0
                vRet.push_back(pSubDir->GetShortName());
274
0
            }
275
0
        }
276
0
    }
277
0
}
278
279
0
VCString CWebSock::GetDirs(CModule* pModule, bool bIsTemplate) {
280
0
    CString sHomeSkinsDir(CZNC::Get().GetZNCPath() + "/webskins/");
281
0
    CString sSkinName(GetSkinName());
282
0
    VCString vsResult;
283
284
    // Module specific paths
285
286
0
    if (pModule) {
287
0
        const CString& sModName(pModule->GetModName());
288
289
        // 1. ~/.znc/webskins/<user_skin_setting>/mods/<mod_name>/
290
        //
291
0
        if (!sSkinName.empty()) {
292
0
            vsResult.push_back(GetSkinPath(sSkinName) + "/mods/" + sModName +
293
0
                               "/");
294
0
        }
295
296
        // 2. ~/.znc/webskins/_default_/mods/<mod_name>/
297
        //
298
0
        vsResult.push_back(GetSkinPath("_default_") + "/mods/" + sModName +
299
0
                           "/");
300
301
        // 3. ./modules/<mod_name>/tmpl/
302
        //
303
0
        vsResult.push_back(pModule->GetModDataDir() + "/tmpl/");
304
305
        // 4. ~/.znc/webskins/<user_skin_setting>/mods/<mod_name>/
306
        //
307
0
        if (!sSkinName.empty()) {
308
0
            vsResult.push_back(GetSkinPath(sSkinName) + "/mods/" + sModName +
309
0
                               "/");
310
0
        }
311
312
        // 5. ~/.znc/webskins/_default_/mods/<mod_name>/
313
        //
314
0
        vsResult.push_back(GetSkinPath("_default_") + "/mods/" + sModName +
315
0
                           "/");
316
0
    }
317
318
    // 6. ~/.znc/webskins/<user_skin_setting>/
319
    //
320
0
    if (!sSkinName.empty()) {
321
0
        vsResult.push_back(GetSkinPath(sSkinName) +
322
0
                           CString(bIsTemplate ? "/tmpl/" : "/"));
323
0
    }
324
325
    // 7. ~/.znc/webskins/_default_/
326
    //
327
0
    vsResult.push_back(GetSkinPath("_default_") +
328
0
                       CString(bIsTemplate ? "/tmpl/" : "/"));
329
330
0
    return vsResult;
331
0
}
332
333
0
CString CWebSock::FindTmpl(CModule* pModule, const CString& sName) {
334
0
    VCString vsDirs = GetDirs(pModule, true);
335
0
    CString sFile = pModule->GetModName() + "_" + sName;
336
0
    for (const CString& sDir : vsDirs) {
337
0
        if (CFile::Exists(CDir::ChangeDir(sDir, sFile))) {
338
0
            m_Template.AppendPath(sDir);
339
0
            return sFile;
340
0
        }
341
0
    }
342
0
    return sName;
343
0
}
344
345
0
void CWebSock::SetPaths(CModule* pModule, bool bIsTemplate) {
346
0
    m_Template.ClearPaths();
347
348
0
    VCString vsDirs = GetDirs(pModule, bIsTemplate);
349
0
    for (const CString& sDir : vsDirs) {
350
0
        m_Template.AppendPath(sDir);
351
0
    }
352
353
0
    m_bPathsSet = true;
354
0
}
355
356
0
void CWebSock::SetVars() {
357
0
    m_Template["SessionUser"] = GetUser();
358
0
    m_Template["SessionIP"] = GetRemoteIP();
359
0
    m_Template["Tag"] = CZNC::GetTag(GetSession()->GetUser() != nullptr, true);
360
0
    m_Template["Version"] = CZNC::GetVersion();
361
0
    m_Template["SkinName"] = GetSkinName();
362
0
    m_Template["_CSRF_Check"] = GetCSRFCheck();
363
0
    m_Template["URIPrefix"] = GetURIPrefix();
364
365
0
    if (GetSession()->IsAdmin()) {
366
0
        m_Template["IsAdmin"] = "true";
367
0
    }
368
369
0
    GetSession()->FillMessageLoops(m_Template);
370
0
    GetSession()->ClearMessageLoops();
371
372
    // Global Mods
373
0
    CModules& vgMods = CZNC::Get().GetModules();
374
0
    for (CModule* pgMod : vgMods) {
375
0
        AddModLoop("GlobalModLoop", *pgMod);
376
0
    }
377
378
    // User Mods
379
0
    if (IsLoggedIn()) {
380
0
        CModules& vMods = GetSession()->GetUser()->GetModules();
381
382
0
        for (CModule* pMod : vMods) {
383
0
            AddModLoop("UserModLoop", *pMod);
384
0
        }
385
386
0
        vector<CIRCNetwork*> vNetworks = GetSession()->GetUser()->GetNetworks();
387
0
        for (CIRCNetwork* pNetwork : vNetworks) {
388
0
            CModules& vnMods = pNetwork->GetModules();
389
390
0
            CTemplate& Row = m_Template.AddRow("NetworkModLoop");
391
0
            Row["NetworkName"] = pNetwork->GetName();
392
393
0
            for (CModule* pnMod : vnMods) {
394
0
                AddModLoop("ModLoop", *pnMod, &Row);
395
0
            }
396
0
        }
397
0
    }
398
399
0
    if (IsLoggedIn()) {
400
0
        m_Template["LoggedIn"] = "true";
401
0
    }
402
0
}
403
404
bool CWebSock::AddModLoop(const CString& sLoopName, CModule& Module,
405
0
                          CTemplate* pTemplate) {
406
0
    if (!pTemplate) {
407
0
        pTemplate = &m_Template;
408
0
    }
409
410
0
    CString sTitle(Module.GetWebMenuTitle());
411
412
0
    if (!sTitle.empty() && (IsLoggedIn() || (!Module.WebRequiresLogin() &&
413
0
                                             !Module.WebRequiresAdmin())) &&
414
0
        (GetSession()->IsAdmin() || !Module.WebRequiresAdmin())) {
415
0
        CTemplate& Row = pTemplate->AddRow(sLoopName);
416
0
        bool bActiveModule = false;
417
418
0
        Row["ModName"] = Module.GetModName();
419
0
        Row["ModPath"] = Module.GetWebPath();
420
0
        Row["Title"] = sTitle;
421
422
0
        if (m_sModName == Module.GetModName()) {
423
0
            CString sModuleType = GetPath().Token(1, false, "/");
424
0
            if (sModuleType == "global" &&
425
0
                Module.GetType() == CModInfo::GlobalModule) {
426
0
                bActiveModule = true;
427
0
            } else if (sModuleType == "user" &&
428
0
                       Module.GetType() == CModInfo::UserModule) {
429
0
                bActiveModule = true;
430
0
            } else if (sModuleType == "network" &&
431
0
                       Module.GetType() == CModInfo::NetworkModule) {
432
0
                CIRCNetwork* Network = Module.GetNetwork();
433
0
                if (Network) {
434
0
                    CString sNetworkName = GetPath().Token(2, false, "/");
435
0
                    if (sNetworkName == Network->GetName()) {
436
0
                        bActiveModule = true;
437
0
                    }
438
0
                } else {
439
0
                    bActiveModule = true;
440
0
                }
441
0
            }
442
0
        }
443
444
0
        if (bActiveModule) {
445
0
            Row["Active"] = "true";
446
0
        }
447
448
0
        if (Module.GetUser()) {
449
0
            Row["Username"] = Module.GetUser()->GetUsername();
450
0
        }
451
452
0
        VWebSubPages& vSubPages = Module.GetSubPages();
453
454
0
        for (TWebSubPage& SubPage : vSubPages) {
455
            // bActive is whether or not the current url matches this subpage
456
            // (params will be checked below)
457
0
            bool bActive = (m_sModName == Module.GetModName() &&
458
0
                            m_sPage == SubPage->GetName() && bActiveModule);
459
460
0
            if (SubPage->RequiresAdmin() && !GetSession()->IsAdmin()) {
461
                // Don't add admin-only subpages to requests from non-admin
462
                // users
463
0
                continue;
464
0
            }
465
466
0
            CTemplate& SubRow = Row.AddRow("SubPageLoop");
467
468
0
            SubRow["ModName"] = Module.GetModName();
469
0
            SubRow["ModPath"] = Module.GetWebPath();
470
0
            SubRow["PageName"] = SubPage->GetName();
471
0
            SubRow["Title"] = SubPage->GetTitle().empty() ? SubPage->GetName()
472
0
                                                          : SubPage->GetTitle();
473
474
0
            CString& sParams = SubRow["Params"];
475
476
0
            const VPair& vParams = SubPage->GetParams();
477
0
            for (const pair<CString, CString>& ssNV : vParams) {
478
0
                if (!sParams.empty()) {
479
0
                    sParams += "&";
480
0
                }
481
482
0
                if (!ssNV.first.empty()) {
483
0
                    if (!ssNV.second.empty()) {
484
0
                        sParams += ssNV.first.Escape_n(CString::EURL);
485
0
                        sParams += "=";
486
0
                        sParams += ssNV.second.Escape_n(CString::EURL);
487
0
                    }
488
489
0
                    if (bActive && GetParam(ssNV.first, false) != ssNV.second) {
490
0
                        bActive = false;
491
0
                    }
492
0
                }
493
0
            }
494
495
0
            if (bActive) {
496
0
                SubRow["Active"] = "true";
497
0
            }
498
0
        }
499
500
0
        return true;
501
0
    }
502
503
0
    return false;
504
0
}
505
506
CWebSock::EPageReqResult CWebSock::PrintStaticFile(const CString& sPath,
507
                                                   CString& sPageRet,
508
0
                                                   CModule* pModule) {
509
0
    SetPaths(pModule);
510
0
    CString sFile = m_Template.ExpandFile(sPath.TrimLeft_n("/"));
511
0
    DEBUG("About to print [" + sFile + "]");
512
    // Either PrintFile() fails and sends an error page or it succeeds and
513
    // sends a result. In both cases we don't have anything more to do.
514
0
    PrintFile(sFile);
515
0
    return PAGE_DONE;
516
0
}
517
518
CWebSock::EPageReqResult CWebSock::PrintTemplate(const CString& sPageName,
519
                                                 CString& sPageRet,
520
0
                                                 CModule* pModule) {
521
0
    SetVars();
522
523
0
    m_Template["PageName"] = sPageName;
524
525
0
    if (pModule) {
526
0
        m_Template["ModName"] = pModule->GetModName();
527
528
0
        if (m_Template.find("Title") == m_Template.end()) {
529
0
            m_Template["Title"] = pModule->GetWebMenuTitle();
530
0
        }
531
532
0
        std::vector<CTemplate*>* breadcrumbs =
533
0
            m_Template.GetLoop("BreadCrumbs");
534
0
        if (breadcrumbs->size() == 1 &&
535
0
            m_Template["Title"] != pModule->GetModName()) {
536
            // Module didn't add its own breadcrumbs, so add a generic one...
537
            // But it'll be useless if it's the same as module name
538
0
            CTemplate& bread = m_Template.AddRow("BreadCrumbs");
539
0
            bread["Text"] = m_Template["Title"];
540
0
        }
541
0
    }
542
543
0
    if (!m_bPathsSet) {
544
0
        SetPaths(pModule, true);
545
0
    }
546
547
0
    if (m_Template.GetFileName().empty() &&
548
0
        !m_Template.SetFile(sPageName + ".tmpl")) {
549
0
        return PAGE_NOTFOUND;
550
0
    }
551
552
0
    if (m_Template.PrintString(sPageRet)) {
553
0
        return PAGE_PRINT;
554
0
    } else {
555
0
        return PAGE_NOTFOUND;
556
0
    }
557
0
}
558
559
0
CString CWebSock::GetSkinPath(const CString& sSkinName) {
560
0
    const CString sSkin = sSkinName.Replace_n("/", "_").Replace_n(".", "_");
561
562
0
    CString sRet = CZNC::Get().GetZNCPath() + "/webskins/" + sSkin;
563
564
0
    if (!CFile::IsDir(sRet)) {
565
0
        sRet = CZNC::Get().GetCurPath() + "/webskins/" + sSkin;
566
567
0
        if (!CFile::IsDir(sRet)) {
568
0
            sRet = CString(_SKINDIR_) + "/" + sSkin;
569
0
        }
570
0
    }
571
572
0
    return sRet + "/";
573
0
}
574
575
0
bool CWebSock::ForceLogin() {
576
0
    if (GetSession()->IsLoggedIn()) {
577
0
        return true;
578
0
    }
579
580
0
    GetSession()->AddError("You must login to view that page");
581
0
    Redirect("/");
582
0
    return false;
583
0
}
584
585
0
CString CWebSock::GetRequestCookie(const CString& sKey) {
586
0
    const CString sPrefixedKey = CString(GetLocalPort()) + "-" + sKey;
587
0
    CString sRet;
588
589
0
    if (!m_sModName.empty()) {
590
0
        sRet = CHTTPSock::GetRequestCookie("Mod-" + m_sModName + "-" +
591
0
                                           sPrefixedKey);
592
0
    }
593
594
0
    if (sRet.empty()) {
595
0
        return CHTTPSock::GetRequestCookie(sPrefixedKey);
596
0
    }
597
598
0
    return sRet;
599
0
}
600
601
0
bool CWebSock::SendCookie(const CString& sKey, const CString& sValue) {
602
0
    const CString sPrefixedKey = CString(GetLocalPort()) + "-" + sKey;
603
604
0
    if (!m_sModName.empty()) {
605
0
        return CHTTPSock::SendCookie("Mod-" + m_sModName + "-" + sPrefixedKey,
606
0
                                     sValue);
607
0
    }
608
609
0
    return CHTTPSock::SendCookie(sPrefixedKey, sValue);
610
0
}
611
612
0
void CWebSock::OnPageRequest(const CString& sURI) {
613
0
    CString sPageRet;
614
0
    EPageReqResult eRet = OnPageRequestInternal(sURI, sPageRet);
615
0
    switch (eRet) {
616
0
        case PAGE_PRINT:
617
0
            PrintPage(sPageRet);
618
0
            break;
619
0
        case PAGE_DEFERRED:
620
            // Something else will later call Close()
621
0
            break;
622
0
        case PAGE_DONE:
623
            // Redirect or something like that, it's done, just make sure
624
            // the connection will be closed
625
0
            Close(CLT_AFTERWRITE);
626
0
            break;
627
0
        case PAGE_NOTFOUND:
628
0
        default:
629
0
            PrintNotFound();
630
0
            break;
631
0
    }
632
0
}
633
634
CWebSock::EPageReqResult CWebSock::OnPageRequestInternal(const CString& sURI,
635
0
                                                         CString& sPageRet) {
636
    // Check that their session really belongs to their IP address. IP-based
637
    // authentication is bad, but here it's just an extra layer that makes
638
    // stealing cookies harder to pull off.
639
    //
640
    // When their IP is wrong, we give them an invalid cookie. This makes
641
    // sure that they will get a new cookie on their next request.
642
0
    if (CZNC::Get().GetProtectWebSessions() &&
643
0
        GetSession()->GetIP() != GetRemoteIP()) {
644
0
        DEBUG("Expected IP: " << GetSession()->GetIP());
645
0
        DEBUG("Remote IP:   " << GetRemoteIP());
646
0
        SendCookie("SessionId", "WRONG_IP_FOR_SESSION");
647
0
        PrintErrorPage(403, "Access denied",
648
0
                       "This session does not belong to your IP.");
649
0
        return PAGE_DONE;
650
0
    }
651
652
    // For pages *not provided* by modules, a CSRF check is performed which involves:
653
    // Ensure that they really POSTed from one our forms by checking if they
654
    // know the "secret" CSRF check value. Don't do this for login since
655
    // CSRF against the login form makes no sense and the login form does a
656
    // cookies-enabled check which would break otherwise.
657
    // Don't do this, if user authenticated using http-basic auth, because:
658
    // 1. they obviously know the password,
659
    // 2. it's easier to automate some tasks e.g. user creation, without need to
660
    //    care about cookies and CSRF
661
0
    if (IsPost() && !m_bBasicAuth && !sURI.StartsWith("/mods/") &&
662
0
        !ValidateCSRFCheck(sURI)) {
663
0
        DEBUG("Expected _CSRF_Check: " << GetCSRFCheck());
664
0
        DEBUG("Actual _CSRF_Check:   " << GetParam("_CSRF_Check"));
665
0
        PrintErrorPage(
666
0
            403, "Access denied",
667
0
            "POST requests need to send "
668
0
            "a secret token to prevent cross-site request forgery attacks.");
669
0
        return PAGE_DONE;
670
0
    }
671
672
0
    SendCookie("SessionId", GetSession()->GetId());
673
674
0
    if (GetSession()->IsLoggedIn()) {
675
0
        m_sUser = GetSession()->GetUser()->GetUsername();
676
0
        m_bLoggedIn = true;
677
0
    }
678
0
    CLanguageScope user_language(
679
0
        m_bLoggedIn ? GetSession()->GetUser()->GetLanguage() : "");
680
681
    // Handle the static pages that don't require a login
682
0
    if (sURI == "/") {
683
0
        if (!m_bLoggedIn && GetParam("cookie_check", false).ToBool() &&
684
0
            GetRequestCookie("SessionId").empty()) {
685
0
            GetSession()->AddError(
686
0
                "Your browser does not have cookies enabled for this site!");
687
0
        }
688
0
        return PrintTemplate("index", sPageRet);
689
0
    } else if (sURI == "/favicon.ico") {
690
0
        return PrintStaticFile("/pub/favicon.ico", sPageRet);
691
0
    } else if (sURI == "/robots.txt") {
692
0
        return PrintStaticFile("/pub/robots.txt", sPageRet);
693
0
    } else if (sURI == "/logout") {
694
0
        GetSession()->SetUser(nullptr);
695
0
        SetLoggedIn(false);
696
0
        Redirect("/");
697
698
        // We already sent a reply
699
0
        return PAGE_DONE;
700
0
    } else if (sURI == "/login") {
701
0
        if (GetParam("submitted").ToBool()) {
702
0
            m_sUser = GetParam("user");
703
0
            m_sPass = GetParam("pass");
704
0
            m_bLoggedIn = OnLogin(m_sUser, m_sPass, false);
705
706
            // AcceptedLogin()/RefusedLogin() will call Redirect()
707
0
            return PAGE_DEFERRED;
708
0
        }
709
710
0
        Redirect("/");  // the login form is here
711
0
        return PAGE_DONE;
712
0
    } else if (sURI.StartsWith("/pub/")) {
713
0
        return PrintStaticFile(sURI, sPageRet);
714
0
    } else if (sURI.StartsWith("/skinfiles/")) {
715
0
        CString sSkinName = sURI.substr(11);
716
0
        CString::size_type uPathStart = sSkinName.find("/");
717
0
        if (uPathStart != CString::npos) {
718
0
            CString sFilePath = sSkinName.substr(uPathStart + 1);
719
0
            sSkinName.erase(uPathStart);
720
721
0
            m_Template.ClearPaths();
722
0
            m_Template.AppendPath(GetSkinPath(sSkinName) + "pub");
723
724
0
            if (PrintFile(m_Template.ExpandFile(sFilePath))) {
725
0
                return PAGE_DONE;
726
0
            } else {
727
0
                return PAGE_NOTFOUND;
728
0
            }
729
0
        }
730
0
        return PAGE_NOTFOUND;
731
0
    } else if (sURI.StartsWith("/mods/") || sURI.StartsWith("/modfiles/")) {
732
        // Make sure modules are treated as directories
733
0
        if (!sURI.EndsWith("/") && !sURI.Contains(".") &&
734
0
            !sURI.TrimLeft_n("/mods/").TrimLeft_n("/").Contains("/")) {
735
0
            Redirect(sURI + "/");
736
0
            return PAGE_DONE;
737
0
        }
738
739
        // The URI looks like:
740
        // /mods/[type]/([network]/)?[module][/page][?arg1=val1&arg2=val2...]
741
742
0
        m_sPath = GetPath().TrimLeft_n("/");
743
744
0
        m_sPath.TrimPrefix("mods/");
745
0
        m_sPath.TrimPrefix("modfiles/");
746
747
0
        CString sType = m_sPath.Token(0, false, "/");
748
0
        m_sPath = m_sPath.Token(1, true, "/");
749
750
0
        CModInfo::EModuleType eModType;
751
0
        if (sType.Equals("global")) {
752
0
            eModType = CModInfo::GlobalModule;
753
0
        } else if (sType.Equals("user")) {
754
0
            eModType = CModInfo::UserModule;
755
0
        } else if (sType.Equals("network")) {
756
0
            eModType = CModInfo::NetworkModule;
757
0
        } else {
758
0
            PrintErrorPage(403, "Forbidden",
759
0
                           "Unknown module type [" + sType + "]");
760
0
            return PAGE_DONE;
761
0
        }
762
763
0
        if ((eModType != CModInfo::GlobalModule) && !ForceLogin()) {
764
            // Make sure we have a valid user
765
0
            return PAGE_DONE;
766
0
        }
767
768
0
        CIRCNetwork* pNetwork = nullptr;
769
0
        if (eModType == CModInfo::NetworkModule) {
770
0
            CString sNetwork = m_sPath.Token(0, false, "/");
771
0
            m_sPath = m_sPath.Token(1, true, "/");
772
773
0
            pNetwork = GetSession()->GetUser()->FindNetwork(sNetwork);
774
775
0
            if (!pNetwork) {
776
0
                PrintErrorPage(404, "Not Found",
777
0
                               "Network [" + sNetwork + "] not found.");
778
0
                return PAGE_DONE;
779
0
            }
780
0
        }
781
782
0
        m_sModName = m_sPath.Token(0, false, "/");
783
0
        m_sPage = m_sPath.Token(1, true, "/");
784
785
0
        if (m_sPage.empty()) {
786
0
            m_sPage = "index";
787
0
        }
788
789
0
        DEBUG("Path [" + m_sPath + "], Module [" + m_sModName + "], Page [" +
790
0
              m_sPage + "]");
791
792
0
        CModule* pModule = nullptr;
793
794
0
        switch (eModType) {
795
0
            case CModInfo::GlobalModule:
796
0
                pModule = CZNC::Get().GetModules().FindModule(m_sModName);
797
0
                break;
798
0
            case CModInfo::UserModule:
799
0
                pModule = GetSession()->GetUser()->GetModules().FindModule(
800
0
                    m_sModName);
801
0
                break;
802
0
            case CModInfo::NetworkModule:
803
0
                pModule = pNetwork->GetModules().FindModule(m_sModName);
804
0
                break;
805
0
        }
806
807
0
        if (!pModule) return PAGE_NOTFOUND;
808
809
        // Pass CSRF check to module.
810
        // Note that the normal CSRF checks are not applied to /mods/ URLs.
811
0
        if (IsPost() && !m_bBasicAuth &&
812
0
            !pModule->ValidateWebRequestCSRFCheck(*this, m_sPage)) {
813
0
            DEBUG("Expected _CSRF_Check: " << GetCSRFCheck());
814
0
            DEBUG("Actual _CSRF_Check:   " << GetParam("_CSRF_Check"));
815
0
            PrintErrorPage(
816
0
                403, "Access denied",
817
0
                "POST requests need to send "
818
0
                "a secret token to prevent cross-site request forgery attacks.");
819
0
            return PAGE_DONE;
820
0
        }
821
822
0
        m_Template["ModPath"] = pModule->GetWebPath();
823
0
        m_Template["ModFilesPath"] = pModule->GetWebFilesPath();
824
825
0
        if (pModule->WebRequiresLogin() && !ForceLogin()) {
826
0
            return PAGE_PRINT;
827
0
        } else if (pModule->WebRequiresAdmin() && !GetSession()->IsAdmin()) {
828
0
            PrintErrorPage(403, "Forbidden",
829
0
                           "You need to be an admin to access this module");
830
0
            return PAGE_DONE;
831
0
        } else if (pModule->GetType() != CModInfo::GlobalModule &&
832
0
                   pModule->GetUser() != GetSession()->GetUser()) {
833
0
            PrintErrorPage(403, "Forbidden",
834
0
                           "You must login as " +
835
0
                               pModule->GetUser()->GetUsername() +
836
0
                               " in order to view this page");
837
0
            return PAGE_DONE;
838
0
        } else if (pModule->OnWebPreRequest(*this, m_sPage)) {
839
0
            return PAGE_DEFERRED;
840
0
        }
841
842
0
        VWebSubPages& vSubPages = pModule->GetSubPages();
843
844
0
        for (TWebSubPage& SubPage : vSubPages) {
845
0
            bool bActive = (m_sModName == pModule->GetModName() &&
846
0
                            m_sPage == SubPage->GetName());
847
848
0
            if (bActive && SubPage->RequiresAdmin() &&
849
0
                !GetSession()->IsAdmin()) {
850
0
                PrintErrorPage(403, "Forbidden",
851
0
                               "You need to be an admin to access this page");
852
0
                return PAGE_DONE;
853
0
            }
854
0
        }
855
856
0
        if (pModule && pModule->GetType() != CModInfo::GlobalModule &&
857
0
            (!IsLoggedIn() || pModule->GetUser() != GetSession()->GetUser())) {
858
0
            AddModLoop("UserModLoop", *pModule);
859
0
        }
860
861
0
        if (sURI.StartsWith("/modfiles/")) {
862
0
            m_Template.AppendPath(GetSkinPath(GetSkinName()) + "/mods/" +
863
0
                                  m_sModName + "/files/");
864
0
            m_Template.AppendPath(pModule->GetModDataDir() + "/files/");
865
866
0
            if (PrintFile(m_Template.ExpandFile(m_sPage.TrimLeft_n("/")))) {
867
0
                return PAGE_PRINT;
868
0
            } else {
869
0
                return PAGE_NOTFOUND;
870
0
            }
871
0
        } else {
872
0
            SetPaths(pModule, true);
873
874
0
            CTemplate& breadModule = m_Template.AddRow("BreadCrumbs");
875
0
            breadModule["Text"] = pModule->GetModName();
876
0
            breadModule["URL"] = pModule->GetWebPath();
877
878
            /* if a module returns false from OnWebRequest, it does not
879
               want the template to be printed, usually because it did a
880
               redirect. */
881
0
            if (pModule->OnWebRequest(*this, m_sPage, m_Template)) {
882
                // If they already sent a reply, let's assume
883
                // they did what they wanted to do.
884
0
                if (SentHeader()) {
885
0
                    return PAGE_DONE;
886
0
                }
887
0
                return PrintTemplate(m_sPage, sPageRet, pModule);
888
0
            }
889
890
0
            if (!SentHeader()) {
891
0
                PrintErrorPage(
892
0
                    404, "Not Implemented",
893
0
                    "The requested module does not acknowledge web requests");
894
0
            }
895
0
            return PAGE_DONE;
896
0
        }
897
0
    } else {
898
0
        CString sPage(sURI.Trim_n("/"));
899
0
        if (sPage.length() < 32) {
900
0
            for (unsigned int a = 0; a < sPage.length(); a++) {
901
0
                unsigned char c = sPage[a];
902
903
0
                if ((c < '0' || c > '9') && (c < 'a' || c > 'z') &&
904
0
                    (c < 'A' || c > 'Z') && c != '_') {
905
0
                    return PAGE_NOTFOUND;
906
0
                }
907
0
            }
908
909
0
            return PrintTemplate(sPage, sPageRet);
910
0
        }
911
0
    }
912
913
0
    return PAGE_NOTFOUND;
914
0
}
915
916
0
void CWebSock::PrintErrorPage(const CString& sMessage) {
917
0
    m_Template.SetFile("Error.tmpl");
918
919
0
    m_Template["Action"] = "error";
920
0
    m_Template["Title"] = "Error";
921
0
    m_Template["Error"] = sMessage;
922
0
}
923
924
static inline bool compareLastActive(
925
    const std::pair<const CString, CWebSession*>& first,
926
0
    const std::pair<const CString, CWebSession*>& second) {
927
0
    return first.second->GetLastActive() < second.second->GetLastActive();
928
0
}
929
930
0
std::shared_ptr<CWebSession> CWebSock::GetSession() {
931
0
    if (m_spSession) {
932
0
        return m_spSession;
933
0
    }
934
935
0
    const CString sCookieSessionId = GetRequestCookie("SessionId");
936
0
    std::shared_ptr<CWebSession>* pSession =
937
0
        Sessions.m_mspSessions.GetItem(sCookieSessionId);
938
939
0
    if (pSession != nullptr) {
940
        // Refresh the timeout
941
0
        Sessions.m_mspSessions.AddItem((*pSession)->GetId(), *pSession);
942
0
        (*pSession)->UpdateLastActive();
943
0
        m_spSession = *pSession;
944
0
        DEBUG("Found existing session from cookie: [" + sCookieSessionId +
945
0
              "] IsLoggedIn(" +
946
0
              CString((*pSession)->IsLoggedIn()
947
0
                          ? "true, " + ((*pSession)->GetUser()->GetUsername())
948
0
                          : "false") +
949
0
              ")");
950
0
        return *pSession;
951
0
    }
952
953
0
    if (Sessions.m_mIPSessions.count(GetRemoteIP()) > m_uiMaxSessions) {
954
0
        pair<mIPSessionsIterator, mIPSessionsIterator> p =
955
0
            Sessions.m_mIPSessions.equal_range(GetRemoteIP());
956
0
        mIPSessionsIterator it =
957
0
            std::min_element(p.first, p.second, compareLastActive);
958
0
        DEBUG("Remote IP:   " << GetRemoteIP() << "; discarding session ["
959
0
                              << it->second->GetId() << "]");
960
0
        Sessions.m_mspSessions.RemItem(it->second->GetId());
961
0
    }
962
963
0
    CString sSessionID;
964
0
    do {
965
0
        sSessionID = CString::RandomString(32);
966
0
        sSessionID += ":" + GetRemoteIP() + ":" + CString(GetRemotePort());
967
0
        sSessionID += ":" + GetLocalIP() + ":" + CString(GetLocalPort());
968
0
        sSessionID += ":" + CString(time(nullptr));
969
0
        sSessionID = sSessionID.SHA256();
970
971
0
        DEBUG("Auto generated session: [" + sSessionID + "]");
972
0
    } while (Sessions.m_mspSessions.HasItem(sSessionID));
973
974
0
    std::shared_ptr<CWebSession> spSession(
975
0
        new CWebSession(sSessionID, GetRemoteIP()));
976
0
    Sessions.m_mspSessions.AddItem(spSession->GetId(), spSession);
977
978
0
    m_spSession = spSession;
979
980
0
    return spSession;
981
0
}
982
983
0
CString CWebSock::GetCSRFCheck() {
984
0
    std::shared_ptr<CWebSession> pSession = GetSession();
985
0
    return pSession->GetId().MD5();
986
0
}
987
988
0
bool CWebSock::ValidateCSRFCheck(const CString& sURI) {
989
0
    return sURI == "/login" || GetParam("_CSRF_Check") == GetCSRFCheck();
990
0
}
991
992
bool CWebSock::OnLogin(const CString& sUser, const CString& sPass,
993
0
                       bool bBasic) {
994
0
    DEBUG("=================== CWebSock::OnLogin(), basic auth? "
995
0
          << std::boolalpha << bBasic);
996
0
    m_spAuth = std::make_shared<CWebAuth>(this, sUser, sPass, bBasic);
997
998
    // Some authentication module could need some time, block this socket
999
    // until then. CWebAuth will UnPauseRead().
1000
0
    PauseRead();
1001
0
    CZNC::Get().AuthUser(m_spAuth);
1002
1003
    // If CWebAuth already set this, don't change it.
1004
0
    return IsLoggedIn();
1005
0
}
1006
1007
0
Csock* CWebSock::GetSockObj(const CString& sHost, unsigned short uPort) {
1008
    // All listening is done by CListener, thus CWebSock should never have
1009
    // to listen, but since GetSockObj() is pure virtual...
1010
0
    DEBUG("CWebSock::GetSockObj() called - this should never happen!");
1011
0
    return nullptr;
1012
0
}
1013
1014
0
CString CWebSock::GetSkinName() {
1015
0
    std::shared_ptr<CWebSession> spSession = GetSession();
1016
1017
0
    if (spSession->IsLoggedIn() &&
1018
0
        !spSession->GetUser()->GetSkinName().empty()) {
1019
0
        return spSession->GetUser()->GetSkinName();
1020
0
    }
1021
1022
0
    return CZNC::Get().GetSkinName();
1023
0
}