/***************************************************************************
- * Copyright (C) 2005-2018 by the Quassel Project *
+ * Copyright (C) 2005-2020 by the Quassel Project *
* devel@quassel-irc.org *
* *
* This program is free software; you can redistribute it and/or modify *
#include "corenetwork.h"
+#include <algorithm>
+
#include <QDebug>
#include <QHostInfo>
+#include <QTextBoundaryFinder>
#include "core.h"
#include "coreidentity.h"
#include "corenetworkconfig.h"
#include "coresession.h"
#include "coreuserinputhandler.h"
-#include "networkevent.h"
-
-// IRCv3 capabilities
+#include "ircencoder.h"
#include "irccap.h"
+#include "irctag.h"
+#include "networkevent.h"
CoreNetwork::CoreNetwork(const NetworkId& networkid, CoreSession* session)
: Network(networkid, session)
, _coreSession(session)
, _userInputHandler(new CoreUserInputHandler(this))
+ , _metricsServer(Core::instance()->metricsServer())
, _autoReconnectCount(0)
, _quitRequested(false)
, _disconnectExpected(false)
- ,
-
- _previousConnectionAttemptFailed(false)
+ , _previousConnectionAttemptFailed(false)
, _lastUsedServerIndex(0)
- ,
-
- _requestedUserModes('-')
+ , _requestedUserModes('-')
{
// Check if raw IRC logging is enabled
_debugLogRawIrc = (Quassel::isOptionSet("debug-irc") || Quassel::isOptionSet("debug-irc-id"));
setAutoWhoInterval(networkConfig()->autoWhoInterval());
QHash<QString, QString> channels = coreSession()->persistentChannels(networkId());
- foreach (QString chan, channels.keys()) {
+ for (const QString& chan : channels.keys()) {
_channelKeys[chan.toLower()] = channels[chan];
}
QHash<QString, QByteArray> bufferCiphers = coreSession()->bufferCiphers(networkId());
- foreach (QString buffer, bufferCiphers.keys()) {
+ for (const QString& buffer : bufferCiphers.keys()) {
storeChannelCipherKey(buffer.toLower(), bufferCiphers[buffer]);
}
connect(&socket, selectOverload<QAbstractSocket::SocketError>(&QAbstractSocket::error), this, &CoreNetwork::onSocketError);
connect(&socket, &QAbstractSocket::stateChanged, this, &CoreNetwork::onSocketStateChanged);
connect(&socket, &QIODevice::readyRead, this, &CoreNetwork::onSocketHasData);
-#ifdef HAVE_SSL
connect(&socket, &QSslSocket::encrypted, this, &CoreNetwork::onSocketInitialized);
connect(&socket, selectOverload<const QList<QSslError>&>(&QSslSocket::sslErrors), this, &CoreNetwork::onSslErrors);
-#endif
connect(this, &CoreNetwork::newEvent, coreSession()->eventManager(), &EventManager::postEvent);
// Custom rate limiting
disconnect(&socket, nullptr, this, nullptr);
if (!forceDisconnect()) {
qWarning() << QString{"Could not disconnect from network %1 (network ID: %2, user ID: %3)"}
- .arg(networkName())
- .arg(networkId().toInt())
- .arg(userId().toInt());
+ .arg(networkName())
+ .arg(networkId().toInt())
+ .arg(userId().toInt());
}
}
_socketId = Core::instance()->identServer()->addWaitingSocket();
}
+ if (_metricsServer) {
+ _metricsServer->addNetwork(userId());
+ }
+
if (!reconnecting && useAutoReconnect() && _autoReconnectCount == 0) {
_autoReconnectTimer.setInterval(autoReconnectInterval() * 1000);
if (unlimitedReconnectRetries())
else if (_previousConnectionAttemptFailed) {
// cycle to next server if previous connection attempt failed
_previousConnectionAttemptFailed = false;
- showMessage(Message::Server, BufferInfo::StatusBuffer, "", tr("Connection failed. Cycling to next Server"));
+ showMessage(NetworkInternalMessage(
+ Message::Server,
+ BufferInfo::StatusBuffer,
+ "",
+ tr("Connection failed. Cycling to next server...")
+ ));
if (++_lastUsedServerIndex >= serverList().size()) {
_lastUsedServerIndex = 0;
}
Server server = usedServer();
displayStatusMsg(tr("Connecting to %1:%2...").arg(server.host).arg(server.port));
- showMessage(Message::Server, BufferInfo::StatusBuffer, "", tr("Connecting to %1:%2...").arg(server.host).arg(server.port));
+ showMessage(NetworkInternalMessage(
+ Message::Server,
+ BufferInfo::StatusBuffer,
+ "",
+ tr("Connecting to %1:%2...").arg(server.host).arg(server.port)
+ ));
if (server.useProxy) {
- QNetworkProxy proxy((QNetworkProxy::ProxyType)server.proxyType, server.proxyHost, server.proxyPort, server.proxyUser, server.proxyPass);
+ QNetworkProxy proxy((QNetworkProxy::ProxyType) server.proxyType, server.proxyHost, server.proxyPort, server.proxyUser, server.proxyPass);
socket.setProxy(proxy);
}
else {
// hostname of the server. Qt's DNS cache also isn't used by the proxy so we don't need to refresh the entry.
QHostInfo::fromName(server.host);
}
-#ifdef HAVE_SSL
if (server.useSsl) {
CoreIdentity* identity = identityPtr();
if (identity) {
else {
socket.connectToHost(server.host, server.port);
}
-#else
- socket.connectToHost(server.host, server.port);
-#endif
}
void CoreNetwork::disconnectFromIrc(bool requested, const QString& reason, bool withReconnect)
}
disablePingTimeout();
_msgQueue.clear();
+ if (_metricsServer) {
+ _metricsServer->messageQueue(userId(), 0);
+ }
IrcUser* me_ = me();
if (me_) {
else
_quitReason = reason;
- showMessage(Message::Server,
- BufferInfo::StatusBuffer,
- "",
- tr("Disconnecting. (%1)").arg((!requested && !withReconnect) ? tr("Core Shutdown") : _quitReason));
+ showMessage(NetworkInternalMessage(
+ Message::Server,
+ BufferInfo::StatusBuffer,
+ "",
+ tr("Disconnecting. (%1)").arg((!requested && !withReconnect) ? tr("Core Shutdown") : _quitReason)
+ ));
if (socket.state() == QAbstractSocket::UnconnectedState) {
onSocketDisconnected();
}
void CoreNetwork::onSocketCloseTimeout()
{
qWarning() << QString{"Timed out quitting network %1 (network ID: %2, user ID: %3)"}
- .arg(networkName())
- .arg(networkId().toInt())
- .arg(userId().toInt());
+ .arg(networkName())
+ .arg(networkId().toInt())
+ .arg(userId().toInt());
socket.abort();
}
// Add to back, waiting in order
_msgQueue.append(s);
}
+ if (_metricsServer) {
+ _metricsServer->messageQueue(userId(), _msgQueue.size());
+ }
}
}
-void CoreNetwork::putCmd(const QString& cmd, const QList<QByteArray>& params, const QByteArray& prefix, const bool prepend)
+void CoreNetwork::putCmd(const QString& cmd, const QList<QByteArray>& params, const QByteArray& prefix, const QHash<IrcTagKey, QString>& tags, bool prepend)
{
- QByteArray msg;
-
- if (!prefix.isEmpty())
- msg += ":" + prefix + " ";
- msg += cmd.toUpper().toLatin1();
-
- for (int i = 0; i < params.size(); i++) {
- msg += " ";
-
- if (i == params.size() - 1 && (params[i].contains(' ') || (!params[i].isEmpty() && params[i][0] == ':')))
- msg += ":";
-
- msg += params[i];
- }
-
- putRawLine(msg, prepend);
+ putRawLine(IrcEncoder::writeMessage(tags, prefix, cmd, params), prepend);
}
-void CoreNetwork::putCmd(const QString& cmd, const QList<QList<QByteArray>>& params, const QByteArray& prefix, const bool prependAll)
+void CoreNetwork::putCmd(const QString& cmd, const QList<QList<QByteArray>>& params, const QByteArray& prefix, const QHash<IrcTagKey, QString>& tags, bool prependAll)
{
QListIterator<QList<QByteArray>> i(params);
while (i.hasNext()) {
QList<QByteArray> msg = i.next();
- putCmd(cmd, msg, prefix, prependAll);
+ putCmd(cmd, msg, prefix, tags, prependAll);
}
}
{
while (socket.canReadLine()) {
QByteArray s = socket.readLine();
+ if (_metricsServer) {
+ _metricsServer->receiveDataNetwork(userId(), s.size());
+ }
if (s.endsWith("\r\n"))
s.chop(2);
else if (s.endsWith("\n"))
_previousConnectionAttemptFailed = true;
qWarning() << qPrintable(tr("Could not connect to %1 (%2)").arg(networkName(), socket.errorString()));
emit connectionError(socket.errorString());
- showMessage(Message::Error, BufferInfo::StatusBuffer, "", tr("Connection failure: %1").arg(socket.errorString()));
+ showMessage(NetworkInternalMessage(
+ Message::Error,
+ BufferInfo::StatusBuffer,
+ "",
+ tr("Connection failure: %1").arg(socket.errorString())
+ ));
emitConnectionError(socket.errorString());
if (socket.state() < QAbstractSocket::ConnectedState) {
onSocketDisconnected();
Server server = usedServer();
-#ifdef HAVE_SSL
// Non-SSL connections enter here only once, always emit socketInitialized(...) in these cases
// SSL connections call socketInitialized() twice, only emit socketInitialized(...) on the first (not yet encrypted) run
if (!server.useSsl || !socket.isEncrypted()) {
// We'll finish setup once we're encrypted, and called again
return;
}
-#else
- emit socketInitialized(identity, localAddress(), localPort(), peerAddress(), peerPort(), _socketId);
-#endif
socket.setSocketOption(QAbstractSocket::KeepAliveOption, true);
// Request capabilities as per IRCv3.2 specifications
// Older servers should ignore this; newer servers won't downgrade to RFC1459
- showMessage(Message::Server, BufferInfo::StatusBuffer, "", tr("Requesting capability list..."));
+ showMessage(NetworkInternalMessage(
+ Message::Server,
+ BufferInfo::StatusBuffer,
+ "",
+ tr("Requesting capability list...")
+ ));
putRawLine(serverEncode(QString("CAP LS 302")));
if (!server.password.isEmpty()) {
{
disablePingTimeout();
_msgQueue.clear();
+ if (_metricsServer) {
+ _metricsServer->messageQueue(userId(), 0);
+ }
_autoWhoCycleTimer.stop();
_autoWhoTimer.stop();
IrcUser* me_ = me();
if (me_) {
- foreach (QString channel, me_->channels())
- showMessage(Message::Quit, BufferInfo::ChannelBuffer, channel, _quitReason, me_->hostmask());
+ for (const QString& channel : me_->channels()) {
+ showMessage(NetworkInternalMessage(
+ Message::Quit,
+ BufferInfo::ChannelBuffer,
+ channel,
+ _quitReason, me_->hostmask()
+ ));
+ }
}
setConnected(false);
else
_autoReconnectTimer.start();
}
+
+ if (_metricsServer) {
+ _metricsServer->removeNetwork(userId());
+ }
}
void CoreNetwork::onSocketStateChanged(QAbstractSocket::SocketState socketState)
}
// send perform list
- foreach (QString line, perform()) {
+ for (const QString& line : perform()) {
if (!line.isEmpty())
userInput(statusBuf, line);
}
// rejoin channels we've been in
if (rejoinChannels()) {
QStringList channels, keys;
- foreach (QString chan, coreSession()->persistentChannels(networkId()).keys()) {
+ for (const QString& chan : coreSession()->persistentChannels(networkId()).keys()) {
QString key = channelKey(chan);
if (!key.isEmpty()) {
channels.prepend(chan);
qDebug() << "UserId:" << userId() << "Network:" << networkName() << "missed" << _pingCount << "pings."
<< "BA:" << socket.bytesAvailable() << "BTW:" << socket.bytesToWrite();
}
- if ((int)_pingCount >= networkConfig()->maxPingCount() && (now - _lastPingTime) <= (_pingTimer.interval() + (1 * 1000))) {
+ if ((int) _pingCount >= networkConfig()->maxPingCount() && (now - _lastPingTime) <= (_pingTimer.interval() + (1 * 1000))) {
// In transitioning to 64-bit time, the interval no longer needs converted down to seconds.
// However, to reduce the risk of breaking things by changing past behavior, we still allow
// up to 1 second missed instead of enforcing a stricter 1 millisecond allowance.
/******** Custom Rate Limiting ********/
-void CoreNetwork::updateRateLimiting(const bool forceUnlimited)
+void CoreNetwork::updateRateLimiting(bool forceUnlimited)
{
// Verify and apply custom rate limiting options, always resetting the delay and burst size
// (safe-guarding against accidentally starting the timer), but don't reset the token bucket as
void CoreNetwork::serverCapAdded(const QString& capability)
{
+ // Exclude skipped capabilities
+ if (skipCaps().contains(capability)) {
+ return;
+ }
+
// Check if it's a known capability; if so, add it to the list
// Handle special cases first
if (capability == IrcCap::SASL) {
// Only request SASL if it's enabled
- if (networkInfo().useSasl)
+ if (useSasl()) {
queueCap(capability);
+ }
}
else if (IrcCap::knownCaps.contains(capability)) {
// Handling for general known capabilities
}
// Handle capabilities that require further messages sent to the IRC server
- // If you change this list, ALSO change the list in CoreNetwork::capsRequiringServerMessages
+ // If you change this list, ALSO change the list in CoreNetwork::capsRequiringConfiguration
if (capability == IrcCap::SASL) {
// If SASL mechanisms specified, limit to what's accepted for authentication
// if the current identity has a cert set, use SASL EXTERNAL
// FIXME use event
-#ifdef HAVE_SSL
if (!identityPtr()->sslCert().isNull()) {
if (saslMaybeSupports(IrcCap::SaslMech::EXTERNAL)) {
// EXTERNAL authentication supported, send request
putRawLine(serverEncode("AUTHENTICATE EXTERNAL"));
}
else {
- showMessage(Message::Error, BufferInfo::StatusBuffer, "", tr("SASL EXTERNAL authentication not supported"));
+ showMessage(NetworkInternalMessage(
+ Message::Error,
+ BufferInfo::StatusBuffer,
+ "",
+ tr("SASL EXTERNAL authentication not supported")
+ ));
sendNextCap();
}
}
else {
-#endif
if (saslMaybeSupports(IrcCap::SaslMech::PLAIN)) {
// PLAIN authentication supported, send request
// Only working with PLAIN atm, blowfish later
putRawLine(serverEncode("AUTHENTICATE PLAIN"));
}
else {
- showMessage(Message::Error, BufferInfo::StatusBuffer, "", tr("SASL PLAIN authentication not supported"));
+ showMessage(NetworkInternalMessage(
+ Message::Error,
+ BufferInfo::StatusBuffer,
+ "",
+ tr("SASL PLAIN authentication not supported")
+ ));
sendNextCap();
}
-#ifdef HAVE_SSL
}
-#endif
}
}
// 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(Message::Server,
- BufferInfo::StatusBuffer,
- "",
- tr("Could not negotiate some capabilities, retrying individually (%1)...").arg(_capsQueuedLastBundle.join(", ")));
+ 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
void CoreNetwork::beginCapNegotiation()
{
- // Don't begin negotiation if no capabilities are queued to request
- if (!capNegotiationInProgress()) {
- // If the server doesn't have any capabilities, but supports CAP LS, continue on with the
- // normal connection.
- showMessage(Message::Server, BufferInfo::StatusBuffer, "", tr("No capabilities available"));
+ // 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(Message::Server, BufferInfo::StatusBuffer, "", tr("Ready to negotiate (found: %1)").arg(caps().join(", ")));
+ 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(Message::Server, BufferInfo::StatusBuffer, "", tr("Negotiating capabilities (requesting: %1)...").arg(queuedCapsDisplay));
+ showMessage(NetworkInternalMessage(
+ Message::Server,
+ BufferInfo::StatusBuffer,
+ "",
+ tr("Negotiating capabilities (requesting: %1)...").arg(queuedCapsDisplay)
+ ));
sendNextCap();
}
void CoreNetwork::sendNextCap()
{
- if (capNegotiationInProgress()) {
+ 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 (networkInfo().useSasl && !capEnabled(IrcCap::SASL))
- showMessage(Message::Error, BufferInfo::StatusBuffer, "", tr("SASL authentication currently not supported by server"));
+ if (useSasl() && !capEnabled(IrcCap::SASL))
+ showMessage(NetworkInternalMessage(
+ Message::Error,
+ BufferInfo::StatusBuffer,
+ "",
+ tr("SASL authentication currently not supported by server")
+ ));
if (_capNegotiationActive) {
- showMessage(Message::Server,
- BufferInfo::StatusBuffer,
- "",
- tr("Capability negotiation finished (enabled: %1)").arg(capsEnabled().join(", ")));
+ showMessage(NetworkInternalMessage(
+ Message::Server,
+ BufferInfo::StatusBuffer,
+ "",
+ tr("Capability negotiation finished (enabled: %1)").arg(capsEnabled().join(", "))
+ ));
_capNegotiationActive = false;
}
// 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(serverEncode(chanOrNick), QString::number(IrcCap::ACCOUNT_NOTIFY_WHOX_NUM))));
+ QString("WHO %1 n%chtsunfra,%2")
+ .arg(chanOrNick, QString::number(IrcCap::ACCOUNT_NOTIFY_WHOX_NUM))
+ ));
}
else {
// Fall back to normal WHO
}
}
-#ifdef HAVE_SSL
void CoreNetwork::onSslErrors(const QList<QSslError>& sslErrors)
{
Server server = usedServer();
// Add the error reason if known
sslErrorMessage.append(tr(" (Reason: %1)").arg(sslErrors.first().errorString()));
}
- showMessage(Message::Error, BufferInfo::StatusBuffer, "", sslErrorMessage);
+ showMessage(NetworkInternalMessage(
+ Message::Error,
+ BufferInfo::StatusBuffer,
+ "",
+ sslErrorMessage
+ ));
// Disconnect, triggering a reconnect in case it's a temporary issue with certificate
// validity, network trouble, etc.
// Add the error reason if known
sslErrorMessage.append(tr(" (Reason: %1)").arg(sslErrors.first().errorString()));
}
- showMessage(Message::Info, BufferInfo::StatusBuffer, "", sslErrorMessage);
+ showMessage(NetworkInternalMessage(
+ Message::Info,
+ BufferInfo::StatusBuffer,
+ "",
+ sslErrorMessage
+ ));
// Proceed with the connection
socket.ignoreSslErrors();
}
}
-#endif // HAVE_SSL
-
void CoreNetwork::checkTokenBucket()
{
if (_skipMessageRates) {
- if (_msgQueue.size() == 0) {
+ if (_msgQueue.empty()) {
// Message queue emptied; stop the timer and bail out
_tokenBucketTimer.stop();
return;
}
// As long as there's tokens available and messages remaining, sending messages from the queue
- while (_msgQueue.size() > 0 && _tokenBucket > 0) {
+ while (!_msgQueue.empty() && _tokenBucket > 0) {
writeToSocket(_msgQueue.takeFirst());
+ if (_metricsServer) {
+ _metricsServer->messageQueue(userId(), _msgQueue.size());
+ }
}
}
}
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--;