• Skip to content
  • Skip to link menu
KDE 4.6 API Reference
  • KDE API Reference
  • KDE-PIM Libraries
  • 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 
00073       LoginJob *q;
00074 
00075       QString userName;
00076       QString password;
00077 
00078       LoginJob::EncryptionMode encryptionMode;
00079       QString authMode;
00080       AuthState authState;
00081       QStringList capabilities;
00082       bool plainLoginDisabled;
00083 
00084       sasl_conn_t *conn;
00085       sasl_interact_t *client_interact;
00086   };
00087 }
00088 
00089 using namespace KIMAP;
00090 
00091 bool LoginJobPrivate::sasl_interact()
00092 {
00093   kDebug() <<"sasl_interact";
00094   sasl_interact_t *interact = client_interact;
00095 
00096   //some mechanisms do not require username && pass, so it doesn't need a popup
00097   //window for getting this info
00098   for ( ; interact->id != SASL_CB_LIST_END; interact++ ) {
00099     if ( interact->id == SASL_CB_AUTHNAME ||
00100          interact->id == SASL_CB_PASS ) {
00101       //TODO: dialog for use name??
00102       break;
00103     }
00104   }
00105 
00106   interact = client_interact;
00107   while( interact->id != SASL_CB_LIST_END ) {
00108     kDebug() <<"SASL_INTERACT id:" << interact->id;
00109     switch( interact->id ) {
00110       case SASL_CB_USER:
00111       case SASL_CB_AUTHNAME:
00112         kDebug() <<"SASL_CB_[USER|AUTHNAME]: '" << userName <<"'";
00113         interact->result = strdup( userName.toUtf8() );
00114         interact->len = strlen( (const char *) interact->result );
00115         break;
00116       case SASL_CB_PASS:
00117         kDebug() <<"SASL_CB_PASS: [hidden]";
00118         interact->result = strdup( password.toUtf8() );
00119         interact->len = strlen( (const char *) interact->result );
00120         break;
00121       default:
00122         interact->result = 0;
00123         interact->len = 0;
00124         break;
00125     }
00126     interact++;
00127   }
00128   return true;
00129 }
00130 
00131 
00132 LoginJob::LoginJob( Session *session )
00133   : Job( *new LoginJobPrivate(this, session, i18n("Login")) )
00134 {
00135   Q_D(LoginJob);
00136   connect(d->sessionInternal(), SIGNAL(encryptionNegotiationResult(bool)), this, SLOT(sslResponse(bool)));
00137 }
00138 
00139 LoginJob::~LoginJob()
00140 {
00141 }
00142 
00143 QString LoginJob::userName() const
00144 {
00145   Q_D(const LoginJob);
00146   return d->userName;
00147 }
00148 
00149 void LoginJob::setUserName( const QString &userName )
00150 {
00151   Q_D(LoginJob);
00152   d->userName = userName;
00153 }
00154 
00155 QString LoginJob::password() const
00156 {
00157   Q_D(const LoginJob);
00158   return d->password;
00159 }
00160 
00161 void LoginJob::setPassword( const QString &password )
00162 {
00163   Q_D(LoginJob);
00164   d->password = password;
00165 }
00166 
00167 void LoginJob::doStart()
00168 {
00169   Q_D(LoginJob);
00170 
00171   // Don't authenticate on a session in the authenticated state
00172   if ( session()->state() == Session::Authenticated || session()->state() == Session::Selected ) {
00173     setError( UserDefinedError );
00174     setErrorText( i18n("IMAP session in the wrong state for authentication") );
00175     emitResult();
00176     return;
00177   }
00178 
00179   // Trigger encryption negotiation only if needed
00180   EncryptionMode encryptionMode = d->encryptionMode;
00181 
00182   switch ( d->sessionInternal()->negotiatedEncryption() ) {
00183   case KTcpSocket::UnknownSslVersion:
00184     break; // Do nothing the encryption mode still needs to be negotiated
00185 
00186   // For the other cases, pretend we're going unencrypted as that's the
00187   // encryption mode already set on the session
00188   // (so for instance we won't issue another STARTTLS for nothing if that's
00189   // not needed)
00190   case KTcpSocket::SslV2:
00191     if ( encryptionMode==SslV2 ) {
00192       encryptionMode = Unencrypted;
00193     }
00194     break;
00195   case KTcpSocket::SslV3:
00196     if ( encryptionMode==SslV3 ) {
00197       encryptionMode = Unencrypted;
00198     }
00199     break;
00200   case KTcpSocket::TlsV1:
00201     if ( encryptionMode==TlsV1 ) {
00202       encryptionMode = Unencrypted;
00203     }
00204     break;
00205   case KTcpSocket::AnySslVersion:
00206     if ( encryptionMode==AnySslVersion ) {
00207       encryptionMode = Unencrypted;
00208     }
00209     break;
00210   }
00211 
00212   if (encryptionMode == SslV2
00213    || encryptionMode == SslV3
00214    || encryptionMode == SslV3_1
00215    || encryptionMode == AnySslVersion) {
00216     KTcpSocket::SslVersion version = KTcpSocket::SslV2;
00217     if (encryptionMode == SslV3)
00218       version = KTcpSocket::SslV3;
00219     if (encryptionMode == SslV3_1)
00220       version = KTcpSocket::SslV3_1;
00221     if (encryptionMode == AnySslVersion)
00222       version = KTcpSocket::AnySslVersion;
00223     d->sessionInternal()->startSsl(version);
00224 
00225   } else if (encryptionMode == TlsV1) {
00226     d->authState = LoginJobPrivate::StartTls;
00227     d->tags << d->sessionInternal()->sendCommand( "STARTTLS" );
00228 
00229   } else  if (encryptionMode == Unencrypted ) {
00230     if (d->authMode.isEmpty()) {
00231       d->authState = LoginJobPrivate::Login;
00232       d->tags << d->sessionInternal()->sendCommand( "LOGIN",
00233                                                   '"'+quoteIMAP( d->userName ).toUtf8()+'"'
00234                                                  +' '
00235                                                  +'"'+quoteIMAP(d->password ).toUtf8()+'"' );
00236     } else {
00237       if (!d->startAuthentication()) {
00238         emitResult();
00239       }
00240     }
00241   }
00242 }
00243 
00244 void LoginJob::handleResponse( const Message &response )
00245 {
00246   Q_D(LoginJob);
00247 
00248   //set the actual command name for standard responses
00249   QString commandName = i18n("Login");
00250   if (d->authState == LoginJobPrivate::Capability) {
00251     commandName = i18n("Capability");
00252   } else if (d->authState == LoginJobPrivate::StartTls) {
00253     commandName = i18n("StartTls");
00254   }
00255 
00256   if ( d->authMode == QLatin1String( "PLAIN" ) && !response.content.isEmpty() && response.content.first().toString()=="+" ) {
00257     if ( response.content.size()>1 && response.content.at( 1 ).toString()=="OK" ) {
00258       return;
00259     }
00260 
00261     QByteArray challengeResponse;
00262     challengeResponse+= '\0';
00263     challengeResponse+= d->userName.toUtf8();
00264     challengeResponse+= '\0';
00265     challengeResponse+= d->password.toUtf8();
00266     challengeResponse = challengeResponse.toBase64();
00267     d->sessionInternal()->sendData( challengeResponse );
00268 
00269   } else if ( !response.content.isEmpty()
00270        && d->tags.contains( response.content.first().toString() ) ) {
00271     if ( response.content.size() < 2 ) {
00272       setErrorText( i18n("%1 failed, malformed reply from the server.", commandName) );
00273       emitResult();
00274     } else if ( response.content[1].toString() != "OK" ) {
00275         //server replied with NO or BAD for SASL authentication
00276         if (d->authState == LoginJobPrivate::Authenticate) {
00277           sasl_dispose( &d->conn );
00278         }
00279 
00280         setError( UserDefinedError );
00281         setErrorText( i18n("%1 failed, server replied: %2", commandName, response.toString().constData()) );
00282         emitResult();
00283     } else if ( response.content[1].toString() == "OK")    {
00284       if (d->authState == LoginJobPrivate::Authenticate) {
00285         sasl_dispose( &d->conn ); //SASL authentication done
00286         emitResult();
00287       } else if (d->authState == LoginJobPrivate::Capability) {
00288 
00289         //cleartext login, if enabled
00290         if (d->authMode.isEmpty()) {
00291           if (d->plainLoginDisabled) {
00292             setError( UserDefinedError );
00293             setErrorText( i18n("Login failed, plain login is disabled by the server.") );
00294             emitResult();
00295           } else {
00296             d->authState = LoginJobPrivate::Login;
00297             d->tags << d->sessionInternal()->sendCommand( "LOGIN",
00298                                                         '"'+quoteIMAP( d->userName ).toUtf8()+'"'
00299                                                        +' '
00300                                                        +'"'+quoteIMAP( d->password ).toUtf8()+'"');
00301           }
00302         }
00303 
00304         //find the selected SASL authentication method
00305         Q_FOREACH(const QString &capability, d->capabilities) {
00306           if (capability.startsWith(QLatin1String("AUTH="))) {
00307             QString authType = capability.mid(5);
00308             if (authType == d->authMode) {
00309                 if (!d->startAuthentication()) {
00310                   emitResult(); //problem, we're done
00311                 }
00312             }
00313           }
00314         }
00315       } else if (d->authState == LoginJobPrivate::StartTls) {
00316         d->sessionInternal()->startSsl(KTcpSocket::TlsV1);
00317       } else {
00318         emitResult(); //got an OK, command done
00319       }
00320     }
00321   } else if ( response.content.size() >= 2 ) {
00322     if ( d->authState == LoginJobPrivate::Authenticate ) {
00323       if (!d->answerChallenge(QByteArray::fromBase64(response.content[1].toString()))) {
00324         emitResult(); //error, we're done
00325       }
00326     } else if ( response.content[1].toString()=="CAPABILITY" ) {
00327       bool authModeSupported = d->authMode.isEmpty();
00328       for (int i = 2; i < response.content.size(); ++i) {
00329         QString capability = response.content[i].toString();
00330         d->capabilities << capability;
00331         if (capability == "LOGINDISABLED") {
00332           d->plainLoginDisabled = true;
00333         }
00334         QString authMode = capability.mid(5);
00335         if (authMode == d->authMode) {
00336           authModeSupported = true;
00337         }
00338       }
00339       kDebug() << "Capabilities after STARTTLS: " << d->capabilities;
00340       if (!authModeSupported) {
00341         setError( UserDefinedError );
00342         setErrorText( i18n("Login failed, authentication mode %1 is not supported by the server.", d->authMode) );
00343         d->authState = LoginJobPrivate::Login; //just to treat the upcoming OK correctly
00344       }
00345     }
00346   }
00347 }
00348 
00349 bool LoginJobPrivate::startAuthentication()
00350 {
00351   //SASL authentication
00352   if (!initSASL()) {
00353     q->setError( LoginJob::UserDefinedError );
00354     q->setErrorText( i18n("Login failed, client cannot initialize the SASL library.") );
00355     return false;
00356   }
00357 
00358   authState = LoginJobPrivate::Authenticate;
00359   const char *out = 0;
00360   uint outlen = 0;
00361   const char *mechusing = 0;
00362 
00363   int result = sasl_client_new( "imap", m_session->hostName().toLatin1(), 0, 0, callbacks, 0, &conn );
00364   if ( result != SASL_OK ) {
00365     kDebug() <<"sasl_client_new failed with:" << result;
00366     q->setError( LoginJob::UserDefinedError );
00367     q->setErrorText( QString::fromUtf8( sasl_errdetail( conn ) ) );
00368     return false;
00369   }
00370 
00371   do {
00372     result = sasl_client_start(conn, authMode.toLatin1(), &client_interact, capabilities.contains("SASL-IR") ? &out : 0, &outlen, &mechusing);
00373 
00374     if ( result == SASL_INTERACT ) {
00375       if ( !sasl_interact() ) {
00376         sasl_dispose( &conn );
00377         q->setError( LoginJob::UserDefinedError ); //TODO: check up the actual error
00378         return false;
00379       }
00380     }
00381   } while ( result == SASL_INTERACT );
00382 
00383   if ( result != SASL_CONTINUE && result != SASL_OK ) {
00384     kDebug() <<"sasl_client_start failed with:" << result;
00385     q->setError( LoginJob::UserDefinedError );
00386     q->setErrorText( QString::fromUtf8( sasl_errdetail( conn ) ) );
00387     sasl_dispose( &conn );
00388     return false;
00389   }
00390 
00391   QByteArray tmp = QByteArray::fromRawData( out, outlen );
00392   QByteArray challenge = tmp.toBase64();
00393 
00394   if ( challenge.isEmpty() ) {
00395     tags << sessionInternal()->sendCommand( "AUTHENTICATE", authMode.toLatin1() );
00396   } else {
00397     tags << sessionInternal()->sendCommand( "AUTHENTICATE", authMode.toLatin1() + ' ' + challenge );
00398   }
00399 
00400   return true;
00401 }
00402 
00403 bool LoginJobPrivate::answerChallenge(const QByteArray &data)
00404 {
00405   QByteArray challenge = data;
00406   int result = -1;
00407   const char *out = 0;
00408   uint outlen = 0;
00409   do {
00410     result = sasl_client_step(conn, challenge.isEmpty() ? 0 : challenge.data(),
00411                               challenge.size(),
00412                               &client_interact,
00413                               &out, &outlen);
00414 
00415     if (result == SASL_INTERACT) {
00416       if ( !sasl_interact() ) {
00417         q->setError( LoginJob::UserDefinedError ); //TODO: check up the actual error
00418         sasl_dispose( &conn );
00419         return false;
00420       }
00421     }
00422   } while ( result == SASL_INTERACT );
00423 
00424   if ( result != SASL_CONTINUE && result != SASL_OK ) {
00425     kDebug() <<"sasl_client_step failed with:" << result;
00426     q->setError( LoginJob::UserDefinedError ); //TODO: check up the actual error
00427     q->setErrorText( QString::fromUtf8( sasl_errdetail( conn ) ) );
00428     sasl_dispose( &conn );
00429     return false;
00430   }
00431 
00432   QByteArray tmp = QByteArray::fromRawData( out, outlen );
00433   challenge = tmp.toBase64();
00434 
00435   sessionInternal()->sendData( challenge );
00436 
00437   return true;
00438 }
00439 
00440 void LoginJobPrivate::sslResponse(bool response)
00441 {
00442   if (response) {
00443     authState = LoginJobPrivate::Capability;
00444     tags << sessionInternal()->sendCommand( "CAPABILITY" );
00445   } else {
00446     q->setError( LoginJob::UserDefinedError );
00447     q->setErrorText( i18n("Login failed, TLS negotiation failed." ));
00448     encryptionMode = LoginJob::Unencrypted;
00449     q->emitResult();
00450   }
00451 }
00452 
00453 void LoginJob::setEncryptionMode(EncryptionMode mode)
00454 {
00455   Q_D(LoginJob);
00456   d->encryptionMode = mode;
00457 }
00458 
00459 LoginJob::EncryptionMode LoginJob::encryptionMode()
00460 {
00461   Q_D(LoginJob);
00462   return d->encryptionMode;
00463 }
00464 
00465 void LoginJob::setAuthenticationMode(AuthenticationMode mode)
00466 {
00467   Q_D(LoginJob);
00468   switch (mode)
00469   {
00470     case ClearText: d->authMode = "";
00471       break;
00472     case Login: d->authMode = "LOGIN";
00473       break;
00474     case Plain: d->authMode = "PLAIN";
00475       break;
00476     case CramMD5: d->authMode = "CRAM-MD5";
00477       break;
00478     case DigestMD5: d->authMode = "DIGEST-MD5";
00479       break;
00480     case GSSAPI: d->authMode = "GSSAPI";
00481       break;
00482     case Anonymous: d->authMode = "ANONYMOUS";
00483       break;
00484     default:
00485       d->authMode = "";
00486   }
00487 }
00488 
00489 void LoginJob::connectionLost()
00490 {
00491   Q_D(LoginJob);
00492 
00493   //don't emit the result if the connection was lost before getting the tls result, as it can mean
00494   //the TLS handshake failed and the socket was reconnected in normal mode
00495   if (d->authState != LoginJobPrivate::StartTls) {
00496     emitResult();
00497   }
00498 
00499 }
00500 
00501 
00502 #include "loginjob.moc"

KIMAP Library

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

KDE-PIM Libraries

Skip menu "KDE-PIM Libraries"
  • akonadi
  •   contact
  •   kmime
  • kabc
  • 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
Generated for KDE-PIM Libraries by doxygen 1.7.4
This website is maintained by Adriaan de Groot and Allen Winter.
KDE® and the K Desktop Environment® logo are registered trademarks of KDE e.V. | Legal