sessionthread.cpp 11.2 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
    Copyright (c) 2009 Kevin Ottens <ervin@kde.org>

    This library is free software; you can redistribute it and/or modify it
    under the terms of the GNU Library General Public License as published by
    the Free Software Foundation; either version 2 of the License, or (at your
    option) any later version.

    This library is distributed in the hope that it will be useful, but WITHOUT
    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
    License for more details.

    You should have received a copy of the GNU Library General Public License
    along with this library; see the file COPYING.LIB.  If not, write to the
    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
    02110-1301, USA.
*/

#include "sessionthread_p.h"

22
23
#include <KSslErrorUiData>

Laurent Montel's avatar
Laurent Montel committed
24
25
#include <QDebug>
#include <QThread>
26
#include <QNetworkProxy>
27
#include <QSslCipher>
Laurent Montel's avatar
Laurent Montel committed
28
#include "kimap_debug.h"
29

30
#include "imapstreamparser.h"
31
#include "response_p.h"
32
33
34

using namespace KIMAP;

Laurent Montel's avatar
Laurent Montel committed
35
Q_DECLARE_METATYPE(KSslErrorUiData)
36
37
38

namespace {
static const int _kimap_abstractSocketError = qRegisterMetaType<QAbstractSocket::SocketError>();
39
static const int _kimap_sslErrorUiData = qRegisterMetaType<KSslErrorUiData>();
40
}
41

Laurent Montel's avatar
Laurent Montel committed
42
43
SessionThread::SessionThread(const QString &hostName, quint16 port)
    : QObject(), m_hostName(hostName), m_port(port),
44
45
      m_encryptedMode(false),
      m_useProxy(false)
46
{
Laurent Montel's avatar
Laurent Montel committed
47
48
49
50
51
    // Just like the Qt docs now recommend, for event-driven threads:
    // don't derive from QThread, create one directly and move the object to it.
    QThread *thread = new QThread();
    moveToThread(thread);
    thread->start();
Laurent Montel's avatar
Laurent Montel committed
52
    QMetaObject::invokeMethod(this, &SessionThread::threadInit);
53
54
55
56
}

SessionThread::~SessionThread()
{
Laurent Montel's avatar
Laurent Montel committed
57
    QMetaObject::invokeMethod(this, &SessionThread::threadQuit);
Laurent Montel's avatar
Laurent Montel committed
58
    if (!thread()->wait(10 * 1000)) {
Laurent Montel's avatar
Laurent Montel committed
59
        qCWarning(KIMAP_LOG) << "Session thread refuses to die, killing harder...";
Laurent Montel's avatar
Laurent Montel committed
60
61
62
63
64
        thread()->terminate();
        // Make sure to wait until it's done, otherwise it can crash when the pthread callback is called
        thread()->wait();
    }
    delete thread();
65
66
}

67
68
69
70
71
72
// Called in primary thread, passes setting to secondary thread
void SessionThread::setUseNetworkProxy(bool useProxy)
{
    QMetaObject::invokeMethod(this, [this, useProxy]() { setUseProxyInternal(useProxy); }, Qt::QueuedConnection);
}

Laurent Montel's avatar
Laurent Montel committed
73
// Called in primary thread
Laurent Montel's avatar
Laurent Montel committed
74
void SessionThread::sendData(const QByteArray &payload)
75
{
Laurent Montel's avatar
Laurent Montel committed
76
    QMutexLocker locker(&m_mutex);
77

Laurent Montel's avatar
Laurent Montel committed
78
    m_dataQueue.enqueue(payload);
Laurent Montel's avatar
Laurent Montel committed
79
    QMetaObject::invokeMethod(this, &SessionThread::writeDataQueue);
80
81
}

Laurent Montel's avatar
Laurent Montel committed
82
// Called in secondary thread
83
void SessionThread::writeDataQueue()
84
{
Laurent Montel's avatar
Laurent Montel committed
85
86
87
88
89
90
91
92
93
    Q_ASSERT(QThread::currentThread() == thread());
    if (!m_socket) {
        return;
    }
    QMutexLocker locker(&m_mutex);

    while (!m_dataQueue.isEmpty()) {
        m_socket->write(m_dataQueue.dequeue());
    }
94
95
}

