Don't tinker with config and data files on Mac for now
[quassel.git] / src / core / core.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-09 by the Quassel Project                          *
3  *   devel@quassel-irc.org                                                 *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) version 3.                                           *
9  *                                                                         *
10  *   This program is distributed in the hope that it will be useful,       *
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
19  ***************************************************************************/
20
21 #include <QMetaObject>
22 #include <QMetaMethod>
23 #include <QCoreApplication>
24
25 #include "core.h"
26 #include "coresession.h"
27 #include "coresettings.h"
28 #include "quassel.h"
29 #include "signalproxy.h"
30 #include "sqlitestorage.h"
31 #include "network.h"
32 #include "logger.h"
33
34 #include "util.h"
35
36 // migration related
37 #include <QFile>
38
39 Core *Core::instanceptr = 0;
40
41 Core *Core::instance() {
42   if(instanceptr) return instanceptr;
43   instanceptr = new Core();
44   instanceptr->init();
45   return instanceptr;
46 }
47
48 void Core::destroy() {
49   delete instanceptr;
50   instanceptr = 0;
51 }
52
53 Core::Core() : storage(0) {
54   _startTime = QDateTime::currentDateTime().toUTC();  // for uptime :)
55
56   loadTranslation(QLocale::system());
57
58 #ifndef Q_WS_MAC
59
60   // FIXME: MIGRATION 0.3 -> 0.4: Move database and core config to new location
61   // Move settings, note this does not delete the old files
62 #ifdef Q_WS_WIN
63   QSettings::Format format = QSettings::IniFormat;
64 #else
65   QSettings::Format format = QSettings::NativeFormat;
66 #endif
67   QString newFilePath = Quassel::configDirPath() + "quasselcore"
68   + ((format == QSettings::NativeFormat) ? QLatin1String(".conf") : QLatin1String(".ini"));
69   QSettings newSettings(newFilePath, format);
70
71   if(newSettings.value("Config/Version").toUInt() != 1) {
72     qWarning() << "\n\n*** IMPORTANT: Config and data file locations have changed. Attempting to auto-migrate your core settings...";
73 #   ifdef Q_WS_MAC
74     QString org = "quassel-irc.org";
75 #   else
76     QString org = "Quassel Project";
77 #   endif
78     QSettings oldSettings(org, "Quassel Core");
79     if(oldSettings.allKeys().count()) {
80       foreach(QString key, oldSettings.allKeys())
81         newSettings.setValue(key, oldSettings.value(key));
82       newSettings.setValue("Config/Version", 1);
83       qWarning() << "*   Your core settings have been migrated to" << newFilePath;
84
85 #ifdef Q_OS_WIN32
86       QString quasselDir = qgetenv("APPDATA") + "/quassel/";
87 #elif defined Q_WS_MAC
88       QString quasselDir = QDir::homePath() + "/Library/Application Support/Quassel/";
89 #else
90       QString quasselDir = QDir::homePath() + "/.quassel/";
91 #endif
92
93       QFileInfo info(Quassel::configDirPath() + "quassel-storage.sqlite");
94       if(!info.exists()) {
95       // move database, if we found it
96         QFile oldDb(quasselDir + "quassel-storage.sqlite");
97         if(oldDb.exists()) {
98           bool success = oldDb.rename(Quassel::configDirPath() + "quassel-storage.sqlite");
99           if(success)
100             qWarning() << "*   Your database has been moved to" << Quassel::configDirPath() + "quassel-storage.sqlite";
101           else
102             qWarning() << "!!! Moving your database has failed. Please move it manually into" << Quassel::configDirPath();
103         }
104       }
105       // move certificate
106       QFileInfo certInfo(quasselDir + "quasselCert.pem");
107       if(certInfo.exists()) {
108         QFile cert(quasselDir + "quasselCert.pem");
109         bool success = cert.rename(Quassel::configDirPath() + "quasselCert.pem");
110         if(success)
111           qWarning() << "*   Your certificate has been moved to" << Quassel::configDirPath() + "quasselCert.pem";
112         else
113           qWarning() << "!!! Moving your certificate has failed. Please move it manually into" << Quassel::configDirPath();
114       }
115       qWarning() << "*** Migration completed.\n\n";
116     }
117   }
118 #endif /* !Q_WS_MAC */
119   // MIGRATION end
120
121   // Register storage backends here!
122   registerStorageBackend(new SqliteStorage(this));
123
124   if(!_storageBackends.count()) {
125     qWarning() << qPrintable(tr("Could not initialize any storage backend! Exiting..."));
126     qWarning() << qPrintable(tr("Currently, Quassel only supports SQLite3. You need to build your\n"
127                                 "Qt library with the sqlite plugin enabled in order for quasselcore\n"
128                                 "to work."));
129     exit(1); // TODO make this less brutal (especially for mono client -> popup)
130   }
131   connect(&_storageSyncTimer, SIGNAL(timeout()), this, SLOT(syncStorage()));
132   _storageSyncTimer.start(10 * 60 * 1000); // 10 minutes
133 }
134
135 void Core::init() {
136   configured = false;
137
138   CoreSettings cs;
139
140   if(!(configured = initStorage(cs.storageSettings().toMap()))) {
141     qWarning() << "Core is currently not configured! Please connect with a Quassel Client for basic setup.";
142
143   }
144
145   connect(&_server, SIGNAL(newConnection()), this, SLOT(incomingConnection()));
146   connect(&_v6server, SIGNAL(newConnection()), this, SLOT(incomingConnection()));
147   if(!startListening()) exit(1); // TODO make this less brutal
148 }
149
150 Core::~Core() {
151   foreach(QTcpSocket *socket, blocksizes.keys()) {
152     socket->disconnectFromHost();  // disconnect non authed clients
153   }
154   qDeleteAll(sessions);
155   qDeleteAll(_storageBackends);
156 }
157
158 /*** Session Restore ***/
159
160 void Core::saveState() {
161   CoreSettings s;
162   QVariantMap state;
163   QVariantList activeSessions;
164   foreach(UserId user, instance()->sessions.keys()) activeSessions << QVariant::fromValue<UserId>(user);
165   state["CoreStateVersion"] = 1;
166   state["ActiveSessions"] = activeSessions;
167   s.setCoreState(state);
168 }
169
170 void Core::restoreState() {
171   if(!instance()->configured) {
172     // qWarning() << qPrintable(tr("Cannot restore a state for an unconfigured core!"));
173     return;
174   }
175   if(instance()->sessions.count()) {
176     qWarning() << qPrintable(tr("Calling restoreState() even though active sessions exist!"));
177     return;
178   }
179   CoreSettings s;
180   /* We don't check, since we are at the first version since switching to Git
181   uint statever = s.coreState().toMap()["CoreStateVersion"].toUInt();
182   if(statever < 1) {
183     qWarning() << qPrintable(tr("Core state too old, ignoring..."));
184     return;
185   }
186   */
187   QVariantList activeSessions = s.coreState().toMap()["ActiveSessions"].toList();
188   if(activeSessions.count() > 0) {
189     quInfo() << "Restoring previous core state...";
190     foreach(QVariant v, activeSessions) {
191       UserId user = v.value<UserId>();
192       instance()->createSession(user, true);
193     }
194   }
195 }
196
197 /*** Core Setup ***/
198 QString Core::setupCoreForInternalUsage() {
199   Q_ASSERT(!_storageBackends.isEmpty());
200   QVariantMap setupData;
201   qsrand(QDateTime::currentDateTime().toTime_t());
202   int pass = 0;
203   for(int i = 0; i < 10; i++) {
204     pass *= 10;
205     pass += qrand() % 10;
206   }
207   setupData["AdminUser"] = "AdminUser";
208   setupData["AdminPasswd"] = QString::number(pass);
209   setupData["Backend"] = _storageBackends[_storageBackends.keys().first()]->displayName();
210   return setupCore(setupData);
211 }
212
213 QString Core::setupCore(QVariantMap setupData) {
214   QString user = setupData.take("AdminUser").toString();
215   QString password = setupData.take("AdminPasswd").toString();
216   if(user.isEmpty() || password.isEmpty()) {
217     return tr("Admin user or password not set.");
218   }
219   if(!initStorage(setupData, true)) {
220     return tr("Could not setup storage!");
221   }
222   CoreSettings s;
223   s.setStorageSettings(setupData);
224   quInfo() << qPrintable(tr("Creating admin user..."));
225   storage->addUser(user, password);
226   startListening();  // TODO check when we need this
227   return QString();
228 }
229
230 /*** Storage Handling ***/
231
232 bool Core::registerStorageBackend(Storage *backend) {
233   if(backend->isAvailable()) {
234     _storageBackends[backend->displayName()] = backend;
235     return true;
236   } else {
237     backend->deleteLater();
238     return false;
239   }
240 }
241
242 void Core::unregisterStorageBackend(Storage *backend) {
243   _storageBackends.remove(backend->displayName());
244   backend->deleteLater();
245 }
246
247 // old db settings:
248 // "Type" => "sqlite"
249 bool Core::initStorage(QVariantMap dbSettings, bool setup) {
250   QString backend = dbSettings["Backend"].toString();
251   if(backend.isEmpty()) {
252     //qWarning() << "No storage backend selected!";
253     return configured = false;
254   }
255
256   if(_storageBackends.contains(backend)) {
257     storage = _storageBackends[backend];
258   } else {
259     qCritical() << "Selected storage backend is not available:" << backend;
260     return configured = false;
261   }
262   if(!storage->init(dbSettings)) {
263     if(!setup || !(storage->setup(dbSettings) && storage->init(dbSettings))) {
264       qCritical() << "Could not init storage!";
265       storage = 0;
266       return configured = false;
267     }
268   }
269   // delete all other backends
270   foreach(Storage *s, _storageBackends.values()) {
271     if(s != storage) s->deleteLater();
272   }
273   _storageBackends.clear();
274
275   connect(storage, SIGNAL(bufferInfoUpdated(UserId, const BufferInfo &)), this, SIGNAL(bufferInfoUpdated(UserId, const BufferInfo &)));
276   return configured = true;
277 }
278
279 void Core::syncStorage() {
280   if(storage) storage->sync();
281 }
282
283 /*** Storage Access ***/
284 bool Core::createNetwork(UserId user, NetworkInfo &info) {
285   NetworkId networkId = instance()->storage->createNetwork(user, info);
286   if(!networkId.isValid())
287     return false;
288
289   info.networkId = networkId;
290   return true;
291 }
292
293 /*** Network Management ***/
294
295 bool Core::startListening() {
296   // in mono mode we only start a local port if a port is specified in the cli call
297   if(Quassel::runMode() == Quassel::Monolithic && !Quassel::isOptionSet("port"))
298     return true;
299
300   bool success = false;
301   uint port = Quassel::optionValue("port").toUInt();
302
303   const QString listen = Quassel::optionValue("listen");
304   const QStringList listen_list = listen.split(",", QString::SkipEmptyParts);
305   if(listen_list.size() > 0) {
306     foreach (const QString listen_term, listen_list) {  // TODO: handle multiple interfaces for same TCP version gracefully
307       QHostAddress addr;
308       if(!addr.setAddress(listen_term)) {
309         qCritical() << qPrintable(
310           tr("Invalid listen address %1")
311             .arg(listen_term)
312         );
313       } else {
314         switch(addr.protocol()) {
315           case QAbstractSocket::IPv4Protocol:
316             if(_server.listen(addr, port)) {
317               quInfo() << qPrintable(
318                 tr("Listening for GUI clients on IPv4 %1 port %2 using protocol version %3")
319                   .arg(addr.toString())
320                   .arg(_server.serverPort())
321                   .arg(Quassel::buildInfo().protocolVersion)
322               );
323               success = true;
324             } else
325               quWarning() << qPrintable(
326                 tr("Could not open IPv4 interface %1:%2: %3")
327                   .arg(addr.toString())
328                   .arg(port)
329                   .arg(_server.errorString()));
330             break;
331           case QAbstractSocket::IPv6Protocol:
332             if(_v6server.listen(addr, port)) {
333               quInfo() << qPrintable(
334                 tr("Listening for GUI clients on IPv6 %1 port %2 using protocol version %3")
335                   .arg(addr.toString())
336                   .arg(_v6server.serverPort())
337                   .arg(Quassel::buildInfo().protocolVersion)
338               );
339               success = true;
340             } else {
341               // if v4 succeeded on Any, the port will be already in use - don't display the error then
342               // FIXME: handle this more sanely, make sure we can listen to both v4 and v6 by default!
343               if(!success || _v6server.serverError() != QAbstractSocket::AddressInUseError)
344                 quWarning() << qPrintable(
345                   tr("Could not open IPv6 interface %1:%2: %3")
346                   .arg(addr.toString())
347                   .arg(port)
348                   .arg(_v6server.errorString()));
349             }
350             break;
351           default:
352             qCritical() << qPrintable(
353               tr("Invalid listen address %1, unknown network protocol")
354                   .arg(listen_term)
355             );
356             break;
357         }
358       }
359     }
360   }
361   if(!success)
362     quError() << qPrintable(tr("Could not open any network interfaces to listen on!"));
363
364   return success;
365 }
366
367 void Core::stopListening(const QString &reason) {
368   bool wasListening = false;
369   if(_server.isListening()) {
370     wasListening = true;
371     _server.close();
372   }
373   if(_v6server.isListening()) {
374     wasListening = true;
375     _v6server.close();
376   }
377   if(wasListening) {
378     if(reason.isEmpty())
379       quInfo() << "No longer listening for GUI clients.";
380     else
381       quInfo() << qPrintable(reason);
382   }
383 }
384
385 void Core::incomingConnection() {
386   QTcpServer *server = qobject_cast<QTcpServer *>(sender());
387   Q_ASSERT(server);
388   while(server->hasPendingConnections()) {
389     QTcpSocket *socket = server->nextPendingConnection();
390     connect(socket, SIGNAL(disconnected()), this, SLOT(clientDisconnected()));
391     connect(socket, SIGNAL(readyRead()), this, SLOT(clientHasData()));
392     connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(socketError(QAbstractSocket::SocketError)));
393
394     QVariantMap clientInfo;
395     blocksizes.insert(socket, (quint32)0);
396     quInfo() << qPrintable(tr("Client connected from"))  << qPrintable(socket->peerAddress().toString());
397
398     if(!configured) {
399       stopListening(tr("Closing server for basic setup."));
400     }
401   }
402 }
403
404 void Core::clientHasData() {
405   QTcpSocket *socket = dynamic_cast<QTcpSocket*>(sender());
406   Q_ASSERT(socket && blocksizes.contains(socket));
407   QVariant item;
408   while(SignalProxy::readDataFromDevice(socket, blocksizes[socket], item)) {
409     QVariantMap msg = item.toMap();
410     processClientMessage(socket, msg);
411     if(!blocksizes.contains(socket)) break;  // this socket is no longer ours to handle!
412   }
413 }
414
415 void Core::processClientMessage(QTcpSocket *socket, const QVariantMap &msg) {
416   if(!msg.contains("MsgType")) {
417     // Client is way too old, does not even use the current init format
418     qWarning() << qPrintable(tr("Antique client trying to connect... refusing."));
419     socket->close();
420     return;
421   }
422   // OK, so we have at least an init message format we can understand
423   if(msg["MsgType"] == "ClientInit") {
424     QVariantMap reply;
425
426     // Just version information -- check it!
427     uint ver = msg["ProtocolVersion"].toUInt();
428     if(ver < Quassel::buildInfo().coreNeedsProtocol) {
429       reply["MsgType"] = "ClientInitReject";
430       reply["Error"] = tr("<b>Your Quassel Client is too old!</b><br>"
431       "This core needs at least client/core protocol version %1.<br>"
432       "Please consider upgrading your client.").arg(Quassel::buildInfo().coreNeedsProtocol);
433       SignalProxy::writeDataToDevice(socket, reply);
434       qWarning() << qPrintable(tr("Client")) << qPrintable(socket->peerAddress().toString()) << qPrintable(tr("too old, rejecting."));
435       socket->close(); return;
436     }
437
438     reply["CoreVersion"] = Quassel::buildInfo().fancyVersionString;
439     reply["CoreDate"] = Quassel::buildInfo().buildDate;
440     reply["ProtocolVersion"] = Quassel::buildInfo().protocolVersion;
441     // TODO: Make the core info configurable
442     int uptime = startTime().secsTo(QDateTime::currentDateTime().toUTC());
443     int updays = uptime / 86400; uptime %= 86400;
444     int uphours = uptime / 3600; uptime %= 3600;
445     int upmins = uptime / 60;
446     reply["CoreInfo"] = tr("<b>Quassel Core Version %1</b><br>"
447                            "Built: %2<br>"
448                            "Up %3d%4h%5m (since %6)").arg(Quassel::buildInfo().fancyVersionString)
449                                                      .arg(Quassel::buildInfo().buildDate)
450       .arg(updays).arg(uphours,2,10,QChar('0')).arg(upmins,2,10,QChar('0')).arg(startTime().toString(Qt::TextDate));
451
452 #ifdef HAVE_SSL
453     SslServer *sslServer = qobject_cast<SslServer *>(&_server);
454     QSslSocket *sslSocket = qobject_cast<QSslSocket *>(socket);
455     bool supportSsl = (bool)sslServer && (bool)sslSocket && sslServer->isCertValid();
456 #else
457     bool supportSsl = false;
458 #endif
459
460 #ifndef QT_NO_COMPRESS
461     bool supportsCompression = true;
462 #else
463     bool supportsCompression = false;
464 #endif
465
466     reply["SupportSsl"] = supportSsl;
467     reply["SupportsCompression"] = supportsCompression;
468     // switch to ssl/compression after client has been informed about our capabilities (see below)
469
470     reply["LoginEnabled"] = true;
471
472     // check if we are configured, start wizard otherwise
473     if(!configured) {
474       reply["Configured"] = false;
475       QList<QVariant> backends;
476       foreach(Storage *backend, _storageBackends.values()) {
477         QVariantMap v;
478         v["DisplayName"] = backend->displayName();
479         v["Description"] = backend->description();
480         backends.append(v);
481       }
482       reply["StorageBackends"] = backends;
483       reply["LoginEnabled"] = false;
484     } else {
485       reply["Configured"] = true;
486     }
487     clientInfo[socket] = msg; // store for future reference
488     reply["MsgType"] = "ClientInitAck";
489     SignalProxy::writeDataToDevice(socket, reply);
490
491 #ifdef HAVE_SSL
492     // after we told the client that we are ssl capable we switch to ssl mode
493     if(supportSsl && msg["UseSsl"].toBool()) {
494       qDebug() << qPrintable(tr("Starting TLS for Client:"))  << qPrintable(socket->peerAddress().toString());
495       connect(sslSocket, SIGNAL(sslErrors(const QList<QSslError> &)), this, SLOT(sslErrors(const QList<QSslError> &)));
496       sslSocket->startServerEncryption();
497     }
498 #endif
499
500 #ifndef QT_NO_COMPRESS
501     if(supportsCompression && msg["UseCompression"].toBool()) {
502       socket->setProperty("UseCompression", true);
503       qDebug() << "Using compression for Client:" << qPrintable(socket->peerAddress().toString());
504     }
505 #endif
506
507   } else {
508     // for the rest, we need an initialized connection
509     if(!clientInfo.contains(socket)) {
510       QVariantMap reply;
511       reply["MsgType"] = "ClientLoginReject";
512       reply["Error"] = tr("<b>Client not initialized!</b><br>You need to send an init message before trying to login.");
513       SignalProxy::writeDataToDevice(socket, reply);
514       qWarning() << qPrintable(tr("Client")) << qPrintable(socket->peerAddress().toString()) << qPrintable(tr("did not send an init message before trying to login, rejecting."));
515       socket->close(); return;
516     }
517     if(msg["MsgType"] == "CoreSetupData") {
518       QVariantMap reply;
519       QString result = setupCore(msg["SetupData"].toMap());
520       if(!result.isEmpty()) {
521         reply["MsgType"] = "CoreSetupReject";
522         reply["Error"] = result;
523       } else {
524         reply["MsgType"] = "CoreSetupAck";
525       }
526       SignalProxy::writeDataToDevice(socket, reply);
527     } else if(msg["MsgType"] == "ClientLogin") {
528       QVariantMap reply;
529       UserId uid = storage->validateUser(msg["User"].toString(), msg["Password"].toString());
530       if(uid == 0) {
531         reply["MsgType"] = "ClientLoginReject";
532         reply["Error"] = tr("<b>Invalid username or password!</b><br>The username/password combination you supplied could not be found in the database.");
533         SignalProxy::writeDataToDevice(socket, reply);
534         return;
535       }
536       reply["MsgType"] = "ClientLoginAck";
537       SignalProxy::writeDataToDevice(socket, reply);
538       quInfo() << qPrintable(tr("Client")) << qPrintable(socket->peerAddress().toString()) << qPrintable(tr("initialized and authenticated successfully as \"%1\" (UserId: %2).").arg(msg["User"].toString()).arg(uid.toInt()));
539       setupClientSession(socket, uid);
540     }
541   }
542 }
543
544 // Potentially called during the initialization phase (before handing the connection off to the session)
545 void Core::clientDisconnected() {
546   QTcpSocket *socket = qobject_cast<QTcpSocket *>(sender());
547   if(socket) {
548     // here it's safe to call methods on socket!
549     quInfo() << qPrintable(tr("Non-authed client disconnected.")) << qPrintable(socket->peerAddress().toString());
550     blocksizes.remove(socket);
551     clientInfo.remove(socket);
552     socket->deleteLater();
553   } else {
554     // we have to crawl through the hashes and see if we find a victim to remove
555     qDebug() << qPrintable(tr("Non-authed client disconnected. (socket allready destroyed)"));
556
557     // DO NOT CALL ANY METHODS ON socket!!
558     socket = static_cast<QTcpSocket *>(sender());
559
560     QHash<QTcpSocket *, quint32>::iterator blockSizeIter = blocksizes.begin();
561     while(blockSizeIter != blocksizes.end()) {
562       if(blockSizeIter.key() == socket) {
563         blockSizeIter = blocksizes.erase(blockSizeIter);
564       } else {
565         blockSizeIter++;
566       }
567     }
568
569     QHash<QTcpSocket *, QVariantMap>::iterator clientInfoIter = clientInfo.begin();
570     while(clientInfoIter != clientInfo.end()) {
571       if(clientInfoIter.key() == socket) {
572         clientInfoIter = clientInfo.erase(clientInfoIter);
573       } else {
574         clientInfoIter++;
575       }
576     }
577   }
578
579
580   // make server listen again if still not configured
581   if (!configured) {
582     startListening();
583   }
584
585   // TODO remove unneeded sessions - if necessary/possible...
586   // Suggestion: kill sessions if they are not connected to any network and client.
587 }
588
589 void Core::setupClientSession(QTcpSocket *socket, UserId uid) {
590   // Find or create session for validated user
591   SessionThread *sess;
592   if(sessions.contains(uid)) sess = sessions[uid];
593   else sess = createSession(uid);
594   // Hand over socket, session then sends state itself
595   disconnect(socket, 0, this, 0);
596   blocksizes.remove(socket);
597   clientInfo.remove(socket);
598   if(!sess) {
599     qWarning() << qPrintable(tr("Could not initialize session for client:")) << qPrintable(socket->peerAddress().toString());
600     socket->close();
601   }
602   sess->addClient(socket);
603 }
604
605 void Core::setupInternalClientSession(SignalProxy *proxy) {
606   if(!configured) {
607     stopListening();
608     setupCoreForInternalUsage();
609   }
610
611   UserId uid = storage->internalUser();
612
613   // Find or create session for validated user
614   SessionThread *sess;
615   if(sessions.contains(uid))
616     sess = sessions[uid];
617   else
618     sess = createSession(uid);
619   sess->addClient(proxy);
620 }
621
622 SessionThread *Core::createSession(UserId uid, bool restore) {
623   if(sessions.contains(uid)) {
624     qWarning() << "Calling createSession() when a session for the user already exists!";
625     return 0;
626   }
627   SessionThread *sess = new SessionThread(uid, restore, this);
628   sessions[uid] = sess;
629   sess->start();
630   return sess;
631 }
632
633 #ifdef HAVE_SSL
634 void Core::sslErrors(const QList<QSslError> &errors) {
635   Q_UNUSED(errors);
636   QSslSocket *socket = qobject_cast<QSslSocket *>(sender());
637   if(socket)
638     socket->ignoreSslErrors();
639 }
640 #endif
641
642 void Core::socketError(QAbstractSocket::SocketError err) {
643   QAbstractSocket *socket = qobject_cast<QAbstractSocket *>(sender());
644   if(socket && err != QAbstractSocket::RemoteHostClosedError)
645     qWarning() << "Core::socketError()" << socket << err << socket->errorString();
646 }