1 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
2
3 This file is part of the Trojita Qt IMAP e-mail client,
4 http://trojita.flaska.net/
5
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License as
8 published by the Free Software Foundation; either version 2 of
9 the License or (at your option) version 3 or any later version
10 accepted by the membership of KDE e.V. (or its successor approved
11 by the membership of KDE e.V.), which shall act as a proxy
12 defined in Section 14 of version 3 of the license.
13
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
18
19 You should have received a copy of the GNU General Public License
20 along with this program. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23 #include "IODeviceSocket.h"
24 #include <stdexcept>
25 #include <QNetworkProxy>
26 #include <QNetworkProxyFactory>
27 #include <QNetworkProxyQuery>
28 #include <QSslConfiguration>
29 #include <QSslSocket>
30 #include <QTimer>
31 #include "TrojitaZlibStatus.h"
32 #if TROJITA_COMPRESS_DEFLATE
33 #include "3rdparty/rfc1951.h"
34 #endif
35 #include "Common/InvokeMethod.h"
36
37 namespace Streams {
38
IODeviceSocket(QIODevice * device)39 IODeviceSocket::IODeviceSocket(QIODevice *device): d(device), m_compressor(0), m_decompressor(0)
40 {
41 connect(d, &QIODevice::readyRead, this, &IODeviceSocket::handleReadyRead);
42 connect(d, &QIODevice::readChannelFinished, this, &IODeviceSocket::handleStateChanged);
43 delayedDisconnect = new QTimer();
44 delayedDisconnect->setSingleShot(true);
45 connect(delayedDisconnect, &QTimer::timeout, this, &IODeviceSocket::emitError);
46 EMIT_LATER_NOARG(this, delayedStart);
47 }
48
~IODeviceSocket()49 IODeviceSocket::~IODeviceSocket()
50 {
51 d->deleteLater();
52 #if TROJITA_COMPRESS_DEFLATE
53 delete m_compressor;
54 delete m_decompressor;
55 #endif
56 }
57
canReadLine()58 bool IODeviceSocket::canReadLine()
59 {
60 #if TROJITA_COMPRESS_DEFLATE
61 if (m_decompressor) {
62 return m_decompressor->canReadLine();
63 }
64 #endif
65 return d->canReadLine();
66 }
67
read(qint64 maxSize)68 QByteArray IODeviceSocket::read(qint64 maxSize)
69 {
70 #if TROJITA_COMPRESS_DEFLATE
71 if (m_decompressor) {
72 return m_decompressor->read(maxSize);
73 }
74 #endif
75 return d->read(maxSize);
76 }
77
readLine(qint64 maxSize)78 QByteArray IODeviceSocket::readLine(qint64 maxSize)
79 {
80 #if TROJITA_COMPRESS_DEFLATE
81 if (m_decompressor) {
82 // FIXME: well, we apparently don't respect the maxSize argument...
83 return m_decompressor->readLine();
84 }
85 #endif
86 return d->readLine(maxSize);
87 }
88
write(const QByteArray & byteArray)89 qint64 IODeviceSocket::write(const QByteArray &byteArray)
90 {
91 #if TROJITA_COMPRESS_DEFLATE
92 if (m_compressor) {
93 m_compressor->write(d, &const_cast<QByteArray&>(byteArray));
94 return byteArray.size();
95 }
96 #endif
97 return d->write(byteArray);
98 }
99
startTls()100 void IODeviceSocket::startTls()
101 {
102 QSslSocket *sock = qobject_cast<QSslSocket *>(d);
103 if (! sock)
104 throw std::invalid_argument("This IODeviceSocket is not a QSslSocket, and therefore doesn't support STARTTLS.");
105 #if TROJITA_COMPRESS_DEFLATE
106 if (m_compressor || m_decompressor)
107 throw std::invalid_argument("DEFLATE is already active, cannot STARTTLS");
108 #endif
109 sock->startClientEncryption();
110 }
111
startDeflate()112 void IODeviceSocket::startDeflate()
113 {
114 if (m_compressor || m_decompressor)
115 throw std::invalid_argument("DEFLATE compression is already active");
116
117 #if TROJITA_COMPRESS_DEFLATE
118 m_compressor = new Rfc1951Compressor();
119 m_decompressor = new Rfc1951Decompressor();
120 #else
121 throw std::invalid_argument("Trojita got built without zlib support");
122 #endif
123 }
124
handleReadyRead()125 void IODeviceSocket::handleReadyRead()
126 {
127 #if TROJITA_COMPRESS_DEFLATE
128 if (m_decompressor) {
129 m_decompressor->consume(d);
130 }
131 #endif
132 emit readyRead();
133 }
134
emitError()135 void IODeviceSocket::emitError()
136 {
137 emit disconnected(disconnectedMessage);
138 }
139
ProcessSocket(QProcess * proc,const QString & executable,const QStringList & args)140 ProcessSocket::ProcessSocket(QProcess *proc, const QString &executable, const QStringList &args):
141 IODeviceSocket(proc), executable(executable), args(args)
142 {
143 connect(proc, &QProcess::stateChanged, this, &ProcessSocket::handleStateChanged);
144 connect(proc, static_cast<void (QProcess::*)(QProcess::ProcessError)>(&QProcess::error), this, &ProcessSocket::handleProcessError);
145 }
146
~ProcessSocket()147 ProcessSocket::~ProcessSocket()
148 {
149 close();
150 }
151
close()152 void ProcessSocket::close()
153 {
154 QProcess *proc = qobject_cast<QProcess *>(d);
155 Q_ASSERT(proc);
156 // Be nice to it, let it die peacefully before using an axe
157 // QTBUG-5990, don't call waitForFinished() on a process which hadn't started
158 if (proc->state() == QProcess::Running) {
159 proc->terminate();
160 proc->waitForFinished(200);
161 proc->kill();
162 }
163 }
164
isDead()165 bool ProcessSocket::isDead()
166 {
167 QProcess *proc = qobject_cast<QProcess *>(d);
168 Q_ASSERT(proc);
169 return proc->state() != QProcess::Running;
170 }
171
handleProcessError(QProcess::ProcessError err)172 void ProcessSocket::handleProcessError(QProcess::ProcessError err)
173 {
174 Q_UNUSED(err);
175 QProcess *proc = qobject_cast<QProcess *>(d);
176 Q_ASSERT(proc);
177 delayedDisconnect->stop();
178 emit disconnected(tr("Disconnected: %1").arg(proc->errorString()));
179 }
180
handleStateChanged()181 void ProcessSocket::handleStateChanged()
182 {
183 /* Qt delivers the stateChanged() signal before the error() one.
184 That's a problem because we really want to provide a nice error message
185 to the user and QAbstractSocket::error() is not set yet by the time this
186 function executes. That's why we have to delay the first disconnected() signal. */
187
188 QProcess *proc = qobject_cast<QProcess *>(d);
189 Q_ASSERT(proc);
190 switch (proc->state()) {
191 case QProcess::Running:
192 emit stateChanged(Imap::CONN_STATE_CONNECTED_PRETLS_PRECAPS, tr("The process has started"));
193 break;
194 case QProcess::Starting:
195 emit stateChanged(Imap::CONN_STATE_CONNECTING, tr("Starting process `%1 %2`").arg(executable, args.join(QStringLiteral(" "))));
196 break;
197 case QProcess::NotRunning: {
198 if (delayedDisconnect->isActive())
199 break;
200 QString stdErr = QString::fromLocal8Bit(proc->readAllStandardError());
201 if (stdErr.isEmpty())
202 disconnectedMessage = tr("The process has exited with return code %1.").arg(
203 proc->exitCode());
204 else
205 disconnectedMessage = tr("The process has exited with return code %1:\n\n%2").arg(
206 proc->exitCode()).arg(stdErr);
207 delayedDisconnect->start();
208 }
209 break;
210 }
211 }
212
delayedStart()213 void ProcessSocket::delayedStart()
214 {
215 QProcess *proc = qobject_cast<QProcess *>(d);
216 Q_ASSERT(proc);
217 proc->start(executable, args);
218 }
219
SslTlsSocket(QSslSocket * sock,const QString & host,const quint16 port,const bool startEncrypted)220 SslTlsSocket::SslTlsSocket(QSslSocket *sock, const QString &host, const quint16 port, const bool startEncrypted):
221 IODeviceSocket(sock), startEncrypted(startEncrypted), host(host), port(port), m_proxySettings(ProxySettings::RespectSystemProxy)
222 {
223 // The Qt API for deciding about whereabouts of a SSL connection is unfortunately blocking, ie. one is expected to
224 // call a function from a slot attached to the sslErrors signal to tell the code whether to proceed or not.
225 // In QML, one cannot display a dialog box with a nested event loop, so this means that we have to deal with SSL/TLS
226 // establishing at higher level.
227 sock->ignoreSslErrors();
228 sock->setProtocol(QSsl::AnyProtocol);
229 sock->setPeerVerifyMode(QSslSocket::QueryPeer);
230
231 // In response to the attacks related to the SSL compression, Digia has decided to disable SSL compression starting in
232 // Qt 4.8.4 -- see http://qt.digia.com/en/Release-Notes/security-issue-september-2012/.
233 // I have brought this up on the imap-protocol mailing list; the consensus seemed to be that the likelihood of an
234 // successful exploit on an IMAP conversation is very unlikely. The compression itself is, on the other hand, a
235 // very worthwhile goal, so we explicitly enable it again.
236 // Unfortunately, this was backported to older Qt versions as well (see qt4.git's 3488f1db96dbf70bb0486d3013d86252ebf433e0),
237 // but there is no way of enabling compression back again.
238 QSslConfiguration sslConf = sock->sslConfiguration();
239 sslConf.setSslOption(QSsl::SslOptionDisableCompression, false);
240 sock->setSslConfiguration(sslConf);
241
242 connect(sock, &QSslSocket::encrypted, this, &Socket::encrypted);
243 connect(sock, &QAbstractSocket::stateChanged, this, &SslTlsSocket::handleStateChanged);
244 connect(sock, static_cast<void (QAbstractSocket::*)(QAbstractSocket::SocketError)>(&QAbstractSocket::error),
245 this, &SslTlsSocket::handleSocketError);
246 }
247
setProxySettings(const ProxySettings proxySettings,const QString & protocolTag)248 void SslTlsSocket::setProxySettings(const ProxySettings proxySettings, const QString &protocolTag)
249 {
250 m_proxySettings = proxySettings;
251 m_protocolTag = protocolTag;
252 }
253
close()254 void SslTlsSocket::close()
255 {
256 QSslSocket *sock = qobject_cast<QSslSocket*>(d);
257 Q_ASSERT(sock);
258 sock->abort();
259 emit disconnected(tr("Connection closed"));
260 }
261
handleStateChanged()262 void SslTlsSocket::handleStateChanged()
263 {
264 /* Qt delivers the stateChanged() signal before the error() one.
265 That's a problem because we really want to provide a nice error message
266 to the user and QAbstractSocket::error() is not set yet by the time this
267 function executes. That's why we have to delay the first disconnected() signal. */
268
269 QAbstractSocket *sock = qobject_cast<QAbstractSocket *>(d);
270 Q_ASSERT(sock);
271 QString proxyMsg;
272 switch (sock->proxy().type()) {
273 case QNetworkProxy::NoProxy:
274 break;
275 case QNetworkProxy::HttpCachingProxy:
276 Q_ASSERT_X(false, "proxy detection",
277 "Qt should have returned a proxy capable of tunneling, but we got back an HTTP proxy.");
278 break;
279 case QNetworkProxy::FtpCachingProxy:
280 Q_ASSERT_X(false, "proxy detection",
281 "Qt should have returned a proxy capable of tunneling, but we got back an FTP proxy.");
282 break;
283 case QNetworkProxy::DefaultProxy:
284 proxyMsg = tr(" (via proxy %1)").arg(sock->proxy().hostName());
285 break;
286 case QNetworkProxy::Socks5Proxy:
287 proxyMsg = tr(" (via SOCKS5 proxy %1)").arg(sock->proxy().hostName());
288 break;
289 case QNetworkProxy::HttpProxy:
290 proxyMsg = tr(" (via HTTP proxy %1)").arg(sock->proxy().hostName());
291 break;
292 }
293 switch (sock->state()) {
294 case QAbstractSocket::HostLookupState:
295 emit stateChanged(Imap::CONN_STATE_HOST_LOOKUP, tr("Looking up %1%2...").arg(host,
296 sock->proxy().capabilities().testFlag(QNetworkProxy::HostNameLookupCapability) ?
297 proxyMsg : QString()));
298 break;
299 case QAbstractSocket::ConnectingState:
300 emit stateChanged(Imap::CONN_STATE_CONNECTING, tr("Connecting to %1:%2%3%4...").arg(
301 host, QString::number(port), startEncrypted ? tr(" (SSL)") : QString(),
302 sock->proxy().capabilities().testFlag(QNetworkProxy::TunnelingCapability) ?
303 proxyMsg : QString()));
304 break;
305 case QAbstractSocket::BoundState:
306 case QAbstractSocket::ListeningState:
307 break;
308 case QAbstractSocket::ConnectedState:
309 if (! startEncrypted) {
310 emit stateChanged(Imap::CONN_STATE_CONNECTED_PRETLS_PRECAPS, tr("Connected"));
311 } else {
312 emit stateChanged(Imap::CONN_STATE_SSL_HANDSHAKE, tr("Negotiating encryption..."));
313 }
314 break;
315 case QAbstractSocket::UnconnectedState:
316 case QAbstractSocket::ClosingState:
317 disconnectedMessage = tr("Socket is disconnected: %1").arg(sock->errorString());
318 delayedDisconnect->start();
319 break;
320 }
321 }
322
handleSocketError(QAbstractSocket::SocketError err)323 void SslTlsSocket::handleSocketError(QAbstractSocket::SocketError err)
324 {
325 Q_UNUSED(err);
326 QAbstractSocket *sock = qobject_cast<QAbstractSocket *>(d);
327 Q_ASSERT(sock);
328 delayedDisconnect->stop();
329 emit disconnected(tr("The underlying socket is having troubles when processing connection to %1:%2: %3").arg(
330 host, QString::number(port), sock->errorString()));
331 }
332
isDead()333 bool SslTlsSocket::isDead()
334 {
335 QAbstractSocket *sock = qobject_cast<QAbstractSocket *>(d);
336 Q_ASSERT(sock);
337 return sock->state() != QAbstractSocket::ConnectedState;
338 }
339
delayedStart()340 void SslTlsSocket::delayedStart()
341 {
342 QSslSocket *sock = qobject_cast<QSslSocket *>(d);
343 Q_ASSERT(sock);
344
345 switch (m_proxySettings) {
346 case Streams::ProxySettings::RespectSystemProxy:
347 {
348 QNetworkProxy setting;
349 QNetworkProxyQuery query = QNetworkProxyQuery(host, port, m_protocolTag, QNetworkProxyQuery::TcpSocket);
350
351 // set to true if a capable setting is found
352 bool capableSettingFound = false;
353
354 // set to true if at least one valid setting is found
355 bool settingFound = false;
356
357 // FIXME: this static function works particularly slow in Windows
358 QList<QNetworkProxy> proxySettingsList = QNetworkProxyFactory::systemProxyForQuery(query);
359
360 /* Proxy Settings are read from the user's environment variables by the above static method.
361 * A peculiar case is with *nix systems, where an undefined environment variable is returned as
362 * an empty string. Such entries *might* exist in our proxySettingsList, and shouldn't be processed.
363 * One good check is to use hostName() of the QNetworkProxy object, and treat the Proxy Setting as invalid if
364 * the host name is empty. */
365 Q_FOREACH (setting, proxySettingsList) {
366 if (!setting.hostName().isEmpty()) {
367 settingFound = true;
368
369 // now check whether setting has capabilities
370 if (setting.capabilities().testFlag(QNetworkProxy::TunnelingCapability)) {
371 sock->setProxy(setting);
372 capableSettingFound = true;
373 break;
374 }
375 }
376 }
377
378 if (!settingFound || proxySettingsList.isEmpty()) {
379 sock->setProxy(QNetworkProxy::NoProxy);
380 } else if (!capableSettingFound) {
381 emit disconnected(tr("The underlying socket is having troubles when processing connection to %1:%2: %3")
382 .arg(host, QString::number(port), QStringLiteral("Cannot find proxy setting capable of tunneling")));
383 }
384 break;
385 }
386 case Streams::ProxySettings::DirectConnect:
387 sock->setProxy(QNetworkProxy::NoProxy);
388 break;
389 }
390
391 if (startEncrypted)
392 sock->connectToHostEncrypted(host, port);
393 else
394 sock->connectToHost(host, port);
395 }
396
sslChain() const397 QList<QSslCertificate> SslTlsSocket::sslChain() const
398 {
399 QSslSocket *sock = qobject_cast<QSslSocket *>(d);
400 Q_ASSERT(sock);
401 return sock->peerCertificateChain();
402 }
403
sslErrors() const404 QList<QSslError> SslTlsSocket::sslErrors() const
405 {
406 QSslSocket *sock = qobject_cast<QSslSocket *>(d);
407 Q_ASSERT(sock);
408 return sock->sslErrors();
409 }
410
isConnectingEncryptedSinceStart() const411 bool SslTlsSocket::isConnectingEncryptedSinceStart() const
412 {
413 return startEncrypted;
414 }
415
416 }
417