diff --git a/.gitignore b/.gitignore index aa2f4964..4b760b40 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,8 @@ Makefile /modules/modpython/*.pyc /modules/*.pyc +/test/ConfigTest + # Compiled Object files *.o diff --git a/Chan.cpp b/Chan.cpp index 8d26391f..093e6fec 100644 --- a/Chan.cpp +++ b/Chan.cpp @@ -10,8 +10,9 @@ #include "IRCSock.h" #include "User.h" #include "znc.h" +#include "Config.h" -CChan::CChan(const CString& sName, CUser* pUser, bool bInConfig) { +CChan::CChan(const CString& sName, CUser* pUser, bool bInConfig, CConfig *pConfig) { m_sName = sName.Token(0); m_sKey = sName.Token(1); m_pUser = pUser; @@ -27,6 +28,23 @@ CChan::CChan(const CString& sName, CUser* pUser, bool bInConfig) { m_bKeepBuffer = m_pUser->KeepBuffer(); m_bDisabled = false; Reset(); + + if (pConfig) { + CString sValue; + if (pConfig->FindStringEntry("buffer", sValue)) + SetBufferCount(sValue.ToUInt(), true); + if (pConfig->FindStringEntry("keepbuffer", sValue)) + SetKeepBuffer(sValue.ToBool()); + if (pConfig->FindStringEntry("detached", sValue)) + SetDetached(sValue.ToBool()); + if (pConfig->FindStringEntry("autocycle", sValue)) + if (sValue.Equals("true")) + CUtils::PrintError("WARNING: AutoCycle has been removed, instead try -> LoadModule = autocycle " + sName); + if (pConfig->FindStringEntry("key", sValue)) + SetKey(sValue); + if (pConfig->FindStringEntry("modes", sValue)) + SetDefaultModes(sValue); + } } CChan::~CChan() { diff --git a/Chan.h b/Chan.h index e781ddba..0064210d 100644 --- a/Chan.h +++ b/Chan.h @@ -24,6 +24,7 @@ using std::set; // Forward Declarations class CUser; class CClient; +class CConfig; // !Forward Declarations class CChan { @@ -51,7 +52,7 @@ public: M_Except = 'e' } EModes; - CChan(const CString& sName, CUser* pUser, bool bInConfig); + CChan(const CString& sName, CUser* pUser, bool bInConfig, CConfig *pConfig = NULL); ~CChan(); void Reset(); diff --git a/Config.cpp b/Config.cpp new file mode 100644 index 00000000..c17e66b2 --- /dev/null +++ b/Config.cpp @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2004-2011 See the AUTHORS file for details. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published + * by the Free Software Foundation. + */ + +#include "Config.h" +#include +#include + +struct ConfigStackEntry { + CString sTag; + CString sName; + CConfig Config; + + ConfigStackEntry(const CString& Tag, const CString Name) { + sTag = Tag; + sName = Name; + } +}; + +CConfig::CConfigEntry::CConfigEntry() + : m_pSubConfig(NULL) { +} + +CConfig::CConfigEntry::CConfigEntry(const CConfig& Config) + : m_pSubConfig(new CConfig(Config)) { +} + +CConfig::CConfigEntry::CConfigEntry(const CConfigEntry& other) + : m_pSubConfig(NULL) { + if (other.m_pSubConfig) + m_pSubConfig = new CConfig(*other.m_pSubConfig); +} + +CConfig::CConfigEntry::~CConfigEntry() +{ + delete m_pSubConfig; +} + +CConfig::CConfigEntry& CConfig::CConfigEntry::operator=(const CConfigEntry& other) { + delete m_pSubConfig; + if (other.m_pSubConfig) + m_pSubConfig = new CConfig(*other.m_pSubConfig); + else + m_pSubConfig = NULL; + return *this; +} + +bool CConfig::Parse(CFile& file, CString& sErrorMsg) +{ + CString sLine; + unsigned int uLineNum = 0; + CConfig *pActiveConfig = this; + std::stack ConfigStack; + bool bCommented = false; // support for /**/ style comments + + if (!file.Seek(0)) + return "Could not seek to the beginning of the config."; + + while (file.ReadLine(sLine)) { + uLineNum++; + +#define ERROR(arg) do { \ + std::stringstream stream; \ + stream << "Error on line " << uLineNum << ": " << arg; \ + sErrorMsg = stream.str(); \ + m_SubConfigs.clear(); \ + m_ConfigEntries.clear(); \ + return false; \ +} while (0) + + // Remove all leading spaces and trailing line endings + sLine.TrimLeft(); + sLine.TrimRight("\r\n"); + + if ((sLine.empty()) || (sLine[0] == '#') || (sLine.Left(2) == "//")) { + continue; + } + + if (sLine.Left(2) == "/*") { + if (sLine.Right(2) != "*/") { + bCommented = true; + } + + continue; + } + + if (bCommented) { + if (sLine.Right(2) == "*/") { + bCommented = false; + } + + continue; + } + + if ((sLine.Left(1) == "<") && (sLine.Right(1) == ">")) { + sLine.LeftChomp(); + sLine.RightChomp(); + sLine.Trim(); + + CString sTag = sLine.Token(0); + CString sValue = sLine.Token(1, true); + + sTag.Trim(); + sValue.Trim(); + + if (sTag.Left(1) == "/") { + sTag = sTag.substr(1); + + if (!sValue.empty()) + ERROR("Malformated closing tag. Expected \"\"."); + if (ConfigStack.empty()) + ERROR("Closing tag \"" << sTag << "\" which is not open."); + + const struct ConfigStackEntry& entry = ConfigStack.top(); + CConfig myConfig(entry.Config); + CString sName(entry.sName); + + if (!sTag.Equals(entry.sTag)) + ERROR("Closing tag \"" << sTag << "\" which is not open."); + + // This breaks entry + ConfigStack.pop(); + + if (ConfigStack.empty()) + pActiveConfig = this; + else + pActiveConfig = &ConfigStack.top().Config; + + SubConfig &conf = pActiveConfig->m_SubConfigs[sTag.AsLower()]; + SubConfig::const_iterator it = conf.find(sName); + + if (it != conf.end()) + ERROR("Duplicate entry for tag \"" << sTag << "\" name \"" << sName << "\"."); + + conf[sName] = CConfigEntry(myConfig); + } else { + if (sValue.empty()) + ERROR("Empty block name at begin of block."); + ConfigStack.push(ConfigStackEntry(sTag.AsLower(), sValue)); + pActiveConfig = &ConfigStack.top().Config; + } + + continue; + } + + // If we have a regular line, figure out where it goes + CString sName = sLine.Token(0, false, "="); + CString sValue = sLine.Token(1, true, "="); + + // Only remove the first space, people might want + // leading spaces (e.g. in the MOTD). + if (sValue.Left(1) == " ") + sValue.LeftChomp(); + + // We don't have any names with spaces, trim all + // leading/trailing spaces. + sName.Trim(); + + if (sName.empty() || sValue.empty()) + ERROR("Malformed line"); + + CString sNameLower = sName.AsLower(); + pActiveConfig->m_ConfigEntries[sNameLower].push_back(sValue); + } + + if (bCommented) + ERROR("Comment not closed at end of file."); + + if (!ConfigStack.empty()) { + const CString& sTag = ConfigStack.top().sTag; + ERROR("Not all tags are closed at the end of the file. Inner-most open tag is \"" << sTag << "\"."); + } + + return true; +} diff --git a/Config.h b/Config.h new file mode 100644 index 00000000..e8853bbe --- /dev/null +++ b/Config.h @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2004-2011 See the AUTHORS file for details. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published + * by the Free Software Foundation. + */ + +#ifndef CONFIG_H +#define CONFIG_H + +#include "ZNCString.h" +#include "FileUtils.h" + +class CConfig { +public: + struct CConfigEntry { + CConfigEntry(); + CConfigEntry(const CConfig& Config); + CConfigEntry(const CConfigEntry& other); + ~CConfigEntry(); + CConfigEntry& operator=(const CConfigEntry& other); + + CConfig* m_pSubConfig; + }; + + typedef map EntryMap; + typedef map SubConfig; + typedef map SubConfigMap; + + typedef EntryMap::const_iterator EntryMapIterator; + typedef SubConfigMap::const_iterator SubConfigMapIterator; + + EntryMapIterator BeginEntries() const { + return m_ConfigEntries.begin(); + } + EntryMapIterator EndEntries() const { + return m_ConfigEntries.end(); + } + + SubConfigMapIterator BeginSubConfigs() const { + return m_SubConfigs.begin(); + } + SubConfigMapIterator EndSubConfigs() const { + return m_SubConfigs.end(); + } + + bool FindStringVector(const CString& sName, VCString& vsList) { + EntryMap::iterator it = m_ConfigEntries.find(sName); + vsList.clear(); + if (it == m_ConfigEntries.end()) + return false; + vsList = it->second; + m_ConfigEntries.erase(it); + return true; + } + + bool FindStringEntry(const CString& sName, CString& sRes) { + EntryMap::iterator it = m_ConfigEntries.find(sName); + sRes.clear(); + if (it == m_ConfigEntries.end() || it->second.empty()) + return false; + sRes = it->second.front(); + it->second.erase(it->second.begin()); + if (it->second.empty()) + m_ConfigEntries.erase(it); + return true; + } + + bool FindSubConfig(const CString& sName, SubConfig& Config) { + SubConfigMap::iterator it = m_SubConfigs.find(sName); + if (it == m_SubConfigs.end()) { + Config.clear(); + return false; + } + Config = it->second; + m_SubConfigs.erase(it); + return true; + } + + bool empty() const { + return m_ConfigEntries.empty() && m_SubConfigs.empty(); + } + + bool Parse(CFile& file, CString& sErrorMsg); + +private: + EntryMap m_ConfigEntries; + SubConfigMap m_SubConfigs; +}; + +#endif // !CONFIG_H diff --git a/Makefile.in b/Makefile.in index 1873c2a7..6111cd64 100644 --- a/Makefile.in +++ b/Makefile.in @@ -31,7 +31,7 @@ INSTALL_DATA := @INSTALL_DATA@ LIB_SRCS := ZNCString.cpp Csocket.cpp znc.cpp User.cpp IRCSock.cpp Client.cpp DCCBounce.cpp \ DCCSock.cpp Chan.cpp Nick.cpp Server.cpp Modules.cpp MD5.cpp Buffer.cpp Utils.cpp \ FileUtils.cpp HTTPSock.cpp Template.cpp ClientCommand.cpp Socket.cpp SHA256.cpp \ - WebModules.cpp Listener.cpp + WebModules.cpp Listener.cpp Config.cpp BIN_SRCS := main.cpp LIB_OBJS := $(patsubst %cpp,%o,$(LIB_SRCS)) BIN_OBJS := $(patsubst %cpp,%o,$(BIN_SRCS)) diff --git a/User.cpp b/User.cpp index b3c28f51..30a1e8cd 100644 --- a/User.cpp +++ b/User.cpp @@ -8,6 +8,7 @@ #include "User.h" #include "Chan.h" +#include "Config.h" #include "DCCSock.h" #include "IRCSock.h" #include "Server.h" @@ -111,6 +112,217 @@ CUser::~CUser() { CZNC::Get().GetManager().DelCronByAddr(m_pUserTimer); } +template +struct TOption { + const char *name; + void (CUser::*pSetter)(T); +}; + +bool CUser::ParseConfig(CConfig* pConfig, CString& sError) { + TOption StringOptions[] = { + { "nick", &CUser::SetNick }, + { "quitmsg", &CUser::SetQuitMsg }, + { "altnick", &CUser::SetAltNick }, + { "ident", &CUser::SetIdent }, + { "realname", &CUser::SetRealName }, + { "chanmodes", &CUser::SetDefaultChanModes }, + { "bindhost", &CUser::SetBindHost }, + { "vhost", &CUser::SetBindHost }, + { "dccbindhost", &CUser::SetDCCBindHost }, + { "dccvhost", &CUser::SetDCCBindHost }, + { "timestampformat", &CUser::SetTimestampFormat }, + { "skin", &CUser::SetSkinName }, + }; + size_t numStringOptions = sizeof(StringOptions) / sizeof(StringOptions[0]); + TOption UIntOptions[] = { + { "jointries", &CUser::SetJoinTries }, + { "maxjoins", &CUser::SetMaxJoins }, + }; + size_t numUIntOptions = sizeof(UIntOptions) / sizeof(UIntOptions[0]); + TOption BoolOptions[] = { + { "keepbuffer", &CUser::SetKeepBuffer }, + { "multiclients", &CUser::SetMultiClients }, + { "bouncedccs", &CUser::SetBounceDCCs }, + { "denyloadmod", &CUser::SetDenyLoadMod }, + { "admin", &CUser::SetAdmin }, + { "denysetbindhost", &CUser::SetDenySetBindHost }, + { "denysetvhost", &CUser::SetDenySetBindHost }, + { "appendtimestamp", &CUser::SetTimestampAppend }, + { "prependtimestamp", &CUser::SetTimestampPrepend }, + { "ircconnectenabled", &CUser::SetIRCConnectEnabled }, + }; + size_t numBoolOptions = sizeof(BoolOptions) / sizeof(BoolOptions[0]); + + for (size_t i = 0; i < numStringOptions; i++) { + CString sValue; + if (pConfig->FindStringEntry(StringOptions[i].name, sValue)) + (this->*StringOptions[i].pSetter)(sValue); + } + for (size_t i = 0; i < numUIntOptions; i++) { + CString sValue; + if (pConfig->FindStringEntry(UIntOptions[i].name, sValue)) + (this->*UIntOptions[i].pSetter)(sValue.ToUInt()); + } + for (size_t i = 0; i < numBoolOptions; i++) { + CString sValue; + if (pConfig->FindStringEntry(BoolOptions[i].name, sValue)) + (this->*BoolOptions[i].pSetter)(sValue.ToBool()); + } + + VCString vsList; + VCString::const_iterator vit; + pConfig->FindStringVector("allow", vsList); + for (vit = vsList.begin(); vit != vsList.end(); ++vit) { + AddAllowedHost(*vit); + } + pConfig->FindStringVector("ctcpreply", vsList); + for (vit = vsList.begin(); vit != vsList.end(); ++vit) { + const CString& sValue = *vit; + AddCTCPReply(sValue.Token(0), sValue.Token(1, true)); + } + pConfig->FindStringVector("server", vsList); + for (vit = vsList.begin(); vit != vsList.end(); ++vit) { + CUtils::PrintAction("Adding Server [" + *vit + "]"); + CUtils::PrintStatus(AddServer(*vit)); + } + pConfig->FindStringVector("chan", vsList); + for (vit = vsList.begin(); vit != vsList.end(); ++vit) { + AddChan(*vit, true); + } + + CString sValue; + if (pConfig->FindStringEntry("buffer", sValue)) + SetBufferCount(sValue.ToUInt(), true); + if (pConfig->FindStringEntry("awaysuffix", sValue)) { + CUtils::PrintMessage("WARNING: AwaySuffix has been depricated, instead try -> LoadModule = awaynick %nick%_" + sValue); + } + if (pConfig->FindStringEntry("autocycle", sValue)) { + if (sValue.Equals("true")) + CUtils::PrintError("WARNING: AutoCycle has been removed, instead try -> LoadModule = autocycle"); + } + if (pConfig->FindStringEntry("keepnick", sValue)) { + if (sValue.Equals("true")) + CUtils::PrintError("WARNING: KeepNick has been deprecated, instead try -> LoadModule = keepnick"); + } + if (pConfig->FindStringEntry("statusprefix", sValue)) { + if (!SetStatusPrefix(sValue)) { + sError = "Invalid StatusPrefix [" + sValue + "] Must be 1-5 chars, no spaces."; + CUtils::PrintError(sError); + return false; + } + } + if (pConfig->FindStringEntry("timezoneoffset", sValue)) { + SetTimezoneOffset(sValue.ToDouble()); + } + if (pConfig->FindStringEntry("timestamp", sValue)) { + if (!sValue.Trim_n().Equals("true")) { + if (sValue.Trim_n().Equals("append")) { + SetTimestampAppend(true); + SetTimestampPrepend(false); + } else if (sValue.Trim_n().Equals("prepend")) { + SetTimestampAppend(false); + SetTimestampPrepend(true); + } else if (sValue.Trim_n().Equals("false")) { + SetTimestampAppend(false); + SetTimestampPrepend(false); + } else { + SetTimestampFormat(sValue); + } + } + } + if (pConfig->FindStringEntry("dcclookupmethod", sValue)) + SetUseClientIP(sValue.Equals("Client")); + pConfig->FindStringEntry("pass", sValue); + // There are different formats for this available: + // Pass = + // Pass = - + // Pass = plain# + // Pass = # + // Pass = ### + // 'Salted hash' means hash of 'password' + 'salt' + // Possible hashes are md5 and sha256 + if (sValue.Right(1) == "-") { + sValue.RightChomp(); + sValue.Trim(); + SetPass(sValue, CUser::HASH_MD5); + } else { + CString sMethod = sValue.Token(0, false, "#"); + CString sPass = sValue.Token(1, true, "#"); + if (sMethod == "md5" || sMethod == "sha256") { + CUser::eHashType type = CUser::HASH_MD5; + if (sMethod == "sha256") + type = CUser::HASH_SHA256; + + CString sSalt = sPass.Token(1, false, "#"); + sPass = sPass.Token(0, false, "#"); + SetPass(sPass, type, sSalt); + } else if (sMethod == "plain") { + SetPass(sPass, CUser::HASH_NONE); + } else { + SetPass(sValue, CUser::HASH_NONE); + } + } + + CConfig::SubConfig subConf; + CConfig::SubConfig::const_iterator subIt; + pConfig->FindSubConfig("chan", subConf); + for (subIt = subConf.begin(); subIt != subConf.end(); ++subIt) { + const CString& sChanName = subIt->first; + CConfig* pSubConf = subIt->second.m_pSubConfig; + CChan* pChan = new CChan(sChanName, this, true, pSubConf); + + if (!pSubConf->empty()) { + sError = "Unhandled lines in config for User [" + GetUserName() + "], Channel [" + sChanName + "]!"; + CUtils::PrintError(sError); + + CZNC::DumpConfig(pSubConf); + return false; + } + + // Save the channel name, because AddChan + // deletes the CChannel*, if adding fails + sError = pChan->GetName(); + if (!AddChan(pChan)) { + sError = "Channel [" + sError + "] defined more than once"; + CUtils::PrintError(sError); + return false; + } + sError.clear(); + } + + pConfig->FindStringVector("loadmodule", vsList); + for (vit = vsList.begin(); vit != vsList.end(); ++vit) { + sValue = *vit; + CString sModName = sValue.Token(0); + + // XXX Legacy crap, added in znc 0.089 + if (sModName == "discon_kick") { + CUtils::PrintMessage("NOTICE: [discon_kick] was renamed, loading [disconkick] instead"); + sModName = "disconkick"; + } + + CUtils::PrintAction("Loading Module [" + sModName + "]"); + CString sModRet; + CString sArgs = sValue.Token(1, true); + + bool bModRet = GetModules().LoadModule(sModName, sArgs, this, sModRet); + + // If the module was loaded, sModRet contains + // "Loaded Module [name] ..." and we strip away this beginning. + if (bModRet) + sModRet = sModRet.Token(1, true, sModName + "] "); + + CUtils::PrintStatus(bModRet, sModRet); + if (!bModRet) { + sError = sModRet; + return false; + } + continue; + } + + return true; +} + void CUser::DelModules() { delete m_pModules; m_pModules = NULL; diff --git a/User.h b/User.h index 9d49d2ca..b9b5ae03 100644 --- a/User.h +++ b/User.h @@ -22,6 +22,7 @@ using std::vector; class CChan; class CClient; +class CConfig; class CIRCSock; class CUserTimer; class CServer; @@ -33,6 +34,8 @@ public: CUser(const CString& sUserName); ~CUser(); + bool ParseConfig(CConfig* Config, CString& sError); + enum eHashType { HASH_NONE, HASH_MD5, diff --git a/configure.ac b/configure.ac index dbd633ba..e63e6845 100644 --- a/configure.ac +++ b/configure.ac @@ -474,6 +474,7 @@ AC_CONFIG_FILES([man/Makefile]) AC_CONFIG_FILES([znc.pc]) AC_CONFIG_FILES([znc-uninstalled.pc]) AC_CONFIG_FILES([modules/Makefile]) +AC_CONFIG_FILES([test/Makefile]) AC_OUTPUT echo diff --git a/test/ConfigTest.cpp b/test/ConfigTest.cpp new file mode 100644 index 00000000..8c5bb9fd --- /dev/null +++ b/test/ConfigTest.cpp @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2004-2011 See the AUTHORS file for details. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 as published + * by the Free Software Foundation. + */ + +#include "Config.h" +#include + +class CConfigTest { +public: + CConfigTest(const CString& sConfig) : m_sConfig(sConfig) { } + virtual ~CConfigTest() { m_File.Delete(); } + + virtual bool Test() = 0; + +protected: + CFile& WriteFile() { + char sName[] = "./temp-XXXXXX"; + int fd = mkstemp(sName); + m_File.Open(sName, O_RDWR); + close(fd); + + m_File.Write(m_sConfig); + + return m_File; + } + +private: + CFile m_File; + CString m_sConfig; +}; + +class CConfigErrorTest : public CConfigTest { +public: + CConfigErrorTest(const CString& sConfig, const CString& sError) + : CConfigTest(sConfig), m_sError(sError) { } + + bool Test() { + CFile &File = WriteFile(); + + CConfig conf; + CString sError; + bool res = conf.Parse(File, sError); + if (res) { + std::cout << "Didn't error out!\n"; + return false; + } + + if (sError != m_sError) { + std::cout << "Wrong error\n Expected: " << m_sError << "\n Got: " << sError << std::endl; + return false; + } + + return true; + } +private: + CString m_sError; +}; + +class CConfigSuccessTest : public CConfigTest { +public: + CConfigSuccessTest(const CString& sConfig, const CString& sExpectedOutput) + : CConfigTest(sConfig), m_sOutput(sExpectedOutput) { } + + bool Test() { + CFile &File = WriteFile(); + // Verify that Parse() rewinds the file + File.Seek(12); + + CConfig conf; + CString sError; + bool res = conf.Parse(File, sError); + if (!res) { + std::cout << "Error'd out! (" + sError + ")\n"; + return false; + } + if (!sError.empty()) { + std::cout << "Non-empty error string!\n"; + return false; + } + + CString sOutput; + ToString(sOutput, conf); + + if (sOutput != m_sOutput) { + std::cout << "Wrong output\n Expected: " << m_sOutput << "\n Got: " << sOutput << std::endl; + return false; + } + + return true; + } + + void ToString(CString& sRes, CConfig& conf) { + CConfig::EntryMapIterator it = conf.BeginEntries(); + while (it != conf.EndEntries()) { + const CString& sKey = it->first; + const VCString& vsEntries = it->second; + VCString::const_iterator i = vsEntries.begin(); + if (i == vsEntries.end()) + sRes += sKey + " <- Error, empty list!\n"; + else + while (i != vsEntries.end()) { + sRes += sKey + "=" + *i + "\n"; + i++; + } + ++it; + } + + CConfig::SubConfigMapIterator it2 = conf.BeginSubConfigs(); + while (it2 != conf.EndSubConfigs()) { + map::const_iterator it3 = it2->second.begin(); + + while (it3 != it2->second.end()) { + sRes += "->" + it2->first + "/" + it3->first + "\n"; + ToString(sRes, *it3->second.m_pSubConfig); + sRes += "<-\n"; + ++it3; + } + + ++it2; + } + } + +private: + CString m_sOutput; +}; + +int main() { +#define TEST_ERROR(a, b) new CConfigErrorTest(a, b) +#define TEST_SUCCESS(a, b) new CConfigSuccessTest(a, b) +#define ARRAY_SIZE(a) (sizeof(a)/(sizeof((a)[0]))) + CConfigTest *tests[] = { + TEST_SUCCESS("", ""), + /* duplicate entries */ + TEST_SUCCESS("Foo = bar\nFoo = baz\n", "foo=bar\nfoo=baz\n"), + TEST_SUCCESS("Foo = baz\nFoo = bar\n", "foo=baz\nfoo=bar\n"), + /* sub configs */ + TEST_ERROR("", "Error on line 1: Closing tag \"foo\" which is not open."), + TEST_ERROR("\n\n", "Error on line 2: Closing tag \"bar\" which is not open."), + TEST_ERROR("", "Error on line 1: Not all tags are closed at the end of the file. Inner-most open tag is \"foo\"."), + TEST_ERROR("\n", "Error on line 1: Empty block name at begin of block."), + TEST_ERROR("\n\n\n", "Error on line 4: Duplicate entry for tag \"foo\" name \"1\"."), + TEST_SUCCESS("\n", "->foo/a\n<-\n"), + TEST_SUCCESS("\n \n \n", "->a/b\n->c/d\n<-\n<-\n"), + TEST_SUCCESS(" \t \nfoo = bar\n\tFooO = bar\n", "->a/B\nfoo=bar\nfooo=bar\n<-\n"), + /* comments */ + TEST_SUCCESS("Foo = bar // baz\n// Bar = baz", "foo=bar // baz\n"), + TEST_SUCCESS("Foo = bar /* baz */\n/*** Foo = baz ***/\n /**** asdsdfdf \n Some quite invalid stuff ***/\n", "foo=bar /* baz */\n"), + TEST_ERROR("\n/* Just a comment\n", "Error on line 3: Comment not closed at end of file."), + }; + unsigned int i; + unsigned int failed = 0; + + for (i = 0; i < ARRAY_SIZE(tests); i++) { + if (!tests[i]->Test()) + failed++; + delete tests[i]; + } + + return failed; +} diff --git a/test/Makefile.in b/test/Makefile.in new file mode 100644 index 00000000..691d611b --- /dev/null +++ b/test/Makefile.in @@ -0,0 +1,46 @@ +SHELL := @SHELL@ + +# Support out-of-tree builds +srcdir := @srcdir@ +VPATH := @srcdir@ + +CXX := @CXX@ +CXXFLAGS := @DEFS@ @CPPFLAGS@ @CXXFLAGS@ -I.. +LDFLAGS := @LDFLAGS@ +LIBS := @LIBS@ + +TARGETS := ConfigTest +OBJS := $(addsuffix .o, $(TARGETS)) +ZNC_OBJS := Config.o FileUtils.o Utils.o ZNCString.o MD5.o SHA256.o +ZNC_OBJS := $(addprefix ../, $(ZNC_OBJS)) + +ifneq "$(V)" "" +VERBOSE=1 +endif +ifeq "$(VERBOSE)" "" +Q=@ +E=@echo +C=-s +else +Q= +E=@\# +C= +endif + +.PHONY: test + +all: $(TARGETS) + +ConfigTest: ConfigTest.o + $(E) Linking $@... + $(Q)$(CXX) $(LDFLAGS) -o $@ $< $(ZNC_OBJS) $(LIBS) + +%.o: %.cpp Makefile + @mkdir -p .depend + $(E) Building $@... + $(Q)$(CXX) $(CXXFLAGS) -c -o $@ $< -MMD -MF .depend/$@.dep + +test: $(TARGETS) + for test in $(TARGETS) ; do \ + $$test || exit 1 ; \ + done diff --git a/znc.cpp b/znc.cpp index 045bf03a..bf534735 100644 --- a/znc.cpp +++ b/znc.cpp @@ -12,6 +12,7 @@ #include "Server.h" #include "User.h" #include "Listener.h" +#include "Config.h" #include static inline CString FormatBindError() { @@ -1008,13 +1009,11 @@ bool CZNC::DoRehash(CString& sError) m_pLockFile = pFile; CFile &File = *pFile; - // This fd is re-used for rehashing, so we must seek back to the beginning! - if (!File.Seek(0)) { - sError = "Could not seek to the beginning of the config."; + CConfig config; + if (!config.Parse(File, sError)) { CUtils::PrintStatus(false, sError); return false; } - CUtils::PrintStatus(true); m_vsBindHosts.clear(); @@ -1026,610 +1025,189 @@ bool CZNC::DoRehash(CString& sError) m_vpListeners.erase(m_vpListeners.begin()); } - CString sLine; - bool bCommented = false; // support for /**/ style comments - CUser* pUser = NULL; // Used to keep track of which user block we are in - CUser* pRealUser = NULL; // If we rehash a user, this is the real one - CChan* pChan = NULL; // Used to keep track of which chan block we are in - unsigned int uLineNum = 0; MCString msModules; // Modules are queued for later loading - while (File.ReadLine(sLine)) { - uLineNum++; + const char *szListenerEntries[] = { + "listen", "listen6", "listen4", + "listener", "listener6", "listener4" + }; + const size_t numListenerEntries = sizeof(szListenerEntries) / sizeof(szListenerEntries[0]); + VCString vsList; - // Remove all leading spaces and trailing line endings - sLine.TrimLeft(); - sLine.TrimRight("\r\n"); + for (size_t i = 0; i < numListenerEntries; i++) { + config.FindStringVector(szListenerEntries[i], vsList); + VCString::const_iterator it = vsList.begin(); - if ((sLine.empty()) || (sLine[0] == '#') || (sLine.Left(2) == "//")) { - continue; + for (; it != vsList.end(); ++it) { + if (!AddListener(szListenerEntries[i] + CString(" ") + *it, sError)) + return false; } + } - if (sLine.Left(2) == "/*") { - if (sLine.Right(2) != "*/") { - bCommented = true; - } + VCString::const_iterator vit; + config.FindStringVector("loadmodule", vsList); + for (vit = vsList.begin(); vit != vsList.end(); ++vit) { + CString sModName = vit->Token(0); + CString sArgs = vit->Token(1, true); - continue; - } - - if (bCommented) { - if (sLine.Right(2) == "*/") { - bCommented = false; - } - - continue; - } - - if ((sLine.Left(1) == "<") && (sLine.Right(1) == ">")) { - sLine.LeftChomp(); - sLine.RightChomp(); - sLine.Trim(); - - CString sTag = sLine.substr(0, sLine.find_first_of(" \t\r\n")); - CString sValue = (sTag.size() < sLine.size()) ? sLine.substr(sTag.size() +1) : ""; - - sTag.Trim(); - sValue.Trim(); - - if (sLine.Left(1) == "/") { - sTag = sTag.substr(1); - - if (pUser) { - if (pChan) { - if (sTag.Equals("Chan")) { - // Save the channel name, because AddChan - // deletes the CChannel*, if adding fails - sError = pChan->GetName(); - if (!pUser->AddChan(pChan)) { - sError = "Channel [" + sError + "] defined more than once"; - CUtils::PrintError(sError); - return false; - } - sError.clear(); - pChan = NULL; - continue; - } - } else if (sTag.Equals("User")) { - CString sErr; - - if (pRealUser) { - if (!pRealUser->Clone(*pUser, sErr) - || !AddUser(pRealUser, sErr)) { - sError = "Invalid user [" + pUser->GetUserName() + "] " + sErr; - DEBUG("CUser::Clone() failed in rehash"); - } - pUser->SetBeingDeleted(true); - delete pUser; - pUser = NULL; - } else if (!AddUser(pUser, sErr)) { - sError = "Invalid user [" + pUser->GetUserName() + "] " + sErr; - } - - if (!sError.empty()) { - CUtils::PrintError(sError); - if (pUser) { - pUser->SetBeingDeleted(true); - delete pUser; - pUser = NULL; - } - return false; - } - - pUser = NULL; - pRealUser = NULL; - continue; - } - } - } else if (sTag.Equals("User")) { - if (pUser) { - sError = "You may not nest tags inside of other tags."; - CUtils::PrintError(sError); - return false; - } - - if (sValue.empty()) { - sError = "You must supply a username in the tag."; - CUtils::PrintError(sError); - return false; - } - - if (m_msUsers.find(sValue) != m_msUsers.end()) { - sError = "User [" + sValue + "] defined more than once."; - CUtils::PrintError(sError); - return false; - } - - CUtils::PrintMessage("Loading user [" + sValue + "]"); - - // Either create a CUser* or use an existing one - map::iterator it = m_msDelUsers.find(sValue); - - if (it != m_msDelUsers.end()) { - pRealUser = it->second; - m_msDelUsers.erase(it); - } else - pRealUser = NULL; - - pUser = new CUser(sValue); - - 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; - } - } - - continue; - } else if (sTag.Equals("Chan")) { - if (!pUser) { - sError = " tags must be nested inside of a tag."; - CUtils::PrintError(sError); - return false; - } - - if (pChan) { - sError = "You may not nest tags inside of other tags."; - CUtils::PrintError(sError); - return false; - } - - pChan = new CChan(sValue, pUser, true); - continue; - } - } - - // If we have a regular line, figure out where it goes - CString sName = sLine.Token(0, false, "="); - CString sValue = sLine.Token(1, true, "="); - - // Only remove the first space, people might want - // leading spaces (e.g. in the MOTD). - if (sValue.Left(1) == " ") - sValue.LeftChomp(); - - // We don't have any names with spaces, trim all - // leading/trailing spaces. - sName.Trim(); - - if ((!sName.empty()) && (!sValue.empty())) { - if (pUser) { - if (pChan) { - if (sName.Equals("Buffer")) { - pChan->SetBufferCount(sValue.ToUInt(), true); - continue; - } else if (sName.Equals("KeepBuffer")) { - pChan->SetKeepBuffer(sValue.Equals("true")); - continue; - } else if (sName.Equals("Detached")) { - pChan->SetDetached(sValue.Equals("true")); - continue; - } else if (sName.Equals("AutoCycle")) { - if (sValue.Equals("true")) { - CUtils::PrintError("WARNING: AutoCycle has been removed, instead try -> LoadModule = autocycle " + pChan->GetName()); - } - continue; - } else if (sName.Equals("Key")) { - pChan->SetKey(sValue); - continue; - } else if (sName.Equals("Modes")) { - pChan->SetDefaultModes(sValue); - continue; - } - } else { - if (sName.Equals("Buffer")) { - pUser->SetBufferCount(sValue.ToUInt(), true); - continue; - } else if (sName.Equals("KeepBuffer")) { - pUser->SetKeepBuffer(sValue.Equals("true")); - continue; - } else if (sName.Equals("Nick")) { - pUser->SetNick(sValue); - continue; - } else if (sName.Equals("CTCPReply")) { - pUser->AddCTCPReply(sValue.Token(0), sValue.Token(1, true)); - continue; - } else if (sName.Equals("QuitMsg")) { - pUser->SetQuitMsg(sValue); - continue; - } else if (sName.Equals("AltNick")) { - pUser->SetAltNick(sValue); - continue; - } else if (sName.Equals("AwaySuffix")) { - CUtils::PrintMessage("WARNING: AwaySuffix has been depricated, instead try -> LoadModule = awaynick %nick%_" + sValue); - continue; - } else if (sName.Equals("AutoCycle")) { - if (sValue.Equals("true")) { - CUtils::PrintError("WARNING: AutoCycle has been removed, instead try -> LoadModule = autocycle"); - } - continue; - } else if (sName.Equals("Pass")) { - // There are different formats for this available: - // Pass = - // Pass = - - // Pass = plain# - // Pass = # - // Pass = ### - // 'Salted hash' means hash of 'password' + 'salt' - // Possible hashes are md5 and sha256 - if (sValue.Right(1) == "-") { - sValue.RightChomp(); - sValue.Trim(); - pUser->SetPass(sValue, CUser::HASH_MD5); - } else { - CString sMethod = sValue.Token(0, false, "#"); - CString sPass = sValue.Token(1, true, "#"); - if (sMethod == "md5" || sMethod == "sha256") { - CUser::eHashType type = CUser::HASH_MD5; - if (sMethod == "sha256") - type = CUser::HASH_SHA256; - - CString sSalt = sPass.Token(1, false, "#"); - sPass = sPass.Token(0, false, "#"); - pUser->SetPass(sPass, type, sSalt); - } else if (sMethod == "plain") { - pUser->SetPass(sPass, CUser::HASH_NONE); - } else { - pUser->SetPass(sValue, CUser::HASH_NONE); - } - } - - continue; - } else if (sName.Equals("MultiClients")) { - pUser->SetMultiClients(sValue.Equals("true")); - continue; - } else if (sName.Equals("BounceDCCs")) { - pUser->SetBounceDCCs(sValue.Equals("true")); - continue; - } else if (sName.Equals("Ident")) { - pUser->SetIdent(sValue); - continue; - } else if (sName.Equals("DenyLoadMod")) { - pUser->SetDenyLoadMod(sValue.Equals("true")); - continue; - } else if (sName.Equals("Admin")) { - pUser->SetAdmin(sValue.Equals("true")); - continue; - } else if (sName.Equals("DenySetBindHost") || sName.Equals("DenySetVHost")) { - pUser->SetDenySetBindHost(sValue.Equals("true")); - continue; - } else if (sName.Equals("StatusPrefix")) { - if (!pUser->SetStatusPrefix(sValue)) { - sError = "Invalid StatusPrefix [" + sValue + "] Must be 1-5 chars, no spaces."; - CUtils::PrintError(sError); - return false; - } - continue; - } else if (sName.Equals("DCCLookupMethod")) { - pUser->SetUseClientIP(sValue.Equals("Client")); - continue; - } else if (sName.Equals("RealName")) { - pUser->SetRealName(sValue); - continue; - } else if (sName.Equals("KeepNick")) { - if (sValue.Equals("true")) { - CUtils::PrintError("WARNING: KeepNick has been deprecated, instead try -> LoadModule = keepnick"); - } - continue; - } else if (sName.Equals("ChanModes")) { - pUser->SetDefaultChanModes(sValue); - continue; - } else if (sName.Equals("BindHost") || sName.Equals("VHost")) { - pUser->SetBindHost(sValue); - continue; - } else if (sName.Equals("DCCBindHost") || sName.Equals("DCCVHost")) { - pUser->SetDCCBindHost(sValue); - continue; - } else if (sName.Equals("Allow")) { - pUser->AddAllowedHost(sValue); - continue; - } else if (sName.Equals("Server")) { - CUtils::PrintAction("Adding Server [" + sValue + "]"); - CUtils::PrintStatus(pUser->AddServer(sValue)); - continue; - } else if (sName.Equals("Chan")) { - pUser->AddChan(sValue, true); - continue; - } else if (sName.Equals("TimestampFormat")) { - pUser->SetTimestampFormat(sValue); - continue; - } else if (sName.Equals("AppendTimestamp")) { - pUser->SetTimestampAppend(sValue.ToBool()); - continue; - } else if (sName.Equals("PrependTimestamp")) { - pUser->SetTimestampPrepend(sValue.ToBool()); - continue; - } else if (sName.Equals("IRCConnectEnabled")) { - pUser->SetIRCConnectEnabled(sValue.ToBool()); - continue; - } else if (sName.Equals("Timestamp")) { - if (!sValue.Trim_n().Equals("true")) { - if (sValue.Trim_n().Equals("append")) { - pUser->SetTimestampAppend(true); - pUser->SetTimestampPrepend(false); - } else if (sValue.Trim_n().Equals("prepend")) { - pUser->SetTimestampAppend(false); - pUser->SetTimestampPrepend(true); - } else if (sValue.Trim_n().Equals("false")) { - pUser->SetTimestampAppend(false); - pUser->SetTimestampPrepend(false); - } else { - pUser->SetTimestampFormat(sValue); - } - } - continue; - } else if (sName.Equals("TimezoneOffset")) { - pUser->SetTimezoneOffset(sValue.ToDouble()); // there is no ToFloat() - continue; - } else if (sName.Equals("JoinTries")) { - pUser->SetJoinTries(sValue.ToUInt()); - continue; - } else if (sName.Equals("MaxJoins")) { - pUser->SetMaxJoins(sValue.ToUInt()); - continue; - } else if (sName.Equals("Skin")) { - pUser->SetSkinName(sValue); - continue; - } else if (sName.Equals("LoadModule")) { - CString sModName = sValue.Token(0); - - // XXX Legacy crap, added in znc 0.089 - if (sModName == "discon_kick") { - CUtils::PrintMessage("NOTICE: [discon_kick] was renamed, loading [disconkick] instead"); - sModName = "disconkick"; - } - - CUtils::PrintAction("Loading Module [" + sModName + "]"); - CString sModRet; - CString sArgs = sValue.Token(1, true); - - bool bModRet = pUser->GetModules().LoadModule(sModName, sArgs, pUser, sModRet); - - // If the module was loaded, sModRet contains - // "Loaded Module [name] ..." and we strip away this beginning. - if (bModRet) - sModRet = sModRet.Token(1, true, sModName + "] "); - - CUtils::PrintStatus(bModRet, sModRet); - if (!bModRet) { - sError = sModRet; - return false; - } - continue; - } - } - } else { - if (sName.Equals("Listen") || sName.Equals("Listen6") || sName.Equals("Listen4") - || sName.Equals("Listener") || sName.Equals("Listener6") || sName.Equals("Listener4")) { - 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.find(" ") != CString::npos) { - sBindHost = sValue.Token(0, false, " "); - sPort = sValue.Token(1, true, " "); - } else { - sPort = sValue; - } - - if (sPort.Left(1) == "+") { - sPort.LeftChomp(); - bSSL = true; - } - - 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"; - } - - unsigned short uPort = sPort.ToUShort(); - CUtils::PrintAction("Binding to port [" + CString((bSSL) ? "+" : "") + CString(uPort) + "]" + sHostComment + sIPV6Comment); - -#ifndef HAVE_IPV6 - if (ADDR_IPV6ONLY == eAddr) { - sError = "IPV6 is not enabled"; - CUtils::PrintStatus(false, sError); - return false; - } -#endif - -#ifndef HAVE_LIBSSL - if (bSSL) { - sError = "SSL is not enabled"; - CUtils::PrintStatus(false, sError); - return false; - } -#else - CString sPemFile = GetPemLocation(); - - if (bSSL && !CFile::Exists(sPemFile)) { - sError = "Unable to locate pem file: [" + 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; - } - - CUtils::PrintAction("Binding to port [+" + CString(uPort) + "]" + sHostComment + sIPV6Comment); - } -#endif - if (!uPort) { - sError = "Invalid port"; - CUtils::PrintStatus(false, sError); - return false; - } - - CListener* pListener = new CListener(uPort, sBindHost, bSSL, eAddr, eAccept); - - if (!pListener->Listen()) { - sError = FormatBindError(); - CUtils::PrintStatus(false, sError); - delete pListener; - return false; - } - - m_vpListeners.push_back(pListener); - CUtils::PrintStatus(true); - - continue; - } else if (sName.Equals("LoadModule")) { - CString sModName = sValue.Token(0); - CString sArgs = sValue.Token(1, true); - - 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, NULL, sModRet); - - // If the module was loaded, sModRet contains - // "Loaded Module [name] ..." and we strip away this beginning. - if (bModRet) - sModRet = sModRet.Token(1, true, sModName + "] "); - - CUtils::PrintStatus(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, NULL, sModRet); - - // If the module was loaded, sModRet contains - // "Loaded Module [name] ..." and we strip away this beginning. - if (bModRet) - sModRet = sModRet.Token(1, true, sModName + "] "); - - CUtils::PrintStatus(bModRet, sModRet); - if (!bModRet) { - sError = sModRet; - return false; - } - } else - CUtils::PrintMessage("Module [" + sModName + "] already loaded."); - - msModules[sModName] = sArgs; - continue; - // Convert old-style ISpoofFormat's and ISpoofFile to identfile module - } else if (sName.Equals("ISpoofFormat") || sName.Equals("ISpoofFile")) { - CModule *pIdentFileMod = GetModules().FindModule("identfile"); - if (!pIdentFileMod) { - CUtils::PrintAction("Loading Global Module [identfile]"); - - CString sModRet; - bool bModRet = GetModules().LoadModule("identfile", "", NULL, sModRet); - - if (bModRet) { - sModRet = sModRet.Token(1, true, "identfile] "); - } - - CUtils::PrintStatus(bModRet, sModRet); - if (!bModRet) { - sError = sModRet; - return false; - } - - pIdentFileMod = GetModules().FindModule("identfile"); - msModules["identfile"] = ""; - } - - if (!pIdentFileMod->SetNV(sName.TrimPrefix_n("ISpoof"), sValue)) { - sError = "Failed to convert " + sName + " to the identfile module"; - CUtils::PrintError(sError); - return false; - } - continue; - } else if (sName.Equals("MOTD")) { - AddMotd(sValue); - continue; - } else if (sName.Equals("BindHost") || sName.Equals("VHost")) { - AddBindHost(sValue); - continue; - } else if (sName.Equals("PidFile")) { - m_sPidFile = sValue; - continue; - } else if (sName.Equals("Skin")) { - SetSkinName(sValue); - continue; - } else if (sName.Equals("StatusPrefix")) { - m_sStatusPrefix = sValue; - continue; - } else if (sName.Equals("ConnectDelay")) { - m_uiConnectDelay = sValue.ToUInt(); - continue; - } else if (sName.Equals("ServerThrottle")) { - m_sConnectThrottle.SetTTL(sValue.ToUInt()*1000); - continue; - } else if (sName.Equals("AnonIPLimit")) { - m_uiAnonIPLimit = sValue.ToUInt(); - continue; - } else if (sName.Equals("MaxBufferSize")) { - m_uiMaxBufferSize = sValue.ToUInt(); - continue; - } else if (sName.Equals("SSLCertFile")) { - m_sSSLCertFile = sValue; - continue; - } - } - - } - - { - sError = "Unhandled line " + CString(uLineNum) + " in config: [" + sLine + "]"; + 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, NULL, sModRet); + + // If the module was loaded, sModRet contains + // "Loaded Module [name] ..." and we strip away this beginning. + if (bModRet) + sModRet = sModRet.Token(1, true, sModName + "] "); + + CUtils::PrintStatus(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, NULL, sModRet); + + // If the module was loaded, sModRet contains + // "Loaded Module [name] ..." and we strip away this beginning. + if (bModRet) + sModRet = sModRet.Token(1, true, sModName + "] "); + + CUtils::PrintStatus(bModRet, sModRet); + if (!bModRet) { + sError = sModRet; + return false; + } + } else + CUtils::PrintMessage("Module [" + sModName + "] already loaded."); + + msModules[sModName] = sArgs; } + config.FindStringVector("motd", vsList); + for (vit = vsList.begin(); vit != vsList.end(); ++vit) { + AddMotd(*vit); + } + + config.FindStringVector("bindhost", vsList); + for (vit = vsList.begin(); vit != vsList.end(); ++vit) { + AddBindHost(*vit); + } + config.FindStringVector("vhost", vsList); + for (vit = vsList.begin(); vit != vsList.end(); ++vit) { + AddBindHost(*vit); + } + + 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("skin", sVal)) + SetSkinName(sVal); + if (config.FindStringEntry("connectdelay", sVal)) + m_uiConnectDelay = 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(); + + CConfig::SubConfig subConf; + CConfig::SubConfig::const_iterator subIt; + config.FindSubConfig("user", subConf); + for (subIt = subConf.begin(); subIt != subConf.end(); ++subIt) { + const CString& sUserName = subIt->first; + CConfig* pSubConf = subIt->second.m_pSubConfig; + CUser* pRealUser = NULL; + + CUtils::PrintMessage("Loading user [" + sUserName + "]"); + + // Either create a CUser* or use an existing one + map::iterator it = m_msDelUsers.find(sUserName); + + if (it != m_msDelUsers.end()) { + pRealUser = it->second; + m_msDelUsers.erase(it); + } + + 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); + delete pUser; + pUser = NULL; + return false; + } + + if (!pSubConf->empty()) { + sError = "Unhandled lines in config for User [" + sUserName + "]!"; + CUtils::PrintError(sError); + + DumpConfig(pSubConf); + return false; + } + + CString sErr; + if (pRealUser) { + if (!pRealUser->Clone(*pUser, sErr) + || !AddUser(pRealUser, sErr)) { + sError = "Invalid user [" + pUser->GetUserName() + "] " + sErr; + DEBUG("CUser::Clone() failed in rehash"); + } + pUser->SetBeingDeleted(true); + delete pUser; + pUser = NULL; + } else if (!AddUser(pUser, sErr)) { + sError = "Invalid user [" + pUser->GetUserName() + "] " + sErr; + } + + if (!sError.empty()) { + CUtils::PrintError(sError); + if (pUser) { + pUser->SetBeingDeleted(true); + delete pUser; + pUser = NULL; + } + return false; + } + + pUser = NULL; + pRealUser = NULL; + } + + if (!config.empty()) { + sError = "Unhandled lines in config!"; + CUtils::PrintError(sError); + + DumpConfig(&config); + return false; + } + + // Unload modules which are no longer in the config set ssUnload; for (size_t i = 0; i < GetModules().size(); i++) { @@ -1646,20 +1224,6 @@ bool CZNC::DoRehash(CString& sError) CUtils::PrintMessage("Could not unload [" + *it + "]"); } - if (pChan) { - sError = "Last section not properly closed. File truncated?"; - CUtils::PrintError(sError); - delete pChan; - return false; - } - - if (pUser) { - sError = "Last section not properly closed. File truncated?"; - CUtils::PrintError(sError); - delete pUser; - return false; - } - if (m_msUsers.empty()) { sError = "You must define at least one user in your config."; CUtils::PrintError(sError); @@ -1680,6 +1244,30 @@ bool CZNC::DoRehash(CString& sError) return true; } +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::ClearBindHosts() { m_vsBindHosts.clear(); } @@ -1800,6 +1388,120 @@ CListener* CZNC::FindListener(u_short uPort, const CString& sBindHost, EAddrType return NULL; } +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.find(" ") != CString::npos) { + sBindHost = sValue.Token(0, false, " "); + sPort = sValue.Token(1, true, " "); + } else { + sPort = sValue; + } + + if (sPort.Left(1) == "+") { + sPort.LeftChomp(); + bSSL = true; + } + + 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"; + } + + unsigned short uPort = sPort.ToUShort(); + CUtils::PrintAction("Binding to port [" + CString((bSSL) ? "+" : "") + CString(uPort) + "]" + sHostComment + sIPV6Comment); + +#ifndef HAVE_IPV6 + if (ADDR_IPV6ONLY == eAddr) { + sError = "IPV6 is not enabled"; + CUtils::PrintStatus(false, sError); + return false; + } +#endif + +#ifndef HAVE_LIBSSL + if (bSSL) { + sError = "SSL is not enabled"; + CUtils::PrintStatus(false, sError); + return false; + } +#else + CString sPemFile = GetPemLocation(); + + if (bSSL && !CFile::Exists(sPemFile)) { + sError = "Unable to locate pem file: [" + 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; + } + + CUtils::PrintAction("Binding to port [+" + CString(uPort) + "]" + sHostComment + sIPV6Comment); + } +#endif + if (!uPort) { + sError = "Invalid port"; + CUtils::PrintStatus(false, sError); + return false; + } + + CListener* pListener = new CListener(uPort, sBindHost, bSSL, eAddr, eAccept); + + 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(CListener* pListener) { if (!pListener->GetRealListener()) { // Listener doesnt actually listen diff --git a/znc.h b/znc.h index 17f3a2fb..1bf60f27 100644 --- a/znc.h +++ b/znc.h @@ -21,6 +21,7 @@ using std::map; class CListener; class CUser; class CConnectUserTimer; +class CConfig; class CZNC { public: @@ -142,12 +143,15 @@ public: // Never call this unless you are CConnectUserTimer::~CConnectUserTimer() void LeakConnectUser(CConnectUserTimer *pTimer); + static void DumpConfig(const CConfig* Config); + private: CFile* InitPidFile(); bool DoRehash(CString& sError); // Returns true if something was done bool HandleUserDeletion(); CString MakeConfigHeader(); + bool AddListener(const CString& sLine, CString& sError); protected: time_t m_TimeStarted;