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

KIMAP Library

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

Skip menu "kdepimlibs-4.10.5 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
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