From 8d77faa260d2a3eb0de7e6a78e7baad0d6cb7b62 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 28 Sep 2014 01:17:03 +0200 Subject: [PATCH] Allow clients to specify an ID via PASS or USER - PASS [user[@identifier][/network]:]password - USER user[@identifier][/network] ... NOTE: There's a slight ambiguosity with the '@' character, which happens to be a valid character in usernames, but also acts as a marker for the identifier. Therefore, '@' is considered as part of the username if it's followed by non-word characters (as in an email address), otherwise as a marker for an identifier. This is only an enabler for #343. The rest can be done with modules: - managing client ID specific playback buffers - filtering channels based on the client ID The reason this should be part of ZNC core is that only global modules have access to OnUnknownUserRaw(), which is needed to capture USER/PASS. First of all, the aforementioned modules shouldn't be global. Furthermore, it would be possible to have only one module that parsed and removed the client ID so that ZNC core woulnd't choke. --- Makefile.in | 2 +- include/znc/Client.h | 9 +++++ src/Client.cpp | 91 ++++++++++++++++++++++++++++++++++---------- test/ClientTest.cpp | 64 +++++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+), 21 deletions(-) create mode 100644 test/ClientTest.cpp diff --git a/Makefile.in b/Makefile.in index ae6235af..74d8896e 100644 --- a/Makefile.in +++ b/Makefile.in @@ -47,7 +47,7 @@ LIB_SRCS := $(addprefix src/,$(LIB_SRCS)) BIN_SRCS := src/main.cpp LIB_OBJS := $(patsubst %cpp,%o,$(LIB_SRCS)) BIN_OBJS := $(patsubst %cpp,%o,$(BIN_SRCS)) -TESTS := StringTest ConfigTest UtilsTest ThreadTest NickTest +TESTS := StringTest ConfigTest UtilsTest ThreadTest NickTest ClientTest TESTS := $(addprefix test/,$(addsuffix .o,$(TESTS))) CLEAN := znc src/*.o test/*.o core core.* .version_extra .depend modules/.depend unittest DISTCLEAN := Makefile config.log config.status znc-buildmod \ diff --git a/include/znc/Client.h b/include/znc/Client.h index 938df057..855357e5 100644 --- a/include/znc/Client.h +++ b/include/znc/Client.h @@ -112,6 +112,7 @@ public: CString GetNick(bool bAllowIRCNick = true) const; CString GetNickMask() const; + CString GetIdentifier() const { return m_sIdentifier; } bool HasNamesx() const { return m_bNamesx; } bool HasUHNames() const { return m_bUHNames; } bool IsAway() const { return m_bAway; } @@ -119,6 +120,8 @@ public: bool HasBatch() const { return m_bBatch; } bool HasSelfMessage() const { return m_bSelfMessage; } + static bool IsValidIdentifier(const CString& sIdentifier); + void UserCommand(CString& sLine); void UserPortCommand(CString& sLine); void StatusCTCP(const CString& sCommand); @@ -157,6 +160,9 @@ public: private: void HandleCap(const CString& sLine); void RespondCap(const CString& sResponse); + void ParsePass(const CString& sAuthLine); + void ParseUser(const CString& sAuthLine); + void ParseIdentifier(const CString& sAuthLine); protected: bool m_bGotPass; @@ -175,8 +181,11 @@ protected: CString m_sPass; CString m_sUser; CString m_sNetwork; + CString m_sIdentifier; std::shared_ptr m_spAuth; SCString m_ssAcceptedCaps; + + friend class ClientTest; }; #endif // !_CLIENT_H diff --git a/src/Client.cpp b/src/Client.cpp index 05cc2e21..b79e3576 100644 --- a/src/Client.cpp +++ b/src/Client.cpp @@ -118,20 +118,7 @@ void CClient::ReadLine(const CString& sData) { m_bGotPass = true; CString sAuthLine = sLine.Token(1, true).TrimPrefix_n(); - - // [user[/network]:]password - if (sAuthLine.find(":") == CString::npos) { - m_sPass = sAuthLine; - sAuthLine = ""; - } else { - m_sPass = sAuthLine.Token(1, true, ":"); - sAuthLine = sAuthLine.Token(0, false, ":"); - } - - if (!sAuthLine.empty()) { - m_sUser = sAuthLine.Token(0, false, "/"); - m_sNetwork = sAuthLine.Token(1, true, "/"); - } + ParsePass(sAuthLine); AuthUser(); return; // Don't forward this msg. ZNC has already registered us. @@ -144,12 +131,10 @@ void CClient::ReadLine(const CString& sData) { AuthUser(); return; // Don't forward this msg. ZNC will handle nick changes until auth is complete } else if (sCommand.Equals("USER")) { - // user[/network] CString sAuthLine = sLine.Token(1); if (m_sUser.empty() && !sAuthLine.empty()) { - m_sUser = sAuthLine.Token(0, false, "/"); - m_sNetwork = sAuthLine.Token(1, true, "/"); + ParseUser(sAuthLine); } m_bGotUser = true; @@ -771,9 +756,12 @@ void CClient::PutIRC(const CString& sLine) { CString CClient::GetFullName() const { if (!m_pUser) return GetRemoteIP(); - if (!m_pNetwork) - return m_pUser->GetUserName(); - return m_pUser->GetUserName() + "/" + m_pNetwork->GetName(); + CString sFullName = m_pUser->GetUserName(); + if (!m_sIdentifier.empty()) + sFullName += "@" + m_sIdentifier; + if (m_pNetwork) + sFullName += "/" + m_pNetwork->GetName(); + return sFullName; } void CClient::PutClient(const CString& sLine) { @@ -849,6 +837,25 @@ CString CClient::GetNickMask() const { return GetNick() + "!" + (m_pNetwork ? m_pNetwork->GetBindHost() : m_pUser->GetIdent()) + "@" + sHost; } +bool CClient::IsValidIdentifier(const CString& sIdentifier) { + // ^[-\w]+$ + + if (sIdentifier.empty()) { + return false; + } + + const char *p = sIdentifier.c_str(); + while (*p) { + if (*p != '_' && *p != '-' && !isalnum(*p)) { + return false; + } + + p++; + } + + return true; +} + void CClient::RespondCap(const CString& sResponse) { PutClient(":irc.znc.in CAP " + GetNick() + " " + sResponse); @@ -971,3 +978,47 @@ void CClient::HandleCap(const CString& sLine) PutClient(":irc.znc.in 410 " + GetNick() + " " + sSubCmd + " :Invalid CAP subcommand"); } } + +void CClient::ParsePass(const CString& sAuthLine) { + // [user[@identifier][/network]:]password + + const size_t uColon = sAuthLine.find(":"); + if (uColon != CString::npos) { + m_sPass = sAuthLine.substr(uColon + 1); + + ParseUser(sAuthLine.substr(0, uColon)); + } else { + m_sPass = sAuthLine; + } +} + +void CClient::ParseUser(const CString& sAuthLine) { + // user[@identifier][/network] + + const size_t uSlash = sAuthLine.rfind("/"); + if (uSlash != CString::npos) { + m_sNetwork = sAuthLine.substr(uSlash + 1); + + ParseIdentifier(sAuthLine.substr(0, uSlash)); + } else { + ParseIdentifier(sAuthLine); + } +} + +void CClient::ParseIdentifier(const CString& sAuthLine) { + // user[@identifier] + + const size_t uAt = sAuthLine.rfind("@"); + if (uAt != CString::npos) { + const CString sId = sAuthLine.substr(uAt + 1); + + if (IsValidIdentifier(sId)) { + m_sIdentifier = sId; + m_sUser = sAuthLine.substr(0, uAt); + } else { + m_sUser = sAuthLine; + } + } else { + m_sUser = sAuthLine; + } +} diff --git a/test/ClientTest.cpp b/test/ClientTest.cpp new file mode 100644 index 00000000..36edc967 --- /dev/null +++ b/test/ClientTest.cpp @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2004-2014 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 +#include +#include + +class ClientTest : public ::testing::Test { +protected: + void SetUp() { CZNC::CreateInstance(); } + void TearDown() { CZNC::DestroyInstance(); } + void testPass(const CString& sInput, const CString& sUser, const CString& sIdentifier, const CString& sNetwork, const CString& sPass) const { + CClient client; + client.ParsePass(sInput); + EXPECT_EQ(sUser, client.m_sUser); + EXPECT_EQ(sIdentifier, client.m_sIdentifier); + EXPECT_EQ(sNetwork, client.m_sNetwork); + EXPECT_EQ(sPass, client.m_sPass); + } + + void testUser(const CString& sInput, const CString& sUser, const CString& sIdentifier, const CString& sNetwork) const { + CClient client; + client.ParseUser(sInput); + EXPECT_EQ(sUser, client.m_sUser); + EXPECT_EQ(sIdentifier, client.m_sIdentifier); + EXPECT_EQ(sNetwork, client.m_sNetwork); + } +}; + +TEST_F(ClientTest, Pass) { + testPass("p@ss#w0rd", "", "", "", "p@ss#w0rd"); + testPass("user:p@ss#w0rd", "user", "", "", "p@ss#w0rd"); + testPass("user/net-work:p@ss#w0rd", "user", "", "net-work", "p@ss#w0rd"); + testPass("user@identifier:p@ss#w0rd", "user", "identifier", "", "p@ss#w0rd"); + testPass("user@identifier/net-work:p@ss#w0rd", "user", "identifier", "net-work", "p@ss#w0rd"); + + testPass("user@znc.in:p@ss#w0rd", "user@znc.in", "", "", "p@ss#w0rd"); + testPass("user@znc.in/net-work:p@ss#w0rd", "user@znc.in", "", "net-work", "p@ss#w0rd"); + testPass("user@znc.in@identifier:p@ss#w0rd", "user@znc.in", "identifier", "", "p@ss#w0rd"); + testPass("user@znc.in@identifier/net-work:p@ss#w0rd", "user@znc.in", "identifier", "net-work", "p@ss#w0rd"); +} + +TEST_F(ClientTest, User) { + testUser("user/net-work", "user", "", "net-work"); + testUser("user@identifier", "user", "identifier", ""); + testUser("user@identifier/net-work", "user", "identifier", "net-work"); + + testUser("user@znc.in/net-work", "user@znc.in", "", "net-work"); + testUser("user@znc.in@identifier", "user@znc.in", "identifier", ""); + testUser("user@znc.in@identifier/net-work", "user@znc.in", "identifier", "net-work"); +}