loginjob.cpp 12 KB
Newer Older
Gregory Schlomoff's avatar
Gregory Schlomoff committed
1
/*
2
3
4
  SPDX-FileCopyrightText: 2010 BetterInbox <contact@betterinbox.com>
  SPDX-FileContributor: Christophe Laveault <christophe@betterinbox.com>
  SPDX-FileContributor: Gregory Schlomoff <gregory.schlomoff@gmail.com>
Daniel Vrátil's avatar
Daniel Vrátil committed
5

6
  SPDX-License-Identifier: LGPL-2.1-or-later
Gregory Schlomoff's avatar
Gregory Schlomoff committed
7
8
9
10
*/

#include "loginjob.h"
#include "job_p.h"
Laurent Montel's avatar
Laurent Montel committed
11
#include "ksmtp_debug.h"
Gregory Schlomoff's avatar
Gregory Schlomoff committed
12
13
14
#include "serverresponse_p.h"
#include "session_p.h"

Daniel Vrátil's avatar
Daniel Vrátil committed
15
16
#include <KLocalizedString>

17
18
19
#include <QJsonDocument>
#include <QJsonObject>

20
21
22
23
extern "C" {
#include <sasl/sasl.h>
}

Laurent Montel's avatar
Laurent Montel committed
24
25
26
27
28
29
30
31
32
33
namespace
{
static const sasl_callback_t callbacks[] = {{SASL_CB_ECHOPROMPT, nullptr, nullptr},
                                            {SASL_CB_NOECHOPROMPT, nullptr, nullptr},
                                            {SASL_CB_GETREALM, nullptr, nullptr},
                                            {SASL_CB_USER, nullptr, nullptr},
                                            {SASL_CB_AUTHNAME, nullptr, nullptr},
                                            {SASL_CB_PASS, nullptr, nullptr},
                                            {SASL_CB_CANON_USER, nullptr, nullptr},
                                            {SASL_CB_LIST_END, nullptr, nullptr}};
34
35
}

Laurent Montel's avatar
Laurent Montel committed
36
37
namespace KSmtp
{
Gregory Schlomoff's avatar
Gregory Schlomoff committed
38
39
40
class LoginJobPrivate : public JobPrivate
{
public:
Daniel Vrátil's avatar
Daniel Vrátil committed
41
    LoginJobPrivate(LoginJob *job, Session *session, const QString &name)
42
43
44
45
        : JobPrivate(session, name)
        , m_preferedAuthMode(LoginJob::Login)
        , m_actualAuthMode(LoginJob::UnknownAuth)
        , q(job)
Daniel Vrátil's avatar
Daniel Vrátil committed
46
47
    {
    }
48

Laurent Montel's avatar
Laurent Montel committed
49
50
51
    ~LoginJobPrivate() override
    {
    }
Daniel Vrátil's avatar
Daniel Vrátil committed
52

53
54
55
    bool sasl_interact();
    bool sasl_init();
    bool sasl_challenge(const QByteArray &data);
Daniel Vrátil's avatar
Daniel Vrátil committed
56

57
58
59
    bool authenticate();
    bool selectAuthentication();

60
61
    LoginJob::AuthMode authModeFromCommand(const QByteArray &mech) const;
    QByteArray authCommand(LoginJob::AuthMode mode) const;
Daniel Vrátil's avatar
Daniel Vrátil committed
62
63
64
65

    QString m_userName;
    QString m_password;
    LoginJob::AuthMode m_preferedAuthMode;
66
67
    LoginJob::AuthMode m_actualAuthMode;

Laurent Montel's avatar
Laurent Montel committed
68
69
    sasl_conn_t *m_saslConn = nullptr;
    sasl_interact_t *m_saslClient = nullptr;
70
71

private:
Laurent Montel's avatar
Laurent Montel committed
72
    LoginJob *const q;
Gregory Schlomoff's avatar
Gregory Schlomoff committed
73
74
75
76
77
78
};
}

using namespace KSmtp;

