Merge pull request #1936 from DarthGandalf/sasl

SASL Authentication for Clients
This commit is contained in:
Alexey Sokolov
2025-03-16 14:09:28 +00:00
committed by GitHub
19 changed files with 987 additions and 53 deletions
+41 -1
View File
@@ -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 <typename T>
@@ -266,6 +288,15 @@ class CClient : public CIRCSocket {
unsigned int DetachChans(const std::set<CChan*>& 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<CAuthBase> 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.
+8
View File
@@ -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"); }
+44
View File
@@ -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<CModule*>, 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);
+79 -8
View File
@@ -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<SCString::const_iterator, bool> 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<CString, set<CString>> MSCString;
MSCString m_PubKeys;
MSCString m_KeyToUser;
};
template <>
+4
View File
@@ -216,6 +216,10 @@ class CFailToBanMod : public CModule {
Add(sRemoteIP, 1);
}
void OnClientLogin() override {
Remove(GetClient()->GetRemoteIP());
}
EModRet OnLoginAttempt(std::shared_ptr<CAuthBase> Auth) override {
// e.g. webadmin ends up here
const CString& sRemoteIP = Auth->GetRemoteIP();
+1
View File
@@ -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});" }
+5
View File
@@ -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()
+36 -7
View File
@@ -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 <std_deque.i>
namespace std {
template<class K> class set {
public:
set();
set(const set<K>&);
};
template<class K> class set {
public:
set();
set(const set<K>&);
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<CString>;
typedef std::vector<CString> VCString;
/*%template(MNicks) std::map<CString, CNick>;*/
/*%template(SModInfo) std::set<CModInfo>;
/*%template(SModInfo) std::set<CModInfo>;*/
%template(SCString) std::set<CString>;
typedef std::set<CString> SCString;*/
typedef std::set<CString> SCString;
%template(PerlMCString) std::map<CString, CString>;
class MCString : public std::map<CString, CString> {};
/*%template(PerlModulesVector) std::vector<CModule*>;*/
@@ -294,6 +316,13 @@ typedef std::vector<std::pair<CString, CString> > VPair;
return %$result;
}
*GetNicks = *_GetNicks_;
package ZNC::SCString;
sub keys {
my $self = shift;
my $keys = $self->keys_;
return @$keys;
}
%}
/* vim: set filetype=cpp: */
+7
View File
@@ -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) {
+4
View File
@@ -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)
+6
View File
@@ -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,
+12
View File
@@ -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
+55
View File
@@ -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 <znc/User.h>
#include <znc/znc.h>
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<CSASLMechanismPlain>(CModInfo& Info) {
Info.SetWikiPage("saslplainauth");
}
GLOBALMODULEDEFS(
CSASLMechanismPlain,
t_s("Allows users to authenticate via the PLAIN SASL mechanism."))
+282 -36
View File
@@ -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<CClientAuth>(this, m_sUser, m_sPass);
if (m_sSASLUser.empty()) {
m_spAuth = std::make_shared<CClientAuth>(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<CClientSASLAuth>(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<CString, std::function<void(CClient*, bool bVal)>>&
CClient::CoreCaps() {
static const std::map<CString, std::function<void(CClient*, bool bVal)>> mCoreCaps = []{
std::map<CString, std::function<void(CClient*, bool bVal)>> 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<CString, std::function<void(CClient*, bool bVal)>>
mCoreCaps = [] {
std::map<CString, std::function<void(CClient*, bool bVal)>>
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();
+2 -1
View File
@@ -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<CString, Type> mTypes = {
{"ACCOUNT", Type::Account},
{"AUTHENTICATE", Type::Authenticate},
{"AWAY", Type::Away},
{"CAP", Type::Capability},
{"CHGHOST", Type::ChgHost},
+37
View File
@@ -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<CModInfo>& ssMods,
{"chansaver", CModInfo::UserModule},
{"controlpanel", CModInfo::UserModule},
{"corecaps", CModInfo::GlobalModule},
{"saslplainauth", CModInfo::GlobalModule},
{"simple_away", CModInfo::NetworkModule},
{"webadmin", CModInfo::GlobalModule}};
+140
View File
@@ -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
+118
View File
@@ -19,6 +19,8 @@
#include "znctest.h"
#include <QSslSocket>
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
+106
View File
@@ -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