clazy: fix QString(QLatin1String(...))
[trojita.git] / src / Streams / IODeviceSocket.cpp
blobe9265e9ba219c1a386088bb588043765655f5b2e
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"
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"
37 namespace Streams {
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()
51 d->deleteLater();
52 #if TROJITA_COMPRESS_DEFLATE
53 delete m_compressor;
54 delete m_decompressor;
55 #endif
58 bool IODeviceSocket::canReadLine()
60 #if TROJITA_COMPRESS_DEFLATE
61 if (m_decompressor) {
62 return m_decompressor->canReadLine();
64 #endif
65 return d->canReadLine();
68 QByteArray IODeviceSocket::read(qint64 maxSize)
70 #if TROJITA_COMPRESS_DEFLATE
71 if (m_decompressor) {
72 return m_decompressor->read(maxSize);
74 #endif
75 return d->read(maxSize);
78 QByteArray IODeviceSocket::readLine(qint64 maxSize)
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();
85 #endif
86 return d->readLine(maxSize);
89 qint64 IODeviceSocket::write(const QByteArray &byteArray)
91 #if TROJITA_COMPRESS_DEFLATE
92 if (m_compressor) {
93 m_compressor->write(d, &const_cast<QByteArray&>(byteArray));
94 return byteArray.size();
96 #endif
97 return d->write(byteArray);
100 void IODeviceSocket::startTls()
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();
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();
120 #else
121 throw std::invalid_argument("Trojita got built without zlib support");
122 #endif
125 void IODeviceSocket::handleReadyRead()
127 #if TROJITA_COMPRESS_DEFLATE
128 if (m_decompressor) {
129 m_decompressor->consume(d);
131 #endif
132 emit readyRead();
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()
149 close();
152 void ProcessSocket::close()
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();
165 bool ProcessSocket::isDead()
167 QProcess *proc = qobject_cast<QProcess *>(d);
168 Q_ASSERT(proc);
169 return proc->state() != QProcess::Running;
172 void ProcessSocket::handleProcessError(QProcess::ProcessError err)
174 Q_UNUSED(err);
175 QProcess *proc = qobject_cast<QProcess *>(d);
176 Q_ASSERT(proc);
177 delayedDisconnect->stop();
178 emit disconnected(tr("The QProcess is having troubles: %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);
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 QProcess has exited with return code %1.").arg(
203 proc->exitCode());
204 else
205 disconnectedMessage = tr("The QProcess has exited with return code %1:\n\n%2").arg(
206 proc->exitCode()).arg(stdErr);
207 delayedDisconnect->start();
209 break;
213 void ProcessSocket::delayedStart()
215 QProcess *proc = qobject_cast<QProcess *>(d);
216 Q_ASSERT(proc);
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);
257 Q_ASSERT(sock);
258 sock->abort();
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);
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;
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..."));
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;
323 void SslTlsSocket::handleSocketError(QAbstractSocket::SocketError err)
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()));
333 bool SslTlsSocket::isDead()
335 QAbstractSocket *sock = qobject_cast<QAbstractSocket *>(d);
336 Q_ASSERT(sock);
337 return sock->state() != QAbstractSocket::ConnectedState;
340 void SslTlsSocket::delayedStart()
342 QSslSocket *sock = qobject_cast<QSslSocket *>(d);
343 Q_ASSERT(sock);
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()) {
367 settingFound = true;
369 // now check whether setting has capabilities
370 if (setting.capabilities().testFlag(QNetworkProxy::TunnelingCapability)) {
371 sock->setProxy(setting);
372 capableSettingFound = true;
373 break;
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")));
384 break;
386 case Streams::ProxySettings::DirectConnect:
387 sock->setProxy(QNetworkProxy::NoProxy);
388 break;
391 if (startEncrypted)
392 sock->connectToHostEncrypted(host, port);
393 else
394 sock->connectToHost(host, port);
397 QList<QSslCertificate> SslTlsSocket::sslChain() const
399 QSslSocket *sock = qobject_cast<QSslSocket *>(d);
400 Q_ASSERT(sock);
401 return sock->peerCertificateChain();
404 QList<QSslError> SslTlsSocket::sslErrors() const
406 QSslSocket *sock = qobject_cast<QSslSocket *>(d);
407 Q_ASSERT(sock);
408 return sock->sslErrors();
411 bool SslTlsSocket::isConnectingEncryptedSinceStart() const
413 return startEncrypted;