LoginJob::LoginJob(Session *session)
Daniel Vrátil's avatar
Daniel Vrátil committed
79
    : Job(*new LoginJobPrivate(this, session, i18n("Login")))
Gregory Schlomoff's avatar
Gregory Schlomoff committed
80
81
82
83
84
85
86
87
88
{
}

LoginJob::~LoginJob()
{
}

void LoginJob::setUserName(const QString &userName)
{
Daniel Vrátil's avatar
Daniel Vrátil committed
89
90
    Q_D(LoginJob);
    d->m_userName = userName;
Gregory Schlomoff's avatar
Gregory Schlomoff committed
91
92
93
94
}

void LoginJob::setPassword(const QString &password)
{
Daniel Vrátil's avatar
Daniel Vrátil committed
95
96
    Q_D(LoginJob);
    d->m_password = password;
Gregory Schlomoff's avatar
Gregory Schlomoff committed
97
98
99
100
}

void LoginJob::setPreferedAuthMode(AuthMode mode)
{
Daniel Vrátil's avatar
Daniel Vrátil committed
101
102
103
104
105
106
107
    Q_D(LoginJob);

    if (mode == UnknownAuth) {
        qCWarning(KSMTP_LOG) << "LoginJob: Cannot set preferred authentication mode to Unknown";
        return;
    }
    d->m_preferedAuthMode = mode;
Gregory Schlomoff's avatar
Gregory Schlomoff committed
108
109
}

110
111
112
113
114
LoginJob::AuthMode LoginJob::usedAuthMode() const
{
    return d_func()->m_actualAuthMode;
}

Gregory Schlomoff's avatar
Gregory Schlomoff committed
115
116
void LoginJob::doStart()
{
Daniel Vrátil's avatar
Daniel Vrátil committed
117
118
    Q_D(LoginJob);

119
    const auto negotiatedEnc = d->sessionInternal()->negotiatedEncryption();
120
121
122
123
124
125
    if (negotiatedEnc != QSsl::UnknownProtocol || d->m_session->encryptionMode() == Session::Unencrypted) {
        // Socket already encrypted, or no encryption requested: continue with authentication
        if (!d->authenticate()) {
            emitResult();
        }
    } else if (d->m_session->encryptionMode() == Session::TLS) {
126
        d->sessionInternal()->startSsl();
127
    } else if (d->m_session->encryptionMode() == Session::STARTTLS) {
Daniel Vrátil's avatar
Daniel Vrátil committed
128
129
130
131
132
        if (session()->allowsTls()) {
            sendCommand(QByteArrayLiteral("STARTTLS"));
        } else {
            qCWarning(KSMTP_LOG) << "STARTTLS not supported by the server!";
            setError(KJob::UserDefinedError);
133
            setErrorText(i18n("STARTTLS is not supported by the server, try using SSL/TLS instead."));
Daniel Vrátil's avatar
Daniel Vrátil committed
134
135
            emitResult();
        }
Daniel Vrátil's avatar
Daniel Vrátil committed
136
    }
Gregory Schlomoff's avatar
Gregory Schlomoff committed
137
138
139
140
}

