+
+ // This should be fairly rare, e.g. services restarting during negotiation, so simplicity wins
+ // over efficiency. If this becomes an issue, implement a binary splicing system instead,
+ // keeping track of which halves of the group fail, dividing the set each time.
+
+ // Add most recently tried capability set to individual list, re-requesting them one at a time
+ _capsQueuedIndividual.append(_capsQueuedLastBundle);
+ // Warn of this issue to explain the slower login. Servers usually shouldn't trigger this.
+ showMessage(NetworkInternalMessage(
+ Message::Server,
+ BufferInfo::StatusBuffer,
+ "",
+ tr("Could not negotiate some capabilities, retrying individually (%1)...").arg(_capsQueuedLastBundle.join(", "))
+ ));
+ // Capabilities are already removed from the capability bundle queue via takeQueuedCaps(), no
+ // need to remove them here.
+ // Clear the most recently tried set to reduce risk that mistakes elsewhere causes retrying
+ // indefinitely.
+ _capsQueuedLastBundle.clear();
+}
+
+void CoreNetwork::beginCapNegotiation()
+{
+ // Check if any available capabilities have been disabled
+ QStringList capsSkipped;
+ if (!skipCaps().isEmpty() && !caps().isEmpty()) {
+ // Find the entries that are common to skipCaps() and caps(). This represents any
+ // capabilities supported by the server that were skipped.
+
+ // Both skipCaps() and caps() are already lowercase
+ // std::set_intersection requires sorted lists, and we can't modify the original lists.
+ //
+ // skipCaps() should already be sorted. caps() is intentionally not sorted elsewhere so
+ // Quassel can show the capabilities in the order transmitted by the network.
+ auto sortedCaps = caps();
+ sortedCaps.sort();
+
+ // Find the intersection between skipped caps and server-supplied caps
+ std::set_intersection(skipCaps().cbegin(), skipCaps().cend(),
+ sortedCaps.cbegin(), sortedCaps.cend(),
+ std::back_inserter(capsSkipped));
+ }
+
+ if (!capsPendingNegotiation()) {
+ // No capabilities are queued for request, determine the reason why
+ QString capStatusMsg;
+ if (caps().empty()) {
+ // The server doesn't provide any capabilities, but supports CAP LS
+ capStatusMsg = tr("No capabilities available");
+ }
+ else if (capsEnabled().empty()) {
+ // The server supports capabilities (caps() is not empty) but Quassel doesn't support
+ // anything offered. This should be uncommon.
+ capStatusMsg =
+ tr("None of the capabilities provided by the server are supported (found: %1)")
+ .arg(caps().join(", "));
+ }
+ else {
+ // Quassel has enabled some capabilities, but there are no further capabilities that can
+ // be negotiated.
+ // (E.g. the user has manually run "/cap ls 302" after initial negotiation.)
+ capStatusMsg =
+ tr("No additional capabilities are supported (found: %1; currently enabled: %2)")
+ .arg(caps().join(", "), capsEnabled().join(", "));
+ }
+ // Inform the user of the situation
+ showMessage(NetworkInternalMessage(
+ Message::Server,
+ BufferInfo::StatusBuffer,
+ "",
+ capStatusMsg
+ ));
+
+ if (!capsSkipped.isEmpty()) {
+ // Mention that some capabilities are skipped
+ showMessage(NetworkInternalMessage(
+ Message::Server,
+ BufferInfo::StatusBuffer,
+ "",
+ tr("Quassel is configured to ignore some capabilities (skipped: %1)").arg(capsSkipped.join(", "))
+ ));
+ }
+
+ // End any ongoing capability negotiation, allowing connection to continue
+ endCapNegotiation();
+ return;
+ }
+
+ _capNegotiationActive = true;
+ showMessage(NetworkInternalMessage(
+ Message::Server,
+ BufferInfo::StatusBuffer,
+ "",
+ tr("Ready to negotiate (found: %1)").arg(caps().join(", "))
+ ));
+
+ if (!capsSkipped.isEmpty()) {
+ // Mention that some capabilities are skipped
+ showMessage(NetworkInternalMessage(
+ Message::Server,
+ BufferInfo::StatusBuffer,
+ "",
+ tr("Quassel is configured to ignore some capabilities (skipped: %1)").arg(capsSkipped.join(", "))
+ ));
+ }
+
+ // Build a list of queued capabilities, starting with individual, then bundled, only adding the
+ // comma separator between the two if needed (both individual and bundled caps exist).
+ QString queuedCapsDisplay = _capsQueuedIndividual.join(", ")
+ + ((!_capsQueuedIndividual.empty() && !_capsQueuedBundled.empty()) ? ", " : "")
+ + _capsQueuedBundled.join(", ");
+ showMessage(NetworkInternalMessage(
+ Message::Server,
+ BufferInfo::StatusBuffer,
+ "",
+ tr("Negotiating capabilities (requesting: %1)...").arg(queuedCapsDisplay)
+ ));
+
+ sendNextCap();
+}
+
+void CoreNetwork::sendNextCap()
+{
+ if (capsPendingNegotiation()) {
+ // Request the next set of capabilities and remove them from the list
+ putRawLine(serverEncode(QString("CAP REQ :%1").arg(takeQueuedCaps())));
+ }
+ else {
+ // No pending desired capabilities, capability negotiation finished
+ // If SASL requested but not available, print a warning
+ if (useSasl() && !capEnabled(IrcCap::SASL))
+ showMessage(NetworkInternalMessage(
+ Message::Error,
+ BufferInfo::StatusBuffer,
+ "",
+ tr("SASL authentication currently not supported by server")
+ ));
+
+ if (_capNegotiationActive) {
+ showMessage(NetworkInternalMessage(
+ Message::Server,
+ BufferInfo::StatusBuffer,
+ "",
+ tr("Capability negotiation finished (enabled: %1)").arg(capsEnabled().join(", "))
+ ));
+ _capNegotiationActive = false;
+ }
+
+ endCapNegotiation();
+ }
+}
+
+void CoreNetwork::endCapNegotiation()
+{
+ // If nick registration is already complete, CAP END is not required
+ if (!_capInitialNegotiationEnded) {
+ putRawLine(serverEncode(QString("CAP END")));
+ _capInitialNegotiationEnded = true;
+ }
+}
+
+/******** AutoWHO ********/
+
+void CoreNetwork::startAutoWhoCycle()
+{
+ if (!_autoWhoQueue.isEmpty()) {
+ _autoWhoCycleTimer.stop();
+ return;
+ }
+ _autoWhoQueue = channels();
+}
+
+void CoreNetwork::queueAutoWhoOneshot(const QString& name)
+{
+ // Prepend so these new channels/nicks are the first to be checked
+ // Don't allow duplicates
+ if (!_autoWhoQueue.contains(name.toLower())) {
+ _autoWhoQueue.prepend(name.toLower());
+ }
+ if (capEnabled(IrcCap::AWAY_NOTIFY)) {
+ // When away-notify is active, the timer's stopped. Start a new cycle to who this channel.
+ setAutoWhoEnabled(true);
+ }
+}
+
+void CoreNetwork::setAutoWhoDelay(int delay)
+{
+ _autoWhoTimer.setInterval(delay * 1000);
+}
+
+void CoreNetwork::setAutoWhoInterval(int interval)
+{
+ _autoWhoCycleTimer.setInterval(interval * 1000);
+}
+
+void CoreNetwork::setAutoWhoEnabled(bool enabled)
+{
+ if (enabled && isConnected() && !_autoWhoTimer.isActive())
+ _autoWhoTimer.start();
+ else if (!enabled) {
+ _autoWhoTimer.stop();
+ _autoWhoCycleTimer.stop();
+ }
+}
+
+void CoreNetwork::sendAutoWho()
+{
+ // Don't send autowho if there are still some pending
+ if (_autoWhoPending.count())
+ return;
+
+ while (!_autoWhoQueue.isEmpty()) {
+ QString chanOrNick = _autoWhoQueue.takeFirst();
+ // Check if it's a known channel or nick
+ IrcChannel* ircchan = ircChannel(chanOrNick);
+ IrcUser* ircuser = ircUser(chanOrNick);
+ if (ircchan) {
+ // Apply channel limiting rules
+ // If using away-notify, don't impose channel size limits in order to capture away
+ // state of everyone. Auto-who won't run on a timer so network impact is minimal.
+ if (networkConfig()->autoWhoNickLimit() > 0 && ircchan->ircUsers().count() >= networkConfig()->autoWhoNickLimit()
+ && !capEnabled(IrcCap::AWAY_NOTIFY))
+ continue;
+ _autoWhoPending[chanOrNick.toLower()]++;
+ }
+ else if (ircuser) {
+ // Checking a nick, add it to the pending list
+ _autoWhoPending[ircuser->nick().toLower()]++;
+ }
+ else {
+ // Not a channel or a nick, skip it
+ qDebug() << "Skipping who polling of unknown channel or nick" << chanOrNick;
+ continue;
+ }
+ if (supports("WHOX")) {
+ // Use WHO extended to poll away users and/or user accounts
+ // Explicitly only match on nickname ("n"), don't rely on server defaults
+ //
+ // WHO <nickname> n%chtsunfra,<unique_number>
+ //
+ // See http://faerion.sourceforge.net/doc/irc/whox.var
+ // And https://github.com/quakenet/snircd/blob/master/doc/readme.who
+ // And https://github.com/hexchat/hexchat/blob/57478b65758e6b697b1d82ce21075e74aa475efc/src/common/proto-irc.c#L752
+ putRawLine(serverEncode(
+ QString("WHO %1 n%chtsunfra,%2")
+ .arg(chanOrNick, QString::number(IrcCap::ACCOUNT_NOTIFY_WHOX_NUM))
+ ));
+ }
+ else {
+ // Fall back to normal WHO
+ //
+ // Note: According to RFC 1459, "WHO <phrase>" can fall back to searching realname,
+ // hostmask, etc. There's nothing we can do about that :(
+ //
+ // See https://tools.ietf.org/html/rfc1459#section-4.5.1
+ putRawLine(serverEncode(QString("WHO %1").arg(chanOrNick)));
+ }
+ break;
+ }
+
+ if (_autoWhoQueue.isEmpty() && networkConfig()->autoWhoEnabled() && !_autoWhoCycleTimer.isActive() && !capEnabled(IrcCap::AWAY_NOTIFY)) {
+ // Timer was stopped, means a new cycle is due immediately
+ // Don't run a new cycle if using away-notify; server will notify as appropriate
+ _autoWhoCycleTimer.start();
+ startAutoWhoCycle();
+ }
+ else if (capEnabled(IrcCap::AWAY_NOTIFY) && _autoWhoCycleTimer.isActive()) {
+ // Don't run another who cycle if away-notify is enabled
+ _autoWhoCycleTimer.stop();
+ }
+}
+
+void CoreNetwork::onSslErrors(const QList<QSslError>& sslErrors)
+{
+ Server server = usedServer();
+ if (server.sslVerify) {
+ // Treat the SSL error as a hard error
+ QString sslErrorMessage = tr("Encrypted connection couldn't be verified, disconnecting "
+ "since verification is required");
+ if (!sslErrors.empty()) {
+ // Add the error reason if known
+ sslErrorMessage.append(tr(" (Reason: %1)").arg(sslErrors.first().errorString()));
+ }
+ showMessage(NetworkInternalMessage(
+ Message::Error,
+ BufferInfo::StatusBuffer,
+ "",
+ sslErrorMessage
+ ));
+
+ // Disconnect, triggering a reconnect in case it's a temporary issue with certificate
+ // validity, network trouble, etc.
+ disconnectFromIrc(false, QString("Encrypted connection not verified"), true /* withReconnect */);
+ }
+ else {
+ // Treat the SSL error as a warning, continue to connect anyways
+ QString sslErrorMessage = tr("Encrypted connection couldn't be verified, continuing "
+ "since verification is not required");
+ if (!sslErrors.empty()) {
+ // Add the error reason if known
+ sslErrorMessage.append(tr(" (Reason: %1)").arg(sslErrors.first().errorString()));
+ }
+ showMessage(NetworkInternalMessage(
+ Message::Info,
+ BufferInfo::StatusBuffer,
+ "",
+ sslErrorMessage
+ ));
+
+ // Proceed with the connection
+ socket.ignoreSslErrors();
+ }
+}
+
+void CoreNetwork::checkTokenBucket()
+{
+ if (_skipMessageRates) {
+ if (_msgQueue.empty()) {
+ // Message queue emptied; stop the timer and bail out
+ _tokenBucketTimer.stop();
+ return;
+ }
+ // Otherwise, we're emptying the queue, continue on as normal
+ }
+
+ // Process whatever messages are pending
+ fillBucketAndProcessQueue();
+}
+
+void CoreNetwork::fillBucketAndProcessQueue()
+{
+ // If there's less tokens than burst size, refill the token bucket by 1
+ if (_tokenBucket < _burstSize) {
+ _tokenBucket++;
+ }
+
+ // As long as there's tokens available and messages remaining, sending messages from the queue
+ while (!_msgQueue.empty() && _tokenBucket > 0) {
+ writeToSocket(_msgQueue.takeFirst());
+ if (_metricsServer) {
+ _metricsServer->messageQueue(userId(), _msgQueue.size());
+ }
+ }
+}
+
+void CoreNetwork::writeToSocket(const QByteArray& data)
+{
+ // Log the message if enabled and network ID matches or allows all
+ if (_debugLogRawIrc && (_debugLogRawNetId == -1 || networkId().toInt() == _debugLogRawNetId)) {
+ // Include network ID
+ qDebug() << "IRC net" << networkId() << ">>" << data;
+ }
+ socket.write(data);
+ socket.write("\r\n");
+ if (_metricsServer) {
+ _metricsServer->transmitDataNetwork(userId(), data.size() + 2);
+ }
+ if (!_skipMessageRates) {
+ // Only subtract from the token bucket if message rate limiting is enabled
+ _tokenBucket--;
+ }
+}
+
+Network::Server CoreNetwork::usedServer() const
+{
+ if (_lastUsedServerIndex < serverList().count())
+ return serverList()[_lastUsedServerIndex];
+
+ if (!serverList().isEmpty())
+ return serverList()[0];
+
+ return Network::Server();
+}
+
+void CoreNetwork::requestConnect() const
+{
+ if (_shuttingDown) {
+ return;
+ }
+ if (connectionState() != Disconnected) {
+ qWarning() << "Requesting connect while already being connected!";
+ return;
+ }
+ QMetaObject::invokeMethod(const_cast<CoreNetwork*>(this), "connectToIrc", Qt::QueuedConnection);
+}
+
+void CoreNetwork::requestDisconnect() const
+{
+ if (_shuttingDown) {
+ return;
+ }
+ if (connectionState() == Disconnected) {
+ qWarning() << "Requesting disconnect while not being connected!";
+ return;
+ }
+ userInputHandler()->handleQuit(BufferInfo(), QString());
+}
+
+void CoreNetwork::requestSetNetworkInfo(const NetworkInfo& info)
+{
+ Network::Server currentServer = usedServer();
+ setNetworkInfo(info);
+ Core::updateNetwork(coreSession()->user(), info);
+
+ // the order of the servers might have changed,
+ // so we try to find the previously used server
+ _lastUsedServerIndex = 0;
+ for (int i = 0; i < serverList().count(); i++) {
+ Network::Server server = serverList()[i];
+ if (server.host == currentServer.host && server.port == currentServer.port) {
+ _lastUsedServerIndex = i;
+ break;
+ }
+ }
+}
+
+QList<QList<QByteArray>> CoreNetwork::splitMessage(const QString& cmd,
+ const QString& message,
+ const std::function<QList<QByteArray>(QString&)>& cmdGenerator)
+{
+ QString wrkMsg(message);
+ QList<QList<QByteArray>> msgsToSend;
+
+ // do while (wrkMsg.size() > 0)
+ do {
+ // First, check to see if the whole message can be sent at once. The
+ // cmdGenerator function is passed in by the caller and is used to encode
+ // and encrypt (if applicable) the message, since different callers might
+ // want to use different encoding or encode different values.
+ int splitPos = wrkMsg.size();
+ QList<QByteArray> initialSplitMsgEnc = cmdGenerator(wrkMsg);
+ int initialOverrun = userInputHandler()->lastParamOverrun(cmd, initialSplitMsgEnc);
+
+ if (initialOverrun) {
+ // If the message was too long to be sent, first try splitting it along
+ // word boundaries with QTextBoundaryFinder.
+ QString splitMsg(wrkMsg);
+ QTextBoundaryFinder qtbf(QTextBoundaryFinder::Word, splitMsg);
+ qtbf.setPosition(initialSplitMsgEnc[1].size() - initialOverrun);
+ QList<QByteArray> splitMsgEnc;
+ int overrun = initialOverrun;
+
+ while (overrun) {
+ splitPos = qtbf.toPreviousBoundary();
+
+ // splitPos==-1 means the QTBF couldn't find a split point at all and
+ // splitPos==0 means the QTBF could only find a boundary at the beginning of
+ // the string. Neither one of these works for us.
+ if (splitPos > 0) {
+ // If a split point could be found, split the message there, calculate the
+ // overrun, and continue with the loop.
+ splitMsg = splitMsg.left(splitPos);
+ splitMsgEnc = cmdGenerator(splitMsg);
+ overrun = userInputHandler()->lastParamOverrun(cmd, splitMsgEnc);
+ }
+ else {
+ // If a split point could not be found (the beginning of the message
+ // is reached without finding a split point short enough to send) and we
+ // are still in Word mode, switch to Grapheme mode. We also need to restore
+ // the full wrkMsg to splitMsg, since splitMsg may have been cut down during
+ // the previous attempt to find a split point.
+ if (qtbf.type() == QTextBoundaryFinder::Word) {
+ splitMsg = wrkMsg;
+ splitPos = splitMsg.size();
+ QTextBoundaryFinder graphemeQtbf(QTextBoundaryFinder::Grapheme, splitMsg);
+ graphemeQtbf.setPosition(initialSplitMsgEnc[1].size() - initialOverrun);
+ qtbf = graphemeQtbf;
+ }
+ else {
+ // If the QTBF fails to find a split point in Grapheme mode, we give up.
+ // This should never happen, but it should be handled anyway.
+ qWarning() << "Unexpected failure to split message!";
+ return msgsToSend;
+ }
+ }
+ }
+
+ // Once a message of sendable length has been found, remove it from the wrkMsg and
+ // add it to the list of messages to be sent.
+ wrkMsg.remove(0, splitPos);
+ msgsToSend.append(splitMsgEnc);
+ }
+ else {
+ // If the entire remaining message is short enough to be sent all at once, remove
+ // it from the wrkMsg and add it to the list of messages to be sent.
+ wrkMsg.remove(0, splitPos);
+ msgsToSend.append(initialSplitMsgEnc);
+ }
+ } while (wrkMsg.size() > 0);
+
+ return msgsToSend;