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