aboutsummaryrefslogtreecommitdiff
path: root/lib/ssosession.cpp
blob: 5f3479b812f00d6b23873f14e1906b78bfdcff2e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// SPDX-FileCopyrightText: 2020 Kitsune Ral <kitsune-ral@users.sf.net>
// SPDX-License-Identifier: LGPL-2.1-or-later

#include "ssosession.h"

#include "connection.h"
#include "csapi/sso_login_redirect.h"

#include <QtNetwork/QTcpServer>
#include <QtNetwork/QTcpSocket>
#include <QtCore/QCoreApplication>
#include <QtCore/QStringBuilder>

using namespace Quotient;

class SsoSession::Private {
public:
    Private(SsoSession* q, const QString& initialDeviceName = {},
            const QString& deviceId = {}, Connection* connection = nullptr)
        : initialDeviceName(initialDeviceName)
        , deviceId(deviceId)
        , connection(connection)
    {
        auto* server = new QTcpServer(q);
        server->listen();
        // The "/returnToApplication" part is just a hint for the end-user,
        // the callback will work without it equally well.
        callbackUrl = QStringLiteral("http://localhost:%1/returnToApplication")
                          .arg(server->serverPort());
        ssoUrl = connection->getUrlForApi<RedirectToSSOJob>(callbackUrl);

        QObject::connect(server, &QTcpServer::newConnection, q, [this, server] {
            qCDebug(MAIN) << "SSO callback initiated";
            socket = server->nextPendingConnection();
            server->close();
            QObject::connect(socket, &QTcpSocket::readyRead, socket, [this] {
                requestData.append(socket->readAll());
                if (!socket->atEnd() && !requestData.endsWith("\r\n\r\n")) {
                    qDebug(MAIN) << "Incomplete request, waiting for more data";
                    return;
                }
                processCallback();
            });
            QObject::connect(socket, &QTcpSocket::disconnected, socket,
                             &QTcpSocket::deleteLater);
        });
    }
    void processCallback();
    void sendHttpResponse(const QByteArray& code, const QByteArray& msg);
    void onError(const QByteArray& code, const QString& errorMsg);

    QString initialDeviceName;
    QString deviceId;
    Connection* connection;
    QString callbackUrl {};
    QUrl ssoUrl {};
    QTcpSocket* socket = nullptr;
    QByteArray requestData {};
};

SsoSession::SsoSession(Connection* connection, const QString& initialDeviceName,
                       const QString& deviceId)
    : QObject(connection)
    , d(makeImpl<Private>(this, initialDeviceName, deviceId, connection))
{
    qCDebug(MAIN) << "SSO session constructed";
}

SsoSession::~SsoSession()
{
    qCDebug(MAIN) << "SSO session deconstructed";
}

QUrl SsoSession::ssoUrl() const { return d->ssoUrl; }

QUrl SsoSession::callbackUrl() const { return QUrl(d->callbackUrl); }

void SsoSession::Private::processCallback()
{
    // https://matrix.org/docs/guides/sso-for-client-developers
    // Inspired by Clementine's src/internet/core/localredirectserver.cpp
    // (see at https://github.com/clementine-player/Clementine/)
    const auto& requestParts = requestData.split(' ');
    if (requestParts.size() < 2 || requestParts[1].isEmpty()) {
        onError("400 Bad Request", tr("No login token in SSO callback"));
        return;
    }
    const auto& QueryItemName = QStringLiteral("loginToken");
    QUrlQuery query { QUrl(requestParts[1]).query() };
    if (!query.hasQueryItem(QueryItemName)) {
        onError("400 Bad Request", tr("Malformed single sign-on callback"));
    }
    qCDebug(MAIN) << "Found the token in SSO callback, logging in";
    connection->loginWithToken(query.queryItemValue(QueryItemName).toLatin1(),
                               initialDeviceName, deviceId);
    connect(connection, &Connection::connected, socket, [this] {
        const QString msg =
            "The application '" % QCoreApplication::applicationName()
            % "' has successfully logged in as a user " % connection->userId()
            % " with device id " % connection->deviceId()
            % ". This window can be closed. Thank you.\r\n";
        sendHttpResponse("200 OK", msg.toHtmlEscaped().toUtf8());
        socket->disconnectFromHost();
    });
    connect(connection, &Connection::loginError, socket, [this] {
        onError("401 Unauthorised", tr("Login failed"));
        socket->disconnectFromHost();
    });
}

void SsoSession::Private::sendHttpResponse(const QByteArray& code,
                                           const QByteArray& msg)
{
    socket->write("HTTP/1.0 ");
    socket->write(code);
    socket->write("\r\n"
                  "Content-type: text/html;charset=UTF-8\r\n"
                  "\r\n\r\n");
    socket->write(msg);
    socket->write("\r\n");
}

void SsoSession::Private::onError(const QByteArray& code,
                                  const QString& errorMsg)
{
    qCWarning(MAIN).nospace() << errorMsg;
    sendHttpResponse(code, "<h3>" + errorMsg.toUtf8() + "</h3>");
    // [kitsune] Yeah, I know, dirty. Maybe the "right" way would be to have
    // an intermediate signal but that seems just a fight for purity.
    emit connection->loginError(errorMsg, requestData);
}