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 Thu Aug 2 2012 15:24:24 by doxygen 1.7.5 written by Dimitri van Heesch, © 1997-2006
Documentation copyright © 1996-2012 The KDE developers.
Generated on Thu Aug 2 2012 15:24:24 by doxygen 1.7.5 written by Dimitri van Heesch, © 1997-2006
KDE's Doxygen guidelines are available online.