void LoginJob::handleResponse(const ServerResponse &r)
{
Daniel Vrátil's avatar
Daniel Vrátil committed
141
    Q_D(LoginJob);
Gregory Schlomoff's avatar
Gregory Schlomoff committed
142

Daniel Vrátil's avatar
Daniel Vrátil committed
143
144
    // Handle server errors
    handleErrors(r);
Gregory Schlomoff's avatar
Gregory Schlomoff committed
145

Daniel Vrátil's avatar
Daniel Vrátil committed
146
147
    // Server accepts TLS connection
    if (r.isCode(220)) {
148
        d->sessionInternal()->startSsl();
149
        return;
Daniel Vrátil's avatar
Daniel Vrátil committed
150
    }
Gregory Schlomoff's avatar
Gregory Schlomoff committed
151

Daniel Vrátil's avatar
Daniel Vrátil committed
152
    // Available authentication mechanisms
Laurent Montel's avatar
Laurent Montel committed
153
    if (r.isCode(25) && r.text().startsWith("AUTH ")) { // krazy:exclude=strings
154
        d->sessionInternal()->setAuthenticationMethods(r.text().remove(0, QByteArray("AUTH ").count()).split(' '));
Daniel Vrátil's avatar
Daniel Vrátil committed
155
        d->authenticate();
156
        return;
Gregory Schlomoff's avatar
Gregory Schlomoff committed
157
    }
Daniel Vrátil's avatar
Daniel Vrátil committed
158
159

    // Send account data
160
161
    if (r.isCode(334)) {
        if (d->m_actualAuthMode == Plain) {
Laurent Montel's avatar
Laurent Montel committed
162
            const QByteArray challengeResponse = '\0' + d->m_userName.toUtf8() + '\0' + d->m_password.toUtf8();
163
164
165
166
167
            sendCommand(challengeResponse.toBase64());
        } else {
            if (!d->sasl_challenge(QByteArray::fromBase64(r.text()))) {
                emitResult();
            }
Daniel Vrátil's avatar
Daniel Vrátil committed
168
        }
169
        return;
Gregory Schlomoff's avatar
Gregory Schlomoff committed
170
171
    }

Daniel Vrátil's avatar
Daniel Vrátil committed
172
173
174
175
176
    // Final agreement
    if (r.isCode(235)) {
        d->sessionInternal()->setState(Session::Authenticated);
        emitResult();
    }
Gregory Schlomoff's avatar
Gregory Schlomoff committed
177
178
}

179
bool LoginJobPrivate::selectAuthentication()
Gregory Schlomoff's avatar
Gregory Schlomoff committed
180
{
181
    const QStringList availableModes = m_session->availableAuthModes();
Daniel Vrátil's avatar
Daniel Vrátil committed
182
183

    if (availableModes.contains(QString::fromLatin1(authCommand(m_preferedAuthMode)))) {
184
        m_actualAuthMode = m_preferedAuthMode;
Daniel Vrátil's avatar
Daniel Vrátil committed
185
    } else if (availableModes.contains(QString::fromLatin1(authCommand(LoginJob::Login)))) {
186
        m_actualAuthMode = LoginJob::Login;
Daniel Vrátil's avatar
Daniel Vrátil committed
187
    } else if (availableModes.contains(QString::fromLatin1(authCommand(LoginJob::Plain)))) {
188
        m_actualAuthMode = LoginJob::Plain;
Daniel Vrátil's avatar
Daniel Vrátil committed
189
190
191
192
    } else {
        qCWarning(KSMTP_LOG) << "LoginJob: Couldn't choose an authentication method. Please retry with : " << availableModes;
        q->setError(KJob::UserDefinedError);
        q->setErrorText(i18n("Could not authenticate to the SMTP server because no matching authentication method has been found"));
193
        return false;
Daniel Vrátil's avatar
Daniel Vrátil committed
194
    }
195
196

    return true;
Gregory Schlomoff's avatar
Gregory Schlomoff committed
197
198
}

199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
bool LoginJobPrivate::sasl_init()
{
    if (sasl_client_init(nullptr) != SASL_OK) {
        qCWarning(KSMTP_LOG) << "Failed to initialize SASL";
        return false;
    }
    return true;
}

bool LoginJobPrivate::sasl_interact()
{
    sasl_interact_t *interact = m_saslClient;

    while (interact->id != SASL_CB_LIST_END) {
        qCDebug(KSMTP_LOG) << "SASL_INTERACT Id" << interact->id;
        switch (interact->id) {
Laurent Montel's avatar
Laurent Montel committed
215
216
        case SASL_CB_AUTHNAME: {
            // case SASL_CB_USER:
217
            qCDebug(KSMTP_LOG) << "SASL_CB_[USER|AUTHNAME]: '" << m_userName << "'";
218
219
220
            const auto username = m_userName.toUtf8();
            interact->result = strdup(username.constData());
            interact->len = username.size();
221
            break;
222
        }
Laurent Montel's avatar
Laurent Montel committed
223
        case SASL_CB_PASS: {
224
            qCDebug(KSMTP_LOG) << "SASL_CB_PASS: [hidden]";
225
226
227
            const auto pass = m_password.toUtf8();
            interact->result = strdup(pass.constData());
            interact->len = pass.size();
228
            break;
229
        }
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
        default:
            interact->result = nullptr;
            interact->len = 0;
            break;
        }
        ++interact;
    }

    return true;
}

