File indexing completed on 2024-12-22 05:03:15

0001 /*
0002     SPDX-FileCopyrightText: 2015 Daniel Vrátil <dvratil@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "kmanagersieve_debug.h"
0008 #include "response.h"
0009 #include "session.h"
0010 #include "sessionthread_p.h"
0011 
0012 #include <QSslCipher>
0013 #include <QThread>
0014 #include <QTimer>
0015 
0016 #include <KLocalizedString>
0017 
0018 #include <KIO/Global>
0019 #include <KIO/Job>
0020 
0021 #include "sasl-common.h"
0022 #include <cstring> // strlen()
0023 
0024 using namespace KManageSieve;
0025 
0026 static const sasl_callback_t callbacks[] = {{SASL_CB_ECHOPROMPT, nullptr, nullptr},
0027                                             {SASL_CB_NOECHOPROMPT, nullptr, nullptr},
0028                                             {SASL_CB_GETREALM, nullptr, nullptr},
0029                                             {SASL_CB_USER, nullptr, nullptr},
0030                                             {SASL_CB_AUTHNAME, nullptr, nullptr},
0031                                             {SASL_CB_PASS, nullptr, nullptr},
0032                                             {SASL_CB_CANON_USER, nullptr, nullptr},
0033                                             {SASL_CB_LIST_END, nullptr, nullptr}};
0034 
0035 SessionThread::SessionThread(Session *session, QObject *parent)
0036     : QObject(parent)
0037     , m_session(session)
0038 {
0039     static bool saslInitialized = false;
0040     if (!saslInitialized) {
0041         // Call initSASL() from main thread
0042         initSASL();
0043         saslInitialized = true;
0044     }
0045 
0046     auto thread = new QThread();
0047     moveToThread(thread);
0048     thread->start();
0049     QMetaObject::invokeMethod(this, "doInit");
0050 }
0051 
0052 SessionThread::~SessionThread()
0053 {
0054     QMetaObject::invokeMethod(this, &SessionThread::doDestroy, Qt::QueuedConnection);
0055     if (!thread()->wait(10 * 1000)) {
0056         thread()->terminate();
0057         thread()->wait();
0058     }
0059 
0060     delete thread();
0061 }
0062 
0063 // Called in secondary thread
0064 void SessionThread::doInit()
0065 {
0066     Q_ASSERT(QThread::currentThread() == thread());
0067     m_socket = std::make_unique<QSslSocket>();
0068     connect(m_socket.get(), &QSslSocket::readyRead, this, &SessionThread::slotDataReceived);
0069     connect(m_socket.get(), &QAbstractSocket::errorOccurred, this, &SessionThread::slotSocketError);
0070     connect(m_socket.get(), &QSslSocket::disconnected, this, &SessionThread::socketDisconnected);
0071     connect(m_socket.get(), &QSslSocket::connected, this, &SessionThread::socketConnected);
0072 }
0073 
0074 // Called in secondary thread
0075 void SessionThread::doDestroy()
0076 {
0077     Q_ASSERT(QThread::currentThread() == thread());
0078 
0079     doDisconnectFromHost(false);
0080     m_socket.reset();
0081     delete m_sslCheck;
0082 
0083     thread()->quit();
0084 }
0085 
0086 // Called in main thread
0087 void SessionThread::connectToHost(const QUrl &url)
0088 {
0089     QMetaObject::invokeMethod(
0090         this,
0091         [this, url]() {
0092             doConnectToHost(url);
0093         },
0094         Qt::QueuedConnection);
0095 }
0096 
0097 // Called in secondary thread
0098 void SessionThread::doConnectToHost(const QUrl &url)
0099 {
0100     Q_ASSERT(QThread::currentThread() == thread());
0101 
0102     if (m_socket->state() == QAbstractSocket::ConnectedState || m_socket->state() == QAbstractSocket::ConnectingState) {
0103         return;
0104     }
0105 
0106     m_url = url;
0107     m_socket->connectToHost(url.host(), url.port() ? url.port() : 4190);
0108 }
0109 
0110 // Called in main thread
0111 void SessionThread::disconnectFromHost(bool sendLogout)
0112 {
0113     QMetaObject::invokeMethod(
0114         this,
0115         [this, sendLogout]() {
0116             doDisconnectFromHost(sendLogout);
0117         },
0118         Qt::QueuedConnection);
0119 }
0120 
0121 // Called in secondary thread
0122 void SessionThread::doDisconnectFromHost(bool sendLogout)
0123 {
0124     Q_ASSERT(QThread::currentThread() == thread());
0125 
0126     if (sendLogout) {
0127         doSendData("LOGOUT");
0128     }
0129     m_socket->disconnectFromHost();
0130 }
0131 
0132 // Called in main thread
0133 void SessionThread::sendData(const QByteArray &data)
0134 {
0135     QMetaObject::invokeMethod(
0136         this,
0137         [this, data]() {
0138             doSendData(data);
0139         },
0140         Qt::QueuedConnection);
0141 }
0142 
0143 // Called in secondary thread
0144 void SessionThread::doSendData(const QByteArray &data)
0145 {
0146     Q_ASSERT(QThread::currentThread() == thread());
0147 
0148     qCDebug(KMANAGERSIEVE_LOG) << "C: " << data;
0149     m_socket->write(data);
0150     m_socket->write("\r\n");
0151 }
0152 
0153 // Called in secondary thread
0154 void SessionThread::slotDataReceived()
0155 {
0156     Q_ASSERT(QThread::currentThread() == thread());
0157     if (m_pendingQuantity > 0) {
0158         const QByteArray buffer = m_socket->read(qMin(m_pendingQuantity, m_socket->bytesAvailable()));
0159         m_data += buffer;
0160         m_pendingQuantity -= buffer.size();
0161         if (m_pendingQuantity <= 0) {
0162             qCDebug(KMANAGERSIEVE_LOG) << "S: " << m_data.trimmed();
0163             Q_EMIT responseReceived(m_lastResponse, m_data);
0164         } else {
0165             return; // waiting for more data
0166         }
0167     }
0168 
0169     while (m_socket->canReadLine()) {
0170         QByteArray line = m_socket->readLine();
0171         if (line.endsWith("\r\n")) { // krazy:exclude=strings
0172             line.chop(2);
0173         }
0174         if (line.isEmpty()) {
0175             continue; // ignore CRLF after data blocks
0176         }
0177         qCDebug(KMANAGERSIEVE_LOG) << "S: " << line;
0178         Response r;
0179         if (!r.parseResponse(line)) {
0180             qCDebug(KMANAGERSIEVE_LOG) << "protocol violation!";
0181             doDisconnectFromHost(false);
0182         }
0183         qCDebug(KMANAGERSIEVE_LOG) << r.type() << r.key() << r.value() << r.extra() << r.quantity();
0184 
0185         m_lastResponse = r;
0186         if (r.quantity() > 0) {
0187             m_data.clear();
0188             m_pendingQuantity = r.quantity();
0189             slotDataReceived(); // in case the data block is already completely in the buffer
0190             return;
0191         } else if (r.operationResult() == Response::Bye) {
0192             doDisconnectFromHost(false);
0193             return;
0194         }
0195         Q_EMIT responseReceived(r, QByteArray());
0196     }
0197 }
0198 
0199 // Called in secondary thread
0200 void SessionThread::slotSocketError()
0201 {
0202     Q_ASSERT(QThread::currentThread() == thread());
0203 
0204     qCWarning(KMANAGERSIEVE_LOG) << Q_FUNC_INFO << m_socket->error() << m_socket->errorString();
0205 
0206     Q_EMIT error(m_socket->error(), m_socket->errorString());
0207     doDisconnectFromHost(false);
0208 }
0209 
0210 // Called in main thread
0211 void SessionThread::startAuthentication()
0212 {
0213     QMetaObject::invokeMethod(this, &SessionThread::doStartAuthentication, Qt::QueuedConnection);
0214 }
0215 
0216 // Called in secondary thread
0217 void SessionThread::handleSaslAuthError()
0218 {
0219     Q_EMIT error(QAbstractSocket::UnknownSocketError, KIO::buildErrorString(KIO::ERR_CANNOT_AUTHENTICATE, QString::fromUtf8(sasl_errdetail(m_sasl_conn))));
0220     doDisconnectFromHost(true);
0221 }
0222 
0223 // Called in secondary thread
0224 void SessionThread::doStartAuthentication()
0225 {
0226     Q_ASSERT(QThread::currentThread() == thread());
0227 
0228     int result;
0229     m_sasl_conn = nullptr;
0230     m_sasl_client_interact = nullptr;
0231     const char *out = nullptr;
0232     uint outlen;
0233     const char *mechusing = nullptr;
0234 
0235     result = sasl_client_new("sieve", m_url.host().toLatin1().constData(), nullptr, nullptr, callbacks, 0, &m_sasl_conn);
0236     if (result != SASL_OK) {
0237         handleSaslAuthError();
0238         return;
0239     }
0240 
0241     do {
0242         result = sasl_client_start(m_sasl_conn,
0243                                    m_session->requestedSaslMethod().join(QLatin1Char(' ')).toLatin1().constData(),
0244                                    &m_sasl_client_interact,
0245                                    &out,
0246                                    &outlen,
0247                                    &mechusing);
0248         if (result == SASL_INTERACT) {
0249             if (!saslInteract(m_sasl_client_interact)) {
0250                 handleSaslAuthError();
0251                 sasl_dispose(&m_sasl_conn);
0252                 return;
0253             }
0254         }
0255     } while (result == SASL_INTERACT);
0256 
0257     if (result != SASL_CONTINUE && result != SASL_OK) {
0258         handleSaslAuthError();
0259         sasl_dispose(&m_sasl_conn);
0260         return;
0261     }
0262 
0263     qCDebug(KMANAGERSIEVE_LOG) << "Preferred authentication method is " << mechusing << ".";
0264 
0265     QByteArray authCommand = "AUTHENTICATE \"" + QByteArray(mechusing) + QByteArray("\"");
0266     const QByteArray challenge = QByteArray::fromRawData(out, outlen).toBase64();
0267     if (!challenge.isEmpty()) {
0268         authCommand += " \"";
0269         authCommand += challenge;
0270         authCommand += '\"';
0271     }
0272     doSendData(authCommand);
0273 }
0274 
0275 // Called in main thread
0276 void SessionThread::continueAuthentication(const Response &response, const QByteArray &data)
0277 {
0278     QMetaObject::invokeMethod(this, "doContinueAuthentication", Qt::QueuedConnection, Q_ARG(KManageSieve::Response, response), Q_ARG(QByteArray, data));
0279 }
0280 
0281 // Called in secondary thread
0282 void SessionThread::doContinueAuthentication(const Response &response, const QByteArray &data)
0283 {
0284     Q_ASSERT(QThread::currentThread() == thread());
0285 
0286     if (response.operationResult() == Response::Other) {
0287         if (!saslClientStep(data)) {
0288             handleSaslAuthError();
0289             return;
0290         }
0291     } else {
0292         sasl_dispose(&m_sasl_conn);
0293         if (response.operationSuccessful()) {
0294             qCDebug(KMANAGERSIEVE_LOG) << "Authentication complete.";
0295             Q_EMIT authenticationDone();
0296         } else {
0297             Q_EMIT error(QAbstractSocket::UnknownSocketError,
0298                          KIO::buildErrorString(KIO::ERR_CANNOT_AUTHENTICATE,
0299                                                i18n("Authentication failed.\nMost likely the password is wrong.\nThe server responded:\n%1",
0300                                                     QString::fromLatin1(response.action()))));
0301             doDisconnectFromHost(true);
0302         }
0303     }
0304 }
0305 
0306 // Called in secondary thread
0307 bool SessionThread::saslInteract(void *in)
0308 {
0309     Q_ASSERT(QThread::currentThread() == thread());
0310 
0311     qCDebug(KMANAGERSIEVE_LOG) << "SessionThread::saslInteract";
0312     auto *interact = (sasl_interact_t *)in;
0313 
0314     // some mechanisms do not require username && pass, so it doesn't need a popup
0315     // window for getting this info
0316     for (; interact->id != SASL_CB_LIST_END; ++interact) {
0317         if (interact->id == SASL_CB_AUTHNAME || interact->id == SASL_CB_PASS) {
0318             if (m_url.userName().isEmpty() || m_url.password().isEmpty()) {
0319                 AuthDetails authData;
0320                 QMetaObject::invokeMethod(m_session,
0321                                           "requestAuthDetails",
0322                                           Qt::BlockingQueuedConnection,
0323                                           Q_RETURN_ARG(KManageSieve::AuthDetails, authData),
0324                                           Q_ARG(QUrl, m_url));
0325 
0326                 if (authData.valid) {
0327                     m_url.setUserName(authData.username);
0328                     m_url.setPassword(authData.password);
0329                 } else {
0330                     return false;
0331                 }
0332             }
0333             break;
0334         }
0335     }
0336 
0337     interact = (sasl_interact_t *)in;
0338     while (interact->id != SASL_CB_LIST_END) {
0339         qCDebug(KMANAGERSIEVE_LOG) << "SASL_INTERACT id: " << interact->id;
0340         switch (interact->id) {
0341         case SASL_CB_USER:
0342         case SASL_CB_AUTHNAME:
0343             qCDebug(KMANAGERSIEVE_LOG) << "SASL_CB_[AUTHNAME|USER]: '" << m_url.userName() << "'";
0344             interact->result = strdup(m_url.userName().toUtf8().constData());
0345             if (interact->result) {
0346                 interact->len = strlen((const char *)interact->result);
0347             } else {
0348                 interact->len = 0;
0349             }
0350             break;
0351         case SASL_CB_PASS:
0352             qCDebug(KMANAGERSIEVE_LOG) << "SASL_CB_PASS: [hidden] ";
0353             interact->result = strdup(m_url.password().toUtf8().constData());
0354             if (interact->result) {
0355                 interact->len = strlen((const char *)interact->result);
0356             } else {
0357                 interact->len = 0;
0358             }
0359             break;
0360         default:
0361             interact->result = nullptr;
0362             interact->len = 0;
0363             break;
0364         }
0365         interact++;
0366     }
0367     return true;
0368 }
0369 
0370 // Called in secondary thread
0371 bool SessionThread::saslClientStep(const QByteArray &challenge)
0372 {
0373     int result;
0374     const char *out = nullptr;
0375     uint outlen;
0376 
0377     const QByteArray challenge_decoded = QByteArray::fromBase64(challenge);
0378     do {
0379         result = sasl_client_step(m_sasl_conn,
0380                                   challenge_decoded.isEmpty() ? nullptr : challenge_decoded.data(),
0381                                   challenge_decoded.size(),
0382                                   &m_sasl_client_interact,
0383                                   &out,
0384                                   &outlen);
0385         if (result == SASL_INTERACT) {
0386             if (!saslInteract(m_sasl_client_interact)) {
0387                 sasl_dispose(&m_sasl_conn);
0388                 return false;
0389             }
0390         }
0391     } while (result == SASL_INTERACT);
0392 
0393     qCDebug(KMANAGERSIEVE_LOG) << "sasl_client_step: " << result;
0394     if (result != SASL_CONTINUE && result != SASL_OK) {
0395         qCDebug(KMANAGERSIEVE_LOG) << "sasl_client_step failed with: " << result << QString::fromUtf8(sasl_errdetail(m_sasl_conn));
0396         sasl_dispose(&m_sasl_conn);
0397         return false;
0398     }
0399 
0400     doSendData('\"' + QByteArray::fromRawData(out, outlen).toBase64() + '\"');
0401     return true;
0402 }
0403 
0404 // Called in main thread
0405 void SessionThread::startSsl()
0406 {
0407     QMetaObject::invokeMethod(this, &SessionThread::doStartSsl, Qt::QueuedConnection);
0408 }
0409 
0410 // Called in secondary thread
0411 void SessionThread::doStartSsl()
0412 {
0413     Q_ASSERT(QThread::currentThread() == thread());
0414 
0415     qCDebug(KMANAGERSIEVE_LOG) << "SessionThread::doStartSsl()";
0416     if (!m_sslCheck) {
0417         m_sslCheck = new QTimer(this);
0418         m_sslCheck->setInterval(60 * 1000);
0419         connect(m_sslCheck, &QTimer::timeout, this, &SessionThread::slotSslTimeout);
0420     }
0421     m_socket->setProtocol(QSsl::SecureProtocols);
0422     m_socket->ignoreSslErrors();
0423     connect(m_socket.get(), &QSslSocket::encrypted, this, &SessionThread::slotEncryptedDone);
0424     m_sslCheck->start();
0425     m_socket->startClientEncryption();
0426 }
0427 
0428 // Called in secondary thread
0429 void SessionThread::slotSslTimeout()
0430 {
0431     Q_ASSERT(QThread::currentThread() == thread());
0432 
0433     disconnect(m_socket.get(), &QSslSocket::encrypted, this, &SessionThread::slotEncryptedDone);
0434     sslResult(false);
0435 }
0436 
0437 // Called in secondary thread
0438 void SessionThread::slotEncryptedDone()
0439 {
0440     Q_ASSERT(QThread::currentThread() == thread());
0441 
0442     m_sslCheck->stop();
0443     sslResult(true);
0444 }
0445 
0446 // Called in secondary thread
0447 void SessionThread::sslResult(bool encrypted)
0448 {
0449     Q_ASSERT(QThread::currentThread() == thread());
0450 
0451     const QSslCipher cipher = m_socket->sessionCipher();
0452     const int numberOfSslError = m_socket->sslHandshakeErrors().count();
0453     if (!encrypted || numberOfSslError > 0 || !m_socket->isEncrypted() || cipher.isNull() || cipher.usedBits() == 0) {
0454         qCDebug(KMANAGERSIEVE_LOG) << "Initial SSL handshake failed. cipher.isNull() is" << cipher.isNull() << ", cipher.usedBits() is" << cipher.usedBits()
0455                                    << ", the socket says:" << m_socket->errorString() << "and the list of SSL errors contains" << numberOfSslError << "items.";
0456 
0457         Q_EMIT sslError(KSslErrorUiData(m_socket.get()));
0458     } else {
0459         Q_EMIT sslDone();
0460     }
0461 }
0462 
0463 #include "moc_sessionthread_p.cpp"