• Skip to content
  • Skip to link menu
KDE API Reference
  • KDE API Reference
  • kdepimlibs API Reference
  • KDE Home
  • Contact Us
 

KIMAP Library

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

KDE's Doxygen guidelines are available online.

KIMAP Library

Skip menu "KIMAP Library"
  • Main Page
  • Namespace List
  • Namespace Members
  • Alphabetical List
  • Class List
  • Class Hierarchy
  • Class Members
  • File List
  • Related Pages

kdepimlibs API Reference

Skip menu "kdepimlibs API Reference"
  • akonadi
  •   contact
  •   kmime
  •   socialutils
  • kabc
  • kalarmcal
  • kblog
  • kcal
  • kcalcore
  • kcalutils
  • kholidays
  • kimap
  • kioslave
  •   imap4
  •   mbox
  •   nntp
  • kldap
  • kmbox
  • kmime
  • kontactinterface
  • kpimidentities
  • kpimtextedit
  • kpimutils
  • kresources
  • ktnef
  • kxmlrpcclient
  • mailtransport
  • microblog
  • qgpgme
  • syndication
  •   atom
  •   rdf
  •   rss2

Search



Report problems with this website to our bug tracking system.
Contact the specific authors with questions and comments about the page contents.

KDE® and the K Desktop Environment® logo are registered trademarks of KDE e.V. | Legal