Introduce netsplit detection/handling
authorSebastian Goth <seezer@roath.org>
Thu, 30 Jul 2009 15:33:44 +0000 (17:33 +0200)
committerSebastian Goth <seezer@roath.org>
Mon, 24 Aug 2009 16:04:24 +0000 (18:04 +0200)
src/common/message.h
src/core/CMakeLists.txt
src/core/ircserverhandler.cpp
src/core/ircserverhandler.h
src/core/netsplit.cpp [new file with mode: 0644]
src/core/netsplit.h [new file with mode: 0644]

index 1cf0f67..b5d52a4 100644 (file)
@@ -33,21 +33,23 @@ class Message {
 public:
   /** The different types a message can have for display */
   enum Type {
-    Plain     = 0x0001,
-    Notice    = 0x0002,
-    Action    = 0x0004,
-    Nick      = 0x0008,
-    Mode      = 0x0010,
-    Join      = 0x0020,
-    Part      = 0x0040,
-    Quit      = 0x0080,
-    Kick      = 0x0100,
-    Kill      = 0x0200,
-    Server    = 0x0400,
-    Info      = 0x0800,
-    Error     = 0x1000,
-    DayChange = 0x2000,
-    Topic     = 0x4000
+    Plain     = 0x00001,
+    Notice    = 0x00002,
+    Action    = 0x00004,
+    Nick      = 0x00008,
+    Mode      = 0x00010,
+    Join      = 0x00020,
+    Part      = 0x00040,
+    Quit      = 0x00080,
+    Kick      = 0x00100,
+    Kill      = 0x00200,
+    Server    = 0x00400,
+    Info      = 0x00800,
+    Error     = 0x01000,
+    DayChange = 0x02000,
+    Topic     = 0x04000,
+    NetsplitJoin = 0x08000,
+    NetsplitQuit = 0x10000,
   };
 
   // DO NOT CHANGE without knowing what you do, some of these flags are stored in the database
index 3ec5df2..d5624d6 100644 (file)
@@ -27,6 +27,7 @@ set(SOURCES
     coreusersettings.cpp
     ctcphandler.cpp
     ircserverhandler.cpp
+    netsplit.cpp
     postgresqlstorage.cpp
     sessionthread.cpp
     sqlitestorage.cpp
@@ -52,6 +53,7 @@ set(MOC_HDRS
     coresession.h
     ctcphandler.h
     ircserverhandler.h
+    netsplit.h
     postgresqlstorage.h
     sqlitestorage.h
     storage.h
index b942bc2..b03b0d7 100644 (file)
@@ -186,9 +186,23 @@ void IrcServerHandler::handleJoin(const QString &prefix, const QList<QByteArray>
 
   QString channel = serverDecode(params[0]);
   IrcUser *ircuser = network()->updateNickFromMask(prefix);
-  emit displayMsg(Message::Join, BufferInfo::ChannelBuffer, channel, channel, prefix);
+
+  bool handledByNetsplit = false;
+  if(!_netsplits.empty()) {
+    foreach(Netsplit* n, _netsplits) {
+      handledByNetsplit = n->userJoined(prefix, channel);
+      if(handledByNetsplit)
+        break;
+    }
+  }
+
+  // normal join
+  if(!handledByNetsplit) {
+    emit displayMsg(Message::Join, BufferInfo::ChannelBuffer, channel, channel, prefix);
+    ircuser->joinChannel(channel);
+  }
   //qDebug() << "IrcServerHandler::handleJoin()" << prefix << params;
-  ircuser->joinChannel(channel);
+
   if(network()->isMe(ircuser)) {
     network()->setChannelJoined(channel);
     putCmd("MODE", params[0]); // we want to know the modes of the channel we just joined, so we ask politely
@@ -461,10 +475,28 @@ void IrcServerHandler::handleQuit(const QString &prefix, const QList<QByteArray>
   if(params.count() > 0)
     msg = userDecode(ircuser->nick(), params[0]);
 
-  foreach(QString channel, ircuser->channels())
-    emit displayMsg(Message::Quit, BufferInfo::ChannelBuffer, channel, msg, prefix);
-
-  ircuser->quit();
+  // check if netsplit
+  if(Netsplit::isNetsplit(msg)) {
+    Netsplit *n;
+    if(!_netsplits.contains(msg)) {
+      n = new Netsplit();
+      connect(n, SIGNAL(finished()), this, SLOT(handleNetsplitFinished()));
+      connect(n, SIGNAL(netsplitJoin(QString,QStringList,QString)), this, SLOT(handleNetsplitJoin(QString,QStringList,QString)));
+      connect(n, SIGNAL(netsplitQuit(QString,QStringList,QString)), this, SLOT(handleNetsplitQuit(QString,QStringList,QString)));
+      _netsplits.insert(msg, n);
+    }
+    else {
+      n = _netsplits[msg];
+    }
+    // add this user to the netsplit
+    n->userQuit(prefix, ircuser->channels(),msg);
+  }
+  // normal quit
+  else {
+    foreach(QString channel, ircuser->channels())
+      emit displayMsg(Message::Quit, BufferInfo::ChannelBuffer, channel, msg, prefix);
+    ircuser->quit();
+  }
 }
 
 void IrcServerHandler::handleTopic(const QString &prefix, const QList<QByteArray> &params) {
@@ -1013,6 +1045,38 @@ void IrcServerHandler::handle433(const QString &prefix, const QList<QByteArray>
   tryNextNick(errnick);
 }
 
+/* Handle signals from Netsplit objects  */
+
+void IrcServerHandler::handleNetsplitJoin(const QString &channel, const QStringList &users, const QString& quitMessage)
+{
+  QString msg = users.join(":").append(':').append(quitMessage);
+  emit displayMsg(Message::NetsplitJoin, BufferInfo::ChannelBuffer, channel, msg);
+
+  foreach(QString user, users) {
+    IrcUser *iu = network()->ircUser(nickFromMask(user));
+    if(iu)
+      iu->joinChannel(channel);
+  }
+}
+
+void IrcServerHandler::handleNetsplitQuit(const QString &channel, const QStringList &users, const QString& quitMessage)
+{
+  QString msg = users.join(":").append(':').append(quitMessage);
+  emit displayMsg(Message::NetsplitQuit, BufferInfo::ChannelBuffer, channel, msg);
+  foreach(QString user, users) {
+    IrcUser *iu = network()->ircUser(nickFromMask(user));
+    if(iu)
+      iu->quit();
+  }
+}
+
+void IrcServerHandler::handleNetsplitFinished()
+{
+  Netsplit* n = qobject_cast<Netsplit*>(sender());
+  _netsplits.remove(_netsplits.key(n));
+  n->deleteLater();
+}
+
 /* */
 
 // FIXME networkConnection()->setChannelKey("") for all ERR replies indicating that a JOIN went wrong
index 90db4b4..f035eb9 100644 (file)
@@ -22,6 +22,7 @@
 #define IRCSERVERHANDLER_H
 
 #include "basichandler.h"
+#include "netsplit.h"
 
 class IrcServerHandler : public BasicHandler {
   Q_OBJECT
@@ -81,6 +82,28 @@ public slots:
 
   void defaultHandler(QString cmd, const QString &prefix, const QList<QByteArray> &params);
 
+private slots:
+  //! Joins after a netsplit
+  /** This slot handles a bulk-join after a netsplit is over
+    * \param channel The channel the users joined
+    * \param users   The list of users that joind the channel
+    * \param quitMessage The message we received when the netsplit occured
+    */
+  void handleNetsplitJoin(const QString &channel, const QStringList &users, const QString &quitMessage);
+
+  //! Quits after a netsplit
+  /** This slot handles a bulk-quit after a netsplit occured
+    * \param channel The channel the users quitted
+    * \param users   The list of users that got split
+    * \param quitMessage The message we received when the netsplit occured
+    */
+  void handleNetsplitQuit(const QString &channel, const QStringList &users, const QString &quitMessage);
+
+  //! Netsplit finished
+  /** This slot deletes the netsplit object that sent the finished() signal
+    */
+  void handleNetsplitFinished();
+
 private:
   void tryNextNick(const QString &errnick, bool erroneus = false);
   bool checkParamCount(const QString &methodName, const QList<QByteArray> &params, int minParams);
@@ -90,6 +113,11 @@ private:
 
   bool _whois;
   QString _target;
+
+  // structure to organize netsplits
+  // key: quit message
+  // value: the corresponding netsplit object
+  QHash<QString, Netsplit*> _netsplits;
 };
 
 
diff --git a/src/core/netsplit.cpp b/src/core/netsplit.cpp
new file mode 100644 (file)
index 0000000..89d27a4
--- /dev/null
@@ -0,0 +1,100 @@
+/***************************************************************************
+ *   Copyright (C) 2005-09 by the Quassel Project                          *
+ *   devel@quassel-irc.org                                                 *
+ *                                                                         *
+ *   This program is free software; you can redistribute it and/or modify  *
+ *   it under the terms of the GNU General Public License as published by  *
+ *   the Free Software Foundation; either version 2 of the License, or     *
+ *   (at your option) version 3.                                           *
+ *                                                                         *
+ *   This program is distributed in the hope that it will be useful,       *
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
+ *   GNU General Public License for more details.                          *
+ *                                                                         *
+ *   You should have received a copy of the GNU General Public License     *
+ *   along with this program; if not, write to the                         *
+ *   Free Software Foundation, Inc.,                                       *
+ *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
+ ***************************************************************************/
+
+#include "netsplit.h"
+
+#include <QRegExp>
+
+Netsplit::Netsplit()
+    : _quitMsg("")
+{
+  _discardTimer.setSingleShot(true);
+  _joinTimer.setSingleShot(true);
+  _quitTimer.setSingleShot(true);
+
+  connect(&_discardTimer, SIGNAL(timeout()), this, SIGNAL(finished()));
+
+  connect(&_joinTimer, SIGNAL(timeout()), this, SLOT(joinTimeout()));
+  connect(&_quitTimer, SIGNAL(timeout()), this, SLOT(quitTimeout()));
+
+  // wait for a maximum of 1 hour until we discard the netsplit
+  _discardTimer.start(3600000);
+}
+
+void Netsplit::userQuit(const QString &sender, const QStringList &channels, const QString &msg)
+{
+  if(_quitMsg.isEmpty())
+    _quitMsg = msg;
+  foreach(QString channel, channels) {
+    _quits[channel].append(sender);
+  }
+  // now let's wait 5s to finish the netsplit-quit
+  _quitTimer.start(5000);
+}
+
+bool Netsplit::userJoined(const QString &sender, const QString &channel) {
+  if(!_quits.contains(channel))
+    return false;
+
+  QStringList &users = _quits[channel];
+  int idx = users.indexOf(sender);
+  if(idx == -1)
+    return false;
+
+  _joins[channel].append(users.takeAt(idx));
+  if(users.empty())
+    _quits.remove(channel);
+
+  // now let's wait 5s to finish the netsplit-join
+  _joinTimer.start(5000);
+  return true;
+}
+
+bool Netsplit::isNetsplit(const QString &quitMessage)
+{
+  // check if we find some common chars that disqualify the netsplit as such
+  if(quitMessage.contains(':') || quitMessage.contains('/'))
+    return false;
+
+  // now test if message consists only of two dns names as the RFC requests
+  // but also allow the commonly used "*.net *.split"
+  QRegExp hostRx("^(?:[\\w\\d-.]+|\\*)\\.[\\w\\d-]+\\s(?:[\\w\\d-.]+|\\*)\\.[\\w\\d-]+$");
+  if(hostRx.exactMatch(quitMessage))
+    return true;
+
+  return false;
+}
+
+void Netsplit::joinTimeout()
+{
+  QHash<QString, QStringList>::iterator it;
+  for(it = _joins.begin(); it != _joins.end(); ++it)
+    emit netsplitJoin(it.key(), it.value(),_quitMsg);
+  _joins.clear();
+  _discardTimer.stop();
+  emit finished();
+}
+
+void Netsplit::quitTimeout()
+{
+  QHash<QString, QStringList>::iterator it;
+  for(it = _quits.begin(); it != _quits.end(); ++it)
+    emit netsplitQuit(it.key(), it.value(),_quitMsg);
+}
diff --git a/src/core/netsplit.h b/src/core/netsplit.h
new file mode 100644 (file)
index 0000000..135a3f0
--- /dev/null
@@ -0,0 +1,102 @@
+/***************************************************************************
+ *   Copyright (C) 2005-09 by the Quassel Project                          *
+ *   devel@quassel-irc.org                                                 *
+ *                                                                         *
+ *   This program is free software; you can redistribute it and/or modify  *
+ *   it under the terms of the GNU General Public License as published by  *
+ *   the Free Software Foundation; either version 2 of the License, or     *
+ *   (at your option) version 3.                                           *
+ *                                                                         *
+ *   This program is distributed in the hope that it will be useful,       *
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
+ *   GNU General Public License for more details.                          *
+ *                                                                         *
+ *   You should have received a copy of the GNU General Public License     *
+ *   along with this program; if not, write to the                         *
+ *   Free Software Foundation, Inc.,                                       *
+ *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
+ ***************************************************************************/
+
+#ifndef NETSPLIT_H
+#define NETSPLIT_H
+
+#include <QObject>
+#include <QTimer>
+#include <QHash>
+#include <QStringList>
+
+class Netsplit : public QObject
+{
+  Q_OBJECT
+public:
+  Netsplit();
+
+  //! Add a user to the netsplit
+  /** Call this method if you noticed a netsplit.
+    * \note This method doesn't check if it really is a netsplit.
+    * \note Check with isNetsplit(const QString &quitMessage) before calling it!
+    *
+    * \param sender   The sender string of the quitting user
+    * \param channels The channels that user shared with us
+    * \param msg      The quit message
+    */
+  void userQuit(const QString &sender, const QStringList &channels, const QString &msg);
+
+  //! Remove a user from the netsplit
+  /** Call this method if a user joined after a netsplit occured.
+
+    *
+    * \param sender   The sender string of the joined user
+    * \param channels The channels that user shares with us
+    * \return true if user was found in the netsplit
+    */
+  bool userJoined(const QString &sender, const QString &channel);
+
+  //! Check if a string matches the criteria for a netsplit
+  /** \param quitMessage The message to be checked
+    * \return true if the message is a netsplit
+    */
+  static bool isNetsplit(const QString &quitMessage);
+
+signals:
+  //! A bulk-join of netsplitted users timed out
+  /** Whenever _joinTimer() times out, we consider the bulk-join to be finished and emit that signal
+    * for every channel
+    * \param channel The IRC channel
+    * \param users   A list of all users that joined that channel
+    * \param quitMessage The Quitmessage and thus the servers that got split
+    */
+  void netsplitJoin(const QString &channel, const QStringList &users, const QString &quitMessage);
+
+  //! A bulk-quit of netsplitted users timed out
+  /** Whenever _quitTimer() times out, we consider the bulk-quit to be finished and emit that signal
+    * for every channel
+    * \param channel The IRC channel
+    * \param users   A list of all users that quitted in that channel
+    * \param quitMessage The Quitmessage and thus the servers that got split
+    */
+  void netsplitQuit(const QString &channel, const QStringList &users, const QString &quitMessage);
+
+  //! The Netsplit is considered finished
+  /** This signal is emitted right after all netsplitJoin signals have been sent or whenever the
+    * internal timer signals a timeout.
+    * Simply delete the object and remove it from structures when you receive that signal.
+    */
+  void finished();
+
+private slots:
+  void joinTimeout();
+  void quitTimeout();
+
+private:
+  QString _quitMsg;
+  QHash<QString, QStringList> _joins;
+  QHash<QString, QStringList> _quits;
+
+  QTimer _joinTimer;
+  QTimer _quitTimer;
+  QTimer _discardTimer;
+};
+
+#endif // NETSPLIT_H