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