Kgapi

fullauthenticationjob.cpp
1/*
2 * This file is part of LibKGAPI
3 *
4 * SPDX-FileCopyrightText: 2020 Daniel Vrátil <dvratil@kde.org>
5 *
6 * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
7 */
8
9#include "account.h"
10#include "accountinfo/accountinfo.h"
11#include "accountinfo/accountinfofetchjob.h"
12#include "debug.h"
13#include "fullauthenticationjob_p.h"
14#include "newtokensfetchjob_p.h"
15
16#include <QAbstractSocket>
17#include <QDateTime>
18#include <QDesktopServices>
19#include <QTcpServer>
20#include <QTcpSocket>
21#include <QUrl>
22#include <QUrlQuery>
23#include <memory>
24
25using namespace KGAPI2;
26
27namespace KGAPI2
28{
29
30class Q_DECL_HIDDEN FullAuthenticationJob::Private
31{
32public:
33 Private(const AccountPtr &account, const QString &apiKey, const QString &secretKey, FullAuthenticationJob *qq)
34 : mAccount(account)
35 , mApiKey(apiKey)
36 , mSecretKey(secretKey)
37 , q(qq)
38 {
39 }
40
41 void emitError(Error error, const QString &text)
42 {
43 q->setError(error);
44 q->setErrorString(text);
45 q->emitFinished();
46 }
47
48 void socketError(QAbstractSocket::SocketError error)
49 {
50 if (mConnection) {
51 mConnection->deleteLater();
52 }
53 qCDebug(KGAPIDebug) << "Socket error when receiving response:" << error;
54 emitError(InvalidResponse, tr("Error receiving response: %1").arg(error));
55 }
56
57 void socketReady()
58 {
59 Q_ASSERT(mConnection);
60 const QByteArray data = mConnection->readLine();
61 const QString title = tr("Authentication successful");
62 const QString text = tr("You can close this tab and return to the application now.");
63 mConnection->write("HTTP/1.1 200 OK\n"
64 "Content-Type: text/html\n"
65 "\n"
66 "<!DOCTYPE><html>"
67 "<head><meta charset=\"UTF-8\"><title>" + title.toUtf8() + "</title></head>"
68 "<body><h1>" + text.toUtf8() + "</h1></body>"
69 "</html>\n");
70 mConnection->flush();
71 mConnection->deleteLater();
72 qCDebug(KGAPIDebug) << "Got connection on socket";
73
74 const auto line = data.split(' ');
75 if (line.size() != 3 || line.at(0) != QByteArray("GET") || !line.at(2).startsWith(QByteArray("HTTP/1.1"))) {
76 qCDebug(KGAPIDebug) << "Token response invalid";
77 emitError(InvalidResponse, tr("Token response invalid"));
78 return;
79 }
80
81 // qCDebug(KGAPIDebug) << "Receiving data on socket: " << data;
82 const QUrl url(QString::fromLatin1(line.at(1)));
83 const QUrlQuery query(url);
84 const QString code = query.queryItemValue(QStringLiteral("code"));
85 if (code.isEmpty()) {
86 const QString error = query.queryItemValue(QStringLiteral("error"));
87 if (!error.isEmpty()) {
88 qCDebug(KGAPIDebug) << "Google has returned an error response:" << error;
89 emitError(UnknownError, error);
90 } else {
91 qCDebug(KGAPIDebug) << "Could not extract token from HTTP answer";
92 emitError(InvalidAccount, tr("Could not extract token from HTTP answer"));
93 }
94 return;
95 }
96
97 auto fetch = new KGAPI2::NewTokensFetchJob(code, mApiKey, mSecretKey, mServerPort);
98 q->connect(fetch, &Job::finished, q, [this](Job *job) {
99 tokensReceived(job);
100 });
101 }
102
103 void tokensReceived(Job *job)
104 {
105 auto tokensFetchJob = qobject_cast<NewTokensFetchJob *>(job);
106 if (tokensFetchJob->error()) {
107 qCDebug(KGAPIDebug) << "Error when retrieving tokens:" << job->errorString();
108 emitError(static_cast<Error>(job->error()), job->errorString());
109 return;
110 }
111
112 mAccount->setAccessToken(tokensFetchJob->accessToken());
113 mAccount->setRefreshToken(tokensFetchJob->refreshToken());
114 mAccount->setExpireDateTime(QDateTime::currentDateTime().addSecs(tokensFetchJob->expiresIn()));
115 tokensFetchJob->deleteLater();
116
117 auto fetchJob = new KGAPI2::AccountInfoFetchJob(mAccount, q);
118 q->connect(fetchJob, &Job::finished, q, [this](Job *job) {
119 accountInfoReceived(job);
120 });
121 qCDebug(KGAPIDebug) << "Requesting AccountInfo";
122 }
123
124 void accountInfoReceived(Job *job)
125 {
126 if (job->error()) {
127 qCDebug(KGAPIDebug) << "Error when retrieving AccountInfo:" << job->errorString();
128 emitError(static_cast<Error>(job->error()), job->errorString());
129 return;
130 }
131
132 const auto objects = qobject_cast<AccountInfoFetchJob *>(job)->items();
133 Q_ASSERT(!objects.isEmpty());
134
135 const auto accountInfo = objects.first().staticCast<AccountInfo>();
136 mAccount->setAccountName(accountInfo->email());
137
138 job->deleteLater();
139
140 q->emitFinished();
141 }
142
143public:
144 AccountPtr mAccount;
145 QString mApiKey;
146 QString mSecretKey;
147 QString mUsername;
148
149 std::unique_ptr<QTcpServer> mServer;
150 QTcpSocket *mConnection = nullptr;
151 uint16_t mServerPort = 0;
152
153private:
154 FullAuthenticationJob *const q;
155};
156
157} // namespace KGAPI2
158
159FullAuthenticationJob::FullAuthenticationJob(const AccountPtr &account, const QString &apiKey, const QString &secretKey, QObject *parent)
160 : Job(parent)
161 , d(new Private(account, apiKey, secretKey, this))
162{
163}
164
165FullAuthenticationJob::~FullAuthenticationJob() = default;
166
167void FullAuthenticationJob::setServerPort(uint16_t port)
168{
169 d->mServerPort = port;
170}
171
172void FullAuthenticationJob::setUsername(const QString &username)
173{
174 d->mUsername = username;
175}
176
177AccountPtr FullAuthenticationJob::account() const
178{
179 return d->mAccount;
180}
181
182void FullAuthenticationJob::start()
183{
184 if (d->mAccount.isNull()) {
185 d->emitError(InvalidAccount, tr("Invalid account"));
186 return;
187 }
188 if (d->mAccount->scopes().isEmpty()) {
189 d->emitError(InvalidAccount, tr("No scopes to authenticate for"));
190 return;
191 }
192
193 QStringList scopes;
194 scopes.reserve(d->mAccount->scopes().size());
195 const auto scopesList = d->mAccount->scopes();
196 for (const QUrl &scope : scopesList) {
197 scopes << scope.toString();
198 }
199
200 d->mServer = std::make_unique<QTcpServer>();
201 if (!d->mServer->listen(QHostAddress::LocalHost, d->mServerPort)) {
202 d->emitError(InvalidAccount, tr("Could not start OAuth HTTP server"));
203 return;
204 }
205 d->mServerPort = d->mServer->serverPort();
206 connect(d->mServer.get(), &QTcpServer::acceptError, this, [this](QAbstractSocket::SocketError e) {
207 d->socketError(e);
208 });
209 connect(d->mServer.get(), &QTcpServer::newConnection, this, [this]() {
210 d->mConnection = d->mServer->nextPendingConnection();
211 d->mConnection->setParent(this);
212 connect(d->mConnection,
213 static_cast<void (QAbstractSocket::*)(QAbstractSocket::SocketError)>(&QAbstractSocket::errorOccurred),
214 this,
215 [this](QAbstractSocket::SocketError e) {
216 d->socketError(e);
217 });
218 connect(d->mConnection, &QTcpSocket::readyRead, this, [this]() {
219 d->socketReady();
220 });
221 d->mServer->close();
222 });
223
224 QUrl url(QStringLiteral("https://accounts.google.com/o/oauth2/auth"));
225 QUrlQuery query(url);
226 query.addQueryItem(QStringLiteral("client_id"), d->mApiKey);
227 query.addQueryItem(QStringLiteral("redirect_uri"), QStringLiteral("http://127.0.0.1:%1").arg(d->mServerPort));
228 query.addQueryItem(QStringLiteral("scope"), scopes.join(QLatin1Char(' ')));
229 query.addQueryItem(QStringLiteral("response_type"), QStringLiteral("code"));
230 if (!d->mUsername.isEmpty()) {
231 query.addQueryItem(QStringLiteral("login_hint"), d->mUsername);
232 }
233 url.setQuery(query);
234
236}
237
238void FullAuthenticationJob::handleReply(const QNetworkReply * /*reply*/, const QByteArray & /*rawData*/)
239{
240 // This is never supposed to be called.
241 Q_UNREACHABLE();
242}
243
244void FullAuthenticationJob::dispatchRequest(QNetworkAccessManager * /*accessManager*/,
245 const QNetworkRequest & /*request*/,
246 const QByteArray & /*data*/,
247 const QString & /*contentType*/)
248{
249 // This is never supposed to be called.
250 Q_UNREACHABLE();
251}
252
253#include "moc_fullauthenticationjob_p.cpp"
A job to fetch AccountInfo.
AccountInfo contains information about user's Google account.
Definition accountinfo.h:32
Abstract base class for all jobs in LibKGAPI.
Definition job.h:41
KGAPI2::Error error() const
Error code.
Definition job.cpp:391
void finished(KGAPI2::Job *job)
Emitted when job has finished.
QString errorString() const
Error string.
Definition job.cpp:406
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
A job to fetch a single map tile described by a StaticMapUrl.
Definition blog.h:16
Error
Job error codes.
Definition types.h:176
@ UnknownError
LibKGAPI error - a general unidentified error.
Definition types.h:179
@ InvalidAccount
LibKGAPI error - the KGAPI2::Account object is invalid.
Definition types.h:185
@ InvalidResponse
LibKGAPI error - Google returned invalid response.
Definition types.h:183
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
char at(qsizetype i) const const
QList< QByteArray > split(char sep) const const
QDateTime currentDateTime()
bool openUrl(const QUrl &url)
void readyRead()
void reserve(qsizetype size)
void deleteLater()
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
QByteArray toUtf8() const const
QString join(QChar separator) const const
void acceptError(QAbstractSocket::SocketError socketError)
void newConnection()
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:19:51 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.