Libksieve

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

KDE's Doxygen guidelines are available online.