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"