diff --git a/include/znc/Client.h b/include/znc/Client.h index 2440cb8f..c400de91 100644 --- a/include/znc/Client.h +++ b/include/znc/Client.h @@ -98,77 +98,7 @@ class CClientAuth : public CAuthBase { class CClient : public CIRCSocket { public: - CClient() - : CIRCSocket(), - m_bGotPass(false), - m_bGotNick(false), - m_bGotUser(false), - m_bInCap(false), - m_bCapNotify(false), - m_bAwayNotify(false), - m_bAccountNotify(false), - m_bAccountTag(false), - m_bExtendedJoin(false), - m_bNamesx(false), - m_bUHNames(false), - m_bAway(false), - m_bServerTime(false), - m_bBatch(false), - m_bEchoMessage(false), - m_bSelfMessage(false), - m_bPlaybackActive(false), - m_pUser(nullptr), - m_pNetwork(nullptr), - m_sNick("unknown-nick"), - m_sPass(""), - m_sUser(""), - m_sNetwork(""), - m_sIdentifier(""), - m_spAuth(), - m_ssAcceptedCaps(), - m_ssSupportedTags(), - m_mCoreCaps({ - {"multi-prefix", - {false, [this](bool bVal) { m_bNamesx = bVal; }}}, - {"userhost-in-names", - {false, [this](bool bVal) { m_bUHNames = bVal; }}}, - {"echo-message", - {false, [this](bool bVal) { m_bEchoMessage = bVal; }}}, - {"server-time", - {false, [this](bool bVal) { - m_bServerTime = bVal; - SetTagSupport("time", bVal); - }}}, - {"batch", {false, [this](bool bVal) { - m_bBatch = bVal; - SetTagSupport("batch", bVal); - }}}, - {"cap-notify", - {false, [this](bool bVal) { m_bCapNotify = bVal; }}}, - {"away-notify", - {true, [this](bool bVal) { m_bAwayNotify = bVal; }}}, - {"account-notify", - {true, [this](bool bVal) { m_bAccountNotify = bVal; }}}, - {"account-tag", - {true, [this](bool bVal) { - m_bAccountTag = bVal; - SetTagSupport("account", bVal); - }}}, - {"extended-join", - {true, [this](bool bVal) { m_bExtendedJoin = bVal; }}}, - }) { - EnableReadLine(); - // RFC says a line can have 512 chars max, but we are - // a little more gentle ;) - SetMaxBufferThreshold(1024); - - // For compatibility with older clients - m_mCoreCaps["znc.in/server-time-iso"] = m_mCoreCaps["server-time"]; - m_mCoreCaps["znc.in/batch"] = m_mCoreCaps["batch"]; - m_mCoreCaps["znc.in/self-message"] = { - false, [this](bool bVal) { m_bSelfMessage = bVal; }}; - } - + CClient(); virtual ~CClient(); CClient(const CClient&) = delete; @@ -181,6 +111,8 @@ class CClient : public CIRCSocket { CString GetNick(bool bAllowIRCNick = true) const; CString GetNickMask() const; CString GetIdentifier() const { return m_sIdentifier; } + unsigned short int CapVersion() const { return m_uCapVersion; } + bool HasCap302() const { return CapVersion() >= 302; } bool HasCapNotify() const { return m_bCapNotify; } bool HasAwayNotify() const { return m_bAwayNotify; } bool HasAccountNotify() const { return m_bAccountNotify; } @@ -293,8 +225,9 @@ class CClient : public CIRCSocket { */ void SetTagSupport(const CString& sTag, bool bState); - void NotifyServerDependentCaps(const SCString& ssCaps); - void ClearServerDependentCaps(); + /** Notifies client about one specific cap which server has just notified us about. + */ + void NotifyServerDependentCap(const CString& sCap, bool bValue, const CString& sValue); void ReadLine(const CString& sData) override; bool SendMotd(); @@ -349,6 +282,7 @@ class CClient : public CIRCSocket { bool m_bGotPass; bool m_bGotNick; bool m_bGotUser; + unsigned short int m_uCapVersion; bool m_bInCap; bool m_bCapNotify; bool m_bAwayNotify; @@ -373,16 +307,14 @@ class CClient : public CIRCSocket { std::shared_ptr m_spAuth; SCString m_ssAcceptedCaps; SCString m_ssSupportedTags; - // The capabilities supported by the ZNC core - capability names mapped - // to a pair which contains a bool describing whether the capability is - // server-dependent, and a capability value change handler. - std::map>> - m_mCoreCaps; - // A subset of CIRCSock::GetAcceptedCaps(), the caps that can be listed - // in CAP LS and may be notified to the client with CAP NEW (cap-notify). - SCString m_ssServerDependentCaps; + // 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. + static const std::map>& + CoreCaps(); friend class ClientTest; + friend class CCoreCaps; }; #endif // !ZNC_CLIENT_H diff --git a/include/znc/IRCNetwork.h b/include/znc/IRCNetwork.h index 8ac5ce75..a41cfd5f 100644 --- a/include/znc/IRCNetwork.h +++ b/include/znc/IRCNetwork.h @@ -156,6 +156,8 @@ class CIRCNetwork : private CCoreTranslationMixin { void IRCConnected(); void IRCDisconnected(); void CheckIRCConnect(); + void NotifyClientsAboutServerDependentCap(const CString& sCap, bool bValue); + bool IsServerCapAccepted(const CString& sCap) const; bool PutIRC(const CString& sLine); bool PutIRC(const CMessage& Message); diff --git a/include/znc/IRCSock.h b/include/znc/IRCSock.h index a158e6de..506c6527 100644 --- a/include/znc/IRCSock.h +++ b/include/znc/IRCSock.h @@ -162,6 +162,8 @@ class CIRCSock : public CIRCSocket { bool IsCapAccepted(const CString& sCap) { return 1 == m_ssAcceptedCaps.count(sCap); } + CString GetCapLsValue(const CString& sKey, + const CString& sDefault = "") const; const MCString& GetISupport() const { return m_mISupport; } CString GetISupport(const CString& sKey, const CString& sDefault = "") const; @@ -192,7 +194,7 @@ class CIRCSock : public CIRCSocket { bool OnTextMessage(CTextMessage& Message); bool OnTopicMessage(CTopicMessage& Message); bool OnWallopsMessage(CMessage& Message); - bool OnServerCapAvailable(const CString& sCap); + bool OnServerCapAvailable(const CString& sCap, const CString& sValue); // !Message Handlers void SetNick(const CString& sNick); @@ -223,6 +225,7 @@ class CIRCSock : public CIRCSocket { unsigned int m_uCapPaused; SCString m_ssAcceptedCaps; SCString m_ssPendingCaps; + MCString m_msCapLsValues; time_t m_lastCTCP; unsigned int m_uNumCTCP; static const time_t m_uCTCPFloodTime; @@ -237,6 +240,7 @@ class CIRCSock : public CIRCSocket { VCString m_vsSSLError; friend class CIRCFloodTimer; + friend class CCoreCaps; }; #endif // !ZNC_IRCSOCK_H diff --git a/include/znc/Modules.h b/include/znc/Modules.h index 700cf2e0..c27bb86b 100644 --- a/include/znc/Modules.h +++ b/include/znc/Modules.h @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -166,6 +167,19 @@ class CFPTimer; class CSockManager; // !Forward Declarations +class CCapability { + public: + virtual ~CCapability() = default; + virtual void OnServerChangedSupport(CIRCNetwork* pNetwork, bool bState) {} + virtual void OnClientChangedSupport(CClient* pClient, bool bState) {} + + CModule* GetModule() { return m_pModule; } + void SetModule(CModule* p) { m_pModule = p; } + + protected: + CModule* m_pModule = nullptr; +}; + class CTimer : public CCron { public: CTimer(CModule* pModule, unsigned int uInterval, unsigned int uCycles, @@ -998,13 +1012,28 @@ class CModule { virtual EModRet OnTopic(CNick& Nick, CChan& Channel, CString& sTopic); /** Called for every CAP received via CAP LS from server. + * If you need to also advertise the cap to clients, use + * AddServerDependentCapability() instead. * @param sCap capability supported by server. * @return true if your module supports this CAP and * needs to turn it on with CAP REQ. */ virtual bool OnServerCapAvailable(const CString& sCap); + /** Called for every CAP received via CAP LS from server. + * By default just calls OnServerCapAvailable() without sValue, so + * overriding one of the two is enough. + * If you need to also advertise the cap to clients, use + * AddServerDependentCapability() instead. + * @param sCap capability name supported by server. + * @param sValue value. + * @return true if your module supports this CAP and + * needs to turn it on with CAP REQ. + */ + virtual bool OnServerCap302Available(const CString& sCap, const CString& sValue); /** Called for every CAP accepted or rejected by server * (with CAP ACK or CAP NAK after our CAP REQ). + * If you need to also advertise the cap to clients, use + * AddServerDependentCapability() instead. * @param sCap capability accepted/rejected by server. * @param bSuccess true if capability was accepted, false if rejected. */ @@ -1283,8 +1312,32 @@ class CModule { virtual EModRet OnUnknownUserRaw(CClient* pClient, CString& sLine); virtual EModRet OnUnknownUserRawMessage(CMessage& Message); + /** Called after login, and also during JumpNetwork. */ + virtual void OnClientAttached(); + /** Called upon disconnect, and also during JumpNetwork. */ + virtual void OnClientDetached(); + +#ifndef SWIG + /** Simple API to support client capabilities which depend on server to support that capability. + * It is built on top of other CAP related API, but removes boilerplate, + * and handles some tricky cases related to cap-notify and JumpNetwork. To + * use, create a subclass of CCapability, and pass to this function; it + * will automatically set the module pointer, then call the callbacks to + * notify you when server and client accepted support of the capability, or + * stopped supporting it. Note that it's not a strict toggle: e.g. + * sometimes client will disable the cap even when it was already disabled + * for that client. + * For perl and python modules, this function accepts 3 parameters: + * name, server callback, client callback; signatures of the callbacks are + * the same as of the virtual functions you'd implement in C++. + */ + void AddServerDependentCapability(const CString& sName, std::unique_ptr pCap); +#endif + /** Called when a client told us CAP LS. Use ssCaps.insert("cap-name") * for announcing capabilities which your module supports. + * If you need to adverite the cap to clients only when it's also supported + * by the server, use AddServerDependentCapability() instead. * @param pClient The client which requested the list. * @param ssCaps set of caps which will be sent to client. */ @@ -1298,6 +1351,8 @@ class CModule { virtual bool IsClientCapSupported(CClient* pClient, const CString& sCap, bool bState); /** Called when we actually need to turn a capability on or off for a client. + * If you need to adverite the cap to clients only when it's also supported + * by the server, use AddServerDependentCapability() instead. * If implementing a custom capability, make sure to call * pClient->SetTagSupport("tag-name", bState) for each tag that the * capability provides. @@ -1359,6 +1414,26 @@ class CModule { const CString& sContext = "") const; #endif + // Default implementations of several callbacks to make + // AddServerDependentCapability work in modpython/modperl. + // Don't worry about existence of these functions. + bool InternalServerDependentCapsOnServerCap302Available( + const CString& sCap, const CString& sValue); + void InternalServerDependentCapsOnServerCapResult(const CString& sCap, + bool bSuccess); + void InternalServerDependentCapsOnClientCapLs(CClient* pClient, + SCString& ssCaps); + bool InternalServerDependentCapsIsClientCapSupported(CClient* pClient, + const CString& sCap, + bool bState); + void InternalServerDependentCapsOnClientCapRequest(CClient* pClient, + const CString& sCap, + bool bState); + void InternalServerDependentCapsOnClientAttached(); + void InternalServerDependentCapsOnClientDetached(); + void InternalServerDependentCapsOnIRCConnected(); + void InternalServerDependentCapsOnIRCDisconnected(); + protected: CModInfo::EModuleType m_eType; CString m_sDescription; @@ -1378,6 +1453,7 @@ class CModule { CString m_sArgs; CString m_sModPath; CTranslationDomainRefHolder m_Translation; + std::map> m_mServerDependentCaps; private: MCString @@ -1541,8 +1617,10 @@ class CModules : public std::vector, private CCoreTranslationMixin { bool OnSendToClientMessage(CMessage& Message); bool OnSendToIRC(CString& sLine); bool OnSendToIRCMessage(CMessage& Message); + bool OnClientAttached(); + bool OnClientDetached(); - bool OnServerCapAvailable(const CString& sCap); + bool OnServerCapAvailable(const CString& sCap, const CString& sValue); bool OnServerCapResult(const CString& sCap, bool bSuccess); CModule* FindModule(const CString& sModule) const; diff --git a/modules/corecaps.cpp b/modules/corecaps.cpp new file mode 100644 index 00000000..7b7dce59 --- /dev/null +++ b/modules/corecaps.cpp @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2004-2024 ZNC, see the NOTICE file for details. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include + +class CCoreCaps : public CModule { + // Note: for historical reasons CClient and CIRCSock have such fields, but + // really they should not. + // TODO: move these fields and their handling from core to this module. + class AwayNotify : public CCapability { + void OnServerChangedSupport(CIRCNetwork* pNetwork, + bool bState) override { + pNetwork->GetIRCSock()->m_bAwayNotify = bState; + } + void OnClientChangedSupport(CClient* pClient, bool bState) override { + pClient->m_bAwayNotify = bState; + } + }; + + class AccountNotify : public CCapability { + void OnServerChangedSupport(CIRCNetwork* pNetwork, + bool bState) override { + pNetwork->GetIRCSock()->m_bAccountNotify = bState; + } + void OnClientChangedSupport(CClient* pClient, bool bState) override { + pClient->m_bAccountNotify = bState; + } + }; + + class AccountTag : public CCapability { + void OnServerChangedSupport(CIRCNetwork* pNetwork, + bool bState) override { + pNetwork->GetIRCSock()->m_bAccountTag = bState; + } + void OnClientChangedSupport(CClient* pClient, bool bState) override { + pClient->m_bAccountTag = bState; + pClient->SetTagSupport("account", bState); + } + }; + + class ExtendedJoin : public CCapability { + void OnServerChangedSupport(CIRCNetwork* pNetwork, + bool bState) override { + pNetwork->GetIRCSock()->m_bExtendedJoin = bState; + } + void OnClientChangedSupport(CClient* pClient, bool bState) override { + pClient->m_bExtendedJoin = bState; + } + }; + + public: + MODCONSTRUCTOR(CCoreCaps) { + AddServerDependentCapability("away-notify", std::make_unique()); + AddServerDependentCapability("account-notify", std::make_unique()); + AddServerDependentCapability("account-tag", std::make_unique()); + AddServerDependentCapability("extended-join", std::make_unique()); + } +}; + +GLOBALMODULEDEFS( + CCoreCaps, + t_s("Adds support for several IRC capabilities, extracted from ZNC core.")) diff --git a/modules/modperl.cpp b/modules/modperl.cpp index 05878749..1a39b5a6 100644 --- a/modules/modperl.cpp +++ b/modules/modperl.cpp @@ -374,6 +374,41 @@ CPerlSocket::~CPerlSocket() { } } +CPerlCapability::~CPerlCapability() { + SvREFCNT_dec(m_serverCb); + SvREFCNT_dec(m_clientCb); +} + +void CPerlCapability::OnServerChangedSupport(CIRCNetwork* pNetwork, bool bState) { + PSTART; + PUSH_PTR(CIRCNetwork*, pNetwork); + mXPUSHi(bState); + PUTBACK; + ret = call_sv(m_serverCb, G_EVAL | G_ARRAY); + SPAGAIN; + SP -= ret; + ax = (SP - PL_stack_base) + 1; + if (SvTRUE(ERRSV)) { + DEBUG("Perl hook OnServerChangedSupport died with: " + PString(ERRSV)); + } + PEND; +} + +void CPerlCapability::OnClientChangedSupport(CClient* pClient, bool bState) { + PSTART; + PUSH_PTR(CClient*, pClient); + mXPUSHi(bState); + PUTBACK; + ret = call_sv(m_clientCb, G_EVAL | G_ARRAY); + SPAGAIN; + SP -= ret; + ax = (SP - PL_stack_base) + 1; + if (SvTRUE(ERRSV)) { + DEBUG("Perl hook OnServerChangedSupport died with: " + PString(ERRSV)); + } + PEND; +} + template <> void TModInfo(CModInfo& Info) { Info.SetWikiPage("modperl"); diff --git a/modules/modperl/functions.in b/modules/modperl/functions.in index c4f843e9..87d4c867 100644 --- a/modules/modperl/functions.in +++ b/modules/modperl/functions.in @@ -61,7 +61,10 @@ EModRet OnPrivNotice(CNick& Nick, CString& sMessage) EModRet OnChanNotice(CNick& Nick, CChan& Channel, CString& sMessage) EModRet OnTopic(CNick& Nick, CChan& Channel, CString& sTopic) bool OnServerCapAvailable(const CString& sCap) +bool OnServerCap302Available(const CString& sCap, const CString& sValue) void OnServerCapResult(const CString& sCap, bool bSuccess) +void OnClientAttached() +void OnClientDetached() EModRet OnTimerAutoJoin(CChan& Channel) bool OnEmbeddedWebRequest(CWebSock& WebSock, const CString& sPageName, CTemplate& Tmpl) EModRet OnAddNetwork(CIRCNetwork& Network, CString& sErrorRet) diff --git a/modules/modperl/modperl.i b/modules/modperl/modperl.i index 0d7583bd..a0d6dd99 100644 --- a/modules/modperl/modperl.i +++ b/modules/modperl/modperl.i @@ -189,6 +189,10 @@ class MCString : public std::map {}; bool ExistsNV(const CString& sName) { return $self->EndNV() != $self->FindNV(sName); } + void AddServerDependentCapability(const CString& sName, SV* serverCb, + SV* clientCb) { + $self->AddServerDependentCapability(sName, std::make_unique(serverCb, clientCb)); + } } %extend CModules { diff --git a/modules/modperl/module.h b/modules/modperl/module.h index 3b31c915..2c2e5c06 100644 --- a/modules/modperl/module.h +++ b/modules/modperl/module.h @@ -116,7 +116,10 @@ class ZNC_EXPORT_LIB_EXPORT CPerlModule : public CModule { CString& sMessage) override; EModRet OnTopic(CNick& Nick, CChan& Channel, CString& sTopic) override; bool OnServerCapAvailable(const CString& sCap) override; + bool OnServerCap302Available(const CString& sCap, const CString& sValue) override; void OnServerCapResult(const CString& sCap, bool bSuccess) override; + void OnClientAttached() override; + void OnClientDetached() override; EModRet OnTimerAutoJoin(CChan& Channel) override; bool OnEmbeddedWebRequest(CWebSock&, const CString&, CTemplate&) override; EModRet OnAddNetwork(CIRCNetwork& Network, CString& sErrorRet) override; @@ -213,6 +216,20 @@ inline CPerlSocket* CreatePerlSocket(CPerlModule* pModule, SV* perlObj) { return new CPerlSocket(pModule, perlObj); } +class ZNC_EXPORT_LIB_EXPORT CPerlCapability : public CCapability { + public: + CPerlCapability(SV* serverCb, SV* clientCb) + : m_serverCb(newSVsv(serverCb)), m_clientCb(newSVsv(clientCb)) {} + ~CPerlCapability(); + + void OnServerChangedSupport(CIRCNetwork* pNetwork, bool bState) override; + void OnClientChangedSupport(CClient* pClient, bool bState) override; + + private: + SV* m_serverCb; + SV* m_clientCb; +}; + inline bool HaveIPv6() { #ifdef HAVE_IPV6 return true; diff --git a/modules/modperl/startup.pl b/modules/modperl/startup.pl index 22b05382..e32cbe50 100644 --- a/modules/modperl/startup.pl +++ b/modules/modperl/startup.pl @@ -405,6 +405,9 @@ sub OnPrivNotice {} sub OnChanNotice {} sub OnTopic {} sub OnServerCapAvailable {} +sub OnServerCap302Available { my ($self, $cap, $value) = @_; $self->OnServerCapAvailable($cap) } +sub OnClientAttached {} +sub OnClientDetached {} sub OnServerCapResult {} sub OnTimerAutoJoin {} sub OnEmbeddedWebRequest {} diff --git a/modules/modpython.cpp b/modules/modpython.cpp index ec2a1186..630cfef3 100644 --- a/modules/modpython.cpp +++ b/modules/modpython.cpp @@ -478,6 +478,49 @@ CPySocket::~CPySocket() { Py_CLEAR(m_pyObj); } +CPyCapability::CPyCapability(PyObject* serverCb, PyObject* clientCb) + : m_serverCb(serverCb), m_clientCb(clientCb) { + Py_INCREF(serverCb); + Py_INCREF(clientCb); +} + +CPyCapability::~CPyCapability() { + Py_CLEAR(m_serverCb); + Py_CLEAR(m_clientCb); +} + +void CPyCapability::OnServerChangedSupport(CIRCNetwork* pNetwork, bool bState) { + PyObject* pyArg_Network = + SWIG_NewInstanceObj(pNetwork, SWIG_TypeQuery("CIRCNetwork*"), 0); + PyObject* pyArg_bState = Py_BuildValue("l", (long int)bState); + PyObject* pyRes = PyObject_CallFunctionObjArgs(m_serverCb, pyArg_Network, + pyArg_bState, nullptr); + if (!pyRes) { + CString sPyErr = ((CPyModule*)GetModule())->GetPyExceptionStr(); + DEBUG("modpython: " << GetModule()->GetModName() + << "/OnServerChangedSupport failed: " << sPyErr); + } + Py_CLEAR(pyRes); + Py_CLEAR(pyArg_bState); + Py_CLEAR(pyArg_Network); +} + +void CPyCapability::OnClientChangedSupport(CClient* pClient, bool bState) { + PyObject* pyArg_Client = + SWIG_NewInstanceObj(pClient, SWIG_TypeQuery("CClient*"), 0); + PyObject* pyArg_bState = Py_BuildValue("l", (long int)bState); + PyObject* pyRes = PyObject_CallFunctionObjArgs(m_clientCb, pyArg_Client, + pyArg_bState, nullptr); + if (!pyRes) { + CString sPyErr = ((CPyModule*)GetModule())->GetPyExceptionStr(); + DEBUG("modpython: " << GetModule()->GetModName() + << "/OnClientChangedSupport failed: " << sPyErr); + } + Py_CLEAR(pyRes); + Py_CLEAR(pyArg_bState); + Py_CLEAR(pyArg_Client); +} + CPyModule* CPyModCommand::GetModule() { return this->m_pModule; } diff --git a/modules/modpython/functions.in b/modules/modpython/functions.in index f779e577..e66ce0db 100644 --- a/modules/modpython/functions.in +++ b/modules/modpython/functions.in @@ -61,7 +61,10 @@ EModRet OnPrivNotice(CNick& Nick, CString& sMessage) EModRet OnChanNotice(CNick& Nick, CChan& Channel, CString& sMessage) EModRet OnTopic(CNick& Nick, CChan& Channel, CString& sTopic) bool OnServerCapAvailable(const CString& sCap) +bool OnServerCap302Available(const CString& sCap, const CString& sValue) void OnServerCapResult(const CString& sCap, bool bSuccess) +void OnClientAttached() +void OnClientDetached() EModRet OnTimerAutoJoin(CChan& Channel) bool OnEmbeddedWebRequest(CWebSock& WebSock, const CString& sPageName, CTemplate& Tmpl) EModRet OnAddNetwork(CIRCNetwork& Network, CString& sErrorRet) diff --git a/modules/modpython/modpython.i b/modules/modpython/modpython.i index 65604da2..38db2a92 100644 --- a/modules/modpython/modpython.i +++ b/modules/modpython/modpython.i @@ -241,6 +241,10 @@ class CPyRetBool { bool ExistsNV(const CString& sName) { return $self->EndNV() != $self->FindNV(sName); } + void AddServerDependentCapability(const CString& sName, PyObject* serverCb, + PyObject* clientCb) { + $self->AddServerDependentCapability(sName, std::make_unique(serverCb, clientCb)); + } } %extend CModules { diff --git a/modules/modpython/module.h b/modules/modpython/module.h index 36807a7c..333aa3ee 100644 --- a/modules/modpython/module.h +++ b/modules/modpython/module.h @@ -136,7 +136,10 @@ class ZNC_EXPORT_LIB_EXPORT CPyModule : public CModule { CString& sMessage) override; EModRet OnTopic(CNick& Nick, CChan& Channel, CString& sTopic) override; bool OnServerCapAvailable(const CString& sCap) override; + bool OnServerCap302Available(const CString& sCap, const CString& sValue) override; void OnServerCapResult(const CString& sCap, bool bSuccess) override; + void OnClientAttached() override; + void OnClientDetached() override; EModRet OnTimerAutoJoin(CChan& Channel) override; bool OnEmbeddedWebRequest(CWebSock&, const CString&, CTemplate&) override; EModRet OnAddNetwork(CIRCNetwork& Network, CString& sErrorRet) override; @@ -366,3 +369,17 @@ inline CPyModCommand* CreatePyModCommand(CPyModule* pModule, PyObject* pyObj) { return new CPyModCommand(pModule, sCmd, sArgs, sDesc, pyObj); } + +class ZNC_EXPORT_LIB_EXPORT CPyCapability : public CCapability { + public: + CPyCapability(PyObject* serverCb, PyObject* clientCb); + ~CPyCapability(); + + void OnServerChangedSupport(CIRCNetwork* pNetwork, bool bState) override; + void OnClientChangedSupport(CClient* pClient, bool bState) override; + + private: + PyObject* m_serverCb; + PyObject* m_clientCb; +}; + diff --git a/modules/modpython/znc.py b/modules/modpython/znc.py index 7b9767f6..ce4f2743 100644 --- a/modules/modpython/znc.py +++ b/modules/modpython/znc.py @@ -410,6 +410,15 @@ class Module: def OnServerCapAvailable(self, sCap): pass + def OnServerCap302Available(self, sCap, sValue): + return self.OnServerCapAvailable(sCap) + + def OnClientAttached(self): + pass + + def OnClientDetached(self): + pass + def OnServerCapResult(self, sCap, bSuccess): pass diff --git a/src/Client.cpp b/src/Client.cpp index 976397e6..ac2dd93f 100644 --- a/src/Client.cpp +++ b/src/Client.cpp @@ -75,6 +75,41 @@ using std::vector; } \ } +CClient::CClient() : CIRCSocket(), + m_bGotPass(false), + m_bGotNick(false), + m_bGotUser(false), + m_uCapVersion(0), + m_bInCap(false), + m_bCapNotify(false), + m_bAwayNotify(false), + m_bAccountNotify(false), + m_bAccountTag(false), + m_bExtendedJoin(false), + m_bNamesx(false), + m_bUHNames(false), + m_bAway(false), + m_bServerTime(false), + m_bBatch(false), + m_bEchoMessage(false), + m_bSelfMessage(false), + m_bPlaybackActive(false), + m_pUser(nullptr), + m_pNetwork(nullptr), + m_sNick("unknown-nick"), + m_sPass(""), + m_sUser(""), + m_sNetwork(""), + m_sIdentifier(""), + m_spAuth(), + m_ssAcceptedCaps(), + m_ssSupportedTags() { + EnableReadLine(); + // RFC says a line can have 512 chars max, but we are + // a little more gentle ;) + SetMaxBufferThreshold(1024); +} + CClient::~CClient() { if (m_spAuth) { CClientAuth* pAuth = (CClientAuth*)&(*m_spAuth); @@ -236,7 +271,7 @@ void CClient::SetNetwork(CIRCNetwork* pNetwork, bool bDisconnect, m_pNetwork->ClientDisconnected(this); if (bDisconnect) { - ClearServerDependentCaps(); + NETWORKMODULECALL(OnClientDetached(), m_pUser, m_pNetwork, this, NOTHING); // Tell the client they are no longer in these channels. const vector& vChans = m_pNetwork->GetChans(); for (const CChan* pChan : vChans) { @@ -258,6 +293,7 @@ void CClient::SetNetwork(CIRCNetwork* pNetwork, bool bDisconnect, } else if (m_pUser) { m_pUser->UserConnected(this); } + NETWORKMODULECALL(OnClientAttached(), m_pUser, m_pNetwork, this, NOTHING); } } @@ -691,24 +727,77 @@ void CClient::RespondCap(const CString& sResponse) { PutClient(":irc.znc.in CAP " + GetNick() + " " + sResponse); } +static VCString MultiLine(const SCString& ssCaps) { + VCString vsRes = {""}; + for (const CString& sCap : ssCaps) { + if (vsRes.back().length() + sCap.length() > 400) { + vsRes.push_back(sCap); + } else { + if (!vsRes.back().empty()) { + vsRes.back() += " "; + } + vsRes.back() += sCap; + } + } + return vsRes; +} + +const std::map>& +CClient::CoreCaps() { + static const std::map> mCoreCaps = []{ + std::map> mCoreCaps = { + {"multi-prefix", + [](CClient* pClient, bool bVal) { pClient->m_bNamesx = bVal; }}, + {"userhost-in-names", + [](CClient* pClient, bool bVal) { pClient->m_bUHNames = bVal; }}, + {"echo-message", + [](CClient* pClient, bool bVal) { pClient->m_bEchoMessage = bVal; }}, + {"server-time", + [](CClient* pClient, bool bVal) { + pClient->m_bServerTime = bVal; + pClient->SetTagSupport("time", bVal); + }}, + {"batch", [](CClient* pClient, bool bVal) { + pClient->m_bBatch = bVal; + pClient->SetTagSupport("batch", bVal); + }}, + {"cap-notify", + [](CClient* pClient, bool bVal) { pClient->m_bCapNotify = bVal; }}, + }; + + // 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; +} + void CClient::HandleCap(const CMessage& Message) { CString sSubCmd = Message.GetParam(0); if (sSubCmd.Equals("LS")) { + m_uCapVersion = std::max(m_uCapVersion, Message.GetParam(1).ToUShort()); SCString ssOfferCaps; - for (const auto& it : m_mCoreCaps) { - bool bServerDependent = std::get<0>(it.second); - if (!bServerDependent || - m_ssServerDependentCaps.count(it.first) > 0) - ssOfferCaps.insert(it.first); + for (const auto& it : CoreCaps()) { + ssOfferCaps.insert(it.first); } - GLOBALMODULECALL(OnClientCapLs(this, ssOfferCaps), NOTHING); - CString sRes = - CString(" ").Join(ssOfferCaps.begin(), ssOfferCaps.end()); - RespondCap("LS :" + sRes); + NETWORKMODULECALL(OnClientCapLs(this, ssOfferCaps), GetUser(), GetNetwork(), this, NOTHING); + VCString vsCaps = MultiLine(ssOfferCaps); m_bInCap = true; - if (Message.GetParam(1).ToInt() >= 302) { + if (HasCap302()) { m_bCapNotify = true; + for (int i = 0; i < vsCaps.size() - 1; ++i) { + RespondCap("LS * :" + vsCaps[i]); + } + RespondCap("LS :" + vsCaps.back()); + } else { + // Can't send more than one line of caps :( + RespondCap("LS :" + vsCaps.front()); } } else if (sSubCmd.Equals("END")) { m_bInCap = false; @@ -729,13 +818,11 @@ void CClient::HandleCap(const CMessage& Message) { if (sCap.TrimPrefix("-")) bVal = false; bool bAccepted = false; - const auto& it = m_mCoreCaps.find(sCap); - if (m_mCoreCaps.end() != it) { - bool bServerDependent = std::get<0>(it->second); - bAccepted = !bServerDependent || - m_ssServerDependentCaps.count(sCap) > 0; + auto it = CoreCaps().find(sCap); + if (CoreCaps().end() != it) { + bAccepted = true; } - GLOBALMODULECALL(IsClientCapSupported(this, sCap, bVal), + NETWORKMODULECALL(IsClientCapSupported(this, sCap, bVal), GetUser(), GetNetwork(), this, &bAccepted); if (!bAccepted) { @@ -751,12 +838,12 @@ void CClient::HandleCap(const CMessage& Message) { CString sCap = sToken; if (sCap.TrimPrefix("-")) bVal = false; - auto handler_it = m_mCoreCaps.find(sCap); - if (m_mCoreCaps.end() != handler_it) { - const auto& handler = std::get<1>(handler_it->second); - handler(bVal); + auto handler_it = CoreCaps().find(sCap); + if (CoreCaps().end() != handler_it) { + const auto& handler = handler_it->second; + handler(this, bVal); } - GLOBALMODULECALL(OnClientCapRequest(this, sCap, bVal), NOTHING); + NETWORKMODULECALL(OnClientCapRequest(this, sCap, bVal), GetUser(), GetNetwork(), this, NOTHING); if (bVal) { m_ssAcceptedCaps.insert(sCap); @@ -767,9 +854,16 @@ void CClient::HandleCap(const CMessage& Message) { RespondCap("ACK :" + Message.GetParam(1)); } else if (sSubCmd.Equals("LIST")) { - CString sList = - CString(" ").Join(m_ssAcceptedCaps.begin(), m_ssAcceptedCaps.end()); - RespondCap("LIST :" + sList); + VCString vsCaps = MultiLine(m_ssAcceptedCaps); + if (HasCap302()) { + for (int i = 0; i < vsCaps.size() - 1; ++i) { + RespondCap("LIST * :" + vsCaps[i]); + } + RespondCap("LIST :" + vsCaps.back()); + } else { + // Can't send more than one line of caps :( + RespondCap("LISTS :" + vsCaps.front()); + } } else { PutClient(":irc.znc.in 410 " + GetNick() + " " + sSubCmd + " :Invalid CAP subcommand"); @@ -828,41 +922,21 @@ void CClient::SetTagSupport(const CString& sTag, bool bState) { } } -void CClient::NotifyServerDependentCaps(const SCString& ssCaps) { - for (const CString& sCap : ssCaps) { - const auto& it = m_mCoreCaps.find(sCap); - if (m_mCoreCaps.end() != it) { - bool bServerDependent = std::get<0>(it->second); - if (bServerDependent) { - m_ssServerDependentCaps.insert(sCap); +void CClient::NotifyServerDependentCap(const CString& sCap, bool bValue, const CString& sValue) { + if (bValue) { + if (HasCapNotify()) { + if (HasCap302() && !sValue.empty()) { + PutClient(":irc.znc.in CAP " + GetNick() + " NEW :" + sCap + "=" + sValue); + } else { + PutClient(":irc.znc.in CAP " + GetNick() + " NEW :" + sCap); } } - } - - if (HasCapNotify() && !m_ssServerDependentCaps.empty()) { - CString sCaps = CString(" ").Join(m_ssServerDependentCaps.begin(), - m_ssServerDependentCaps.end()); - PutClient(":irc.znc.in CAP " + GetNick() + " NEW :" + sCaps); - } -} - -void CClient::ClearServerDependentCaps() { - if (HasCapNotify() && !m_ssServerDependentCaps.empty()) { - CString sCaps = CString(" ").Join(m_ssServerDependentCaps.begin(), - m_ssServerDependentCaps.end()); - PutClient(":irc.znc.in CAP " + GetNick() + " DEL :" + sCaps); - - for (const CString& sCap : m_ssServerDependentCaps) { - const auto& it = m_mCoreCaps.find(sCap); - if (m_mCoreCaps.end() != it) { - const auto& handler = std::get<1>(it->second); - handler(false); - } - m_ssAcceptedCaps.erase(sCap); + } else { + if (HasCapNotify()) { + PutClient(":irc.znc.in CAP " + GetNick() + " DEL :" + sCap); } + m_ssAcceptedCaps.erase(sCap); } - - m_ssServerDependentCaps.clear(); } template diff --git a/src/IRCNetwork.cpp b/src/IRCNetwork.cpp index 567e14f1..e46df7d0 100644 --- a/src/IRCNetwork.cpp +++ b/src/IRCNetwork.cpp @@ -660,10 +660,6 @@ void CIRCNetwork::ClientConnected(CClient* pClient) { size_t uIdx, uSize; - if (m_pIRCSock) { - pClient->NotifyServerDependentCaps(m_pIRCSock->GetAcceptedCaps()); - } - pClient->SetPlaybackActive(true); if (m_RawBuffer.IsEmpty()) { @@ -1393,10 +1389,6 @@ bool CIRCNetwork::IsIRCConnected() const { void CIRCNetwork::SetIRCSocket(CIRCSock* pIRCSock) { m_pIRCSock = pIRCSock; } void CIRCNetwork::IRCConnected() { - const SCString& ssCaps = m_pIRCSock->GetAcceptedCaps(); - for (CClient* pClient : m_vClients) { - pClient->NotifyServerDependentCaps(ssCaps); - } if (m_uJoinDelay > 0) { m_pJoinTimer->Delay(m_uJoinDelay); } else { @@ -1405,9 +1397,6 @@ void CIRCNetwork::IRCConnected() { } void CIRCNetwork::IRCDisconnected() { - for (CClient* pClient : m_vClients) { - pClient->ClearServerDependentCaps(); - } m_pIRCSock = nullptr; SetIRCServer(""); @@ -1417,6 +1406,17 @@ void CIRCNetwork::IRCDisconnected() { CheckIRCConnect(); } +void CIRCNetwork::NotifyClientsAboutServerDependentCap(const CString& sCap, bool bValue) { + CString sValue = GetIRCSock() ? GetIRCSock()->GetCapLsValue(sCap) : ""; + for (CClient* pClient : m_vClients) { + pClient->NotifyServerDependentCap(sCap, bValue, sValue); + } +} + +bool CIRCNetwork::IsServerCapAccepted(const CString& sCap) const { + return m_pIRCSock && m_pIRCSock->IsCapAccepted(sCap); +} + void CIRCNetwork::SetIRCConnectEnabled(bool b) { m_bIRCConnectEnabled = b; diff --git a/src/IRCSock.cpp b/src/IRCSock.cpp index 00cc997e..e3473707 100644 --- a/src/IRCSock.cpp +++ b/src/IRCSock.cpp @@ -246,7 +246,9 @@ void CIRCSock::SendNextCap() { if (!m_uCapPaused) { if (m_ssPendingCaps.empty()) { // We already got all needed ACK/NAK replies. - PutIRC("CAP END"); + if (!m_bAuthed) { + PutIRC("CAP END"); + } } else { CString sCap = *m_ssPendingCaps.begin(); m_ssPendingCaps.erase(m_ssPendingCaps.begin()); @@ -262,9 +264,9 @@ void CIRCSock::ResumeCap() { SendNextCap(); } -bool CIRCSock::OnServerCapAvailable(const CString& sCap) { +bool CIRCSock::OnServerCapAvailable(const CString& sCap, const CString& sValue) { bool bResult = false; - IRCSOCKMODULECALL(OnServerCapAvailable(sCap), &bResult); + IRCSOCKMODULECALL(OnServerCapAvailable(sCap, sValue), &bResult); return bResult; } @@ -347,67 +349,94 @@ bool CIRCSock::OnAwayMessage(CMessage& Message) { } bool CIRCSock::OnCapabilityMessage(CMessage& Message) { - // CAPs are supported only before authorization. - if (!m_bAuthed) { - // The first parameter is most likely "*". No idea why, the - // CAP spec don't mention this, but all implementations - // I've seen add this extra asterisk - CString sSubCmd = Message.GetParam(1); + // The first parameter is most likely "*". No idea why, the + // CAP spec don't mention this, but all implementations + // I've seen add this extra asterisk + CString sSubCmd = Message.GetParam(1); - // If the caplist of a reply is too long, it's split - // into multiple replies. A "*" is prepended to show - // that the list was split into multiple replies. - // This is useful mainly for LS. For ACK and NAK - // replies, there's no real need for this, because - // we request only 1 capability per line. - // If we will need to support broken servers or will - // send several requests per line, need to delay ACK - // actions until all ACK lines are received and - // to recognize past request of NAK by 100 chars - // of this reply. - CString sArgs; - if (Message.GetParam(2) == "*") { - sArgs = Message.GetParam(3); - } else { - sArgs = Message.GetParam(2); + // If the caplist of a reply is too long, it's split + // into multiple replies. A "*" is prepended to show + // that the list was split into multiple replies. + // This is useful mainly for LS. For ACK and NAK + // replies, there's no real need for this, because + // we request only 1 capability per line. + // If we will need to support broken servers or will + // send several requests per line, need to delay ACK + // actions until all ACK lines are received and + // to recognize past request of NAK by 100 chars + // of this reply. + // As for LS, we shouldn't don't send END after receiving first line, + // because interesting caps can be on next line. + CString sArgs; + bool bSendNext = true; + if (Message.GetParam(2) == "*") { + bSendNext = false; + sArgs = Message.GetParam(3); + } else { + sArgs = Message.GetParam(2); + } + + std::map> mSupportedCaps = { + {"multi-prefix", [this](bool bVal) { m_bNamesx = bVal; }}, + {"userhost-in-names", [this](bool bVal) { m_bUHNames = bVal; }}, + {"cap-notify", [](bool bVal) {}}, + {"server-time", [this](bool bVal) { m_bServerTime = bVal; }}, + {"znc.in/server-time-iso", + [this](bool bVal) { m_bServerTime = bVal; }}, + }; + + auto RemoveCap = [&](const CString& sCap) { + IRCSOCKMODULECALL(OnServerCapResult(sCap, false), NOTHING); + auto it = mSupportedCaps.find(sCap); + if (it != mSupportedCaps.end()) { + it->second(false); } + m_ssAcceptedCaps.erase(sCap); + m_ssPendingCaps.erase(sCap); + }; - std::map> mSupportedCaps = { - {"multi-prefix", [this](bool bVal) { m_bNamesx = bVal; }}, - {"userhost-in-names", [this](bool bVal) { m_bUHNames = bVal; }}, - {"away-notify", [this](bool bVal) { m_bAwayNotify = bVal; }}, - {"account-notify", [this](bool bVal) { m_bAccountNotify = bVal; }}, - {"account-tag", [this](bool bVal) { m_bAccountTag = bVal; }}, - {"extended-join", [this](bool bVal) { m_bExtendedJoin = bVal; }}, - {"server-time", [this](bool bVal) { m_bServerTime = bVal; }}, - {"znc.in/server-time-iso", - [this](bool bVal) { m_bServerTime = bVal; }}, - }; + if (sSubCmd == "LS" || sSubCmd == "NEW") { + VCString vsTokens; + sArgs.Split(" ", vsTokens, false); - if (sSubCmd == "LS") { - VCString vsTokens; - sArgs.Split(" ", vsTokens, false); - - for (const CString& sCap : vsTokens) { - if (OnServerCapAvailable(sCap) || mSupportedCaps.count(sCap)) { - m_ssPendingCaps.insert(sCap); - } + for (const CString& sToken : vsTokens) { + CString sCap, sValue; + int eq = sToken.find('='); + if (eq == std::string::npos) { + sCap = sToken; + } else { + sCap = sToken.substr(0, eq); + sValue = sToken.substr(eq + 1); } - } else if (sSubCmd == "ACK") { - sArgs.Trim(); - IRCSOCKMODULECALL(OnServerCapResult(sArgs, true), NOTHING); - const auto& it = mSupportedCaps.find(sArgs); - if (it != mSupportedCaps.end()) { - it->second(true); + m_msCapLsValues[sCap] = sValue; + if (OnServerCapAvailable(sCap, sValue) || mSupportedCaps.count(sCap)) { + m_ssPendingCaps.insert(sCap); } - m_ssAcceptedCaps.insert(sArgs); - } else if (sSubCmd == "NAK") { - // This should work because there's no [known] - // capability with length of name more than 100 characters. - sArgs.Trim(); - IRCSOCKMODULECALL(OnServerCapResult(sArgs, false), NOTHING); } + } else if (sSubCmd == "ACK") { + sArgs.Trim(); + IRCSOCKMODULECALL(OnServerCapResult(sArgs, true), NOTHING); + auto it = mSupportedCaps.find(sArgs); + if (it != mSupportedCaps.end()) { + it->second(true); + } + m_ssAcceptedCaps.insert(sArgs); + } else if (sSubCmd == "NAK") { + // This should work because there's no [known] + // capability with length of name more than 100 characters. + sArgs.Trim(); + RemoveCap(sArgs); + } else if (sSubCmd == "DEL") { + VCString vsTokens; + sArgs.Split(" ", vsTokens, false); + for (const CString& sCap : vsTokens) { + RemoveCap(sCap); + m_msCapLsValues.erase(sCap); + } + } + + if (bSendNext) { SendNextCap(); } // Don't forward any CAP stuff to the client @@ -1222,7 +1251,7 @@ void CIRCSock::Connected() { &bReturn); if (bReturn) return; - PutIRC("CAP LS"); + PutIRC("CAP LS 302"); if (!sPass.empty()) { PutIRC("PASS " + sPass); @@ -1411,6 +1440,16 @@ CString CIRCSock::GetISupport(const CString& sKey, } } +CString CIRCSock::GetCapLsValue(const CString& sKey, + const CString& sDefault) const { + MCString::const_iterator i = m_msCapLsValues.find(sKey); + if (i == m_msCapLsValues.end()) { + return sDefault; + } else { + return i->second; + } +} + void CIRCSock::SendAltNick(const CString& sBadNick) { const CString& sLastNick = m_Nick.GetNick(); diff --git a/src/Modules.cpp b/src/Modules.cpp index e3835c72..badbfd72 100644 --- a/src/Modules.cpp +++ b/src/Modules.cpp @@ -14,6 +14,7 @@ * limitations under the License. */ +#include #include #include #include @@ -160,6 +161,31 @@ CModule::CModule(ModHandle pDLL, CUser* pUser, CIRCNetwork* pNetwork, } CModule::~CModule() { + for (const auto& [sName, pCap] : m_mServerDependentCaps) { + // pCap->OnClientChangedSupport is useless (and even dangerous) to call + // from the destructor, since the derived CModule class is gone already. + // But still need to tell clients via cap-notify that the cap is gone. + switch (GetType()) { + case CModInfo::NetworkModule: + GetNetwork()->NotifyClientsAboutServerDependentCap(sName, + false); + break; + case CModInfo::UserModule: + for (CIRCNetwork* pNetwork : GetUser()->GetNetworks()) { + pNetwork->NotifyClientsAboutServerDependentCap(sName, + false); + } + break; + case CModInfo::GlobalModule: + for (auto& [_, pUser] : CZNC::Get().GetUserMap()) { + for (CIRCNetwork* pNetwork : pUser->GetNetworks()) { + pNetwork->NotifyClientsAboutServerDependentCap(sName, + false); + } + } + } + } + while (!m_sTimers.empty()) { RemTimer(*m_sTimers.begin()); } @@ -606,7 +632,24 @@ bool CModule::OnBoot() { return true; } void CModule::OnPreRehash() {} void CModule::OnPostRehash() {} void CModule::OnIRCDisconnected() {} +void CModule::InternalServerDependentCapsOnIRCDisconnected() { + OnIRCDisconnected(); + for (const auto& [sName, pCap] : m_mServerDependentCaps) { + GetNetwork()->NotifyClientsAboutServerDependentCap(sName, false); + for (CClient* pClient : GetNetwork()->GetClients()) { + pCap->OnClientChangedSupport(pClient, false); + } + } +} void CModule::OnIRCConnected() {} +void CModule::InternalServerDependentCapsOnIRCConnected() { + OnIRCConnected(); + for (const auto& [sName, pCap] : m_mServerDependentCaps) { + if (GetNetwork()->IsServerCapAccepted(sName)) { + GetNetwork()->NotifyClientsAboutServerDependentCap(sName, true); + } + } +} CModule::EModRet CModule::OnIRCConnecting(CIRCSock* IRCSock) { return CONTINUE; } @@ -997,9 +1040,59 @@ CModule::EModRet CModule::OnSendToIRC(CString& sLine) { return CONTINUE; } CModule::EModRet CModule::OnSendToIRCMessage(CMessage& Message) { return CONTINUE; } +void CModule::OnClientAttached() {} +void CModule::InternalServerDependentCapsOnClientAttached() { + OnClientAttached(); + if (!GetNetwork()) return; + for (const auto& [sName, pCap] : m_mServerDependentCaps) { + if (GetNetwork()->IsServerCapAccepted(sName)) { + GetClient()->NotifyServerDependentCap(sName, true, GetNetwork()->GetIRCSock()->GetCapLsValue(sName)); + } + } +} +void CModule::OnClientDetached() {} +void CModule::InternalServerDependentCapsOnClientDetached() { + OnClientDetached(); + for (const auto& [sName, pCap] : m_mServerDependentCaps) { + GetClient()->NotifyServerDependentCap(sName, false, ""); + pCap->OnClientChangedSupport(GetClient(), false); + } +} bool CModule::OnServerCapAvailable(const CString& sCap) { return false; } +bool CModule::OnServerCap302Available(const CString& sCap, + const CString& sValue) { + return OnServerCapAvailable(sCap); +} +bool CModule::InternalServerDependentCapsOnServerCap302Available(const CString& sCap, + const CString& sValue) { + auto it = m_mServerDependentCaps.find(sCap); + if (it == m_mServerDependentCaps.end()) + return OnServerCap302Available(sCap, sValue); + if (GetNetwork()->IsServerCapAccepted(sCap)) { + // This can happen when server sent CAP NEW with another value. + GetNetwork()->NotifyClientsAboutServerDependentCap(sCap, true); + // It's enabled already, no need to REQ it again. + return false; + } + return true; +} void CModule::OnServerCapResult(const CString& sCap, bool bSuccess) {} +void CModule::InternalServerDependentCapsOnServerCapResult(const CString& sCap, + bool bSuccess) { + OnServerCapResult(sCap, bSuccess); + auto it = m_mServerDependentCaps.find(sCap); + if (it == m_mServerDependentCaps.end()) return; + it->second->OnServerChangedSupport(GetNetwork(), bSuccess); + if (GetNetwork()->GetIRCSock()->IsAuthed()) { + GetNetwork()->NotifyClientsAboutServerDependentCap(sCap, bSuccess); + if (!bSuccess) { + for (CClient* pClient : GetNetwork()->GetClients()) { + it->second->OnClientChangedSupport(pClient, false); + } + } + } +} bool CModule::PutIRC(const CString& sLine) { return m_pNetwork ? m_pNetwork->PutIRC(sLine) : false; @@ -1069,12 +1162,44 @@ CModule::EModRet CModule::OnUnknownUserRawMessage(CMessage& Message) { return CONTINUE; } void CModule::OnClientCapLs(CClient* pClient, SCString& ssCaps) {} +void CModule::InternalServerDependentCapsOnClientCapLs(CClient* pClient, SCString& ssCaps) { + for (const auto& [sName, pCap] : m_mServerDependentCaps) { + if (GetNetwork() && GetNetwork()->IsServerCapAccepted(sName)) { + if (pClient->HasCap302()) { + CString sValue = + GetNetwork()->GetIRCSock()->GetCapLsValue(sName); + if (!sValue.empty()) { + ssCaps.insert(sName + '=' + sValue); + } else { + ssCaps.insert(sName); + } + } else { + ssCaps.insert(sName); + } + } + } + OnClientCapLs(pClient, ssCaps); +} bool CModule::IsClientCapSupported(CClient* pClient, const CString& sCap, - bool bState) { - return false; + bool bState) { return false; } +bool CModule::InternalServerDependentCapsIsClientCapSupported( + CClient* pClient, const CString& sCap, bool bState) { + auto it = m_mServerDependentCaps.find(sCap); + if (it == m_mServerDependentCaps.end()) + return IsClientCapSupported(pClient, sCap, bState); + if (!bState) return true; + return GetNetwork() && GetNetwork()->IsServerCapAccepted(sCap); } void CModule::OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState) {} +void CModule::InternalServerDependentCapsOnClientCapRequest(CClient* pClient, + const CString& sCap, + bool bState) { + OnClientCapRequest(pClient, sCap, bState); + auto it = m_mServerDependentCaps.find(sCap); + if (it == m_mServerDependentCaps.end()) return; + it->second->OnClientChangedSupport(pClient, bState); +} CModule::EModRet CModule::OnModuleLoading(const CString& sModName, const CString& sArgs, CModInfo::EModuleType eType, @@ -1092,6 +1217,11 @@ CModule::EModRet CModule::OnGetModInfo(CModInfo& ModInfo, } void CModule::OnGetAvailableMods(set& ssMods, CModInfo::EModuleType eType) {} +void CModule::AddServerDependentCapability(const CString& sName, + std::unique_ptr pCap) { + pCap->SetModule(this); + m_mServerDependentCaps[sName] = std::move(pCap); +} CModules::CModules() : m_pUser(nullptr), m_pNetwork(nullptr), m_pClient(nullptr) {} @@ -1131,7 +1261,7 @@ bool CModules::OnPostRehash() { return false; } bool CModules::OnIRCConnected() { - MODUNLOADCHK(OnIRCConnected()); + MODUNLOADCHK(InternalServerDependentCapsOnIRCConnected()); return false; } bool CModules::OnIRCConnecting(CIRCSock* pIRCSock) { @@ -1149,7 +1279,7 @@ bool CModules::OnBroadcast(CString& sMessage) { MODHALTCHK(OnBroadcast(sMessage)); } bool CModules::OnIRCDisconnected() { - MODUNLOADCHK(OnIRCDisconnected()); + MODUNLOADCHK(InternalServerDependentCapsOnIRCDisconnected()); return false; } @@ -1489,9 +1619,17 @@ bool CModules::OnModCTCP(const CString& sMessage) { MODUNLOADCHK(OnModCTCP(sMessage)); return false; } +bool CModules::OnClientAttached() { + MODUNLOADCHK(InternalServerDependentCapsOnClientAttached()); + return false; +} +bool CModules::OnClientDetached() { + MODUNLOADCHK(InternalServerDependentCapsOnClientDetached()); + return false; +} // Why MODHALTCHK works only with functions returning EModRet ? :( -bool CModules::OnServerCapAvailable(const CString& sCap) { +bool CModules::OnServerCapAvailable(const CString& sCap, const CString& sValue) { bool bResult = false; for (CModule* pMod : *this) { try { @@ -1499,12 +1637,19 @@ bool CModules::OnServerCapAvailable(const CString& sCap) { pMod->SetClient(m_pClient); if (m_pUser) { CUser* pOldUser = pMod->GetUser(); + CIRCNetwork* pOldNetwork = pMod->GetNetwork(); pMod->SetUser(m_pUser); - bResult |= pMod->OnServerCapAvailable(sCap); + pMod->SetNetwork(m_pNetwork); + bResult |= + pMod->InternalServerDependentCapsOnServerCap302Available( + sCap, sValue); pMod->SetUser(pOldUser); + pMod->SetNetwork(pOldNetwork); } else { // WTF? Is that possible? - bResult |= pMod->OnServerCapAvailable(sCap); + bResult |= + pMod->InternalServerDependentCapsOnServerCap302Available( + sCap, sValue); } pMod->SetClient(pOldClient); } catch (const CModule::EModException& e) { @@ -1517,7 +1662,7 @@ bool CModules::OnServerCapAvailable(const CString& sCap) { } bool CModules::OnServerCapResult(const CString& sCap, bool bSuccess) { - MODUNLOADCHK(OnServerCapResult(sCap, bSuccess)); + MODUNLOADCHK(InternalServerDependentCapsOnServerCapResult(sCap, bSuccess)); return false; } @@ -1555,7 +1700,7 @@ bool CModules::OnUnknownUserRawMessage(CMessage& Message) { } bool CModules::OnClientCapLs(CClient* pClient, SCString& ssCaps) { - MODUNLOADCHK(OnClientCapLs(pClient, ssCaps)); + MODUNLOADCHK(InternalServerDependentCapsOnClientCapLs(pClient, ssCaps)); return false; } @@ -1569,12 +1714,19 @@ bool CModules::IsClientCapSupported(CClient* pClient, const CString& sCap, pMod->SetClient(m_pClient); if (m_pUser) { CUser* pOldUser = pMod->GetUser(); + CIRCNetwork* pOldNetwork = pMod->GetNetwork(); pMod->SetUser(m_pUser); - bResult |= pMod->IsClientCapSupported(pClient, sCap, bState); + pMod->SetNetwork(m_pNetwork); + bResult |= + pMod->InternalServerDependentCapsIsClientCapSupported( + pClient, sCap, bState); pMod->SetUser(pOldUser); + pMod->SetNetwork(pOldNetwork); } else { // WTF? Is that possible? - bResult |= pMod->IsClientCapSupported(pClient, sCap, bState); + bResult |= + pMod->InternalServerDependentCapsIsClientCapSupported( + pClient, sCap, bState); } pMod->SetClient(pOldClient); } catch (const CModule::EModException& e) { @@ -1588,7 +1740,8 @@ bool CModules::IsClientCapSupported(CClient* pClient, const CString& sCap, bool CModules::OnClientCapRequest(CClient* pClient, const CString& sCap, bool bState) { - MODUNLOADCHK(OnClientCapRequest(pClient, sCap, bState)); + MODUNLOADCHK( + InternalServerDependentCapsOnClientCapRequest(pClient, sCap, bState)); return false; } @@ -1876,6 +2029,7 @@ void CModules::GetDefaultMods(set& ssMods, const map ns = { {"chansaver", CModInfo::UserModule}, {"controlpanel", CModInfo::UserModule}, + {"corecaps", CModInfo::GlobalModule}, {"simple_away", CModInfo::NetworkModule}, {"webadmin", CModInfo::GlobalModule}}; diff --git a/test/ClientTest.cpp b/test/ClientTest.cpp index eea656c4..0ff6370a 100644 --- a/test/ClientTest.cpp +++ b/test/ClientTest.cpp @@ -172,22 +172,6 @@ TEST_F(ClientTest, UserhostInNames) { // aka UHNAMES ElementsAre(msg.ToString(), extmsg.ToString())); } -TEST_F(ClientTest, ExtendedJoin) { - m_pTestSock->ReadLine(":server CAP * ACK :extended-join"); - m_pTestClient->Reset(); - - CMessage msg(":nick!user@host JOIN #channel"); - CMessage extmsg(":nick!user@host JOIN #channel account :Real Name"); - EXPECT_FALSE(m_pTestClient->HasExtendedJoin()); - m_pTestClient->PutClient(extmsg); - EXPECT_THAT(m_pTestClient->vsLines, ElementsAre(msg.ToString())); - m_pTestClient->SetExtendedJoin(true); - EXPECT_TRUE(m_pTestClient->HasExtendedJoin()); - m_pTestClient->PutClient(extmsg); - EXPECT_THAT(m_pTestClient->vsLines, - ElementsAre(msg.ToString(), extmsg.ToString())); -} - TEST_F(ClientTest, StatusMsg) { m_pTestSock->ReadLine( ":irc.znc.in 001 me :Welcome to the Internet Relay Network me"); diff --git a/test/integration/framework/znctest.cpp b/test/integration/framework/znctest.cpp index 9adec608..1b81db6a 100644 --- a/test/integration/framework/znctest.cpp +++ b/test/integration/framework/znctest.cpp @@ -48,6 +48,14 @@ void WriteConfig(QString path) { p.ReadUntil("Launch ZNC now?"); p.Write("no"); p.ShouldFinishItself(); // clang-format on + + // Default 30s is too slow for the test + QFile conf(path + "/configs/znc.conf"); + ASSERT_TRUE(conf.open(QIODevice::Append | QIODevice::Text)); + QTextStream out(&conf); + out << R"( + ServerThrottle = 5 + )"; } void ZNCTest::SetUp() { diff --git a/test/integration/tests/core.cpp b/test/integration/tests/core.cpp index b95adbf6..7c90e90c 100644 --- a/test/integration/tests/core.cpp +++ b/test/integration/tests/core.cpp @@ -166,6 +166,7 @@ TEST_F(ZNCTest, InvalidConfigInChan) { auto znc = Run(); znc->ShouldFinishItself(1); } + TEST_F(ZNCTest, Encoding) { auto znc = Run(); auto ircd = ConnectIRCd(); @@ -270,6 +271,55 @@ TEST_F(ZNCTest, AwayNotify) { client.Write("znc shutdown"); } +TEST_F(ZNCTest, ExtendedJoin) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = LoginClient(); + ircd.Write(":server 001 user :welcome"); + client.ReadUntil(" 001 "); + ircd.Write(":nick!user@host JOIN #channel account :Real Name"); + // Not sure why it is like this when server sends such format unexpectedly. + client.ReadUntil("JOIN #channel account :Real Name"); + ircd.Write("CAP nick ACK extended-join"); + ircd.Write(":nick!user@host JOIN #channel2 account :Real Name"); + QByteArray line; + client.ReadUntilAndGet("JOIN", line); + EXPECT_EQ(line.toStdString(), "JOIN #channel2"); + client.Write("CAP REQ extended-join"); + client.ReadUntil("CAP user ACK :extended-join"); + ircd.Write(":nick!user@host JOIN #channel3 account :Real Name"); + client.ReadUntil("JOIN #channel3 account :Real Name"); +} + +TEST_F(ZNCTest, CAP302LSWaitFull) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + ircd.ReadUntil("CAP LS 302"); + ircd.Write("CAP user LS * :away-notify"); + ASSERT_THAT(ircd.ReadRemainder().toStdString(), Not(HasSubstr("away-notify"))); + ircd.Write("CAP user LS :blahblah"); + ircd.ReadUntil("CAP REQ :away-notify"); +} + +TEST_F(ZNCTest, CAP302NewDel) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = LoginClient(); + ircd.Write("CAP nick LS :blahblah"); + ircd.ReadUntil("CAP END"); + ircd.Write(":server 001 nick :Hello"); + client.Write("CAP REQ :away-notify"); + client.ReadUntil("NAK :away-notify"); + client.Write("CAP REQ :cap-notify"); + client.ReadUntil("ACK :cap-notify"); + ircd.Write("CAP nick NEW :away-notify"); + ircd.ReadUntil("CAP REQ :away-notify"); + ircd.Write("CAP nick ACK :away-notify"); + client.ReadUntil("CAP nick NEW :away-notify"); + ircd.Write("CAP nick DEL :away-notify"); + client.ReadUntil("CAP nick DEL :away-notify"); +} + TEST_F(ZNCTest, JoinKey) { QFile conf(m_dir.path() + "/configs/znc.conf"); ASSERT_TRUE(conf.open(QIODevice::Append | QIODevice::Text)); @@ -431,6 +481,214 @@ TEST_F(ZNCTest, DenyOptions) { client2.ReadUntil("Access denied!"); } +TEST_F(ZNCTest, CAP302MultiLS) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = LoginClient(); + InstallModule("testmod.cpp", R"( + #include + #include + class TestModule : public CModule { + public: + MODCONSTRUCTOR(TestModule) {} + void OnClientCapLs(CClient* pClient, SCString& ssCaps) override { + for (int i = 0; i < 100; ++i) { + ssCaps.insert("testcap-" + CString(i)); + } + } + }; + GLOBALMODULEDEFS(TestModule, "Test") + )"); + client.Write("znc loadmod testmod"); + client.ReadUntil("Loaded module testmod"); + + auto client2 = ConnectClient(); + client2.Write("CAP LS"); + client2.ReadUntil("LS :"); + auto rem = client2.ReadRemainder(); + ASSERT_GT(rem.indexOf("testcap-10"), 10); + ASSERT_EQ(rem.indexOf("testcap-80"), -1); + ASSERT_EQ(rem.indexOf("LS"), -1); + + client2 = ConnectClient(); + client2.Write("CAP LS 302"); + client2.ReadUntil("LS * :"); + rem = client2.ReadRemainder(); + int w = 0; + ASSERT_GT(w = rem.indexOf("testcap-10"), 1); + ASSERT_GT(w = rem.indexOf("testcap-22", w), 1); + ASSERT_GT(w = rem.indexOf("LS * :", w), 1); + ASSERT_GT(rem.indexOf("testcap-80", w), 1); + ASSERT_GT(rem.indexOf("LS :", w), 1); +} + +TEST_F(ZNCTest, CAP302LSValue) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = LoginClient(); + InstallModule("testmod.cpp", R"( + #include + #include + class TestModule : public CModule { + public: + MODCONSTRUCTOR(TestModule) {} + void OnClientCapLs(CClient* pClient, SCString& ssCaps) override { + if (pClient->HasCap302()) { + ssCaps.insert("testcap=blah"); + } else { + ssCaps.insert("testcap"); + } + } + }; + GLOBALMODULEDEFS(TestModule, "Test") + )"); + client.Write("znc loadmod testmod"); + client.ReadUntil("Loaded module testmod"); + + auto client2 = ConnectClient(); + client2.Write("CAP LS"); + client2.ReadUntil("testcap "); + + client2 = ConnectClient(); + client2.Write("CAP LS 302"); + client2.ReadUntil("testcap="); +} + +class AllLanguages : public ZNCTest, public testing::WithParamInterface {}; + +INSTANTIATE_TEST_CASE_P(LanguagesTests, AllLanguages, testing::Values(1, 2, 3)); + +TEST_P(AllLanguages, ServerDependentCapInModule) { + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = LoginClient(); + switch (GetParam()) { + case 1: + InstallModule("testmod.cpp", R"( + #include + class TestModule : public CModule { + class TestCap : public CCapability { + void OnServerChangedSupport(CIRCNetwork* pNetwork, bool bState) override { + GetModule()->PutModule("Server changed support: " + CString(bState)); + } + void OnClientChangedSupport(CClient* pClient, bool bState) override { + GetModule()->PutModule("Client changed support: " + CString(bState)); + } + }; + public: + MODCONSTRUCTOR(TestModule) { + AddServerDependentCapability("testcap", std::make_unique()); + } + }; + MODULEDEFS(TestModule, "Test") + )"); + break; + case 2: + if (QProcessEnvironment::systemEnvironment().value( + "DISABLED_ZNC_PERL_PYTHON_TEST") == "1") { + return; + } + znc->CanLeak(); + InstallModule("testmod.py", R"( + import znc + class testmod(znc.Module): + def OnLoad(self, args, ret): + def server_change(net, state): + self.PutModule('Server changed support: ' + ('true' if state else 'false')) + def client_change(client, state): + self.PutModule('Client changed support: ' + ('true' if state else 'false')) + self.AddServerDependentCapability('testcap', server_change, client_change) + return True + )"); + client.Write("znc loadmod modpython"); + break; + case 3: + if (QProcessEnvironment::systemEnvironment().value( + "DISABLED_ZNC_PERL_PYTHON_TEST") == "1") { + return; + } + znc->CanLeak(); + InstallModule("testmod.pm", R"( + package testmod; + use base 'ZNC::Module'; + sub OnLoad { + my $self = shift; + my $listen = $self->AddServerDependentCapability('testcap', sub { + my ($net, $state) = @_; + $self->PutModule('Server changed support: ' . ($state ? 'true' : 'false')); + }, sub { + my ($client, $state) = @_; + $self->PutModule('Client changed support: ' . ($state ? 'true' : 'false')); + }); + return 1; + } + 1; + )"); + client.Write("znc loadmod modperl"); + break; + } + client.Write("znc loadmod testmod"); + client.ReadUntil("Loaded module testmod"); + client.Write("znc addnetwork net2"); + client.Close(); + + client = ConnectClient(); + client.Write("CAP LS 302"); + client.Write("PASS :hunter2"); + client.Write("NICK nick"); + client.Write("USER user/test x x :x"); + client.Write("CAP END"); + client.ReadUntil("Welcome"); + + ircd.Write("001 nick Welcome"); + ircd.Write("CAP nick NEW testcap=value"); + ircd.ReadUntil("CAP REQ :testcap"); + ircd.Write("CAP nick ACK :testcap"); + client.ReadUntil(":Server changed support: true"); + client.ReadUntil("CAP nick NEW :testcap=value"); + client.Write("CAP REQ testcap"); + client.ReadUntil(":Client changed support: true"); + client.ReadUntil("CAP nick ACK :testcap"); + + client.Write("CAP LS"); + client.ReadUntil(" testcap=value "); + + ircd.Write("CAP nick DEL testcap"); + client.ReadUntil(":Server changed support: false"); + client.ReadUntil("CAP nick DEL :testcap"); + client.ReadUntil(":Client changed support: false"); + + ircd.Close(); + // TODO combine multiple DELs to single line + client.ReadUntil("CAP nick DEL :testcap"); + client.ReadUntil(":Client changed support: false"); + + ircd = ConnectIRCd(); + ircd.ReadUntil("CAP LS 302"); + ircd.Write("CAP nick LS :testcap=new"); + ircd.ReadUntil("CAP REQ :testcap"); + ircd.Write("CAP nick ACK :testcap"); + client.ReadUntil(":Server changed support: true"); + ircd.ReadUntil("CAP END"); + // NEW waits until 001 + ASSERT_THAT(ircd.ReadRemainder().toStdString(), Not(HasSubstr("testcap"))); + ircd.Write("001 nick Welcome"); + // TODO combine multiple NEWs to single line + client.ReadUntil("CAP nick NEW :testcap=new"); + client.ReadUntil("Welcome"); + + // NEW with new value without DEL + ircd.Write("CAP nick NEW testcap=another"); + client.ReadUntil("CAP nick NEW :testcap=another"); + + client.Write("znc jumpnetwork net2"); + client.ReadUntil("CAP nick DEL :testcap"); + client.ReadUntil(":Client changed support: false"); + + client.Write("znc jumpnetwork test"); + client.ReadUntil("CAP nick NEW :testcap=another"); +} + TEST_F(ZNCTest, HashUpgrade) { QFile conf(m_dir.path() + "/configs/znc.conf"); ASSERT_TRUE(conf.open(QIODevice::Append | QIODevice::Text));