aboutsummaryrefslogtreecommitdiff
path: root/lib/ssosession.cpp
blob: 0f8f96e1abec2bb800e88b6f8896b2722cdbaf6f (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
#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;

struct SsoSession::Private {
    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,
                             [this] { socket->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(std::make_unique<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 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);
}