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"