Laurent Montel's avatar
Laurent Montel committed
96
// Called in secondary thread
97
void SessionThread::readMessage()
98
{
Laurent Montel's avatar
Laurent Montel committed
99
100
    Q_ASSERT(QThread::currentThread() == thread());
    if (!m_stream || m_stream->availableDataSize() == 0) {
101
        return;
102
    }
Kevin Ottens's avatar
Kevin Ottens committed
103

104
105
    Response message;
    QList<Response::Part> *payload = &message.content;
Laurent Montel's avatar
Laurent Montel committed
106
107
108
109
110
111

    try {
        while (!m_stream->atCommandEnd()) {
            if (m_stream->hasString()) {
                QByteArray string = m_stream->readString();
                if (string == "NIL") {
112
                    *payload << Response::Part(QList<QByteArray>());
Laurent Montel's avatar
Laurent Montel committed
113
                } else {
114
                    *payload << Response::Part(string);
Laurent Montel's avatar
Laurent Montel committed
115
116
                }
            } else if (m_stream->hasList()) {
117
                *payload << Response::Part(m_stream->readParenthesizedList());
Laurent Montel's avatar
Laurent Montel committed
118
119
120
121
122
123
124
125
126
            } else if (m_stream->hasResponseCode()) {
                payload = &message.responseCode;
            } else if (m_stream->atResponseCodeEnd()) {
                payload = &message.content;
            } else if (m_stream->hasLiteral()) {
                QByteArray literal;
                while (!m_stream->atLiteralEnd()) {
                    literal += m_stream->readLiteralPart();
                }
127
                *payload << Response::Part(literal);
Laurent Montel's avatar
Laurent Montel committed
128
129
130
131
132
133
134
135
            } else {
                // Oops! Something really bad happened, we won't be able to recover
                // so close the socket immediately
                qWarning("Inconsistent state, probably due to some packet loss");
                doCloseSocket();
                return;
            }
        }
Kevin Ottens's avatar
Kevin Ottens committed
136

Laurent Montel's avatar
Laurent Montel committed
137
        emit responseReceived(message);
138

139
    } catch (const KIMAP::ImapParserException &e) {
Laurent Montel's avatar
Laurent Montel committed
140
        qCWarning(KIMAP_LOG) << "The stream parser raised an exception:" << e.what();
Laurent Montel's avatar
Laurent Montel committed
141
142
143
    }

    if (m_stream->availableDataSize() > 1) {
Laurent Montel's avatar
Laurent Montel committed
144
        QMetaObject::invokeMethod(this, &SessionThread::readMessage, Qt::QueuedConnection);
Laurent Montel's avatar
Laurent Montel committed
145
    }
146

147
148
}

Laurent Montel's avatar
Laurent Montel committed
149
// Called in main thread
150
void SessionThread::closeSocket()
151
{
Laurent Montel's avatar
Laurent Montel committed
152
    QMetaObject::invokeMethod(this, &SessionThread::doCloseSocket, Qt::QueuedConnection);
153
154
}

Laurent Montel's avatar
Laurent Montel committed
155
// Called in secondary thread
156
157
void SessionThread::doCloseSocket()
{
Laurent Montel's avatar
Laurent Montel committed
158
159
160
161
162
    Q_ASSERT(QThread::currentThread() == thread());
    if (!m_socket) {
        return;
    }
    m_encryptedMode = false;
Laurent Montel's avatar
Laurent Montel committed
163
    qCDebug(KIMAP_LOG) << "close";
Laurent Montel's avatar
Laurent Montel committed
164
    m_socket->close();
165
}
166

