KIMAP2

loginjob.cpp
1 /*
2  Copyright (c) 2009 Kevin Ottens <[email protected]>
3  Copyright (c) 2009 Andras Mantia <[email protected]>
4  Copyright (c) 2017 Christian Mollekopf <[email protected]>
5 
6  This library is free software; you can redistribute it and/or modify it
7  under the terms of the GNU Library General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or (at your
9  option) any later version.
10 
11  This library is distributed in the hope that it will be useful, but WITHOUT
12  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
14  License for more details.
15 
16  You should have received a copy of the GNU Library General Public License
17  along with this library; see the file COPYING.LIB. If not, write to the
18  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19  02110-1301, USA.
20 */
21 
22 #include "loginjob.h"
23 
24 #include "kimap_debug.h"
25 
26 #include "job_p.h"
27 #include "message_p.h"
28 #include "session_p.h"
29 #include "rfccodecs.h"
30 
31 #include "common.h"
32 
33 extern "C" {
34 #include <sasl/sasl.h>
35 }
36 
37 static const sasl_callback_t callbacks[] = {
38  { SASL_CB_ECHOPROMPT, Q_NULLPTR, nullptr },
39  { SASL_CB_NOECHOPROMPT, Q_NULLPTR, nullptr },
40  { SASL_CB_GETREALM, Q_NULLPTR, nullptr },
41  { SASL_CB_USER, Q_NULLPTR, nullptr },
42  { SASL_CB_AUTHNAME, Q_NULLPTR, nullptr },
43  { SASL_CB_PASS, Q_NULLPTR, nullptr },
44  { SASL_CB_CANON_USER, Q_NULLPTR, nullptr },
45  { SASL_CB_LIST_END, Q_NULLPTR, nullptr }
46 };
47 
48 namespace KIMAP2
49 {
50 class LoginJobPrivate : public JobPrivate
51 {
52 public:
53  enum AuthState {
54  StartTls = 0,
55  Capability,
56  Login,
57  Authenticate
58  };
59 
60  LoginJobPrivate(LoginJob *job, Session *session, const QString &name) : JobPrivate(session, name), q(job)
61  {
62  conn = Q_NULLPTR;
63  client_interact = Q_NULLPTR;
64  }
65  ~LoginJobPrivate() { }
66  bool sasl_interact();
67 
68  bool startAuthentication();
69  void sendPlainLogin();
70  bool answerChallenge(const QByteArray &data);
71  void sslResponse(bool response);
72  void saveServerGreeting(const Message &response);
73  void login();
74  void retrieveCapabilities();
75 
76  LoginJob *q;
77 
78  QString userName;
79  QString authorizationName;
80  QString password;
81  QString serverGreeting;
82 
84  bool startTls = false;
85  QString authMode;
86  AuthState authState = Login;
88  bool plainLoginDisabled = false;
89  bool connectionIsEncrypted = false;
90 
91  sasl_conn_t *conn;
92  sasl_interact_t *client_interact;
93 };
94 }
95 
96 using namespace KIMAP2;
97 
98 bool LoginJobPrivate::sasl_interact()
99 {
100  qCDebug(KIMAP2_LOG) << "sasl_interact";
101  sasl_interact_t *interact = client_interact;
102 
103  //some mechanisms do not require username && pass, so it doesn't need a popup
104  //window for getting this info
105  for (; interact->id != SASL_CB_LIST_END; interact++) {
106  if (interact->id == SASL_CB_AUTHNAME ||
107  interact->id == SASL_CB_PASS) {
108  //TODO: dialog for use name??
109  break;
110  }
111  }
112 
113  interact = client_interact;
114  while (interact->id != SASL_CB_LIST_END) {
115  qCDebug(KIMAP2_LOG) << "SASL_INTERACT id:" << interact->id;
116  switch (interact->id) {
117  case SASL_CB_AUTHNAME:
118  if (!authorizationName.isEmpty()) {
119  qCDebug(KIMAP2_LOG) << "SASL_CB_[AUTHNAME]: '" << authorizationName << "'";
120  interact->result = strdup(authorizationName.toUtf8());
121  interact->len = strlen((const char *) interact->result);
122  break;
123  }
124  case SASL_CB_USER:
125  qCDebug(KIMAP2_LOG) << "SASL_CB_[USER|AUTHNAME]: '" << userName << "'";
126  interact->result = strdup(userName.toUtf8());
127  interact->len = strlen((const char *) interact->result);
128  break;
129  case SASL_CB_PASS:
130  qCDebug(KIMAP2_LOG) << "SASL_CB_PASS: [hidden]";
131  interact->result = strdup(password.toUtf8());
132  interact->len = strlen((const char *) interact->result);
133  break;
134  default:
135  interact->result = Q_NULLPTR;
136  interact->len = 0;
137  break;
138  }
139  interact++;
140  }
141  //FIXME This should return false at least in some cases
142  return true;
143 }
144 
145 LoginJob::LoginJob(Session *session)
146  : Job(*new LoginJobPrivate(this, session, QString::fromUtf8("Login")))
147 {
148  qCDebug(KIMAP2_LOG) << this;
149 }
150 
151 LoginJob::~LoginJob()
152 {
153  qCDebug(KIMAP2_LOG) << this;
154 }
155 
156 QString LoginJob::userName() const
157 {
158  Q_D(const LoginJob);
159  return d->userName;
160 }
161 
162 void LoginJob::setUserName(const QString &userName)
163 {
164  Q_D(LoginJob);
165  d->userName = userName;
166 }
167 
168 QString LoginJob::authorizationName() const
169 {
170  Q_D(const LoginJob);
171  return d->authorizationName;
172 }
173 
174 void LoginJob::setAuthorizationName(const QString &authorizationName)
175 {
176  Q_D(LoginJob);
177  d->authorizationName = authorizationName;
178 }
179 
180 QString LoginJob::password() const
181 {
182  Q_D(const LoginJob);
183  return d->password;
184 }
185 
186 void LoginJob::setPassword(const QString &password)
187 {
188  Q_D(LoginJob);
189  d->password = password;
190 }
191 
192 /*
193  * The IMAP authentication procedure is unfortunately ridiculously complicated due to the many different options:
194  *
195  * An IMAP Session always has the following structure:
196  * * Connection is established.
197  * * Server sends greeting.
198  * * Client authenticates somehow.
199  * * .....
200  *
201  * If the we have a plain connection it's simple:
202  * * Wait for the greeting
203  * * Login using the chosen authentication mechanism
204  *
205  * If we're using TLS (without STARTTLS, so directly):
206  * * Immediately initiate TLS handshake.
207  * * Wait for the greeting
208  * * Get CAPABILITIES to figure out which AUTH mechs are supported
209  * * Login using the chosen authentication mechanism
210  *
211  * If we're using TLS with STARTTLS:
212  * * Wait for the greeting (on the unencrypted connection)
213  * * Send STARTTLS and wait for OK
214  * * Initiate TLS handshake
215  * * Get CAPABILITIES to figure out which AUTH mechs are supported
216  * * Login using the chosen authentication mechanism
217  */
218 void LoginJob::doStart()
219 {
220  Q_D(LoginJob);
221 
222  qCDebug(KIMAP2_LOG) << "doStart" << this;
223 
224  connect(d->sessionInternal(), SIGNAL(encryptionNegotiationResult(bool)), this, SLOT(sslResponse(bool)));
225 
226  if (session()->state() == Session::Disconnected) {
227  auto guard = new QObject(this);
228  QObject::connect(session(), &Session::stateChanged, guard, [d, guard](KIMAP2::Session::State newState, KIMAP2::Session::State) {
229  qCDebug(KIMAP2_LOG) << "Session state changed" << newState;
230  d->login();
231  delete guard;
232  });
233  if (!d->startTls && d->encryptionMode != QSsl::UnknownProtocol) {
234  //We have to encrypt for the greeting
235  d->sessionInternal()->startSsl(d->encryptionMode);
236  }
237  //We wait for the server greeting
238  return;
239  } else {
240  qCInfo(KIMAP2_LOG) << "Session is ready, carring on";
241  //The session is ready, we can carry on.
242  d->login();
243  }
244 
245 }
246 
247 void LoginJobPrivate::login()
248 {
249  // Don't authenticate on a session in the authenticated state
250  if (q->session()->isConnected()) {
251  q->setError(LoginJob::UserDefinedError);
252  q->setErrorText(QString::fromUtf8("IMAP session in the wrong state for authentication"));
253  q->emitResult();
254  return;
255  }
256 
257  if (startTls) {
258  //With STARTTLS we have to try to upgrade our connection before the login
259  qCInfo(KIMAP2_LOG) << "Starting with tls";
260  authState = LoginJobPrivate::StartTls;
261  sendCommand("STARTTLS", {});
262  return;
263  } else {
264  //If this is supposed to be unecrypted or already encrypted we can retrieve capabilties. Otherwise we wait for the sslResponse.
265  if (encryptionMode == QSsl::UnknownProtocol || connectionIsEncrypted) {
266  retrieveCapabilities();
267  } else {
268  qCInfo(KIMAP2_LOG) << "Waiting for encryption before retrieveing capabilities.";
269  }
270  }
271 
272 }
273 
274 void LoginJobPrivate::sslResponse(bool response)
275 {
276  qCDebug(KIMAP2_LOG) << "Got an ssl response " << response;
277  connectionIsEncrypted = response;
278  if (response) {
279  //It's possible that we receive the ssl info before we receive the server greeting.
280  //In that case we're still in the Disconnected state and shouldn't retrieve the capabilities just yet.
281  //We'll try again via login once the state changes.
282  if (m_session->state() != Session::Disconnected) {
283  retrieveCapabilities();
284  }
285  } else {
286  q->setError(LoginFailed);
287  q->setErrorText(QString::fromUtf8("Login failed, TLS negotiation failed."));
288  encryptionMode = QSsl::UnknownProtocol;
289  q->emitResult();
290  }
291 }
292 
293 void LoginJobPrivate::retrieveCapabilities()
294 {
295  qCDebug(KIMAP2_LOG) << "Retrieving capabilities.";
296  authState = LoginJobPrivate::Capability;
297  sendCommand("CAPABILITY", {});
298 }
299 
300 void LoginJob::handleResponse(const Message &response)
301 {
302  Q_D(LoginJob);
303 
304  if (response.content.isEmpty()) {
305  return;
306  }
307 
308  //set the actual command name for standard responses
309  QString commandName = QStringLiteral("Login");
310  if (d->authState == LoginJobPrivate::Capability) {
311  commandName = QStringLiteral("Capability");
312  } else if (d->authState == LoginJobPrivate::StartTls) {
313  commandName = QStringLiteral("StartTls");
314  }
315 
316  enum ResponseCode {
317  OK,
318  ERR,
319  UNTAGGED,
320  CONTINUATION,
321  MALFORMED
322  };
323 
324  QByteArray tag = response.content.first().toString();
325  ResponseCode code = OK;
326 
327  qCDebug(KIMAP2_LOG) << commandName << tag;
328 
329  if (tag == "+") {
330  code = CONTINUATION;
331  } else if (tag == "*") {
332  if (response.content.size() < 2) {
333  code = MALFORMED; // Received empty untagged response
334  } else {
335  code = UNTAGGED;
336  }
337  } else if (d->tags.contains(tag)) {
338  if (response.content.size() < 2) {
339  code = MALFORMED;
340  } else if (response.content[1].toString() == "OK") {
341  code = OK;
342  } else {
343  code = ERR;
344  }
345  }
346 
347  switch (code) {
348  case MALFORMED:
349  // We'll handle it later
350  break;
351 
352  case ERR:
353  //server replied with NO or BAD for SASL authentication
354  if (d->authState == LoginJobPrivate::Authenticate) {
355  sasl_dispose(&d->conn);
356  }
357 
358  setError(LoginFailed);
359  setErrorText(QString("%1 failed, server replied: %2").arg(commandName).arg(QLatin1String(response.toString().constData())));
360  emitResult();
361  return;
362 
363  case UNTAGGED:
364  // The only untagged response interesting for us here is CAPABILITY
365  if (response.content[1].toString() == "CAPABILITY") {
366  QList<Message::Part>::const_iterator p = response.content.begin() + 2;
367  while (p != response.content.end()) {
368  QString capability = QLatin1String(p->toString());
369  d->capabilities << capability;
370  if (capability == QLatin1String("LOGINDISABLED")) {
371  d->plainLoginDisabled = true;
372  }
373  ++p;
374  }
375  qCInfo(KIMAP2_LOG) << "Capabilities updated: " << d->capabilities;
376  }
377  break;
378 
379  case CONTINUATION:
380  if (d->authState != LoginJobPrivate::Authenticate) {
381  // Received unexpected continuation response for something
382  // other than AUTHENTICATE command
383  code = MALFORMED;
384  break;
385  }
386 
387  if (d->authMode == QLatin1String("PLAIN")) {
388  if (response.content.size() > 1 && response.content.at(1).toString() == "OK") {
389  return;
390  }
391  QByteArray challengeResponse;
392  if (!d->authorizationName.isEmpty()) {
393  challengeResponse += d->authorizationName.toUtf8();
394  }
395  challengeResponse += '\0';
396  challengeResponse += d->userName.toUtf8();
397  challengeResponse += '\0';
398  challengeResponse += d->password.toUtf8();
399  challengeResponse = challengeResponse.toBase64();
400  d->sessionInternal()->sendData(challengeResponse);
401  } else if (response.content.size() >= 2) {
402  if (!d->answerChallenge(QByteArray::fromBase64(response.content[1].toString()))) {
403  emitResult(); //error, we're done
404  }
405  } else {
406  // Received empty continuation for authMode other than PLAIN
407  code = MALFORMED;
408  }
409  break;
410 
411  case OK:
412  switch (d->authState) {
413  case LoginJobPrivate::StartTls:
414  //Start encryption and wait for sslResponse
415  d->sessionInternal()->startSsl(d->encryptionMode);
416  break;
417  case LoginJobPrivate::Capability:
418  //cleartext login, if enabled
419  if (d->authMode.isEmpty()) {
420  if (d->plainLoginDisabled) {
421  setError(LoginFailed);
422  setErrorText(QString("Login failed, plain login is disabled by the server."));
423  emitResult();
424  } else {
425  d->sendPlainLogin();
426  }
427  } else {
428  bool authModeSupported = false;
429  //PLAIN is always supported as defined in the standard. We should also get an AUTH= capability, but in case a server doesn't properly announce it we'll just accept it anyways.
430  if (d->authMode == "PLAIN") {
431  authModeSupported = true;
432  }
433  //find the selected SASL authentication method
434  Q_FOREACH (const QString &capability, d->capabilities) {
435  if (capability.startsWith(QLatin1String("AUTH="))) {
436  if (capability.mid(5) == d->authMode) {
437  authModeSupported = true;
438  break;
439  }
440  }
441  }
442  if (!authModeSupported) {
443  setError(LoginFailed);
444  setErrorText(QString("Login failed, authentication mode %1 is not supported by the server.").arg(d->authMode));
445  emitResult();
446  } else if (!d->startAuthentication()) {
447  emitResult(); //problem, we're done
448  }
449  }
450  break;
451 
452  case LoginJobPrivate::Authenticate:
453  sasl_dispose(&d->conn); //SASL authentication done
454  // Fall through
455  case LoginJobPrivate::Login:
456  d->saveServerGreeting(response);
457  emitResult(); //got an OK, command done
458  break;
459 
460  }
461 
462  }
463 
464  if (code == MALFORMED) {
465  setErrorText(QString("%1 failed, malformed reply from the server.").arg(commandName));
466  emitResult();
467  }
468 }
469 
470 bool LoginJobPrivate::startAuthentication()
471 {
472  //SASL authentication
473  if (!initSASL()) {
474  q->setError(LoginFailed);
475  q->setErrorText(QString("Login failed, client cannot initialize the SASL library."));
476  return false;
477  }
478 
479  authState = LoginJobPrivate::Authenticate;
480  const char *out = Q_NULLPTR;
481  uint outlen = 0;
482  const char *mechusing = Q_NULLPTR;
483 
484  int result = sasl_client_new("imap", m_session->hostName().toLatin1(), Q_NULLPTR, nullptr, callbacks, 0, &conn);
485  if (result != SASL_OK) {
486  qCWarning(KIMAP2_LOG) << "sasl_client_new failed with:" << result;
487  q->setError(LoginFailed);
488  q->setErrorText(QString::fromUtf8(sasl_errdetail(conn)));
489  return false;
490  }
491 
492  do {
493  result = sasl_client_start(conn, authMode.toLatin1(), &client_interact, capabilities.contains(QStringLiteral("SASL-IR")) ? &out : Q_NULLPTR, &outlen, &mechusing);
494 
495  if (result == SASL_INTERACT) {
496  if (!sasl_interact()) {
497  sasl_dispose(&conn);
498  q->setError(LoginFailed); //TODO: check up the actual error
499  q->setErrorText(QString("sasl_interact failed"));
500  return false;
501  }
502  }
503  } while (result == SASL_INTERACT);
504 
505  if (result != SASL_CONTINUE && result != SASL_OK) {
506  qCWarning(KIMAP2_LOG) << "sasl_client_start failed with:" << result;
507  q->setError(LoginFailed);
508  q->setErrorText(QString::fromUtf8(sasl_errdetail(conn)));
509  sasl_dispose(&conn);
510  return false;
511  }
512 
513  QByteArray tmp = QByteArray::fromRawData(out, outlen);
514  QByteArray challenge = tmp.toBase64();
515 
516  if (challenge.isEmpty()) {
517  sendCommand("AUTHENTICATE", authMode.toLatin1());
518  } else {
519  sendCommand("AUTHENTICATE", authMode.toLatin1() + ' ' + challenge);
520  }
521 
522  return true;
523 }
524 
525 void LoginJobPrivate::sendPlainLogin()
526 {
527  authState = LoginJobPrivate::Login;
528  qCDebug(KIMAP2_LOG) << "sending LOGIN";
529  sendCommand("LOGIN",
530  '"' + quoteIMAP(userName).toUtf8() + '"' +
531  ' ' +
532  '"' + quoteIMAP(password).toUtf8() + '"');
533 }
534 
535 bool LoginJobPrivate::answerChallenge(const QByteArray &data)
536 {
537  QByteArray challenge = data;
538  int result = -1;
539  const char *out = Q_NULLPTR;
540  uint outlen = 0;
541  do {
542  result = sasl_client_step(conn, challenge.isEmpty() ? Q_NULLPTR : challenge.data(),
543  challenge.size(),
544  &client_interact,
545  &out, &outlen);
546 
547  if (result == SASL_INTERACT) {
548  if (!sasl_interact()) {
549  q->setError(LoginFailed); //TODO: check up the actual error
550  q->setErrorText(QString("sasl_interact failed"));
551  sasl_dispose(&conn);
552  return false;
553  }
554  }
555  } while (result == SASL_INTERACT);
556 
557  if (result != SASL_CONTINUE && result != SASL_OK) {
558  qCWarning(KIMAP2_LOG) << "sasl_client_step failed with:" << result;
559  q->setError(LoginFailed); //TODO: check up the actual error
560  q->setErrorText(QString::fromUtf8(sasl_errdetail(conn)));
561  sasl_dispose(&conn);
562  return false;
563  }
564 
565  QByteArray tmp = QByteArray::fromRawData(out, outlen);
566  challenge = tmp.toBase64();
567 
568  sessionInternal()->sendData(challenge);
569 
570  return true;
571 }
572 
573 void LoginJob::setEncryptionMode(QSsl::SslProtocol mode, bool startTls)
574 {
575  Q_D(LoginJob);
576  d->encryptionMode = mode;
577  d->startTls = startTls;
578 }
579 
580 QSsl::SslProtocol LoginJob::encryptionMode()
581 {
582  Q_D(LoginJob);
583  return d->encryptionMode;
584 }
585 
586 void LoginJob::setAuthenticationMode(AuthenticationMode mode)
587 {
588  Q_D(LoginJob);
589  switch (mode) {
590  case ClearText: d->authMode = QLatin1String("");
591  break;
592  case Login: d->authMode = QStringLiteral("LOGIN");
593  break;
594  case Plain: d->authMode = QStringLiteral("PLAIN");
595  break;
596  case CramMD5: d->authMode = QStringLiteral("CRAM-MD5");
597  break;
598  case DigestMD5: d->authMode = QStringLiteral("DIGEST-MD5");
599  break;
600  case GSSAPI: d->authMode = QStringLiteral("GSSAPI");
601  break;
602  case Anonymous: d->authMode = QStringLiteral("ANONYMOUS");
603  break;
604  case XOAuth2: d->authMode = QStringLiteral("XOAUTH2");
605  break;
606  default:
607  d->authMode = QStringLiteral("");
608  }
609 }
610 
611 void LoginJob::connectionLost()
612 {
613  Q_D(LoginJob);
614 
615  qCWarning(KIMAP2_LOG) << "Connection to server lost " << d->m_socketError;
616  if (d->m_socketError == QSslSocket::SslHandshakeFailedError) {
617  setError(SslHandshakeFailed);
618  setErrorText(QString::fromUtf8("SSL handshake failed."));
619  emitResult();
620  } else if (d->m_socketError == QSslSocket::HostNotFoundError) {
621  setError(HostNotFound);
622  setErrorText(QString::fromUtf8("Host not found."));
623  emitResult();
624  } else {
625  setError(CouldNotConnect);
626  setErrorText(QString::fromUtf8("Connection to server lost."));
627  emitResult();
628  }
629 }
630 
631 void LoginJobPrivate::saveServerGreeting(const Message &response)
632 {
633  // Concatenate the parts of the server response into a string, while dropping the first two parts
634  // (the response tag and the "OK" code), and being careful not to add useless extra whitespace.
635 
636  for (int i = 2; i < response.content.size(); i++) {
637  if (response.content.at(i).type() == Message::Part::List) {
638  serverGreeting += QLatin1Char('(');
639  foreach (const QByteArray &item, response.content.at(i).toList()) {
640  serverGreeting += QLatin1String(item) + QLatin1Char(' ');
641  }
642  serverGreeting.chop(1);
643  serverGreeting += QStringLiteral(") ");
644  } else {
645  serverGreeting += QLatin1String(response.content.at(i).toString()) + QLatin1Char(' ');
646  }
647  }
648  serverGreeting.chop(1);
649 }
650 
651 QString LoginJob::serverGreeting() const
652 {
653  Q_D(const LoginJob);
654  return d->serverGreeting;
655 }
656 
657 #include "moc_loginjob.cpp"
QString fromUtf8(const char *str, int size)
QByteArray fromRawData(const char *data, int size)
KIMAP_EXPORT QByteArray quoteIMAP(const QByteArray &src)
Replaces " with \" and \ with \ " and \ characters.
Definition: rfccodecs.cpp:161
QByteArray toBase64(QByteArray::Base64Options options) const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
char at(int i) const const
QByteArray fromBase64(const QByteArray &base64, QByteArray::Base64Options options)
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
Capabilities capabilities()
QString name(StandardShortcut id)
QList::iterator begin()
int size() const const
KLEO_EXPORT std::unique_ptr< GpgME::DefaultAssuanTransaction > sendCommand(std::shared_ptr< GpgME::Context > &assuanContext, const std::string &command, GpgME::Error &err)
Provides handlers for various RFC/MIME encodings.
QString mid(int position, int n) const const
Q_D(Todo)
char * data()
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sun Feb 5 2023 04:11:00 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.