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