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

Skip menu "kdepimlibs-4.9.1 API Reference"
  • akonadi
  •   contact
  •   kmime
  • kabc
  • kalarmcal
  • kblog
  • kcal
  • kcalcore
  • kcalutils
  • kholidays
  • kimap
  • kioslave
  •   imap4
  •   mbox
  •   nntp
  • kldap
  • kmbox
  • kmime
  • kontactinterface
  • kpimidentities
  • kpimtextedit
  •   richtextbuilders
  • 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