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);
}
|