Libksieve

sessionthread.cpp
1/*
2 SPDX-FileCopyrightText: 2015 Daniel Vrátil <dvratil@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "kmanagersieve_debug.h"
8#include "response.h"
9#include "session.h"
10#include "sessionthread_p.h"
11
12#include <QSslCipher>
13#include <QThread>
14#include <QTimer>
15
16#include <KLocalizedString>
17
18#include <KIO/Global>
19#include <KIO/Job>
20
21#include "sasl-common.h"
22#include <cstring> // strlen()
23
24using namespace KManageSieve;
25
26static const sasl_callback_t callbacks[] = {{SASL_CB_ECHOPROMPT, nullptr, nullptr},
27 {SASL_CB_NOECHOPROMPT, nullptr, nullptr},
28 {SASL_CB_GETREALM, nullptr, nullptr},
29 {SASL_CB_USER, nullptr, nullptr},
30 {SASL_CB_AUTHNAME, nullptr, nullptr},
31 {SASL_CB_PASS, nullptr, nullptr},
32 {SASL_CB_CANON_USER, nullptr, nullptr},
33 {SASL_CB_LIST_END, nullptr, nullptr}};
34
35SessionThread::SessionThread(Session *session, QObject *parent)
36 : QObject(parent)
37 , m_session(session)
38{
39 static bool saslInitialized = false;
40 if (!saslInitialized) {
41 // Call initSASL() from main thread
42 initSASL();
43 saslInitialized = true;
44 }
45
46 auto thread = new QThread();
47 moveToThread(thread);
48 thread->start();
49 QMetaObject::invokeMethod(this, "doInit");
50}
51
52SessionThread::~SessionThread()
53{
54 QMetaObject::invokeMethod(this, &SessionThread::doDestroy, Qt::QueuedConnection);
55 if (!thread()->wait(10 * 1000)) {
56 thread()->terminate();
57 thread()->wait();
58 }
59
60 delete thread();
61}
62
63// Called in secondary thread
64void SessionThread::doInit()
65{
66 Q_ASSERT(QThread::currentThread() == thread());
67 m_socket = std::make_unique<QSslSocket>();
68 connect(m_socket.get(), &QSslSocket::readyRead, this, &SessionThread::slotDataReceived);
69 connect(m_socket.get(), &QAbstractSocket::errorOccurred, this, &SessionThread::slotSocketError);
70 connect(m_socket.get(), &QSslSocket::disconnected, this, &SessionThread::socketDisconnected);
71 connect(m_socket.get(), &QSslSocket::connected, this, &SessionThread::socketConnected);
72}
73
74// Called in secondary thread
75void SessionThread::doDestroy()
76{
77 Q_ASSERT(QThread::currentThread() == thread());
78
79 doDisconnectFromHost(false);
80 m_socket.reset();
81 delete m_sslCheck;
82
83 thread()->quit();
84}
85
86// Called in main thread
87void SessionThread::connectToHost(const QUrl &url)
88{
90 this,
91 [this, url]() {
92 doConnectToHost(url);
93 },
95}
96
97// Called in secondary thread
98void SessionThread::doConnectToHost(const QUrl &url)
99{
100 Q_ASSERT(QThread::currentThread() == thread());
101
102 if (m_socket->state() == QAbstractSocket::ConnectedState || m_socket->state() == QAbstractSocket::ConnectingState) {
103 return;
104 }
105
106 m_url = url;
107 m_socket->connectToHost(url.host(), url.port() ? url.port() : 4190);
108}
109
110// Called in main thread
111void SessionThread::disconnectFromHost(bool sendLogout)
112{
114 this,
115 [this, sendLogout]() {
116 doDisconnectFromHost(sendLogout);
117 },
119}
120
121// Called in secondary thread
122void SessionThread::doDisconnectFromHost(bool sendLogout)
123{
124 Q_ASSERT(QThread::currentThread() == thread());
125
126 if (sendLogout) {
127 doSendData("LOGOUT");
128 }
129 m_socket->disconnectFromHost();
130}
131
132// Called in main thread
133void SessionThread::sendData(const QByteArray &data)
134{
136 this,
137 [this, data]() {
138 doSendData(data);
139 },
141}
142
143// Called in secondary thread
144void SessionThread::doSendData(const QByteArray &data)
145{
146 Q_ASSERT(QThread::currentThread() == thread());
147
148 qCDebug(KMANAGERSIEVE_LOG) << "C: " << data;
149 m_socket->write(data);
150 m_socket->write("\r\n");
151}
152
153// Called in secondary thread
154void SessionThread::slotDataReceived()
155{
156 Q_ASSERT(QThread::currentThread() == thread());
157 if (m_pendingQuantity > 0) {
158 const QByteArray buffer = m_socket->read(qMin(m_pendingQuantity, m_socket->bytesAvailable()));
159 m_data += buffer;
160 m_pendingQuantity -= buffer.size();
161 if (m_pendingQuantity <= 0) {
162 qCDebug(KMANAGERSIEVE_LOG) << "S: " << m_data.trimmed();
163 Q_EMIT responseReceived(m_lastResponse, m_data);
164 } else {
165 return; // waiting for more data
166 }
167 }
168
169 while (m_socket->canReadLine()) {
170 QByteArray line = m_socket->readLine();
171 if (line.endsWith("\r\n")) { // krazy:exclude=strings
172 line.chop(2);
173 }
174 if (line.isEmpty()) {
175 continue; // ignore CRLF after data blocks
176 }
177 qCDebug(KMANAGERSIEVE_LOG) << "S: " << line;
178 Response r;
179 if (!r.parseResponse(line)) {
180 qCDebug(KMANAGERSIEVE_LOG) << "protocol violation!";
181 doDisconnectFromHost(false);
182 }
183 qCDebug(KMANAGERSIEVE_LOG) << r.type() << r.key() << r.value() << r.extra() << r.quantity();
184
185 m_lastResponse = r;
186 if (r.quantity() > 0) {
187 m_data.clear();
188 m_pendingQuantity = r.quantity();
189 slotDataReceived(); // in case the data block is already completely in the buffer
190 return;
191 } else if (r.operationResult() == Response::Bye) {
192 doDisconnectFromHost(false);
193 return;
194 }
195 Q_EMIT responseReceived(r, QByteArray());
196 }
197}
198
199// Called in secondary thread
200void SessionThread::slotSocketError()
201{
202 Q_ASSERT(QThread::currentThread() == thread());
203
204 qCWarning(KMANAGERSIEVE_LOG) << Q_FUNC_INFO << m_socket->error() << m_socket->errorString();
205
206 Q_EMIT error(m_socket->error(), m_socket->errorString());
207 doDisconnectFromHost(false);
208}
209
210// Called in main thread
211void SessionThread::startAuthentication()
212{
213 QMetaObject::invokeMethod(this, &SessionThread::doStartAuthentication, Qt::QueuedConnection);
214}
215
216// Called in secondary thread
217void SessionThread::handleSaslAuthError()
218{
219 Q_EMIT error(QAbstractSocket::UnknownSocketError, KIO::buildErrorString(KIO::ERR_CANNOT_AUTHENTICATE, QString::fromUtf8(sasl_errdetail(m_sasl_conn))));
220 doDisconnectFromHost(true);
221}
222
223// Called in secondary thread
224void SessionThread::doStartAuthentication()
225{
226 Q_ASSERT(QThread::currentThread() == thread());
227
228 int result;
229 m_sasl_conn = nullptr;
230 m_sasl_client_interact = nullptr;
231 const char *out = nullptr;
232 uint outlen;
233 const char *mechusing = nullptr;
234
235 result = sasl_client_new("sieve", m_url.host().toLatin1().constData(), nullptr, nullptr, callbacks, 0, &m_sasl_conn);
236 if (result != SASL_OK) {
237 handleSaslAuthError();
238 return;
239 }
240
241 do {
242 result = sasl_client_start(m_sasl_conn,
243 m_session->requestedSaslMethod().join(QLatin1Char(' ')).toLatin1().constData(),
244 &m_sasl_client_interact,
245 &out,
246 &outlen,
247 &mechusing);
248 if (result == SASL_INTERACT) {
249 if (!saslInteract(m_sasl_client_interact)) {
250 handleSaslAuthError();
251 sasl_dispose(&m_sasl_conn);
252 return;
253 }
254 }
255 } while (result == SASL_INTERACT);
256
257 if (result != SASL_CONTINUE && result != SASL_OK) {
258 handleSaslAuthError();
259 sasl_dispose(&m_sasl_conn);
260 return;
261 }
262
263 qCDebug(KMANAGERSIEVE_LOG) << "Preferred authentication method is " << mechusing << ".";
264
265 QByteArray authCommand = "AUTHENTICATE \"" + QByteArray(mechusing) + QByteArray("\"");
266 const QByteArray challenge = QByteArray::fromRawData(out, outlen).toBase64();
267 if (!challenge.isEmpty()) {
268 authCommand += " \"";
269 authCommand += challenge;
270 authCommand += '\"';
271 }
272 doSendData(authCommand);
273}
274
275// Called in main thread
276void SessionThread::continueAuthentication(const Response &response, const QByteArray &data)
277{
278 QMetaObject::invokeMethod(this, "doContinueAuthentication", Qt::QueuedConnection, Q_ARG(KManageSieve::Response, response), Q_ARG(QByteArray, data));
279}
280
281// Called in secondary thread
282void SessionThread::doContinueAuthentication(const Response &response, const QByteArray &data)
283{
284 Q_ASSERT(QThread::currentThread() == thread());
285
286 if (response.operationResult() == Response::Other) {
287 if (!saslClientStep(data)) {
288 handleSaslAuthError();
289 return;
290 }
291 } else {
292 sasl_dispose(&m_sasl_conn);
293 if (response.operationSuccessful()) {
294 qCDebug(KMANAGERSIEVE_LOG) << "Authentication complete.";
295 Q_EMIT authenticationDone();
296 } else {
298 KIO::buildErrorString(KIO::ERR_CANNOT_AUTHENTICATE,
299 i18n("Authentication failed.\nMost likely the password is wrong.\nThe server responded:\n%1",
300 QString::fromLatin1(response.action()))));
301 doDisconnectFromHost(true);
302 }
303 }
304}
305
306// Called in secondary thread
307bool SessionThread::saslInteract(void *in)
308{
309 Q_ASSERT(QThread::currentThread() == thread());
310
311 qCDebug(KMANAGERSIEVE_LOG) << "SessionThread::saslInteract";
312 auto *interact = (sasl_interact_t *)in;
313
314 // some mechanisms do not require username && pass, so it doesn't need a popup
315 // window for getting this info
316 for (; interact->id != SASL_CB_LIST_END; ++interact) {
317 if (interact->id == SASL_CB_AUTHNAME || interact->id == SASL_CB_PASS) {
318 if (m_url.userName().isEmpty() || m_url.password().isEmpty()) {
319 AuthDetails authData;
321 "requestAuthDetails",
323 Q_RETURN_ARG(KManageSieve::AuthDetails, authData),
324 Q_ARG(QUrl, m_url));
325
326 if (authData.valid) {
327 m_url.setUserName(authData.username);
328 m_url.setPassword(authData.password);
329 } else {
330 return false;
331 }
332 }
333 break;
334 }
335 }
336
337 interact = (sasl_interact_t *)in;
338 while (interact->id != SASL_CB_LIST_END) {
339 qCDebug(KMANAGERSIEVE_LOG) << "SASL_INTERACT id: " << interact->id;
340 switch (interact->id) {
341 case SASL_CB_USER:
342 case SASL_CB_AUTHNAME:
343 qCDebug(KMANAGERSIEVE_LOG) << "SASL_CB_[AUTHNAME|USER]: '" << m_url.userName() << "'";
344 interact->result = strdup(m_url.userName().toUtf8().constData());
345 if (interact->result) {
346 interact->len = strlen((const char *)interact->result);
347 } else {
348 interact->len = 0;
349 }
350 break;
351 case SASL_CB_PASS:
352 qCDebug(KMANAGERSIEVE_LOG) << "SASL_CB_PASS: [hidden] ";
353 interact->result = strdup(m_url.password().toUtf8().constData());
354 if (interact->result) {
355 interact->len = strlen((const char *)interact->result);
356 } else {
357 interact->len = 0;
358 }
359 break;
360 default:
361 interact->result = nullptr;
362 interact->len = 0;
363 break;
364 }
365 interact++;
366 }
367 return true;
368}
369
370// Called in secondary thread
371bool SessionThread::saslClientStep(const QByteArray &challenge)
372{
373 int result;
374 const char *out = nullptr;
375 uint outlen;
376
377 const QByteArray challenge_decoded = QByteArray::fromBase64(challenge);
378 do {
379 result = sasl_client_step(m_sasl_conn,
380 challenge_decoded.isEmpty() ? nullptr : challenge_decoded.data(),
381 challenge_decoded.size(),
382 &m_sasl_client_interact,
383 &out,
384 &outlen);
385 if (result == SASL_INTERACT) {
386 if (!saslInteract(m_sasl_client_interact)) {
387 sasl_dispose(&m_sasl_conn);
388 return false;
389 }
390 }
391 } while (result == SASL_INTERACT);
392
393 qCDebug(KMANAGERSIEVE_LOG) << "sasl_client_step: " << result;
394 if (result != SASL_CONTINUE && result != SASL_OK) {
395 qCDebug(KMANAGERSIEVE_LOG) << "sasl_client_step failed with: " << result << QString::fromUtf8(sasl_errdetail(m_sasl_conn));
396 sasl_dispose(&m_sasl_conn);
397 return false;
398 }
399
400 doSendData('\"' + QByteArray::fromRawData(out, outlen).toBase64() + '\"');
401 return true;
402}
403
404// Called in main thread
405void SessionThread::startSsl()
406{
407 QMetaObject::invokeMethod(this, &SessionThread::doStartSsl, Qt::QueuedConnection);
408}
409
410// Called in secondary thread
411void SessionThread::doStartSsl()
412{
413 Q_ASSERT(QThread::currentThread() == thread());
414
415 qCDebug(KMANAGERSIEVE_LOG) << "SessionThread::doStartSsl()";
416 if (!m_sslCheck) {
417 m_sslCheck = new QTimer(this);
418 m_sslCheck->setInterval(60 * 1000);
419 connect(m_sslCheck, &QTimer::timeout, this, &SessionThread::slotSslTimeout);
420 }
421 m_socket->setProtocol(QSsl::SecureProtocols);
422 m_socket->ignoreSslErrors();
423 connect(m_socket.get(), &QSslSocket::encrypted, this, &SessionThread::slotEncryptedDone);
424 m_sslCheck->start();
425 m_socket->startClientEncryption();
426}
427
428// Called in secondary thread
429void SessionThread::slotSslTimeout()
430{
431 Q_ASSERT(QThread::currentThread() == thread());
432
433 disconnect(m_socket.get(), &QSslSocket::encrypted, this, &SessionThread::slotEncryptedDone);
434 sslResult(false);
435}
436
437// Called in secondary thread
438void SessionThread::slotEncryptedDone()
439{
440 Q_ASSERT(QThread::currentThread() == thread());
441
442 m_sslCheck->stop();
443 sslResult(true);
444}
445
446// Called in secondary thread
447void SessionThread::sslResult(bool encrypted)
448{
449 Q_ASSERT(QThread::currentThread() == thread());
450
451 const QSslCipher cipher = m_socket->sessionCipher();
452 const int numberOfSslError = m_socket->sslHandshakeErrors().count();
453 if (!encrypted || numberOfSslError > 0 || !m_socket->isEncrypted() || cipher.isNull() || cipher.usedBits() == 0) {
454 qCDebug(KMANAGERSIEVE_LOG) << "Initial SSL handshake failed. cipher.isNull() is" << cipher.isNull() << ", cipher.usedBits() is" << cipher.usedBits()
455 << ", the socket says:" << m_socket->errorString() << "and the list of SSL errors contains" << numberOfSslError << "items.";
456
457 Q_EMIT sslError(KSslErrorUiData(m_socket.get()));
458 } else {
459 Q_EMIT sslDone();
460 }
461}
462
463#include "moc_sessionthread_p.cpp"
A response from a managesieve server.
Definition response.h:18
QString i18n(const char *text, const TYPE &arg...)
KIOCORE_EXPORT QString buildErrorString(int errorCode, const QString &errorText)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
void errorOccurred(QAbstractSocket::SocketError socketError)
void chop(qsizetype n)
bool endsWith(QByteArrayView bv) const const
QByteArray fromBase64(const QByteArray &base64, Base64Options options)
QByteArray fromRawData(const char *data, qsizetype size)
bool isEmpty() const const
qsizetype size() const const
QByteArray toBase64(Base64Options options) const const
bool invokeMethod(QObject *context, Functor &&function, FunctorReturnType *ret)
SecureProtocols
QString fromLatin1(QByteArrayView str)
QString fromUtf8(QByteArrayView str)
QueuedConnection
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QThread * currentThread()
void timeout()
QString host(ComponentFormattingOptions options) const const
int port(int defaultPort) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:17:19 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.