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