• 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.12
  • 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 {
376  // Received empty continuation for authMode other than PLAIN
377  code = MALFORMED;
378  }
379  break;
380 
381  case OK:
382 
383  switch ( d->authState ) {
384  case LoginJobPrivate::StartTls:
385  d->sessionInternal()->startSsl( KTcpSocket::TlsV1 );
386  break;
387 
388  case LoginJobPrivate::Capability:
389  //cleartext login, if enabled
390  if ( d->authMode.isEmpty() ) {
391  if ( d->plainLoginDisabled ) {
392  setError( UserDefinedError );
393  setErrorText( i18n( "Login failed, plain login is disabled by the server." ) );
394  emitResult();
395  } else {
396  d->authState = LoginJobPrivate::Login;
397  d->tags << d->sessionInternal()->sendCommand( "LOGIN",
398  '"' + quoteIMAP( d->userName ).toUtf8() + '"' +
399  ' ' +
400  '"' + quoteIMAP( d->password ).toUtf8() + '"' );
401  }
402  } else {
403  bool authModeSupported = false;
404  //find the selected SASL authentication method
405  Q_FOREACH ( const QString &capability, d->capabilities ) {
406  if ( capability.startsWith( QLatin1String( "AUTH=" ) ) ) {
407  if ( capability.mid( 5 ) == d->authMode ) {
408  authModeSupported = true;
409  break;
410  }
411  }
412  }
413  if ( !authModeSupported ) {
414  setError( UserDefinedError );
415  setErrorText( i18n( "Login failed, authentication mode %1 is not supported by the server.", d->authMode ) );
416  emitResult();
417  } else if ( !d->startAuthentication() ) {
418  emitResult(); //problem, we're done
419  }
420  }
421  break;
422 
423  case LoginJobPrivate::Authenticate:
424  sasl_dispose( &d->conn ); //SASL authentication done
425  // Fall through
426  case LoginJobPrivate::Login:
427  d->saveServerGreeting( response );
428  emitResult(); //got an OK, command done
429  break;
430 
431  }
432 
433  }
434 
435  if ( code == MALFORMED ) {
436  setErrorText( i18n( "%1 failed, malformed reply from the server.", commandName ) );
437  emitResult();
438  }
439 }
440 
441 bool LoginJobPrivate::startAuthentication()
442 {
443  //SASL authentication
444  if ( !initSASL() ) {
445  q->setError( LoginJob::UserDefinedError );
446  q->setErrorText( i18n( "Login failed, client cannot initialize the SASL library." ) );
447  return false;
448  }
449 
450  authState = LoginJobPrivate::Authenticate;
451  const char *out = 0;
452  uint outlen = 0;
453  const char *mechusing = 0;
454 
455  int result = sasl_client_new( "imap", m_session->hostName().toLatin1(), 0, 0, callbacks, 0, &conn );
456  if ( result != SASL_OK ) {
457  kDebug() << "sasl_client_new failed with:" << result;
458  q->setError( LoginJob::UserDefinedError );
459  q->setErrorText( QString::fromUtf8( sasl_errdetail( conn ) ) );
460  return false;
461  }
462 
463  do {
464  result = sasl_client_start( conn, authMode.toLatin1(), &client_interact, capabilities.contains( QLatin1String("SASL-IR") ) ? &out : 0, &outlen, &mechusing );
465 
466  if ( result == SASL_INTERACT ) {
467  if ( !sasl_interact() ) {
468  sasl_dispose( &conn );
469  q->setError( LoginJob::UserDefinedError ); //TODO: check up the actual error
470  return false;
471  }
472  }
473  } while ( result == SASL_INTERACT );
474 
475  if ( result != SASL_CONTINUE && result != SASL_OK ) {
476  kDebug() << "sasl_client_start failed with:" << result;
477  q->setError( LoginJob::UserDefinedError );
478  q->setErrorText( QString::fromUtf8( sasl_errdetail( conn ) ) );
479  sasl_dispose( &conn );
480  return false;
481  }
482 
483  QByteArray tmp = QByteArray::fromRawData( out, outlen );
484  QByteArray challenge = tmp.toBase64();
485 
486  if ( challenge.isEmpty() ) {
487  tags << sessionInternal()->sendCommand( "AUTHENTICATE", authMode.toLatin1() );
488  } else {
489  tags << sessionInternal()->sendCommand( "AUTHENTICATE", authMode.toLatin1() + ' ' + challenge );
490  }
491 
492  return true;
493 }
494 
495 bool LoginJobPrivate::answerChallenge(const QByteArray &data)
496 {
497  QByteArray challenge = data;
498  int result = -1;
499  const char *out = 0;
500  uint outlen = 0;
501  do {
502  result = sasl_client_step( conn, challenge.isEmpty() ? 0 : challenge.data(),
503  challenge.size(),
504  &client_interact,
505  &out, &outlen );
506 
507  if ( result == SASL_INTERACT ) {
508  if ( !sasl_interact() ) {
509  q->setError( LoginJob::UserDefinedError ); //TODO: check up the actual error
510  sasl_dispose( &conn );
511  return false;
512  }
513  }
514  } while ( result == SASL_INTERACT );
515 
516  if ( result != SASL_CONTINUE && result != SASL_OK ) {
517  kDebug() << "sasl_client_step failed with:" << result;
518  q->setError( LoginJob::UserDefinedError ); //TODO: check up the actual error
519  q->setErrorText( QString::fromUtf8( sasl_errdetail( conn ) ) );
520  sasl_dispose( &conn );
521  return false;
522  }
523 
524  QByteArray tmp = QByteArray::fromRawData( out, outlen );
525  challenge = tmp.toBase64();
526 
527  sessionInternal()->sendData( challenge );
528 
529  return true;
530 }
531 
532 void LoginJobPrivate::sslResponse(bool response)
533 {
534  if ( response ) {
535  authState = LoginJobPrivate::Capability;
536  tags << sessionInternal()->sendCommand( "CAPABILITY" );
537  } else {
538  q->setError( LoginJob::UserDefinedError );
539  q->setErrorText( i18n( "Login failed, TLS negotiation failed." ) );
540  encryptionMode = LoginJob::Unencrypted;
541  q->emitResult();
542  }
543 }
544 
545 void LoginJob::setEncryptionMode(EncryptionMode mode)
546 {
547  Q_D( LoginJob );
548  d->encryptionMode = mode;
549 }
550 
551 LoginJob::EncryptionMode LoginJob::encryptionMode()
552 {
553  Q_D( LoginJob );
554  return d->encryptionMode;
555 }
556 
557 void LoginJob::setAuthenticationMode(AuthenticationMode mode)
558 {
559  Q_D( LoginJob );
560  switch ( mode ) {
561  case ClearText: d->authMode = QLatin1String( "");
562  break;
563  case Login: d->authMode = QLatin1String("LOGIN");
564  break;
565  case Plain: d->authMode = QLatin1String("PLAIN");
566  break;
567  case CramMD5: d->authMode = QLatin1String("CRAM-MD5");
568  break;
569  case DigestMD5: d->authMode = QLatin1String("DIGEST-MD5");
570  break;
571  case GSSAPI: d->authMode = QLatin1String("GSSAPI");
572  break;
573  case Anonymous: d->authMode = QLatin1String("ANONYMOUS");
574  break;
575  default:
576  d->authMode = QLatin1String("");
577  }
578 }
579 
580 void LoginJob::connectionLost()
581 {
582  Q_D( LoginJob );
583 
584  //don't emit the result if the connection was lost before getting the tls result, as it can mean
585  //the TLS handshake failed and the socket was reconnected in normal mode
586  if ( d->authState != LoginJobPrivate::StartTls ) {
587  setError( ERR_COULD_NOT_CONNECT );
588  setErrorText( i18n( "Connection to server lost." ) );
589  emitResult();
590  }
591 
592 }
593 
594 void LoginJobPrivate::saveServerGreeting(const Message &response)
595 {
596  // Concatenate the parts of the server response into a string, while dropping the first two parts
597  // (the response tag and the "OK" code), and being careful not to add useless extra whitespace.
598 
599  for ( int i = 2; i < response.content.size(); i++ ) {
600  if ( response.content.at( i ).type() == Message::Part::List ) {
601  serverGreeting += QLatin1Char('(');
602  foreach ( const QByteArray &item, response.content.at( i ).toList() ) {
603  serverGreeting += QLatin1String(item) + QLatin1Char(' ');
604  }
605  serverGreeting.chop( 1 );
606  serverGreeting += QLatin1String(") ");
607  } else {
608  serverGreeting+= QLatin1String(response.content.at( i ).toString()) + QLatin1Char(' ');
609  }
610  }
611  serverGreeting.chop( 1 );
612 }
613 
614 QString LoginJob::serverGreeting() const
615 {
616  Q_D( const LoginJob );
617  return d->serverGreeting;
618 }
619 
620 #include "moc_loginjob.cpp"
rfccodecs.h
This file is part of the IMAP support library and defines the RfcCodecs class.
This file is part of the KDE documentation.
Documentation copyright © 1996-2014 The KDE developers.
Generated on Tue Oct 14 2014 23:00:08 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
  • kldap
  • kmbox
  • kmime
  • kpimidentities
  • kpimtextedit
  • kresources
  • ktnef
  • kxmlrpcclient
  • microblog

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