• Skip to content
  • Skip to link menu
KDE 4.8 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       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   //set the actual command name for standard responses
00251   QString commandName = i18n("Login");
00252   if (d->authState == LoginJobPrivate::Capability) {
00253     commandName = i18n("Capability");
00254   } else if (d->authState == LoginJobPrivate::StartTls) {
00255     commandName = i18n("StartTls");
00256   }
00257 
00258   if ( d->authMode == QLatin1String( "PLAIN" ) && !response.content.isEmpty() && response.content.first().toString()=="+" ) {
00259     if ( response.content.size()>1 && response.content.at( 1 ).toString()=="OK" ) {
00260       return;
00261     }
00262 
00263     QByteArray challengeResponse;
00264     challengeResponse+= '\0';
00265     challengeResponse+= d->userName.toUtf8();
00266     challengeResponse+= '\0';
00267     challengeResponse+= d->password.toUtf8();
00268     challengeResponse = challengeResponse.toBase64();
00269     d->sessionInternal()->sendData( challengeResponse );
00270 
00271   } else if ( !response.content.isEmpty()
00272        && d->tags.contains( response.content.first().toString() ) ) {
00273     if ( response.content.size() < 2 ) {
00274       setErrorText( i18n("%1 failed, malformed reply from the server.", commandName) );
00275       emitResult();
00276     } else if ( response.content[1].toString() != "OK" ) {
00277         //server replied with NO or BAD for SASL authentication
00278         if (d->authState == LoginJobPrivate::Authenticate) {
00279           sasl_dispose( &d->conn );
00280         }
00281 
00282         setError( UserDefinedError );
00283         setErrorText( i18n("%1 failed, server replied: %2", commandName, response.toString().constData()) );
00284         emitResult();
00285     } else if ( response.content[1].toString() == "OK")    {
00286       if (d->authState == LoginJobPrivate::Authenticate) {
00287         sasl_dispose( &d->conn ); //SASL authentication done
00288         d->saveServerGreeting( response );
00289         emitResult();
00290       } else if (d->authState == LoginJobPrivate::Capability) {
00291 
00292         //cleartext login, if enabled
00293         if (d->authMode.isEmpty()) {
00294           if (d->plainLoginDisabled) {
00295             setError( UserDefinedError );
00296             setErrorText( i18n("Login failed, plain login is disabled by the server.") );
00297             emitResult();
00298           } else {
00299             d->authState = LoginJobPrivate::Login;
00300             d->tags << d->sessionInternal()->sendCommand( "LOGIN",
00301                                                         '"'+quoteIMAP( d->userName ).toUtf8()+'"'
00302                                                        +' '
00303                                                        +'"'+quoteIMAP( d->password ).toUtf8()+'"');
00304           }
00305         }
00306 
00307         //find the selected SASL authentication method
00308         Q_FOREACH(const QString &capability, d->capabilities) {
00309           if (capability.startsWith(QLatin1String("AUTH="))) {
00310             QString authType = capability.mid(5);
00311             if (authType == d->authMode) {
00312                 if (!d->startAuthentication()) {
00313                   emitResult(); //problem, we're done
00314                 }
00315             }
00316           }
00317         }
00318       } else if (d->authState == LoginJobPrivate::StartTls) {
00319         d->sessionInternal()->startSsl(KTcpSocket::TlsV1);
00320       } else {
00321         d->saveServerGreeting( response );
00322         emitResult(); //got an OK, command done
00323       }
00324     }
00325   } else if ( response.content.size() >= 2 ) {
00326     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     } else if ( d->authState == LoginJobPrivate::Authenticate ) {
00346       if (!d->answerChallenge(QByteArray::fromBase64(response.content[1].toString()))) {
00347         emitResult(); //error, we're done
00348       }
00349     }
00350   }
00351 }
00352 
00353 bool LoginJobPrivate::startAuthentication()
00354 {
00355   //SASL authentication
00356   if (!initSASL()) {
00357     q->setError( LoginJob::UserDefinedError );
00358     q->setErrorText( i18n("Login failed, client cannot initialize the SASL library.") );
00359     return false;
00360   }
00361 
00362   authState = LoginJobPrivate::Authenticate;
00363   const char *out = 0;
00364   uint outlen = 0;
00365   const char *mechusing = 0;
00366 
00367   int result = sasl_client_new( "imap", m_session->hostName().toLatin1(), 0, 0, callbacks, 0, &conn );
00368   if ( result != SASL_OK ) {
00369     kDebug() <<"sasl_client_new failed with:" << result;
00370     q->setError( LoginJob::UserDefinedError );
00371     q->setErrorText( QString::fromUtf8( sasl_errdetail( conn ) ) );
00372     return false;
00373   }
00374 
00375   do {
00376     result = sasl_client_start(conn, authMode.toLatin1(), &client_interact, capabilities.contains("SASL-IR") ? &out : 0, &outlen, &mechusing);
00377 
00378     if ( result == SASL_INTERACT ) {
00379       if ( !sasl_interact() ) {
00380         sasl_dispose( &conn );
00381         q->setError( LoginJob::UserDefinedError ); //TODO: check up the actual error
00382         return false;
00383       }
00384     }
00385   } while ( result == SASL_INTERACT );
00386 
00387   if ( result != SASL_CONTINUE && result != SASL_OK ) {
00388     kDebug() <<"sasl_client_start failed with:" << result;
00389     q->setError( LoginJob::UserDefinedError );
00390     q->setErrorText( QString::fromUtf8( sasl_errdetail( conn ) ) );
00391     sasl_dispose( &conn );
00392     return false;
00393   }
00394 
00395   QByteArray tmp = QByteArray::fromRawData( out, outlen );
00396   QByteArray challenge = tmp.toBase64();
00397 
00398   if ( challenge.isEmpty() ) {
00399     tags << sessionInternal()->sendCommand( "AUTHENTICATE", authMode.toLatin1() );
00400   } else {
00401     tags << sessionInternal()->sendCommand( "AUTHENTICATE", authMode.toLatin1() + ' ' + challenge );
00402   }
00403 
00404   return true;
00405 }
00406 
00407 bool LoginJobPrivate::answerChallenge(const QByteArray &data)
00408 {
00409   QByteArray challenge = data;
00410   int result = -1;
00411   const char *out = 0;
00412   uint outlen = 0;
00413   do {
00414     result = sasl_client_step(conn, challenge.isEmpty() ? 0 : challenge.data(),
00415                               challenge.size(),
00416                               &client_interact,
00417                               &out, &outlen);
00418 
00419     if (result == SASL_INTERACT) {
00420       if ( !sasl_interact() ) {
00421         q->setError( LoginJob::UserDefinedError ); //TODO: check up the actual error
00422         sasl_dispose( &conn );
00423         return false;
00424       }
00425     }
00426   } while ( result == SASL_INTERACT );
00427 
00428   if ( result != SASL_CONTINUE && result != SASL_OK ) {
00429     kDebug() <<"sasl_client_step failed with:" << result;
00430     q->setError( LoginJob::UserDefinedError ); //TODO: check up the actual error
00431     q->setErrorText( QString::fromUtf8( sasl_errdetail( conn ) ) );
00432     sasl_dispose( &conn );
00433     return false;
00434   }
00435 
00436   QByteArray tmp = QByteArray::fromRawData( out, outlen );
00437   challenge = tmp.toBase64();
00438 
00439   sessionInternal()->sendData( challenge );
00440 
00441   return true;
00442 }
00443 
00444 void LoginJobPrivate::sslResponse(bool response)
00445 {
00446   if (response) {
00447     authState = LoginJobPrivate::Capability;
00448     tags << sessionInternal()->sendCommand( "CAPABILITY" );
00449   } else {
00450     q->setError( LoginJob::UserDefinedError );
00451     q->setErrorText( i18n("Login failed, TLS negotiation failed." ));
00452     encryptionMode = LoginJob::Unencrypted;
00453     q->emitResult();
00454   }
00455 }
00456 
00457 void LoginJob::setEncryptionMode(EncryptionMode mode)
00458 {
00459   Q_D(LoginJob);
00460   d->encryptionMode = mode;
00461 }
00462 
00463 LoginJob::EncryptionMode LoginJob::encryptionMode()
00464 {
00465   Q_D(LoginJob);
00466   return d->encryptionMode;
00467 }
00468 
00469 void LoginJob::setAuthenticationMode(AuthenticationMode mode)
00470 {
00471   Q_D(LoginJob);
00472   switch (mode)
00473   {
00474     case ClearText: d->authMode = "";
00475       break;
00476     case Login: d->authMode = "LOGIN";
00477       break;
00478     case Plain: d->authMode = "PLAIN";
00479       break;
00480     case CramMD5: d->authMode = "CRAM-MD5";
00481       break;
00482     case DigestMD5: d->authMode = "DIGEST-MD5";
00483       break;
00484     case GSSAPI: d->authMode = "GSSAPI";
00485       break;
00486     case Anonymous: d->authMode = "ANONYMOUS";
00487       break;
00488     default:
00489       d->authMode = "";
00490   }
00491 }
00492 
00493 void LoginJob::connectionLost()
00494 {
00495   Q_D(LoginJob);
00496 
00497   //don't emit the result if the connection was lost before getting the tls result, as it can mean
00498   //the TLS handshake failed and the socket was reconnected in normal mode
00499   if (d->authState != LoginJobPrivate::StartTls) {
00500     setError( ERR_COULD_NOT_CONNECT );
00501     setErrorText( i18n("Connection to server lost.") );
00502     emitResult();
00503   }
00504 
00505 }
00506 
00507 void LoginJobPrivate::saveServerGreeting(const Message &response)
00508 {
00509   // Concatenate the parts of the server response into a string, while dropping the first two parts
00510   // (the response tag and the "OK" code), and being careful not to add useless extra whitespace.
00511 
00512   for ( int i=2; i<response.content.size(); i++) {
00513     if ( response.content.at(i).type()==Message::Part::List ) {
00514       serverGreeting+='(';
00515       foreach ( const QByteArray &item, response.content.at(i).toList() ) {
00516         serverGreeting+=item+' ';
00517       }
00518       serverGreeting.chop(1);
00519       serverGreeting+=") ";
00520     } else {
00521       serverGreeting+=response.content.at(i).toString()+' ';
00522     }
00523   }
00524   serverGreeting.chop(1);
00525 }
00526 
00527 QString LoginJob::serverGreeting() const
00528 {
00529   Q_D(const LoginJob);
00530   return d->serverGreeting;
00531 }
00532 
00533 #include "loginjob.moc"

KIMAP Library

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

KDE-PIM Libraries

Skip menu "KDE-PIM Libraries"
  • 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
Generated for KDE-PIM Libraries by doxygen 1.7.6.1
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