This is the long-awaited monster commit, bringing you a redesigned core arch and...
[quassel.git] / src / core / core.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-08 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 <QMutexLocker>
24 #include <QCoreApplication>
25
26 #include "core.h"
27 #include "coresession.h"
28 #include "coresettings.h"
29 #include "signalproxy.h"
30 #include "sqlitestorage.h"
31
32 Core *Core::instanceptr = 0;
33 QMutex Core::mutex;
34
35 Core *Core::instance() {
36   if(instanceptr) return instanceptr;
37   instanceptr = new Core();
38   instanceptr->init();
39   return instanceptr;
40 }
41
42 void Core::destroy() {
43   delete instanceptr;
44   instanceptr = 0;
45 }
46
47 Core::Core()
48   : storage(0)
49 {
50   startTime = QDateTime::currentDateTime();  // for uptime :)
51 }
52
53 void Core::init() {
54   configured = false;
55
56   CoreSettings cs;
57   if(!(configured = initStorage(cs.databaseSettings().toMap()))) {
58     qWarning("Core is currently not configured!");
59   }
60
61   connect(&server, SIGNAL(newConnection()), this, SLOT(incomingConnection()));
62   startListening(cs.port());
63   guiUser = 0;
64
65 }
66
67 bool Core::initStorage(QVariantMap dbSettings, bool setup) {
68   QString engine = dbSettings["Type"].toString().toLower();
69
70   if(storage) {
71     qDebug() << "Deleting old storage object.";
72     storage->deleteLater();
73     storage = 0;
74   }
75
76   // FIXME register new storageProviders here
77   if(engine == "sqlite" && SqliteStorage::isAvailable()) {
78     storage = new SqliteStorage(this);
79     connect(storage, SIGNAL(bufferInfoUpdated(UserId, const BufferInfo &)), this, SIGNAL(bufferInfoUpdated(UserId, const BufferInfo &)));
80   } else {
81     qWarning() << "Selected StorageBackend is not available:" << dbSettings["Type"].toString();
82     return configured = false;
83   }
84
85   if(setup && !storage->setup(dbSettings)) {
86     return configured = false;
87   }
88
89   return configured = storage->init(dbSettings);
90 }
91
92 Core::~Core() {
93   // FIXME properly shutdown the sessions
94   qDeleteAll(sessions);
95 }
96
97 void Core::restoreState() {
98   return;
99   /*
100   Q_ASSERT(!instance()->sessions.count());
101   CoreSettings s;
102   QList<QVariant> users = s.coreState().toList();
103   if(users.count() > 0) {
104     qDebug() << "Restoring previous core state...";
105     foreach(QVariant v, users) {
106       QVariantMap m = v.toMap();
107       if(m.contains("UserId")) {
108         CoreSession *sess = createSession(m["UserId"].toUInt());
109         sess->restoreState(m["State"]);  // FIXME multithreading
110       }
111     }
112     qDebug() << "...done.";
113   }
114   */
115 }
116
117 void Core::saveState() {
118   /*
119   CoreSettings s;
120   QList<QVariant> users;
121   foreach(CoreSession *sess, instance()->sessions.values()) {
122     QVariantMap m;
123     m["UserId"] = sess->user();  // FIXME multithreading
124     m["State"] = sess->state();
125     users << m;
126   }
127   s.setCoreState(users);
128   */
129 }
130
131 /*** Storage Access ***/
132
133 NetworkId Core::networkId(UserId user, const QString &network) {
134   QMutexLocker locker(&mutex);
135   return instance()->storage->getNetworkId(user, network);
136 }
137
138 BufferInfo Core::bufferInfo(UserId user, const QString &network, const QString &buffer) {
139   //QMutexLocker locker(&mutex);
140   return instance()->storage->getBufferInfo(user, network, buffer);
141 }
142
143 MsgId Core::storeMessage(const Message &message) {
144   QMutexLocker locker(&mutex);
145   return instance()->storage->logMessage(message);
146 }
147
148 QList<Message> Core::requestMsgs(BufferInfo buffer, int lastmsgs, int offset) {
149   QMutexLocker locker(&mutex);
150   return instance()->storage->requestMsgs(buffer, lastmsgs, offset);
151 }
152
153 QList<Message> Core::requestMsgs(BufferInfo buffer, QDateTime since, int offset) {
154   QMutexLocker locker(&mutex);
155   return instance()->storage->requestMsgs(buffer, since, offset);
156 }
157
158 QList<Message> Core::requestMsgRange(BufferInfo buffer, int first, int last) {
159   QMutexLocker locker(&mutex);
160   return instance()->storage->requestMsgRange(buffer, first, last);
161 }
162
163 QList<BufferInfo> Core::requestBuffers(UserId user, QDateTime since) {
164   QMutexLocker locker(&mutex);
165   return instance()->storage->requestBuffers(user, since);
166 }
167
168 /*** Network Management ***/
169
170 bool Core::startListening(uint port) {
171   if(!server.listen(QHostAddress::Any, port)) {
172     qWarning(QString(QString("Could not open GUI client port %1: %2").arg(port).arg(server.errorString())).toAscii());
173     return false;
174   }
175   qDebug() << "Listening for GUI clients on port" << server.serverPort();
176   return true;
177 }
178
179 void Core::stopListening() {
180   server.close();
181   qDebug() << "No longer listening for GUI clients.";
182 }
183
184 void Core::incomingConnection() {
185   // TODO implement SSL
186   while (server.hasPendingConnections()) {
187     QTcpSocket *socket = server.nextPendingConnection();
188     connect(socket, SIGNAL(disconnected()), this, SLOT(clientDisconnected()));
189     connect(socket, SIGNAL(readyRead()), this, SLOT(clientHasData()));
190     QVariantMap clientInfo;
191     blocksizes.insert(socket, (quint32)0);
192     qDebug() << "Client connected from"  << qPrintable(socket->peerAddress().toString());
193
194     if (!configured) {
195       server.close();
196       qDebug() << "Closing server for basic setup.";
197     }
198   }
199 }
200
201 void Core::clientHasData() {
202   QTcpSocket *socket = dynamic_cast<QTcpSocket*>(sender());
203   Q_ASSERT(socket && blocksizes.contains(socket));
204   QVariant item;
205   while(SignalProxy::readDataFromDevice(socket, blocksizes[socket], item)) {
206     QVariantMap msg = item.toMap();
207     if(!msg.contains("MsgType")) {
208       // Client is way too old, does not even use the current init format
209       qWarning() << qPrintable(tr("Antique client trying to connect... refusing."));
210       socket->close();
211       return;
212     }
213     // OK, so we have at least an init message format we can understand
214     if(msg["MsgType"] == "ClientInit") {
215       QVariantMap reply;
216       reply["CoreVersion"] = Global::quasselVersion;
217       reply["CoreDate"] = Global::quasselDate;
218       reply["CoreBuild"] = Global::quasselBuild;
219       // TODO: Make the core info configurable
220       int uptime = startTime.secsTo(QDateTime::currentDateTime());
221       int updays = uptime / 86400; uptime %= 86400;
222       int uphours = uptime / 3600; uptime %= 3600;
223       int upmins = uptime / 60;
224       reply["CoreInfo"] = tr("<b>Quassel Core Version %1 (Build >= %2)</b><br>"
225                              "Up %3d%4h%5m (since %6)").arg(Global::quasselVersion).arg(Global::quasselBuild)
226                              .arg(updays).arg(uphours,2,10,QChar('0')).arg(upmins,2,10,QChar('0')).arg(startTime.toString(Qt::TextDate));
227
228       reply["SupportSsl"] = false;
229       reply["LoginEnabled"] = true;
230       // TODO: check if we are configured, start wizard otherwise
231
232       // Just version information -- check it!
233       if(msg["ClientBuild"].toUInt() < Global::clientBuildNeeded) {
234         reply["MsgType"] = "ClientInitReject";
235         reply["Error"] = tr("<b>Your Quassel Client is too old!</b><br>"
236                             "This core needs at least client version %1 (Build >= %2).<br>"
237                             "Please consider upgrading your client.").arg(Global::quasselVersion).arg(Global::quasselBuild);
238         SignalProxy::writeDataToDevice(socket, reply);
239         qWarning() << qPrintable(tr("Client %1 too old, rejecting.").arg(socket->peerAddress().toString()));
240         socket->close(); return;
241       }
242       clientInfo[socket] = msg; // store for future reference
243       reply["MsgType"] = "ClientInitAck";
244       SignalProxy::writeDataToDevice(socket, reply);
245     } else if(msg["MsgType"] == "ClientLogin") {
246       QVariantMap reply;
247       if(!clientInfo.contains(socket)) {
248         reply["MsgType"] = "ClientLoginReject";
249         reply["Error"] = tr("<b>Client not initialized!</b><br>You need to send an init message before trying to login.");
250         SignalProxy::writeDataToDevice(socket, reply);
251         qWarning() << qPrintable(tr("Client %1 did not send an init message before trying to login, rejecting.").arg(socket->peerAddress().toString()));
252         socket->close(); return;
253       }
254       mutex.lock();
255       UserId uid = storage->validateUser(msg["User"].toString(), msg["Password"].toString());
256       mutex.unlock();
257       if(!uid) {
258         reply["MsgType"] = "ClientLoginReject";
259         reply["Error"] = tr("<b>Invalid username or password!</b><br>The username/password combination you supplied could not be found in the database.");
260         SignalProxy::writeDataToDevice(socket, reply);
261         continue;
262       }
263       reply["MsgType"] = "ClientLoginAck";
264       SignalProxy::writeDataToDevice(socket, reply);
265       qDebug() << qPrintable(tr("Client %1 initialized and authentificated successfully as \"%2\".").arg(socket->peerAddress().toString(), msg["User"].toString()));
266       setupClientSession(socket, uid);
267     }
268     //socket->close(); return;
269     /*
270     // we need to auth the client
271     try {
272       QVariantMap msg = item.toMap();
273       if (msg["GuiProtocol"].toUInt() != GUI_PROTOCOL) {
274         throw Exception("GUI client version mismatch");
275       }
276       if (configured) {
277         processClientInit(socket, msg);
278       } else {
279         processCoreSetup(socket, msg);
280       }
281     } catch(Storage::AuthError) {
282       qWarning() << "Authentification error!";  // FIXME: send auth error to client
283       socket->close();
284       return;
285     } catch(Exception e) {
286       qWarning() << "Client init error:" << e.msg();
287       socket->close();
288       return;
289     } */
290   }
291 }
292
293 // Potentially called during the initialization phase (before handing the connection off to the session)
294 void Core::clientDisconnected() {
295   QTcpSocket *socket = dynamic_cast<QTcpSocket*>(sender());
296   blocksizes.remove(socket);
297   clientInfo.remove(socket);
298   qDebug() << qPrintable(tr("Client %1 disconnected.").arg(socket->peerAddress().toString()));
299   socket->deleteLater();
300   socket = 0;
301
302   // make server listen again if still not configured  FIXME
303   if (!configured) {
304     startListening();
305   }
306
307   // TODO remove unneeded sessions - if necessary/possible...
308   // Suggestion: kill sessions if they are not connected to any network and client.
309 }
310
311   
312   //disconnect(socket, 0, this, 0);
313   /*
314   sessions[uid]->addClient(socket);  // FIXME multithreading
315   qDebug() << "Client initialized successfully.";
316   SignalProxy::writeDataToDevice(socket, reply);
317   */
318
319
320 void Core::processCoreSetup(QTcpSocket *socket, QVariantMap &msg) {
321   if(msg["HasSettings"].toBool()) {
322     QVariantMap auth;
323     auth["User"] = msg["User"];
324     auth["Password"] = msg["Password"];
325     msg.remove("User");
326     msg.remove("Password");
327     qDebug() << "Initializing storage provider" << msg["Type"].toString();
328
329     if(!initStorage(msg, true)) {
330       // notify client to start wizard again
331       qWarning("Core is currently not configured!");
332       QVariantMap reply;
333       reply["StartWizard"] = true;
334       reply["StorageProviders"] = availableStorageProviders();
335       SignalProxy::writeDataToDevice(socket, reply);
336     } else {
337       // write coresettings
338       CoreSettings s;
339       s.setDatabaseSettings(msg);
340       // write admin user to database & make the core listen again to connections
341       storage->addUser(auth["User"].toString(), auth["Password"].toString());
342       startListening();
343       // continue the normal procedure
344       //processClientInit(socket, auth);
345     }
346   } else {
347     // notify client to start wizard
348     QVariantMap reply;
349     reply["StartWizard"] = true;
350     reply["StorageProviders"] = availableStorageProviders();
351     SignalProxy::writeDataToDevice(socket, reply);
352   }
353 }
354
355 void Core::setupClientSession(QTcpSocket *socket, UserId uid) {
356   // Find or create session for validated user
357   SessionThread *sess;
358   if(sessions.contains(uid)) sess = sessions[uid];
359   else sess = createSession(uid);
360   // Hand over socket, session then sends state itself
361   disconnect(socket, 0, this, 0);
362   if(!sess) {
363     qWarning() << qPrintable(tr("Could not initialize session for client %1!").arg(socket->peerAddress().toString()));
364     socket->close();
365   }
366   sess->addClient(socket);
367 }
368
369 SessionThread *Core::createSession(UserId uid) {
370   if(sessions.contains(uid)) {
371     qWarning() << "Calling createSession() when a session for the user already exists!";
372     return 0;
373   }
374   SessionThread *sess = new SessionThread(uid, this);
375   sessions[uid] = sess;
376   sess->start();
377   return sess;
378 }
379
380 QStringList Core::availableStorageProviders() {
381   QStringList storageProviders;
382   if (SqliteStorage::isAvailable()) {
383     storageProviders.append(SqliteStorage::displayName());
384   }
385   // TODO: temporary
386   // storageProviders.append("MySQL");
387   
388   return storageProviders;
389 }