mirror of
https://github.com/znc/znc.git
synced 2026-08-06 08:52:57 +02:00
Merge pull request #1936 from DarthGandalf/sasl
SASL Authentication for Clients
This commit is contained in:
+41
-1
@@ -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.
|
||||
|
||||
@@ -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"); }
|
||||
|
||||
@@ -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
@@ -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 <>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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});" }
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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: */
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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},
|
||||
|
||||
@@ -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}};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user