Implement cap negotiation 3.2 on server side

Used for "server-dependent" caps which already rely on sending NEW and
DEL to client. This functionality is not yet available for caps added by
modules.
This commit is contained in:
Alexey Sokolov
2023-01-09 01:08:04 +00:00
parent d49168b93f
commit c15fc63c55
15 changed files with 201 additions and 74 deletions

View File

@@ -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,102 @@ 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<CString, std::function<void(bool bVal)>> 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; }},
{"cap-notify", [](bool 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; }},
};
auto RemoveCap = [&](const CString& sCap) {
IRCSOCKMODULECALL(OnServerCapResult(sCap, false), NOTHING);
auto it = mSupportedCaps.find(sCap);
if (it != mSupportedCaps.end()) {
it->second(false);
}
std::map<CString, std::function<void(bool bVal)>> 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") {
VCString vsTokens;
sArgs.Split(" ", vsTokens, false);
for (const CString& sCap : vsTokens) {
if (OnServerCapAvailable(sCap) || mSupportedCaps.count(sCap)) {
m_ssPendingCaps.insert(sCap);
}
}
} 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_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);
m_ssAcceptedCaps.erase(sCap);
m_ssPendingCaps.erase(sCap);
if (m_bAuthed) {
m_pNetwork->NotifyServerDependentCap(sCap, false);
}
};
if (sSubCmd == "LS" || sSubCmd == "NEW") {
VCString vsTokens;
sArgs.Split(" ", vsTokens, false);
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);
}
if (OnServerCapAvailable(sCap, sValue) || mSupportedCaps.count(sCap)) {
m_ssPendingCaps.insert(sCap);
}
}
} 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);
if (m_bAuthed) {
m_pNetwork->NotifyServerDependentCap(sArgs, true);
}
} 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);
}
}
if (bSendNext) {
SendNextCap();
}
// Don't forward any CAP stuff to the client
@@ -1222,7 +1259,7 @@ void CIRCSock::Connected() {
&bReturn);
if (bReturn) return;
PutIRC("CAP LS");
PutIRC("CAP LS 302");
if (!sPass.empty()) {
PutIRC("PASS " + sPass);