diff --git a/modules/clientnotify.cpp b/modules/clientnotify.cpp index 596bc31f..e982f2f1 100644 --- a/modules/clientnotify.cpp +++ b/modules/clientnotify.cpp @@ -14,22 +14,28 @@ * limitations under the License. */ -#include #include +#include using std::set; class CClientNotifyMod : public CModule { protected: CString m_sMethod; + bool m_bNewOnly{}; bool m_bOnDisconnect{}; + bool m_bNotifyOnNewIP{}; + bool m_bNotifyOnNewClientID{}; - set m_sClientsSeen; + set m_sClientsSeenIP; + set m_sClientsSeenID; void SaveSettings() { SetNV("method", m_sMethod); SetNV("newonly", m_bNewOnly ? "1" : "0"); + SetNV("notifyonnewip", m_bNotifyOnNewIP ? "1" : "0"); + SetNV("notifyonnewclientid", m_bNotifyOnNewClientID ? "1" : "0"); SetNV("ondisconnect", m_bOnDisconnect ? "1" : "0"); } @@ -48,8 +54,16 @@ class CClientNotifyMod : public CModule { t_d("Sets the notify method"), [=](const CString& sLine) { OnMethodCommand(sLine); }); AddCommand("NewOnly", t_d(""), - t_d("Turns notifications for unseen IP addresses on or off"), + t_d("Turns notifications for unseen connections on or off"), [=](const CString& sLine) { OnNewOnlyCommand(sLine); }); + AddCommand("NotifyOnNewIP", t_d(""), + t_d("Specifies whether you want to be notified about new " + "connections with new IPs"), + [=](const CString& sLine) { OnNotifyOnNewIP(sLine); }); + AddCommand("NotifyOnNewID", t_d(""), + t_d("Specifies whether you want to be notified about new " + "connections with new client IDs"), + [=](const CString& sLine) { OnNotifyOnNewID(sLine); }); AddCommand( "OnDisconnect", t_d(""), t_d("Turns notifications for clients disconnecting on or off"), @@ -68,6 +82,8 @@ class CClientNotifyMod : public CModule { // default = off for these: + m_bNotifyOnNewIP = (GetNV("notifyonnewip") == "1"); + m_bNotifyOnNewClientID = (GetNV("notifyonnewclientid") == "1"); m_bNewOnly = (GetNV("newonly") == "1"); m_bOnDisconnect = (GetNV("ondisconnect") == "1"); @@ -76,18 +92,41 @@ class CClientNotifyMod : public CModule { void OnClientLogin() override { CString sRemoteIP = GetClient()->GetRemoteIP(); - if (!m_bNewOnly || - m_sClientsSeen.find(sRemoteIP) == m_sClientsSeen.end()) { - SendNotification(t_p("", - "Another client authenticated as your user. " - "Use the 'ListClients' command to see all {1} " - "clients.", - GetUser()->GetAllClients().size())( - GetUser()->GetAllClients().size())); + CString sRemoteClientID = GetClient()->GetIdentifier(); - // the set<> will automatically disregard duplicates: - m_sClientsSeen.insert(sRemoteIP); + CString sClientNameMessage{sRemoteIP}; + if (m_bNotifyOnNewClientID && sRemoteClientID != "") { + sClientNameMessage += " / " + sRemoteClientID; } + + auto sendLoginNotification = [&]() { + SendNotification( + t_p("", + "Another client ({1}) authenticated as your user. " + "Use the 'ListClients' command to see all {2} " + "clients.", + GetUser()->GetAllClients().size())( + sClientNameMessage, GetUser()->GetAllClients().size())); + }; + + if (m_bNewOnly) { + // see if we actually got a new client + // TODO: replace setName.find(...) == setName.end() with + // !setName.contains() once ZNC uses C++20 + if ((m_bNotifyOnNewIP && (m_sClientsSeenIP.find(sRemoteIP) == + m_sClientsSeenIP.end())) || + (m_bNotifyOnNewClientID && + (m_sClientsSeenID.find(sRemoteClientID) == + m_sClientsSeenID.end()))) { + sendLoginNotification(); + } + } else { + sendLoginNotification(); + } + + // the set<> will automatically disregard duplicates: + m_sClientsSeenIP.insert(sRemoteIP); + m_sClientsSeenID.insert(sRemoteClientID); } void OnClientDisconnect() override { @@ -127,6 +166,32 @@ class CClientNotifyMod : public CModule { PutModule(t_s("Saved.")); } + void OnNotifyOnNewIP(const CString& sCommand) { + const CString sArg = sCommand.Token(1, true).AsLower(); + + if (sArg.empty()) { + PutModule(t_s("Usage: NotifyOnNewIP ")); + return; + } + + m_bNotifyOnNewIP = sArg.ToBool(); + SaveSettings(); + PutModule(t_s("Saved.")); + } + + void OnNotifyOnNewID(const CString& sCommand) { + const CString sArg = sCommand.Token(1, true).AsLower(); + + if (sArg.empty()) { + PutModule(t_s("Usage: NotifyOnNewID ")); + return; + } + + m_bNotifyOnNewClientID = sArg.ToBool(); + SaveSettings(); + PutModule(t_s("Saved.")); + } + void OnDisconnectCommand(const CString& sCommand) { const CString sArg = sCommand.Token(1, true).AsLower(); @@ -142,9 +207,11 @@ class CClientNotifyMod : public CModule { void OnShowCommand(const CString& sLine) { PutModule( - t_f("Current settings: Method: {1}, for unseen IP addresses only: " - "{2}, notify on disconnecting clients: {3}")( - m_sMethod, m_bNewOnly, m_bOnDisconnect)); + t_f("Current settings: Method: {1}, for unseen only: {2}, notify" + "for unseen IPs: {3}, notify for unseen IDs: {4}, notify on" + "disconnecting clients: {5}")( + m_sMethod, m_bNewOnly, m_bNotifyOnNewIP, m_bNotifyOnNewClientID, + m_bOnDisconnect)); } }; diff --git a/test/integration/framework/znctest.cpp b/test/integration/framework/znctest.cpp index 58c3f926..e06cb834 100644 --- a/test/integration/framework/znctest.cpp +++ b/test/integration/framework/znctest.cpp @@ -72,11 +72,15 @@ Socket ZNCTest::ConnectClient() { return WrapIO(&sock); } -Socket ZNCTest::LoginClient() { +Socket ZNCTest::LoginClient(QString identifier) { auto client = ConnectClient(); client.Write("PASS :hunter2"); client.Write("NICK nick"); - client.Write("USER user/test x x :x"); + if ( identifier.length() == 0 ) { + client.Write("USER user/test x x :x"); + } else { + client.Write("USER user@" + identifier.toUtf8() + "/test x x :x"); + } return client; } diff --git a/test/integration/framework/znctest.h b/test/integration/framework/znctest.h index c03591ef..c3ce7c9e 100644 --- a/test/integration/framework/znctest.h +++ b/test/integration/framework/znctest.h @@ -37,7 +37,7 @@ class ZNCTest : public testing::Test { Socket ConnectIRCd(); Socket ConnectClient(); - Socket LoginClient(); + Socket LoginClient(QString identifier = ""); std::unique_ptr Run(); diff --git a/test/integration/tests/modules.cpp b/test/integration/tests/modules.cpp index 1a35c44f..d7b387c6 100644 --- a/test/integration/tests/modules.cpp +++ b/test/integration/tests/modules.cpp @@ -20,6 +20,7 @@ #include "znctest.h" using testing::HasSubstr; +using testing::Not; namespace znc_inttest { namespace { @@ -54,6 +55,50 @@ TEST_F(ZNCTest, NotifyConnectModule) { "NOTICE nick :*** user@identifier detached from 127.0.0.1"); } +TEST_F(ZNCTest, ClientNotifyModule) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = LoginClient(); + client.Write("znc loadmod clientnotify"); + client.ReadUntil("Loaded module"); + + auto check_not_sent = [](Socket& client, std::string wrongAnswer){ + auto result = QString{client.ReadRemainder()}.toStdString(); + EXPECT_THAT(result, Not(HasSubstr((wrongAnswer)))) << "Got an answer from the ClientNotifyModule even though we didnt want one with the given configuration"; + }; + + auto client2 = LoginClient(); + client.ReadUntil(":Another client (127.0.0.1) authenticated as your user. Use the 'ListClients' command to see all 2 clients."); + auto client3 = LoginClient(); + client.ReadUntil(":Another client (127.0.0.1) authenticated as your user. Use the 'ListClients' command to see all 3 clients."); + + // disable notifications for every message + client.Write("PRIVMSG *clientnotify :NewOnly on"); + + // check that we do not ge a notification after connecting from a know ip + auto client4 = LoginClient(); + check_not_sent(client, ":Another client (127.0.0.1) authenticated as your user. Use the 'ListClients' command to see all 4 clients."); + + // choose to notify only on new client ids + client.Write("PRIVMSG *clientnotify :NotifyOnNewID on"); + + auto client5 = LoginClient("identifier123"); + client.ReadUntil(":Another client (127.0.0.1 / identifier123) authenticated as your user. Use the 'ListClients' command to see all 5 clients."); + auto client6 = LoginClient("identifier123"); + check_not_sent(client, ":Another client (127.0.0.1 / identifier123) authenticated as your user. Use the 'ListClients' command to see all 6 clients."); + + auto client7 = LoginClient("not_identifier123"); + client.ReadUntil(":Another client (127.0.0.1 / not_identifier123) authenticated as your user. Use the 'ListClients' command to see all 7 clients."); + + // choose to notify from both clientids and new IPs + client.Write("PRIVMSG *clientnotify :NotifyOnNewIP on"); + + auto client8 = LoginClient(); + check_not_sent(client, ":Another client (127.0.0.1 / identifier123) authenticated as your user. Use the 'ListClients' command to see all 8 clients."); + auto client9 = LoginClient("definitely_not_identifier123"); + client.ReadUntil(":Another client (127.0.0.1 / definitely_not_identifier123) authenticated as your user. Use the 'ListClients' command to see all 9 clients."); +} + TEST_F(ZNCTest, ShellModule) { auto znc = Run(); auto ircd = ConnectIRCd();