1/*
2 Copyright (c) 2009 Kevin Ottens <ervin@kde.org>
3 Copyright (c) 2009 Andras Mantia <amantia@kde.org>
4
5 This library is free software; you can redistribute it and/or modify it
6 under the terms of the GNU Library General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or (at your
8 option) any later version.
9
10 This library is distributed in the hope that it will be useful, but WITHOUT
11 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
13 License for more details.
14
15 You should have received a copy of the GNU Library General Public License
16 along with this library; see the file COPYING.LIB. If not, write to the
17 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 02110-1301, USA.
19*/
20
21#include "loginjob.h"
22
23#include <KDE/KLocalizedString>
24#include <KDE/KDebug>
25#include <ktcpsocket.h>
26
27#include "job_p.h"
28#include "message_p.h"
29#include "session_p.h"
30#include "rfccodecs.h"
31
32#include "common.h"
33
34extern "C" {
35#include <sasl/sasl.h>
36}
37
38static sasl_callback_t callbacks[] = {
39 { SASL_CB_ECHOPROMPT, NULL, NULL },
40 { SASL_CB_NOECHOPROMPT, NULL, NULL },
41 { SASL_CB_GETREALM, NULL, NULL },
42 { SASL_CB_USER, NULL, NULL },
43 { SASL_CB_AUTHNAME, NULL, NULL },
44 { SASL_CB_PASS, NULL, NULL },
45 { SASL_CB_CANON_USER, NULL, NULL },
46 { SASL_CB_LIST_END, NULL, NULL }
47};
48
49namespace KIMAP
50{
51 class LoginJobPrivate : public JobPrivate
52 {
53 public:
54 enum AuthState {
55 StartTls = 0,
56 Capability,
57 Login,
58 Authenticate
59 };
60
61 LoginJobPrivate( LoginJob *job, Session *session, const QString& name ) : JobPrivate( session, name ), q( job ), encryptionMode( LoginJob::Unencrypted ), authState( Login ), plainLoginDisabled( false ) {
62 conn = 0;
63 client_interact = 0;
64 }
65 ~LoginJobPrivate() { }
66 bool sasl_interact();
67
68 bool startAuthentication();
69 bool answerChallenge(const QByteArray &data);
70 void sslResponse(bool response);
71 void saveServerGreeting(const Message &response);
72
73 LoginJob *q;
74
75 QString userName;
76 QString authorizationName;
77 QString password;
78 QString serverGreeting;
79
80 LoginJob::EncryptionMode encryptionMode;
81 QString authMode;
82 AuthState authState;
83 QStringList capabilities;
84 bool plainLoginDisabled;
85
86 sasl_conn_t *conn;
87 sasl_interact_t *client_interact;
88 };
89}
90
91using namespace KIMAP;
92
93bool LoginJobPrivate::sasl_interact()
94{
95 kDebug() << "sasl_interact";
96 sasl_interact_t *interact = client_interact;
97
98 //some mechanisms do not require username && pass, so it doesn't need a popup
99 //window for getting this info
100 for ( ; interact->id != SASL_CB_LIST_END; interact++ ) {
101 if ( interact->id == SASL_CB_AUTHNAME ||
102 interact->id == SASL_CB_PASS ) {
103 //TODO: dialog for use name??
104 break;
105 }
106 }
107
108 interact = client_interact;
109 while ( interact->id != SASL_CB_LIST_END ) {
110 kDebug() << "SASL_INTERACT id:" << interact->id;
111 switch ( interact->id ) {
112 case SASL_CB_AUTHNAME:
113 if ( !authorizationName.isEmpty() ) {
114 kDebug() << "SASL_CB_[AUTHNAME]: '" << authorizationName << "'";
115 interact->result = strdup( authorizationName.toUtf8() );
116 interact->len = strlen( (const char *) interact->result );
117 break;
118 }
119 case SASL_CB_USER:
120 kDebug() << "SASL_CB_[USER|AUTHNAME]: '" << userName << "'";
121 interact->result = strdup( userName.toUtf8() );
122 interact->len = strlen( (const char *) interact->result );
123 break;
124 case SASL_CB_PASS:
125 kDebug() << "SASL_CB_PASS: [hidden]";
126 interact->result = strdup( password.toUtf8() );
127 interact->len = strlen( (const char *) interact->result );
128 break;
129 default:
130 interact->result = 0;
131 interact->len = 0;
132 break;
133 }
134 interact++;
135 }
136 return true;
137}
138
139LoginJob::LoginJob( Session *session )
140 : Job( *new LoginJobPrivate( this, session, i18n( "Login" ) ) )
141{
142 Q_D( LoginJob );
143 connect( d->sessionInternal(), SIGNAL(encryptionNegotiationResult(bool)), this, SLOT(sslResponse(bool)) );
144 kDebug() << this;
145}
146
147LoginJob::~LoginJob()
148{
149 kDebug() << this;
150}
151
152QString LoginJob::userName() const
153{
154 Q_D( const LoginJob );
155 return d->userName;
156}
157
158void LoginJob::setUserName( const QString &userName )
159{
160 Q_D( LoginJob );
161 d->userName = userName;
162}
163
164QString LoginJob::authorizationName() const
165{
166 Q_D( const LoginJob );
167 return d->authorizationName;
168}
169
170void LoginJob::setAuthorizationName( const QString& authorizationName )
171{
172 Q_D( LoginJob );
173 d->authorizationName = authorizationName;
174}
175
176QString LoginJob::password() const
177{
178 Q_D( const LoginJob );
179 return d->password;
180}
181
182void LoginJob::setPassword( const QString &password )
183{
184 Q_D( LoginJob );
185 d->password = password;
186}
187
188void LoginJob::doStart()
189{
190 Q_D( LoginJob );
191
192 kDebug() << this;
193 // Don't authenticate on a session in the authenticated state
194 if ( session()->state() == Session::Authenticated || session()->state() == Session::Selected ) {
195 setError( UserDefinedError );
196 setErrorText( i18n( "IMAP session in the wrong state for authentication" ) );
197 emitResult();
198 return;
199 }
200
201 // Trigger encryption negotiation only if needed
202 EncryptionMode encryptionMode = d->encryptionMode;
203
204 switch ( d->sessionInternal()->negotiatedEncryption() ) {
205 case KTcpSocket::UnknownSslVersion:
206 break; // Do nothing the encryption mode still needs to be negotiated
207
208 // For the other cases, pretend we're going unencrypted as that's the
209 // encryption mode already set on the session
210 // (so for instance we won't issue another STARTTLS for nothing if that's
211 // not needed)
212 case KTcpSocket::SslV2:
213 if ( encryptionMode == SslV2 ) {
214 encryptionMode = Unencrypted;
215 }
216 break;
217 case KTcpSocket::SslV3:
218 if ( encryptionMode == SslV3 ) {
219 encryptionMode = Unencrypted;
220 }
221 break;
222 case KTcpSocket::TlsV1:
223 if ( encryptionMode == TlsV1 ) {
224 encryptionMode = Unencrypted;
225 }
226 break;
227 case KTcpSocket::AnySslVersion:
228 if ( encryptionMode == AnySslVersion ) {
229 encryptionMode = Unencrypted;
230 }
231 break;
232 }
233
234 if ( encryptionMode == SslV2 ||
235 encryptionMode == SslV3 ||
236 encryptionMode == SslV3_1 ||
237 encryptionMode == AnySslVersion ) {
238 KTcpSocket::SslVersion version = KTcpSocket::SslV2;
239 if ( encryptionMode == SslV3 ) {
240 version = KTcpSocket::SslV3;
241 }
242 if ( encryptionMode == SslV3_1 ) {
243 version = KTcpSocket::SslV3_1;
244 }
245 if ( encryptionMode == AnySslVersion ) {
246 version = KTcpSocket::AnySslVersion;
247 }
248 d->sessionInternal()->startSsl( version );
249
250 } else if ( encryptionMode == TlsV1 ) {
251 d->authState = LoginJobPrivate::StartTls;
252 d->tags << d->sessionInternal()->sendCommand( "STARTTLS" );
253
254 } else if ( encryptionMode == Unencrypted ) {
255 if ( d->authMode.isEmpty() ) {
256 d->authState = LoginJobPrivate::Login;
257 kDebug() << "sending LOGIN";
258 d->tags << d->sessionInternal()->sendCommand( "LOGIN",
259 '"' + quoteIMAP( d->userName ).toUtf8() + '"' +
260 ' ' +
261 '"' + quoteIMAP( d->password ).toUtf8() + '"' );
262 } else {
263 if ( !d->startAuthentication() ) {
264 emitResult();
265 }
266 }
267 }
268}
269
270void LoginJob::handleResponse( const Message &response )
271{
272 Q_D( LoginJob );
273
274 if ( response.content.isEmpty() ) {
275 return;
276 }
277
278 //set the actual command name for standard responses
279 QString commandName = i18n( "Login" );
280 if ( d->authState == LoginJobPrivate::Capability ) {
281 commandName = i18n( "Capability" );
282 } else if ( d->authState == LoginJobPrivate::StartTls ) {
283 commandName = i18n( "StartTls" );
284 }
285
286 enum ResponseCode {
287 OK,
288 ERR,
289 UNTAGGED,
290 CONTINUATION,
291 MALFORMED
292 };
293
294 QByteArray tag = response.content.first().toString();
295 ResponseCode code = OK;
296
297 kDebug() << commandName << tag;
298
299 if ( tag == "+" ) {
300 code = CONTINUATION;
301 } else if ( tag == "*" ) {
302 if ( response.content.size() < 2 ) {
303 code = MALFORMED; // Received empty untagged response
304 } else {
305 code = UNTAGGED;
306 }
307 } else if ( d->tags.contains( tag ) ) {
308 if ( response.content.size() < 2 ) {
309 code = MALFORMED;
310 } else if ( response.content[1].toString() == "OK" ) {
311 code = OK;
312 } else {
313 code = ERR;
314 }
315 }
316
317 switch ( code ) {
318 case MALFORMED:
319 // We'll handle it later
320 break;
321
322 case ERR:
323 //server replied with NO or BAD for SASL authentication
324 if ( d->authState == LoginJobPrivate::Authenticate ) {
325 sasl_dispose( &d->conn );
326 }
327
328 setError( UserDefinedError );
329 setErrorText( i18n( "%1 failed, server replied: %2", commandName, QLatin1String(response.toString().constData()) ) );
330 emitResult();
331 return;
332
333 case UNTAGGED:
334 // The only untagged response interesting for us here is CAPABILITY
335 if ( response.content[1].toString() == "CAPABILITY" ) {
336 QList<Message::Part>::const_iterator p = response.content.begin() + 2;
337 while ( p != response.content.end() ) {
338 QString capability = QLatin1String(p->toString());
339 d->capabilities << capability;
340 if ( capability == QLatin1String("LOGINDISABLED") ) {
341 d->plainLoginDisabled = true;
342 }
343 ++p;
344 }
345 kDebug() << "Capabilities updated: " << d->capabilities;
346 }
347 break;
348
349 case CONTINUATION:
350 if ( d->authState != LoginJobPrivate::Authenticate ) {
351 // Received unexpected continuation response for something
352 // other than AUTHENTICATE command
353 code = MALFORMED;
354 break;
355 }
356
357 if ( d->authMode == QLatin1String( "PLAIN" ) ) {
358 if ( response.content.size()>1 && response.content.at( 1 ).toString() == "OK" ) {
359 return;
360 }
361 QByteArray challengeResponse;
362 if ( !d->authorizationName.isEmpty() ) {
363 challengeResponse+= d->authorizationName.toUtf8();
364 }
365 challengeResponse+= '\0';
366 challengeResponse+= d->userName.toUtf8();
367 challengeResponse+= '\0';
368 challengeResponse+= d->password.toUtf8();
369 challengeResponse = challengeResponse.toBase64();
370 d->sessionInternal()->sendData( challengeResponse );
371 } else if ( response.content.size() >= 2 ) {
372 if ( !d->answerChallenge( QByteArray::fromBase64( response.content[1].toString() ) ) ) {
373 emitResult(); //error, we're done
374 }
375 } else {
376 // Received empty continuation for authMode other than PLAIN
377 code = MALFORMED;
378 }
379 break;
380
381 case OK:
382
383 switch ( d->authState ) {
384 case LoginJobPrivate::StartTls:
385 d->sessionInternal()->startSsl( KTcpSocket::TlsV1 );
386 break;
387
388 case LoginJobPrivate::Capability:
389 //cleartext login, if enabled
390 if ( d->authMode.isEmpty() ) {
391 if ( d->plainLoginDisabled ) {
392 setError( UserDefinedError );
393 setErrorText( i18n( "Login failed, plain login is disabled by the server." ) );
394 emitResult();
395 } else {
396 d->authState = LoginJobPrivate::Login;
397 d->tags << d->sessionInternal()->sendCommand( "LOGIN",
398 '"' + quoteIMAP( d->userName ).toUtf8() + '"' +
399 ' ' +
400 '"' + quoteIMAP( d->password ).toUtf8() + '"' );
401 }
402 } else {
403 bool authModeSupported = false;
404 //find the selected SASL authentication method
405 Q_FOREACH ( const QString &capability, d->capabilities ) {
406 if ( capability.startsWith( QLatin1String( "AUTH=" ) ) ) {
407 if ( capability.mid( 5 ) == d->authMode ) {
408 authModeSupported = true;
409 break;
410 }
411 }
412 }
413 if ( !authModeSupported ) {
414 setError( UserDefinedError );
415 setErrorText( i18n( "Login failed, authentication mode %1 is not supported by the server.", d->authMode ) );
416 emitResult();
417 } else if ( !d->startAuthentication() ) {
418 emitResult(); //problem, we're done
419 }
420 }
421 break;
422
423 case LoginJobPrivate::Authenticate:
424 sasl_dispose( &d->conn ); //SASL authentication done
425 // Fall through
426 case LoginJobPrivate::Login:
427 d->saveServerGreeting( response );
428 emitResult(); //got an OK, command done
429 break;
430
431 }
432
433 }
434
435 if ( code == MALFORMED ) {
436 setErrorText( i18n( "%1 failed, malformed reply from the server.", commandName ) );
437 emitResult();
438 }
439}
440
441bool LoginJobPrivate::startAuthentication()
442{
443 //SASL authentication
444 if ( !initSASL() ) {
445 q->setError( LoginJob::UserDefinedError );
446 q->setErrorText( i18n( "Login failed, client cannot initialize the SASL library." ) );
447 return false;
448 }
449
450 authState = LoginJobPrivate::Authenticate;
451 const char *out = 0;
452 uint outlen = 0;
453 const char *mechusing = 0;
454
455 int result = sasl_client_new( "imap", m_session->hostName().toLatin1(), 0, 0, callbacks, 0, &conn );
456 if ( result != SASL_OK ) {
457 kDebug() << "sasl_client_new failed with:" << result;
458 q->setError( LoginJob::UserDefinedError );
459 q->setErrorText( QString::fromUtf8( sasl_errdetail( conn ) ) );
460 return false;
461 }
462
463 do {
464 result = sasl_client_start( conn, authMode.toLatin1(), &client_interact, capabilities.contains( QLatin1String("SASL-IR") ) ? &out : 0, &outlen, &mechusing );
465
466 if ( result == SASL_INTERACT ) {
467 if ( !sasl_interact() ) {
468 sasl_dispose( &conn );
469 q->setError( LoginJob::UserDefinedError ); //TODO: check up the actual error
470 return false;
471 }
472 }
473 } while ( result == SASL_INTERACT );
474
475 if ( result != SASL_CONTINUE && result != SASL_OK ) {
476 kDebug() << "sasl_client_start failed with:" << result;
477 q->setError( LoginJob::UserDefinedError );
478 q->setErrorText( QString::fromUtf8( sasl_errdetail( conn ) ) );
479 sasl_dispose( &conn );
480 return false;
481 }
482
483 QByteArray tmp = QByteArray::fromRawData( out, outlen );
484 QByteArray challenge = tmp.toBase64();
485
486 if ( challenge.isEmpty() ) {
487 tags << sessionInternal()->sendCommand( "AUTHENTICATE", authMode.toLatin1() );
488 } else {
489 tags << sessionInternal()->sendCommand( "AUTHENTICATE", authMode.toLatin1() + ' ' + challenge );
490 }
491
492 return true;
493}
494
495bool LoginJobPrivate::answerChallenge(const QByteArray &data)
496{
497 QByteArray challenge = data;
498 int result = -1;
499 const char *out = 0;
500 uint outlen = 0;
501 do {
502 result = sasl_client_step( conn, challenge.isEmpty() ? 0 : challenge.data(),
503 challenge.size(),
504 &client_interact,
505 &out, &outlen );
506
507 if ( result == SASL_INTERACT ) {
508 if ( !sasl_interact() ) {
509 q->setError( LoginJob::UserDefinedError ); //TODO: check up the actual error
510 sasl_dispose( &conn );
511 return false;
512 }
513 }
514 } while ( result == SASL_INTERACT );
515
516 if ( result != SASL_CONTINUE && result != SASL_OK ) {
517 kDebug() << "sasl_client_step failed with:" << result;
518 q->setError( LoginJob::UserDefinedError ); //TODO: check up the actual error
519 q->setErrorText( QString::fromUtf8( sasl_errdetail( conn ) ) );
520 sasl_dispose( &conn );
521 return false;
522 }
523
524 QByteArray tmp = QByteArray::fromRawData( out, outlen );
525 challenge = tmp.toBase64();
526
527 sessionInternal()->sendData( challenge );
528
529 return true;
530}
531
532void LoginJobPrivate::sslResponse(bool response)
533{
534 if ( response ) {
535 authState = LoginJobPrivate::Capability;
536 tags << sessionInternal()->sendCommand( "CAPABILITY" );
537 } else {
538 q->setError( LoginJob::UserDefinedError );
539 q->setErrorText( i18n( "Login failed, TLS negotiation failed." ) );
540 encryptionMode = LoginJob::Unencrypted;
541 q->emitResult();
542 }
543}
544
545void LoginJob::setEncryptionMode(EncryptionMode mode)
546{
547 Q_D( LoginJob );
548 d->encryptionMode = mode;
549}
550
551LoginJob::EncryptionMode LoginJob::encryptionMode()
552{
553 Q_D( LoginJob );
554 return d->encryptionMode;
555}
556
557void LoginJob::setAuthenticationMode(AuthenticationMode mode)
558{
559 Q_D( LoginJob );
560 switch ( mode ) {
561 case ClearText: d->authMode = QLatin1String( "");
562 break;
563 case Login: d->authMode = QLatin1String("LOGIN");
564 break;
565 case Plain: d->authMode = QLatin1String("PLAIN");
566 break;
567 case CramMD5: d->authMode = QLatin1String("CRAM-MD5");
568 break;
569 case DigestMD5: d->authMode = QLatin1String("DIGEST-MD5");
570 break;
571 case GSSAPI: d->authMode = QLatin1String("GSSAPI");
572 break;
573 case Anonymous: d->authMode = QLatin1String("ANONYMOUS");
574 break;
575 case XOAuth2: d->authMode = QLatin1String("XOAUTH2");
576 break;
577 default:
578 d->authMode = QLatin1String("");
579 }
580}
581
582void LoginJob::connectionLost()
583{
584 Q_D( LoginJob );
585
586 //don't emit the result if the connection was lost before getting the tls result, as it can mean
587 //the TLS handshake failed and the socket was reconnected in normal mode
588 if ( d->authState != LoginJobPrivate::StartTls ) {
589 kWarning() << "Connection to server lost " << d->m_socketError;
590 if ( d->m_socketError == KTcpSocket::SslHandshakeFailedError) {
591 setError( KJob::UserDefinedError );
592 setErrorText( i18n( "SSL handshake failed." ) );
593 emitResult();
594 } else {
595 setError( ERR_COULD_NOT_CONNECT );
596 setErrorText( i18n( "Connection to server lost." ) );
597 emitResult();
598 }
599 }
600
601}
602
603void LoginJobPrivate::saveServerGreeting(const Message &response)
604{
605 // Concatenate the parts of the server response into a string, while dropping the first two parts
606 // (the response tag and the "OK" code), and being careful not to add useless extra whitespace.
607
608 for ( int i = 2; i < response.content.size(); i++ ) {
609 if ( response.content.at( i ).type() == Message::Part::List ) {
610 serverGreeting += QLatin1Char('(');
611 foreach ( const QByteArray &item, response.content.at( i ).toList() ) {
612 serverGreeting += QLatin1String(item) + QLatin1Char(' ');
613 }
614 serverGreeting.chop( 1 );
615 serverGreeting += QLatin1String(") ");
616 } else {
617 serverGreeting+= QLatin1String(response.content.at( i ).toString()) + QLatin1Char(' ');
618 }
619 }
620 serverGreeting.chop( 1 );
621}
622
623QString LoginJob::serverGreeting() const
624{
625 Q_D( const LoginJob );
626 return d->serverGreeting;
627}
628
629#include "moc_loginjob.cpp"
630