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

KIMAP Library

loginjob.cpp
00001 /*
00002     Copyright (c) 2009 Kevin Ottens <ervin@kde.org>
00003     Copyright (c) 2009 Andras Mantia <amantia@kde.org>
00004 
00005 
00006     This library is free software; you can redistribute it and/or modify it
00007     under the terms of the GNU Library General Public License as published by
00008     the Free Software Foundation; either version 2 of the License, or (at your
00009     option) any later version.
00010 
00011     This library is distributed in the hope that it will be useful, but WITHOUT
00012     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
00013     FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
00014     License for more details.
00015 
00016     You should have received a copy of the GNU Library General Public License
00017     along with this library; see the file COPYING.LIB.  If not, write to the
00018     Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
00019     02110-1301, USA.
00020 */
00021 
00022 #include "loginjob.h"
00023 
00024 #include <KDE/KLocale>
00025 #include <KDE/KDebug>
00026 #include <ktcpsocket.h>
00027 
00028 #include "job_p.h"
00029 #include "message_p.h"
00030 #include "session_p.h"
00031 #include "rfccodecs.h"
00032 
00033 #include "common.h"
00034 
00035 extern "C" {
00036 #include <sasl/sasl.h>
00037 }
00038 
00039 static sasl_callback_t callbacks[] = {
00040     { SASL_CB_ECHOPROMPT, NULL, NULL },
00041     { SASL_CB_NOECHOPROMPT, NULL, NULL },
00042     { SASL_CB_GETREALM, NULL, NULL },
00043     { SASL_CB_USER, NULL, NULL },
00044     { SASL_CB_AUTHNAME, NULL, NULL },
00045     { SASL_CB_PASS, NULL, NULL },
00046     { SASL_CB_CANON_USER, NULL, NULL },
00047     { SASL_CB_LIST_END, NULL, NULL }
00048 };
00049 
00050 namespace KIMAP
00051 {
00052   class LoginJobPrivate : public JobPrivate
00053   {
00054     public:
00055      enum AuthState {
00056         StartTls = 0,
00057         Capability,
00058         Login,
00059         Authenticate
00060       };
00061 
00062       LoginJobPrivate( LoginJob *job, Session *session, const QString& name ) : JobPrivate(session, name), q(job), encryptionMode(LoginJob::Unencrypted),  authState(Login), plainLoginDisabled(false) {
00063         conn = 0;
00064         client_interact = 0;
00065       }
00066       ~LoginJobPrivate() { }
00067       bool sasl_interact();
00068 
00069       bool startAuthentication();
00070       bool answerChallenge(const QByteArray &data);
00071       void sslResponse(bool response);
00072       void saveServerGreeting(const Message &response);
00073 
00074       LoginJob *q;
00075 
00076       QString userName;
00077       QString password;
00078       QString serverGreeting;
00079 
00080       LoginJob::EncryptionMode encryptionMode;
00081       QString authMode;
00082       AuthState authState;
00083       QStringList capabilities;
00084       bool plainLoginDisabled;
00085 
00086       sasl_conn_t *conn;
00087       sasl_interact_t *client_interact;
00088   };
00089 }
00090 
00091 using namespace KIMAP;
00092 
00093 bool LoginJobPrivate::sasl_interact()
00094 {
00095   kDebug() <<"sasl_interact";
00096   sasl_interact_t *interact = client_interact;
00097 
00098   //some mechanisms do not require username && pass, so it doesn't need a popup
00099   //window for getting this info
00100   for ( ; interact->id != SASL_CB_LIST_END; interact++ ) {
00101     if ( interact->id == SASL_CB_AUTHNAME ||
00102          interact->id == SASL_CB_PASS ) {
00103       //TODO: dialog for use name??
00104       break;
00105     }
00106   }
00107 
00108   interact = client_interact;
00109   while( interact->id != SASL_CB_LIST_END ) {
00110     kDebug() <<"SASL_INTERACT id:" << interact->id;
00111     switch( interact->id ) {
00112       case SASL_CB_USER:
00113       case SASL_CB_AUTHNAME:
00114         kDebug() <<"SASL_CB_[USER|AUTHNAME]: '" << userName <<"'";
00115         interact->result = strdup( userName.toUtf8() );
00116         interact->len = strlen( (const char *) interact->result );
00117         break;
00118       case SASL_CB_PASS:
00119         kDebug() <<"SASL_CB_PASS: [hidden]";
00120         interact->result = strdup( password.toUtf8() );
00121         interact->len = strlen( (const char *) interact->result );
00122         break;
00123       default:
00124         interact->result = 0;
00125         interact->len = 0;
00126         break;
00127     }
00128     interact++;
00129   }
00130   return true;
00131 }
00132 
00133 
00134 LoginJob::LoginJob( Session *session )
00135   : Job( *new LoginJobPrivate(this, session, i18n("Login")) )
00136 {
00137   Q_D(LoginJob);
00138   connect(d->sessionInternal(), SIGNAL(encryptionNegotiationResult(bool)), this, SLOT(sslResponse(bool)));
00139 }
00140 
00141 LoginJob::~LoginJob()
00142 {
00143 }
00144 
00145 QString LoginJob::userName() const
00146 {
00147   Q_D(const LoginJob);
00148   return d->userName;
00149 }
00150 
00151 void LoginJob::setUserName( const QString &userName )
00152 {
00153   Q_D(LoginJob);
00154   d->userName = userName;
00155 }
00156 
00157 QString LoginJob::password() const
00158 {
00159   Q_D(const LoginJob);
00160   return d->password;
00161 }
00162 
00163 void LoginJob::setPassword( const QString &password )
00164 {
00165   Q_D(LoginJob);
00166   d->password = password;
00167 }
00168 
00169 void LoginJob::doStart()
00170 {
00171   Q_D(LoginJob);
00172 
00173   // Don't authenticate on a session in the authenticated state
00174   if ( session()->state() == Session::Authenticated || session()->state() == Session::Selected ) {
00175     setError( UserDefinedError );
00176     setErrorText( i18n("IMAP session in the wrong state for authentication") );
00177     emitResult();
00178     return;
00179   }
00180 
00181   // Trigger encryption negotiation only if needed
00182   EncryptionMode encryptionMode = d->encryptionMode;
00183 
00184   switch ( d->sessionInternal()->negotiatedEncryption() ) {
00185   case KTcpSocket::UnknownSslVersion:
00186     break; // Do nothing the encryption mode still needs to be negotiated
00187 
00188   // For the other cases, pretend we're going unencrypted as that's the
00189   // encryption mode already set on the session
00190   // (so for instance we won't issue another STARTTLS for nothing if that's
00191   // not needed)
00192   case KTcpSocket::SslV2:
00193     if ( encryptionMode==SslV2 ) {
00194       encryptionMode = Unencrypted;
00195     }
00196     break;
00197   case KTcpSocket::SslV3:
00198     if ( encryptionMode==SslV3 ) {
00199       encryptionMode = Unencrypted;
00200     }
00201     break;
00202   case KTcpSocket::TlsV1:
00203     if ( encryptionMode==TlsV1 ) {
00204       encryptionMode = Unencrypted;
00205     }
00206     break;
00207   case KTcpSocket::AnySslVersion:
00208     if ( encryptionMode==AnySslVersion ) {
00209       encryptionMode = Unencrypted;
00210     }
00211     break;
00212   }
00213 
00214   if (encryptionMode == SslV2
00215    || encryptionMode == SslV3
00216    || encryptionMode == SslV3_1
00217    || encryptionMode == AnySslVersion) {
00218     KTcpSocket::SslVersion version = KTcpSocket::SslV2;
00219     if (encryptionMode == SslV3)
00220       version = KTcpSocket::SslV3;
00221     if (encryptionMode == SslV3_1)
00222       version = KTcpSocket::SslV3_1;
00223     if (encryptionMode == AnySslVersion)
00224       version = KTcpSocket::AnySslVersion;
00225     d->sessionInternal()->startSsl(version);
00226 
00227   } else if (encryptionMode == TlsV1) {
00228     d->authState = LoginJobPrivate::StartTls;
00229     d->tags << d->sessionInternal()->sendCommand( "STARTTLS" );
00230 
00231   } else  if (encryptionMode == Unencrypted ) {
00232     if (d->authMode.isEmpty()) {
00233       d->authState = LoginJobPrivate::Login;
00234       d->tags << d->sessionInternal()->sendCommand( "LOGIN",
00235                                                   '"'+quoteIMAP( d->userName ).toUtf8()+'"'
00236                                                  +' '
00237                                                  +'"'+quoteIMAP(d->password ).toUtf8()+'"' );
00238     } else {
00239       if (!d->startAuthentication()) {
00240         emitResult();
00241       }
00242     }
00243   }
00244 }
00245 
00246 void LoginJob::handleResponse( const Message &response )
00247 {
00248   Q_D(LoginJob);
00249 
00250   if ( response.content.isEmpty() )
00251     return;
00252 
00253   //set the actual command name for standard responses
00254   QString commandName = i18n("Login");
00255   if (d->authState == LoginJobPrivate::Capability) {
00256     commandName = i18n("Capability");
00257   } else if (d->authState == LoginJobPrivate::StartTls) {
00258     commandName = i18n("StartTls");
00259   }
00260 
00261   enum ResponseCode {
00262     OK,
00263     ERR,
00264     UNTAGGED,
00265     CONTINUATION,
00266     MALFORMED
00267   };
00268 
00269   QByteArray tag = response.content.first().toString();
00270   ResponseCode code;
00271 
00272   if ( tag == "+" ) {
00273     code = CONTINUATION;
00274   } else if ( tag == "*" ) {
00275     if ( response.content.size() < 2 )
00276       code = MALFORMED; // Received empty untagged response
00277     else
00278       code = UNTAGGED;
00279   } else if ( d->tags.contains(tag) ) {
00280     if ( response.content.size() < 2 )
00281       code = MALFORMED;
00282     else if ( response.content[1].toString() == "OK" )
00283       code = OK;
00284     else
00285       code = ERR;
00286   }
00287 
00288   switch (code) {
00289     case MALFORMED:
00290       // We'll handle it later
00291       break;
00292 
00293     case ERR:
00294       //server replied with NO or BAD for SASL authentication
00295       if (d->authState == LoginJobPrivate::Authenticate)
00296         sasl_dispose( &d->conn );
00297 
00298       setError( UserDefinedError );
00299       setErrorText( i18n("%1 failed, server replied: %2", commandName, response.toString().constData()) );
00300       emitResult();
00301       return;
00302 
00303     case UNTAGGED:
00304       // The only untagged response interesting for us here is CAPABILITY
00305       if ( response.content[1].toString() == "CAPABILITY" ) {
00306         QList<Message::Part>::const_iterator p = response.content.begin() + 2;
00307         while (p != response.content.end()) {
00308           QString capability = p->toString();
00309           d->capabilities << capability;
00310           if (capability == "LOGINDISABLED")
00311             d->plainLoginDisabled = true;
00312           ++p;
00313         }
00314         kDebug() << "Capabilities updated: " << d->capabilities;
00315       }
00316       break;
00317 
00318     case CONTINUATION:
00319       if (d->authState != LoginJobPrivate::Authenticate) {
00320         // Received unexpected continuation response for something
00321         // other than AUTHENTICATE command
00322         code = MALFORMED;
00323         break;
00324       }
00325 
00326       if ( d->authMode == QLatin1String( "PLAIN" ) ) {
00327         if ( response.content.size()>1 && response.content.at( 1 ).toString()=="OK" ) {
00328           return;
00329         }
00330         QByteArray challengeResponse;
00331         challengeResponse+= '\0';
00332         challengeResponse+= d->userName.toUtf8();
00333         challengeResponse+= '\0';
00334         challengeResponse+= d->password.toUtf8();
00335         challengeResponse = challengeResponse.toBase64();
00336         d->sessionInternal()->sendData( challengeResponse );
00337       } else if ( response.content.size() >= 2 ) {
00338         if (!d->answerChallenge(QByteArray::fromBase64(response.content[1].toString()))) {
00339           emitResult(); //error, we're done
00340         }
00341       } else {
00342         // Received empty continuation for authMode other than PLAIN
00343         code = MALFORMED;
00344       }
00345       break;
00346 
00347     case OK:
00348 
00349       switch (d->authState) {
00350         case LoginJobPrivate::StartTls:
00351           d->sessionInternal()->startSsl(KTcpSocket::TlsV1);
00352           break;
00353 
00354         case LoginJobPrivate::Capability:
00355           //cleartext login, if enabled
00356           if (d->authMode.isEmpty()) {
00357             if (d->plainLoginDisabled) {
00358               setError( UserDefinedError );
00359               setErrorText( i18n("Login failed, plain login is disabled by the server.") );
00360               emitResult();
00361             } else {
00362               d->authState = LoginJobPrivate::Login;
00363               d->tags << d->sessionInternal()->sendCommand( "LOGIN",
00364                                                           '"'+quoteIMAP( d->userName ).toUtf8()+'"'
00365                                                          +' '
00366                                                          +'"'+quoteIMAP( d->password ).toUtf8()+'"');
00367             }
00368           } else {
00369             bool authModeSupported = false;
00370             //find the selected SASL authentication method
00371             Q_FOREACH(const QString &capability, d->capabilities) {
00372               if (capability.startsWith(QLatin1String("AUTH="))) {
00373                 if (capability.mid(5) == d->authMode) {
00374                   authModeSupported = true;
00375                   break;
00376                 }
00377               }
00378             }
00379             if (!authModeSupported) {
00380               setError( UserDefinedError );
00381               setErrorText( i18n("Login failed, authentication mode %1 is not supported by the server.", d->authMode) );
00382               emitResult();
00383             } else if (!d->startAuthentication()) {
00384               emitResult(); //problem, we're done
00385             }
00386           }
00387           break;
00388 
00389         case LoginJobPrivate::Authenticate:
00390           sasl_dispose( &d->conn ); //SASL authentication done
00391           // Fall through
00392         case LoginJobPrivate::Login:
00393           d->saveServerGreeting( response );
00394           emitResult(); //got an OK, command done
00395           break;
00396 
00397       }
00398 
00399   }
00400 
00401   if ( code == MALFORMED ) {
00402     setErrorText( i18n("%1 failed, malformed reply from the server.", commandName) );
00403     emitResult();
00404   }
00405 }
00406 
00407 bool LoginJobPrivate::startAuthentication()
00408 {
00409   //SASL authentication
00410   if (!initSASL()) {
00411     q->setError( LoginJob::UserDefinedError );
00412     q->setErrorText( i18n("Login failed, client cannot initialize the SASL library.") );
00413     return false;
00414   }
00415 
00416   authState = LoginJobPrivate::Authenticate;
00417   const char *out = 0;
00418   uint outlen = 0;
00419   const char *mechusing = 0;
00420 
00421   int result = sasl_client_new( "imap", m_session->hostName().toLatin1(), 0, 0, callbacks, 0, &conn );
00422   if ( result != SASL_OK ) {
00423     kDebug() <<"sasl_client_new failed with:" << result;
00424     q->setError( LoginJob::UserDefinedError );
00425     q->setErrorText( QString::fromUtf8( sasl_errdetail( conn ) ) );
00426     return false;
00427   }
00428 
00429   do {
00430     result = sasl_client_start(conn, authMode.toLatin1(), &client_interact, capabilities.contains("SASL-IR") ? &out : 0, &outlen, &mechusing);
00431 
00432     if ( result == SASL_INTERACT ) {
00433       if ( !sasl_interact() ) {
00434         sasl_dispose( &conn );
00435         q->setError( LoginJob::UserDefinedError ); //TODO: check up the actual error
00436         return false;
00437       }
00438     }
00439   } while ( result == SASL_INTERACT );
00440 
00441   if ( result != SASL_CONTINUE && result != SASL_OK ) {
00442     kDebug() <<"sasl_client_start failed with:" << result;
00443     q->setError( LoginJob::UserDefinedError );
00444     q->setErrorText( QString::fromUtf8( sasl_errdetail( conn ) ) );
00445     sasl_dispose( &conn );
00446     return false;
00447   }
00448 
00449   QByteArray tmp = QByteArray::fromRawData( out, outlen );
00450   QByteArray challenge = tmp.toBase64();
00451 
00452   if ( challenge.isEmpty() ) {
00453     tags << sessionInternal()->sendCommand( "AUTHENTICATE", authMode.toLatin1() );
00454   } else {
00455     tags << sessionInternal()->sendCommand( "AUTHENTICATE", authMode.toLatin1() + ' ' + challenge );
00456   }
00457 
00458   return true;
00459 }
00460 
00461 bool LoginJobPrivate::answerChallenge(const QByteArray &data)
00462 {
00463   QByteArray challenge = data;
00464   int result = -1;
00465   const char *out = 0;
00466   uint outlen = 0;
00467   do {
00468     result = sasl_client_step(conn, challenge.isEmpty() ? 0 : challenge.data(),
00469                               challenge.size(),
00470                               &client_interact,
00471                               &out, &outlen);
00472 
00473     if (result == SASL_INTERACT) {
00474       if ( !sasl_interact() ) {
00475         q->setError( LoginJob::UserDefinedError ); //TODO: check up the actual error
00476         sasl_dispose( &conn );
00477         return false;
00478       }
00479     }
00480   } while ( result == SASL_INTERACT );
00481 
00482   if ( result != SASL_CONTINUE && result != SASL_OK ) {
00483     kDebug() <<"sasl_client_step failed with:" << result;
00484     q->setError( LoginJob::UserDefinedError ); //TODO: check up the actual error
00485     q->setErrorText( QString::fromUtf8( sasl_errdetail( conn ) ) );
00486     sasl_dispose( &conn );
00487     return false;
00488   }
00489 
00490   QByteArray tmp = QByteArray::fromRawData( out, outlen );
00491   challenge = tmp.toBase64();
00492 
00493   sessionInternal()->sendData( challenge );
00494 
00495   return true;
00496 }
00497 
00498 void LoginJobPrivate::sslResponse(bool response)
00499 {
00500   if (response) {
00501     authState = LoginJobPrivate::Capability;
00502     tags << sessionInternal()->sendCommand( "CAPABILITY" );
00503   } else {
00504     q->setError( LoginJob::UserDefinedError );
00505     q->setErrorText( i18n("Login failed, TLS negotiation failed." ));
00506     encryptionMode = LoginJob::Unencrypted;
00507     q->emitResult();
00508   }
00509 }
00510 
00511 void LoginJob::setEncryptionMode(EncryptionMode mode)
00512 {
00513   Q_D(LoginJob);
00514   d->encryptionMode = mode;
00515 }
00516 
00517 LoginJob::EncryptionMode LoginJob::encryptionMode()
00518 {
00519   Q_D(LoginJob);
00520   return d->encryptionMode;
00521 }
00522 
00523 void LoginJob::setAuthenticationMode(AuthenticationMode mode)
00524 {
00525   Q_D(LoginJob);
00526   switch (mode)
00527   {
00528     case ClearText: d->authMode = "";
00529       break;
00530     case Login: d->authMode = "LOGIN";
00531       break;
00532     case Plain: d->authMode = "PLAIN";
00533       break;
00534     case CramMD5: d->authMode = "CRAM-MD5";
00535       break;
00536     case DigestMD5: d->authMode = "DIGEST-MD5";
00537       break;
00538     case GSSAPI: d->authMode = "GSSAPI";
00539       break;
00540     case Anonymous: d->authMode = "ANONYMOUS";
00541       break;
00542     default:
00543       d->authMode = "";
00544   }
00545 }
00546 
00547 void LoginJob::connectionLost()
00548 {
00549   Q_D(LoginJob);
00550 
00551   //don't emit the result if the connection was lost before getting the tls result, as it can mean
00552   //the TLS handshake failed and the socket was reconnected in normal mode
00553   if (d->authState != LoginJobPrivate::StartTls) {
00554     setError( ERR_COULD_NOT_CONNECT );
00555     setErrorText( i18n("Connection to server lost.") );
00556     emitResult();
00557   }
00558 
00559 }
00560 
00561 void LoginJobPrivate::saveServerGreeting(const Message &response)
00562 {
00563   // Concatenate the parts of the server response into a string, while dropping the first two parts
00564   // (the response tag and the "OK" code), and being careful not to add useless extra whitespace.
00565 
00566   for ( int i=2; i<response.content.size(); i++) {
00567     if ( response.content.at(i).type()==Message::Part::List ) {
00568       serverGreeting+='(';
00569       foreach ( const QByteArray &item, response.content.at(i).toList() ) {
00570         serverGreeting+=item+' ';
00571       }
00572       serverGreeting.chop(1);
00573       serverGreeting+=") ";
00574     } else {
00575       serverGreeting+=response.content.at(i).toString()+' ';
00576     }
00577   }
00578   serverGreeting.chop(1);
00579 }
00580 
00581 QString LoginJob::serverGreeting() const
00582 {
00583   Q_D(const LoginJob);
00584   return d->serverGreeting;
00585 }
00586 
00587 #include "loginjob.moc"
This file is part of the KDE documentation.
Documentation copyright © 1996-2012 The KDE developers.
Generated on Mon Aug 27 2012 22:08:13 by doxygen 1.7.5 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.8.5 API Reference

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