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