bool LoginJobPrivate::sasl_challenge(const QByteArray &challenge)
{
    int result = -1;
    const char *out = nullptr;
    uint outLen = 0;

247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
    if (m_actualAuthMode == LoginJob::XOAuth2) {
        QJsonDocument doc = QJsonDocument::fromJson(challenge);
        if (!doc.isNull() && doc.isObject()) {
            const auto obj = doc.object();
            if (obj.value(QLatin1String("status")).toString() == QLatin1String("400")) {
                q->setError(LoginJob::TokenExpired);
                q->setErrorText(i18n("Token expired"));
                // https://developers.google.com/gmail/imap/xoauth2-protocol#error_response_2
                // "The client sends an empty response ("\r\n") to the challenge containing the error message."
                q->sendCommand("");
                return false;
            }
        }
    }

Laurent Montel's avatar
Laurent Montel committed
262
    for (;;) {
Laurent Montel's avatar
Laurent Montel committed
263
        result = sasl_client_step(m_saslConn, challenge.isEmpty() ? nullptr : challenge.constData(), challenge.size(), &m_saslClient, &out, &outLen);
264
        if (result == SASL_INTERACT) {
Laurent Montel's avatar
Laurent Montel committed
265
            if (!sasl_interact()) {
266
267
268
269
                q->setError(LoginJob::UserDefinedError);
                sasl_dispose(&m_saslConn);
                return false;
            }
Laurent Montel's avatar
Laurent Montel committed
270
        } else {
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
            break;
        }
    }

    if (result != SASL_OK && result != SASL_CONTINUE) {
        const QString saslError = QString::fromUtf8(sasl_errdetail(m_saslConn));
        qCWarning(KSMTP_LOG) << "sasl_client_step failed: " << result << saslError;
        q->setError(LoginJob::UserDefinedError);
        q->setErrorText(saslError);
        sasl_dispose(&m_saslConn);
        return false;
    }

    q->sendCommand(QByteArray::fromRawData(out, outLen).toBase64());

    return true;
}

289
bool LoginJobPrivate::authenticate()
Gregory Schlomoff's avatar
Gregory Schlomoff committed
290
{
291
292
293
294
    if (!selectAuthentication()) {
        return false;
    }

295
296
297
    if (!sasl_init()) {
        q->setError(LoginJob::UserDefinedError);
        q->setErrorText(i18n("Login failed, cannot initialize the SASL library"));
298
        return false;
299
    }
Daniel Vrátil's avatar
Daniel Vrátil committed
300

Laurent Montel's avatar
Laurent Montel committed
301
    int result = sasl_client_new("smtp", m_session->hostName().toUtf8().constData(), nullptr, nullptr, callbacks, 0, &m_saslConn);
302
303
304
305
    if (result != SASL_OK) {
        const auto saslError = QString::fromUtf8(sasl_errdetail(m_saslConn));
        q->setError(LoginJob::UserDefinedError);
        q->setErrorText(saslError);
306
        return false;
307
308
309
310
311
312
    }

    uint outLen = 0;
    const char *out = nullptr;
    const char *actualMech = nullptr;
    const auto authMode = authCommand(m_actualAuthMode);
313

Laurent Montel's avatar
Laurent Montel committed
314
    for (;;) {
315
        qCDebug(KSMTP_LOG) << "Trying authmod" << authMode;
Laurent Montel's avatar
Laurent Montel committed
316
        result = sasl_client_start(m_saslConn, authMode.constData(), &m_saslClient, &out, &outLen, &actualMech);
317
318
319
320
        if (result == SASL_INTERACT) {
            if (!sasl_interact()) {
                sasl_dispose(&m_saslConn);
                q->setError(LoginJob::UserDefinedError);
321
                return false;
322
323
324
325
326
327
328
329
330
331
332
333
334
335
            }
        } else {
            break;
        }
    }

    m_actualAuthMode = authModeFromCommand(actualMech);

    if (result != SASL_CONTINUE && result != SASL_OK) {
        const auto saslError = QString::fromUtf8(sasl_errdetail(m_saslConn));
        qCWarning(KSMTP_LOG) << "sasl_client_start failed with:" << result << saslError;
        q->setError(LoginJob::UserDefinedError);
        q->setErrorText(saslError);
        sasl_dispose(&m_saslConn);
336
        return false;
337
338
339
340
341
342
343
    }

    if (outLen == 0) {
        q->sendCommand("AUTH " + authMode);
    } else {
        q->sendCommand("AUTH " + authMode + ' ' + QByteArray::fromRawData(out, outLen).toBase64());
    }
344
345

    return true;
346
347
348
349
350
351
352
353
354
355
}

