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"