Laurent Montel's avatar
Laurent Montel committed
167
// Called in secondary thread
168
169
void SessionThread::reconnect()
{
Laurent Montel's avatar
Laurent Montel committed
170
    Q_ASSERT(QThread::currentThread() == thread());
Laurent Montel's avatar
Laurent Montel committed
171
    if (m_socket == nullptr) { // threadQuit already called
Laurent Montel's avatar
Laurent Montel committed
172
173
        return;
    }
174
175
    if (m_socket->state() != QSslSocket::ConnectedState &&
            m_socket->state() != QSslSocket::ConnectingState) {
176
177
178
179
180
181
182
183
184
185
186

        QNetworkProxy proxy;
        if (!m_useProxy) {
            qCDebug(KIMAP_LOG) << "Connecting to IMAP server with no proxy";
            proxy.setType(QNetworkProxy::NoProxy);
        } else {
            qCDebug(KIMAP_LOG) << "Connecting to IMAP server using default system proxy";
            proxy.setType(QNetworkProxy::DefaultProxy);
        }
        m_socket->setProxy(proxy);

Laurent Montel's avatar
Laurent Montel committed
187
        if (m_encryptedMode) {
Laurent Montel's avatar
Laurent Montel committed
188
            qCDebug(KIMAP_LOG) << "connectToHostEncrypted" << m_hostName << m_port;
Laurent Montel's avatar
Laurent Montel committed
189
190
            m_socket->connectToHostEncrypted(m_hostName, m_port);
        } else {
Laurent Montel's avatar
Laurent Montel committed
191
            qCDebug(KIMAP_LOG) << "connectToHost" << m_hostName << m_port;
Laurent Montel's avatar
Laurent Montel committed
192
193
            m_socket->connectToHost(m_hostName, m_port);
        }
194
    }
195
196
}

Laurent Montel's avatar
Laurent Montel committed
197
198
// Called in secondary thread
void SessionThread::threadInit()
199
{
Laurent Montel's avatar
Laurent Montel committed
200
    Q_ASSERT(QThread::currentThread() == thread());
201
202
203
    m_socket = std::make_unique<QSslSocket>();
    m_stream = std::make_unique<ImapStreamParser>(m_socket.get());
    connect(m_socket.get(), &QIODevice::readyRead,
204
            this, &SessionThread::readMessage, Qt::QueuedConnection);
Laurent Montel's avatar
Laurent Montel committed
205
206

    // Delay the call to slotSocketDisconnected so that it finishes disconnecting before we call reconnect()
207
    connect(m_socket.get(), &QSslSocket::disconnected,
208
            this, &SessionThread::slotSocketDisconnected, Qt::QueuedConnection);
209
    connect(m_socket.get(), &QSslSocket::connected,
210
            this, &SessionThread::socketConnected);
211
212
213
    connect(m_socket.get(), QOverload<QAbstractSocket::SocketError>::of(&QAbstractSocket::error),
            this, &SessionThread::slotSocketError);
    connect(m_socket.get(), &QIODevice::bytesWritten,
214
            this, &SessionThread::socketActivity);
215
216
217
    connect(m_socket.get(), &QSslSocket::encryptedBytesWritten,
            this, &SessionThread::socketActivity);
    connect(m_socket.get(), &QIODevice::readyRead,
218
            this, &SessionThread::socketActivity);
Laurent Montel's avatar
Laurent Montel committed
219
    QMetaObject::invokeMethod(this, &SessionThread::reconnect, Qt::QueuedConnection);
Laurent Montel's avatar
Laurent Montel committed
220
}
221

Laurent Montel's avatar
Laurent Montel committed
222
223
224
// Called in secondary thread
void SessionThread::threadQuit()
{
Laurent Montel's avatar
Laurent Montel committed
225
    Q_ASSERT(QThread::currentThread() == thread());
226
227
    m_stream.reset();
    m_socket.reset();
Laurent Montel's avatar
Laurent Montel committed
228
    thread()->quit();
229
230
}

231
232
233
234
235
// Called in secondary thread
void SessionThread::setUseProxyInternal(bool useProxy)
{
    m_useProxy = useProxy;
    if (m_socket != nullptr) {
236
        if (m_socket->state() != QSslSocket::UnconnectedState) {
237
238
239
240
241
242
            m_socket->disconnectFromHost();
            QMetaObject::invokeMethod(this, &SessionThread::reconnect, Qt::QueuedConnection);
        }
    }
}

Laurent Montel's avatar
Laurent Montel committed
243
// Called in primary thread
244
void SessionThread::startSsl(QSsl::SslProtocol protocol)
245
{
246
    QMetaObject::invokeMethod(this, [this, protocol]() { doStartSsl(protocol); });
Laurent Montel's avatar
Laurent Montel committed
247
}
248

