1 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
3 This file is part of the Trojita Qt IMAP e-mail client,
4 http://trojita.flaska.net/
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.
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.
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/>.
23 #include "IODeviceSocket.h"
25 #include <QNetworkProxy>
26 #include <QNetworkProxyFactory>
27 #include <QNetworkProxyQuery>
28 #include <QSslConfiguration>
31 #include "TrojitaZlibStatus.h"
32 #if TROJITA_COMPRESS_DEFLATE
33 #include "3rdparty/rfc1951.h"
35 #include "Common/InvokeMethod.h"
39 IODeviceSocket::IODeviceSocket(QIODevice
*device
): d(device
), m_compressor(0), m_decompressor(0)
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
);
49 IODeviceSocket::~IODeviceSocket()
52 #if TROJITA_COMPRESS_DEFLATE
54 delete m_decompressor
;
58 bool IODeviceSocket::canReadLine()
60 #if TROJITA_COMPRESS_DEFLATE
62 return m_decompressor
->canReadLine();
65 return d
->canReadLine();
68 QByteArray
IODeviceSocket::read(qint64 maxSize
)
70 #if TROJITA_COMPRESS_DEFLATE
72 return m_decompressor
->read(maxSize
);
75 return d
->read(maxSize
);
78 QByteArray
IODeviceSocket::readLine(qint64 maxSize
)
80 #if TROJITA_COMPRESS_DEFLATE
82 // FIXME: well, we apparently don't respect the maxSize argument...
83 return m_decompressor
->readLine();
86 return d
->readLine(maxSize
);
89 qint64
IODeviceSocket::write(const QByteArray
&byteArray
)
91 #if TROJITA_COMPRESS_DEFLATE
93 m_compressor
->write(d
, &const_cast<QByteArray
&>(byteArray
));
94 return byteArray
.size();
97 return d
->write(byteArray
);
100 void IODeviceSocket::startTls()
102 QSslSocket
*sock
= qobject_cast
<QSslSocket
*>(d
);
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");
109 sock
->startClientEncryption();
112 void IODeviceSocket::startDeflate()
114 if (m_compressor
|| m_decompressor
)
115 throw std::invalid_argument("DEFLATE compression is already active");
117 #if TROJITA_COMPRESS_DEFLATE
118 m_compressor
= new Rfc1951Compressor();
119 m_decompressor
= new Rfc1951Decompressor();
121 throw std::invalid_argument("Trojita got built without zlib support");
125 void IODeviceSocket::handleReadyRead()
127 #if TROJITA_COMPRESS_DEFLATE
128 if (m_decompressor
) {
129 m_decompressor
->consume(d
);
135 void IODeviceSocket::emitError()
137 emit
disconnected(disconnectedMessage
);
140 ProcessSocket::ProcessSocket(QProcess
*proc
, const QString
&executable
, const QStringList
&args
):
141 IODeviceSocket(proc
), executable(executable
), args(args
)
143 connect(proc
, &QProcess::stateChanged
, this, &ProcessSocket::handleStateChanged
);
144 connect(proc
, static_cast<void (QProcess::*)(QProcess::ProcessError
)>(&QProcess::error
), this, &ProcessSocket::handleProcessError
);
147 ProcessSocket::~ProcessSocket()
152 void ProcessSocket::close()
154 QProcess
*proc
= qobject_cast
<QProcess
*>(d
);
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
) {
160 proc
->waitForFinished(200);
165 bool ProcessSocket::isDead()
167 QProcess
*proc
= qobject_cast
<QProcess
*>(d
);
169 return proc
->state() != QProcess::Running
;
172 void ProcessSocket::handleProcessError(QProcess::ProcessError err
)
175 QProcess
*proc
= qobject_cast
<QProcess
*>(d
);
177 delayedDisconnect
->stop();
178 emit
disconnected(tr("Disconnected: %1").arg(proc
->errorString()));
181 void ProcessSocket::handleStateChanged()
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. */
188 QProcess
*proc
= qobject_cast
<QProcess
*>(d
);
190 switch (proc
->state()) {
191 case QProcess::Running
:
192 emit
stateChanged(Imap::CONN_STATE_CONNECTED_PRETLS_PRECAPS
, tr("The process has started"));
194 case QProcess::Starting
:
195 emit
stateChanged(Imap::CONN_STATE_CONNECTING
, tr("Starting process `%1 %2`").arg(executable
, args
.join(QStringLiteral(" "))));
197 case QProcess::NotRunning
: {
198 if (delayedDisconnect
->isActive())
200 QString stdErr
= QString::fromLocal8Bit(proc
->readAllStandardError());
201 if (stdErr
.isEmpty())
202 disconnectedMessage
= tr("The process has exited with return code %1.").arg(
205 disconnectedMessage
= tr("The process has exited with return code %1:\n\n%2").arg(
206 proc
->exitCode()).arg(stdErr
);
207 delayedDisconnect
->start();
213 void ProcessSocket::delayedStart()
215 QProcess
*proc
= qobject_cast
<QProcess
*>(d
);
217 proc
->start(executable
, args
);
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
)
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
);
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
);
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
);
248 void SslTlsSocket::setProxySettings(const ProxySettings proxySettings
, const QString
&protocolTag
)
250 m_proxySettings
= proxySettings
;
251 m_protocolTag
= protocolTag
;
254 void SslTlsSocket::close()
256 QSslSocket
*sock
= qobject_cast
<QSslSocket
*>(d
);
259 emit
disconnected(tr("Connection closed"));
262 void SslTlsSocket::handleStateChanged()
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. */
269 QAbstractSocket
*sock
= qobject_cast
<QAbstractSocket
*>(d
);
272 switch (sock
->proxy().type()) {
273 case QNetworkProxy::NoProxy
:
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.");
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.");
283 case QNetworkProxy::DefaultProxy
:
284 proxyMsg
= tr(" (via proxy %1)").arg(sock
->proxy().hostName());
286 case QNetworkProxy::Socks5Proxy
:
287 proxyMsg
= tr(" (via SOCKS5 proxy %1)").arg(sock
->proxy().hostName());
289 case QNetworkProxy::HttpProxy
:
290 proxyMsg
= tr(" (via HTTP proxy %1)").arg(sock
->proxy().hostName());
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()));
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()));
305 case QAbstractSocket::BoundState
:
306 case QAbstractSocket::ListeningState
:
308 case QAbstractSocket::ConnectedState
:
309 if (! startEncrypted
) {
310 emit
stateChanged(Imap::CONN_STATE_CONNECTED_PRETLS_PRECAPS
, tr("Connected"));
312 emit
stateChanged(Imap::CONN_STATE_SSL_HANDSHAKE
, tr("Negotiating encryption..."));
315 case QAbstractSocket::UnconnectedState
:
316 case QAbstractSocket::ClosingState
:
317 disconnectedMessage
= tr("Socket is disconnected: %1").arg(sock
->errorString());
318 delayedDisconnect
->start();
323 void SslTlsSocket::handleSocketError(QAbstractSocket::SocketError err
)
326 QAbstractSocket
*sock
= qobject_cast
<QAbstractSocket
*>(d
);
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()));
333 bool SslTlsSocket::isDead()
335 QAbstractSocket
*sock
= qobject_cast
<QAbstractSocket
*>(d
);
337 return sock
->state() != QAbstractSocket::ConnectedState
;
340 void SslTlsSocket::delayedStart()
342 QSslSocket
*sock
= qobject_cast
<QSslSocket
*>(d
);
345 switch (m_proxySettings
) {
346 case Streams::ProxySettings::RespectSystemProxy
:
348 QNetworkProxy setting
;
349 QNetworkProxyQuery query
= QNetworkProxyQuery(host
, port
, m_protocolTag
, QNetworkProxyQuery::TcpSocket
);
351 // set to true if a capable setting is found
352 bool capableSettingFound
= false;
354 // set to true if at least one valid setting is found
355 bool settingFound
= false;
357 // FIXME: this static function works particularly slow in Windows
358 QList
<QNetworkProxy
> proxySettingsList
= QNetworkProxyFactory::systemProxyForQuery(query
);
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()) {
369 // now check whether setting has capabilities
370 if (setting
.capabilities().testFlag(QNetworkProxy::TunnelingCapability
)) {
371 sock
->setProxy(setting
);
372 capableSettingFound
= true;
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")));
386 case Streams::ProxySettings::DirectConnect
:
387 sock
->setProxy(QNetworkProxy::NoProxy
);
392 sock
->connectToHostEncrypted(host
, port
);
394 sock
->connectToHost(host
, port
);
397 QList
<QSslCertificate
> SslTlsSocket::sslChain() const
399 QSslSocket
*sock
= qobject_cast
<QSslSocket
*>(d
);
401 return sock
->peerCertificateChain();
404 QList
<QSslError
> SslTlsSocket::sslErrors() const
406 QSslSocket
*sock
= qobject_cast
<QSslSocket
*>(d
);
408 return sock
->sslErrors();
411 bool SslTlsSocket::isConnectingEncryptedSinceStart() const
413 return startEncrypted
;