Files
znc/src/znc.cpp
Alexey Sokolov b642d92ce7 Switch integration test to mostly use unix sockets
By not using the same hardcoded number for every test, we can parallelize the test now.

There are several cases remaining where we can't easily use unix sockets (e.g. QSslSocket or imapauth module), for that ask kernel what port number is currently free to use. This is a bit racy though.
2025-04-21 00:19:46 +01:00

2184 lines
66 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/znc.h>
#include <znc/FileUtils.h>
#include <znc/IRCSock.h>
#include <znc/Server.h>
#include <znc/User.h>
#include <znc/IRCNetwork.h>
#include <znc/Config.h>
#include <time.h>
#include <tuple>
#include <algorithm>
using std::endl;
using std::cout;
using std::map;
using std::set;
using std::vector;
using std::list;
using std::tuple;
using std::make_tuple;
CZNC::CZNC()
: m_TimeStarted(time(nullptr)),
m_eConfigState(ECONFIG_NOTHING),
m_vpListeners(),
m_msUsers(),
m_msDelUsers(),
m_Manager(),
m_sCurPath(""),
m_sZNCPath(""),
m_sConfigFile(""),
m_sSkinName(""),
m_sStatusPrefix(""),
m_sPidFile(""),
m_sSSLCertFile(""),
m_sSSLKeyFile(""),
m_sSSLDHParamFile(""),
m_sSSLCiphers(""),
m_sSSLProtocols(""),
m_vsBindHosts(),
m_vsTrustedProxies(),
m_vsMotd(),
m_pLockFile(nullptr),
m_uiConnectDelay(5),
m_uiAnonIPLimit(10),
m_uiMaxBufferSize(500),
m_uDisabledSSLProtocols(Csock::EDP_SSL | Csock::EDP_TLSv1 | Csock::EDP_TLSv1_1),
m_pModules(new CModules),
m_uBytesRead(0),
m_uBytesWritten(0),
m_lpConnectQueue(),
m_pConnectQueueTimer(nullptr),
m_uiConnectPaused(0),
m_uiForceEncoding(0),
m_sConnectThrottle(),
m_bProtectWebSessions(true),
m_bHideVersion(false),
m_bAuthOnlyViaModule(false),
m_Translation("znc"),
m_uiConfigWriteDelay(0),
m_pConfigTimer(nullptr) {
if (!InitCsocket()) {
CUtils::PrintError("Could not initialize Csocket!");
exit(-1);
}
m_sConnectThrottle.SetTTL(30000);
}
CZNC::~CZNC() {
m_pModules->UnloadAll();
for (const auto& it : m_msUsers) {
it.second->GetModules().UnloadAll();
const vector<CIRCNetwork*>& networks = it.second->GetNetworks();
for (CIRCNetwork* pNetwork : networks) {
pNetwork->GetModules().UnloadAll();
}
}
for (CListener* pListener : m_vpListeners) {
delete pListener;
}
for (const auto& it : m_msUsers) {
it.second->SetBeingDeleted(true);
}
m_pConnectQueueTimer = nullptr;
// This deletes m_pConnectQueueTimer
m_Manager.Cleanup();
DeleteUsers();
delete m_pModules;
delete m_pLockFile;
ShutdownCsocket();
DeletePidFile();
}
CString CZNC::GetVersion() {
return CString(VERSION_STR) + CString(ZNC_VERSION_EXTRA);
}
CString CZNC::GetTag(bool bIncludeVersion, bool bHTML) {
if (!Get().m_bHideVersion) {
bIncludeVersion = true;
}
CString sAddress = bHTML ? "<a href=\"https://znc.in\">https://znc.in</a>"
: "https://znc.in";
if (!bIncludeVersion) {
return "ZNC - " + sAddress;
}
CString sVersion = GetVersion();
return "ZNC " + sVersion + " - " + sAddress;
}
CString CZNC::GetCompileOptionsString() {
return ZNC_COMPILE_OPTIONS_STRING;
}
CString CZNC::GetUptime() const {
time_t now = time(nullptr);
return CString::ToTimeStr(now - TimeStarted());
}
bool CZNC::OnBoot() {
bool bFail = false;
ALLMODULECALL(OnBoot(), &bFail);
if (bFail) return false;
return true;
}
bool CZNC::HandleUserDeletion() {
if (m_msDelUsers.empty()) return false;
for (const auto& it : m_msDelUsers) {
CUser* pUser = it.second;
pUser->SetBeingDeleted(true);
if (GetModules().OnDeleteUser(*pUser)) {
pUser->SetBeingDeleted(false);
continue;
}
m_msUsers.erase(pUser->GetUsername());
CWebSock::FinishUserSessions(*pUser);
delete pUser;
}
m_msDelUsers.clear();
return true;
}
class CConfigWriteTimer : public CCron {
public:
CConfigWriteTimer(int iSecs) : CCron() {
SetName("Config write timer");
Start(iSecs);
}
protected:
void RunJob() override {
CZNC::Get().SetConfigState(CZNC::ECONFIG_NEED_WRITE);
CZNC::Get().DisableConfigTimer();
}
};
void CZNC::Loop() {
while (true) {
CString sError;
ConfigState eState = GetConfigState();
switch (eState) {
case ECONFIG_NEED_REHASH:
SetConfigState(ECONFIG_NOTHING);
if (RehashConfig(sError)) {
Broadcast("Rehashing succeeded", true);
} else {
Broadcast("Rehashing failed: " + sError, true);
Broadcast("ZNC is in some possibly inconsistent state!",
true);
}
break;
case ECONFIG_DELAYED_WRITE:
SetConfigState(ECONFIG_NOTHING);
if (GetConfigWriteDelay() > 0) {
if (m_pConfigTimer == nullptr) {
m_pConfigTimer = new CConfigWriteTimer(GetConfigWriteDelay());
GetManager().AddCron(m_pConfigTimer);
}
break;
}
/* Fall through */
case ECONFIG_NEED_WRITE:
case ECONFIG_NEED_VERBOSE_WRITE:
SetConfigState(ECONFIG_NOTHING);
// stop pending configuration timer
DisableConfigTimer();
if (!WriteConfig()) {
Broadcast("Writing the config file failed", true);
} else if (eState == ECONFIG_NEED_VERBOSE_WRITE) {
Broadcast("Writing the config succeeded", true);
}
break;
case ECONFIG_NOTHING:
break;
case ECONFIG_NEED_QUIT:
return;
}
// Check for users that need to be deleted
if (HandleUserDeletion()) {
// Also remove those user(s) from the config file
WriteConfig();
}
// Csocket wants micro seconds
// 100 msec to 5 min
m_Manager.DynamicSelectLoop(100 * 1000, 5 * 60 * 1000 * 1000);
}
}
CFile* CZNC::InitPidFile() {
if (!m_sPidFile.empty()) {
CString sFile;
// absolute path or relative to the data dir?
if (m_sPidFile[0] != '/')
sFile = GetZNCPath() + "/" + m_sPidFile;
else
sFile = m_sPidFile;
return new CFile(sFile);
}
return nullptr;
}
bool CZNC::WritePidFile(int iPid) {
CFile* File = InitPidFile();
if (File == nullptr) return false;
CUtils::PrintAction("Writing pid file [" + File->GetLongName() + "]");
bool bRet = false;
if (File->Open(O_WRONLY | O_TRUNC | O_CREAT)) {
File->Write(CString(iPid) + "\n");
File->Close();
bRet = true;
}
delete File;
CUtils::PrintStatus(bRet);
return bRet;
}
bool CZNC::DeletePidFile() {
CFile* File = InitPidFile();
if (File == nullptr) return false;
CUtils::PrintAction("Deleting pid file [" + File->GetLongName() + "]");
bool bRet = File->Delete();
delete File;
CUtils::PrintStatus(bRet);
return bRet;
}
bool CZNC::WritePemFile() {
#ifndef HAVE_LIBSSL
CUtils::PrintError("ZNC was not compiled with ssl support.");
return false;
#else
CString sPemFile = GetPemLocation();
CUtils::PrintAction("Writing Pem file [" + sPemFile + "]");
#ifndef _WIN32
int fd = creat(sPemFile.c_str(), 0600);
if (fd == -1) {
CUtils::PrintStatus(false, "Unable to open");
return false;
}
FILE* f = fdopen(fd, "w");
#else
FILE* f = fopen(sPemFile.c_str(), "w");
#endif
if (!f) {
CUtils::PrintStatus(false, "Unable to open");
return false;
}
CUtils::GenerateCert(f);
fclose(f);
CUtils::PrintStatus(true);
return true;
#endif
}
void CZNC::DeleteUsers() {
for (const auto& it : m_msUsers) {
it.second->SetBeingDeleted(true);
delete it.second;
}
m_msUsers.clear();
DisableConnectQueue();
}
bool CZNC::IsHostAllowed(const CString& sHostMask) const {
for (const auto& it : m_msUsers) {
if (it.second->IsHostAllowed(sHostMask)) {
return true;
}
}
return false;
}
bool CZNC::AllowConnectionFrom(const CString& sIP) const {
if (m_uiAnonIPLimit == 0) return true;
return (GetManager().GetAnonConnectionCount(sIP) < m_uiAnonIPLimit);
}
void CZNC::InitDirs(const CString& sArgvPath, const CString& sDataDir) {
// If the bin was not ran from the current directory, we need to add that
// dir onto our cwd
CString::size_type uPos = sArgvPath.rfind('/');
if (uPos == CString::npos)
m_sCurPath = "./";
else
m_sCurPath = CDir::ChangeDir("./", sArgvPath.Left(uPos), "");
// Try to set the user's home dir, default to binpath on failure
CFile::InitHomePath(m_sCurPath);
if (sDataDir.empty()) {
m_sZNCPath = CFile::GetHomePath() + "/.znc";
} else {
m_sZNCPath = sDataDir;
}
m_sSSLCertFile = m_sZNCPath + "/znc.pem";
}
CString CZNC::GetConfPath(bool bAllowMkDir) const {
CString sConfPath = m_sZNCPath + "/configs";
if (bAllowMkDir && !CFile::Exists(sConfPath)) {
CDir::MakeDir(sConfPath);
}
return sConfPath;
}
CString CZNC::GetUserPath() const {
CString sUserPath = m_sZNCPath + "/users";
if (!CFile::Exists(sUserPath)) {
CDir::MakeDir(sUserPath);
}
return sUserPath;
}
CString CZNC::GetModPath() const {
CString sModPath = m_sZNCPath + "/modules";
return sModPath;
}
const CString& CZNC::GetCurPath() const {
if (!CFile::Exists(m_sCurPath)) {
CDir::MakeDir(m_sCurPath);
}
return m_sCurPath;
}
const CString& CZNC::GetHomePath() const { return CFile::GetHomePath(); }
const CString& CZNC::GetZNCPath() const {
if (!CFile::Exists(m_sZNCPath)) {
CDir::MakeDir(m_sZNCPath);
}
return m_sZNCPath;
}
CString CZNC::GetPemLocation() const {
return CDir::ChangeDir("", m_sSSLCertFile);
}
CString CZNC::GetKeyLocation() const {
return CDir::ChangeDir(
"", m_sSSLKeyFile.empty() ? m_sSSLCertFile : m_sSSLKeyFile);
}
CString CZNC::GetDHParamLocation() const {
return CDir::ChangeDir(
"", m_sSSLDHParamFile.empty() ? m_sSSLCertFile : m_sSSLDHParamFile);
}
CString CZNC::ExpandConfigPath(const CString& sConfigFile, bool bAllowMkDir) {
CString sRetPath;
if (sConfigFile.empty()) {
sRetPath = GetConfPath(bAllowMkDir) + "/znc.conf";
} else {
if (sConfigFile.StartsWith("./") || sConfigFile.StartsWith("../")) {
sRetPath = GetCurPath() + "/" + sConfigFile;
} else if (!sConfigFile.StartsWith("/")) {
sRetPath = GetConfPath(bAllowMkDir) + "/" + sConfigFile;
} else {
sRetPath = sConfigFile;
}
}
return sRetPath;
}
bool CZNC::WriteConfig() {
if (GetConfigFile().empty()) {
DEBUG("Config file name is empty?!");
return false;
}
// We first write to a temporary file and then move it to the right place
CFile* pFile = new CFile(GetConfigFile() + "~");
if (!pFile->Open(O_WRONLY | O_CREAT | O_TRUNC, 0600)) {
DEBUG("Could not write config to " + GetConfigFile() + "~: " +
CString(strerror(errno)));
delete pFile;
return false;
}
// We have to "transfer" our lock on the config to the new file.
// The old file (= inode) is going away and thus a lock on it would be
// useless. These lock should always succeed (races, anyone?).
if (!pFile->TryExLock()) {
DEBUG("Error while locking the new config file, errno says: " +
CString(strerror(errno)));
pFile->Delete();
delete pFile;
return false;
}
pFile->Write(MakeConfigHeader() + "\n");
CConfig config;
config.AddKeyValuePair("AnonIPLimit", CString(m_uiAnonIPLimit));
config.AddKeyValuePair("MaxBufferSize", CString(m_uiMaxBufferSize));
config.AddKeyValuePair("SSLCertFile", CString(GetPemLocation()));
config.AddKeyValuePair("SSLKeyFile", CString(GetKeyLocation()));
config.AddKeyValuePair("SSLDHParamFile", CString(GetDHParamLocation()));
config.AddKeyValuePair("ProtectWebSessions",
CString(m_bProtectWebSessions));
config.AddKeyValuePair("HideVersion", CString(m_bHideVersion));
config.AddKeyValuePair("AuthOnlyViaModule", CString(m_bAuthOnlyViaModule));
config.AddKeyValuePair("Version", CString(VERSION_STR));
config.AddKeyValuePair("ConfigWriteDelay", CString(m_uiConfigWriteDelay));
unsigned int l = 0;
for (CListener* pListener : m_vpListeners) {
config.AddSubConfig("Listener", "listener" + CString(l++),
pListener->ToConfig());
}
config.AddKeyValuePair("ConnectDelay", CString(m_uiConnectDelay));
config.AddKeyValuePair("ServerThrottle",
CString(m_sConnectThrottle.GetTTL() / 1000));
if (!m_sPidFile.empty()) {
config.AddKeyValuePair("PidFile", m_sPidFile.FirstLine());
}
if (!m_sSkinName.empty()) {
config.AddKeyValuePair("Skin", m_sSkinName.FirstLine());
}
if (!m_sStatusPrefix.empty()) {
config.AddKeyValuePair("StatusPrefix", m_sStatusPrefix.FirstLine());
}
if (!m_sSSLCiphers.empty()) {
config.AddKeyValuePair("SSLCiphers", CString(m_sSSLCiphers));
}
if (!m_sSSLProtocols.empty()) {
config.AddKeyValuePair("SSLProtocols", m_sSSLProtocols);
}
for (const CString& sLine : m_vsMotd) {
config.AddKeyValuePair("Motd", sLine.FirstLine());
}
for (const CString& sProxy : m_vsTrustedProxies) {
config.AddKeyValuePair("TrustedProxy", sProxy.FirstLine());
}
CModules& Mods = GetModules();
for (const CModule* pMod : Mods) {
CString sName = pMod->GetModName();
CString sArgs = pMod->GetArgs();
if (!sArgs.empty()) {
sArgs = " " + sArgs.FirstLine();
}
config.AddKeyValuePair("LoadModule", sName.FirstLine() + sArgs);
}
for (const auto& it : m_msUsers) {
CString sErr;
if (!it.second->IsValid(sErr)) {
DEBUG("** Error writing config for user [" << it.first << "] ["
<< sErr << "]");
continue;
}
config.AddSubConfig("User", it.second->GetUsername(),
it.second->ToConfig());
}
config.Write(*pFile);
// If Sync() fails... well, let's hope nothing important breaks..
pFile->Sync();
if (pFile->HadError()) {
DEBUG("Error while writing the config, errno says: " +
CString(strerror(errno)));
pFile->Delete();
delete pFile;
return false;
}
// We wrote to a temporary name, move it to the right place
if (!pFile->Move(GetConfigFile(), true)) {
DEBUG(
"Error while replacing the config file with a new version, errno "
"says "
<< strerror(errno));
pFile->Delete();
delete pFile;
return false;
}
// Everything went fine, just need to update the saved path.
pFile->SetFileName(GetConfigFile());
// Make sure the lock is kept alive as long as we need it.
delete m_pLockFile;
m_pLockFile = pFile;
return true;
}
CString CZNC::MakeConfigHeader() {
return "// WARNING\n"
"//\n"
"// Do NOT edit this file while ZNC is running!\n"
"// Use webadmin or *controlpanel instead.\n"
"//\n"
"// Altering this file by hand will forfeit all support.\n"
"//\n"
"// But if you feel risky, you might want to read help on /znc "
"saveconfig and /znc rehash.\n"
"// Also check https://wiki.znc.in/Configuration\n";
}
bool CZNC::WriteNewConfig(const CString& sConfigFile) {
CString sAnswer, sUser, sNetwork;
VCString vsLines;
vsLines.push_back(MakeConfigHeader());
vsLines.push_back("Version = " + CString(VERSION_STR));
m_sConfigFile = ExpandConfigPath(sConfigFile);
if (CFile::Exists(m_sConfigFile)) {
CUtils::PrintStatus(
false, "WARNING: config [" + m_sConfigFile + "] already exists.");
}
CUtils::PrintMessage("");
CUtils::PrintMessage("-- Global settings --");
CUtils::PrintMessage("");
// Listen
#ifdef HAVE_IPV6
bool b6 = true;
#else
bool b6 = false;
#endif
CString sListenHost;
CString sURIPrefix;
bool bListenSSL = false;
unsigned int uListenPort = 0;
bool bSuccess;
// Unix sockets are not exposed in --makeconf by default, but it's possible
// to trigger this using env var. This is mostly useful for the integration
// test.
char* szListenUnixSocket = getenv("ZNC_LISTEN_UNIX_SOCKET");
if (!szListenUnixSocket) {
do {
bSuccess = true;
while (true) {
if (!CUtils::GetNumInput("Listen on port", uListenPort, 1025,
65534)) {
continue;
}
if (uListenPort == 6667 || uListenPort == 6697) {
CUtils::PrintStatus(
false,
"WARNING: Some web browsers reject ports "
"6667 and 6697. If you intend to");
CUtils::PrintStatus(
false,
"use ZNC's web interface, you might want "
"to use another port.");
if (!CUtils::GetBoolInput("Proceed anyway?", true)) {
continue;
}
}
break;
}
#ifdef HAVE_LIBSSL
bListenSSL = CUtils::GetBoolInput("Listen using SSL", bListenSSL);
#endif
#ifdef HAVE_IPV6
b6 = CUtils::GetBoolInput("Listen using both IPv4 and IPv6", b6);
#endif
// Don't ask for listen host, it may be configured later if needed.
CUtils::PrintAction("Verifying the listener");
CListener* pListener = new CTCPListener(
(unsigned short int)uListenPort, sListenHost, sURIPrefix,
bListenSSL, b6 ? ADDR_ALL : ADDR_IPV4ONLY,
CListener::ACCEPT_ALL);
if (!pListener->Listen()) {
CUtils::PrintStatus(false, FormatBindError());
bSuccess = false;
} else
CUtils::PrintStatus(true);
delete pListener;
} while (!bSuccess);
}
#ifdef HAVE_LIBSSL
CString sPemFile = GetPemLocation();
if (!CFile::Exists(sPemFile)) {
CUtils::PrintMessage("Unable to locate pem file: [" + sPemFile +
"], creating it");
WritePemFile();
}
#endif
vsLines.push_back("<Listener l>");
if (szListenUnixSocket) {
vsLines.push_back("\tPath = " + CString(szListenUnixSocket));
} else {
vsLines.push_back("\tPort = " + CString(uListenPort));
vsLines.push_back("\tIPv4 = true");
vsLines.push_back("\tIPv6 = " + CString(b6));
}
vsLines.push_back("\tSSL = " + CString(bListenSSL));
if (!sListenHost.empty()) {
vsLines.push_back("\tHost = " + sListenHost);
}
vsLines.push_back("</Listener>");
// !Listen
set<CModInfo> ssGlobalMods;
GetModules().GetDefaultMods(ssGlobalMods, CModInfo::GlobalModule);
vector<CString> vsGlobalModNames;
for (const CModInfo& Info : ssGlobalMods) {
vsGlobalModNames.push_back(Info.GetName());
vsLines.push_back("LoadModule = " + Info.GetName());
}
CUtils::PrintMessage(
"Enabled global modules [" +
CString(", ").Join(vsGlobalModNames.begin(), vsGlobalModNames.end()) +
"]");
// User
CUtils::PrintMessage("");
CUtils::PrintMessage("-- Admin user settings --");
CUtils::PrintMessage("");
vsLines.push_back("");
CString sNick;
do {
CUtils::GetInput("Username", sUser, "", "alphanumeric");
} while (!CUser::IsValidUsername(sUser));
vsLines.push_back("<User " + sUser + ">");
sAnswer = CUtils::AskSaltedHashPassForConfig();
vsLines.push_back(sAnswer);
vsLines.push_back("\tAdmin = true");
CUtils::GetInput("Nick", sNick, CUser::MakeCleanUsername(sUser));
vsLines.push_back("\tNick = " + sNick);
CUtils::GetInput("Alternate nick", sAnswer, sNick + "_");
if (!sAnswer.empty()) {
vsLines.push_back("\tAltNick = " + sAnswer);
}
CUtils::GetInput("Ident", sAnswer, sUser);
vsLines.push_back("\tIdent = " + sAnswer);
CUtils::GetInput("Real name", sAnswer, "", "optional");
if (!sAnswer.empty()) {
vsLines.push_back("\tRealName = " + sAnswer);
}
CUtils::GetInput("Bind host", sAnswer, "", "optional");
if (!sAnswer.empty()) {
vsLines.push_back("\tBindHost = " + sAnswer);
}
set<CModInfo> ssUserMods;
GetModules().GetDefaultMods(ssUserMods, CModInfo::UserModule);
vector<CString> vsUserModNames;
for (const CModInfo& Info : ssUserMods) {
vsUserModNames.push_back(Info.GetName());
vsLines.push_back("\tLoadModule = " + Info.GetName());
}
CUtils::PrintMessage(
"Enabled user modules [" +
CString(", ").Join(vsUserModNames.begin(), vsUserModNames.end()) + "]");
CUtils::PrintMessage("");
if (CUtils::GetBoolInput("Set up a network?", true)) {
vsLines.push_back("");
CUtils::PrintMessage("");
CUtils::PrintMessage("-- Network settings --");
CUtils::PrintMessage("");
do {
CUtils::GetInput("Name", sNetwork, "libera");
} while (!CIRCNetwork::IsValidNetwork(sNetwork));
vsLines.push_back("\t<Network " + sNetwork + ">");
set<CModInfo> ssNetworkMods;
GetModules().GetDefaultMods(ssNetworkMods, CModInfo::NetworkModule);
vector<CString> vsNetworkModNames;
for (const CModInfo& Info : ssNetworkMods) {
vsNetworkModNames.push_back(Info.GetName());
vsLines.push_back("\t\tLoadModule = " + Info.GetName());
}
CString sHost, sPass, sHint;
bool bSSL = false;
unsigned int uServerPort = 0;
if (sNetwork.Equals("libera")) {
sHost = "irc.libera.chat";
#ifdef HAVE_LIBSSL
bSSL = true;
#endif
} else {
sHint = "host only";
}
while (!CUtils::GetInput("Server host", sHost, sHost, sHint) ||
!CServer::IsValidHostName(sHost))
;
#ifdef HAVE_LIBSSL
bSSL = CUtils::GetBoolInput("Server uses SSL?", bSSL);
#endif
while (!CUtils::GetNumInput("Server port", uServerPort, 1, 65535,
bSSL ? 6697 : 6667));
CUtils::GetInput("Server password (probably empty)", sPass);
if (sHost.StartsWith("unix:")) {
vsLines.push_back("\t\tServer = " + sHost + " " + sPass);
} else {
vsLines.push_back("\t\tServer = " + sHost +
((bSSL) ? " +" : " ") + CString(uServerPort) +
" " + sPass);
}
CString sChans;
if (CUtils::GetInput("Initial channels", sChans)) {
vsLines.push_back("");
VCString vsChans;
sChans.Replace(",", " ");
sChans.Replace(";", " ");
sChans.Split(" ", vsChans, false, "", "", true, true);
for (const CString& sChan : vsChans) {
vsLines.push_back("\t\t<Chan " + sChan + ">");
vsLines.push_back("\t\t</Chan>");
}
}
CUtils::PrintMessage("Enabled network modules [" +
CString(", ").Join(vsNetworkModNames.begin(),
vsNetworkModNames.end()) +
"]");
vsLines.push_back("\t</Network>");
}
vsLines.push_back("</User>");
CUtils::PrintMessage("");
// !User
CFile File;
bool bFileOK, bFileOpen = false;
do {
CUtils::PrintAction("Writing config [" + m_sConfigFile + "]");
bFileOK = true;
if (CFile::Exists(m_sConfigFile)) {
if (!File.TryExLock(m_sConfigFile)) {
CUtils::PrintStatus(false,
"ZNC is currently running on this config.");
bFileOK = false;
} else {
File.Close();
CUtils::PrintStatus(false, "This config already exists.");
if (CUtils::GetBoolInput(
"Are you sure you want to overwrite it?", false))
CUtils::PrintAction("Overwriting config [" + m_sConfigFile +
"]");
else
bFileOK = false;
}
}
if (bFileOK) {
File.SetFileName(m_sConfigFile);
if (File.Open(O_WRONLY | O_CREAT | O_TRUNC, 0600)) {
bFileOpen = true;
} else {
CUtils::PrintStatus(false, "Unable to open file");
bFileOK = false;
}
}
if (!bFileOK) {
while (!CUtils::GetInput("Please specify an alternate location",
m_sConfigFile, "",
"or \"stdout\" for displaying the config"))
;
if (m_sConfigFile.Equals("stdout"))
bFileOK = true;
else
m_sConfigFile = ExpandConfigPath(m_sConfigFile);
}
} while (!bFileOK);
if (!bFileOpen) {
CUtils::PrintMessage("");
CUtils::PrintMessage("Printing the new config to stdout:");
CUtils::PrintMessage("");
cout << endl << "------------------------------------------------------"
"----------------------" << endl << endl;
}
for (const CString& sLine : vsLines) {
if (bFileOpen) {
File.Write(sLine + "\n");
} else {
cout << sLine << endl;
}
}
if (bFileOpen) {
File.Close();
if (File.HadError())
CUtils::PrintStatus(false,
"There was an error while writing the config");
else
CUtils::PrintStatus(true);
} else {
cout << endl << "------------------------------------------------------"
"----------------------" << endl << endl;
}
if (File.HadError()) {
bFileOpen = false;
CUtils::PrintMessage("Printing the new config to stdout instead:");
cout << endl << "------------------------------------------------------"
"----------------------" << endl << endl;
for (const CString& sLine : vsLines) {
cout << sLine << endl;
}
cout << endl << "------------------------------------------------------"
"----------------------" << endl << endl;
}
const CString sProtocol(bListenSSL ? "https" : "http");
const CString sSSL(bListenSSL ? "+" : "");
CUtils::PrintMessage("");
CUtils::PrintMessage(
"To connect to this ZNC you need to connect to it as your IRC server",
true);
CUtils::PrintMessage(
"using the port that you supplied. You have to supply your login info",
true);
CUtils::PrintMessage(
"as the IRC server password like this: user/network:pass.", true);
CUtils::PrintMessage("");
CUtils::PrintMessage("Try something like this in your IRC client...", true);
CUtils::PrintMessage("/server <znc_server_ip> " + sSSL +
CString(uListenPort) + " " + sUser + ":<pass>",
true);
CUtils::PrintMessage("");
CUtils::PrintMessage(
"To manage settings, users and networks, point your web browser to",
true);
CUtils::PrintMessage(
sProtocol + "://<znc_server_ip>:" + CString(uListenPort) + "/", true);
CUtils::PrintMessage("");
File.UnLock();
bool bWantLaunch = bFileOpen;
if (bWantLaunch) {
// "export ZNC_NO_LAUNCH_AFTER_MAKECONF=1" would cause znc --makeconf to
// not offer immediate launch.
// Useful for distros which want to create config when znc package is
// installed.
// See https://github.com/znc/znc/pull/257
char* szNoLaunch = getenv("ZNC_NO_LAUNCH_AFTER_MAKECONF");
if (szNoLaunch && *szNoLaunch == '1') {
bWantLaunch = false;
}
}
if (bWantLaunch) {
bWantLaunch = CUtils::GetBoolInput("Launch ZNC now?", true);
}
return bWantLaunch;
}
void CZNC::BackupConfigOnce(const CString& sSuffix) {
static bool didBackup = false;
if (didBackup) return;
didBackup = true;
CUtils::PrintAction("Creating a config backup");
CString sBackup = CDir::ChangeDir(m_sConfigFile, "../znc.conf." + sSuffix);
if (CFile::Copy(m_sConfigFile, sBackup))
CUtils::PrintStatus(true, sBackup);
else
CUtils::PrintStatus(false, strerror(errno));
}
bool CZNC::ParseConfig(const CString& sConfig, CString& sError) {
m_sConfigFile = ExpandConfigPath(sConfig, false);
CConfig config;
if (!ReadConfig(config, sError)) return false;
if (!LoadGlobal(config, sError)) return false;
if (!LoadUsers(config, sError)) return false;
return true;
}
bool CZNC::ReadConfig(CConfig& config, CString& sError) {
sError.clear();
CUtils::PrintAction("Opening config [" + m_sConfigFile + "]");
if (!CFile::Exists(m_sConfigFile)) {
sError = "No such file";
CUtils::PrintStatus(false, sError);
CUtils::PrintMessage(
"Restart ZNC with the --makeconf option if you wish to create this "
"config.");
return false;
}
if (!CFile::IsReg(m_sConfigFile)) {
sError = "Not a file";
CUtils::PrintStatus(false, sError);
return false;
}
CFile* pFile = new CFile(m_sConfigFile);
// need to open the config file Read/Write for fcntl()
// exclusive locking to work properly!
if (!pFile->Open(m_sConfigFile, O_RDWR)) {
sError = "Can not open config file";
CUtils::PrintStatus(false, sError);
delete pFile;
return false;
}
if (!pFile->TryExLock()) {
sError = "ZNC is already running on this config.";
CUtils::PrintStatus(false, sError);
delete pFile;
return false;
}
// (re)open the config file
delete m_pLockFile;
m_pLockFile = pFile;
CFile& File = *pFile;
if (!config.Parse(File, sError)) {
CUtils::PrintStatus(false, sError);
return false;
}
CUtils::PrintStatus(true);
// check if config is from old ZNC version and
// create a backup file if necessary
CString sSavedVersion;
config.FindStringEntry("version", sSavedVersion);
config.AddKeyValuePair("version", sSavedVersion);
if (sSavedVersion.empty()) {
CUtils::PrintError(
"Config does not contain a version identifier. It may be be too "
"old or corrupt.");
return false;
}
tuple<unsigned int, unsigned int> tSavedVersion =
make_tuple(sSavedVersion.Token(0, false, ".").ToUInt(),
sSavedVersion.Token(1, false, ".").ToUInt());
tuple<unsigned int, unsigned int> tCurrentVersion =
make_tuple(VERSION_MAJOR, VERSION_MINOR);
if (tSavedVersion < tCurrentVersion) {
CUtils::PrintMessage("Found old config from ZNC " + sSavedVersion +
". Saving a backup of it.");
BackupConfigOnce("pre-" + CString(VERSION_STR));
} else if (tSavedVersion > tCurrentVersion) {
CUtils::PrintError("Config was saved from ZNC " + sSavedVersion +
". It may or may not work with current ZNC " +
GetVersion());
}
return true;
}
bool CZNC::RehashConfig(CString& sError) {
ALLMODULECALL(OnPreRehash(), NOTHING);
CConfig config;
if (!ReadConfig(config, sError)) return false;
if (!LoadGlobal(config, sError)) return false;
// do not reload users - it's dangerous!
ALLMODULECALL(OnPostRehash(), NOTHING);
return true;
}
bool CZNC::LoadGlobal(CConfig& config, CString& sError) {
sError.clear();
CString sSavedVersion;
config.FindStringEntry("version", sSavedVersion);
tuple<unsigned int, unsigned int> tSavedVersion =
make_tuple(sSavedVersion.Token(0, false, ".").ToUInt(),
sSavedVersion.Token(1, false, ".").ToUInt());
MCString msModules; // Modules are queued for later loading
VCString vsList;
config.FindStringVector("loadmodule", vsList);
// Automatically load corecaps if config was upgraded from old version, but
// don't force it if user explicitly unloaded it
if (tSavedVersion < make_tuple(1, 9)) {
vsList.push_back("corecaps");
}
for (const CString& sModLine : vsList) {
CString sModName = sModLine.Token(0);
CString sArgs = sModLine.Token(1, true);
// compatibility for pre-1.0 configs
if (sModName == "saslauth" && tSavedVersion < make_tuple(0, 207)) {
CUtils::PrintMessage(
"saslauth module was renamed to cyrusauth. Loading cyrusauth "
"instead.");
sModName = "cyrusauth";
}
// end-compatibility for pre-1.0 configs
if (msModules.find(sModName) != msModules.end()) {
sError = "Module [" + sModName + "] already loaded";
CUtils::PrintError(sError);
return false;
}
CString sModRet;
CModule* pOldMod;
pOldMod = GetModules().FindModule(sModName);
if (!pOldMod) {
CUtils::PrintAction("Loading global module [" + sModName + "]");
bool bModRet =
GetModules().LoadModule(sModName, sArgs, CModInfo::GlobalModule,
nullptr, nullptr, sModRet);
CUtils::PrintStatus(bModRet, bModRet ? "" : sModRet);
if (!bModRet) {
sError = sModRet;
return false;
}
} else if (pOldMod->GetArgs() != sArgs) {
CUtils::PrintAction("Reloading global module [" + sModName + "]");
bool bModRet = GetModules().ReloadModule(sModName, sArgs, nullptr,
nullptr, sModRet);
CUtils::PrintStatus(bModRet, sModRet);
if (!bModRet) {
sError = sModRet;
return false;
}
} else
CUtils::PrintMessage("Module [" + sModName + "] already loaded.");
msModules[sModName] = sArgs;
}
m_vsMotd.clear();
config.FindStringVector("motd", vsList);
for (const CString& sMotd : vsList) {
AddMotd(sMotd);
}
if (config.FindStringVector("bindhost", vsList)) {
CUtils::PrintStatus(false,
"WARNING: the global BindHost list is deprecated. "
"Ignoring the following lines:");
for (const CString& sHost : vsList) {
CUtils::PrintStatus(false, "BindHost = " + sHost);
}
}
if (config.FindStringVector("vhost", vsList)) {
CUtils::PrintStatus(false,
"WARNING: the global vHost list is deprecated. "
"Ignoring the following lines:");
for (const CString& sHost : vsList) {
CUtils::PrintStatus(false, "vHost = " + sHost);
}
}
m_vsTrustedProxies.clear();
config.FindStringVector("trustedproxy", vsList);
for (const CString& sProxy : vsList) {
AddTrustedProxy(sProxy);
}
CString sVal;
if (config.FindStringEntry("pidfile", sVal)) m_sPidFile = sVal;
if (config.FindStringEntry("statusprefix", sVal)) m_sStatusPrefix = sVal;
if (config.FindStringEntry("sslcertfile", sVal)) m_sSSLCertFile = sVal;
if (config.FindStringEntry("sslkeyfile", sVal)) m_sSSLKeyFile = sVal;
if (config.FindStringEntry("ssldhparamfile", sVal))
m_sSSLDHParamFile = sVal;
if (config.FindStringEntry("sslciphers", sVal)) m_sSSLCiphers = sVal;
if (config.FindStringEntry("skin", sVal)) SetSkinName(sVal);
if (config.FindStringEntry("connectdelay", sVal))
SetConnectDelay(sVal.ToUInt());
if (config.FindStringEntry("serverthrottle", sVal))
m_sConnectThrottle.SetTTL(sVal.ToUInt() * 1000);
if (config.FindStringEntry("anoniplimit", sVal))
m_uiAnonIPLimit = sVal.ToUInt();
if (config.FindStringEntry("maxbuffersize", sVal))
m_uiMaxBufferSize = sVal.ToUInt();
if (config.FindStringEntry("protectwebsessions", sVal))
m_bProtectWebSessions = sVal.ToBool();
if (config.FindStringEntry("hideversion", sVal))
m_bHideVersion = sVal.ToBool();
if (config.FindStringEntry("authonlyviamodule", sVal))
m_bAuthOnlyViaModule = sVal.ToBool();
if (config.FindStringEntry("sslprotocols", sVal)) {
if (!SetSSLProtocols(sVal)) {
VCString vsProtocols = GetAvailableSSLProtocols();
CUtils::PrintError("Invalid SSLProtocols value [" + sVal + "]");
CUtils::PrintError(
"The syntax is [SSLProtocols = [+|-]<protocol> ...]");
CUtils::PrintError(
"Available protocols are [" +
CString(", ").Join(vsProtocols.begin(), vsProtocols.end()) +
"]");
return false;
}
}
if (config.FindStringEntry("configwritedelay", sVal))
m_uiConfigWriteDelay = sVal.ToUInt();
UnloadRemovedModules(msModules);
if (!LoadListeners(config, sError)) return false;
return true;
}
bool CZNC::LoadUsers(CConfig& config, CString& sError) {
sError.clear();
m_msUsers.clear();
CConfig::SubConfig subConf;
config.FindSubConfig("user", subConf);
for (const auto& subIt : subConf) {
const CString& sUsername = subIt.first;
CConfig* pSubConf = subIt.second.m_pSubConfig;
CUtils::PrintMessage("Loading user [" + sUsername + "]");
std::unique_ptr<CUser> pUser(new CUser(sUsername));
if (!m_sStatusPrefix.empty()) {
if (!pUser->SetStatusPrefix(m_sStatusPrefix)) {
sError = "Invalid StatusPrefix [" + m_sStatusPrefix +
"] Must be 1-5 chars, no spaces.";
CUtils::PrintError(sError);
return false;
}
}
if (!pUser->ParseConfig(pSubConf, sError)) {
CUtils::PrintError(sError);
return false;
}
if (!pSubConf->empty()) {
sError = "Unhandled lines in config for User [" + sUsername + "]!";
CUtils::PrintError(sError);
DumpConfig(pSubConf);
return false;
}
CString sErr;
CUser* pRawUser = pUser.release();
if (!AddUser(pRawUser, sErr, true)) {
sError = "Invalid user [" + sUsername + "] " + sErr;
CUtils::PrintError(sError);
pRawUser->SetBeingDeleted(true);
delete pRawUser;
return false;
}
}
if (m_msUsers.empty()) {
sError = "You must define at least one user in your config.";
CUtils::PrintError(sError);
return false;
}
return true;
}
bool CZNC::LoadListeners(CConfig& config, CString& sError) {
sError.clear();
// Delete all listeners
while (!m_vpListeners.empty()) {
delete m_vpListeners[0];
m_vpListeners.erase(m_vpListeners.begin());
}
// compatibility for pre-1.0 configs
const char* szListenerEntries[] = {"listen", "listen6", "listen4",
"listener", "listener6", "listener4"};
VCString vsList;
config.FindStringVector("loadmodule", vsList);
// This has to be after SSLCertFile is handled since it uses that value
for (const char* szEntry : szListenerEntries) {
config.FindStringVector(szEntry, vsList);
for (const CString& sListener : vsList) {
if (!AddListener(szEntry + CString(" ") + sListener, sError))
return false;
}
}
// end-compatibility for pre-1.0 configs
CConfig::SubConfig subConf;
config.FindSubConfig("listener", subConf);
for (const auto& subIt : subConf) {
CConfig* pSubConf = subIt.second.m_pSubConfig;
if (!AddListener(pSubConf, sError)) return false;
if (!pSubConf->empty()) {
sError = "Unhandled lines in Listener config!";
CUtils::PrintError(sError);
CZNC::DumpConfig(pSubConf);
return false;
}
}
if (m_vpListeners.empty()) {
sError = "You must supply at least one Listener in your config.";
CUtils::PrintError(sError);
return false;
}
return true;
}
void CZNC::UnloadRemovedModules(const MCString& msModules) {
// unload modules which are no longer in the config
set<CString> ssUnload;
for (CModule* pCurMod : GetModules()) {
if (msModules.find(pCurMod->GetModName()) == msModules.end())
ssUnload.insert(pCurMod->GetModName());
}
for (const CString& sMod : ssUnload) {
if (GetModules().UnloadModule(sMod))
CUtils::PrintMessage("Unloaded global module [" + sMod + "]");
else
CUtils::PrintMessage("Could not unload [" + sMod + "]");
}
}
void CZNC::DumpConfig(const CConfig* pConfig) {
CConfig::EntryMapIterator eit = pConfig->BeginEntries();
for (; eit != pConfig->EndEntries(); ++eit) {
const CString& sKey = eit->first;
const VCString& vsList = eit->second;
VCString::const_iterator it = vsList.begin();
for (; it != vsList.end(); ++it) {
CUtils::PrintError(sKey + " = " + *it);
}
}
CConfig::SubConfigMapIterator sit = pConfig->BeginSubConfigs();
for (; sit != pConfig->EndSubConfigs(); ++sit) {
const CString& sKey = sit->first;
const CConfig::SubConfig& sSub = sit->second;
CConfig::SubConfig::const_iterator it = sSub.begin();
for (; it != sSub.end(); ++it) {
CUtils::PrintError("SubConfig [" + sKey + " " + it->first + "]:");
DumpConfig(it->second.m_pSubConfig);
}
}
}
void CZNC::ClearTrustedProxies() { m_vsTrustedProxies.clear(); }
bool CZNC::AddTrustedProxy(const CString& sHost) {
if (sHost.empty()) {
return false;
}
for (const CString& sTrustedProxy : m_vsTrustedProxies) {
if (sTrustedProxy.Equals(sHost)) {
return false;
}
}
m_vsTrustedProxies.push_back(sHost);
return true;
}
bool CZNC::RemTrustedProxy(const CString& sHost) {
VCString::iterator it;
for (it = m_vsTrustedProxies.begin(); it != m_vsTrustedProxies.end();
++it) {
if (sHost.Equals(*it)) {
m_vsTrustedProxies.erase(it);
return true;
}
}
return false;
}
void CZNC::Broadcast(const CString& sMessage, bool bAdminOnly, CUser* pSkipUser,
CClient* pSkipClient) {
for (const auto& it : m_msUsers) {
if (bAdminOnly && !it.second->IsAdmin()) continue;
if (it.second != pSkipUser) {
// TODO: translate message to user's language
CString sMsg = sMessage;
bool bContinue = false;
USERMODULECALL(OnBroadcast(sMsg), it.second, nullptr, &bContinue);
if (bContinue) continue;
it.second->PutStatusNotice("*** " + sMsg, nullptr, pSkipClient);
}
}
}
CModule* CZNC::FindModule(const CString& sModName, const CString& sUsername) {
if (sUsername.empty()) {
return CZNC::Get().GetModules().FindModule(sModName);
}
CUser* pUser = FindUser(sUsername);
return (!pUser) ? nullptr : pUser->GetModules().FindModule(sModName);
}
CModule* CZNC::FindModule(const CString& sModName, CUser* pUser) {
if (pUser) {
return pUser->GetModules().FindModule(sModName);
}
return CZNC::Get().GetModules().FindModule(sModName);
}
bool CZNC::UpdateModule(const CString& sModule) {
CModule* pModule;
map<CUser*, CString> musLoaded;
map<CIRCNetwork*, CString> mnsLoaded;
// Unload the module for every user and network
for (const auto& it : m_msUsers) {
CUser* pUser = it.second;
pModule = pUser->GetModules().FindModule(sModule);
if (pModule) {
musLoaded[pUser] = pModule->GetArgs();
pUser->GetModules().UnloadModule(sModule);
}
// See if the user has this module loaded to a network
vector<CIRCNetwork*> vNetworks = pUser->GetNetworks();
for (CIRCNetwork* pNetwork : vNetworks) {
pModule = pNetwork->GetModules().FindModule(sModule);
if (pModule) {
mnsLoaded[pNetwork] = pModule->GetArgs();
pNetwork->GetModules().UnloadModule(sModule);
}
}
}
// Unload the global module
bool bGlobal = false;
CString sGlobalArgs;
pModule = GetModules().FindModule(sModule);
if (pModule) {
bGlobal = true;
sGlobalArgs = pModule->GetArgs();
GetModules().UnloadModule(sModule);
}
// Lets reload everything
bool bError = false;
CString sErr;
// Reload the global module
if (bGlobal) {
if (!GetModules().LoadModule(sModule, sGlobalArgs,
CModInfo::GlobalModule, nullptr, nullptr,
sErr)) {
DEBUG("Failed to reload [" << sModule << "] globally [" << sErr
<< "]");
bError = true;
}
}
// Reload the module for all users
for (const auto& it : musLoaded) {
CUser* pUser = it.first;
const CString& sArgs = it.second;
if (!pUser->GetModules().LoadModule(
sModule, sArgs, CModInfo::UserModule, pUser, nullptr, sErr)) {
DEBUG("Failed to reload [" << sModule << "] for ["
<< pUser->GetUsername() << "] [" << sErr
<< "]");
bError = true;
}
}
// Reload the module for all networks
for (const auto& it : mnsLoaded) {
CIRCNetwork* pNetwork = it.first;
const CString& sArgs = it.second;
if (!pNetwork->GetModules().LoadModule(
sModule, sArgs, CModInfo::NetworkModule, pNetwork->GetUser(),
pNetwork, sErr)) {
DEBUG("Failed to reload ["
<< sModule << "] for [" << pNetwork->GetUser()->GetUsername()
<< "/" << pNetwork->GetName() << "] [" << sErr << "]");
bError = true;
}
}
return !bError;
}
CUser* CZNC::FindUser(const CString& sUsername) {
map<CString, CUser*>::iterator it = m_msUsers.find(sUsername);
if (it != m_msUsers.end()) {
return it->second;
}
return nullptr;
}
bool CZNC::DeleteUser(const CString& sUsername) {
CUser* pUser = FindUser(sUsername);
if (!pUser) {
return false;
}
m_msDelUsers[pUser->GetUsername()] = pUser;
return true;
}
bool CZNC::AddUser(CUser* pUser, CString& sErrorRet, bool bStartup) {
if (FindUser(pUser->GetUsername()) != nullptr) {
sErrorRet = t_s("User already exists");
DEBUG("User [" << pUser->GetUsername() << "] - already exists");
return false;
}
if (!pUser->IsValid(sErrorRet)) {
DEBUG("Invalid user [" << pUser->GetUsername() << "] - [" << sErrorRet
<< "]");
return false;
}
bool bFailed = false;
// do not call OnAddUser hook during ZNC startup
if (!bStartup) {
GLOBALMODULECALL(OnAddUser(*pUser, sErrorRet), &bFailed);
}
if (bFailed) {
DEBUG("AddUser [" << pUser->GetUsername() << "] aborted by a module ["
<< sErrorRet << "]");
return false;
}
m_msUsers[pUser->GetUsername()] = pUser;
return true;
}
CListener* CZNC::FindListener(u_short uPort, const CString& sBindHost,
EAddrType eAddr) {
for (CListener* pListener : m_vpListeners) {
CTCPListener* pTCPListener = dynamic_cast<CTCPListener*>(pListener);
if (!pTCPListener) continue;
if (pTCPListener->GetPort() != uPort) continue;
if (pTCPListener->GetBindHost() != sBindHost) continue;
if (pTCPListener->GetAddrType() != eAddr) continue;
return pListener;
}
return nullptr;
}
CListener* CZNC::FindUnixListener(const CString& sPath) {
for (CListener* pListener : m_vpListeners) {
CUnixListener* pUnixListener = dynamic_cast<CUnixListener*>(pListener);
if (!pUnixListener) continue;
if (pUnixListener->GetPath() != sPath) continue;
return pListener;
}
return nullptr;
}
bool CZNC::AddListener(const CString& sLine, CString& sError) {
CString sName = sLine.Token(0);
CString sValue = sLine.Token(1, true);
EAddrType eAddr = ADDR_ALL;
if (sName.Equals("Listen4") || sName.Equals("Listen") ||
sName.Equals("Listener4")) {
eAddr = ADDR_IPV4ONLY;
}
if (sName.Equals("Listener6")) {
eAddr = ADDR_IPV6ONLY;
}
CListener::EAcceptType eAccept = CListener::ACCEPT_ALL;
if (sValue.TrimPrefix("irc_only "))
eAccept = CListener::ACCEPT_IRC;
else if (sValue.TrimPrefix("web_only "))
eAccept = CListener::ACCEPT_HTTP;
bool bSSL = false;
CString sPort;
CString sBindHost;
if (ADDR_IPV4ONLY == eAddr) {
sValue.Replace(":", " ");
}
if (sValue.Contains(" ")) {
sBindHost = sValue.Token(0, false, " ");
sPort = sValue.Token(1, true, " ");
} else {
sPort = sValue;
}
if (sPort.TrimPrefix("+")) {
bSSL = true;
}
// No support for URIPrefix for old-style configs.
CString sURIPrefix;
unsigned short uPort = sPort.ToUShort();
return AddListener(uPort, sBindHost, sURIPrefix, bSSL, eAddr, eAccept,
sError);
}
bool CZNC::CheckSslAndPemFile(bool bSSL, CString& sError) {
#ifndef HAVE_LIBSSL
if (bSSL) {
sError = t_s("SSL is not enabled");
CUtils::PrintStatus(false, sError);
return false;
}
#else
CString sPemFile = GetPemLocation();
if (bSSL && !CFile::Exists(sPemFile)) {
sError = t_f("Unable to locate pem file: {1}")(sPemFile);
CUtils::PrintStatus(false, sError);
// If stdin is e.g. /dev/null and we call GetBoolInput(),
// we are stuck in an endless loop!
if (isatty(0) &&
CUtils::GetBoolInput("Would you like to create a new pem file?",
true)) {
sError.clear();
WritePemFile();
} else {
return false;
}
}
#endif
return true;
}
bool CZNC::AddTCPListener(unsigned short uPort, const CString& sBindHost,
const CString& sURIPrefix, bool bSSL, EAddrType eAddr,
CListener::EAcceptType eAccept, CString& sError) {
CString sHostComment;
if (!sBindHost.empty()) {
sHostComment = " on host [" + sBindHost + "]";
}
CString sIPV6Comment;
switch (eAddr) {
case ADDR_ALL:
sIPV6Comment = "";
break;
case ADDR_IPV4ONLY:
sIPV6Comment = " using ipv4";
break;
case ADDR_IPV6ONLY:
sIPV6Comment = " using ipv6";
}
CUtils::PrintAction("Binding to port [" + CString((bSSL) ? "+" : "") +
CString(uPort) + "]" + sHostComment + sIPV6Comment);
#ifndef HAVE_IPV6
if (ADDR_IPV6ONLY == eAddr) {
sError = t_s("IPv6 is not enabled");
CUtils::PrintStatus(false, sError);
return false;
}
#endif
if (!CheckSslAndPemFile(bSSL, sError)) return false;
if (!uPort) {
sError = t_s("Invalid port");
CUtils::PrintStatus(false, sError);
return false;
}
CListener* pListener =
new CTCPListener(uPort, sBindHost, sURIPrefix, bSSL, eAddr, eAccept);
return FinishAddingListener(pListener, sError);
}
bool CZNC::AddUnixListener(const CString& sPath, const CString& sURIPrefix,
bool bSSL, CListener::EAcceptType eAccept,
CString& sError) {
CUtils::PrintAction("Binding to path [" + sPath + "]" + (bSSL ? " with SSL" : ""));
if (!CheckSslAndPemFile(bSSL, sError)) return false;
CListener* pListener =
new CUnixListener(sPath, sURIPrefix, bSSL, eAccept);
return FinishAddingListener(pListener, sError);
}
bool CZNC::FinishAddingListener(CListener* pListener, CString& sError) {
if (!pListener->Listen()) {
sError = FormatBindError();
CUtils::PrintStatus(false, sError);
delete pListener;
return false;
}
m_vpListeners.push_back(pListener);
CUtils::PrintStatus(true);
return true;
}
bool CZNC::AddListener(CConfig* pConfig, CString& sError) {
CString sBindHost;
CString sURIPrefix;
CString sPath;
bool bSSL;
bool b4;
#ifdef HAVE_IPV6
bool b6 = true;
#else
bool b6 = false;
#endif
bool bIRC;
bool bWeb;
unsigned short uPort;
bool bTcpListener = true;
if (!pConfig->FindUShortEntry("port", uPort)) {
bTcpListener = false;
if (!pConfig->FindStringEntry("path", sPath)) {
sError = "No port and no path given";
CUtils::PrintError(sError);
return false;
}
}
pConfig->FindBoolEntry("ssl", bSSL, false);
pConfig->FindBoolEntry("allowirc", bIRC, true);
pConfig->FindBoolEntry("allowweb", bWeb, true);
pConfig->FindStringEntry("uriprefix", sURIPrefix);
CListener::EAcceptType eAccept;
if (bIRC && bWeb) {
eAccept = CListener::ACCEPT_ALL;
} else if (bIRC && !bWeb) {
eAccept = CListener::ACCEPT_IRC;
} else if (!bIRC && bWeb) {
eAccept = CListener::ACCEPT_HTTP;
} else {
sError = "Either Web or IRC or both should be selected";
CUtils::PrintError(sError);
return false;
}
// URIPrefix must start with a slash and end without one.
if (!sURIPrefix.empty()) {
if (!sURIPrefix.StartsWith("/")) {
sURIPrefix = "/" + sURIPrefix;
}
if (sURIPrefix.EndsWith("/")) {
sURIPrefix.TrimRight("/");
}
}
if (bTcpListener) {
pConfig->FindStringEntry("host", sBindHost);
pConfig->FindBoolEntry("ipv4", b4, true);
pConfig->FindBoolEntry("ipv6", b6, b6);
EAddrType eAddr;
if (b4 && b6) {
eAddr = ADDR_ALL;
} else if (b4 && !b6) {
eAddr = ADDR_IPV4ONLY;
} else if (!b4 && b6) {
eAddr = ADDR_IPV6ONLY;
} else {
sError = "No address family given";
CUtils::PrintError(sError);
return false;
}
return AddTCPListener(uPort, sBindHost, sURIPrefix, bSSL, eAddr,
eAccept, sError);
}
return AddUnixListener(sPath, sURIPrefix, bSSL, eAccept, sError);
}
bool CZNC::AddListener(CListener* pListener) {
if (!pListener->GetRealListener()) {
// Listener doesn't actually listen
delete pListener;
return false;
}
// We don't check if there is an identical listener already listening
// since one can't listen on e.g. the same port multiple times
m_vpListeners.push_back(pListener);
return true;
}
bool CZNC::DelListener(CListener* pListener) {
auto it = std::find(m_vpListeners.begin(), m_vpListeners.end(), pListener);
if (it != m_vpListeners.end()) {
m_vpListeners.erase(it);
delete pListener;
return true;
}
return false;
}
CString CZNC::FormatBindError() {
CString sError = (errno == 0 ? t_s(("unknown error, check the host name"))
: CString(strerror(errno)));
return t_f("Unable to bind: {1}")(sError);
}
static CZNC* s_pZNC = nullptr;
void CZNC::CreateInstance() {
if (s_pZNC) abort();
s_pZNC = new CZNC();
}
CZNC& CZNC::Get() { return *s_pZNC; }
void CZNC::DestroyInstance() {
delete s_pZNC;
s_pZNC = nullptr;
}
CZNC::TrafficStatsMap CZNC::GetTrafficStats(TrafficStatsPair& Users,
TrafficStatsPair& ZNC,
TrafficStatsPair& Total) {
TrafficStatsMap ret;
unsigned long long uiUsers_in, uiUsers_out, uiZNC_in, uiZNC_out;
const map<CString, CUser*>& msUsers = CZNC::Get().GetUserMap();
uiUsers_in = uiUsers_out = 0;
uiZNC_in = BytesRead();
uiZNC_out = BytesWritten();
for (const auto& it : msUsers) {
ret[it.first] =
TrafficStatsPair(it.second->BytesRead(), it.second->BytesWritten());
uiUsers_in += it.second->BytesRead();
uiUsers_out += it.second->BytesWritten();
}
for (Csock* pSock : m_Manager) {
CUser* pUser = nullptr;
if (pSock->GetSockName().StartsWith("IRC::")) {
pUser = ((CIRCSock*)pSock)->GetNetwork()->GetUser();
} else if (pSock->GetSockName().StartsWith("USR::")) {
pUser = ((CClient*)pSock)->GetUser();
}
if (pUser) {
ret[pUser->GetUsername()].first += pSock->GetBytesRead();
ret[pUser->GetUsername()].second += pSock->GetBytesWritten();
uiUsers_in += pSock->GetBytesRead();
uiUsers_out += pSock->GetBytesWritten();
} else {
uiZNC_in += pSock->GetBytesRead();
uiZNC_out += pSock->GetBytesWritten();
}
}
Users = TrafficStatsPair(uiUsers_in, uiUsers_out);
ZNC = TrafficStatsPair(uiZNC_in, uiZNC_out);
Total = TrafficStatsPair(uiUsers_in + uiZNC_in, uiUsers_out + uiZNC_out);
return ret;
}
CZNC::TrafficStatsMap CZNC::GetNetworkTrafficStats(const CString& sUsername,
TrafficStatsPair& Total) {
TrafficStatsMap Networks;
CUser* pUser = FindUser(sUsername);
if (pUser) {
for (const CIRCNetwork* pNetwork : pUser->GetNetworks()) {
Networks[pNetwork->GetName()].first = pNetwork->BytesRead();
Networks[pNetwork->GetName()].second = pNetwork->BytesWritten();
Total.first += pNetwork->BytesRead();
Total.second += pNetwork->BytesWritten();
}
for (Csock* pSock : m_Manager) {
CIRCNetwork* pNetwork = nullptr;
if (pSock->GetSockName().StartsWith("IRC::")) {
pNetwork = ((CIRCSock*)pSock)->GetNetwork();
} else if (pSock->GetSockName().StartsWith("USR::")) {
pNetwork = ((CClient*)pSock)->GetNetwork();
}
if (pNetwork && pNetwork->GetUser() == pUser) {
Networks[pNetwork->GetName()].first = pSock->GetBytesRead();
Networks[pNetwork->GetName()].second = pSock->GetBytesWritten();
Total.first += pSock->GetBytesRead();
Total.second += pSock->GetBytesWritten();
}
}
}
return Networks;
}
void CZNC::AuthUser(std::shared_ptr<CAuthBase> AuthClass) {
// TODO unless the auth module calls it, CUser::IsHostAllowed() is not
// honoured
bool bReturn = false;
GLOBALMODULECALL(OnLoginAttempt(AuthClass), &bReturn);
if (bReturn) return;
CUser* pUser = FindUser(AuthClass->GetUsername());
if (!pUser || !pUser->CheckPass(AuthClass->GetPassword())) {
AuthClass->RefuseLogin("Invalid Password");
return;
}
CString sHost = AuthClass->GetRemoteIP();
if (!pUser->IsHostAllowed(sHost)) {
AuthClass->RefuseLogin("Your host [" + sHost + "] is not allowed");
return;
}
AuthClass->AcceptLogin(*pUser);
}
class CConnectQueueTimer : public CCron {
public:
CConnectQueueTimer(int iSecs) : CCron() {
SetName("Connect users");
Start(iSecs);
// Don't wait iSecs seconds for first timer run
m_bRunOnNextCall = true;
}
~CConnectQueueTimer() override {
// This is only needed when ZNC shuts down:
// CZNC::~CZNC() sets its CConnectQueueTimer pointer to nullptr and
// calls the manager's Cleanup() which destroys all sockets and
// timers. If something calls CZNC::EnableConnectQueue() here
// (e.g. because a CIRCSock is destroyed), the socket manager
// deletes that timer almost immediately, but CZNC now got a
// dangling pointer to this timer which can crash later on.
//
// Unlikely but possible ;)
CZNC::Get().LeakConnectQueueTimer(this);
}
protected:
void RunJob() override {
list<CIRCNetwork*> ConnectionQueue;
list<CIRCNetwork*>& RealConnectionQueue =
CZNC::Get().GetConnectionQueue();
// Problem: If a network can't connect right now because e.g. it
// is throttled, it will re-insert itself into the connection
// queue. However, we must only give each network a single
// chance during this timer run.
//
// Solution: We move the connection queue to our local list at
// the beginning and work from that.
ConnectionQueue.swap(RealConnectionQueue);
while (!ConnectionQueue.empty()) {
CIRCNetwork* pNetwork = ConnectionQueue.front();
ConnectionQueue.pop_front();
if (pNetwork->Connect()) {
break;
}
}
/* Now re-insert anything that is left in our local list into
* the real connection queue.
*/
RealConnectionQueue.splice(RealConnectionQueue.begin(),
ConnectionQueue);
if (RealConnectionQueue.empty()) {
DEBUG("ConnectQueueTimer done");
CZNC::Get().DisableConnectQueue();
}
}
};
void CZNC::SetConnectDelay(unsigned int i) {
if (i < 1) {
// Don't hammer server with our failed connects
i = 1;
}
if (m_uiConnectDelay != i && m_pConnectQueueTimer != nullptr) {
m_pConnectQueueTimer->Start(i);
}
m_uiConnectDelay = i;
}
VCString CZNC::GetAvailableSSLProtocols() {
// NOTE: keep in sync with SetSSLProtocols()
return {"SSLv2", "SSLv3", "TLSv1", "TLSV1.1", "TLSv1.2"};
}
bool CZNC::SetSSLProtocols(const CString& sProtocols) {
VCString vsProtocols;
sProtocols.Split(" ", vsProtocols, false, "", "", true, true);
unsigned int uDisabledProtocols = Csock::EDP_SSL;
for (CString& sProtocol : vsProtocols) {
unsigned int uFlag = 0;
bool bEnable = sProtocol.TrimPrefix("+");
bool bDisable = sProtocol.TrimPrefix("-");
// NOTE: keep in sync with GetAvailableSSLProtocols()
if (sProtocol.Equals("All")) {
uFlag = ~0;
} else if (sProtocol.Equals("SSLv2")) {
uFlag = Csock::EDP_SSLv2;
} else if (sProtocol.Equals("SSLv3")) {
uFlag = Csock::EDP_SSLv3;
} else if (sProtocol.Equals("TLSv1")) {
uFlag = Csock::EDP_TLSv1;
} else if (sProtocol.Equals("TLSv1.1")) {
uFlag = Csock::EDP_TLSv1_1;
} else if (sProtocol.Equals("TLSv1.2")) {
uFlag = Csock::EDP_TLSv1_2;
} else {
return false;
}
if (bEnable) {
uDisabledProtocols &= ~uFlag;
} else if (bDisable) {
uDisabledProtocols |= uFlag;
} else {
uDisabledProtocols = ~uFlag;
}
}
m_sSSLProtocols = sProtocols;
m_uDisabledSSLProtocols = uDisabledProtocols;
return true;
}
void CZNC::EnableConnectQueue() {
if (!m_pConnectQueueTimer && !m_uiConnectPaused &&
!m_lpConnectQueue.empty()) {
m_pConnectQueueTimer = new CConnectQueueTimer(m_uiConnectDelay);
GetManager().AddCron(m_pConnectQueueTimer);
}
}
void CZNC::DisableConnectQueue() {
if (m_pConnectQueueTimer) {
// This will kill the cron
m_pConnectQueueTimer->Stop();
m_pConnectQueueTimer = nullptr;
}
}
void CZNC::PauseConnectQueue() {
DEBUG("Connection queue paused");
m_uiConnectPaused++;
if (m_pConnectQueueTimer) {
m_pConnectQueueTimer->Pause();
}
}
void CZNC::ResumeConnectQueue() {
DEBUG("Connection queue resumed");
m_uiConnectPaused--;
EnableConnectQueue();
if (m_pConnectQueueTimer) {
m_pConnectQueueTimer->UnPause();
}
}
void CZNC::ForceEncoding() {
m_uiForceEncoding++;
#ifdef HAVE_ICU
for (Csock* pSock : GetManager()) {
pSock->SetEncoding(FixupEncoding(pSock->GetEncoding()));
}
#endif
}
void CZNC::UnforceEncoding() { m_uiForceEncoding--; }
bool CZNC::IsForcingEncoding() const { return m_uiForceEncoding; }
CString CZNC::FixupEncoding(const CString& sEncoding) const {
if (!m_uiForceEncoding) {
return sEncoding;
}
if (sEncoding.empty()) {
return "UTF-8";
}
const char* sRealEncoding = sEncoding.c_str();
if (sEncoding[0] == '*' || sEncoding[0] == '^') {
sRealEncoding++;
}
if (!*sRealEncoding) {
return "UTF-8";
}
#ifdef HAVE_ICU
UErrorCode e = U_ZERO_ERROR;
UConverter* cnv = ucnv_open(sRealEncoding, &e);
if (cnv) {
ucnv_close(cnv);
}
if (U_FAILURE(e)) {
return "UTF-8";
}
#endif
return sEncoding;
}
void CZNC::AddNetworkToQueue(CIRCNetwork* pNetwork) {
// Make sure we are not already in the queue
if (std::find(m_lpConnectQueue.begin(), m_lpConnectQueue.end(), pNetwork) !=
m_lpConnectQueue.end()) {
return;
}
m_lpConnectQueue.push_back(pNetwork);
EnableConnectQueue();
}
void CZNC::LeakConnectQueueTimer(CConnectQueueTimer* pTimer) {
if (m_pConnectQueueTimer == pTimer) m_pConnectQueueTimer = nullptr;
}
bool CZNC::WaitForChildLock() { return m_pLockFile && m_pLockFile->ExLock(); }
void CZNC::DisableConfigTimer() {
if (m_pConfigTimer) {
m_pConfigTimer->Stop();
m_pConfigTimer = nullptr;
}
}