diff --git a/include/znc/Client.h b/include/znc/Client.h index 4fe4c3c0..2c77fa17 100644 --- a/include/znc/Client.h +++ b/include/znc/Client.h @@ -250,11 +250,33 @@ 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); + /** 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. + * 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 @@ -266,6 +288,15 @@ class CClient : public CIRCSocket { unsigned int DetachChans(const std::set& sChans); bool OnActionMessage(CActionMessage& Message); + void OnAuthenticateMessage(const CAuthenticateMessage& Message); + void AbortSASL(const CString& sFullIRCLine); + bool IsDuringSASL() const { return !m_sSASLMechanism.empty(); } + + /** + * Returns set of all available SASL mechanisms. + */ + SCString EnumerateSASLMechanisms() const; + bool OnCTCPMessage(CCTCPMessage& Message); bool OnJoinMessage(CJoinMessage& Message); bool OnModeMessage(CModeMessage& Message); @@ -296,17 +327,26 @@ class CClient : public CIRCSocket { bool m_bBatch; bool m_bEchoMessage; bool m_bSelfMessage; + bool m_bSASLCap; bool m_bPlaybackActive; CUser* m_pUser; 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; + 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/Message.h b/include/znc/Message.h index 7d8d4b3c..b31a4f17 100644 --- a/include/znc/Message.h +++ b/include/znc/Message.h @@ -65,6 +65,7 @@ class CMessage { Unknown, Account, Action, + Authenticate, Away, Capability, ChgHost, @@ -239,6 +240,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 05f374e6..7bdfbcba 100644 --- a/include/znc/Modules.h +++ b/include/znc/Modules.h @@ -1364,6 +1364,42 @@ 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. 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. + */ + 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 + * 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. + */ + virtual EModRet OnClientSASLAuthenticate(const CString& sMechanism, + 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. * @param eType wanted type of the module (user/global). @@ -1665,6 +1701,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 OnClientGetSASLMechanisms(SCString& ssMechanisms); + bool OnClientSASLAborted(); + bool OnClientSASLServerInitialChallenge(const CString& sMechanism, + CString& sResponse); + bool OnClientSASLAuthenticate(const CString& sMechanism, + const CString& sBuffer); + bool OnModuleLoading(const CString& sModName, const CString& sArgs, CModInfo::EModuleType eType, bool& bSuccess, CString& sRetMsg); 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/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(); 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/modules/modpython/functions.in b/modules/modpython/functions.in index e66ce0db..366c6307 100644 --- a/modules/modpython/functions.in +++ b/modules/modpython/functions.in @@ -112,6 +112,10 @@ 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 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 0588dcac..57a7ffbb 100644 --- a/modules/modpython/module.h +++ b/modules/modpython/module.h @@ -194,6 +194,12 @@ class ZNC_EXPORT_LIB_EXPORT CPyModule : public CModule { bool bState) override; void OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState) 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; 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 d53a8fe2..64e429d3 100644 --- a/modules/modpython/znc.py +++ b/modules/modpython/znc.py @@ -478,6 +478,18 @@ class Module: def OnClientCapRequest(self, pClient, sCap, bState): pass + def OnClientGetSASLMechanisms(self, ssMechanisms): + pass + + def OnClientSASLServerInitialChallenge(self, sMechanism, sResponse): + pass + + def OnClientSASLAuthenticate(self, sMechanism, sMessage): + pass + + def OnClientSASLAborted(self): + pass + def OnModuleLoading(self, sModName, sArgs, eType, bSuccess, sRetMsg): pass diff --git a/modules/saslplainauth.cpp b/modules/saslplainauth.cpp new file mode 100644 index 00000000..bc23cd4d --- /dev/null +++ b/modules/saslplainauth.cpp @@ -0,0 +1,55 @@ +/* + * 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. + * 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) {} + + void OnClientGetSASLMechanisms(SCString& ssMechanisms) override { + ssMechanisms.insert("PLAIN"); + } + + EModRet OnClientSASLAuthenticate(const CString& sMechanism, + const CString& sMessage) override { + if (sMechanism != "PLAIN") { + return CONTINUE; + } + + CString sNullSeparator = std::string("\0", 1); + 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; + } + + GetClient()->StartSASLPasswordCheck(sAuthcId, sPassword, sAuthzId); + return HALTMODS; + } +}; + +template <> +void TModInfo(CModInfo& Info) { + Info.SetWikiPage("saslplainauth"); +} + +GLOBALMODULEDEFS( + CSASLMechanismPlain, + t_s("Allows users to authenticate via the PLAIN SASL mechanism.")) diff --git a/src/Client.cpp b/src/Client.cpp index 3618825a..01195b50 100644 --- a/src/Client.cpp +++ b/src/Client.cpp @@ -94,6 +94,7 @@ CClient::CClient() m_bBatch(false), m_bEchoMessage(false), m_bSelfMessage(false), + m_bSASLCap(false), m_bPlaybackActive(false), m_pUser(nullptr), m_pNetwork(nullptr), @@ -102,6 +103,9 @@ CClient::CClient() m_sUser(""), m_sNetwork(""), m_sIdentifier(""), + m_sSASLBuffer(""), + m_sSASLMechanism(""), + m_sSASLUser(""), m_spAuth(), m_ssAcceptedCaps(), m_ssSupportedTags() { @@ -196,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(); @@ -215,6 +219,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; @@ -351,10 +361,40 @@ bool CClient::SendMotd() { } void CClient::AuthUser() { - if (!m_bGotNick || !m_bGotUser || !m_bGotPass || m_bInCap || IsAttached()) + if (!m_bGotNick || !m_bGotUser || m_bInCap || + (m_sSASLUser.empty() && !m_bGotPass) || IsAttached()) return; - m_spAuth = std::make_shared(this, m_sUser, m_sPass); + if (m_sSASLUser.empty()) { + 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); + if (pUser) { + AcceptLogin(*pUser); + } else { + RefuseLogin("SASL login was valid, but user no longer exists"); + } + } +} + +/** 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, 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); } @@ -363,6 +403,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); @@ -379,8 +425,8 @@ void CAuthBase::Invalidate() { m_pSock = nullptr; } void CAuthBase::AcceptLogin(CUser& User) { if (m_pSock) { AcceptedLogin(User); - Invalidate(); } + Invalidate(); } void CAuthBase::RefuseLogin(const CString& sReason) { @@ -408,6 +454,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); @@ -417,6 +469,9 @@ void CClientAuth::AcceptedLogin(CUser& User) { void CClient::AcceptLogin(CUser& User) { m_sPass = ""; m_pUser = &User; + m_sSASLMechanism = ""; + m_sSASLBuffer = ""; + m_sSASLUser = ""; // Set our proper timeout and set back our proper timeout mode // (constructor set a different timeout and mode) @@ -745,37 +800,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; } @@ -786,7 +862,19 @@ void CClient::HandleCap(const CMessage& Message) { m_uCapVersion = std::max(m_uCapVersion, Message.GetParam(1).ToUShort()); SCString ssOfferCaps; for (const auto& it : CoreCaps()) { - 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); @@ -804,7 +892,12 @@ void CClient::HandleCap(const CMessage& Message) { } else if (sSubCmd.Equals("END")) { m_bInCap = false; if (!IsAttached()) { - if (!m_pUser && m_bGotUser && !m_bGotPass) { + if (IsDuringSASL()) { + AbortSASL(":irc.znc.in 904 " + GetNick() + + " :SASL authentication aborted"); + } + + if (m_bGotUser && m_sSASLUser.empty() && !m_bGotPass) { SendRequiredPasswordNotice(); } else { AuthUser(); @@ -886,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("/"); @@ -897,6 +990,8 @@ void CClient::ParseUser(const CString& sAuthLine) { } else { ParseIdentifier(sAuthLine); } + + return m_sUser; } void CClient::ParseIdentifier(const CString& sAuthLine) { @@ -1047,6 +1142,157 @@ bool CClient::OnActionMessage(CActionMessage& Message) { return true; } +void CClient::SendSASLChallenge(CString sMessage) { + constexpr size_t uMaxSASLMsgLength = 400u; + sMessage.Base64Encode(); + size_t uChallengeSize = sMessage.length(); + + for (int i = 0; i < uChallengeSize; i += uMaxSASLMsgLength) { + CString sMsgPart = sMessage.substr(i, uMaxSASLMsgLength); + PutClient("AUTHENTICATE " + sMsgPart); + } + if (uChallengeSize % uMaxSASLMsgLength == 0) { + PutClient("AUTHENTICATE +"); + } +} + +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() + + " :You have already authenticated using SASL"); + return; + } + + auto SASLReset = [this]() { + m_sSASLMechanism = ""; + m_sSASLBuffer = ""; + }; + CString sMessage = Message.GetText(); + + if (sMessage.Equals("*")) { + AbortSASL(":irc.znc.in 906 " + GetNick() + + " :SASL authentication aborted"); + 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; + } + SCString ssMechanisms = EnumerateSASLMechanisms(); + if (ssMechanisms.find(sMessage) == ssMechanisms.end()) { + 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(); + + return; + } + + m_sSASLMechanism = sMessage; + + bool bResult = false; + CString sChallenge; + _GLOBALMODULECALL( + OnClientSASLServerInitialChallenge(m_sSASLMechanism, sChallenge), + nullptr, nullptr, this, &bResult); + if (!bResult) { + SendSASLChallenge(std::move(sChallenge)); + } + return; + } + + if (m_sSASLBuffer.length() + sMessage.length() > 10 * 1024) { + AbortSASL(":irc.znc.in 904 " + GetNick() + " :SASL response too long"); + return; + } + + if (sMessage.length() == uMaxSASLMsgLength) { + m_sSASLBuffer += sMessage; + return; + } + + if (sMessage != "+") { + m_sSASLBuffer += sMessage; + } + + m_sSASLBuffer.Base64Decode(); + + bool bResult = false; + + _GLOBALMODULECALL( + OnClientSASLAuthenticate(m_sSASLMechanism, m_sSASLBuffer), + nullptr, nullptr, this, &bResult); + m_sSASLBuffer.clear(); +} + +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); + return ssMechanisms; +} + bool CClient::OnCTCPMessage(CCTCPMessage& Message) { CString sTargets = Message.GetTarget(); diff --git a/src/Message.cpp b/src/Message.cpp index bb3d8b23..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())); } @@ -278,6 +278,7 @@ void CMessage::InitType() { } else { static std::map mTypes = { {"ACCOUNT", Type::Account}, + {"AUTHENTICATE", Type::Authenticate}, {"AWAY", Type::Away}, {"CAP", Type::Capability}, {"CHGHOST", Type::ChgHost}, diff --git a/src/Modules.cpp b/src/Modules.cpp index 91108218..6cbaba4f 100644 --- a/src/Modules.cpp +++ b/src/Modules.cpp @@ -1192,6 +1192,7 @@ bool CModule::InternalServerDependentCapsIsClientCapSupported( } void CModule::OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState) {} + void CModule::InternalServerDependentCapsOnClientCapRequest(CClient* pClient, const CString& sCap, bool bState) { @@ -1200,6 +1201,21 @@ void CModule::InternalServerDependentCapsOnClientCapRequest(CClient* pClient, if (it == m_mServerDependentCaps.end()) return; it->second->OnClientChangedSupport(pClient, bState); } + +CModule::EModRet CModule::OnClientSASLAuthenticate( + const CString& sMechanism, const CString& sBuffer) { + return CONTINUE; +} + +CModule::EModRet CModule::OnClientSASLServerInitialChallenge( + const CString& sMechanism, CString& sResponse) { + return CONTINUE; +} + +void CModule::OnClientGetSASLMechanisms(SCString& ssMechanisms) {} + +void CModule::OnClientSASLAborted() {} + CModule::EModRet CModule::OnModuleLoading(const CString& sModName, const CString& sArgs, CModInfo::EModuleType eType, @@ -1745,6 +1761,26 @@ bool CModules::OnClientCapRequest(CClient* pClient, const CString& sCap, return false; } +bool CModules::OnClientSASLAuthenticate(const CString& sMechanism, + const CString& sBuffer) { + MODHALTCHK(OnClientSASLAuthenticate(sMechanism, sBuffer)); +} + +bool CModules::OnClientSASLServerInitialChallenge(const CString& sMechanism, + CString& sResponse) { + MODHALTCHK(OnClientSASLServerInitialChallenge(sMechanism, sResponse)); +} + +bool CModules::OnClientGetSASLMechanisms(SCString& ssMechanisms) { + MODUNLOADCHK(OnClientGetSASLMechanisms(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) { @@ -2030,6 +2066,7 @@ void CModules::GetDefaultMods(set& ssMods, {"chansaver", CModInfo::UserModule}, {"controlpanel", CModInfo::UserModule}, {"corecaps", CModInfo::GlobalModule}, + {"saslplainauth", CModInfo::GlobalModule}, {"simple_away", CModInfo::NetworkModule}, {"webadmin", CModInfo::GlobalModule}}; 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 021183d4..3b01147b 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; @@ -346,5 +348,121 @@ TEST_F(ZNCTest, SaslRequire) { auto ircd2 = ConnectIRCd(); } +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, 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 " + 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"); + 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 " + QByteArrayLiteral("moo").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"); + + // 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 } // namespace znc_inttest diff --git a/test/integration/tests/scripting.cpp b/test/integration/tests/scripting.cpp index eacac7e0..2bccaa53 100644 --- a/test/integration/tests/scripting.cpp +++ b/test/integration/tests/scripting.cpp @@ -355,5 +355,111 @@ 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"); +} + +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