LoginJob::AuthMode LoginJobPrivate::authModeFromCommand(const QByteArray &mech) const
{
    if (qstrnicmp(mech.constData(), "PLAIN", 5) == 0) {
        return LoginJob::Plain;
    } else if (qstrnicmp(mech.constData(), "LOGIN", 5) == 0) {
        return LoginJob::Login;
    } else if (qstrnicmp(mech.constData(), "CRAM-MD5", 8) == 0) {
        return LoginJob::CramMD5;
356
357
    } else if (qstrnicmp(mech.constData(), "DIGEST-MD5", 10) == 0) {
        return LoginJob::DigestMD5;
358
359
360
361
362
363
    } else if (qstrnicmp(mech.constData(), "GSSAPI", 6) == 0) {
        return LoginJob::GSSAPI;
    } else if (qstrnicmp(mech.constData(), "NTLM", 4) == 0) {
        return LoginJob::NTLM;
    } else if (qstrnicmp(mech.constData(), "ANONYMOUS", 9) == 0) {
        return LoginJob::Anonymous;
Daniel Vrátil's avatar
Daniel Vrátil committed
364
365
    } else if (qstrnicmp(mech.constData(), "XOAUTH2", 7) == 0) {
        return LoginJob::XOAuth2;
366
367
    } else {
        return LoginJob::UnknownAuth;
Daniel Vrátil's avatar
Daniel Vrátil committed
368
    }
Gregory Schlomoff's avatar
Gregory Schlomoff committed
369
370
}

371
QByteArray LoginJobPrivate::authCommand(LoginJob::AuthMode mode) const
Gregory Schlomoff's avatar
Gregory Schlomoff committed
372
{
Daniel Vrátil's avatar
Daniel Vrátil committed
373
374
    switch (mode) {
    case LoginJob::Plain:
375
        return QByteArrayLiteral("PLAIN");
Daniel Vrátil's avatar
Daniel Vrátil committed
376
    case LoginJob::Login:
377
        return QByteArrayLiteral("LOGIN");
Daniel Vrátil's avatar
Daniel Vrátil committed
378
    case LoginJob::CramMD5:
379
        return QByteArrayLiteral("CRAM-MD5");
380
381
    case LoginJob::DigestMD5:
        return QByteArrayLiteral("DIGEST-MD5");
382
383
384
385
386
387
    case LoginJob::GSSAPI:
        return QByteArrayLiteral("GSSAPI");
    case LoginJob::NTLM:
        return QByteArrayLiteral("NTLM");
    case LoginJob::Anonymous:
        return QByteArrayLiteral("ANONYMOUS");
Daniel Vrátil's avatar
Daniel Vrátil committed
388
389
    case LoginJob::XOAuth2:
        return QByteArrayLiteral("XOAUTH2");
Daniel Vrátil's avatar
Daniel Vrátil committed
390
391
392
    case LoginJob::UnknownAuth:
        return ""; // Should not happen
    }
Laurent Montel's avatar
Laurent Montel committed
393
    return {};
Gregory Schlomoff's avatar
Gregory Schlomoff committed
394
}