Laurent Montel's avatar
Laurent Montel committed
249
// Called in secondary thread (via invokeMethod)
250
void SessionThread::doStartSsl(QSsl::SslProtocol protocol)
Laurent Montel's avatar
Laurent Montel committed
251
{
Laurent Montel's avatar
Laurent Montel committed
252
253
254
255
256
    Q_ASSERT(QThread::currentThread() == thread());
    if (!m_socket) {
        return;
    }

257
258
259
    m_socket->setProtocol(protocol);
    m_socket->ignoreSslErrors(); // Don't worry, errors are handled manually below
    connect(m_socket.get(), &QSslSocket::encrypted, this, &SessionThread::sslConnected);
Laurent Montel's avatar
Laurent Montel committed
260
    m_socket->startClientEncryption();
261
262
}

Laurent Montel's avatar
Laurent Montel committed
263
264
// Called in secondary thread
void SessionThread::slotSocketDisconnected()
265
{
Laurent Montel's avatar
Laurent Montel committed
266
267
    Q_ASSERT(QThread::currentThread() == thread());
    emit socketDisconnected();
268
269
}

Laurent Montel's avatar
Laurent Montel committed
270
// Called in secondary thread
271
void SessionThread::slotSocketError(QAbstractSocket::SocketError error)
272
{
Laurent Montel's avatar
Laurent Montel committed
273
274
275
276
277
    Q_ASSERT(QThread::currentThread() == thread());
    if (!m_socket) {
        return;
    }
    emit socketError(error);
278
279
}

Laurent Montel's avatar
Laurent Montel committed
280
// Called in secondary thread
281
void SessionThread::sslConnected()
282
{
Laurent Montel's avatar
Laurent Montel committed
283
284
285
286
    Q_ASSERT(QThread::currentThread() == thread());
    if (!m_socket) {
        return;
    }
287
    QSslCipher cipher = m_socket->sessionCipher();
288
289
290
291
292
293
#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
    if (!m_socket->sslErrors().isEmpty()
#else
    if (!m_socket->sslHandshakeErrors().isEmpty()
#endif
            || !m_socket->isEncrypted()
294
        || cipher.isNull() || cipher.usedBits() == 0) {
Laurent Montel's avatar
Laurent Montel committed
295
        qCDebug(KIMAP_LOG) << "Initial SSL handshake failed. cipher.isNull() is" << cipher.isNull()
Laurent Montel's avatar
Laurent Montel committed
296
297
298
                           << ", cipher.usedBits() is" << cipher.usedBits()
                           << ", the socket says:" <<  m_socket->errorString()
                           << "and the list of SSL errors contains"
299
300
301
302
303
304
                      #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
                           << m_socket->sslErrors().count()
                      #else
                           << m_socket->sslHandshakeErrors().count()
                      #endif
                           << "items.";
305
        KSslErrorUiData errorData(m_socket.get());
Laurent Montel's avatar
Laurent Montel committed
306
307
        emit sslError(errorData);
    } else {
308
        qCDebug(KIMAP_LOG) << "TLS negotiation done, the negotiated protocol is" << cipher.protocolString();
Laurent Montel's avatar
Laurent Montel committed
309
        m_encryptedMode = true;
310
        emit encryptionNegotiationResult(true, m_socket->sessionProtocol());
Laurent Montel's avatar
Laurent Montel committed
311
    }
312
313
}

314
315
void SessionThread::sslErrorHandlerResponse(bool response)
{
Laurent Montel's avatar
Laurent Montel committed
316
    QMetaObject::invokeMethod(this, [this, response]() { doSslErrorHandlerResponse(response); });
Laurent Montel's avatar
Laurent Montel committed
317
318
319
320
321
}

// Called in secondary thread (via invokeMethod)
void SessionThread::doSslErrorHandlerResponse(bool response)
{
Laurent Montel's avatar
Laurent Montel committed
322
323
324
325
326
327
    Q_ASSERT(QThread::currentThread() == thread());
    if (!m_socket) {
        return;
    }
    if (response) {
        m_encryptedMode = true;
328
        emit encryptionNegotiationResult(true, m_socket->sessionProtocol());
Laurent Montel's avatar
Laurent Montel committed
329
330
331
332
333
334
    } else {
        m_encryptedMode = false;
        //reconnect in unencrypted mode, so new commands can be issued
        m_socket->disconnectFromHost();
        m_socket->waitForDisconnected();
        m_socket->connectToHost(m_hostName, m_port);
335
        emit encryptionNegotiationResult(false, QSsl::UnknownProtocol);
Laurent Montel's avatar
Laurent Montel committed
336
    }
337
338
}

339
#include "moc_sessionthread_p.cpp"