Merge pull request #136 from sandsmark/sonnet
[quassel.git] / src / client / clientauthhandler.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2015 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  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
19  ***************************************************************************/
20
21 #include "clientauthhandler.h"
22
23 // TODO: support system application proxy (new in Qt 4.6)
24
25 #include <QtEndian>
26
27 #ifdef HAVE_SSL
28     #include <QSslSocket>
29 #else
30     #include <QTcpSocket>
31 #endif
32
33 #include "client.h"
34 #include "clientsettings.h"
35 #include "peerfactory.h"
36
37 #if QT_VERSION < 0x050000
38 #    include "../../3rdparty/sha512/sha512.h"
39 #endif
40
41 using namespace Protocol;
42
43 ClientAuthHandler::ClientAuthHandler(CoreAccount account, QObject *parent)
44     : AuthHandler(parent),
45     _peer(0),
46     _account(account),
47     _probing(false),
48     _legacy(false),
49     _connectionFeatures(0)
50 {
51
52 }
53
54
55 void ClientAuthHandler::connectToCore()
56 {
57     CoreAccountSettings s;
58
59 #ifdef HAVE_SSL
60     QSslSocket *socket = new QSslSocket(this);
61     // make sure the warning is shown if we happen to connect without SSL support later
62     s.setAccountValue("ShowNoClientSslWarning", true);
63 #else
64     if (_account.useSsl()) {
65         if (s.accountValue("ShowNoClientSslWarning", true).toBool()) {
66             bool accepted = false;
67             emit handleNoSslInClient(&accepted);
68             if (!accepted) {
69                 emit errorMessage(tr("Unencrypted connection canceled"));
70                 return;
71             }
72             s.setAccountValue("ShowNoClientSslWarning", false);
73         }
74     }
75     QTcpSocket *socket = new QTcpSocket(this);
76 #endif
77
78 // TODO: Handle system proxy
79 #ifndef QT_NO_NETWORKPROXY
80     if (_account.useProxy()) {
81         QNetworkProxy proxy(_account.proxyType(), _account.proxyHostName(), _account.proxyPort(), _account.proxyUser(), _account.proxyPassword());
82         socket->setProxy(proxy);
83     }
84 #endif
85
86     setSocket(socket);
87     connect(socket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), SLOT(onSocketStateChanged(QAbstractSocket::SocketState)));
88     connect(socket, SIGNAL(readyRead()), SLOT(onReadyRead()));
89     connect(socket, SIGNAL(connected()), SLOT(onSocketConnected()));
90
91     emit statusMessage(tr("Connecting to %1...").arg(_account.accountName()));
92     socket->connectToHost(_account.hostName(), _account.port());
93 }
94
95
96 void ClientAuthHandler::onSocketStateChanged(QAbstractSocket::SocketState socketState)
97 {
98     QString text;
99
100     switch(socketState) {
101         case QAbstractSocket::HostLookupState:
102             if (!_legacy)
103                 text = tr("Looking up %1...").arg(_account.hostName());
104             break;
105         case QAbstractSocket::ConnectingState:
106             if (!_legacy)
107                 text = tr("Connecting to %1...").arg(_account.hostName());
108             break;
109         case QAbstractSocket::ConnectedState:
110             text = tr("Connected to %1").arg(_account.hostName());
111             break;
112         case QAbstractSocket::ClosingState:
113             if (!_probing)
114                 text = tr("Disconnecting from %1...").arg(_account.hostName());
115             break;
116         case QAbstractSocket::UnconnectedState:
117             if (!_probing) {
118                 text = tr("Disconnected");
119                 // Ensure the disconnected() signal is sent even if we haven't reached the Connected state yet.
120                 // The baseclass implementation will make sure to only send the signal once.
121                 // However, we do want to prefer a potential socket error signal that may be on route already, so
122                 // give this a chance to overtake us by spinning the loop...
123                 QTimer::singleShot(0, this, SLOT(onSocketDisconnected()));
124             }
125             break;
126         default:
127             break;
128     }
129
130     if (!text.isEmpty()) {
131         emit statusMessage(text);
132     }
133 }
134
135 void ClientAuthHandler::onSocketError(QAbstractSocket::SocketError error)
136 {
137     if (_probing && error == QAbstractSocket::RemoteHostClosedError) {
138         _legacy = true;
139         return;
140     }
141
142     _probing = false; // all other errors are unrelated to probing and should be handled
143     AuthHandler::onSocketError(error);
144 }
145
146
147 void ClientAuthHandler::onSocketDisconnected()
148 {
149     if (_probing && _legacy) {
150         // Remote host has closed the connection while probing
151         _probing = false;
152         disconnect(socket(), SIGNAL(readyRead()), this, SLOT(onReadyRead()));
153         emit statusMessage(tr("Reconnecting in compatibility mode..."));
154         socket()->connectToHost(_account.hostName(), _account.port());
155         return;
156     }
157
158     AuthHandler::onSocketDisconnected();
159 }
160
161
162 void ClientAuthHandler::onSocketConnected()
163 {
164     if (_peer) {
165         qWarning() << Q_FUNC_INFO << "Peer already exists!";
166         return;
167     }
168
169     socket()->setSocketOption(QAbstractSocket::KeepAliveOption, true);
170
171     if (!_legacy) {
172         // First connection attempt, try probing for a capable core
173         _probing = true;
174
175         QDataStream stream(socket()); // stream handles the endianness for us
176         stream.setVersion(QDataStream::Qt_4_2);
177
178         quint32 magic = Protocol::magic;
179 #ifdef HAVE_SSL
180         if (_account.useSsl())
181             magic |= Protocol::Encryption;
182 #endif
183         magic |= Protocol::Compression;
184
185         stream << magic;
186
187         // here goes the list of protocols we support, in order of preference
188         PeerFactory::ProtoList protos = PeerFactory::supportedProtocols();
189         for (int i = 0; i < protos.count(); ++i) {
190             quint32 reply = protos[i].first;
191             reply |= protos[i].second<<8;
192             if (i == protos.count() - 1)
193                 reply |= 0x80000000; // end list
194             stream << reply;
195         }
196
197         socket()->flush(); // make sure the probing data is sent immediately
198         return;
199     }
200
201     // If we arrive here, it's the second connection attempt, meaning probing was not successful -> enable legacy support
202
203     qDebug() << "Legacy core detected, switching to compatibility mode";
204
205     RemotePeer *peer = PeerFactory::createPeer(PeerFactory::ProtoDescriptor(Protocol::LegacyProtocol, 0), this, socket(), Compressor::NoCompression, this);
206     // Only needed for the legacy peer, as all others check the protocol version before instantiation
207     connect(peer, SIGNAL(protocolVersionMismatch(int,int)), SLOT(onProtocolVersionMismatch(int,int)));
208
209     setPeer(peer);
210 }
211
212
213 void ClientAuthHandler::onReadyRead()
214 {
215     if (socket()->bytesAvailable() < 4)
216         return;
217
218     if (!_probing)
219         return; // make sure to not read more data than needed
220
221     _probing = false;
222     disconnect(socket(), SIGNAL(readyRead()), this, SLOT(onReadyRead()));
223
224     quint32 reply;
225     socket()->read((char *)&reply, 4);
226     reply = qFromBigEndian<quint32>(reply);
227
228     Protocol::Type type = static_cast<Protocol::Type>(reply & 0xff);
229     quint16 protoFeatures = static_cast<quint16>(reply>>8 & 0xffff);
230     _connectionFeatures = static_cast<quint8>(reply>>24);
231
232     Compressor::CompressionLevel level;
233     if (_connectionFeatures & Protocol::Compression)
234         level = Compressor::BestCompression;
235     else
236         level = Compressor::NoCompression;
237
238     RemotePeer *peer = PeerFactory::createPeer(PeerFactory::ProtoDescriptor(type, protoFeatures), this, socket(), level, this);
239     if (!peer) {
240         qWarning() << "No valid protocol supported for this core!";
241         emit errorPopup(tr("<b>Incompatible Quassel Core!</b><br>"
242                            "None of the protocols this client speaks are supported by the core you are trying to connect to."));
243
244         requestDisconnect(tr("Core speaks none of the protocols we support"));
245         return;
246     }
247
248     if (peer->protocol() == Protocol::LegacyProtocol) {
249         connect(peer, SIGNAL(protocolVersionMismatch(int,int)), SLOT(onProtocolVersionMismatch(int,int)));
250         _legacy = true;
251     }
252
253     setPeer(peer);
254 }
255
256
257 void ClientAuthHandler::onProtocolVersionMismatch(int actual, int expected)
258 {
259     emit errorPopup(tr("<b>The Quassel Core you are trying to connect to is too old!</b><br>"
260            "We need at least protocol v%1, but the core speaks v%2 only.").arg(expected, actual));
261     requestDisconnect(tr("Incompatible protocol version, connection to core refused"));
262 }
263
264
265 void ClientAuthHandler::setPeer(RemotePeer *peer)
266 {
267     qDebug().nospace() << "Using " << qPrintable(peer->protocolName()) << "...";
268
269     _peer = peer;
270     connect(_peer, SIGNAL(transferProgress(int,int)), SIGNAL(transferProgress(int,int)));
271
272     // The legacy protocol enables SSL later, after registration
273     if (!_account.useSsl() || _legacy)
274         startRegistration();
275     // otherwise, do it now
276     else
277         checkAndEnableSsl(_connectionFeatures & Protocol::Encryption);
278 }
279
280
281 void ClientAuthHandler::startRegistration()
282 {
283     emit statusMessage(tr("Synchronizing to core..."));
284
285     // useSsl will be ignored by non-legacy peers
286     bool useSsl = false;
287 #ifdef HAVE_SSL
288     useSsl = _account.useSsl();
289 #endif
290
291     _peer->dispatch(RegisterClient(Quassel::buildInfo().fancyVersionString, Quassel::buildInfo().buildDate, useSsl));
292 }
293
294
295 void ClientAuthHandler::handle(const ClientDenied &msg)
296 {
297     emit errorPopup(msg.errorString);
298     requestDisconnect(tr("The core refused connection from this client"));
299 }
300
301
302 void ClientAuthHandler::handle(const ClientRegistered &msg)
303 {
304     _coreConfigured = msg.coreConfigured;
305     _backendInfo = msg.backendInfo;
306
307     Client::setCoreFeatures(static_cast<Quassel::Features>(msg.coreFeatures));
308
309     // The legacy protocol enables SSL at this point
310     if(_legacy && _account.useSsl())
311         checkAndEnableSsl(msg.sslSupported);
312     else
313         onConnectionReady();
314 }
315
316
317 void ClientAuthHandler::onConnectionReady()
318 {
319     emit connectionReady();
320     emit statusMessage(tr("Connected to %1").arg(_account.accountName()));
321
322     if (!_coreConfigured) {
323         // start wizard
324         emit startCoreSetup(_backendInfo);
325     }
326     else // TODO: check if we need LoginEnabled
327         login();
328 }
329
330
331 void ClientAuthHandler::setupCore(const SetupData &setupData)
332 {
333     _peer->dispatch(setupData);
334 }
335
336
337 void ClientAuthHandler::handle(const SetupFailed &msg)
338 {
339     emit coreSetupFailed(msg.errorString);
340 }
341
342
343 void ClientAuthHandler::handle(const SetupDone &msg)
344 {
345     Q_UNUSED(msg)
346
347     emit coreSetupSuccessful();
348 }
349
350
351 void ClientAuthHandler::login(const QString &user, const QString &password, bool remember)
352 {
353     _account.setUser(user);
354     _account.setPassword(password);
355     _account.setStorePassword(remember);
356     login();
357 }
358
359
360 void ClientAuthHandler::login(const QString &previousError)
361 {
362     emit statusMessage(tr("Logging in..."));
363     if (_account.user().isEmpty() || _account.password().isEmpty() || !previousError.isEmpty()) {
364         bool valid = false;
365         emit userAuthenticationRequired(&_account, &valid, previousError); // *must* be a synchronous call
366         if (!valid || _account.user().isEmpty() || _account.password().isEmpty()) {
367             requestDisconnect(tr("Login canceled"));
368             return;
369         }
370     }
371
372     _peer->dispatch(Login(_account.user(), _account.password()));
373 }
374
375
376 void ClientAuthHandler::handle(const LoginFailed &msg)
377 {
378     login(msg.errorString);
379 }
380
381
382 void ClientAuthHandler::handle(const LoginSuccess &msg)
383 {
384     Q_UNUSED(msg)
385
386     emit loginSuccessful(_account);
387 }
388
389
390 void ClientAuthHandler::handle(const SessionState &msg)
391 {
392     disconnect(socket(), 0, this, 0); // this is the last message we shall ever get
393
394     // give up ownership of the peer; CoreSession takes responsibility now
395     _peer->setParent(0);
396     emit handshakeComplete(_peer, msg);
397 }
398
399
400 /*** SSL Stuff ***/
401
402 void ClientAuthHandler::checkAndEnableSsl(bool coreSupportsSsl)
403 {
404 #ifndef HAVE_SSL
405     Q_UNUSED(coreSupportsSsl);
406 #else
407     CoreAccountSettings s;
408     if (coreSupportsSsl && _account.useSsl()) {
409         // Make sure the warning is shown next time we don't have SSL in the core
410         s.setAccountValue("ShowNoCoreSslWarning", true);
411
412         QSslSocket *sslSocket = qobject_cast<QSslSocket *>(socket());
413         Q_ASSERT(sslSocket);
414         connect(sslSocket, SIGNAL(encrypted()), SLOT(onSslSocketEncrypted()));
415         connect(sslSocket, SIGNAL(sslErrors(QList<QSslError>)), SLOT(onSslErrors()));
416         qDebug() << "Starting encryption...";
417         sslSocket->flush();
418         sslSocket->startClientEncryption();
419     }
420     else {
421         if (s.accountValue("ShowNoCoreSslWarning", true).toBool()) {
422             bool accepted = false;
423             emit handleNoSslInCore(&accepted);
424             if (!accepted) {
425                 requestDisconnect(tr("Unencrypted connection cancelled"));
426                 return;
427             }
428             s.setAccountValue("ShowNoCoreSslWarning", false);
429             s.setAccountValue("SslCert", QString());
430             s.setAccountValue("SslCertDigestVersion", QVariant(QVariant::Int));
431         }
432         if (_legacy)
433             onConnectionReady();
434         else
435             startRegistration();
436     }
437 #endif
438 }
439
440 #ifdef HAVE_SSL
441
442 void ClientAuthHandler::onSslSocketEncrypted()
443 {
444     QSslSocket *socket = qobject_cast<QSslSocket *>(sender());
445     Q_ASSERT(socket);
446
447     if (!socket->sslErrors().count()) {
448         // Cert is valid, so we don't want to store it as known
449         // That way, a warning will appear in case it becomes invalid at some point
450         CoreAccountSettings s;
451         s.setAccountValue("SSLCert", QString());
452         s.setAccountValue("SslCertDigestVersion", QVariant(QVariant::Int));
453     }
454
455     emit encrypted(true);
456
457     if (_legacy)
458         onConnectionReady();
459     else
460         startRegistration();
461 }
462
463
464 void ClientAuthHandler::onSslErrors()
465 {
466     QSslSocket *socket = qobject_cast<QSslSocket *>(sender());
467     Q_ASSERT(socket);
468
469     CoreAccountSettings s;
470     QByteArray knownDigest = s.accountValue("SslCert").toByteArray();
471     ClientAuthHandler::DigestVersion knownDigestVersion = static_cast<ClientAuthHandler::DigestVersion>(s.accountValue("SslCertDigestVersion").toInt());
472
473     QByteArray calculatedDigest;
474     switch (knownDigestVersion) {
475     case ClientAuthHandler::DigestVersion::Md5:
476         calculatedDigest = socket->peerCertificate().digest(QCryptographicHash::Md5);
477         break;
478
479     case ClientAuthHandler::DigestVersion::Sha2_512:
480 #if QT_VERSION >= 0x050000
481         calculatedDigest = socket->peerCertificate().digest(QCryptographicHash::Sha512);
482 #else
483         calculatedDigest = sha2_512(socket->peerCertificate().toDer());
484 #endif
485         break;
486
487     default:
488         qWarning() << "Certificate digest version" << QString(knownDigestVersion) << "is not supported";
489     }
490     
491     if (knownDigest != calculatedDigest) {
492         bool accepted = false;
493         bool permanently = false;
494         emit handleSslErrors(socket, &accepted, &permanently);
495
496         if (!accepted) {
497             requestDisconnect(tr("Unencrypted connection canceled"));
498             return;
499         }
500
501         if (permanently) {
502 #if QT_VERSION >= 0x050000
503             s.setAccountValue("SslCert", socket->peerCertificate().digest(QCryptographicHash::Sha512));
504 #else
505             s.setAccountValue("SslCert", sha2_512(socket->peerCertificate().toDer()));
506 #endif
507             s.setAccountValue("SslCertDigestVersion", ClientAuthHandler::DigestVersion::Latest);
508         }
509         else {
510             s.setAccountValue("SslCert", QString());
511             s.setAccountValue("SslCertDigestVersion", QVariant(QVariant::Int));
512         }
513     }
514     else if (knownDigestVersion != ClientAuthHandler::DigestVersion::Latest) {
515 #if QT_VERSION >= 0x050000
516         s.setAccountValue("SslCert", socket->peerCertificate().digest(QCryptographicHash::Sha512));
517 #else
518         s.setAccountValue("SslCert", sha2_512(socket->peerCertificate().toDer()));
519 #endif
520         s.setAccountValue("SslCertDigestVersion", ClientAuthHandler::DigestVersion::Latest);
521     }
522
523     socket->ignoreSslErrors();
524 }
525
526 #if QT_VERSION < 0x050000
527 QByteArray ClientAuthHandler::sha2_512(const QByteArray &input) {
528     unsigned char output[64];
529     sha512((unsigned char*) input.constData(), input.size(), output, false);
530     // QByteArray::fromRawData() cannot be used here because that constructor
531     // does not copy "output" and the data is clobbered when the variable goes
532     // out of scope.
533     QByteArray result;
534     result.append((char*) output, 64);
535     return result;
536 }
537 #endif
538
539 #endif /* HAVE_SSL */