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