cmake: avoid de-duplication of user's CXXFLAGS
[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         magic |= Protocol::Encryption;
163         magic |= Protocol::Compression;
164
165         stream << magic;
166
167         // here goes the list of protocols we support, in order of preference
168         PeerFactory::ProtoList protos = PeerFactory::supportedProtocols();
169         for (int i = 0; i < protos.count(); ++i) {
170             quint32 reply = protos[i].first;
171             reply |= protos[i].second << 8;
172             if (i == protos.count() - 1)
173                 reply |= 0x80000000;  // end list
174             stream << reply;
175         }
176
177         socket()->flush();  // make sure the probing data is sent immediately
178         return;
179     }
180
181     // If we arrive here, it's the second connection attempt, meaning probing was not successful -> enable legacy support
182
183     qDebug() << "Legacy core detected, switching to compatibility mode";
184
185     auto* peer = PeerFactory::createPeer(PeerFactory::ProtoDescriptor(Protocol::LegacyProtocol, 0),
186                                          this,
187                                          socket(),
188                                          Compressor::NoCompression,
189                                          this);
190     // Only needed for the legacy peer, as all others check the protocol version before instantiation
191     connect(peer, &RemotePeer::protocolVersionMismatch, this, &ClientAuthHandler::onProtocolVersionMismatch);
192
193     setPeer(peer);
194 }
195
196 void ClientAuthHandler::onReadyRead()
197 {
198     if (socket()->bytesAvailable() < 4)
199         return;
200
201     if (!_probing)
202         return;  // make sure to not read more data than needed
203
204     _probing = false;
205     disconnect(socket(), &QIODevice::readyRead, this, &ClientAuthHandler::onReadyRead);
206
207     quint32 reply;
208     socket()->read((char*)&reply, 4);
209     reply = qFromBigEndian<quint32>(reply);
210
211     auto type = static_cast<Protocol::Type>(reply & 0xff);
212     auto protoFeatures = static_cast<quint16>(reply >> 8 & 0xffff);
213     _connectionFeatures = static_cast<quint8>(reply >> 24);
214
215     Compressor::CompressionLevel level;
216     if (_connectionFeatures & Protocol::Compression)
217         level = Compressor::BestCompression;
218     else
219         level = Compressor::NoCompression;
220
221     RemotePeer* peer = PeerFactory::createPeer(PeerFactory::ProtoDescriptor(type, protoFeatures), this, socket(), level, this);
222     if (!peer) {
223         qWarning() << "No valid protocol supported for this core!";
224         emit errorPopup(tr("<b>Incompatible Quassel Core!</b><br>"
225                            "None of the protocols this client speaks are supported by the core you are trying to connect to."));
226
227         requestDisconnect(tr("Core speaks none of the protocols we support"));
228         return;
229     }
230
231     if (peer->protocol() == Protocol::LegacyProtocol) {
232         connect(peer, &RemotePeer::protocolVersionMismatch, this, &ClientAuthHandler::onProtocolVersionMismatch);
233         _legacy = true;
234     }
235
236     setPeer(peer);
237 }
238
239 void ClientAuthHandler::onProtocolVersionMismatch(int actual, int expected)
240 {
241     emit errorPopup(tr("<b>The Quassel Core you are trying to connect to is too old!</b><br>"
242                        "We need at least protocol v%1, but the core speaks v%2 only.")
243                         .arg(expected, actual));
244     requestDisconnect(tr("Incompatible protocol version, connection to core refused"));
245 }
246
247 void ClientAuthHandler::setPeer(RemotePeer* peer)
248 {
249     qDebug().nospace() << "Using " << qPrintable(peer->protocolName()) << "...";
250
251     _peer = peer;
252     connect(_peer, &RemotePeer::transferProgress, this, &ClientAuthHandler::transferProgress);
253
254     // The legacy protocol enables SSL later, after registration
255     if (_legacy)
256         startRegistration();
257     // otherwise, do it now
258     else
259         checkAndEnableSsl(_connectionFeatures & Protocol::Encryption);
260 }
261
262 void ClientAuthHandler::startRegistration()
263 {
264     emit statusMessage(tr("Synchronizing to core..."));
265
266     _peer->dispatch(Protocol::RegisterClient(Quassel::Features{}, Quassel::buildInfo().fancyVersionString, Quassel::buildInfo().commitDate));
267 }
268
269 void ClientAuthHandler::handle(const Protocol::ClientDenied& msg)
270 {
271     emit errorPopup(msg.errorString);
272     requestDisconnect(tr("The core refused connection from this client"));
273 }
274
275 void ClientAuthHandler::handle(const Protocol::ClientRegistered& msg)
276 {
277     _coreConfigured = msg.coreConfigured;
278     _backendInfo = msg.backendInfo;
279     _authenticatorInfo = msg.authenticatorInfo;
280
281     _peer->setFeatures(std::move(msg.features));
282
283     // The legacy protocol enables SSL at this point
284     if (_legacy)
285         checkAndEnableSsl(msg.sslSupported);
286     else
287         onConnectionReady();
288 }
289
290 void ClientAuthHandler::onConnectionReady()
291 {
292     const auto& coreFeatures = _peer->features();
293     auto unsupported = coreFeatures.toStringList(false);
294     if (!unsupported.isEmpty()) {
295         qInfo() << qPrintable(tr("Core does not support the following features: %1").arg(unsupported.join(", ")));
296     }
297     if (!coreFeatures.unknownFeatures().isEmpty()) {
298         qInfo() << qPrintable(tr("Core supports unknown features: %1").arg(coreFeatures.unknownFeatures().join(", ")));
299     }
300
301     emit connectionReady();
302     emit statusMessage(tr("Connected to %1").arg(_account.accountName()));
303
304     if (!_coreConfigured) {
305         // start wizard
306         emit startCoreSetup(_backendInfo, _authenticatorInfo);
307     }
308     else  // TODO: check if we need LoginEnabled
309         login();
310 }
311
312 void ClientAuthHandler::setupCore(const Protocol::SetupData& setupData)
313 {
314     _peer->dispatch(setupData);
315 }
316
317 void ClientAuthHandler::handle(const Protocol::SetupFailed& msg)
318 {
319     emit coreSetupFailed(msg.errorString);
320 }
321
322 void ClientAuthHandler::handle(const Protocol::SetupDone& msg)
323 {
324     Q_UNUSED(msg)
325
326     emit coreSetupSuccessful();
327 }
328
329 void ClientAuthHandler::login(const QString& user, const QString& password, bool remember)
330 {
331     _account.setUser(user);
332     _account.setPassword(password);
333     _account.setStorePassword(remember);
334     login();
335 }
336
337 void ClientAuthHandler::login(const QString& previousError)
338 {
339     emit statusMessage(tr("Logging in..."));
340     if (_account.user().isEmpty() || _account.password().isEmpty() || !previousError.isEmpty()) {
341         bool valid = false;
342         emit userAuthenticationRequired(&_account, &valid, previousError);  // *must* be a synchronous call
343         if (!valid || _account.user().isEmpty() || _account.password().isEmpty()) {
344             requestDisconnect(tr("Login canceled"));
345             return;
346         }
347     }
348
349     _peer->dispatch(Protocol::Login(_account.user(), _account.password()));
350 }
351
352 void ClientAuthHandler::handle(const Protocol::LoginFailed& msg)
353 {
354     login(msg.errorString);
355 }
356
357 void ClientAuthHandler::handle(const Protocol::LoginSuccess& msg)
358 {
359     Q_UNUSED(msg)
360
361     emit loginSuccessful(_account);
362 }
363
364 void ClientAuthHandler::handle(const Protocol::SessionState& msg)
365 {
366     disconnect(socket(), nullptr, this, nullptr);  // this is the last message we shall ever get
367
368     // give up ownership of the peer; CoreSession takes responsibility now
369     _peer->setParent(nullptr);
370     emit handshakeComplete(_peer, msg);
371 }
372
373 /*** SSL Stuff ***/
374
375 void ClientAuthHandler::checkAndEnableSsl(bool coreSupportsSsl)
376 {
377     CoreAccountSettings s;
378     if (coreSupportsSsl) {
379         // Make sure the warning is shown next time we don't have SSL in the core
380         s.setAccountValue("ShowNoCoreSslWarning", true);
381
382         connect(socket(), &QSslSocket::encrypted, this, &ClientAuthHandler::onSslSocketEncrypted);
383         connect(socket(), selectOverload<const QList<QSslError>&>(&QSslSocket::sslErrors), this, &ClientAuthHandler::onSslErrors);
384         qDebug() << "Starting encryption...";
385         socket()->flush();
386         socket()->startClientEncryption();
387     }
388     else {
389         if (s.accountValue("ShowNoCoreSslWarning", true).toBool()) {
390             bool accepted = false;
391             emit handleNoSslInCore(&accepted);
392             if (!accepted) {
393                 requestDisconnect(tr("Unencrypted connection cancelled"));
394                 return;
395             }
396             s.setAccountValue("ShowNoCoreSslWarning", false);
397             s.setAccountValue("SslCert", QString());
398             s.setAccountValue("SslCertDigestVersion", QVariant(QVariant::Int));
399         }
400         if (_legacy)
401             onConnectionReady();
402         else
403             startRegistration();
404     }
405 }
406
407
408 void ClientAuthHandler::onSslSocketEncrypted()
409 {
410     auto* socket = qobject_cast<QSslSocket*>(sender());
411     Q_ASSERT(socket);
412
413 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
414     if (!socket->sslErrors().count()) {
415 #else
416     if (!socket->sslHandshakeErrors().count()) {
417 #endif
418         // Cert is valid, so we don't want to store it as known
419         // That way, a warning will appear in case it becomes invalid at some point
420         CoreAccountSettings s;
421         s.setAccountValue("SSLCert", QString());
422         s.setAccountValue("SslCertDigestVersion", QVariant(QVariant::Int));
423     }
424
425     emit encrypted(true);
426
427     if (_legacy)
428         onConnectionReady();
429     else
430         startRegistration();
431 }
432
433 void ClientAuthHandler::onSslErrors()
434 {
435     CoreAccountSettings s;
436     QByteArray knownDigest = s.accountValue("SslCert").toByteArray();
437     ClientAuthHandler::DigestVersion knownDigestVersion = static_cast<ClientAuthHandler::DigestVersion>(
438         s.accountValue("SslCertDigestVersion").toInt());
439
440     QByteArray calculatedDigest;
441     switch (knownDigestVersion) {
442     case ClientAuthHandler::DigestVersion::Md5:
443         calculatedDigest = socket()->peerCertificate().digest(QCryptographicHash::Md5);
444         break;
445
446     case ClientAuthHandler::DigestVersion::Sha2_512:
447         calculatedDigest = socket()->peerCertificate().digest(QCryptographicHash::Sha512);
448         break;
449
450     default:
451         qWarning() << "Certificate digest version" << QString(knownDigestVersion) << "is not supported";
452     }
453
454     if (knownDigest != calculatedDigest) {
455         bool accepted = false;
456         bool permanently = false;
457         emit handleSslErrors(socket(), &accepted, &permanently);
458
459         if (!accepted) {
460             requestDisconnect(tr("Unencrypted connection canceled"));
461             return;
462         }
463
464         if (permanently) {
465             s.setAccountValue("SslCert", socket()->peerCertificate().digest(QCryptographicHash::Sha512));
466             s.setAccountValue("SslCertDigestVersion", ClientAuthHandler::DigestVersion::Latest);
467         }
468         else {
469             s.setAccountValue("SslCert", QString());
470             s.setAccountValue("SslCertDigestVersion", QVariant(QVariant::Int));
471         }
472     }
473     else if (knownDigestVersion != ClientAuthHandler::DigestVersion::Latest) {
474         s.setAccountValue("SslCert", socket()->peerCertificate().digest(QCryptographicHash::Sha512));
475         s.setAccountValue("SslCertDigestVersion", ClientAuthHandler::DigestVersion::Latest);
476     }
477
478     socket()->ignoreSslErrors();
479 }