File indexing completed on 2025-03-09 04:52:25

0001 /*
0002  * This file is part of LibKGAPI
0003  *
0004  * SPDX-FileCopyrightText: 2020 Daniel Vrátil <dvratil@kde.org>
0005  *
0006  * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0007  */
0008 
0009 #include "account.h"
0010 #include "accountinfo/accountinfo.h"
0011 #include "accountinfo/accountinfofetchjob.h"
0012 #include "debug.h"
0013 #include "fullauthenticationjob_p.h"
0014 #include "newtokensfetchjob_p.h"
0015 
0016 #include <QAbstractSocket>
0017 #include <QDateTime>
0018 #include <QDesktopServices>
0019 #include <QTcpServer>
0020 #include <QTcpSocket>
0021 #include <QUrl>
0022 #include <QUrlQuery>
0023 #include <memory>
0024 
0025 using namespace KGAPI2;
0026 
0027 namespace KGAPI2
0028 {
0029 
0030 class Q_DECL_HIDDEN FullAuthenticationJob::Private
0031 {
0032 public:
0033     Private(const AccountPtr &account, const QString &apiKey, const QString &secretKey, FullAuthenticationJob *qq)
0034         : mAccount(account)
0035         , mApiKey(apiKey)
0036         , mSecretKey(secretKey)
0037         , q(qq)
0038     {
0039     }
0040 
0041     void emitError(Error error, const QString &text)
0042     {
0043         q->setError(error);
0044         q->setErrorString(text);
0045         q->emitFinished();
0046     }
0047 
0048     void socketError(QAbstractSocket::SocketError error)
0049     {
0050         if (mConnection) {
0051             mConnection->deleteLater();
0052         }
0053         qCDebug(KGAPIDebug) << "Socket error when receiving response:" << error;
0054         emitError(InvalidResponse, tr("Error receiving response: %1").arg(error));
0055     }
0056 
0057     void socketReady()
0058     {
0059         Q_ASSERT(mConnection);
0060         const QByteArray data = mConnection->readLine();
0061         const QString title = tr("Authentication successful");
0062         const QString text = tr("You can close this tab and return to the application now.");
0063         mConnection->write("HTTP/1.1 200 OK\n"
0064                            "Content-Type: text/html\n"
0065                            "\n"
0066                            "<!DOCTYPE><html>"
0067                            "<head><meta charset=\"UTF-8\"><title>" + title.toUtf8() + "</title></head>"
0068                            "<body><h1>" + text.toUtf8() + "</h1></body>"
0069                            "</html>\n");
0070         mConnection->flush();
0071         mConnection->deleteLater();
0072         qCDebug(KGAPIDebug) << "Got connection on socket";
0073 
0074         const auto line = data.split(' ');
0075         if (line.size() != 3 || line.at(0) != QByteArray("GET") || !line.at(2).startsWith(QByteArray("HTTP/1.1"))) {
0076             qCDebug(KGAPIDebug) << "Token response invalid";
0077             emitError(InvalidResponse, tr("Token response invalid"));
0078             return;
0079         }
0080 
0081         // qCDebug(KGAPIDebug) << "Receiving data on socket: " << data;
0082         const QUrl url(QString::fromLatin1(line.at(1)));
0083         const QUrlQuery query(url);
0084         const QString code = query.queryItemValue(QStringLiteral("code"));
0085         if (code.isEmpty()) {
0086             const QString error = query.queryItemValue(QStringLiteral("error"));
0087             if (!error.isEmpty()) {
0088                 qCDebug(KGAPIDebug) << "Google has returned an error response:" << error;
0089                 emitError(UnknownError, error);
0090             } else {
0091                 qCDebug(KGAPIDebug) << "Could not extract token from HTTP answer";
0092                 emitError(InvalidAccount, tr("Could not extract token from HTTP answer"));
0093             }
0094             return;
0095         }
0096 
0097         auto fetch = new KGAPI2::NewTokensFetchJob(code, mApiKey, mSecretKey, mServerPort);
0098         q->connect(fetch, &Job::finished, q, [this](Job *job) {
0099             tokensReceived(job);
0100         });
0101     }
0102 
0103     void tokensReceived(Job *job)
0104     {
0105         auto tokensFetchJob = qobject_cast<NewTokensFetchJob *>(job);
0106         if (tokensFetchJob->error()) {
0107             qCDebug(KGAPIDebug) << "Error when retrieving tokens:" << job->errorString();
0108             emitError(static_cast<Error>(job->error()), job->errorString());
0109             return;
0110         }
0111 
0112         mAccount->setAccessToken(tokensFetchJob->accessToken());
0113         mAccount->setRefreshToken(tokensFetchJob->refreshToken());
0114         mAccount->setExpireDateTime(QDateTime::currentDateTime().addSecs(tokensFetchJob->expiresIn()));
0115         tokensFetchJob->deleteLater();
0116 
0117         auto fetchJob = new KGAPI2::AccountInfoFetchJob(mAccount, q);
0118         q->connect(fetchJob, &Job::finished, q, [this](Job *job) {
0119             accountInfoReceived(job);
0120         });
0121         qCDebug(KGAPIDebug) << "Requesting AccountInfo";
0122     }
0123 
0124     void accountInfoReceived(Job *job)
0125     {
0126         if (job->error()) {
0127             qCDebug(KGAPIDebug) << "Error when retrieving AccountInfo:" << job->errorString();
0128             emitError(static_cast<Error>(job->error()), job->errorString());
0129             return;
0130         }
0131 
0132         const auto objects = qobject_cast<AccountInfoFetchJob *>(job)->items();
0133         Q_ASSERT(!objects.isEmpty());
0134 
0135         const auto accountInfo = objects.first().staticCast<AccountInfo>();
0136         mAccount->setAccountName(accountInfo->email());
0137 
0138         job->deleteLater();
0139 
0140         q->emitFinished();
0141     }
0142 
0143 public:
0144     AccountPtr mAccount;
0145     QString mApiKey;
0146     QString mSecretKey;
0147     QString mUsername;
0148 
0149     std::unique_ptr<QTcpServer> mServer;
0150     QTcpSocket *mConnection = nullptr;
0151     uint16_t mServerPort = 0;
0152 
0153 private:
0154     FullAuthenticationJob *const q;
0155 };
0156 
0157 } // namespace KGAPI2
0158 
0159 FullAuthenticationJob::FullAuthenticationJob(const AccountPtr &account, const QString &apiKey, const QString &secretKey, QObject *parent)
0160     : Job(parent)
0161     , d(new Private(account, apiKey, secretKey, this))
0162 {
0163 }
0164 
0165 FullAuthenticationJob::~FullAuthenticationJob() = default;
0166 
0167 void FullAuthenticationJob::setServerPort(uint16_t port)
0168 {
0169     d->mServerPort = port;
0170 }
0171 
0172 void FullAuthenticationJob::setUsername(const QString &username)
0173 {
0174     d->mUsername = username;
0175 }
0176 
0177 AccountPtr FullAuthenticationJob::account() const
0178 {
0179     return d->mAccount;
0180 }
0181 
0182 void FullAuthenticationJob::start()
0183 {
0184     if (d->mAccount.isNull()) {
0185         d->emitError(InvalidAccount, tr("Invalid account"));
0186         return;
0187     }
0188     if (d->mAccount->scopes().isEmpty()) {
0189         d->emitError(InvalidAccount, tr("No scopes to authenticate for"));
0190         return;
0191     }
0192 
0193     QStringList scopes;
0194     scopes.reserve(d->mAccount->scopes().size());
0195     const auto scopesList = d->mAccount->scopes();
0196     for (const QUrl &scope : scopesList) {
0197         scopes << scope.toString();
0198     }
0199 
0200     d->mServer = std::make_unique<QTcpServer>();
0201     if (!d->mServer->listen(QHostAddress::LocalHost, d->mServerPort)) {
0202         d->emitError(InvalidAccount, tr("Could not start OAuth HTTP server"));
0203         return;
0204     }
0205     d->mServerPort = d->mServer->serverPort();
0206     connect(d->mServer.get(), &QTcpServer::acceptError, this, [this](QAbstractSocket::SocketError e) {
0207         d->socketError(e);
0208     });
0209     connect(d->mServer.get(), &QTcpServer::newConnection, this, [this]() {
0210         d->mConnection = d->mServer->nextPendingConnection();
0211         d->mConnection->setParent(this);
0212         connect(d->mConnection,
0213                 static_cast<void (QAbstractSocket::*)(QAbstractSocket::SocketError)>(&QAbstractSocket::errorOccurred),
0214                 this,
0215                 [this](QAbstractSocket::SocketError e) {
0216                     d->socketError(e);
0217                 });
0218         connect(d->mConnection, &QTcpSocket::readyRead, this, [this]() {
0219             d->socketReady();
0220         });
0221         d->mServer->close();
0222     });
0223 
0224     QUrl url(QStringLiteral("https://accounts.google.com/o/oauth2/auth"));
0225     QUrlQuery query(url);
0226     query.addQueryItem(QStringLiteral("client_id"), d->mApiKey);
0227     query.addQueryItem(QStringLiteral("redirect_uri"), QStringLiteral("http://127.0.0.1:%1").arg(d->mServerPort));
0228     query.addQueryItem(QStringLiteral("scope"), scopes.join(QLatin1Char(' ')));
0229     query.addQueryItem(QStringLiteral("response_type"), QStringLiteral("code"));
0230     if (!d->mUsername.isEmpty()) {
0231         query.addQueryItem(QStringLiteral("login_hint"), d->mUsername);
0232     }
0233     url.setQuery(query);
0234 
0235     QDesktopServices::openUrl(url);
0236 }
0237 
0238 void FullAuthenticationJob::handleReply(const QNetworkReply * /*reply*/, const QByteArray & /*rawData*/)
0239 {
0240     // This is never supposed to be called.
0241     Q_UNREACHABLE();
0242 }
0243 
0244 void FullAuthenticationJob::dispatchRequest(QNetworkAccessManager * /*accessManager*/,
0245                                             const QNetworkRequest & /*request*/,
0246                                             const QByteArray & /*data*/,
0247                                             const QString & /*contentType*/)
0248 {
0249     // This is never supposed to be called.
0250     Q_UNREACHABLE();
0251 }
0252 
0253 #include "moc_fullauthenticationjob_p.cpp"