style button is smaller now
[quassel.git] / src / client / coreconnection.cpp
index 9c8889c..460b3b7 100644 (file)
@@ -38,16 +38,33 @@ CoreConnection::CoreConnection(CoreAccountModel *model, QObject *parent)
   : QObject(parent),
   _model(model),
   _blockSize(0),
+  _state(Disconnected),
+  _wantReconnect(false),
   _progressMinimum(0),
   _progressMaximum(-1),
   _progressValue(-1)
 {
   qRegisterMetaType<ConnectionState>("CoreConnection::ConnectionState");
-
 }
 
 void CoreConnection::init() {
+  Client::signalProxy()->setHeartBeatInterval(30);
   connect(Client::signalProxy(), SIGNAL(disconnected()), SLOT(coreSocketDisconnected()));
+  connect(Client::signalProxy(), SIGNAL(lagUpdated(int)), SIGNAL(lagUpdated(int)));
+
+  _reconnectTimer.setSingleShot(true);
+  connect(&_reconnectTimer, SIGNAL(timeout()), SLOT(reconnectTimeout()));
+
+#ifdef HAVE_KDE
+  connect(Solid::Networking::notifier(), SIGNAL(statusChanged(Solid::Networking::Status)),
+          SLOT(solidNetworkStatusChanged(Solid::Networking::Status)));
+#endif
+
+  CoreConnectionSettings s;
+  s.initAndNotify("PingTimeoutInterval", this, SLOT(pingTimeoutIntervalChanged(QVariant)), 60);
+  s.initAndNotify("ReconnectInterval", this, SLOT(reconnectIntervalChanged(QVariant)), 60);
+  s.notify("NetworkDetectionMode", this, SLOT(networkDetectionModeChanged(QVariant)));
+  networkDetectionModeChanged(s.networkDetectionMode());
 }
 
 void CoreConnection::setProgressText(const QString &text) {
@@ -86,21 +103,93 @@ void CoreConnection::updateProgress(int value, int max) {
   setProgressValue(value);
 }
 
-void CoreConnection::resetConnection() {
-  if(_socket) {
-    disconnect(_socket, 0, this, 0);
-    _socket->deleteLater();
-    _socket = 0;
+void CoreConnection::reconnectTimeout() {
+  if(!_socket) {
+    CoreConnectionSettings s;
+    if(_wantReconnect && s.autoReconnect()) {
+
+#ifdef HAVE_KDE
+      // If using Solid, we don't want to reconnect if we're offline
+      if(s.networkDetectionMode() == CoreConnectionSettings::UseSolid) {
+        if(Solid::Networking::status() != Solid::Networking::Connected
+           && Solid::Networking::status() != Solid::Networking::Unknown) {
+          return;
+        }
+      }
+#endif /* HAVE_KDE */
+
+      reconnectToCore();
+    }
   }
-  _blockSize = 0;
+}
 
-  _coreMsgBuffer.clear();
+void CoreConnection::networkDetectionModeChanged(const QVariant &vmode) {
+  CoreConnectionSettings s;
+  CoreConnectionSettings::NetworkDetectionMode mode = (CoreConnectionSettings::NetworkDetectionMode)vmode.toInt();
+  if(mode == CoreConnectionSettings::UsePingTimeout)
+    Client::signalProxy()->setMaxHeartBeatCount(s.pingTimeoutInterval() / 30);
+  else {
+    Client::signalProxy()->setMaxHeartBeatCount(-1);
+  }
+}
 
-  _netsToSync.clear();
-  _numNetsToSync = 0;
+void CoreConnection::pingTimeoutIntervalChanged(const QVariant &interval) {
+  CoreConnectionSettings s;
+  if(s.networkDetectionMode() == CoreConnectionSettings::UsePingTimeout)
+    Client::signalProxy()->setMaxHeartBeatCount(interval.toInt() / 30); // interval is 30 seconds
+}
 
-  setProgressMaximum(-1); // disable
-  emit connectionMsg(tr("Disconnected from core."));
+void CoreConnection::reconnectIntervalChanged(const QVariant &interval) {
+  _reconnectTimer.setInterval(interval.toInt() * 1000);
+}
+
+#ifdef HAVE_KDE
+
+void CoreConnection::solidNetworkStatusChanged(Solid::Networking::Status status) {
+  CoreConnectionSettings s;
+  if(s.networkDetectionMode() != CoreConnectionSettings::UseSolid)
+    return;
+
+  switch(status) {
+  case Solid::Networking::Unknown:
+  case Solid::Networking::Connected:
+    qDebug() << "Solid: Network status changed to connected or unknown";
+    if(state() == Disconnected) {
+      if(_wantReconnect && s.autoReconnect()) {
+        reconnectToCore();
+      }
+    }
+    break;
+  case Solid::Networking::Unconnected:
+    qDebug() << "Solid: Disconnected";
+    if(!isLocalConnection())
+      disconnectFromCore(tr("Network is down"), true);
+    break;
+  default:
+    break;
+  }
+}
+
+#endif
+
+bool CoreConnection::isEncrypted() const {
+#ifndef HAVE_SSL
+  return false;
+#else
+  QSslSocket *sock = qobject_cast<QSslSocket *>(_socket);
+  return isConnected() && sock && sock->isEncrypted();
+#endif
+}
+
+bool CoreConnection::isLocalConnection() const {
+  if(!isConnected())
+    return false;
+  if(currentAccount().isInternal())
+    return true;
+  if(_socket->peerAddress().isInSubnet(QHostAddress::LocalHost, 0x00ffffff))
+    return true;
+
+  return false;
 }
 
 void CoreConnection::socketStateChanged(QAbstractSocket::SocketState socketState) {
@@ -108,7 +197,7 @@ void CoreConnection::socketStateChanged(QAbstractSocket::SocketState socketState
 
   switch(socketState) {
   case QAbstractSocket::UnconnectedState:
-    text = tr("Disconnected.");
+    text = tr("Disconnected");
     break;
   case QAbstractSocket::HostLookupState:
     text = tr("Looking up %1...").arg(currentAccount().hostName());
@@ -117,7 +206,7 @@ void CoreConnection::socketStateChanged(QAbstractSocket::SocketState socketState
     text = tr("Connecting to %1...").arg(currentAccount().hostName());
     break;
   case QAbstractSocket::ConnectedState:
-    text = tr("Connected to %1.").arg(currentAccount().hostName());
+    text = tr("Connected to %1").arg(currentAccount().hostName());
     break;
   case QAbstractSocket::ClosingState:
     text = tr("Disconnecting from %1...").arg(currentAccount().hostName());
@@ -141,11 +230,9 @@ void CoreConnection::setState(QAbstractSocket::SocketState socketState) {
     break;
   case QAbstractSocket::HostLookupState:
   case QAbstractSocket::ConnectingState:
+  case QAbstractSocket::ConnectedState:  // we'll set it to Connected in connectionReady()
     state = Connecting;
     break;
-  case QAbstractSocket::ConnectedState:
-    state = Connected;
-    break;
   default:
     state = Disconnected;
   }
@@ -157,28 +244,19 @@ void CoreConnection::setState(ConnectionState state) {
   if(state != _state) {
     _state = state;
     emit stateChanged(state);
+    if(state == Disconnected)
+      emit disconnected();
   }
 }
 
-void CoreConnection::setWarningsHandler(const char *slot) {
-  resetWarningsHandler();
-  connect(this, SIGNAL(handleIgnoreWarnings(bool)), this, slot);
-}
-
-void CoreConnection::resetWarningsHandler() {
-  disconnect(this, SIGNAL(handleIgnoreWarnings(bool)), this, 0);
-}
-
 void CoreConnection::coreSocketError(QAbstractSocket::SocketError) {
   qDebug() << "coreSocketError" << _socket << _socket->errorString();
-  emit connectionError(_socket->errorString());
-  resetConnection();
+  disconnectFromCore(_socket->errorString(), true);
 }
 
 void CoreConnection::coreSocketDisconnected() {
-  setState(Disconnected);
-  emit disconnected();
-  resetConnection();
+  qDebug() << Q_FUNC_INFO;
+  resetConnection(true);
   // FIXME handle disconnects gracefully
 }
 
@@ -188,20 +266,20 @@ void CoreConnection::coreHasData() {
     QVariantMap msg = item.toMap();
     if(!msg.contains("MsgType")) {
       // This core is way too old and does not even speak our init protocol...
-      emit connectionError(tr("The Quassel Core you try to connect to is too old! Please consider upgrading."));
-      disconnectFromCore();
+      emit connectionErrorPopup(tr("The Quassel Core you try to connect to is too old! Please consider upgrading."));
+      disconnectFromCore(QString(), false);
       return;
     }
     if(msg["MsgType"] == "ClientInitAck") {
       clientInitAck(msg);
     } else if(msg["MsgType"] == "ClientInitReject") {
-      emit connectionError(msg["Error"].toString());
-      disconnectFromCore();
+      emit connectionErrorPopup(msg["Error"].toString());
+      disconnectFromCore(QString(), false);
       return;
     } else if(msg["MsgType"] == "CoreSetupAck") {
-      //emit coreSetupSuccess();
+      emit coreSetupSuccess();
     } else if(msg["MsgType"] == "CoreSetupReject") {
-      //emit coreSetupFailed(msg["Error"].toString());
+      emit coreSetupFailed(msg["Error"].toString());
     } else if(msg["MsgType"] == "ClientLoginReject") {
       loginFailed(msg["Error"].toString());
     } else if(msg["MsgType"] == "ClientLoginAck") {
@@ -216,8 +294,7 @@ void CoreConnection::coreHasData() {
       sessionStateReceived(msg["SessionState"].toMap());
       break; // this is definitively the last message we process here!
     } else {
-      emit connectionError(tr("<b>Invalid data received from core!</b><br>Disconnecting."));
-      disconnectFromCore();
+      disconnectFromCore(tr("Invalid data received from core"), false);
       return;
     }
   }
@@ -227,9 +304,48 @@ void CoreConnection::coreHasData() {
 }
 
 void CoreConnection::disconnectFromCore() {
-  if(isConnected()) {
-    Client::signalProxy()->removeAllPeers();
-    resetConnection();
+  disconnectFromCore(QString(), false);  // requested disconnect, so don't try to reconnect
+}
+
+void CoreConnection::disconnectFromCore(const QString &errorString, bool wantReconnect) {
+  if(!wantReconnect)
+    _reconnectTimer.stop();
+
+  if(errorString.isEmpty())
+    emit connectionError(tr("Disconnected"));
+  else
+    emit connectionError(errorString);
+
+  Client::signalProxy()->removeAllPeers();
+  resetConnection(wantReconnect);
+}
+
+void CoreConnection::resetConnection(bool wantReconnect) {
+  _wantReconnect = wantReconnect;
+
+  if(_socket) {
+    disconnect(_socket, 0, this, 0);
+    _socket->deleteLater();
+    _socket = 0;
+  }
+  _blockSize = 0;
+
+  _coreMsgBuffer.clear();
+
+  _netsToSync.clear();
+  _numNetsToSync = 0;
+
+  setProgressMaximum(-1); // disable
+  setState(Disconnected);
+  emit lagUpdated(-1);
+
+  emit connectionMsg(tr("Disconnected from core."));
+  emit encrypted(false);
+
+  // initiate if a reconnect if appropriate
+  CoreConnectionSettings s;
+  if(wantReconnect && s.autoReconnect()) {
+    _reconnectTimer.start();
   }
 }
 
@@ -239,22 +355,35 @@ void CoreConnection::reconnectToCore() {
 }
 
 bool CoreConnection::connectToCore(AccountId accId) {
+  if(isConnected())
+    return false;
+
   CoreAccountSettings s;
 
-  if(!accId.isValid()) {
-    // check our settings and figure out what to do
-    if(!s.autoConnectOnStartup())
-      return false;
-    if(s.autoConnectToFixedAccount())
-      accId = s.autoConnectAccount();
-    else
-      accId = s.lastAccount();
-    if(!accId.isValid())
+  // FIXME: Don't force connection to internal core in mono client
+  if(Quassel::runMode() == Quassel::Monolithic) {
+    _account = accountModel()->account(accountModel()->internalAccount());
+    Q_ASSERT(_account.isValid());
+  } else {
+    if(!accId.isValid()) {
+      // check our settings and figure out what to do
+      if(!s.autoConnectOnStartup())
+        return false;
+      if(s.autoConnectToFixedAccount())
+        accId = s.autoConnectAccount();
+      else
+        accId = s.lastAccount();
+      if(!accId.isValid())
+        return false;
+    }
+    _account = accountModel()->account(accId);
+    if(!_account.accountId().isValid()) {
       return false;
-  }
-  _account = accountModel()->account(accId);
-  if(!_account.accountId().isValid()) {
-    return false;
+    }
+    if(Quassel::runMode() != Quassel::Monolithic) {
+      if(_account.isInternal())
+        return false;
+    }
   }
 
   s.setLastAccount(accId);
@@ -263,15 +392,36 @@ bool CoreConnection::connectToCore(AccountId accId) {
 }
 
 void CoreConnection::connectToCurrentAccount() {
-  resetConnection();
+  resetConnection(false);
+
+  if(currentAccount().isInternal()) {
+    if(Quassel::runMode() != Quassel::Monolithic) {
+      qWarning() << "Cannot connect to internal core in client-only mode!";
+      return;
+    }
+    emit startInternalCore();
+    emit connectToInternalCore(Client::instance()->signalProxy());
+    return;
+  }
+
+  CoreAccountSettings s;
 
   Q_ASSERT(!_socket);
 #ifdef HAVE_SSL
   QSslSocket *sock = new QSslSocket(Client::instance());
+  // make sure the warning is shown if we happen to connect without SSL support later
+  s.setAccountValue("ShowNoClientSslWarning", true);
 #else
   if(_account.useSsl()) {
-    emit connectionError(tr("<b>This client is built without SSL Support!</b><br />Disable the usage of SSL in the account settings."));
-    return;
+    if(s.accountValue("ShowNoClientSslWarning", true).toBool()) {
+      bool accepted = false;
+      emit handleNoSslInClient(&accepted);
+      if(!accepted) {
+        emit connectionError(tr("Unencrypted connection canceled"));
+        return;
+      }
+      s.setAccountValue("ShowNoClientSslWarning", false);
+    }
   }
   QTcpSocket *sock = new QTcpSocket(Client::instance());
 #endif
@@ -318,9 +468,9 @@ void CoreConnection::clientInitAck(const QVariantMap &msg) {
   // Core has accepted our version info and sent its own. Let's see if we accept it as well...
   uint ver = msg["ProtocolVersion"].toUInt();
   if(ver < Quassel::buildInfo().clientNeedsProtocol) {
-    emit connectionError(tr("<b>The Quassel Core you are trying to connect to is too old!</b><br>"
+    emit connectionErrorPopup(tr("<b>The Quassel Core you are trying to connect to is too old!</b><br>"
         "Need at least core/client protocol v%1 to connect.").arg(Quassel::buildInfo().clientNeedsProtocol));
-    disconnectFromCore();
+    disconnectFromCore(QString(), false);
     return;
   }
 
@@ -331,28 +481,88 @@ void CoreConnection::clientInitAck(const QVariantMap &msg) {
 #endif
 
   _coreMsgBuffer = msg;
+
 #ifdef HAVE_SSL
+  CoreAccountSettings s;
   if(currentAccount().useSsl()) {
     if(msg["SupportSsl"].toBool()) {
+      // Make sure the warning is shown next time we don't have SSL in the core
+      s.setAccountValue("ShowNoCoreSslWarning", true);
+
       QSslSocket *sslSocket = qobject_cast<QSslSocket *>(_socket);
       Q_ASSERT(sslSocket);
-      connect(sslSocket, SIGNAL(encrypted()), this, SLOT(sslSocketEncrypted()));
-      connect(sslSocket, SIGNAL(sslErrors(const QList<QSslError> &)), this, SLOT(sslErrors(const QList<QSslError> &)));
-
+      connect(sslSocket, SIGNAL(encrypted()), SLOT(sslSocketEncrypted()));
+      connect(sslSocket, SIGNAL(sslErrors(const QList<QSslError> &)), SLOT(sslErrors()));
       sslSocket->startClientEncryption();
     } else {
-      emit connectionError(tr("<b>The Quassel Core you are trying to connect to does not support SSL!</b><br />If you want to connect anyways, disable the usage of SSL in the account settings."));
-      disconnectFromCore();
+      if(s.accountValue("ShowNoCoreSslWarning", true).toBool()) {
+        bool accepted = false;
+        emit handleNoSslInCore(&accepted);
+        if(!accepted) {
+          disconnectFromCore(tr("Unencrypted connection canceled"), false);
+          return;
+        }
+        s.setAccountValue("ShowNoCoreSslWarning", false);
+        s.setAccountValue("SslCert", QString());
+      }
+      connectionReady();
     }
     return;
   }
 #endif
   // if we use SSL we wait for the next step until every SSL warning has been cleared
   connectionReady();
+}
 
+#ifdef HAVE_SSL
+
+void CoreConnection::sslSocketEncrypted() {
+  QSslSocket *socket = qobject_cast<QSslSocket *>(sender());
+  Q_ASSERT(socket);
+
+  if(!socket->sslErrors().count()) {
+    // Cert is valid, so we don't want to store it as known
+    // That way, a warning will appear in case it becomes invalid at some point
+    CoreAccountSettings s;
+    s.setAccountValue("SSLCert", QString());
+  }
+
+  emit encrypted(true);
+  connectionReady();
+}
+
+void CoreConnection::sslErrors() {
+  QSslSocket *socket = qobject_cast<QSslSocket *>(sender());
+  Q_ASSERT(socket);
+
+  CoreAccountSettings s;
+  QByteArray knownDigest = s.accountValue("SslCert").toByteArray();
+
+  if(knownDigest != socket->peerCertificate().digest()) {
+    bool accepted = false;
+    bool permanently = false;
+    emit handleSslErrors(socket, &accepted, &permanently);
+
+    if(!accepted) {
+      disconnectFromCore(tr("Unencrypted connection canceled"), false);
+      return;
+    }
+
+    if(permanently)
+      s.setAccountValue("SslCert", socket->peerCertificate().digest());
+    else
+      s.setAccountValue("SslCert", QString());
+  }
+
+  socket->ignoreSslErrors();
 }
 
+#endif /* HAVE_SSL */
+
 void CoreConnection::connectionReady() {
+  setState(Connected);
+  emit connectionMsg(tr("Connected to %1").arg(currentAccount().accountName()));
+
   if(!_coreMsgBuffer["Configured"].toBool()) {
     // start wizard
     emit startCoreSetup(_coreMsgBuffer["StorageBackends"].toList());
@@ -360,15 +570,22 @@ void CoreConnection::connectionReady() {
     loginToCore();
   }
   _coreMsgBuffer.clear();
-  resetWarningsHandler();
 }
 
-void CoreConnection::loginToCore() {
+void CoreConnection::loginToCore(const QString &user, const QString &password, bool remember) {
+  _account.setUser(user);
+  _account.setPassword(password);
+  _account.setStorePassword(remember);
+  loginToCore();
+}
+
+void CoreConnection::loginToCore(const QString &prevError) {
   emit connectionMsg(tr("Logging in..."));
-  if(currentAccount().user().isEmpty() || currentAccount().password().isEmpty()) {
-    emit userAuthenticationRequired(&_account);  // *must* be a synchronous call
-    if(currentAccount().user().isEmpty() || currentAccount().password().isEmpty()) {
-      disconnectFromCore();
+  if(currentAccount().user().isEmpty() || currentAccount().password().isEmpty() || !prevError.isEmpty()) {
+    bool valid = false;
+    emit userAuthenticationRequired(&_account, &valid, prevError);  // *must* be a synchronous call
+    if(!valid || currentAccount().user().isEmpty() || currentAccount().password().isEmpty()) {
+      disconnectFromCore(tr("Login canceled"), false);
       return;
     }
   }
@@ -381,16 +598,18 @@ void CoreConnection::loginToCore() {
 }
 
 void CoreConnection::loginFailed(const QString &error) {
-  emit userAuthenticationRequired(&_account, error);  // *must* be a synchronous call
-  if(currentAccount().user().isEmpty() || currentAccount().password().isEmpty()) {
-    disconnectFromCore();
-    return;
-  }
-  loginToCore();
+  loginToCore(error);
 }
 
 void CoreConnection::loginSuccess() {
   updateProgress(0, 0);
+
+  // save current account data
+  _model->createOrUpdateAccount(currentAccount());
+  _model->save();
+
+  _reconnectTimer.stop();
+
   setProgressText(tr("Receiving session state"));
   setState(Synchronizing);
   emit connectionMsg(tr("Synchronizing to %1...").arg(currentAccount().accountName()));
@@ -403,10 +622,16 @@ void CoreConnection::sessionStateReceived(const QVariantMap &state) {
   disconnect(_socket, SIGNAL(readyRead()), this, 0);
   disconnect(_socket, SIGNAL(connected()), this, 0);
 
-  //Client::instance()->setConnectedToCore(currentAccount().accountId(), _socket);
   syncToCore(state);
 }
 
+void CoreConnection::internalSessionStateReceived(const QVariant &packedState) {
+  updateProgress(100, 100);
+
+  setState(Synchronizing);
+  syncToCore(packedState.toMap());
+}
+
 void CoreConnection::syncToCore(const QVariantMap &sessionState) {
   setProgressText(tr("Receiving network states"));
   updateProgress(0, 100);
@@ -445,8 +670,9 @@ void CoreConnection::syncToCore(const QVariantMap &sessionState) {
   checkSyncState();
 }
 
+// this is also called for destroyed networks!
 void CoreConnection::networkInitDone() {
-  Network *net = qobject_cast<Network *>(sender());
+  QObject *net = sender();
   Q_ASSERT(net);
   disconnect(net, 0, this, 0);
   _netsToSync.remove(net);
@@ -455,10 +681,17 @@ void CoreConnection::networkInitDone() {
 }
 
 void CoreConnection::checkSyncState() {
-  if(_netsToSync.isEmpty()) {
+  if(_netsToSync.isEmpty() && state() >= Synchronizing) {
     setState(Synchronized);
-    setProgressText(QString());
+    setProgressText(tr("Synchronized to %1").arg(currentAccount().accountName()));
     setProgressMaximum(-1);
     emit synchronized();
   }
 }
+
+void CoreConnection::doCoreSetup(const QVariant &setupData) {
+  QVariantMap setup;
+  setup["MsgType"] = "CoreSetupData";
+  setup["SetupData"] = setupData;
+  SignalProxy::writeDataToDevice(_socket, setup);
+}