Libksieve

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

KDE's Doxygen guidelines are available online.