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