From d27e2cce5c098a5aea25b4dc8db511a3dade4bb8 Mon Sep 17 00:00:00 2001 From: MrLenin Date: Tue, 26 Jun 2018 22:38:27 -0400 Subject: [PATCH 01/19] SASL Authentication for Clients --- include/znc/Client.h | 17 ++++ include/znc/Message.h | 8 ++ include/znc/Modules.h | 17 ++++ modules/saslplain.cpp | 65 +++++++++++++++ src/Client.cpp | 180 +++++++++++++++++++++++++++++++++++++++++- src/Message.cpp | 1 + src/Modules.cpp | 35 ++++++++ 7 files changed, 320 insertions(+), 3 deletions(-) create mode 100644 modules/saslplain.cpp diff --git a/include/znc/Client.h b/include/znc/Client.h index 5428cf44..083f2696 100644 --- a/include/znc/Client.h +++ b/include/znc/Client.h @@ -116,6 +116,10 @@ class CClient : public CIRCSocket { m_bBatch(false), m_bEchoMessage(false), m_bSelfMessage(false), + m_bSasl(false), + m_bSaslAuthenticating(false), + m_bSaslAuthenticated(false), + m_bSaslMultipart(false), m_bPlaybackActive(false), m_pUser(nullptr), m_pNetwork(nullptr), @@ -124,6 +128,8 @@ class CClient : public CIRCSocket { m_sUser(""), m_sNetwork(""), m_sIdentifier(""), + m_sSaslBuffer(""), + m_sSaslMechanism(""), m_spAuth(), m_ssAcceptedCaps(), m_ssSupportedTags(), @@ -156,6 +162,7 @@ class CClient : public CIRCSocket { }}}, {"extended-join", {true, [this](bool bVal) { m_bExtendedJoin = bVal; }}}, + {"sasl", {false, [this](bool bVal) { m_bSasl = bVal; m_bSaslAuthenticating = bVal; }}}, }) { EnableReadLine(); // RFC says a line can have 512 chars max, but we are @@ -333,6 +340,10 @@ class CClient : public CIRCSocket { unsigned int DetachChans(const std::set& sChans); bool OnActionMessage(CActionMessage& Message); + void OnAuthenticateMessage(CAuthenticateMessage& Message); + + CString EnumerateSaslMechanisms(SCString& ssMechanisms); + bool OnCTCPMessage(CCTCPMessage& Message); bool OnJoinMessage(CJoinMessage& Message); bool OnModeMessage(CModeMessage& Message); @@ -362,6 +373,10 @@ class CClient : public CIRCSocket { bool m_bBatch; bool m_bEchoMessage; bool m_bSelfMessage; + bool m_bSasl; + bool m_bSaslAuthenticating; + bool m_bSaslAuthenticated; + bool m_bSaslMultipart; bool m_bPlaybackActive; CUser* m_pUser; CIRCNetwork* m_pNetwork; @@ -370,6 +385,8 @@ class CClient : public CIRCSocket { CString m_sUser; CString m_sNetwork; CString m_sIdentifier; + CString m_sSaslBuffer; + CString m_sSaslMechanism; std::shared_ptr m_spAuth; SCString m_ssAcceptedCaps; SCString m_ssSupportedTags; diff --git a/include/znc/Message.h b/include/znc/Message.h index 0b6d374f..064a6984 100644 --- a/include/znc/Message.h +++ b/include/znc/Message.h @@ -78,6 +78,7 @@ class CMessage { Unknown, Account, Action, + Authenticate, Away, Capability, CTCP, @@ -250,6 +251,13 @@ class CActionMessage : public CTargetMessage { }; REGISTER_ZNC_MESSAGE(CActionMessage); +class CAuthenticateMessage : public CMessage { + public: + CString GetText() const { return GetParam(0); } + void SetText(const CString& sText) { SetParam(0, sText); } +}; +REGISTER_ZNC_MESSAGE(CAuthenticateMessage); + class CCTCPMessage : public CTargetMessage { public: bool IsReply() const { return GetCommand().Equals("NOTICE"); } diff --git a/include/znc/Modules.h b/include/znc/Modules.h index ef8f2219..a36531db 100644 --- a/include/znc/Modules.h +++ b/include/znc/Modules.h @@ -1308,6 +1308,14 @@ class CModule { */ virtual void OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState); + virtual EModRet OnSaslServerChallenge(const CString& sMechanism, + CString& sResponse); + virtual EModRet OnClientSaslAuthenticate(const CString& sMechanism, + const CString& sBuffer, + CString& sUser, + CString& sMechanismResponse, + bool& bAuthenticationSuccess); + virtual void OnGetSaslMechanisms(SCString& ssMechanisms); /** Called when a module is going to be loaded. * @param sModName name of the module. @@ -1587,6 +1595,15 @@ class CModules : public std::vector, private CCoreTranslationMixin { bool IsClientCapSupported(CClient* pClient, const CString& sCap, bool bState); bool OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState); + bool OnSaslServerChallenge(const CString& sMechanism, + CString& sResponse); + bool OnClientSaslAuthenticate(const CString& sMechanism, + const CString& sBuffer, + CString& sUser, + CString& sResponse, + bool& bAuthenticationSuccess); + bool OnGetSaslMechanisms(SCString& ssMechanisms); + bool OnModuleLoading(const CString& sModName, const CString& sArgs, CModInfo::EModuleType eType, bool& bSuccess, CString& sRetMsg); diff --git a/modules/saslplain.cpp b/modules/saslplain.cpp new file mode 100644 index 00000000..ce4cfbb0 --- /dev/null +++ b/modules/saslplain.cpp @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2004-2018 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 + +class CSASLMechanismPlain : public CModule { + public: + MODCONSTRUCTOR(CSASLMechanismPlain) { AddHelpCommand(); } + + EModRet OnClientSaslAuthenticate(const CString& sMechanism, + const CString& sBuffer, CString& sUser, + CString& sMechanismResponse, + bool& bAuthenticationSuccess) override { + if (!sMechanism.Equals("PLAIN")) { + return CONTINUE; + } + + bAuthenticationSuccess = false; + + CString sNullSeparator = std::string("\0", 1); + auto sAuthzId = sBuffer.Token(0, false, sNullSeparator); + auto sAuthcId = sBuffer.Token(1, false, sNullSeparator); + auto sPassword = sBuffer.Token(2, false, sNullSeparator); + + if (sAuthzId.empty()) sAuthzId = sAuthcId; + + auto pUser = CZNC::Get().FindUser(sAuthcId); + + if (!sAuthcId.empty() && !sPassword.empty()) { + if (pUser->CheckPass(sPassword)) { + bAuthenticationSuccess = true; + sUser = sAuthcId; + } + } + + return HALTMODS; + } + + void OnGetSaslMechanisms(SCString& ssMechanisms) override { + ssMechanisms.insert("PLAIN"); + } +}; + +template <> +void TModInfo(CModInfo& Info) { + Info.SetWikiPage("saslplain"); +} + +GLOBALMODULEDEFS( + CSASLMechanismPlain, + t_s("Allows users to authenticate via the PLAIN SASL mechanism.")) diff --git a/src/Client.cpp b/src/Client.cpp index 6d215218..f02676b8 100644 --- a/src/Client.cpp +++ b/src/Client.cpp @@ -179,6 +179,12 @@ void CClient::ReadLine(const CString& sData) { return; } + if (Message.GetType() == CMessage::Type::Authenticate) { + OnAuthenticateMessage(Message); + + return; + } + if (!m_pUser) { // Only CAP, NICK, USER and PASS are allowed before login return; @@ -314,9 +320,16 @@ bool CClient::SendMotd() { } void CClient::AuthUser() { - if (!m_bGotNick || !m_bGotUser || !m_bGotPass || m_bInCap || IsAttached()) + if (!m_bGotNick || !m_bGotUser || m_bInCap || + (!m_bSaslAuthenticated && !m_bGotPass) || IsAttached()) return; + if (m_bSasl && m_bSaslAuthenticated) { + auto pUser = CZNC::Get().FindUser(m_sUser); + AcceptLogin(*pUser); + return; + } + m_spAuth = std::make_shared(this, m_sUser, m_sPass); CZNC::Get().AuthUser(m_spAuth); @@ -380,6 +393,7 @@ void CClientAuth::AcceptedLogin(CUser& User) { void CClient::AcceptLogin(CUser& User) { m_sPass = ""; m_pUser = &User; + m_bSaslAuthenticating = m_bSasl; // Set our proper timeout and set back our proper timeout mode // (constructor set a different timeout and mode) @@ -695,8 +709,15 @@ void CClient::HandleCap(const CMessage& Message) { for (const auto& it : m_mCoreCaps) { bool bServerDependent = std::get<0>(it.second); if (!bServerDependent || - m_ssServerDependentCaps.count(it.first) > 0) + m_ssServerDependentCaps.count(it.first) > 0) { + if (it.first.Equals("sasl")) { + SCString ssMechanisms; + ssOfferCaps.insert(it.first + "=" + + EnumerateSaslMechanisms(ssMechanisms)); + } else { ssOfferCaps.insert(it.first); + } + } } GLOBALMODULECALL(OnClientCapLs(this, ssOfferCaps), NOTHING); CString sRes = @@ -709,7 +730,17 @@ void CClient::HandleCap(const CMessage& Message) { } else if (sSubCmd.Equals("END")) { m_bInCap = false; if (!IsAttached()) { - if (!m_pUser && m_bGotUser && !m_bGotPass) { + if (m_bSasl && !m_bSaslAuthenticated && m_bSaslAuthenticating) { + PutClient(":irc.znc.in 906 " + GetNick() + + " :SASL authentication aborted"); + m_sSaslMechanism = ""; + m_bSaslAuthenticated = false; + m_bSaslMultipart = false; + m_bSaslAuthenticating = false; + } + + if (!m_pUser && m_bGotUser && + (!m_bSaslAuthenticated && !m_bGotPass)) { SendRequiredPasswordNotice(); } else { AuthUser(); @@ -966,6 +997,149 @@ bool CClient::OnActionMessage(CActionMessage& Message) { return true; } +void CClient::OnAuthenticateMessage(CAuthenticateMessage& Message) { + const auto uiMaxSaslMsgLength = 400u; + auto bAuthenticationSuccess = false; + auto sMessage = Message.GetText(); + const auto sBufferSize = sMessage.length(); + SCString ssMechanisms; + + auto SaslReset = [this]() { + m_sSaslMechanism = ""; + m_sSaslBuffer = ""; + m_bSaslMultipart = false; + }; + + auto SaslChallenge = [this](CString sChallenge) { + sChallenge.Base64Encode(); + auto sChallengeSize = sChallenge.length(); + + if (sChallengeSize > uiMaxSaslMsgLength) { + for (auto i = 0u; i < sChallengeSize; i += uiMaxSaslMsgLength) { + CString sMsgPart = sChallenge.substr(i, uiMaxSaslMsgLength); + PutClient("AUTHENTICATE " + sMsgPart); + } + } else { + PutClient("AUTHENTICATE " + sChallenge); + } + }; + + if (!m_bSasl) return; + + if (m_bSaslAuthenticated || IsAttached()) { + PutClient(":irc.znc.in 907 " + GetNick() + + " :You have already authenticated using SASL"); + return; + } + + if (!m_bSaslAuthenticating || sMessage.Equals("*")) { + PutClient(":irc.znc.in 906 " + GetNick() + + " :SASL authentication aborted"); + if (!IsAttached()) { + m_bSaslAuthenticating = false; + SaslReset(); + } + return; + } + + auto sMechanisms = EnumerateSaslMechanisms(ssMechanisms); + + if (sBufferSize > uiMaxSaslMsgLength) { + PutClient(":irc.znc.in 905 " + GetNick() + " :SASL message too long"); + SaslReset(); + return; + } + + if (m_sSaslMechanism.empty()) { + if (ssMechanisms.find(sMessage) == ssMechanisms.end()) { + PutClient(":irc.znc.in 908 " + GetNick() + " " + sMechanisms + + " :are available SASL mechanisms"); + PutClient(":irc.znc.in 904 " + GetNick() + + " :SASL authentication failed"); + SaslReset(); + + return; + } + + m_sSaslMechanism = sMessage; + + auto bResult = false; + CString sChallenge; + GLOBALMODULECALL(OnSaslServerChallenge(m_sSaslMechanism, sChallenge), + &bResult); + if (bResult) { + SaslChallenge(sChallenge); + } else { + PutClient("AUTHENTICATE +"); + } + return; + } + + if (sBufferSize == uiMaxSaslMsgLength) { + m_bSaslMultipart = true; + m_sSaslBuffer.append(sMessage); + + return; + } + + if ((m_bSaslMultipart && !sMessage.Equals("+"))) { + m_sSaslBuffer.append(sMessage); + m_bSaslMultipart = false; + } else if (!m_bSaslMultipart && !sMessage.Equals("+")) { + m_sSaslBuffer.assign(sMessage); + } + + m_sSaslBuffer.Base64Decode(); + + auto sAuthcId = m_sUser; + auto sAuthzId = m_sUser; + + CString sResponse; + bool bResult; + + GLOBALMODULECALL(OnClientSaslAuthenticate( + m_sSaslMechanism, m_sSaslBuffer, sAuthcId, + sResponse, bAuthenticationSuccess), + &bResult); + + if (bResult && !sResponse.empty()) { + SaslChallenge(sResponse); + return; + } + + m_sSaslBuffer.clear(); + + auto pUser = CZNC::Get().FindUser(sAuthcId); + + if (pUser && bAuthenticationSuccess) { + PutClient(":irc.znc.in 900 " + GetNick() + " " + GetNick() + "!" + + pUser->GetIdent() + "@" + GetHostName() + " " + sAuthcId + + " :You are now logged in as " + sAuthzId); + PutClient(":irc.znc.in 903 " + GetNick() + + " :SASL authentication successful"); + m_bSaslAuthenticated = true; + m_bSaslAuthenticating = false; + } else { + PutClient(":irc.znc.in 904 " + GetNick() + " :SASL authentication failed"); + SaslReset(); + } + + return; +} + +CString CClient::EnumerateSaslMechanisms(SCString& ssMechanisms) { + CString sMechanisms; + + GLOBALMODULECALL(OnGetSaslMechanisms(ssMechanisms), NOTHING); + + if (ssMechanisms.size()) { + sMechanisms = + CString(",").Join(ssMechanisms.begin(), ssMechanisms.end()); + } + + return sMechanisms; +} + bool CClient::OnCTCPMessage(CCTCPMessage& Message) { CString sTargets = Message.GetTarget(); diff --git a/src/Message.cpp b/src/Message.cpp index 6a6af073..2956e31b 100644 --- a/src/Message.cpp +++ b/src/Message.cpp @@ -267,6 +267,7 @@ void CMessage::InitType() { } else { std::map mTypes = { {"ACCOUNT", Type::Account}, + {"AUTHENTICATE", Type::Authenticate}, {"AWAY", Type::Away}, {"CAP", Type::Capability}, {"ERROR", Type::Error}, diff --git a/src/Modules.cpp b/src/Modules.cpp index d69d6050..92a89451 100644 --- a/src/Modules.cpp +++ b/src/Modules.cpp @@ -1075,6 +1075,22 @@ bool CModule::IsClientCapSupported(CClient* pClient, const CString& sCap, } void CModule::OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState) {} + +CModule::EModRet CModule::OnClientSaslAuthenticate(const CString& sMechanism, + const CString& sBuffer, + CString& sUser, + CString& sMechanismResponse, + bool& bAuthenticationSuccess) { + return CONTINUE; +} + +CModule::EModRet CModule::OnSaslServerChallenge(const CString& sMechanism, + CString& sResponse) { + return CONTINUE; +} + +void CModule::OnGetSaslMechanisms(SCString& ssMechanisms) {} + CModule::EModRet CModule::OnModuleLoading(const CString& sModName, const CString& sArgs, CModInfo::EModuleType eType, @@ -1592,6 +1608,25 @@ bool CModules::OnClientCapRequest(CClient* pClient, const CString& sCap, return false; } +bool CModules::OnClientSaslAuthenticate(const CString& sMechanism, + const CString& sBuffer, + CString& sUser, + CString& sResponse, + bool& bAuthenticationSuccess) { + MODHALTCHK(OnClientSaslAuthenticate(sMechanism, sBuffer, sUser, + sResponse, bAuthenticationSuccess)); +} + +bool CModules::OnSaslServerChallenge(const CString& sMechanism, + CString& sResponse) { + MODHALTCHK(OnSaslServerChallenge(sMechanism, sResponse)); +} + +bool CModules::OnGetSaslMechanisms(SCString& ssMechanisms) { + MODUNLOADCHK(OnGetSaslMechanisms(ssMechanisms)); + return false; +} + bool CModules::OnModuleLoading(const CString& sModName, const CString& sArgs, CModInfo::EModuleType eType, bool& bSuccess, CString& sRetMsg) { From 1dd995ef77ffe3ba9f6006265ffd4c89eb4d7608 Mon Sep 17 00:00:00 2001 From: delthas Date: Thu, 31 Aug 2023 11:24:53 +0200 Subject: [PATCH 02/19] Reabse and address PR comments --- include/znc/Client.h | 31 +++---- include/znc/Modules.h | 33 ++++++-- modules/modpython/functions.in | 3 + modules/modpython/module.h | 8 ++ modules/modpython/znc.py | 9 ++ modules/saslplain.cpp | 15 ++-- src/Client.cpp | 128 ++++++++++++++--------------- src/Modules.cpp | 18 ++-- test/integration/tests/modules.cpp | 20 +++++ 9 files changed, 166 insertions(+), 99 deletions(-) diff --git a/include/znc/Client.h b/include/znc/Client.h index 083f2696..82eb1bc0 100644 --- a/include/znc/Client.h +++ b/include/znc/Client.h @@ -116,10 +116,8 @@ class CClient : public CIRCSocket { m_bBatch(false), m_bEchoMessage(false), m_bSelfMessage(false), - m_bSasl(false), - m_bSaslAuthenticating(false), - m_bSaslAuthenticated(false), - m_bSaslMultipart(false), + m_bSASL(false), + m_bSASLAuthenticating(false), m_bPlaybackActive(false), m_pUser(nullptr), m_pNetwork(nullptr), @@ -128,8 +126,9 @@ class CClient : public CIRCSocket { m_sUser(""), m_sNetwork(""), m_sIdentifier(""), - m_sSaslBuffer(""), - m_sSaslMechanism(""), + m_sSASLBuffer(""), + m_sSASLMechanism(""), + m_sSASLUser(""), m_spAuth(), m_ssAcceptedCaps(), m_ssSupportedTags(), @@ -162,7 +161,7 @@ class CClient : public CIRCSocket { }}}, {"extended-join", {true, [this](bool bVal) { m_bExtendedJoin = bVal; }}}, - {"sasl", {false, [this](bool bVal) { m_bSasl = bVal; m_bSaslAuthenticating = bVal; }}}, + {"sasl", {false, [this](bool bVal) { m_bSASL = bVal; m_bSASLAuthenticating = bVal; }}}, }) { EnableReadLine(); // RFC says a line can have 512 chars max, but we are @@ -342,7 +341,12 @@ class CClient : public CIRCSocket { bool OnActionMessage(CActionMessage& Message); void OnAuthenticateMessage(CAuthenticateMessage& Message); - CString EnumerateSaslMechanisms(SCString& ssMechanisms); + /** + * Fills all available SASL mechanisms in the passed set, and returns a comma-joined string of those mechanisms. + * @param ssMechanisms Set of supported mechanisms, filled by this method. + * @return A comma-joined string of supported mechanisms. + */ + CString EnumerateSASLMechanisms(SCString& ssMechanisms); bool OnCTCPMessage(CCTCPMessage& Message); bool OnJoinMessage(CJoinMessage& Message); @@ -373,10 +377,8 @@ class CClient : public CIRCSocket { bool m_bBatch; bool m_bEchoMessage; bool m_bSelfMessage; - bool m_bSasl; - bool m_bSaslAuthenticating; - bool m_bSaslAuthenticated; - bool m_bSaslMultipart; + bool m_bSASL; + bool m_bSASLAuthenticating; bool m_bPlaybackActive; CUser* m_pUser; CIRCNetwork* m_pNetwork; @@ -385,8 +387,9 @@ class CClient : public CIRCSocket { CString m_sUser; CString m_sNetwork; CString m_sIdentifier; - CString m_sSaslBuffer; - CString m_sSaslMechanism; + CString m_sSASLBuffer; + CString m_sSASLMechanism; + CString m_sSASLUser; std::shared_ptr m_spAuth; SCString m_ssAcceptedCaps; SCString m_ssSupportedTags; diff --git a/include/znc/Modules.h b/include/znc/Modules.h index a36531db..a9d454ae 100644 --- a/include/znc/Modules.h +++ b/include/znc/Modules.h @@ -1308,14 +1308,35 @@ class CModule { */ virtual void OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState); - virtual EModRet OnSaslServerChallenge(const CString& sMechanism, + /** Called when a client requests SASL authentication. Use ssMechanisms.insert("mechanism") + * for announcing sASL mechanisms which your module supports. + * @param ssMechanisms The set of supported SASL mechanisms to append to. + */ + virtual void OnGetSASLMechanisms(SCString& ssMechanisms); + /** Called when a client has selected a SASL mechanism for SASL authentication. + * If implementing a SASL authentication mechanism, set sResponse to specify an initial challenge + * message to send to the client. Otherwise, an empty response will be sent. + * @param sMechanism The SASL mechanism selected by the client. + * @param sResponse The optional value of an initial SASL challenge message to send to the client. + */ + virtual EModRet OnSASLServerChallenge(const CString& sMechanism, CString& sResponse); - virtual EModRet OnClientSaslAuthenticate(const CString& sMechanism, + /** Called when a client is sending us a SASL message after the mechanism was selected. + * If implementing a SASL authentication mechanism, check the passed credentials, + * then either request more data by sending a challenge in sMechanismResponse, + * reject authentication by setting bAuthenticationSuccess to false, + * or accept authentication by setting bAuthenticationSuccess to true and setting sUser to the authenticated user name. + * @param sMechanism The SASL mechanism selected by the client. + * @param sBuffer The SASL opaque value/credentials sent by the client. + * @param sUser The optional name of the authenticated user to log in the user as, if authentication is accepted. + * @param sMechanismResponse The optional value of a SASL challenge message to reply to the client to ask for more data. + * @param bAuthenticationSuccess If sMechanismResponse is not set, whether to accept or reject the authentication request. + */ + virtual EModRet OnClientSASLAuthenticate(const CString& sMechanism, const CString& sBuffer, CString& sUser, CString& sMechanismResponse, bool& bAuthenticationSuccess); - virtual void OnGetSaslMechanisms(SCString& ssMechanisms); /** Called when a module is going to be loaded. * @param sModName name of the module. @@ -1595,14 +1616,14 @@ class CModules : public std::vector, private CCoreTranslationMixin { bool IsClientCapSupported(CClient* pClient, const CString& sCap, bool bState); bool OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState); - bool OnSaslServerChallenge(const CString& sMechanism, + bool OnGetSASLMechanisms(SCString& ssMechanisms); + bool OnSASLServerChallenge(const CString& sMechanism, CString& sResponse); - bool OnClientSaslAuthenticate(const CString& sMechanism, + bool OnClientSASLAuthenticate(const CString& sMechanism, const CString& sBuffer, CString& sUser, CString& sResponse, bool& bAuthenticationSuccess); - bool OnGetSaslMechanisms(SCString& ssMechanisms); bool OnModuleLoading(const CString& sModName, const CString& sArgs, CModInfo::EModuleType eType, bool& bSuccess, diff --git a/modules/modpython/functions.in b/modules/modpython/functions.in index f779e577..52707995 100644 --- a/modules/modpython/functions.in +++ b/modules/modpython/functions.in @@ -109,6 +109,9 @@ EModRet OnUnknownUserRaw(CClient* pClient, CString& sLine) EModRet OnUnknownUserRawMessage(CMessage& Message) bool IsClientCapSupported(CClient* pClient, const CString& sCap, bool bState) void OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState) +void OnGetSASLMechanisms(SCString& ssMechanisms) +EModRet OnSASLServerChallenge(const CString& sMechanism, CString& sResponse) +EModRet OnClientSASLAuthenticate(const CString& sMechanism, const CString& sBuffer, CString& sUser, CString& sMechanismResponse, bool& bAuthenticationSuccess) EModRet OnModuleLoading(const CString& sModName, const CString& sArgs, CModInfo::EModuleType eType, bool& bSuccess, CString& sRetMsg) EModRet OnModuleUnloading(CModule* pModule, bool& bSuccess, CString& sRetMsg) EModRet OnGetModInfo(CModInfo& ModInfo, const CString& sModule, bool& bSuccess, CString& sRetMsg) diff --git a/modules/modpython/module.h b/modules/modpython/module.h index a0847a20..d4f20d4d 100644 --- a/modules/modpython/module.h +++ b/modules/modpython/module.h @@ -191,6 +191,14 @@ class ZNC_EXPORT_LIB_EXPORT CPyModule : public CModule { bool bState) override; void OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState) override; + void OnGetSASLMechanisms(SCString& ssMechanisms) override; + EModRet OnSASLServerChallenge(const CString& sMechanism, + CString& sResponse) override; + EModRet OnClientSASLAuthenticate(const CString& sMechanism, + const CString& sBuffer, + CString& sUser, + CString& sMechanismResponse, + bool& bAuthenticationSuccess) override; virtual EModRet OnModuleLoading(const CString& sModName, const CString& sArgs, CModInfo::EModuleType eType, bool& bSuccess, diff --git a/modules/modpython/znc.py b/modules/modpython/znc.py index 4e8fc295..b8c56e80 100644 --- a/modules/modpython/znc.py +++ b/modules/modpython/znc.py @@ -469,6 +469,15 @@ class Module: def OnClientCapRequest(self, pClient, sCap, bState): pass + def OnGetSASLMechanisms(self, ssMechanisms): + pass + + def OnSASLServerChallenge(self, sMechanism, sResponse): + pass + + def OnClientSASLAuthenticate(self, sMechanism, sBuffer, sUser, sResponse, bAuthenticationSuccess): + pass + def OnModuleLoading(self, sModName, sArgs, eType, bSuccess, sRetMsg): pass diff --git a/modules/saslplain.cpp b/modules/saslplain.cpp index ce4cfbb0..3f467397 100644 --- a/modules/saslplain.cpp +++ b/modules/saslplain.cpp @@ -21,7 +21,7 @@ class CSASLMechanismPlain : public CModule { public: MODCONSTRUCTOR(CSASLMechanismPlain) { AddHelpCommand(); } - EModRet OnClientSaslAuthenticate(const CString& sMechanism, + EModRet OnClientSASLAuthenticate(const CString& sMechanism, const CString& sBuffer, CString& sUser, CString& sMechanismResponse, bool& bAuthenticationSuccess) override { @@ -32,11 +32,14 @@ class CSASLMechanismPlain : public CModule { bAuthenticationSuccess = false; CString sNullSeparator = std::string("\0", 1); - auto sAuthzId = sBuffer.Token(0, false, sNullSeparator); - auto sAuthcId = sBuffer.Token(1, false, sNullSeparator); - auto sPassword = sBuffer.Token(2, false, sNullSeparator); + auto sAuthzId = sBuffer.Token(0, false, sNullSeparator, true); + auto sAuthcId = sBuffer.Token(1, false, sNullSeparator, true); + auto sPassword = sBuffer.Token(2, false, sNullSeparator, true); - if (sAuthzId.empty()) sAuthzId = sAuthcId; + if (!sAuthzId.empty() && sAuthzId != sAuthcId) { + // Reject custom SASL plain authorization identifiers + return HALTMODS; + } auto pUser = CZNC::Get().FindUser(sAuthcId); @@ -50,7 +53,7 @@ class CSASLMechanismPlain : public CModule { return HALTMODS; } - void OnGetSaslMechanisms(SCString& ssMechanisms) override { + void OnGetSASLMechanisms(SCString& ssMechanisms) override { ssMechanisms.insert("PLAIN"); } }; diff --git a/src/Client.cpp b/src/Client.cpp index f02676b8..c76b94e8 100644 --- a/src/Client.cpp +++ b/src/Client.cpp @@ -321,10 +321,11 @@ bool CClient::SendMotd() { void CClient::AuthUser() { if (!m_bGotNick || !m_bGotUser || m_bInCap || - (!m_bSaslAuthenticated && !m_bGotPass) || IsAttached()) + (m_sSASLUser.empty() && !m_bGotPass) || IsAttached()) return; - if (m_bSasl && m_bSaslAuthenticated) { + if (m_bSASL && !m_sSASLUser.empty()) { + m_sUser = m_sSASLUser; auto pUser = CZNC::Get().FindUser(m_sUser); AcceptLogin(*pUser); return; @@ -393,7 +394,7 @@ void CClientAuth::AcceptedLogin(CUser& User) { void CClient::AcceptLogin(CUser& User) { m_sPass = ""; m_pUser = &User; - m_bSaslAuthenticating = m_bSasl; + m_bSASLAuthenticating = m_bSASL; // Set our proper timeout and set back our proper timeout mode // (constructor set a different timeout and mode) @@ -705,15 +706,16 @@ void CClient::HandleCap(const CMessage& Message) { CString sSubCmd = Message.GetParam(0); if (sSubCmd.Equals("LS")) { + int iCapVersion = Message.GetParam(1).ToInt(); SCString ssOfferCaps; for (const auto& it : m_mCoreCaps) { bool bServerDependent = std::get<0>(it.second); if (!bServerDependent || m_ssServerDependentCaps.count(it.first) > 0) { - if (it.first.Equals("sasl")) { + if (it.first.Equals("sasl") && iCapVersion >= 302) { SCString ssMechanisms; ssOfferCaps.insert(it.first + "=" + - EnumerateSaslMechanisms(ssMechanisms)); + EnumerateSASLMechanisms(ssMechanisms)); } else { ssOfferCaps.insert(it.first); } @@ -724,23 +726,21 @@ void CClient::HandleCap(const CMessage& Message) { CString(" ").Join(ssOfferCaps.begin(), ssOfferCaps.end()); RespondCap("LS :" + sRes); m_bInCap = true; - if (Message.GetParam(1).ToInt() >= 302) { + if (iCapVersion >= 302) { m_bCapNotify = true; } } else if (sSubCmd.Equals("END")) { m_bInCap = false; if (!IsAttached()) { - if (m_bSasl && !m_bSaslAuthenticated && m_bSaslAuthenticating) { + if (m_bSASL && m_sSASLUser.empty() && m_bSASLAuthenticating) { PutClient(":irc.znc.in 906 " + GetNick() + " :SASL authentication aborted"); - m_sSaslMechanism = ""; - m_bSaslAuthenticated = false; - m_bSaslMultipart = false; - m_bSaslAuthenticating = false; + m_sSASLMechanism = ""; + m_bSASLAuthenticating = false; } if (!m_pUser && m_bGotUser && - (!m_bSaslAuthenticated && !m_bGotPass)) { + (m_sSASLUser.empty() && !m_bGotPass)) { SendRequiredPasswordNotice(); } else { AuthUser(); @@ -998,139 +998,139 @@ bool CClient::OnActionMessage(CActionMessage& Message) { } void CClient::OnAuthenticateMessage(CAuthenticateMessage& Message) { - const auto uiMaxSaslMsgLength = 400u; + const auto uiMaxSASLMsgLength = 400u; auto bAuthenticationSuccess = false; auto sMessage = Message.GetText(); - const auto sBufferSize = sMessage.length(); - SCString ssMechanisms; + const auto iBufferSize = sMessage.length(); - auto SaslReset = [this]() { - m_sSaslMechanism = ""; - m_sSaslBuffer = ""; - m_bSaslMultipart = false; + auto SASLReset = [this]() { + m_sSASLMechanism = ""; + m_sSASLBuffer = ""; }; - auto SaslChallenge = [this](CString sChallenge) { + auto SASLChallenge = [this](CString sChallenge) { sChallenge.Base64Encode(); auto sChallengeSize = sChallenge.length(); - if (sChallengeSize > uiMaxSaslMsgLength) { - for (auto i = 0u; i < sChallengeSize; i += uiMaxSaslMsgLength) { - CString sMsgPart = sChallenge.substr(i, uiMaxSaslMsgLength); + if (sChallengeSize > uiMaxSASLMsgLength) { + for (int i = 0; i < sChallengeSize; i += uiMaxSASLMsgLength) { + CString sMsgPart = sChallenge.substr(i, uiMaxSASLMsgLength); PutClient("AUTHENTICATE " + sMsgPart); } - } else { + } else if (sChallengeSize > 0) { PutClient("AUTHENTICATE " + sChallenge); } + if (sChallengeSize % uiMaxSASLMsgLength == 0) { + PutClient("AUTHENTICATE +"); + } }; - if (!m_bSasl) return; + if (!m_bSASL) return; - if (m_bSaslAuthenticated || IsAttached()) { + if (!m_sSASLUser.empty() || IsAttached()) { PutClient(":irc.znc.in 907 " + GetNick() + " :You have already authenticated using SASL"); return; } - if (!m_bSaslAuthenticating || sMessage.Equals("*")) { + if (!m_bSASLAuthenticating || sMessage.Equals("*")) { PutClient(":irc.znc.in 906 " + GetNick() + " :SASL authentication aborted"); if (!IsAttached()) { - m_bSaslAuthenticating = false; - SaslReset(); + m_bSASLAuthenticating = false; + SASLReset(); } return; } - auto sMechanisms = EnumerateSaslMechanisms(ssMechanisms); - - if (sBufferSize > uiMaxSaslMsgLength) { + if (iBufferSize > uiMaxSASLMsgLength) { PutClient(":irc.znc.in 905 " + GetNick() + " :SASL message too long"); - SaslReset(); + SASLReset(); return; } - if (m_sSaslMechanism.empty()) { + if (m_sSASLMechanism.empty()) { + SCString ssMechanisms; + auto sMechanisms = EnumerateSASLMechanisms(ssMechanisms); + if (ssMechanisms.find(sMessage) == ssMechanisms.end()) { PutClient(":irc.znc.in 908 " + GetNick() + " " + sMechanisms + " :are available SASL mechanisms"); PutClient(":irc.znc.in 904 " + GetNick() + " :SASL authentication failed"); - SaslReset(); + SASLReset(); return; } - m_sSaslMechanism = sMessage; + m_sSASLMechanism = sMessage; auto bResult = false; CString sChallenge; - GLOBALMODULECALL(OnSaslServerChallenge(m_sSaslMechanism, sChallenge), + GLOBALMODULECALL(OnSASLServerChallenge(m_sSASLMechanism, sChallenge), &bResult); if (bResult) { - SaslChallenge(sChallenge); + SASLChallenge(sChallenge); } else { PutClient("AUTHENTICATE +"); } return; } - if (sBufferSize == uiMaxSaslMsgLength) { - m_bSaslMultipart = true; - m_sSaslBuffer.append(sMessage); + if (m_sSASLBuffer.length() + sMessage.length() > 10 * 1024) { + PutClient(":irc.znc.in 904 " + GetNick() + " :SASL response too long"); + SASLReset(); + return; + } + if (iBufferSize == uiMaxSASLMsgLength) { + m_sSASLBuffer.append(sMessage); return; } - if ((m_bSaslMultipart && !sMessage.Equals("+"))) { - m_sSaslBuffer.append(sMessage); - m_bSaslMultipart = false; - } else if (!m_bSaslMultipart && !sMessage.Equals("+")) { - m_sSaslBuffer.assign(sMessage); - } + if (sMessage != "+") { + m_sSASLBuffer += sMessage; + } - m_sSaslBuffer.Base64Decode(); - - auto sAuthcId = m_sUser; - auto sAuthzId = m_sUser; + m_sSASLBuffer.Base64Decode(); CString sResponse; bool bResult; - GLOBALMODULECALL(OnClientSaslAuthenticate( - m_sSaslMechanism, m_sSaslBuffer, sAuthcId, + CString sSASLUser; + GLOBALMODULECALL(OnClientSASLAuthenticate( + m_sSASLMechanism, m_sSASLBuffer, sSASLUser, sResponse, bAuthenticationSuccess), &bResult); + m_sSASLBuffer.clear(); if (bResult && !sResponse.empty()) { - SaslChallenge(sResponse); + SASLChallenge(sResponse); return; } - m_sSaslBuffer.clear(); - - auto pUser = CZNC::Get().FindUser(sAuthcId); + auto pUser = CZNC::Get().FindUser(sSASLUser); if (pUser && bAuthenticationSuccess) { PutClient(":irc.znc.in 900 " + GetNick() + " " + GetNick() + "!" + - pUser->GetIdent() + "@" + GetHostName() + " " + sAuthcId + - " :You are now logged in as " + sAuthzId); + pUser->GetIdent() + "@" + GetHostName() + " " + sSASLUser + + " :You are now logged in as " + sSASLUser); PutClient(":irc.znc.in 903 " + GetNick() + " :SASL authentication successful"); - m_bSaslAuthenticated = true; - m_bSaslAuthenticating = false; + m_sSASLUser = sSASLUser; + m_bSASLAuthenticating = false; } else { PutClient(":irc.znc.in 904 " + GetNick() + " :SASL authentication failed"); - SaslReset(); + SASLReset(); } return; } -CString CClient::EnumerateSaslMechanisms(SCString& ssMechanisms) { +CString CClient::EnumerateSASLMechanisms(SCString& ssMechanisms) { CString sMechanisms; - GLOBALMODULECALL(OnGetSaslMechanisms(ssMechanisms), NOTHING); + GLOBALMODULECALL(OnGetSASLMechanisms(ssMechanisms), NOTHING); if (ssMechanisms.size()) { sMechanisms = diff --git a/src/Modules.cpp b/src/Modules.cpp index 92a89451..a9ecd784 100644 --- a/src/Modules.cpp +++ b/src/Modules.cpp @@ -1076,7 +1076,7 @@ bool CModule::IsClientCapSupported(CClient* pClient, const CString& sCap, void CModule::OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState) {} -CModule::EModRet CModule::OnClientSaslAuthenticate(const CString& sMechanism, +CModule::EModRet CModule::OnClientSASLAuthenticate(const CString& sMechanism, const CString& sBuffer, CString& sUser, CString& sMechanismResponse, @@ -1084,12 +1084,12 @@ CModule::EModRet CModule::OnClientSaslAuthenticate(const CString& sMechanism, return CONTINUE; } -CModule::EModRet CModule::OnSaslServerChallenge(const CString& sMechanism, +CModule::EModRet CModule::OnSASLServerChallenge(const CString& sMechanism, CString& sResponse) { return CONTINUE; } -void CModule::OnGetSaslMechanisms(SCString& ssMechanisms) {} +void CModule::OnGetSASLMechanisms(SCString& ssMechanisms) {} CModule::EModRet CModule::OnModuleLoading(const CString& sModName, const CString& sArgs, @@ -1608,22 +1608,22 @@ bool CModules::OnClientCapRequest(CClient* pClient, const CString& sCap, return false; } -bool CModules::OnClientSaslAuthenticate(const CString& sMechanism, +bool CModules::OnClientSASLAuthenticate(const CString& sMechanism, const CString& sBuffer, CString& sUser, CString& sResponse, bool& bAuthenticationSuccess) { - MODHALTCHK(OnClientSaslAuthenticate(sMechanism, sBuffer, sUser, + MODHALTCHK(OnClientSASLAuthenticate(sMechanism, sBuffer, sUser, sResponse, bAuthenticationSuccess)); } -bool CModules::OnSaslServerChallenge(const CString& sMechanism, +bool CModules::OnSASLServerChallenge(const CString& sMechanism, CString& sResponse) { - MODHALTCHK(OnSaslServerChallenge(sMechanism, sResponse)); + MODHALTCHK(OnSASLServerChallenge(sMechanism, sResponse)); } -bool CModules::OnGetSaslMechanisms(SCString& ssMechanisms) { - MODUNLOADCHK(OnGetSaslMechanisms(ssMechanisms)); +bool CModules::OnGetSASLMechanisms(SCString& ssMechanisms) { + MODUNLOADCHK(OnGetSASLMechanisms(ssMechanisms)); return false; } diff --git a/test/integration/tests/modules.cpp b/test/integration/tests/modules.cpp index 6f2737ca..20afd3d1 100644 --- a/test/integration/tests/modules.cpp +++ b/test/integration/tests/modules.cpp @@ -330,5 +330,25 @@ TEST_F(ZNCTest, SaslMechsNotInit) { ircd.ReadUntil("PONG foo"); } +TEST_F(ZNCTest, SaslPlainModule) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = LoginClient(); + client.Write("znc loadmod saslplain"); + client.ReadUntil("Loaded module"); + client.Close(); + + auto client2 = ConnectClient(); + client2.Write("NICK foo"); + client2.Write("CAP LS"); + client2.Write("CAP REQ :sasl"); + client2.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); + client2.Write("USER bar"); + client2.Write("AUTHENTICATE PLAIN"); + client2.ReadUntil("AUTHENTICATE +"); + client2.Write("AUTHENTICATE AHVzZXIAaHVudGVyMg=="); // \0user\0hunter2 + client2.ReadUntil(":irc.znc.in 903 foo :SASL authentication successful"); +} + } // namespace } // namespace znc_inttest From c6ae8d16e5b2b6a3f447162cadf9841c57e6fdbd Mon Sep 17 00:00:00 2001 From: Alexey Sokolov Date: Thu, 13 Feb 2025 20:56:02 +0000 Subject: [PATCH 03/19] saslplain: use CZNC::AuthUser() 1. this should work better with modules such as imapauth 2. it fixes a null pointer dereference when the username wasn't found This module won't work as is yet, and has some other obvious issues with this approach, but was a good starting point. --- modules/saslplain.cpp | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/modules/saslplain.cpp b/modules/saslplain.cpp index 3f467397..ba60bc97 100644 --- a/modules/saslplain.cpp +++ b/modules/saslplain.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2004-2018 ZNC, see the NOTICE file for details. + * 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. @@ -41,15 +41,8 @@ class CSASLMechanismPlain : public CModule { return HALTMODS; } - auto pUser = CZNC::Get().FindUser(sAuthcId); - - if (!sAuthcId.empty() && !sPassword.empty()) { - if (pUser->CheckPass(sPassword)) { - bAuthenticationSuccess = true; - sUser = sAuthcId; - } - } - + auto spAuth = std::make_shared(this, sAuthcId, sPassword); + CZNC::Get().AuthUser(spAuth); return HALTMODS; } From 22f27b2e88de23f87b9a90d617d3c4737c5eac78 Mon Sep 17 00:00:00 2001 From: Alexey Sokolov Date: Thu, 13 Feb 2025 21:03:16 +0000 Subject: [PATCH 04/19] Fix indentation of SASL code. This should help understanding what it's trying to do. --- modules/saslplain.cpp | 6 ++-- src/Client.cpp | 79 ++++++++++++++++++++++--------------------- src/Message.cpp | 2 +- 3 files changed, 44 insertions(+), 43 deletions(-) diff --git a/modules/saslplain.cpp b/modules/saslplain.cpp index ba60bc97..264f7643 100644 --- a/modules/saslplain.cpp +++ b/modules/saslplain.cpp @@ -43,12 +43,12 @@ class CSASLMechanismPlain : public CModule { auto spAuth = std::make_shared(this, sAuthcId, sPassword); CZNC::Get().AuthUser(spAuth); - return HALTMODS; - } + return HALTMODS; + } void OnGetSASLMechanisms(SCString& ssMechanisms) override { ssMechanisms.insert("PLAIN"); - } + } }; template <> diff --git a/src/Client.cpp b/src/Client.cpp index 28b52c15..4206cd7f 100644 --- a/src/Client.cpp +++ b/src/Client.cpp @@ -829,7 +829,7 @@ void CClient::HandleCap(const CMessage& Message) { PutClient(":irc.znc.in 906 " + GetNick() + " :SASL authentication aborted"); m_sSASLMechanism = ""; - m_bSASLAuthenticating = false; + m_bSASLAuthenticating = false; } if (!m_pUser && m_bGotUser && @@ -1105,30 +1105,30 @@ void CClient::OnAuthenticateMessage(CAuthenticateMessage& Message) { }; if (!m_bSASL) return; - - if (!m_sSASLUser.empty() || IsAttached()) { + + if (!m_sSASLUser.empty() || IsAttached()) { PutClient(":irc.znc.in 907 " + GetNick() + " :You have already authenticated using SASL"); return; } - - if (!m_bSASLAuthenticating || sMessage.Equals("*")) { + + if (!m_bSASLAuthenticating || sMessage.Equals("*")) { PutClient(":irc.znc.in 906 " + GetNick() + " :SASL authentication aborted"); if (!IsAttached()) { - m_bSASLAuthenticating = false; + m_bSASLAuthenticating = false; SASLReset(); - } + } return; } - if (iBufferSize > uiMaxSASLMsgLength) { + if (iBufferSize > uiMaxSASLMsgLength) { PutClient(":irc.znc.in 905 " + GetNick() + " :SASL message too long"); SASLReset(); return; } - if (m_sSASLMechanism.empty()) { + if (m_sSASLMechanism.empty()) { SCString ssMechanisms; auto sMechanisms = EnumerateSASLMechanisms(ssMechanisms); @@ -1153,8 +1153,8 @@ void CClient::OnAuthenticateMessage(CAuthenticateMessage& Message) { } else { PutClient("AUTHENTICATE +"); } - return; - } + return; + } if (m_sSASLBuffer.length() + sMessage.length() > 10 * 1024) { PutClient(":irc.znc.in 904 " + GetNick() + " :SASL response too long"); @@ -1162,48 +1162,49 @@ void CClient::OnAuthenticateMessage(CAuthenticateMessage& Message) { return; } - if (iBufferSize == uiMaxSASLMsgLength) { - m_sSASLBuffer.append(sMessage); - return; - } + if (iBufferSize == uiMaxSASLMsgLength) { + m_sSASLBuffer.append(sMessage); + return; + } if (sMessage != "+") { m_sSASLBuffer += sMessage; } - m_sSASLBuffer.Base64Decode(); + m_sSASLBuffer.Base64Decode(); - CString sResponse; - bool bResult; + CString sResponse; + bool bResult; CString sSASLUser; - GLOBALMODULECALL(OnClientSASLAuthenticate( - m_sSASLMechanism, m_sSASLBuffer, sSASLUser, - sResponse, bAuthenticationSuccess), - &bResult); + GLOBALMODULECALL( + OnClientSASLAuthenticate(m_sSASLMechanism, m_sSASLBuffer, sSASLUser, + sResponse, bAuthenticationSuccess), + &bResult); m_sSASLBuffer.clear(); - if (bResult && !sResponse.empty()) { - SASLChallenge(sResponse); + if (bResult && !sResponse.empty()) { + SASLChallenge(sResponse); return; - } + } - auto pUser = CZNC::Get().FindUser(sSASLUser); + auto pUser = CZNC::Get().FindUser(sSASLUser); - if (pUser && bAuthenticationSuccess) { - PutClient(":irc.znc.in 900 " + GetNick() + " " + GetNick() + "!" + - pUser->GetIdent() + "@" + GetHostName() + " " + sSASLUser + - " :You are now logged in as " + sSASLUser); - PutClient(":irc.znc.in 903 " + GetNick() + - " :SASL authentication successful"); - m_sSASLUser = sSASLUser; - m_bSASLAuthenticating = false; - } else { - PutClient(":irc.znc.in 904 " + GetNick() + " :SASL authentication failed"); - SASLReset(); - } + if (pUser && bAuthenticationSuccess) { + PutClient(":irc.znc.in 900 " + GetNick() + " " + GetNick() + "!" + + pUser->GetIdent() + "@" + GetHostName() + " " + sSASLUser + + " :You are now logged in as " + sSASLUser); + PutClient(":irc.znc.in 903 " + GetNick() + + " :SASL authentication successful"); + m_sSASLUser = sSASLUser; + m_bSASLAuthenticating = false; + } else { + PutClient(":irc.znc.in 904 " + GetNick() + + " :SASL authentication failed"); + SASLReset(); + } - return; + return; } CString CClient::EnumerateSASLMechanisms(SCString& ssMechanisms) { diff --git a/src/Message.cpp b/src/Message.cpp index 1538caab..9455002c 100644 --- a/src/Message.cpp +++ b/src/Message.cpp @@ -234,7 +234,7 @@ void CMessage::Parse(const CString& sMessage) { if (m_bColon) { ++begin; m_vsParams.push_back(std::string(begin, end - begin)); - begin = end; + begin = end; } else { m_vsParams.push_back(std::string(next_word())); } From c8266aafda211af2bcd277b16f2c35f5d02b33e3 Mon Sep 17 00:00:00 2001 From: Alexey Sokolov Date: Thu, 13 Feb 2025 21:22:09 +0000 Subject: [PATCH 05/19] Rename new callback, fix build --- include/znc/Modules.h | 6 +++--- modules/modpython/functions.in | 2 +- modules/modpython/module.h | 2 +- modules/modpython/znc.py | 2 +- modules/saslplain.cpp | 4 ++-- src/Client.cpp | 2 +- src/Modules.cpp | 6 +++--- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/include/znc/Modules.h b/include/znc/Modules.h index 41d1c0b2..d773cda9 100644 --- a/include/znc/Modules.h +++ b/include/znc/Modules.h @@ -1364,10 +1364,10 @@ class CModule { virtual void OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState); /** Called when a client requests SASL authentication. Use ssMechanisms.insert("mechanism") - * for announcing sASL mechanisms which your module supports. + * for announcing SASL mechanisms which your module supports. * @param ssMechanisms The set of supported SASL mechanisms to append to. */ - virtual void OnGetSASLMechanisms(SCString& ssMechanisms); + virtual void OnClientGetSASLMechanisms(SCString& ssMechanisms); /** Called when a client has selected a SASL mechanism for SASL authentication. * If implementing a SASL authentication mechanism, set sResponse to specify an initial challenge * message to send to the client. Otherwise, an empty response will be sent. @@ -1694,7 +1694,7 @@ class CModules : public std::vector, private CCoreTranslationMixin { bool IsClientCapSupported(CClient* pClient, const CString& sCap, bool bState); bool OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState); - bool OnGetSASLMechanisms(SCString& ssMechanisms); + bool OnClientGetSASLMechanisms(SCString& ssMechanisms); bool OnSASLServerChallenge(const CString& sMechanism, CString& sResponse); bool OnClientSASLAuthenticate(const CString& sMechanism, diff --git a/modules/modpython/functions.in b/modules/modpython/functions.in index 8333769d..8615f4bf 100644 --- a/modules/modpython/functions.in +++ b/modules/modpython/functions.in @@ -112,7 +112,7 @@ EModRet OnUnknownUserRaw(CClient* pClient, CString& sLine) EModRet OnUnknownUserRawMessage(CMessage& Message) bool IsClientCapSupported(CClient* pClient, const CString& sCap, bool bState) void OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState) -void OnGetSASLMechanisms(SCString& ssMechanisms) +void OnClientGetSASLMechanisms(SCString& ssMechanisms) EModRet OnSASLServerChallenge(const CString& sMechanism, CString& sResponse) EModRet OnClientSASLAuthenticate(const CString& sMechanism, const CString& sBuffer, CString& sUser, CString& sMechanismResponse, bool& bAuthenticationSuccess) EModRet OnModuleLoading(const CString& sModName, const CString& sArgs, CModInfo::EModuleType eType, bool& bSuccess, CString& sRetMsg) diff --git a/modules/modpython/module.h b/modules/modpython/module.h index f2891e04..54244b53 100644 --- a/modules/modpython/module.h +++ b/modules/modpython/module.h @@ -194,7 +194,7 @@ class ZNC_EXPORT_LIB_EXPORT CPyModule : public CModule { bool bState) override; void OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState) override; - void OnGetSASLMechanisms(SCString& ssMechanisms) override; + void OnClientGetSASLMechanisms(SCString& ssMechanisms) override; EModRet OnSASLServerChallenge(const CString& sMechanism, CString& sResponse) override; EModRet OnClientSASLAuthenticate(const CString& sMechanism, diff --git a/modules/modpython/znc.py b/modules/modpython/znc.py index 69fe3d0d..ca8f3617 100644 --- a/modules/modpython/znc.py +++ b/modules/modpython/znc.py @@ -478,7 +478,7 @@ class Module: def OnClientCapRequest(self, pClient, sCap, bState): pass - def OnGetSASLMechanisms(self, ssMechanisms): + def OnClientGetSASLMechanisms(self, ssMechanisms): pass def OnSASLServerChallenge(self, sMechanism, sResponse): diff --git a/modules/saslplain.cpp b/modules/saslplain.cpp index 264f7643..732038f0 100644 --- a/modules/saslplain.cpp +++ b/modules/saslplain.cpp @@ -41,12 +41,12 @@ class CSASLMechanismPlain : public CModule { return HALTMODS; } - auto spAuth = std::make_shared(this, sAuthcId, sPassword); + auto spAuth = std::make_shared(GetClient(), sAuthcId, sPassword); CZNC::Get().AuthUser(spAuth); return HALTMODS; } - void OnGetSASLMechanisms(SCString& ssMechanisms) override { + void OnClientGetSASLMechanisms(SCString& ssMechanisms) override { ssMechanisms.insert("PLAIN"); } }; diff --git a/src/Client.cpp b/src/Client.cpp index 4206cd7f..f2c1fd5e 100644 --- a/src/Client.cpp +++ b/src/Client.cpp @@ -1210,7 +1210,7 @@ void CClient::OnAuthenticateMessage(CAuthenticateMessage& Message) { CString CClient::EnumerateSASLMechanisms(SCString& ssMechanisms) { CString sMechanisms; - GLOBALMODULECALL(OnGetSASLMechanisms(ssMechanisms), NOTHING); + GLOBALMODULECALL(OnClientGetSASLMechanisms(ssMechanisms), NOTHING); if (ssMechanisms.size()) { sMechanisms = diff --git a/src/Modules.cpp b/src/Modules.cpp index eef2d3ce..8a9b0f2f 100644 --- a/src/Modules.cpp +++ b/src/Modules.cpp @@ -1213,7 +1213,7 @@ CModule::EModRet CModule::OnSASLServerChallenge(const CString& sMechanism, return CONTINUE; } -void CModule::OnGetSASLMechanisms(SCString& ssMechanisms) {} +void CModule::OnClientGetSASLMechanisms(SCString& ssMechanisms) {} CModule::EModRet CModule::OnModuleLoading(const CString& sModName, const CString& sArgs, @@ -1774,8 +1774,8 @@ bool CModules::OnSASLServerChallenge(const CString& sMechanism, MODHALTCHK(OnSASLServerChallenge(sMechanism, sResponse)); } -bool CModules::OnGetSASLMechanisms(SCString& ssMechanisms) { - MODUNLOADCHK(OnGetSASLMechanisms(ssMechanisms)); +bool CModules::OnClientGetSASLMechanisms(SCString& ssMechanisms) { + MODUNLOADCHK(OnClientGetSASLMechanisms(ssMechanisms)); return false; } From 4ef64eb4d5f03fc8c781cae49fed603de1e946d4 Mon Sep 17 00:00:00 2001 From: Alexey Sokolov Date: Thu, 13 Feb 2025 21:36:07 +0000 Subject: [PATCH 06/19] rename next sasl module hook --- include/znc/Modules.h | 34 +++++++++++++++++++--------------- modules/modpython/functions.in | 2 +- modules/modpython/module.h | 7 +++---- modules/modpython/znc.py | 2 +- src/Client.cpp | 5 +++-- src/Modules.cpp | 8 ++++---- 6 files changed, 31 insertions(+), 27 deletions(-) diff --git a/include/znc/Modules.h b/include/znc/Modules.h index d773cda9..d5bb9e67 100644 --- a/include/znc/Modules.h +++ b/include/znc/Modules.h @@ -1363,9 +1363,9 @@ class CModule { */ virtual void OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState); - /** Called when a client requests SASL authentication. Use ssMechanisms.insert("mechanism") + /** Called when a client requests SASL authentication. Use ssMechanisms.insert("MECHANISM") * for announcing SASL mechanisms which your module supports. - * @param ssMechanisms The set of supported SASL mechanisms to append to. + * @param ssMechanisms The set of supported SASL mechanisms to append to. */ virtual void OnClientGetSASLMechanisms(SCString& ssMechanisms); /** Called when a client has selected a SASL mechanism for SASL authentication. @@ -1374,18 +1374,23 @@ class CModule { * @param sMechanism The SASL mechanism selected by the client. * @param sResponse The optional value of an initial SASL challenge message to send to the client. */ - virtual EModRet OnSASLServerChallenge(const CString& sMechanism, - CString& sResponse); + virtual EModRet OnClientSASLServerInitialChallenge( + const CString& sMechanism, CString& sResponse); /** Called when a client is sending us a SASL message after the mechanism was selected. - * If implementing a SASL authentication mechanism, check the passed credentials, - * then either request more data by sending a challenge in sMechanismResponse, - * reject authentication by setting bAuthenticationSuccess to false, - * or accept authentication by setting bAuthenticationSuccess to true and setting sUser to the authenticated user name. + * If implementing a SASL authentication mechanism, check the passed + * credentials, then either request more data by sending a challenge in + * sMechanismResponse, reject authentication by setting + * bAuthenticationSuccess to false, or accept authentication by setting + * bAuthenticationSuccess to true and setting sUser to the authenticated + * user name. * @param sMechanism The SASL mechanism selected by the client. * @param sBuffer The SASL opaque value/credentials sent by the client. - * @param sUser The optional name of the authenticated user to log in the user as, if authentication is accepted. - * @param sMechanismResponse The optional value of a SASL challenge message to reply to the client to ask for more data. - * @param bAuthenticationSuccess If sMechanismResponse is not set, whether to accept or reject the authentication request. + * @param sUser The optional name of the authenticated user to log in the + * user as, if authentication is accepted. + * @param sMechanismResponse The optional value of a SASL challenge message + * to reply to the client to ask for more data. + * @param bAuthenticationSuccess If sMechanismResponse is not set, whether + * to accept or reject the authentication request. */ virtual EModRet OnClientSASLAuthenticate(const CString& sMechanism, const CString& sBuffer, @@ -1695,11 +1700,10 @@ class CModules : public std::vector, private CCoreTranslationMixin { bool bState); bool OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState); bool OnClientGetSASLMechanisms(SCString& ssMechanisms); - bool OnSASLServerChallenge(const CString& sMechanism, - CString& sResponse); + bool OnClientSASLServerInitialChallenge(const CString& sMechanism, + CString& sResponse); bool OnClientSASLAuthenticate(const CString& sMechanism, - const CString& sBuffer, - CString& sUser, + const CString& sBuffer, CString& sUser, CString& sResponse, bool& bAuthenticationSuccess); diff --git a/modules/modpython/functions.in b/modules/modpython/functions.in index 8615f4bf..1001f708 100644 --- a/modules/modpython/functions.in +++ b/modules/modpython/functions.in @@ -113,7 +113,7 @@ EModRet OnUnknownUserRawMessage(CMessage& Message) bool IsClientCapSupported(CClient* pClient, const CString& sCap, bool bState) void OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState) void OnClientGetSASLMechanisms(SCString& ssMechanisms) -EModRet OnSASLServerChallenge(const CString& sMechanism, CString& sResponse) +EModRet OnClientSASLServerInitialChallenge(const CString& sMechanism, CString& sResponse) EModRet OnClientSASLAuthenticate(const CString& sMechanism, const CString& sBuffer, CString& sUser, CString& sMechanismResponse, bool& bAuthenticationSuccess) EModRet OnModuleLoading(const CString& sModName, const CString& sArgs, CModInfo::EModuleType eType, bool& bSuccess, CString& sRetMsg) EModRet OnModuleUnloading(CModule* pModule, bool& bSuccess, CString& sRetMsg) diff --git a/modules/modpython/module.h b/modules/modpython/module.h index 54244b53..64d5d728 100644 --- a/modules/modpython/module.h +++ b/modules/modpython/module.h @@ -195,11 +195,10 @@ class ZNC_EXPORT_LIB_EXPORT CPyModule : public CModule { void OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState) override; void OnClientGetSASLMechanisms(SCString& ssMechanisms) override; - EModRet OnSASLServerChallenge(const CString& sMechanism, - CString& sResponse) override; + EModRet OnClientSASLServerInitialChallenge(const CString& sMechanism, + CString& sResponse) override; EModRet OnClientSASLAuthenticate(const CString& sMechanism, - const CString& sBuffer, - CString& sUser, + const CString& sBuffer, CString& sUser, CString& sMechanismResponse, bool& bAuthenticationSuccess) override; virtual EModRet OnModuleLoading(const CString& sModName, diff --git a/modules/modpython/znc.py b/modules/modpython/znc.py index ca8f3617..ae7b0a43 100644 --- a/modules/modpython/znc.py +++ b/modules/modpython/znc.py @@ -481,7 +481,7 @@ class Module: def OnClientGetSASLMechanisms(self, ssMechanisms): pass - def OnSASLServerChallenge(self, sMechanism, sResponse): + def OnClientSASLServerInitialChallenge(self, sMechanism, sResponse): pass def OnClientSASLAuthenticate(self, sMechanism, sBuffer, sUser, sResponse, bAuthenticationSuccess): diff --git a/src/Client.cpp b/src/Client.cpp index f2c1fd5e..02644f3e 100644 --- a/src/Client.cpp +++ b/src/Client.cpp @@ -1146,8 +1146,9 @@ void CClient::OnAuthenticateMessage(CAuthenticateMessage& Message) { auto bResult = false; CString sChallenge; - GLOBALMODULECALL(OnSASLServerChallenge(m_sSASLMechanism, sChallenge), - &bResult); + GLOBALMODULECALL( + OnClientSASLServerInitialChallenge(m_sSASLMechanism, sChallenge), + &bResult); if (bResult) { SASLChallenge(sChallenge); } else { diff --git a/src/Modules.cpp b/src/Modules.cpp index 8a9b0f2f..294a8983 100644 --- a/src/Modules.cpp +++ b/src/Modules.cpp @@ -1208,8 +1208,8 @@ CModule::EModRet CModule::OnClientSASLAuthenticate( return CONTINUE; } -CModule::EModRet CModule::OnSASLServerChallenge(const CString& sMechanism, - CString& sResponse) { +CModule::EModRet CModule::OnClientSASLServerInitialChallenge( + const CString& sMechanism, CString& sResponse) { return CONTINUE; } @@ -1769,9 +1769,9 @@ bool CModules::OnClientSASLAuthenticate(const CString& sMechanism, sResponse, bAuthenticationSuccess)); } -bool CModules::OnSASLServerChallenge(const CString& sMechanism, +bool CModules::OnClientSASLServerInitialChallenge(const CString& sMechanism, CString& sResponse) { - MODHALTCHK(OnSASLServerChallenge(sMechanism, sResponse)); + MODHALTCHK(OnClientSASLServerInitialChallenge(sMechanism, sResponse)); } bool CModules::OnClientGetSASLMechanisms(SCString& ssMechanisms) { From 99a5a52fea724c19928bcd23008e62acdc019fd4 Mon Sep 17 00:00:00 2001 From: Alexey Sokolov Date: Sat, 15 Feb 2025 00:01:29 +0000 Subject: [PATCH 07/19] fail2ban: Reset the counter upon successful login This is for SASL: when multiple mechanisms available it's kinda expected that some of them can fail, e.g. if client attempted EXTERNAL without providing the cert first. --- modules/fail2ban.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/fail2ban.cpp b/modules/fail2ban.cpp index adb18729..fd3a78ba 100644 --- a/modules/fail2ban.cpp +++ b/modules/fail2ban.cpp @@ -216,6 +216,10 @@ class CFailToBanMod : public CModule { Add(sRemoteIP, 1); } + void OnClientLogin() override { + Remove(GetClient()->GetRemoteIP()); + } + EModRet OnLoginAttempt(std::shared_ptr Auth) override { // e.g. webadmin ends up here const CString& sRemoteIP = Auth->GetRemoteIP(); From 6e9980d67fec43d26ca21a3df590f0098690f721 Mon Sep 17 00:00:00 2001 From: Alexey Sokolov Date: Fri, 14 Feb 2025 20:54:19 +0000 Subject: [PATCH 08/19] Various SASL changes --- include/znc/Client.h | 44 +++- include/znc/Modules.h | 41 ++-- modules/modpython/functions.in | 2 +- modules/modpython/module.h | 4 +- modules/modpython/znc.py | 2 +- modules/saslplain.cpp | 25 +-- src/Client.cpp | 346 +++++++++++++++++------------ src/Modules.cpp | 19 +- src/znc.cpp | 3 + test/integration/tests/modules.cpp | 36 ++- 10 files changed, 304 insertions(+), 218 deletions(-) diff --git a/include/znc/Client.h b/include/znc/Client.h index 20cabdca..6d8208d0 100644 --- a/include/znc/Client.h +++ b/include/znc/Client.h @@ -41,6 +41,12 @@ class CAuthBase : private CCoreTranslationMixin { CZNCSock* pSock) : m_sUsername(sUsername), m_sPassword(sPassword), m_pSock(pSock) {} + // If a module tries to do std::make_shared, the vtable of the mutex inside + // shared_ptr will point to the code in the module, and will crash when the + // module is unloaded, e.g. shutdown. This function forces the creation of + // shared_ptr in the 'znc' binary instead of in the module. + static std::shared_ptr WrapPointer(CAuthBase*); + virtual ~CAuthBase() {} CAuthBase(const CAuthBase&) = delete; @@ -96,6 +102,17 @@ class CClientAuth : public CAuthBase { CClient* m_pClient; }; +// Workaround SWIG bug, TODO report it +#ifndef SWIG +/** Username+password auth, which reports success/failure to client via SASL. */ +class CClientSASLAuth : public CClientAuth { + public: + using CClientAuth::CClientAuth; + void AcceptedLogin(CUser& User) override; + void RefusedLogin(const CString& sReason) override; +}; +#endif + class CClient : public CIRCSocket { public: CClient(); @@ -250,6 +267,16 @@ class CClient : public CIRCSocket { CIRCSock* GetIRCSock(); CString GetFullName() const; + /** Sends AUTHENTIATE message to client. + * It encodes it to Base64 and splits to multiple IRC messages if necessary. + */ + void SendSASLChallenge(CString sMessage); + void RefuseSASLLogin(const CString& sReason); + void AcceptSASLLogin(CUser& User); + // Like CZNC::AuthUser() but also stores the pointer, and calls Invalidate() + // if the client is destroyed. + void StartPasswordCheck(std::shared_ptr spAuth); + private: void HandleCap(const CMessage& Message); void RespondCap(const CString& sResponse); @@ -266,14 +293,14 @@ class CClient : public CIRCSocket { unsigned int DetachChans(const std::set& sChans); bool OnActionMessage(CActionMessage& Message); - void OnAuthenticateMessage(CAuthenticateMessage& Message); + void OnAuthenticateMessage(const CAuthenticateMessage& Message); + void AbortSASL(const CString& sFullIRCLine); + bool IsDuringSASL() const { return !m_sSASLMechanism.empty(); } /** - * Fills all available SASL mechanisms in the passed set, and returns a comma-joined string of those mechanisms. - * @param ssMechanisms Set of supported mechanisms, filled by this method. - * @return A comma-joined string of supported mechanisms. + * Returns set of all available SASL mechanisms. */ - CString EnumerateSASLMechanisms(SCString& ssMechanisms); + SCString EnumerateSASLMechanisms() const; bool OnCTCPMessage(CCTCPMessage& Message); bool OnJoinMessage(CJoinMessage& Message); @@ -305,8 +332,7 @@ class CClient : public CIRCSocket { bool m_bBatch; bool m_bEchoMessage; bool m_bSelfMessage; - bool m_bSASL; - bool m_bSASLAuthenticating; + bool m_bSASLCap; bool m_bPlaybackActive; CUser* m_pUser; CIRCNetwork* m_pNetwork; @@ -316,11 +342,15 @@ class CClient : public CIRCSocket { CString m_sNetwork; CString m_sIdentifier; CString m_sSASLBuffer; + // Set while the exchange is in progress CString m_sSASLMechanism; + // Username who successfully logged in using SASL. This is not a CUser* + // because between the 903 and CAP END the user could have been deleted. CString m_sSASLUser; std::shared_ptr m_spAuth; SCString m_ssAcceptedCaps; SCString m_ssSupportedTags; + SCString m_ssPreviouslyFailedSASLMechanisms; // The capabilities supported by the ZNC core - capability names mapped to // change handler. Note: this lists caps which don't require support on IRC // server. diff --git a/include/znc/Modules.h b/include/znc/Modules.h index d5bb9e67..dcdb9e93 100644 --- a/include/znc/Modules.h +++ b/include/znc/Modules.h @@ -1363,40 +1363,39 @@ class CModule { */ virtual void OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState); + /** Called when a client requests SASL authentication. Use ssMechanisms.insert("MECHANISM") * for announcing SASL mechanisms which your module supports. * @param ssMechanisms The set of supported SASL mechanisms to append to. */ virtual void OnClientGetSASLMechanisms(SCString& ssMechanisms); /** Called when a client has selected a SASL mechanism for SASL authentication. - * If implementing a SASL authentication mechanism, set sResponse to specify an initial challenge - * message to send to the client. Otherwise, an empty response will be sent. + * If implementing a SASL authentication mechanism, set sResponse to + * specify an initial challenge message to send to the client. Otherwise, an + * empty response will be sent. To avoid sending any immediate response, + * return HALT; in that case the module should schedule calling + * GetClient()->SendSASLChallenge() with the initial response: in IRC SASL, + * server always responds first. * @param sMechanism The SASL mechanism selected by the client. - * @param sResponse The optional value of an initial SASL challenge message to send to the client. + * @param sResponse The optional value of an initial SASL challenge message + * to send to the client. */ virtual EModRet OnClientSASLServerInitialChallenge( const CString& sMechanism, CString& sResponse); /** Called when a client is sending us a SASL message after the mechanism was selected. * If implementing a SASL authentication mechanism, check the passed * credentials, then either request more data by sending a challenge in - * sMechanismResponse, reject authentication by setting - * bAuthenticationSuccess to false, or accept authentication by setting - * bAuthenticationSuccess to true and setting sUser to the authenticated - * user name. + * GetClient()->SendSASLChallenge(), or reject authentication by calling + * GetClient()->RefuseSASLLogin(), or accept it by calling + * GetClient()->AcceptSASLLogin(). * @param sMechanism The SASL mechanism selected by the client. - * @param sBuffer The SASL opaque value/credentials sent by the client. - * @param sUser The optional name of the authenticated user to log in the - * user as, if authentication is accepted. - * @param sMechanismResponse The optional value of a SASL challenge message - * to reply to the client to ask for more data. - * @param bAuthenticationSuccess If sMechanismResponse is not set, whether - * to accept or reject the authentication request. + * @param sMessage The SASL opaque value/credentials sent by the client, + * after debase64ing and concatenating if it was split. */ virtual EModRet OnClientSASLAuthenticate(const CString& sMechanism, - const CString& sBuffer, - CString& sUser, - CString& sMechanismResponse, - bool& bAuthenticationSuccess); + const CString& sMessage); + /** Called when a client sent '*' to abort SASL, or aborted it for another reason. */ + virtual void OnClientSASLAborted(); /** Called when a module is going to be loaded. * @param sModName name of the module. @@ -1699,13 +1698,13 @@ class CModules : public std::vector, private CCoreTranslationMixin { bool IsClientCapSupported(CClient* pClient, const CString& sCap, bool bState); bool OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState); + bool OnClientGetSASLMechanisms(SCString& ssMechanisms); + bool OnClientSASLAborted(); bool OnClientSASLServerInitialChallenge(const CString& sMechanism, CString& sResponse); bool OnClientSASLAuthenticate(const CString& sMechanism, - const CString& sBuffer, CString& sUser, - CString& sResponse, - bool& bAuthenticationSuccess); + const CString& sBuffer); bool OnModuleLoading(const CString& sModName, const CString& sArgs, CModInfo::EModuleType eType, bool& bSuccess, diff --git a/modules/modpython/functions.in b/modules/modpython/functions.in index 1001f708..b662507f 100644 --- a/modules/modpython/functions.in +++ b/modules/modpython/functions.in @@ -114,7 +114,7 @@ bool IsClientCapSupported(CClient* pClient, const CString& sCap, bool bState) void OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState) void OnClientGetSASLMechanisms(SCString& ssMechanisms) EModRet OnClientSASLServerInitialChallenge(const CString& sMechanism, CString& sResponse) -EModRet OnClientSASLAuthenticate(const CString& sMechanism, const CString& sBuffer, CString& sUser, CString& sMechanismResponse, bool& bAuthenticationSuccess) +EModRet OnClientSASLAuthenticate(const CString& sMechanism, const CString& sMessage) EModRet OnModuleLoading(const CString& sModName, const CString& sArgs, CModInfo::EModuleType eType, bool& bSuccess, CString& sRetMsg) EModRet OnModuleUnloading(CModule* pModule, bool& bSuccess, CString& sRetMsg) EModRet OnGetModInfo(CModInfo& ModInfo, const CString& sModule, bool& bSuccess, CString& sRetMsg) diff --git a/modules/modpython/module.h b/modules/modpython/module.h index 64d5d728..ed701246 100644 --- a/modules/modpython/module.h +++ b/modules/modpython/module.h @@ -198,9 +198,7 @@ class ZNC_EXPORT_LIB_EXPORT CPyModule : public CModule { EModRet OnClientSASLServerInitialChallenge(const CString& sMechanism, CString& sResponse) override; EModRet OnClientSASLAuthenticate(const CString& sMechanism, - const CString& sBuffer, CString& sUser, - CString& sMechanismResponse, - bool& bAuthenticationSuccess) override; + const CString& sMessage) override; virtual EModRet OnModuleLoading(const CString& sModName, const CString& sArgs, CModInfo::EModuleType eType, bool& bSuccess, diff --git a/modules/modpython/znc.py b/modules/modpython/znc.py index ae7b0a43..d2a3dc49 100644 --- a/modules/modpython/znc.py +++ b/modules/modpython/znc.py @@ -484,7 +484,7 @@ class Module: def OnClientSASLServerInitialChallenge(self, sMechanism, sResponse): pass - def OnClientSASLAuthenticate(self, sMechanism, sBuffer, sUser, sResponse, bAuthenticationSuccess): + def OnClientSASLAuthenticate(self, sMechanism, sMessage): pass def OnModuleLoading(self, sModName, sArgs, eType, bSuccess, sRetMsg): diff --git a/modules/saslplain.cpp b/modules/saslplain.cpp index 732038f0..bca40346 100644 --- a/modules/saslplain.cpp +++ b/modules/saslplain.cpp @@ -21,34 +21,31 @@ class CSASLMechanismPlain : public CModule { public: MODCONSTRUCTOR(CSASLMechanismPlain) { AddHelpCommand(); } + void OnClientGetSASLMechanisms(SCString& ssMechanisms) override { + ssMechanisms.insert("PLAIN"); + } + EModRet OnClientSASLAuthenticate(const CString& sMechanism, - const CString& sBuffer, CString& sUser, - CString& sMechanismResponse, - bool& bAuthenticationSuccess) override { + const CString& sMessage) override { if (!sMechanism.Equals("PLAIN")) { return CONTINUE; } - bAuthenticationSuccess = false; - CString sNullSeparator = std::string("\0", 1); - auto sAuthzId = sBuffer.Token(0, false, sNullSeparator, true); - auto sAuthcId = sBuffer.Token(1, false, sNullSeparator, true); - auto sPassword = sBuffer.Token(2, false, sNullSeparator, true); + CString sAuthzId = sMessage.Token(0, false, sNullSeparator, true); + CString sAuthcId = sMessage.Token(1, false, sNullSeparator, true); + CString sPassword = sMessage.Token(2, false, sNullSeparator, true); if (!sAuthzId.empty() && sAuthzId != sAuthcId) { // Reject custom SASL plain authorization identifiers + GetClient()->RefuseSASLLogin("No support for custom AuthzId"); return HALTMODS; } - auto spAuth = std::make_shared(GetClient(), sAuthcId, sPassword); - CZNC::Get().AuthUser(spAuth); + auto spAuth = CAuthBase::WrapPointer(new CClientSASLAuth(GetClient(), sAuthcId, sPassword)); + GetClient()->StartPasswordCheck(spAuth); return HALTMODS; } - - void OnClientGetSASLMechanisms(SCString& ssMechanisms) override { - ssMechanisms.insert("PLAIN"); - } }; template <> diff --git a/src/Client.cpp b/src/Client.cpp index 02644f3e..724630b0 100644 --- a/src/Client.cpp +++ b/src/Client.cpp @@ -94,8 +94,7 @@ CClient::CClient() m_bBatch(false), m_bEchoMessage(false), m_bSelfMessage(false), - m_bSASL(false), - m_bSASLAuthenticating(false), + m_bSASLCap(false), m_bPlaybackActive(false), m_pUser(nullptr), m_pNetwork(nullptr), @@ -366,14 +365,22 @@ void CClient::AuthUser() { (m_sSASLUser.empty() && !m_bGotPass) || IsAttached()) return; - if (m_bSASL && !m_sSASLUser.empty()) { - m_sUser = m_sSASLUser; - auto pUser = CZNC::Get().FindUser(m_sUser); - AcceptLogin(*pUser); - return; + if (m_sSASLUser.empty()) { + StartPasswordCheck( + std::make_shared(this, m_sUser, m_sPass)); + } else { + // Already logged in, but the user could have been deleted meanwhile. + CUser* pUser = CZNC::Get().FindUser(m_sSASLUser); + if (pUser) { + AcceptLogin(*pUser); + } else { + RefuseLogin("SASL login was valid, but user no longer exists"); + } } +} - m_spAuth = std::make_shared(this, m_sUser, m_sPass); +void CClient::StartPasswordCheck(std::shared_ptr spAuth) { + m_spAuth = spAuth; CZNC::Get().AuthUser(m_spAuth); } @@ -382,6 +389,12 @@ CClientAuth::CClientAuth(CClient* pClient, const CString& sUsername, const CString& sPassword) : CAuthBase(sUsername, sPassword, pClient), m_pClient(pClient) {} +void CClientSASLAuth::RefusedLogin(const CString& sReason) { + if (m_pClient) { + m_pClient->RefuseSASLLogin(sReason); + } +} + void CClientAuth::RefusedLogin(const CString& sReason) { if (m_pClient) { m_pClient->RefuseLogin(sReason); @@ -398,8 +411,12 @@ void CAuthBase::Invalidate() { m_pSock = nullptr; } void CAuthBase::AcceptLogin(CUser& User) { if (m_pSock) { AcceptedLogin(User); - Invalidate(); } + Invalidate(); +} + +std::shared_ptr CAuthBase::WrapPointer(CAuthBase* p) { + return std::shared_ptr(p); } void CAuthBase::RefuseLogin(const CString& sReason) { @@ -427,6 +444,12 @@ void CClient::RefuseLogin(const CString& sReason) { Close(Csock::CLT_AFTERWRITE); } +void CClientSASLAuth::AcceptedLogin(CUser& User) { + if (m_pClient) { + m_pClient->AcceptSASLLogin(User); + } +} + void CClientAuth::AcceptedLogin(CUser& User) { if (m_pClient) { m_pClient->AcceptLogin(User); @@ -436,7 +459,9 @@ void CClientAuth::AcceptedLogin(CUser& User) { void CClient::AcceptLogin(CUser& User) { m_sPass = ""; m_pUser = &User; - m_bSASLAuthenticating = m_bSASL; + m_sSASLMechanism = ""; + m_sSASLBuffer = ""; + m_sSASLUser = ""; // Set our proper timeout and set back our proper timeout mode // (constructor set a different timeout and mode) @@ -765,37 +790,58 @@ static VCString MultiLine(const SCString& ssCaps) { const std::map>& CClient::CoreCaps() { - static const std::map> mCoreCaps = []{ - std::map> mCoreCaps = { - {"multi-prefix", - [](CClient* pClient, bool bVal) { pClient->m_bNamesx = bVal; }}, - {"userhost-in-names", - [](CClient* pClient, bool bVal) { pClient->m_bUHNames = bVal; }}, - {"echo-message", - [](CClient* pClient, bool bVal) { pClient->m_bEchoMessage = bVal; }}, - {"server-time", - [](CClient* pClient, bool bVal) { - pClient->m_bServerTime = bVal; - pClient->SetTagSupport("time", bVal); - }}, - {"batch", [](CClient* pClient, bool bVal) { - pClient->m_bBatch = bVal; - pClient->SetTagSupport("batch", bVal); - }}, - {"cap-notify", - [](CClient* pClient, bool bVal) { pClient->m_bCapNotify = bVal; }}, - {"chghost", [](CClient* pClient, bool bVal) { pClient->m_bChgHost = bVal; }}, - }; + static const std::map> + mCoreCaps = [] { + std::map> + mCoreCaps = { + {"multi-prefix", + [](CClient* pClient, bool bVal) { + pClient->m_bNamesx = bVal; + }}, + {"userhost-in-names", + [](CClient* pClient, bool bVal) { + pClient->m_bUHNames = bVal; + }}, + {"echo-message", + [](CClient* pClient, bool bVal) { + pClient->m_bEchoMessage = bVal; + }}, + {"server-time", + [](CClient* pClient, bool bVal) { + pClient->m_bServerTime = bVal; + pClient->SetTagSupport("time", bVal); + }}, + {"batch", + [](CClient* pClient, bool bVal) { + pClient->m_bBatch = bVal; + pClient->SetTagSupport("batch", bVal); + }}, + {"cap-notify", + [](CClient* pClient, bool bVal) { + pClient->m_bCapNotify = bVal; + }}, + {"chghost", [](CClient* pClient, + bool bVal) { pClient->m_bChgHost = bVal; }}, + {"sasl", + [](CClient* pClient, bool bVal) { + if (pClient->IsDuringSASL() && !bVal) { + pClient->AbortSASL( + ":irc.znc.in 904 " + pClient->GetNick() + + " :SASL authentication aborted"); + } + pClient->m_bSASLCap = bVal; + }}, + }; - // For compatibility with older clients - mCoreCaps["znc.in/server-time-iso"] = mCoreCaps["server-time"]; - mCoreCaps["znc.in/batch"] = mCoreCaps["batch"]; - mCoreCaps["znc.in/self-message"] = [](CClient* pClient, bool bVal) { - pClient->m_bSelfMessage = bVal; - }; + // For compatibility with older clients + mCoreCaps["znc.in/server-time-iso"] = mCoreCaps["server-time"]; + mCoreCaps["znc.in/batch"] = mCoreCaps["batch"]; + mCoreCaps["znc.in/self-message"] = [](CClient* pClient, bool bVal) { + pClient->m_bSelfMessage = bVal; + }; - return mCoreCaps; - }(); + return mCoreCaps; + }(); return mCoreCaps; } @@ -806,8 +852,19 @@ void CClient::HandleCap(const CMessage& Message) { m_uCapVersion = std::max(m_uCapVersion, Message.GetParam(1).ToUShort()); SCString ssOfferCaps; for (const auto& it : CoreCaps()) { - // TODO sasl value enumerating mechanisms - ssOfferCaps.insert(it.first); + // TODO figure out a better API for this, including for modules + if (HasCap302() && it.first == "sasl") { + SCString ssMechanisms = EnumerateSASLMechanisms(); + if (ssMechanisms.empty()) { + // See the comment near 908. Here "sasl=" would also have wrong meaning. + ssMechanisms.insert("*"); + } + ssOfferCaps.insert(it.first + "=" + + CString(",").Join(ssMechanisms.begin(), + ssMechanisms.end())); + } else { + ssOfferCaps.insert(it.first); + } } NETWORKMODULECALL(OnClientCapLs(this, ssOfferCaps), GetUser(), GetNetwork(), this, NOTHING); VCString vsCaps = MultiLine(ssOfferCaps); @@ -825,15 +882,12 @@ void CClient::HandleCap(const CMessage& Message) { } else if (sSubCmd.Equals("END")) { m_bInCap = false; if (!IsAttached()) { - if (m_bSASL && m_sSASLUser.empty() && m_bSASLAuthenticating) { - PutClient(":irc.znc.in 906 " + GetNick() + + if (IsDuringSASL()) { + AbortSASL(":irc.znc.in 904 " + GetNick() + " :SASL authentication aborted"); - m_sSASLMechanism = ""; - m_bSASLAuthenticating = false; } - if (!m_pUser && m_bGotUser && - (m_sSASLUser.empty() && !m_bGotPass)) { + if (m_bGotUser && m_sSASLUser.empty() && !m_bGotPass) { SendRequiredPasswordNotice(); } else { AuthUser(); @@ -1076,35 +1130,25 @@ bool CClient::OnActionMessage(CActionMessage& Message) { return true; } -void CClient::OnAuthenticateMessage(CAuthenticateMessage& Message) { - const auto uiMaxSASLMsgLength = 400u; - auto bAuthenticationSuccess = false; - auto sMessage = Message.GetText(); - const auto iBufferSize = sMessage.length(); +void CClient::SendSASLChallenge(CString sMessage) { + constexpr size_t uMaxSASLMsgLength = 400u; + sMessage.Base64Encode(); + size_t uChallengeSize = sMessage.length(); - auto SASLReset = [this]() { - m_sSASLMechanism = ""; - m_sSASLBuffer = ""; - }; + for (int i = 0; i < uChallengeSize; i += uMaxSASLMsgLength) { + CString sMsgPart = sMessage.substr(i, uMaxSASLMsgLength); + PutClient("AUTHENTICATE " + sMsgPart); + } + if (uChallengeSize % uMaxSASLMsgLength == 0) { + PutClient("AUTHENTICATE +"); + } +} - auto SASLChallenge = [this](CString sChallenge) { - sChallenge.Base64Encode(); - auto sChallengeSize = sChallenge.length(); - - if (sChallengeSize > uiMaxSASLMsgLength) { - for (int i = 0; i < sChallengeSize; i += uiMaxSASLMsgLength) { - CString sMsgPart = sChallenge.substr(i, uiMaxSASLMsgLength); - PutClient("AUTHENTICATE " + sMsgPart); - } - } else if (sChallengeSize > 0) { - PutClient("AUTHENTICATE " + sChallenge); - } - if (sChallengeSize % uiMaxSASLMsgLength == 0) { - PutClient("AUTHENTICATE +"); - } - }; - - if (!m_bSASL) return; +void CClient::OnAuthenticateMessage(const CAuthenticateMessage& Message) { + if (!m_bSASLCap) { + PutClient(":irc.znc.in 904 " + GetNick() + " :SASL not enabled"); + return; + } if (!m_sSASLUser.empty() || IsAttached()) { PutClient(":irc.znc.in 907 " + GetNick() + @@ -1112,29 +1156,53 @@ void CClient::OnAuthenticateMessage(CAuthenticateMessage& Message) { return; } - if (!m_bSASLAuthenticating || sMessage.Equals("*")) { - PutClient(":irc.znc.in 906 " + GetNick() + + auto SASLReset = [this]() { + m_sSASLMechanism = ""; + m_sSASLBuffer = ""; + }; + CString sMessage = Message.GetText(); + + if (sMessage.Equals("*")) { + AbortSASL(":irc.znc.in 906 " + GetNick() + " :SASL authentication aborted"); - if (!IsAttached()) { - m_bSASLAuthenticating = false; + return; + } + + constexpr size_t uMaxSASLMsgLength = 400u; + if (sMessage.length() > uMaxSASLMsgLength) { + AbortSASL(":irc.znc.in 905 " + GetNick() + " :SASL message too long"); + return; + } + + if (!IsDuringSASL()) { + if (m_ssPreviouslyFailedSASLMechanisms.find(sMessage) != + m_ssPreviouslyFailedSASLMechanisms.end()) { + // This prevents the client from brute forcing multiple passwords + // on the same connection. + PutClient(":irc.znc.in 904 " + GetNick() + + " :SASL authentication failed"); SASLReset(); + return; } - return; - } - - if (iBufferSize > uiMaxSASLMsgLength) { - PutClient(":irc.znc.in 905 " + GetNick() + " :SASL message too long"); - SASLReset(); - return; - } - - if (m_sSASLMechanism.empty()) { - SCString ssMechanisms; - auto sMechanisms = EnumerateSASLMechanisms(ssMechanisms); - + SCString ssMechanisms = EnumerateSASLMechanisms(); if (ssMechanisms.find(sMessage) == ssMechanisms.end()) { - PutClient(":irc.znc.in 908 " + GetNick() + " " + sMechanisms + - " :are available SASL mechanisms"); + if (ssMechanisms.empty()) { + // If it happens that no mechanisms are available, an empty + // string will cause issues with IRC frames. Probably we should + // disable the whole 'sasl' cap, but that becomes complicated + // because need to track changes to the list of available caps + // (modules adding new mechanisms) and send cap-notify. This + // hack is simpler to do. And if a client decides to use + // actually use this fake '*' mechanism, they probably won't + // succeed anyway. + PutClient(":irc.znc.in 908 " + GetNick() + + " * :No SASL mechanisms are available"); + } else { + PutClient(":irc.znc.in 908 " + GetNick() + " " + + CString(",").Join(ssMechanisms.begin(), + ssMechanisms.end()) + + " :are available SASL mechanisms"); + } PutClient(":irc.znc.in 904 " + GetNick() + " :SASL authentication failed"); SASLReset(); @@ -1144,27 +1212,24 @@ void CClient::OnAuthenticateMessage(CAuthenticateMessage& Message) { m_sSASLMechanism = sMessage; - auto bResult = false; + bool bResult = false; CString sChallenge; - GLOBALMODULECALL( + _GLOBALMODULECALL( OnClientSASLServerInitialChallenge(m_sSASLMechanism, sChallenge), - &bResult); - if (bResult) { - SASLChallenge(sChallenge); - } else { - PutClient("AUTHENTICATE +"); + nullptr, nullptr, this, &bResult); + if (!bResult) { + SendSASLChallenge(std::move(sChallenge)); } return; } if (m_sSASLBuffer.length() + sMessage.length() > 10 * 1024) { - PutClient(":irc.znc.in 904 " + GetNick() + " :SASL response too long"); - SASLReset(); + AbortSASL(":irc.znc.in 904 " + GetNick() + " :SASL response too long"); return; } - if (iBufferSize == uiMaxSASLMsgLength) { - m_sSASLBuffer.append(sMessage); + if (sMessage.length() == uMaxSASLMsgLength) { + m_sSASLBuffer += sMessage; return; } @@ -1174,51 +1239,46 @@ void CClient::OnAuthenticateMessage(CAuthenticateMessage& Message) { m_sSASLBuffer.Base64Decode(); - CString sResponse; - bool bResult; + bool bResult = false; - CString sSASLUser; - GLOBALMODULECALL( - OnClientSASLAuthenticate(m_sSASLMechanism, m_sSASLBuffer, sSASLUser, - sResponse, bAuthenticationSuccess), - &bResult); + _GLOBALMODULECALL( + OnClientSASLAuthenticate(m_sSASLMechanism, m_sSASLBuffer), + nullptr, nullptr, this, &bResult); m_sSASLBuffer.clear(); - - if (bResult && !sResponse.empty()) { - SASLChallenge(sResponse); - return; - } - - auto pUser = CZNC::Get().FindUser(sSASLUser); - - if (pUser && bAuthenticationSuccess) { - PutClient(":irc.znc.in 900 " + GetNick() + " " + GetNick() + "!" + - pUser->GetIdent() + "@" + GetHostName() + " " + sSASLUser + - " :You are now logged in as " + sSASLUser); - PutClient(":irc.znc.in 903 " + GetNick() + - " :SASL authentication successful"); - m_sSASLUser = sSASLUser; - m_bSASLAuthenticating = false; - } else { - PutClient(":irc.znc.in 904 " + GetNick() + - " :SASL authentication failed"); - SASLReset(); - } - - return; } -CString CClient::EnumerateSASLMechanisms(SCString& ssMechanisms) { - CString sMechanisms; +void CClient::AbortSASL(const CString& sFullIRCLine) { + PutClient(sFullIRCLine); + _GLOBALMODULECALL(OnClientSASLAborted(), nullptr, nullptr, this, NOTHING); + m_sSASLMechanism = ""; + m_sSASLBuffer = ""; +} +void CClient::RefuseSASLLogin(const CString& sReason) { + PutClient(":irc.znc.in 904 " + GetNick() + " :" + sReason); + m_ssPreviouslyFailedSASLMechanisms.insert(m_sSASLMechanism); + m_sSASLMechanism = ""; + m_sSASLBuffer = ""; + _GLOBALMODULECALL(OnFailedLogin("", GetRemoteIP()), nullptr, nullptr, this, + NOTHING); +} + +void CClient::AcceptSASLLogin(CUser& User) { + PutClient(":irc.znc.in 900 " + GetNick() + " " + GetNick() + "!" + + User.GetIdent() + "@" + GetHostName() + " " + User.GetUsername() + + " :You are now logged in as " + User.GetUsername()); + PutClient(":irc.znc.in 903 " + GetNick() + + " :SASL authentication successful"); + m_sSASLMechanism = ""; + m_sSASLBuffer = ""; + m_sSASLUser = User.GetUsername(); +} + +SCString CClient::EnumerateSASLMechanisms() const { + SCString ssMechanisms; + // FIXME Currently GetClient()==nullptr due to const GLOBALMODULECALL(OnClientGetSASLMechanisms(ssMechanisms), NOTHING); - - if (ssMechanisms.size()) { - sMechanisms = - CString(",").Join(ssMechanisms.begin(), ssMechanisms.end()); - } - - return sMechanisms; + return ssMechanisms; } bool CClient::OnCTCPMessage(CCTCPMessage& Message) { diff --git a/src/Modules.cpp b/src/Modules.cpp index 294a8983..a5678099 100644 --- a/src/Modules.cpp +++ b/src/Modules.cpp @@ -1203,8 +1203,7 @@ void CModule::InternalServerDependentCapsOnClientCapRequest(CClient* pClient, } CModule::EModRet CModule::OnClientSASLAuthenticate( - const CString& sMechanism, const CString& sBuffer, CString& sUser, - CString& sMechanismResponse, bool& bAuthenticationSuccess) { + const CString& sMechanism, const CString& sBuffer) { return CONTINUE; } @@ -1215,6 +1214,8 @@ CModule::EModRet CModule::OnClientSASLServerInitialChallenge( void CModule::OnClientGetSASLMechanisms(SCString& ssMechanisms) {} +void CModule::OnClientSASLAborted() {} + CModule::EModRet CModule::OnModuleLoading(const CString& sModName, const CString& sArgs, CModInfo::EModuleType eType, @@ -1761,12 +1762,8 @@ bool CModules::OnClientCapRequest(CClient* pClient, const CString& sCap, } bool CModules::OnClientSASLAuthenticate(const CString& sMechanism, - const CString& sBuffer, - CString& sUser, - CString& sResponse, - bool& bAuthenticationSuccess) { - MODHALTCHK(OnClientSASLAuthenticate(sMechanism, sBuffer, sUser, - sResponse, bAuthenticationSuccess)); + const CString& sBuffer) { + MODHALTCHK(OnClientSASLAuthenticate(sMechanism, sBuffer)); } bool CModules::OnClientSASLServerInitialChallenge(const CString& sMechanism, @@ -1779,6 +1776,11 @@ bool CModules::OnClientGetSASLMechanisms(SCString& ssMechanisms) { return false; } +bool CModules::OnClientSASLAborted() { + MODUNLOADCHK(OnClientSASLAborted()); + return false; +} + bool CModules::OnModuleLoading(const CString& sModName, const CString& sArgs, CModInfo::EModuleType eType, bool& bSuccess, CString& sRetMsg) { @@ -2064,6 +2066,7 @@ void CModules::GetDefaultMods(set& ssMods, {"chansaver", CModInfo::UserModule}, {"controlpanel", CModInfo::UserModule}, {"corecaps", CModInfo::GlobalModule}, + {"saslplain", CModInfo::GlobalModule}, {"simple_away", CModInfo::NetworkModule}, {"webadmin", CModInfo::GlobalModule}}; diff --git a/src/znc.cpp b/src/znc.cpp index 40d3720a..5c77503d 100644 --- a/src/znc.cpp +++ b/src/znc.cpp @@ -1109,6 +1109,9 @@ bool CZNC::LoadGlobal(CConfig& config, CString& sError) { if (tSavedVersion < make_tuple(1, 9)) { vsList.push_back("corecaps"); } + if (tSavedVersion < make_tuple(1, 10)) { + vsList.push_back("saslplain"); + } for (const CString& sModLine : vsList) { CString sModName = sModLine.Token(0); diff --git a/test/integration/tests/modules.cpp b/test/integration/tests/modules.cpp index 8d6af6b8..2c7efd1b 100644 --- a/test/integration/tests/modules.cpp +++ b/test/integration/tests/modules.cpp @@ -330,26 +330,6 @@ TEST_F(ZNCTest, SaslMechsNotInit) { ircd.ReadUntil("PONG foo"); } -TEST_F(ZNCTest, SaslPlainModule) { - auto znc = Run(); - auto ircd = ConnectIRCd(); - auto client = LoginClient(); - client.Write("znc loadmod saslplain"); - client.ReadUntil("Loaded module"); - client.Close(); - - auto client2 = ConnectClient(); - client2.Write("NICK foo"); - client2.Write("CAP LS"); - client2.Write("CAP REQ :sasl"); - client2.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); - client2.Write("USER bar"); - client2.Write("AUTHENTICATE PLAIN"); - client2.ReadUntil("AUTHENTICATE +"); - client2.Write("AUTHENTICATE AHVzZXIAaHVudGVyMg=="); // \0user\0hunter2 - client2.ReadUntil(":irc.znc.in 903 foo :SASL authentication successful"); -} - TEST_F(ZNCTest, SaslRequire) { auto znc = Run(); auto ircd = ConnectIRCd(); @@ -366,5 +346,21 @@ TEST_F(ZNCTest, SaslRequire) { auto ircd2 = ConnectIRCd(); } +TEST_F(ZNCTest, SaslAuthPlain) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = ConnectClient(); + client.Write("NICK foo"); + client.Write("CAP LS"); + client.ReadUntil(" sasl "); + client.Write("CAP REQ :sasl"); + client.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); + client.Write("USER bar"); + client.Write("AUTHENTICATE PLAIN"); + client.ReadUntil("AUTHENTICATE +"); + client.Write("AUTHENTICATE AHVzZXIAaHVudGVyMg=="); // \0user\0hunter2 + client.ReadUntil(":irc.znc.in 903 foo :SASL authentication successful"); +} + } // namespace } // namespace znc_inttest From af2175390160b1558d844036b270d419b86471e9 Mon Sep 17 00:00:00 2001 From: Alexey Sokolov Date: Sat, 22 Feb 2025 16:03:46 +0000 Subject: [PATCH 09/19] Rename saslplain to saslplainauth for consistency with other auth modules --- modules/{saslplain.cpp => saslplainauth.cpp} | 0 src/Modules.cpp | 2 +- src/znc.cpp | 3 --- 3 files changed, 1 insertion(+), 4 deletions(-) rename modules/{saslplain.cpp => saslplainauth.cpp} (100%) diff --git a/modules/saslplain.cpp b/modules/saslplainauth.cpp similarity index 100% rename from modules/saslplain.cpp rename to modules/saslplainauth.cpp diff --git a/src/Modules.cpp b/src/Modules.cpp index a5678099..6cbaba4f 100644 --- a/src/Modules.cpp +++ b/src/Modules.cpp @@ -2066,7 +2066,7 @@ void CModules::GetDefaultMods(set& ssMods, {"chansaver", CModInfo::UserModule}, {"controlpanel", CModInfo::UserModule}, {"corecaps", CModInfo::GlobalModule}, - {"saslplain", CModInfo::GlobalModule}, + {"saslplainauth", CModInfo::GlobalModule}, {"simple_away", CModInfo::NetworkModule}, {"webadmin", CModInfo::GlobalModule}}; diff --git a/src/znc.cpp b/src/znc.cpp index 5c77503d..40d3720a 100644 --- a/src/znc.cpp +++ b/src/znc.cpp @@ -1109,9 +1109,6 @@ bool CZNC::LoadGlobal(CConfig& config, CString& sError) { if (tSavedVersion < make_tuple(1, 9)) { vsList.push_back("corecaps"); } - if (tSavedVersion < make_tuple(1, 10)) { - vsList.push_back("saslplain"); - } for (const CString& sModLine : vsList) { CString sModName = sModLine.Token(0); From 4f07558a7d8277d8c8e5f4c686ca65d5809b3e94 Mon Sep 17 00:00:00 2001 From: Alexey Sokolov Date: Sun, 23 Feb 2025 09:25:24 +0000 Subject: [PATCH 10/19] Remove a footgun by decreasing the SASL API surface for modules Even without the bad vtable of mutex inside shared_ptr, if a module creates a subclass of CAuthBase, but then unloads, we have the same problem. --- include/znc/Client.h | 24 ++++-------------------- modules/saslplainauth.cpp | 3 +-- src/Client.cpp | 20 ++++++++++++-------- 3 files changed, 17 insertions(+), 30 deletions(-) diff --git a/include/znc/Client.h b/include/znc/Client.h index 6d8208d0..c56961ab 100644 --- a/include/znc/Client.h +++ b/include/znc/Client.h @@ -41,12 +41,6 @@ class CAuthBase : private CCoreTranslationMixin { CZNCSock* pSock) : m_sUsername(sUsername), m_sPassword(sPassword), m_pSock(pSock) {} - // If a module tries to do std::make_shared, the vtable of the mutex inside - // shared_ptr will point to the code in the module, and will crash when the - // module is unloaded, e.g. shutdown. This function forces the creation of - // shared_ptr in the 'znc' binary instead of in the module. - static std::shared_ptr WrapPointer(CAuthBase*); - virtual ~CAuthBase() {} CAuthBase(const CAuthBase&) = delete; @@ -102,17 +96,6 @@ class CClientAuth : public CAuthBase { CClient* m_pClient; }; -// Workaround SWIG bug, TODO report it -#ifndef SWIG -/** Username+password auth, which reports success/failure to client via SASL. */ -class CClientSASLAuth : public CClientAuth { - public: - using CClientAuth::CClientAuth; - void AcceptedLogin(CUser& User) override; - void RefusedLogin(const CString& sReason) override; -}; -#endif - class CClient : public CIRCSocket { public: CClient(); @@ -273,9 +256,10 @@ class CClient : public CIRCSocket { void SendSASLChallenge(CString sMessage); void RefuseSASLLogin(const CString& sReason); void AcceptSASLLogin(CUser& User); - // Like CZNC::AuthUser() but also stores the pointer, and calls Invalidate() - // if the client is destroyed. - void StartPasswordCheck(std::shared_ptr spAuth); + /** Start potentially asynchronous process of checking the credentials. + * When finished, will send the success/failure SASL numerics to the + * client. This is mostly useful for SASL PLAIN. */ + void StartSASLPasswordCheck(const CString& sUser, const CString& sPassword); private: void HandleCap(const CMessage& Message); diff --git a/modules/saslplainauth.cpp b/modules/saslplainauth.cpp index bca40346..b0231d6c 100644 --- a/modules/saslplainauth.cpp +++ b/modules/saslplainauth.cpp @@ -42,8 +42,7 @@ class CSASLMechanismPlain : public CModule { return HALTMODS; } - auto spAuth = CAuthBase::WrapPointer(new CClientSASLAuth(GetClient(), sAuthcId, sPassword)); - GetClient()->StartPasswordCheck(spAuth); + GetClient()->StartSASLPasswordCheck(sAuthcId, sPassword); return HALTMODS; } }; diff --git a/src/Client.cpp b/src/Client.cpp index 724630b0..7796bd7c 100644 --- a/src/Client.cpp +++ b/src/Client.cpp @@ -366,8 +366,8 @@ void CClient::AuthUser() { return; if (m_sSASLUser.empty()) { - StartPasswordCheck( - std::make_shared(this, m_sUser, m_sPass)); + m_spAuth = std::make_shared(this, m_sUser, m_sPass); + CZNC::Get().AuthUser(m_spAuth); } else { // Already logged in, but the user could have been deleted meanwhile. CUser* pUser = CZNC::Get().FindUser(m_sSASLUser); @@ -379,8 +379,16 @@ void CClient::AuthUser() { } } -void CClient::StartPasswordCheck(std::shared_ptr spAuth) { - m_spAuth = spAuth; +/** Username+password auth, which reports success/failure to client via SASL. */ +class CClientSASLAuth : public CClientAuth { + public: + using CClientAuth::CClientAuth; + void AcceptedLogin(CUser& User) override; + void RefusedLogin(const CString& sReason) override; +}; + +void CClient::StartSASLPasswordCheck(const CString& sUser, const CString& sPassword) { + m_spAuth = std::make_shared(this, sUser, sPassword); CZNC::Get().AuthUser(m_spAuth); } @@ -415,10 +423,6 @@ void CAuthBase::AcceptLogin(CUser& User) { Invalidate(); } -std::shared_ptr CAuthBase::WrapPointer(CAuthBase* p) { - return std::shared_ptr(p); -} - void CAuthBase::RefuseLogin(const CString& sReason) { if (!m_pSock) return; From ed20d489b6d7b05e643deb36a4fb403e72a9a466 Mon Sep 17 00:00:00 2001 From: Alexey Sokolov Date: Sun, 23 Feb 2025 22:41:01 +0100 Subject: [PATCH 11/19] Apply suggestions from code review Co-authored-by: dgw --- modules/saslplainauth.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/saslplainauth.cpp b/modules/saslplainauth.cpp index b0231d6c..68bc062e 100644 --- a/modules/saslplainauth.cpp +++ b/modules/saslplainauth.cpp @@ -49,7 +49,7 @@ class CSASLMechanismPlain : public CModule { template <> void TModInfo(CModInfo& Info) { - Info.SetWikiPage("saslplain"); + Info.SetWikiPage("saslplainauth"); } GLOBALMODULEDEFS( From 8778a2bb5d9c392416839e28f3c3e354dbe0f8ab Mon Sep 17 00:00:00 2001 From: Alexey Sokolov Date: Mon, 24 Feb 2025 09:21:30 +0000 Subject: [PATCH 12/19] Parse network and client from authzid. Tests will be in future commit --- include/znc/Client.h | 18 +++++++++++++++--- include/znc/Modules.h | 3 +++ modules/saslplainauth.cpp | 8 +++----- src/Client.cpp | 14 +++++++++++--- 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/include/znc/Client.h b/include/znc/Client.h index c56961ab..2c77fa17 100644 --- a/include/znc/Client.h +++ b/include/znc/Client.h @@ -258,14 +258,25 @@ class CClient : public CIRCSocket { void AcceptSASLLogin(CUser& User); /** Start potentially asynchronous process of checking the credentials. * When finished, will send the success/failure SASL numerics to the - * client. This is mostly useful for SASL PLAIN. */ - void StartSASLPasswordCheck(const CString& sUser, const CString& sPassword); + * client. This is mostly useful for SASL PLAIN. + * sAuthorizationId is internally passed through ParseUser() to extract + * network and client id. + * Currently sUser should match the username from + * sAuthorizationId: either in full, or just the username part; but in a + * future version we may add an ability to actually login as a different + * user, but with your password. + */ + void StartSASLPasswordCheck(const CString& sUser, const CString& sPassword, + const CString& sAuthorizationId); + /** Gathers username, client id, network name, if present. Returns username + * cleaned from client id and network name. + */ + CString ParseUser(const CString& sAuthLine); private: void HandleCap(const CMessage& Message); void RespondCap(const CString& sResponse); void ParsePass(const CString& sAuthLine); - void ParseUser(const CString& sAuthLine); void ParseIdentifier(const CString& sAuthLine); template @@ -322,6 +333,7 @@ class CClient : public CIRCSocket { CIRCNetwork* m_pNetwork; CString m_sNick; CString m_sPass; + // User who didn't necessarily login yet, or might not even exist. CString m_sUser; CString m_sNetwork; CString m_sIdentifier; diff --git a/include/znc/Modules.h b/include/znc/Modules.h index dcdb9e93..7bdfbcba 100644 --- a/include/znc/Modules.h +++ b/include/znc/Modules.h @@ -1388,6 +1388,9 @@ class CModule { * GetClient()->SendSASLChallenge(), or reject authentication by calling * GetClient()->RefuseSASLLogin(), or accept it by calling * GetClient()->AcceptSASLLogin(). + * At some point before accepting the login, you should call + * GetClient()->ParseUser(authz-id) to let it know the network name to + * attach to and the client id. * @param sMechanism The SASL mechanism selected by the client. * @param sMessage The SASL opaque value/credentials sent by the client, * after debase64ing and concatenating if it was split. diff --git a/modules/saslplainauth.cpp b/modules/saslplainauth.cpp index 68bc062e..4cf7a7e4 100644 --- a/modules/saslplainauth.cpp +++ b/modules/saslplainauth.cpp @@ -36,13 +36,11 @@ class CSASLMechanismPlain : public CModule { CString sAuthcId = sMessage.Token(1, false, sNullSeparator, true); CString sPassword = sMessage.Token(2, false, sNullSeparator, true); - if (!sAuthzId.empty() && sAuthzId != sAuthcId) { - // Reject custom SASL plain authorization identifiers - GetClient()->RefuseSASLLogin("No support for custom AuthzId"); - return HALTMODS; + if (sAuthzId.empty()) { + sAuthzId = sAuthcId; } - GetClient()->StartSASLPasswordCheck(sAuthcId, sPassword); + GetClient()->StartSASLPasswordCheck(sAuthcId, sPassword, sAuthzId); return HALTMODS; } }; diff --git a/src/Client.cpp b/src/Client.cpp index 7796bd7c..c21925c4 100644 --- a/src/Client.cpp +++ b/src/Client.cpp @@ -387,8 +387,14 @@ class CClientSASLAuth : public CClientAuth { void RefusedLogin(const CString& sReason) override; }; -void CClient::StartSASLPasswordCheck(const CString& sUser, const CString& sPassword) { - m_spAuth = std::make_shared(this, sUser, sPassword); +void CClient::StartSASLPasswordCheck(const CString& sUser, + const CString& sPassword, const CString& sAuthorizationId) { + ParseUser(sAuthorizationId); + if (sUser != m_sUser && sUser != sAuthorizationId) { + RefuseSASLLogin("No support for custom AuthzId"); + } + + m_spAuth = std::make_shared(this, m_sUser, sPassword); CZNC::Get().AuthUser(m_spAuth); } @@ -973,7 +979,7 @@ void CClient::ParsePass(const CString& sAuthLine) { } } -void CClient::ParseUser(const CString& sAuthLine) { +CString CClient::ParseUser(const CString& sAuthLine) { // user[@identifier][/network] const size_t uSlash = sAuthLine.rfind("/"); @@ -984,6 +990,8 @@ void CClient::ParseUser(const CString& sAuthLine) { } else { ParseIdentifier(sAuthLine); } + + return m_sUser; } void CClient::ParseIdentifier(const CString& sAuthLine) { From 37457105f33385133425cce03585ef993490d948 Mon Sep 17 00:00:00 2001 From: Alexey Sokolov Date: Mon, 24 Feb 2025 20:01:34 +0000 Subject: [PATCH 13/19] Support SASL EXTERNAL in certauth --- modules/certauth.cpp | 87 +++++++++++++++++++++++++++--- modules/saslplainauth.cpp | 4 +- test/integration/tests/modules.cpp | 80 +++++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 10 deletions(-) diff --git a/modules/certauth.cpp b/modules/certauth.cpp index 1875ad4b..2a6dbecc 100644 --- a/modules/certauth.cpp +++ b/modules/certauth.cpp @@ -53,15 +53,18 @@ class CSSLClientCertMod : public CModule { for (MCString::const_iterator it = BeginNV(); it != EndNV(); ++it) { VCString vsKeys; + const CString& sUser = it->first; - if (CZNC::Get().FindUser(it->first) == nullptr) { - DEBUG("Unknown user in saved data [" + it->first + "]"); + if (CZNC::Get().FindUser(sUser) == nullptr) { + DEBUG("Unknown user in saved data [" + sUser + "]"); continue; } it->second.Split(" ", vsKeys, false); - for (const CString& sKey : vsKeys) { - m_PubKeys[it->first].insert(sKey.AsLower()); + for (CString& sKey : vsKeys) { + sKey.MakeLower(); + m_PubKeys[sUser].insert(sKey); + m_KeyToUser[sKey].insert(sUser); } } @@ -90,12 +93,14 @@ class CSSLClientCertMod : public CModule { return SaveRegistry(); } - bool AddKey(CUser* pUser, const CString& sKey) { + bool AddKey(CUser* pUser, CString sKey) { + sKey.MakeLower(); const pair pair = - m_PubKeys[pUser->GetUsername()].insert(sKey.AsLower()); + m_PubKeys[pUser->GetUsername()].insert(sKey); if (pair.second) { Save(); + m_KeyToUser[sKey].insert(pUser->GetUsername()); } return pair.second; @@ -118,7 +123,7 @@ class CSSLClientCertMod : public CModule { MSCString::const_iterator it = m_PubKeys.find(sUser); if (it == m_PubKeys.end()) { - DEBUG("No saved pubkeys for this client"); + DEBUG("No saved pubkeys for this user"); return CONTINUE; } @@ -135,6 +140,56 @@ class CSSLClientCertMod : public CModule { return HALT; } + void OnClientGetSASLMechanisms(SCString& ssMechanisms) override { + ssMechanisms.insert("EXTERNAL"); + } + + EModRet OnClientSASLAuthenticate(const CString& sMechanism, + const CString& sMessage) override { + if (sMechanism != "EXTERNAL") { + return CONTINUE; + } + CString sUser = GetClient()->ParseUser(sMessage); + const CString sKey = GetKey(GetClient()); + DEBUG("Key: " << sKey); + + if (sKey.empty()) { + GetClient()->RefuseSASLLogin("No client cert presented"); + return HALT; + } + + auto it = m_KeyToUser.find(sKey); + if (it == m_KeyToUser.end()) { + GetClient()->RefuseSASLLogin("Client cert not recognized"); + return HALT; + } + + const SCString& ssUsers = it->second; + + if (ssUsers.empty()) { + GetClient()->RefuseSASLLogin("Key found, but list of users is empty, please report bug"); + return HALT; + } + + if (sUser.empty()) { + sUser = *ssUsers.begin(); + } else if (ssUsers.count(sUser) == 0) { + GetClient()->RefuseSASLLogin( + "The specified user doesn't have this key"); + return HALT; + } + + CUser* pUser = CZNC::Get().FindUser(sUser); + if (!pUser) { + GetClient()->RefuseSASLLogin("User not found"); + return HALT; + } + + DEBUG("Accepted cert auth for " << sUser); + GetClient()->AcceptSASLLogin(*pUser); + return HALT; + } + void HandleShowCommand(const CString& sLine) { const CString sPubKey = GetKey(GetClient()); @@ -211,8 +266,16 @@ class CSSLClientCertMod : public CModule { id--; } + CString sKey = *it2; it->second.erase(it2); if (it->second.size() == 0) m_PubKeys.erase(it); + + it = m_KeyToUser.find(sKey); + if (it != m_KeyToUser.end()) { + it->second.erase(GetUser()->GetUsername()); + if (it->second.empty()) m_KeyToUser.erase(it); + } + PutModule(t_s("Removed")); Save(); @@ -259,11 +322,18 @@ class CSSLClientCertMod : public CModule { } else if (sPageName == "delete") { MSCString::iterator it = m_PubKeys.find(pUser->GetUsername()); if (it != m_PubKeys.end()) { - if (it->second.erase(WebSock.GetParam("key", false))) { + CString sKey = WebSock.GetParam("key", false); + if (it->second.erase(sKey)) { if (it->second.size() == 0) { m_PubKeys.erase(it); } + it = m_KeyToUser.find(sKey); + if (it != m_KeyToUser.end()) { + it->second.erase(pUser->GetUsername()); + if (it->second.empty()) m_KeyToUser.erase(it); + } + Save(); } } @@ -279,6 +349,7 @@ class CSSLClientCertMod : public CModule { // Maps user names to a list of allowed pubkeys typedef map> MSCString; MSCString m_PubKeys; + MSCString m_KeyToUser; }; template <> diff --git a/modules/saslplainauth.cpp b/modules/saslplainauth.cpp index 4cf7a7e4..bc23cd4d 100644 --- a/modules/saslplainauth.cpp +++ b/modules/saslplainauth.cpp @@ -19,7 +19,7 @@ class CSASLMechanismPlain : public CModule { public: - MODCONSTRUCTOR(CSASLMechanismPlain) { AddHelpCommand(); } + MODCONSTRUCTOR(CSASLMechanismPlain) {} void OnClientGetSASLMechanisms(SCString& ssMechanisms) override { ssMechanisms.insert("PLAIN"); @@ -27,7 +27,7 @@ class CSASLMechanismPlain : public CModule { EModRet OnClientSASLAuthenticate(const CString& sMechanism, const CString& sMessage) override { - if (!sMechanism.Equals("PLAIN")) { + if (sMechanism != "PLAIN") { return CONTINUE; } diff --git a/test/integration/tests/modules.cpp b/test/integration/tests/modules.cpp index 2c7efd1b..6863e6c5 100644 --- a/test/integration/tests/modules.cpp +++ b/test/integration/tests/modules.cpp @@ -19,6 +19,8 @@ #include "znctest.h" +#include + using testing::HasSubstr; using testing::Not; @@ -362,5 +364,83 @@ TEST_F(ZNCTest, SaslAuthPlain) { client.ReadUntil(":irc.znc.in 903 foo :SASL authentication successful"); } +TEST_F(ZNCTest, SaslAuthExternal) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + ircd.Write(":server 001 nick :Hello"); + auto client = LoginClient(); + client.Write("znc addport +12346 all all"); + client.ReadUntil(":Port added"); + client.Write("znc loadmod certauth"); + client.ReadUntil("Loaded"); + client.Close(); + + QSslSocket sock; + // Could generate a new one for the test, but this one is good enough + sock.setLocalCertificate(m_dir.path() + "/znc.pem"); + sock.setPrivateKey(m_dir.path() + "/znc.pem"); + sock.setPeerVerifyMode(QSslSocket::VerifyNone); + sock.connectToHostEncrypted("127.0.0.1", 12346); + ASSERT_TRUE(sock.waitForConnected()) << sock.errorString().toStdString(); + ASSERT_TRUE(sock.waitForEncrypted()) << sock.errorString().toStdString(); + auto client2 = WrapIO(&sock); + client2.Write("PASS :hunter2"); + client2.Write("NICK nick"); + client2.Write("USER user/test x x :x"); + client2.Write("privmsg *certauth add"); + client2.ReadUntil("added"); + + auto Reconnect = [&] { + client2.Close(); + ASSERT_TRUE(sock.state() == QAbstractSocket::UnconnectedState || sock.waitForDisconnected()) + << sock.errorString().toStdString(); + sock.connectToHostEncrypted("127.0.0.1", 12346); + ASSERT_TRUE(sock.waitForConnected()) + << sock.errorString().toStdString(); + ASSERT_TRUE(sock.waitForEncrypted()) + << sock.errorString().toStdString(); + client2.Write("CAP REQ sasl"); + client2.Write("NICK nick"); + client2.Write("USER u x x :x"); + client2.ReadUntil("ACK :sasl"); + client2.Write("AUTHENTICATE EXTERNAL"); + client2.ReadUntil("AUTHENTICATE +"); + }; + + Reconnect(); + ircd.Write(":friend PRIVMSG nick :hello"); + client2.Write("AUTHENTICATE +"); + client2.ReadUntil( + ":irc.znc.in 900 nick nick!user@127.0.0.1 user :You are now logged in " + "as user"); + client2.ReadUntil(":irc.znc.in 903 nick :SASL authentication successful"); + client2.Write("CAP END"); + // '[' comes from lack of server-time + client2.ReadUntil(":friend PRIVMSG nick :["); + + Reconnect(); + client2.Write("AUTHENTICATE " + QString("user/te").toUtf8().toBase64()); + client2.ReadUntil( + ":irc.znc.in 900 nick nick!user@127.0.0.1 user :You are now logged in " + "as user"); + client2.ReadUntil(":irc.znc.in 903 nick :SASL authentication successful"); + client2.Write("CAP END"); + client2.ReadUntil( + ":*status!status@znc.in PRIVMSG nick :Network te doesn't exist."); + + Reconnect(); + client2.Write("AUTHENTICATE " + QString("moo").toUtf8().toBase64()); + client2.ReadUntil( + ":irc.znc.in 904 nick :The specified user doesn't have this key"); + + client = LoginClient(); + client.Write("privmsg *certauth :del 1"); + client.ReadUntil("Removed"); + Reconnect(); + client2.Write("AUTHENTICATE +"); + client2.ReadUntil( + ":irc.znc.in 904 nick :Client cert not recognized"); +} + } // namespace } // namespace znc_inttest From 9f8015b8aded38bca13f246b7b7c7806cf3f31ea Mon Sep 17 00:00:00 2001 From: Alexey Sokolov Date: Fri, 28 Feb 2025 18:10:12 +0100 Subject: [PATCH 14/19] Add several more tests to sasl --- test/integration/tests/modules.cpp | 152 ++++++++++++++++++++++++++++- 1 file changed, 148 insertions(+), 4 deletions(-) diff --git a/test/integration/tests/modules.cpp b/test/integration/tests/modules.cpp index 6863e6c5..f7c88a7d 100644 --- a/test/integration/tests/modules.cpp +++ b/test/integration/tests/modules.cpp @@ -348,7 +348,7 @@ TEST_F(ZNCTest, SaslRequire) { auto ircd2 = ConnectIRCd(); } -TEST_F(ZNCTest, SaslAuthPlain) { +TEST_F(ZNCTest, SaslAuthPlainSimple) { auto znc = Run(); auto ircd = ConnectIRCd(); auto client = ConnectClient(); @@ -360,10 +360,142 @@ TEST_F(ZNCTest, SaslAuthPlain) { client.Write("USER bar"); client.Write("AUTHENTICATE PLAIN"); client.ReadUntil("AUTHENTICATE +"); - client.Write("AUTHENTICATE AHVzZXIAaHVudGVyMg=="); // \0user\0hunter2 + client.Write("AUTHENTICATE " + QByteArrayLiteral("\0user\0hunter2").toBase64()); client.ReadUntil(":irc.znc.in 903 foo :SASL authentication successful"); } +TEST_F(ZNCTest, SaslAuthPlainCopyInZ) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = ConnectClient(); + client.Write("NICK foo"); + client.Write("CAP LS"); + client.ReadUntil(" sasl "); + client.Write("CAP REQ :sasl"); + client.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); + client.Write("USER bar"); + client.Write("AUTHENTICATE PLAIN"); + client.ReadUntil("AUTHENTICATE +"); + client.Write("AUTHENTICATE " + QByteArrayLiteral("user@phone\0user@phone\0hunter2").toBase64()); + client.ReadUntil(":irc.znc.in 903 foo :SASL authentication successful"); + client.Write("CAP END"); + client.Write("znc listclients"); + client.ReadUntil("phone"); +} + +TEST_F(ZNCTest, SaslAuthPlainPartialInZ) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = ConnectClient(); + client.Write("NICK foo"); + client.Write("CAP LS"); + client.ReadUntil(" sasl "); + client.Write("CAP REQ :sasl"); + client.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); + client.Write("USER bar"); + client.Write("AUTHENTICATE PLAIN"); + client.ReadUntil("AUTHENTICATE +"); + client.Write("AUTHENTICATE " + QByteArrayLiteral("user@phone\0user\0hunter2").toBase64()); + client.ReadUntil(":irc.znc.in 903 foo :SASL authentication successful"); + client.Write("CAP END"); + client.Write("znc listclients"); + client.ReadUntil("phone"); +} + +TEST_F(ZNCTest, SaslAuthPlainDifferentZ) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = ConnectClient(); + client.Write("NICK foo"); + client.Write("CAP LS"); + client.ReadUntil(" sasl "); + client.Write("CAP REQ :sasl"); + client.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); + client.Write("USER bar"); + client.Write("AUTHENTICATE PLAIN"); + client.ReadUntil("AUTHENTICATE +"); + client.Write("AUTHENTICATE " + QByteArrayLiteral("user@phone\0user@tablet\0hunter2").toBase64()); + client.ReadUntil(":irc.znc.in 904 foo :No support for custom AuthzId"); +} + +TEST_F(ZNCTest, SaslAuthPlainWrongPassword) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = ConnectClient(); + client.Write("NICK foo"); + client.Write("CAP LS"); + client.ReadUntil(" sasl "); + client.Write("CAP REQ :sasl"); + client.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); + client.Write("USER bar"); + client.Write("AUTHENTICATE PLAIN"); + client.ReadUntil("AUTHENTICATE +"); + client.Write("AUTHENTICATE " + QByteArrayLiteral("\0user\0hunter3").toBase64()); + client.ReadUntil(":irc.znc.in 904 foo :Invalid Password"); + + // Try again on the same connection + client.Write("AUTHENTICATE PLAIN"); + client.ReadUntil(":irc.znc.in 904 foo :SASL authentication failed"); +} + +TEST_F(ZNCTest, SaslAuthPlainWrongUser) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = ConnectClient(); + client.Write("NICK foo"); + client.Write("CAP LS"); + client.ReadUntil(" sasl "); + client.Write("CAP REQ :sasl"); + client.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); + client.Write("USER bar"); + client.Write("AUTHENTICATE PLAIN"); + client.ReadUntil("AUTHENTICATE +"); + client.Write("AUTHENTICATE " + QByteArrayLiteral("\0anotheruser\0hunter2").toBase64()); + client.ReadUntil(":irc.znc.in 904 foo :Invalid Password"); +} + +TEST_F(ZNCTest, SaslAuthPlainImapAuth) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + QTcpServer imap; + ASSERT_TRUE(imap.listen(QHostAddress::LocalHost, 12346)) << imap.errorString().toStdString(); + auto client = LoginClient(); + client.Write("znc loadmod imapauth 127.0.0.1 12346 %@mail.test.com"); + client.ReadUntil("Loaded"); + + auto client2 = ConnectClient(); + client2.Write("NICK foo"); + client2.Write("CAP REQ :sasl"); + client2.Write("USER bar"); + client2.Write("AUTHENTICATE PLAIN"); + client2.Write("AUTHENTICATE " + QByteArrayLiteral("\0user@phone/net\0hunter3").toBase64()); + client2.ReadUntil("ACK :sasl"); + + ASSERT_TRUE(imap.waitForNewConnection(30000 /* msec */)); + auto imapsock = WrapIO(imap.nextPendingConnection()); + imapsock.Write("* OK IMAP4rev1 Service Ready"); + imapsock.ReadUntil("AUTH LOGIN user@mail.test.com hunter3"); + imapsock.Write("AUTH OK"); + + client2.ReadUntil(":irc.znc.in 903 foo :SASL authentication successful"); +} + +TEST_F(ZNCTest, SaslAuthAbort) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = ConnectClient(); + client.Write("NICK foo"); + client.Write("CAP LS"); + client.ReadUntil(" sasl "); + client.Write("CAP REQ :sasl"); + client.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); + client.Write("USER bar"); + client.Write("AUTHENTICATE PLAIN"); + client.ReadUntil("AUTHENTICATE +"); + client.Write("AUTHENTICATE *"); + client.ReadUntil(":irc.znc.in 906 foo :SASL authentication aborted"); +} + TEST_F(ZNCTest, SaslAuthExternal) { auto znc = Run(); auto ircd = ConnectIRCd(); @@ -419,7 +551,7 @@ TEST_F(ZNCTest, SaslAuthExternal) { client2.ReadUntil(":friend PRIVMSG nick :["); Reconnect(); - client2.Write("AUTHENTICATE " + QString("user/te").toUtf8().toBase64()); + client2.Write("AUTHENTICATE " + QByteArrayLiteral("user/te").toBase64()); client2.ReadUntil( ":irc.znc.in 900 nick nick!user@127.0.0.1 user :You are now logged in " "as user"); @@ -429,7 +561,7 @@ TEST_F(ZNCTest, SaslAuthExternal) { ":*status!status@znc.in PRIVMSG nick :Network te doesn't exist."); Reconnect(); - client2.Write("AUTHENTICATE " + QString("moo").toUtf8().toBase64()); + client2.Write("AUTHENTICATE " + QByteArrayLiteral("moo").toBase64()); client2.ReadUntil( ":irc.znc.in 904 nick :The specified user doesn't have this key"); @@ -440,6 +572,18 @@ TEST_F(ZNCTest, SaslAuthExternal) { client2.Write("AUTHENTICATE +"); client2.ReadUntil( ":irc.znc.in 904 nick :Client cert not recognized"); + + // Wrong mechanism + auto client3 = ConnectClient(); + client3.Write("CAP LS 302"); + client3.Write("NICK nick"); + client3.ReadUntil(" sasl=EXTERNAL,PLAIN "); + client3.Write("CAP REQ :sasl"); + client3.ReadUntil("ACK :sasl"); + client3.Write("AUTHENTICATE FOO"); + client3.ReadUntil(":irc.znc.in 908 nick EXTERNAL,PLAIN :are available SASL mechanisms"); + client3.ReadUntil( + ":irc.znc.in 904 nick :SASL authentication failed"); } } // namespace From 93e364b296941670ee51969ef59feefa4af21c77 Mon Sep 17 00:00:00 2001 From: Alexey Sokolov Date: Sat, 15 Mar 2025 22:43:35 +0000 Subject: [PATCH 15/19] Add modpython sasl test --- modules/modpython/functions.in | 1 + modules/modpython/module.h | 1 + modules/modpython/znc.py | 3 ++ test/integration/tests/scripting.cpp | 49 ++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+) diff --git a/modules/modpython/functions.in b/modules/modpython/functions.in index b662507f..366c6307 100644 --- a/modules/modpython/functions.in +++ b/modules/modpython/functions.in @@ -115,6 +115,7 @@ void OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState) void OnClientGetSASLMechanisms(SCString& ssMechanisms) EModRet OnClientSASLServerInitialChallenge(const CString& sMechanism, CString& sResponse) EModRet OnClientSASLAuthenticate(const CString& sMechanism, const CString& sMessage) +void OnClientSASLAborted() EModRet OnModuleLoading(const CString& sModName, const CString& sArgs, CModInfo::EModuleType eType, bool& bSuccess, CString& sRetMsg) EModRet OnModuleUnloading(CModule* pModule, bool& bSuccess, CString& sRetMsg) EModRet OnGetModInfo(CModInfo& ModInfo, const CString& sModule, bool& bSuccess, CString& sRetMsg) diff --git a/modules/modpython/module.h b/modules/modpython/module.h index ed701246..57a7ffbb 100644 --- a/modules/modpython/module.h +++ b/modules/modpython/module.h @@ -199,6 +199,7 @@ class ZNC_EXPORT_LIB_EXPORT CPyModule : public CModule { CString& sResponse) override; EModRet OnClientSASLAuthenticate(const CString& sMechanism, const CString& sMessage) override; + void OnClientSASLAborted() override; virtual EModRet OnModuleLoading(const CString& sModName, const CString& sArgs, CModInfo::EModuleType eType, bool& bSuccess, diff --git a/modules/modpython/znc.py b/modules/modpython/znc.py index d2a3dc49..64e429d3 100644 --- a/modules/modpython/znc.py +++ b/modules/modpython/znc.py @@ -487,6 +487,9 @@ class Module: def OnClientSASLAuthenticate(self, sMechanism, sMessage): pass + def OnClientSASLAborted(self): + pass + def OnModuleLoading(self, sModName, sArgs, eType, bSuccess, sRetMsg): pass diff --git a/test/integration/tests/scripting.cpp b/test/integration/tests/scripting.cpp index eacac7e0..516cd9c4 100644 --- a/test/integration/tests/scripting.cpp +++ b/test/integration/tests/scripting.cpp @@ -355,5 +355,54 @@ TEST_F(ZNCTest, ModpythonCommand) { client.ReadUntil(":*cmdtest!cmdtest@znc.in PRIVMSG nick :ping понг"); } +TEST_F(ZNCTest, ModpythonSaslAuth) { +#ifndef WANT_PYTHON + GTEST_SKIP() << "Modpython is disabled"; +#endif + auto znc = Run(); + znc->CanLeak(); + + InstallModule("sasltest.py", R"( + import znc + + class sasltest(znc.Module): + + module_types = [znc.CModInfo.GlobalModule] + + def OnClientGetSASLMechanisms(self, ssMechanisms): + ssMechanisms.insert("FOO") + + def OnClientSASLServerInitialChallenge(self, sMechanism, sResponse): + if sMechanism == "FOO": + sResponse.s = "Welcome" + return znc.CONTINUE + + def OnClientSASLAuthenticate(self, sMechanism, sMessage): + if sMechanism == "FOO": + user = znc.CZNC.Get().FindUser("user") + self.GetClient().AcceptSASLLogin(user) + return znc.HALT + return znc.CONTINUE + + )"); + auto ircd = ConnectIRCd(); + auto client = LoginClient(); + client.Write("znc loadmod modpython"); + client.Write("znc loadmod sasltest"); + client.ReadUntil("Loaded"); + + auto client2 = ConnectClient(); + client2.Write("CAP LS 302"); + client2.Write("NICK nick"); + client2.ReadUntil(" sasl=FOO,PLAIN "); + client2.Write("CAP REQ :sasl"); + client2.Write("AUTHENTICATE FOO"); + client2.ReadUntil("AUTHENTICATE " + QByteArrayLiteral("Welcome").toBase64()); + client2.Write("AUTHENTICATE +"); + client2.ReadUntil( + ":irc.znc.in 900 nick nick!user@127.0.0.1 user :You are now logged in " + "as user"); +} + } // namespace } // namespace znc_inttest From a7dffb8ff1d8747a7d13045c4b3433312c61154d Mon Sep 17 00:00:00 2001 From: Alexey Sokolov Date: Sun, 16 Mar 2025 00:42:27 +0000 Subject: [PATCH 16/19] Add modperl support for sasl --- modules/modperl/codegen.pl | 1 + modules/modperl/functions.in | 5 +++ modules/modperl/modperl.i | 43 +++++++++++++++++---- modules/modperl/module.h | 7 ++++ test/integration/tests/scripting.cpp | 57 ++++++++++++++++++++++++++++ 5 files changed, 106 insertions(+), 7 deletions(-) diff --git a/modules/modperl/codegen.pl b/modules/modperl/codegen.pl index 1fa60a61..2c6a2652 100755 --- a/modules/modperl/codegen.pl +++ b/modules/modperl/codegen.pl @@ -98,6 +98,7 @@ while (<$in>) { say $out "\t\tPUSH_PTR($sub*, *i);"; say $out "\t}"; } + when (/SCString/) { my $b=$a->{base}; $b=~s/^const//; say $out "\tPUSH_PTR($b*, &$a->{var});" } when (/CString/) { say $out "\tPUSH_STR($a->{var});" } when (/\*$/) { my $t=$a->{type}; $t=~s/^const//; say $out "\tPUSH_PTR($t, $a->{var});" } when (/&$/) { my $b=$a->{base}; $b=~s/^const//; say $out "\tPUSH_PTR($b*, &$a->{var});" } diff --git a/modules/modperl/functions.in b/modules/modperl/functions.in index 87d4c867..105acb61 100644 --- a/modules/modperl/functions.in +++ b/modules/modperl/functions.in @@ -103,3 +103,8 @@ EModRet OnChanNoticeMessage(CNoticeMessage& Message) EModRet OnTopicMessage(CTopicMessage& Message) EModRet OnSendToClientMessage(CMessage& Message) EModRet OnSendToIRCMessage(CMessage& Message) + +void OnClientGetSASLMechanisms(SCString& ssMechanisms) +EModRet OnClientSASLServerInitialChallenge(const CString& sMechanism, CString& sResponse) +EModRet OnClientSASLAuthenticate(const CString& sMechanism, const CString& sMessage) +void OnClientSASLAborted() diff --git a/modules/modperl/modperl.i b/modules/modperl/modperl.i index 55ecff9a..25f04d39 100644 --- a/modules/modperl/modperl.i +++ b/modules/modperl/modperl.i @@ -49,6 +49,7 @@ #include "znc/Buffer.h" #include "modperl/module.h" #define stat struct stat +#include "modperl/pstring.h" %} %apply long { off_t }; @@ -66,11 +67,32 @@ %include namespace std { - template class set { - public: - set(); - set(const set&); - }; + template class set { + public: + set(); + set(const set&); + unsigned int size() const; + bool empty() const; + void clear(); + void insert(const K& key); + void erase(const K& key); + %extend { + bool has_key(const K& key) { + auto i = self->find(key); + return i != self->end(); + } + SV* keys_() { + AV* av = newAV_alloc_x(self->size()); + // assume SCString + int i = 0; + for (const auto& a : *self) { + av_store(av, i++, PString(a).GetSV(false)); + } + SV* result = newRV_noinc((SV*)av); + return sv_2mortal(result); + } + } + }; } %include "modperl/CString.i" @@ -98,9 +120,9 @@ namespace std { %template(VCString) std::vector; typedef std::vector VCString; /*%template(MNicks) std::map;*/ -/*%template(SModInfo) std::set; +/*%template(SModInfo) std::set;*/ %template(SCString) std::set; -typedef std::set SCString;*/ +typedef std::set SCString; %template(PerlMCString) std::map; class MCString : public std::map {}; /*%template(PerlModulesVector) std::vector;*/ @@ -294,6 +316,13 @@ typedef std::vector > VPair; return %$result; } *GetNicks = *_GetNicks_; + + package ZNC::SCString; + sub keys { + my $self = shift; + my $keys = $self->keys_; + return @$keys; + } %} /* vim: set filetype=cpp: */ diff --git a/modules/modperl/module.h b/modules/modperl/module.h index 3dbff660..61caa74b 100644 --- a/modules/modperl/module.h +++ b/modules/modperl/module.h @@ -160,6 +160,13 @@ class ZNC_EXPORT_LIB_EXPORT CPerlModule : public CModule { EModRet OnTopicMessage(CTopicMessage& Message) override; EModRet OnSendToClientMessage(CMessage& Message) override; EModRet OnSendToIRCMessage(CMessage& Message) override; + + void OnClientGetSASLMechanisms(SCString& ssMechanisms) override; + EModRet OnClientSASLServerInitialChallenge(const CString& sMechanism, + CString& sResponse) override; + EModRet OnClientSASLAuthenticate(const CString& sMechanism, + const CString& sMessage) override; + void OnClientSASLAborted() override; }; static inline CPerlModule* AsPerlModule(CModule* p) { diff --git a/test/integration/tests/scripting.cpp b/test/integration/tests/scripting.cpp index 516cd9c4..2bccaa53 100644 --- a/test/integration/tests/scripting.cpp +++ b/test/integration/tests/scripting.cpp @@ -404,5 +404,62 @@ TEST_F(ZNCTest, ModpythonSaslAuth) { "as user"); } +TEST_F(ZNCTest, ModperlSaslAuth) { +#ifndef WANT_PERL + GTEST_SKIP() << "Modperl is disabled"; +#endif + auto znc = Run(); + znc->CanLeak(); + + InstallModule("sasltest.pm", R"( + package sasltest; + use base 'ZNC::Module'; + sub module_types { $ZNC::CModInfo::GlobalModule } + + sub OnClientGetSASLMechanisms { + my $self = shift; + my $mechs = shift; + $mechs->insert('FOO'); + } + + sub OnClientSASLServerInitialChallenge { + if ($_[1] eq "FOO") { + $_[2] = "Welcome"; + } + return $ZNC::CONTINUE; + } + + sub OnClientSASLAuthenticate { + my $self = $_[0]; + if ($_[1] eq "FOO") { + my $user = ZNC::CZNC::Get()->FindUser("user"); + $self->GetClient->AcceptSASLLogin($user); + return $ZNC::HALT; + } + return $ZNC::CONTINUE; + } + + 1; +)"); + + auto ircd = ConnectIRCd(); + auto client = LoginClient(); + client.Write("znc loadmod modperl"); + client.Write("znc loadmod sasltest"); + client.ReadUntil("Loaded"); + + auto client2 = ConnectClient(); + client2.Write("CAP LS 302"); + client2.Write("NICK nick"); + client2.ReadUntil(" sasl=FOO,PLAIN "); + client2.Write("CAP REQ :sasl"); + client2.Write("AUTHENTICATE FOO"); + client2.ReadUntil("AUTHENTICATE " + QByteArrayLiteral("Welcome").toBase64()); + client2.Write("AUTHENTICATE +"); + client2.ReadUntil( + ":irc.znc.in 900 nick nick!user@127.0.0.1 user :You are now logged in " + "as user"); +} + } // namespace } // namespace znc_inttest From 0a45c4710af22bd43e67164abb9e8a14e393a993 Mon Sep 17 00:00:00 2001 From: Alexey Sokolov Date: Sun, 16 Mar 2025 09:44:10 +0000 Subject: [PATCH 17/19] SASL: Support receiving USER and NICK after CAP END --- src/Client.cpp | 2 +- test/integration/tests/modules.cpp | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Client.cpp b/src/Client.cpp index c21925c4..01195b50 100644 --- a/src/Client.cpp +++ b/src/Client.cpp @@ -200,7 +200,7 @@ void CClient::ReadLine(const CString& sData) { } m_bGotUser = true; - if (m_bGotPass) { + if (m_bGotPass || !m_sSASLUser.empty()) { AuthUser(); } else if (!m_bInCap) { SendRequiredPasswordNotice(); diff --git a/test/integration/tests/modules.cpp b/test/integration/tests/modules.cpp index f7c88a7d..53ad1f2d 100644 --- a/test/integration/tests/modules.cpp +++ b/test/integration/tests/modules.cpp @@ -16,6 +16,7 @@ #include #include +#include #include "znctest.h" @@ -480,6 +481,24 @@ TEST_F(ZNCTest, SaslAuthPlainImapAuth) { client2.ReadUntil(":irc.znc.in 903 foo :SASL authentication successful"); } +TEST_F(ZNCTest, SaslAuthUserAfterCapEnd) { + // kvirc sends this + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = ConnectClient(); + client.Write("CAP LS"); + client.Write("PING :::1"); + client.Write("CAP REQ :sasl"); + client.Write("AUTHENTICATE PLAIN"); + client.Write("AUTHENTICATE " + + QByteArrayLiteral("\0user\0hunter2").toBase64()); + client.Write("CAP END"); + client.ReadUntil("903 unknown-nick :SASL authentication successful"); + client.Write("NICK nick"); + client.Write("USER user 0 1 :2"); + client.ReadUntil("001"); +} + TEST_F(ZNCTest, SaslAuthAbort) { auto znc = Run(); auto ircd = ConnectIRCd(); From 89e6d58d5a588adb64331a02039b107f70f0bdfe Mon Sep 17 00:00:00 2001 From: Alexey Sokolov Date: Sun, 16 Mar 2025 09:45:48 +0000 Subject: [PATCH 18/19] SASL: Move tests which are not about modules from modules.cpp to core.cpp --- test/integration/tests/core.cpp | 140 +++++++++++++++++++++++++++++ test/integration/tests/modules.cpp | 140 ----------------------------- 2 files changed, 140 insertions(+), 140 deletions(-) diff --git a/test/integration/tests/core.cpp b/test/integration/tests/core.cpp index f02667e7..2a7a62d0 100644 --- a/test/integration/tests/core.cpp +++ b/test/integration/tests/core.cpp @@ -824,5 +824,145 @@ TEST_F(ZNCTest, ChgHostOnlyNicksAlreadyOnChannels) { Not(HasSubstr("JOIN #chan2")))); } +TEST_F(ZNCTest, SaslAuthPlainSimple) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = ConnectClient(); + client.Write("NICK foo"); + client.Write("CAP LS"); + client.ReadUntil(" sasl "); + client.Write("CAP REQ :sasl"); + client.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); + client.Write("USER bar"); + client.Write("AUTHENTICATE PLAIN"); + client.ReadUntil("AUTHENTICATE +"); + client.Write("AUTHENTICATE " + QByteArrayLiteral("\0user\0hunter2").toBase64()); + client.ReadUntil(":irc.znc.in 903 foo :SASL authentication successful"); +} + +TEST_F(ZNCTest, SaslAuthPlainCopyInZ) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = ConnectClient(); + client.Write("NICK foo"); + client.Write("CAP LS"); + client.ReadUntil(" sasl "); + client.Write("CAP REQ :sasl"); + client.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); + client.Write("USER bar"); + client.Write("AUTHENTICATE PLAIN"); + client.ReadUntil("AUTHENTICATE +"); + client.Write("AUTHENTICATE " + QByteArrayLiteral("user@phone\0user@phone\0hunter2").toBase64()); + client.ReadUntil(":irc.znc.in 903 foo :SASL authentication successful"); + client.Write("CAP END"); + client.Write("znc listclients"); + client.ReadUntil("phone"); +} + +TEST_F(ZNCTest, SaslAuthPlainPartialInZ) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = ConnectClient(); + client.Write("NICK foo"); + client.Write("CAP LS"); + client.ReadUntil(" sasl "); + client.Write("CAP REQ :sasl"); + client.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); + client.Write("USER bar"); + client.Write("AUTHENTICATE PLAIN"); + client.ReadUntil("AUTHENTICATE +"); + client.Write("AUTHENTICATE " + QByteArrayLiteral("user@phone\0user\0hunter2").toBase64()); + client.ReadUntil(":irc.znc.in 903 foo :SASL authentication successful"); + client.Write("CAP END"); + client.Write("znc listclients"); + client.ReadUntil("phone"); +} + +TEST_F(ZNCTest, SaslAuthPlainDifferentZ) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = ConnectClient(); + client.Write("NICK foo"); + client.Write("CAP LS"); + client.ReadUntil(" sasl "); + client.Write("CAP REQ :sasl"); + client.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); + client.Write("USER bar"); + client.Write("AUTHENTICATE PLAIN"); + client.ReadUntil("AUTHENTICATE +"); + client.Write("AUTHENTICATE " + QByteArrayLiteral("user@phone\0user@tablet\0hunter2").toBase64()); + client.ReadUntil(":irc.znc.in 904 foo :No support for custom AuthzId"); +} + +TEST_F(ZNCTest, SaslAuthPlainWrongPassword) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = ConnectClient(); + client.Write("NICK foo"); + client.Write("CAP LS"); + client.ReadUntil(" sasl "); + client.Write("CAP REQ :sasl"); + client.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); + client.Write("USER bar"); + client.Write("AUTHENTICATE PLAIN"); + client.ReadUntil("AUTHENTICATE +"); + client.Write("AUTHENTICATE " + QByteArrayLiteral("\0user\0hunter3").toBase64()); + client.ReadUntil(":irc.znc.in 904 foo :Invalid Password"); + + // Try again on the same connection + client.Write("AUTHENTICATE PLAIN"); + client.ReadUntil(":irc.znc.in 904 foo :SASL authentication failed"); +} + +TEST_F(ZNCTest, SaslAuthPlainWrongUser) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = ConnectClient(); + client.Write("NICK foo"); + client.Write("CAP LS"); + client.ReadUntil(" sasl "); + client.Write("CAP REQ :sasl"); + client.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); + client.Write("USER bar"); + client.Write("AUTHENTICATE PLAIN"); + client.ReadUntil("AUTHENTICATE +"); + client.Write("AUTHENTICATE " + QByteArrayLiteral("\0anotheruser\0hunter2").toBase64()); + client.ReadUntil(":irc.znc.in 904 foo :Invalid Password"); +} + +TEST_F(ZNCTest, SaslAuthUserAfterCapEnd) { + // kvirc sends this + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = ConnectClient(); + client.Write("CAP LS"); + client.Write("PING :::1"); + client.Write("CAP REQ :sasl"); + client.Write("AUTHENTICATE PLAIN"); + client.Write("AUTHENTICATE " + + QByteArrayLiteral("\0user\0hunter2").toBase64()); + client.Write("CAP END"); + client.ReadUntil("903 unknown-nick :SASL authentication successful"); + client.Write("NICK nick"); + client.Write("USER user 0 1 :2"); + client.ReadUntil("001"); +} + +TEST_F(ZNCTest, SaslAuthAbort) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = ConnectClient(); + client.Write("NICK foo"); + client.Write("CAP LS"); + client.ReadUntil(" sasl "); + client.Write("CAP REQ :sasl"); + client.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); + client.Write("USER bar"); + client.Write("AUTHENTICATE PLAIN"); + client.ReadUntil("AUTHENTICATE +"); + client.Write("AUTHENTICATE *"); + client.ReadUntil(":irc.znc.in 906 foo :SASL authentication aborted"); +} + } // namespace } // namespace znc_inttest diff --git a/test/integration/tests/modules.cpp b/test/integration/tests/modules.cpp index 53ad1f2d..6b94931b 100644 --- a/test/integration/tests/modules.cpp +++ b/test/integration/tests/modules.cpp @@ -349,112 +349,6 @@ TEST_F(ZNCTest, SaslRequire) { auto ircd2 = ConnectIRCd(); } -TEST_F(ZNCTest, SaslAuthPlainSimple) { - auto znc = Run(); - auto ircd = ConnectIRCd(); - auto client = ConnectClient(); - client.Write("NICK foo"); - client.Write("CAP LS"); - client.ReadUntil(" sasl "); - client.Write("CAP REQ :sasl"); - client.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); - client.Write("USER bar"); - client.Write("AUTHENTICATE PLAIN"); - client.ReadUntil("AUTHENTICATE +"); - client.Write("AUTHENTICATE " + QByteArrayLiteral("\0user\0hunter2").toBase64()); - client.ReadUntil(":irc.znc.in 903 foo :SASL authentication successful"); -} - -TEST_F(ZNCTest, SaslAuthPlainCopyInZ) { - auto znc = Run(); - auto ircd = ConnectIRCd(); - auto client = ConnectClient(); - client.Write("NICK foo"); - client.Write("CAP LS"); - client.ReadUntil(" sasl "); - client.Write("CAP REQ :sasl"); - client.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); - client.Write("USER bar"); - client.Write("AUTHENTICATE PLAIN"); - client.ReadUntil("AUTHENTICATE +"); - client.Write("AUTHENTICATE " + QByteArrayLiteral("user@phone\0user@phone\0hunter2").toBase64()); - client.ReadUntil(":irc.znc.in 903 foo :SASL authentication successful"); - client.Write("CAP END"); - client.Write("znc listclients"); - client.ReadUntil("phone"); -} - -TEST_F(ZNCTest, SaslAuthPlainPartialInZ) { - auto znc = Run(); - auto ircd = ConnectIRCd(); - auto client = ConnectClient(); - client.Write("NICK foo"); - client.Write("CAP LS"); - client.ReadUntil(" sasl "); - client.Write("CAP REQ :sasl"); - client.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); - client.Write("USER bar"); - client.Write("AUTHENTICATE PLAIN"); - client.ReadUntil("AUTHENTICATE +"); - client.Write("AUTHENTICATE " + QByteArrayLiteral("user@phone\0user\0hunter2").toBase64()); - client.ReadUntil(":irc.znc.in 903 foo :SASL authentication successful"); - client.Write("CAP END"); - client.Write("znc listclients"); - client.ReadUntil("phone"); -} - -TEST_F(ZNCTest, SaslAuthPlainDifferentZ) { - auto znc = Run(); - auto ircd = ConnectIRCd(); - auto client = ConnectClient(); - client.Write("NICK foo"); - client.Write("CAP LS"); - client.ReadUntil(" sasl "); - client.Write("CAP REQ :sasl"); - client.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); - client.Write("USER bar"); - client.Write("AUTHENTICATE PLAIN"); - client.ReadUntil("AUTHENTICATE +"); - client.Write("AUTHENTICATE " + QByteArrayLiteral("user@phone\0user@tablet\0hunter2").toBase64()); - client.ReadUntil(":irc.znc.in 904 foo :No support for custom AuthzId"); -} - -TEST_F(ZNCTest, SaslAuthPlainWrongPassword) { - auto znc = Run(); - auto ircd = ConnectIRCd(); - auto client = ConnectClient(); - client.Write("NICK foo"); - client.Write("CAP LS"); - client.ReadUntil(" sasl "); - client.Write("CAP REQ :sasl"); - client.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); - client.Write("USER bar"); - client.Write("AUTHENTICATE PLAIN"); - client.ReadUntil("AUTHENTICATE +"); - client.Write("AUTHENTICATE " + QByteArrayLiteral("\0user\0hunter3").toBase64()); - client.ReadUntil(":irc.znc.in 904 foo :Invalid Password"); - - // Try again on the same connection - client.Write("AUTHENTICATE PLAIN"); - client.ReadUntil(":irc.znc.in 904 foo :SASL authentication failed"); -} - -TEST_F(ZNCTest, SaslAuthPlainWrongUser) { - auto znc = Run(); - auto ircd = ConnectIRCd(); - auto client = ConnectClient(); - client.Write("NICK foo"); - client.Write("CAP LS"); - client.ReadUntil(" sasl "); - client.Write("CAP REQ :sasl"); - client.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); - client.Write("USER bar"); - client.Write("AUTHENTICATE PLAIN"); - client.ReadUntil("AUTHENTICATE +"); - client.Write("AUTHENTICATE " + QByteArrayLiteral("\0anotheruser\0hunter2").toBase64()); - client.ReadUntil(":irc.znc.in 904 foo :Invalid Password"); -} - TEST_F(ZNCTest, SaslAuthPlainImapAuth) { auto znc = Run(); auto ircd = ConnectIRCd(); @@ -481,40 +375,6 @@ TEST_F(ZNCTest, SaslAuthPlainImapAuth) { client2.ReadUntil(":irc.znc.in 903 foo :SASL authentication successful"); } -TEST_F(ZNCTest, SaslAuthUserAfterCapEnd) { - // kvirc sends this - auto znc = Run(); - auto ircd = ConnectIRCd(); - auto client = ConnectClient(); - client.Write("CAP LS"); - client.Write("PING :::1"); - client.Write("CAP REQ :sasl"); - client.Write("AUTHENTICATE PLAIN"); - client.Write("AUTHENTICATE " + - QByteArrayLiteral("\0user\0hunter2").toBase64()); - client.Write("CAP END"); - client.ReadUntil("903 unknown-nick :SASL authentication successful"); - client.Write("NICK nick"); - client.Write("USER user 0 1 :2"); - client.ReadUntil("001"); -} - -TEST_F(ZNCTest, SaslAuthAbort) { - auto znc = Run(); - auto ircd = ConnectIRCd(); - auto client = ConnectClient(); - client.Write("NICK foo"); - client.Write("CAP LS"); - client.ReadUntil(" sasl "); - client.Write("CAP REQ :sasl"); - client.ReadUntil(":irc.znc.in CAP foo ACK :sasl"); - client.Write("USER bar"); - client.Write("AUTHENTICATE PLAIN"); - client.ReadUntil("AUTHENTICATE +"); - client.Write("AUTHENTICATE *"); - client.ReadUntil(":irc.znc.in 906 foo :SASL authentication aborted"); -} - TEST_F(ZNCTest, SaslAuthExternal) { auto znc = Run(); auto ircd = ConnectIRCd(); From ab4125692c0a64b1295e73b2ce6ea8ee44ee47f7 Mon Sep 17 00:00:00 2001 From: Alexey Sokolov Date: Sun, 16 Mar 2025 11:14:01 +0000 Subject: [PATCH 19/19] Remove accidentally added header from previous commit --- test/integration/tests/modules.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/test/integration/tests/modules.cpp b/test/integration/tests/modules.cpp index 6b94931b..3b01147b 100644 --- a/test/integration/tests/modules.cpp +++ b/test/integration/tests/modules.cpp @@ -16,7 +16,6 @@ #include #include -#include #include "znctest.h"