File indexing completed on 2024-06-09 05:21:30
0001 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net> 0002 0003 This file is part of the Trojita Qt IMAP e-mail client, 0004 http://trojita.flaska.net/ 0005 0006 This program is free software; you can redistribute it and/or 0007 modify it under the terms of the GNU General Public License as 0008 published by the Free Software Foundation; either version 2 of 0009 the License or (at your option) version 3 or any later version 0010 accepted by the membership of KDE e.V. (or its successor approved 0011 by the membership of KDE e.V.), which shall act as a proxy 0012 defined in Section 14 of version 3 of the license. 0013 0014 This program is distributed in the hope that it will be useful, 0015 but WITHOUT ANY WARRANTY; without even the implied warranty of 0016 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0017 GNU General Public License for more details. 0018 0019 You should have received a copy of the GNU General Public License 0020 along with this program. If not, see <http://www.gnu.org/licenses/>. 0021 */ 0022 0023 #include "IODeviceSocket.h" 0024 #include <stdexcept> 0025 #include <QNetworkProxy> 0026 #include <QNetworkProxyFactory> 0027 #include <QNetworkProxyQuery> 0028 #include <QSslConfiguration> 0029 #include <QSslSocket> 0030 #include <QTimer> 0031 #include "TrojitaZlibStatus.h" 0032 #if TROJITA_COMPRESS_DEFLATE 0033 #include "3rdparty/rfc1951.h" 0034 #endif 0035 #include "Common/InvokeMethod.h" 0036 0037 namespace Streams { 0038 0039 IODeviceSocket::IODeviceSocket(QIODevice *device): d(device), m_compressor(0), m_decompressor(0) 0040 { 0041 connect(d, &QIODevice::readyRead, this, &IODeviceSocket::handleReadyRead); 0042 connect(d, &QIODevice::readChannelFinished, this, &IODeviceSocket::handleStateChanged); 0043 delayedDisconnect = new QTimer(); 0044 delayedDisconnect->setSingleShot(true); 0045 connect(delayedDisconnect, &QTimer::timeout, this, &IODeviceSocket::emitError); 0046 EMIT_LATER_NOARG(this, delayedStart); 0047 } 0048 0049 IODeviceSocket::~IODeviceSocket() 0050 { 0051 d->deleteLater(); 0052 #if TROJITA_COMPRESS_DEFLATE 0053 delete m_compressor; 0054 delete m_decompressor; 0055 #endif 0056 } 0057 0058 bool IODeviceSocket::canReadLine() 0059 { 0060 #if TROJITA_COMPRESS_DEFLATE 0061 if (m_decompressor) { 0062 return m_decompressor->canReadLine(); 0063 } 0064 #endif 0065 return d->canReadLine(); 0066 } 0067 0068 QByteArray IODeviceSocket::read(qint64 maxSize) 0069 { 0070 #if TROJITA_COMPRESS_DEFLATE 0071 if (m_decompressor) { 0072 return m_decompressor->read(maxSize); 0073 } 0074 #endif 0075 return d->read(maxSize); 0076 } 0077 0078 QByteArray IODeviceSocket::readLine(qint64 maxSize) 0079 { 0080 #if TROJITA_COMPRESS_DEFLATE 0081 if (m_decompressor) { 0082 // FIXME: well, we apparently don't respect the maxSize argument... 0083 return m_decompressor->readLine(); 0084 } 0085 #endif 0086 return d->readLine(maxSize); 0087 } 0088 0089 qint64 IODeviceSocket::write(const QByteArray &byteArray) 0090 { 0091 #if TROJITA_COMPRESS_DEFLATE 0092 if (m_compressor) { 0093 m_compressor->write(d, &const_cast<QByteArray&>(byteArray)); 0094 return byteArray.size(); 0095 } 0096 #endif 0097 return d->write(byteArray); 0098 } 0099 0100 void IODeviceSocket::startTls() 0101 { 0102 QSslSocket *sock = qobject_cast<QSslSocket *>(d); 0103 if (! sock) 0104 throw std::invalid_argument("This IODeviceSocket is not a QSslSocket, and therefore doesn't support STARTTLS."); 0105 #if TROJITA_COMPRESS_DEFLATE 0106 if (m_compressor || m_decompressor) 0107 throw std::invalid_argument("DEFLATE is already active, cannot STARTTLS"); 0108 #endif 0109 sock->startClientEncryption(); 0110 } 0111 0112 void IODeviceSocket::startDeflate() 0113 { 0114 if (m_compressor || m_decompressor) 0115 throw std::invalid_argument("DEFLATE compression is already active"); 0116 0117 #if TROJITA_COMPRESS_DEFLATE 0118 m_compressor = new Rfc1951Compressor(); 0119 m_decompressor = new Rfc1951Decompressor(); 0120 #else 0121 throw std::invalid_argument("Trojita got built without zlib support"); 0122 #endif 0123 } 0124 0125 void IODeviceSocket::handleReadyRead() 0126 { 0127 #if TROJITA_COMPRESS_DEFLATE 0128 if (m_decompressor) { 0129 m_decompressor->consume(d); 0130 } 0131 #endif 0132 emit readyRead(); 0133 } 0134 0135 void IODeviceSocket::emitError() 0136 { 0137 emit disconnected(disconnectedMessage); 0138 } 0139 0140 ProcessSocket::ProcessSocket(QProcess *proc, const QString &executable, const QStringList &args): 0141 IODeviceSocket(proc), executable(executable), args(args) 0142 { 0143 connect(proc, &QProcess::stateChanged, this, &ProcessSocket::handleStateChanged); 0144 connect(proc, &QProcess::errorOccurred, this, &ProcessSocket::handleProcessError); 0145 } 0146 0147 ProcessSocket::~ProcessSocket() 0148 { 0149 close(); 0150 } 0151 0152 void ProcessSocket::close() 0153 { 0154 QProcess *proc = qobject_cast<QProcess *>(d); 0155 Q_ASSERT(proc); 0156 // Be nice to it, let it die peacefully before using an axe 0157 // QTBUG-5990, don't call waitForFinished() on a process which hadn't started 0158 if (proc->state() == QProcess::Running) { 0159 proc->terminate(); 0160 proc->waitForFinished(200); 0161 proc->kill(); 0162 } 0163 } 0164 0165 bool ProcessSocket::isDead() 0166 { 0167 QProcess *proc = qobject_cast<QProcess *>(d); 0168 Q_ASSERT(proc); 0169 return proc->state() != QProcess::Running; 0170 } 0171 0172 void ProcessSocket::handleProcessError(QProcess::ProcessError err) 0173 { 0174 Q_UNUSED(err); 0175 QProcess *proc = qobject_cast<QProcess *>(d); 0176 Q_ASSERT(proc); 0177 delayedDisconnect->stop(); 0178 emit disconnected(tr("Disconnected: %1").arg(proc->errorString())); 0179 } 0180 0181 void ProcessSocket::handleStateChanged() 0182 { 0183 /* Qt delivers the stateChanged() signal before the error() one. 0184 That's a problem because we really want to provide a nice error message 0185 to the user and QAbstractSocket::error() is not set yet by the time this 0186 function executes. That's why we have to delay the first disconnected() signal. */ 0187 0188 QProcess *proc = qobject_cast<QProcess *>(d); 0189 Q_ASSERT(proc); 0190 switch (proc->state()) { 0191 case QProcess::Running: 0192 emit stateChanged(Imap::CONN_STATE_CONNECTED_PRETLS_PRECAPS, tr("The process has started")); 0193 break; 0194 case QProcess::Starting: 0195 emit stateChanged(Imap::CONN_STATE_CONNECTING, tr("Starting process `%1 %2`").arg(executable, args.join(QStringLiteral(" ")))); 0196 break; 0197 case QProcess::NotRunning: { 0198 if (delayedDisconnect->isActive()) 0199 break; 0200 QString stdErr = QString::fromLocal8Bit(proc->readAllStandardError()); 0201 if (stdErr.isEmpty()) 0202 disconnectedMessage = tr("The process has exited with return code %1.").arg( 0203 proc->exitCode()); 0204 else 0205 disconnectedMessage = tr("The process has exited with return code %1:\n\n%2").arg( 0206 proc->exitCode()).arg(stdErr); 0207 delayedDisconnect->start(); 0208 } 0209 break; 0210 } 0211 } 0212 0213 void ProcessSocket::delayedStart() 0214 { 0215 QProcess *proc = qobject_cast<QProcess *>(d); 0216 Q_ASSERT(proc); 0217 proc->start(executable, args); 0218 } 0219 0220 SslTlsSocket::SslTlsSocket(QSslSocket *sock, const QString &host, const quint16 port, const bool startEncrypted): 0221 IODeviceSocket(sock), startEncrypted(startEncrypted), host(host), port(port), m_proxySettings(ProxySettings::RespectSystemProxy) 0222 { 0223 // The Qt API for deciding about whereabouts of a SSL connection is unfortunately blocking, ie. one is expected to 0224 // call a function from a slot attached to the sslErrors signal to tell the code whether to proceed or not. 0225 // In QML, one cannot display a dialog box with a nested event loop, so this means that we have to deal with SSL/TLS 0226 // establishing at higher level. 0227 sock->ignoreSslErrors(); 0228 sock->setProtocol(QSsl::AnyProtocol); 0229 sock->setPeerVerifyMode(QSslSocket::QueryPeer); 0230 0231 // In response to the attacks related to the SSL compression, Digia has decided to disable SSL compression starting in 0232 // Qt 4.8.4 -- see http://qt.digia.com/en/Release-Notes/security-issue-september-2012/. 0233 // I have brought this up on the imap-protocol mailing list; the consensus seemed to be that the likelihood of an 0234 // successful exploit on an IMAP conversation is very unlikely. The compression itself is, on the other hand, a 0235 // very worthwhile goal, so we explicitly enable it again. 0236 // Unfortunately, this was backported to older Qt versions as well (see qt4.git's 3488f1db96dbf70bb0486d3013d86252ebf433e0), 0237 // but there is no way of enabling compression back again. 0238 QSslConfiguration sslConf = sock->sslConfiguration(); 0239 sslConf.setSslOption(QSsl::SslOptionDisableCompression, false); 0240 sock->setSslConfiguration(sslConf); 0241 0242 connect(sock, &QSslSocket::encrypted, this, &Socket::encrypted); 0243 connect(sock, &QAbstractSocket::stateChanged, this, &SslTlsSocket::handleStateChanged); 0244 connect(sock, &QAbstractSocket::errorOccurred, this, &SslTlsSocket::handleSocketError); 0245 } 0246 0247 void SslTlsSocket::setProxySettings(const ProxySettings proxySettings, const QString &protocolTag) 0248 { 0249 m_proxySettings = proxySettings; 0250 m_protocolTag = protocolTag; 0251 } 0252 0253 void SslTlsSocket::close() 0254 { 0255 QSslSocket *sock = qobject_cast<QSslSocket*>(d); 0256 Q_ASSERT(sock); 0257 sock->abort(); 0258 emit disconnected(tr("Connection closed")); 0259 } 0260 0261 void SslTlsSocket::handleStateChanged() 0262 { 0263 /* Qt delivers the stateChanged() signal before the error() one. 0264 That's a problem because we really want to provide a nice error message 0265 to the user and QAbstractSocket::error() is not set yet by the time this 0266 function executes. That's why we have to delay the first disconnected() signal. */ 0267 0268 QAbstractSocket *sock = qobject_cast<QAbstractSocket *>(d); 0269 Q_ASSERT(sock); 0270 QString proxyMsg; 0271 switch (sock->proxy().type()) { 0272 case QNetworkProxy::NoProxy: 0273 break; 0274 case QNetworkProxy::HttpCachingProxy: 0275 Q_ASSERT_X(false, "proxy detection", 0276 "Qt should have returned a proxy capable of tunneling, but we got back an HTTP proxy."); 0277 break; 0278 case QNetworkProxy::FtpCachingProxy: 0279 Q_ASSERT_X(false, "proxy detection", 0280 "Qt should have returned a proxy capable of tunneling, but we got back an FTP proxy."); 0281 break; 0282 case QNetworkProxy::DefaultProxy: 0283 proxyMsg = tr(" (via proxy %1)").arg(sock->proxy().hostName()); 0284 break; 0285 case QNetworkProxy::Socks5Proxy: 0286 proxyMsg = tr(" (via SOCKS5 proxy %1)").arg(sock->proxy().hostName()); 0287 break; 0288 case QNetworkProxy::HttpProxy: 0289 proxyMsg = tr(" (via HTTP proxy %1)").arg(sock->proxy().hostName()); 0290 break; 0291 } 0292 switch (sock->state()) { 0293 case QAbstractSocket::HostLookupState: 0294 emit stateChanged(Imap::CONN_STATE_HOST_LOOKUP, tr("Looking up %1%2...").arg(host, 0295 sock->proxy().capabilities().testFlag(QNetworkProxy::HostNameLookupCapability) ? 0296 proxyMsg : QString())); 0297 break; 0298 case QAbstractSocket::ConnectingState: 0299 emit stateChanged(Imap::CONN_STATE_CONNECTING, tr("Connecting to %1:%2%3%4...").arg( 0300 host, QString::number(port), startEncrypted ? tr(" (SSL)") : QString(), 0301 sock->proxy().capabilities().testFlag(QNetworkProxy::TunnelingCapability) ? 0302 proxyMsg : QString())); 0303 break; 0304 case QAbstractSocket::BoundState: 0305 case QAbstractSocket::ListeningState: 0306 break; 0307 case QAbstractSocket::ConnectedState: 0308 if (! startEncrypted) { 0309 emit stateChanged(Imap::CONN_STATE_CONNECTED_PRETLS_PRECAPS, tr("Connected")); 0310 } else { 0311 emit stateChanged(Imap::CONN_STATE_SSL_HANDSHAKE, tr("Negotiating encryption...")); 0312 } 0313 break; 0314 case QAbstractSocket::UnconnectedState: 0315 case QAbstractSocket::ClosingState: 0316 disconnectedMessage = tr("Socket is disconnected: %1").arg(sock->errorString()); 0317 delayedDisconnect->start(); 0318 break; 0319 } 0320 } 0321 0322 void SslTlsSocket::handleSocketError(QAbstractSocket::SocketError err) 0323 { 0324 Q_UNUSED(err); 0325 QAbstractSocket *sock = qobject_cast<QAbstractSocket *>(d); 0326 Q_ASSERT(sock); 0327 delayedDisconnect->stop(); 0328 emit disconnected(tr("The underlying socket is having troubles when processing connection to %1:%2: %3").arg( 0329 host, QString::number(port), sock->errorString())); 0330 } 0331 0332 bool SslTlsSocket::isDead() 0333 { 0334 QAbstractSocket *sock = qobject_cast<QAbstractSocket *>(d); 0335 Q_ASSERT(sock); 0336 return sock->state() != QAbstractSocket::ConnectedState; 0337 } 0338 0339 void SslTlsSocket::delayedStart() 0340 { 0341 QSslSocket *sock = qobject_cast<QSslSocket *>(d); 0342 Q_ASSERT(sock); 0343 0344 switch (m_proxySettings) { 0345 case Streams::ProxySettings::RespectSystemProxy: 0346 { 0347 QNetworkProxy setting; 0348 QNetworkProxyQuery query = QNetworkProxyQuery(host, port, m_protocolTag, QNetworkProxyQuery::TcpSocket); 0349 0350 // set to true if a capable setting is found 0351 bool capableSettingFound = false; 0352 0353 // set to true if at least one valid setting is found 0354 bool settingFound = false; 0355 0356 // FIXME: this static function works particularly slow in Windows 0357 QList<QNetworkProxy> proxySettingsList = QNetworkProxyFactory::systemProxyForQuery(query); 0358 0359 /* Proxy Settings are read from the user's environment variables by the above static method. 0360 * A peculiar case is with *nix systems, where an undefined environment variable is returned as 0361 * an empty string. Such entries *might* exist in our proxySettingsList, and shouldn't be processed. 0362 * One good check is to use hostName() of the QNetworkProxy object, and treat the Proxy Setting as invalid if 0363 * the host name is empty. */ 0364 Q_FOREACH (setting, proxySettingsList) { 0365 if (!setting.hostName().isEmpty()) { 0366 settingFound = true; 0367 0368 // now check whether setting has capabilities 0369 if (setting.capabilities().testFlag(QNetworkProxy::TunnelingCapability)) { 0370 sock->setProxy(setting); 0371 capableSettingFound = true; 0372 break; 0373 } 0374 } 0375 } 0376 0377 if (!settingFound || proxySettingsList.isEmpty()) { 0378 sock->setProxy(QNetworkProxy::NoProxy); 0379 } else if (!capableSettingFound) { 0380 emit disconnected(tr("The underlying socket is having troubles when processing connection to %1:%2: %3") 0381 .arg(host, QString::number(port), QStringLiteral("Cannot find proxy setting capable of tunneling"))); 0382 } 0383 break; 0384 } 0385 case Streams::ProxySettings::DirectConnect: 0386 sock->setProxy(QNetworkProxy::NoProxy); 0387 break; 0388 } 0389 0390 if (startEncrypted) 0391 sock->connectToHostEncrypted(host, port); 0392 else 0393 sock->connectToHost(host, port); 0394 } 0395 0396 QList<QSslCertificate> SslTlsSocket::sslChain() const 0397 { 0398 QSslSocket *sock = qobject_cast<QSslSocket *>(d); 0399 Q_ASSERT(sock); 0400 return sock->peerCertificateChain(); 0401 } 0402 0403 QList<QSslError> SslTlsSocket::sslErrors() const 0404 { 0405 QSslSocket *sock = qobject_cast<QSslSocket *>(d); 0406 Q_ASSERT(sock); 0407 return sock->sslHandshakeErrors(); 0408 } 0409 0410 bool SslTlsSocket::isConnectingEncryptedSinceStart() const 0411 { 0412 return startEncrypted; 0413 } 0414 0415 }