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