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