File indexing completed on 2024-05-12 05:17:23
0001 /* 0002 Copyright (c) 2009 Kevin Ottens <ervin@kde.org> 0003 Copyright (c) 2009 Andras Mantia <amantia@kde.org> 0004 Copyright (c) 2017 Christian Mollekopf <mollekopf@kolabsys.com> 0005 0006 This library is free software; you can redistribute it and/or modify it 0007 under the terms of the GNU Library General Public License as published by 0008 the Free Software Foundation; either version 2 of the License, or (at your 0009 option) any later version. 0010 0011 This library is distributed in the hope that it will be useful, but WITHOUT 0012 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 0013 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public 0014 License for more details. 0015 0016 You should have received a copy of the GNU Library General Public License 0017 along with this library; see the file COPYING.LIB. If not, write to the 0018 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 0019 02110-1301, USA. 0020 */ 0021 0022 #include "loginjob.h" 0023 0024 #include "kimap_debug.h" 0025 0026 #include "job_p.h" 0027 #include "message_p.h" 0028 #include "session_p.h" 0029 #include "rfccodecs.h" 0030 0031 #include "common.h" 0032 0033 extern "C" { 0034 #include <sasl/sasl.h> 0035 } 0036 0037 static const sasl_callback_t callbacks[] = { 0038 { SASL_CB_ECHOPROMPT, Q_NULLPTR, nullptr }, 0039 { SASL_CB_NOECHOPROMPT, Q_NULLPTR, nullptr }, 0040 { SASL_CB_GETREALM, Q_NULLPTR, nullptr }, 0041 { SASL_CB_USER, Q_NULLPTR, nullptr }, 0042 { SASL_CB_AUTHNAME, Q_NULLPTR, nullptr }, 0043 { SASL_CB_PASS, Q_NULLPTR, nullptr }, 0044 { SASL_CB_CANON_USER, Q_NULLPTR, nullptr }, 0045 { SASL_CB_LIST_END, Q_NULLPTR, nullptr } 0046 }; 0047 0048 namespace KIMAP2 0049 { 0050 class LoginJobPrivate : public JobPrivate 0051 { 0052 public: 0053 enum AuthState { 0054 StartTls = 0, 0055 Capability, 0056 Login, 0057 Authenticate 0058 }; 0059 0060 LoginJobPrivate(LoginJob *job, Session *session, const QString &name) : JobPrivate(session, name), q(job) 0061 { 0062 conn = Q_NULLPTR; 0063 client_interact = Q_NULLPTR; 0064 } 0065 ~LoginJobPrivate() { } 0066 bool sasl_interact(); 0067 0068 bool startAuthentication(); 0069 void sendPlainLogin(); 0070 bool answerChallenge(const QByteArray &data); 0071 void sslResponse(bool response); 0072 void saveServerGreeting(const Message &response); 0073 void login(); 0074 void retrieveCapabilities(); 0075 0076 LoginJob *q; 0077 0078 QString userName; 0079 QString authorizationName; 0080 QString password; 0081 QString serverGreeting; 0082 0083 QSsl::SslProtocol encryptionMode = QSsl::UnknownProtocol; 0084 bool startTls = false; 0085 QString authMode; 0086 AuthState authState = Login; 0087 QStringList capabilities; 0088 bool plainLoginDisabled = false; 0089 bool connectionIsEncrypted = false; 0090 0091 sasl_conn_t *conn; 0092 sasl_interact_t *client_interact; 0093 }; 0094 } 0095 0096 using namespace KIMAP2; 0097 0098 bool LoginJobPrivate::sasl_interact() 0099 { 0100 qCDebug(KIMAP2_LOG) << "sasl_interact"; 0101 sasl_interact_t *interact = client_interact; 0102 0103 //some mechanisms do not require username && pass, so it doesn't need a popup 0104 //window for getting this info 0105 for (; interact->id != SASL_CB_LIST_END; interact++) { 0106 if (interact->id == SASL_CB_AUTHNAME || 0107 interact->id == SASL_CB_PASS) { 0108 //TODO: dialog for use name?? 0109 break; 0110 } 0111 } 0112 0113 interact = client_interact; 0114 while (interact->id != SASL_CB_LIST_END) { 0115 qCDebug(KIMAP2_LOG) << "SASL_INTERACT id:" << interact->id; 0116 switch (interact->id) { 0117 case SASL_CB_AUTHNAME: 0118 if (!authorizationName.isEmpty()) { 0119 qCDebug(KIMAP2_LOG) << "SASL_CB_[AUTHNAME]: '" << authorizationName << "'"; 0120 interact->result = strdup(authorizationName.toUtf8()); 0121 interact->len = strlen((const char *) interact->result); 0122 break; 0123 } 0124 case SASL_CB_USER: 0125 qCDebug(KIMAP2_LOG) << "SASL_CB_[USER|AUTHNAME]: '" << userName << "'"; 0126 interact->result = strdup(userName.toUtf8()); 0127 interact->len = strlen((const char *) interact->result); 0128 break; 0129 case SASL_CB_PASS: 0130 qCDebug(KIMAP2_LOG) << "SASL_CB_PASS: [hidden]"; 0131 interact->result = strdup(password.toUtf8()); 0132 interact->len = strlen((const char *) interact->result); 0133 break; 0134 default: 0135 interact->result = Q_NULLPTR; 0136 interact->len = 0; 0137 break; 0138 } 0139 interact++; 0140 } 0141 //FIXME This should return false at least in some cases 0142 return true; 0143 } 0144 0145 LoginJob::LoginJob(Session *session) 0146 : Job(*new LoginJobPrivate(this, session, QString::fromUtf8("Login"))) 0147 { 0148 qCDebug(KIMAP2_LOG) << this; 0149 } 0150 0151 LoginJob::~LoginJob() 0152 { 0153 qCDebug(KIMAP2_LOG) << this; 0154 } 0155 0156 QString LoginJob::userName() const 0157 { 0158 Q_D(const LoginJob); 0159 return d->userName; 0160 } 0161 0162 void LoginJob::setUserName(const QString &userName) 0163 { 0164 Q_D(LoginJob); 0165 d->userName = userName; 0166 } 0167 0168 QString LoginJob::authorizationName() const 0169 { 0170 Q_D(const LoginJob); 0171 return d->authorizationName; 0172 } 0173 0174 void LoginJob::setAuthorizationName(const QString &authorizationName) 0175 { 0176 Q_D(LoginJob); 0177 d->authorizationName = authorizationName; 0178 } 0179 0180 QString LoginJob::password() const 0181 { 0182 Q_D(const LoginJob); 0183 return d->password; 0184 } 0185 0186 void LoginJob::setPassword(const QString &password) 0187 { 0188 Q_D(LoginJob); 0189 d->password = password; 0190 } 0191 0192 /* 0193 * The IMAP authentication procedure is unfortunately ridiculously complicated due to the many different options: 0194 * 0195 * An IMAP Session always has the following structure: 0196 * * Connection is established. 0197 * * Server sends greeting. 0198 * * Client authenticates somehow. 0199 * * ..... 0200 * 0201 * If the we have a plain connection it's simple: 0202 * * Wait for the greeting 0203 * * Login using the chosen authentication mechanism 0204 * 0205 * If we're using TLS (without STARTTLS, so directly): 0206 * * Immediately initiate TLS handshake. 0207 * * Wait for the greeting 0208 * * Get CAPABILITIES to figure out which AUTH mechs are supported 0209 * * Login using the chosen authentication mechanism 0210 * 0211 * If we're using TLS with STARTTLS: 0212 * * Wait for the greeting (on the unencrypted connection) 0213 * * Send STARTTLS and wait for OK 0214 * * Initiate TLS handshake 0215 * * Get CAPABILITIES to figure out which AUTH mechs are supported 0216 * * Login using the chosen authentication mechanism 0217 */ 0218 void LoginJob::doStart() 0219 { 0220 Q_D(LoginJob); 0221 0222 qCDebug(KIMAP2_LOG) << "doStart" << this; 0223 0224 connect(d->sessionInternal(), SIGNAL(encryptionNegotiationResult(bool)), this, SLOT(sslResponse(bool))); 0225 0226 if (session()->state() == Session::Disconnected) { 0227 auto guard = new QObject(this); 0228 QObject::connect(session(), &Session::stateChanged, guard, [d, guard](KIMAP2::Session::State newState, KIMAP2::Session::State) { 0229 qCDebug(KIMAP2_LOG) << "Session state changed" << newState; 0230 d->login(); 0231 delete guard; 0232 }); 0233 if (!d->startTls && d->encryptionMode != QSsl::UnknownProtocol) { 0234 //We have to encrypt for the greeting 0235 d->sessionInternal()->startSsl(d->encryptionMode); 0236 } 0237 //We wait for the server greeting 0238 return; 0239 } else { 0240 qCInfo(KIMAP2_LOG) << "Session is ready, carring on"; 0241 //The session is ready, we can carry on. 0242 d->login(); 0243 } 0244 0245 } 0246 0247 void LoginJobPrivate::login() 0248 { 0249 // Don't authenticate on a session in the authenticated state 0250 if (q->session()->isConnected()) { 0251 q->setError(LoginJob::UserDefinedError); 0252 q->setErrorText(QString::fromUtf8("IMAP session in the wrong state for authentication")); 0253 q->emitResult(); 0254 return; 0255 } 0256 0257 if (startTls) { 0258 //With STARTTLS we have to try to upgrade our connection before the login 0259 qCInfo(KIMAP2_LOG) << "Starting with tls"; 0260 authState = LoginJobPrivate::StartTls; 0261 sendCommand("STARTTLS", {}); 0262 return; 0263 } else { 0264 //If this is supposed to be unecrypted or already encrypted we can retrieve capabilties. Otherwise we wait for the sslResponse. 0265 if (encryptionMode == QSsl::UnknownProtocol || connectionIsEncrypted) { 0266 retrieveCapabilities(); 0267 } else { 0268 qCInfo(KIMAP2_LOG) << "Waiting for encryption before retrieveing capabilities."; 0269 } 0270 } 0271 0272 } 0273 0274 void LoginJobPrivate::sslResponse(bool response) 0275 { 0276 qCDebug(KIMAP2_LOG) << "Got an ssl response " << response; 0277 connectionIsEncrypted = response; 0278 if (response) { 0279 //It's possible that we receive the ssl info before we receive the server greeting. 0280 //In that case we're still in the Disconnected state and shouldn't retrieve the capabilities just yet. 0281 //We'll try again via login once the state changes. 0282 if (m_session->state() != Session::Disconnected) { 0283 retrieveCapabilities(); 0284 } 0285 } else { 0286 q->setError(LoginFailed); 0287 q->setErrorText(QString::fromUtf8("Login failed, TLS negotiation failed.")); 0288 encryptionMode = QSsl::UnknownProtocol; 0289 q->emitResult(); 0290 } 0291 } 0292 0293 void LoginJobPrivate::retrieveCapabilities() 0294 { 0295 qCDebug(KIMAP2_LOG) << "Retrieving capabilities."; 0296 authState = LoginJobPrivate::Capability; 0297 sendCommand("CAPABILITY", {}); 0298 } 0299 0300 void LoginJob::handleResponse(const Message &response) 0301 { 0302 Q_D(LoginJob); 0303 0304 if (response.content.isEmpty()) { 0305 return; 0306 } 0307 0308 //set the actual command name for standard responses 0309 QString commandName = QStringLiteral("Login"); 0310 if (d->authState == LoginJobPrivate::Capability) { 0311 commandName = QStringLiteral("Capability"); 0312 } else if (d->authState == LoginJobPrivate::StartTls) { 0313 commandName = QStringLiteral("StartTls"); 0314 } 0315 0316 enum ResponseCode { 0317 OK, 0318 ERR, 0319 UNTAGGED, 0320 CONTINUATION, 0321 MALFORMED 0322 }; 0323 0324 QByteArray tag = response.content.first().toString(); 0325 ResponseCode code = OK; 0326 0327 qCDebug(KIMAP2_LOG) << commandName << tag; 0328 0329 if (tag == "+") { 0330 code = CONTINUATION; 0331 } else if (tag == "*") { 0332 if (response.content.size() < 2) { 0333 code = MALFORMED; // Received empty untagged response 0334 } else { 0335 code = UNTAGGED; 0336 } 0337 } else if (d->tags.contains(tag)) { 0338 if (response.content.size() < 2) { 0339 code = MALFORMED; 0340 } else if (response.content[1].toString() == "OK") { 0341 code = OK; 0342 } else { 0343 code = ERR; 0344 } 0345 } 0346 0347 switch (code) { 0348 case MALFORMED: 0349 // We'll handle it later 0350 break; 0351 0352 case ERR: 0353 //server replied with NO or BAD for SASL authentication 0354 if (d->authState == LoginJobPrivate::Authenticate) { 0355 sasl_dispose(&d->conn); 0356 } 0357 0358 setError(LoginFailed); 0359 setErrorText(QString("%1 failed, server replied: %2").arg(commandName).arg(QLatin1String(response.toString().constData()))); 0360 emitResult(); 0361 return; 0362 0363 case UNTAGGED: 0364 // The only untagged response interesting for us here is CAPABILITY 0365 if (response.content[1].toString() == "CAPABILITY") { 0366 QList<Message::Part>::const_iterator p = response.content.begin() + 2; 0367 while (p != response.content.end()) { 0368 QString capability = QLatin1String(p->toString()); 0369 d->capabilities << capability; 0370 if (capability == QLatin1String("LOGINDISABLED")) { 0371 d->plainLoginDisabled = true; 0372 } 0373 ++p; 0374 } 0375 qCInfo(KIMAP2_LOG) << "Capabilities updated: " << d->capabilities; 0376 } 0377 break; 0378 0379 case CONTINUATION: 0380 if (d->authState != LoginJobPrivate::Authenticate) { 0381 // Received unexpected continuation response for something 0382 // other than AUTHENTICATE command 0383 code = MALFORMED; 0384 break; 0385 } 0386 0387 if (d->authMode == QLatin1String("PLAIN")) { 0388 if (response.content.size() > 1 && response.content.at(1).toString() == "OK") { 0389 return; 0390 } 0391 QByteArray challengeResponse; 0392 if (!d->authorizationName.isEmpty()) { 0393 challengeResponse += d->authorizationName.toUtf8(); 0394 } 0395 challengeResponse += '\0'; 0396 challengeResponse += d->userName.toUtf8(); 0397 challengeResponse += '\0'; 0398 challengeResponse += d->password.toUtf8(); 0399 challengeResponse = challengeResponse.toBase64(); 0400 d->sessionInternal()->sendData(challengeResponse); 0401 } else if (response.content.size() >= 2) { 0402 if (!d->answerChallenge(QByteArray::fromBase64(response.content[1].toString()))) { 0403 emitResult(); //error, we're done 0404 } 0405 } else { 0406 // Received empty continuation for authMode other than PLAIN 0407 code = MALFORMED; 0408 } 0409 break; 0410 0411 case OK: 0412 switch (d->authState) { 0413 case LoginJobPrivate::StartTls: 0414 //Start encryption and wait for sslResponse 0415 d->sessionInternal()->startSsl(d->encryptionMode); 0416 break; 0417 case LoginJobPrivate::Capability: 0418 //cleartext login, if enabled 0419 if (d->authMode.isEmpty()) { 0420 if (d->plainLoginDisabled) { 0421 setError(LoginFailed); 0422 setErrorText(QString("Login failed, plain login is disabled by the server.")); 0423 emitResult(); 0424 } else { 0425 d->sendPlainLogin(); 0426 } 0427 } else { 0428 bool authModeSupported = false; 0429 //PLAIN is always supported as defined in the standard. We should also get an AUTH= capability, but in case a server doesn't properly announce it we'll just accept it anyways. 0430 if (d->authMode == "PLAIN") { 0431 authModeSupported = true; 0432 } 0433 //find the selected SASL authentication method 0434 Q_FOREACH (const QString &capability, d->capabilities) { 0435 if (capability.startsWith(QLatin1String("AUTH="))) { 0436 if (capability.mid(5) == d->authMode) { 0437 authModeSupported = true; 0438 break; 0439 } 0440 } 0441 } 0442 if (!authModeSupported) { 0443 setError(LoginFailed); 0444 setErrorText(QString("Login failed, authentication mode %1 is not supported by the server.").arg(d->authMode)); 0445 emitResult(); 0446 } else if (!d->startAuthentication()) { 0447 emitResult(); //problem, we're done 0448 } 0449 } 0450 break; 0451 0452 case LoginJobPrivate::Authenticate: 0453 sasl_dispose(&d->conn); //SASL authentication done 0454 // Fall through 0455 case LoginJobPrivate::Login: 0456 d->saveServerGreeting(response); 0457 emitResult(); //got an OK, command done 0458 break; 0459 0460 } 0461 0462 } 0463 0464 if (code == MALFORMED) { 0465 setErrorText(QString("%1 failed, malformed reply from the server.").arg(commandName)); 0466 emitResult(); 0467 } 0468 } 0469 0470 bool LoginJobPrivate::startAuthentication() 0471 { 0472 //SASL authentication 0473 if (!initSASL()) { 0474 q->setError(LoginFailed); 0475 q->setErrorText(QString("Login failed, client cannot initialize the SASL library.")); 0476 return false; 0477 } 0478 0479 authState = LoginJobPrivate::Authenticate; 0480 const char *out = Q_NULLPTR; 0481 uint outlen = 0; 0482 const char *mechusing = Q_NULLPTR; 0483 0484 int result = sasl_client_new("imap", m_session->hostName().toLatin1(), Q_NULLPTR, nullptr, callbacks, 0, &conn); 0485 if (result != SASL_OK) { 0486 qCWarning(KIMAP2_LOG) << "sasl_client_new failed with:" << result; 0487 q->setError(LoginFailed); 0488 q->setErrorText(QString::fromUtf8(sasl_errdetail(conn))); 0489 return false; 0490 } 0491 0492 do { 0493 result = sasl_client_start(conn, authMode.toLatin1(), &client_interact, capabilities.contains(QStringLiteral("SASL-IR")) ? &out : Q_NULLPTR, &outlen, &mechusing); 0494 0495 if (result == SASL_INTERACT) { 0496 if (!sasl_interact()) { 0497 sasl_dispose(&conn); 0498 q->setError(LoginFailed); //TODO: check up the actual error 0499 q->setErrorText(QString("sasl_interact failed")); 0500 return false; 0501 } 0502 } 0503 } while (result == SASL_INTERACT); 0504 0505 if (result != SASL_CONTINUE && result != SASL_OK) { 0506 qCWarning(KIMAP2_LOG) << "sasl_client_start failed with:" << result; 0507 q->setError(LoginFailed); 0508 q->setErrorText(QString::fromUtf8(sasl_errdetail(conn))); 0509 sasl_dispose(&conn); 0510 return false; 0511 } 0512 0513 QByteArray tmp = QByteArray::fromRawData(out, outlen); 0514 QByteArray challenge = tmp.toBase64(); 0515 0516 if (challenge.isEmpty()) { 0517 sendCommand("AUTHENTICATE", authMode.toLatin1()); 0518 } else { 0519 sendCommand("AUTHENTICATE", authMode.toLatin1() + ' ' + challenge); 0520 } 0521 0522 return true; 0523 } 0524 0525 void LoginJobPrivate::sendPlainLogin() 0526 { 0527 authState = LoginJobPrivate::Login; 0528 qCDebug(KIMAP2_LOG) << "sending LOGIN"; 0529 sendCommand("LOGIN", 0530 '"' + quoteIMAP(userName).toUtf8() + '"' + 0531 ' ' + 0532 '"' + quoteIMAP(password).toUtf8() + '"'); 0533 } 0534 0535 bool LoginJobPrivate::answerChallenge(const QByteArray &data) 0536 { 0537 QByteArray challenge = data; 0538 int result = -1; 0539 const char *out = Q_NULLPTR; 0540 uint outlen = 0; 0541 do { 0542 result = sasl_client_step(conn, challenge.isEmpty() ? Q_NULLPTR : challenge.data(), 0543 challenge.size(), 0544 &client_interact, 0545 &out, &outlen); 0546 0547 if (result == SASL_INTERACT) { 0548 if (!sasl_interact()) { 0549 q->setError(LoginFailed); //TODO: check up the actual error 0550 q->setErrorText(QString("sasl_interact failed")); 0551 sasl_dispose(&conn); 0552 return false; 0553 } 0554 } 0555 } while (result == SASL_INTERACT); 0556 0557 if (result != SASL_CONTINUE && result != SASL_OK) { 0558 qCWarning(KIMAP2_LOG) << "sasl_client_step failed with:" << result; 0559 q->setError(LoginFailed); //TODO: check up the actual error 0560 q->setErrorText(QString::fromUtf8(sasl_errdetail(conn))); 0561 sasl_dispose(&conn); 0562 return false; 0563 } 0564 0565 QByteArray tmp = QByteArray::fromRawData(out, outlen); 0566 challenge = tmp.toBase64(); 0567 0568 sessionInternal()->sendData(challenge); 0569 0570 return true; 0571 } 0572 0573 void LoginJob::setEncryptionMode(QSsl::SslProtocol mode, bool startTls) 0574 { 0575 Q_D(LoginJob); 0576 d->encryptionMode = mode; 0577 d->startTls = startTls; 0578 } 0579 0580 QSsl::SslProtocol LoginJob::encryptionMode() 0581 { 0582 Q_D(LoginJob); 0583 return d->encryptionMode; 0584 } 0585 0586 void LoginJob::setAuthenticationMode(AuthenticationMode mode) 0587 { 0588 Q_D(LoginJob); 0589 switch (mode) { 0590 case ClearText: d->authMode = QLatin1String(""); 0591 break; 0592 case Login: d->authMode = QStringLiteral("LOGIN"); 0593 break; 0594 case Plain: d->authMode = QStringLiteral("PLAIN"); 0595 break; 0596 case CramMD5: d->authMode = QStringLiteral("CRAM-MD5"); 0597 break; 0598 case DigestMD5: d->authMode = QStringLiteral("DIGEST-MD5"); 0599 break; 0600 case GSSAPI: d->authMode = QStringLiteral("GSSAPI"); 0601 break; 0602 case Anonymous: d->authMode = QStringLiteral("ANONYMOUS"); 0603 break; 0604 case XOAuth2: d->authMode = QStringLiteral("XOAUTH2"); 0605 break; 0606 default: 0607 d->authMode = QStringLiteral(""); 0608 } 0609 } 0610 0611 void LoginJob::connectionLost() 0612 { 0613 Q_D(LoginJob); 0614 0615 qCWarning(KIMAP2_LOG) << "Connection to server lost " << d->m_socketError; 0616 if (d->m_socketError == QSslSocket::SslHandshakeFailedError) { 0617 setError(SslHandshakeFailed); 0618 setErrorText(QString::fromUtf8("SSL handshake failed.")); 0619 emitResult(); 0620 } else if (d->m_socketError == QSslSocket::HostNotFoundError) { 0621 setError(HostNotFound); 0622 setErrorText(QString::fromUtf8("Host not found.")); 0623 emitResult(); 0624 } else { 0625 setError(CouldNotConnect); 0626 setErrorText(QString::fromUtf8("Connection to server lost.")); 0627 emitResult(); 0628 } 0629 } 0630 0631 void LoginJobPrivate::saveServerGreeting(const Message &response) 0632 { 0633 // Concatenate the parts of the server response into a string, while dropping the first two parts 0634 // (the response tag and the "OK" code), and being careful not to add useless extra whitespace. 0635 0636 for (int i = 2; i < response.content.size(); i++) { 0637 if (response.content.at(i).type() == Message::Part::List) { 0638 serverGreeting += QLatin1Char('('); 0639 foreach (const QByteArray &item, response.content.at(i).toList()) { 0640 serverGreeting += QLatin1String(item) + QLatin1Char(' '); 0641 } 0642 serverGreeting.chop(1); 0643 serverGreeting += QStringLiteral(") "); 0644 } else { 0645 serverGreeting += QLatin1String(response.content.at(i).toString()) + QLatin1Char(' '); 0646 } 0647 } 0648 serverGreeting.chop(1); 0649 } 0650 0651 QString LoginJob::serverGreeting() const 0652 { 0653 Q_D(const LoginJob); 0654 return d->serverGreeting; 0655 } 0656 0657 #include "moc_loginjob.cpp"