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