File indexing completed on 2024-12-22 04:57:53

0001 /*
0002     SPDX-License-Identifier: BSD-2-Clause
0003 */
0004 
0005 #include <QCryptographicHash>
0006 #include <QDateTime>
0007 #include <QJsonObject>
0008 #include <QList>
0009 #include <QMap>
0010 #include <QNetworkRequest>
0011 #include <QPair>
0012 #include <QVariantMap>
0013 #if QT_VERSION >= 0x050000
0014 #include <QJsonDocument>
0015 #include <QUrlQuery>
0016 #else
0017 #include <QScriptEngine>
0018 #include <QScriptValueIterator>
0019 #endif
0020 
0021 #include "debug.h"
0022 #include "o0globals.h"
0023 #include "o0settingsstore.h"
0024 #include "o2.h"
0025 #include "o2replyserver.h"
0026 
0027 /// Parse JSON data into a QVariantMap
0028 static QVariantMap parseTokenResponse(const QByteArray &data)
0029 {
0030 #if QT_VERSION >= 0x050000
0031     QJsonParseError err;
0032     QJsonDocument doc = QJsonDocument::fromJson(data, &err);
0033     if (err.error != QJsonParseError::NoError) {
0034         qCWarning(TOMBOYNOTESRESOURCE_LOG) << "parseTokenResponse: Failed to parse token response due to err:" << err.errorString();
0035         return {};
0036     }
0037 
0038     if (!doc.isObject()) {
0039         qCWarning(TOMBOYNOTESRESOURCE_LOG) << "parseTokenResponse: Token response is not an object";
0040         return {};
0041     }
0042 
0043     return doc.object().toVariantMap();
0044 #else
0045     QScriptEngine engine;
0046     QScriptValue value = engine.evaluate("(" + QString(data) + QLatin1Char(')'));
0047     QScriptValueIterator it(value);
0048     QVariantMap map;
0049 
0050     while (it.hasNext()) {
0051         it.next();
0052         map.insert(it.name(), it.value().toVariant());
0053     }
0054 
0055     return map;
0056 #endif
0057 }
0058 
0059 /// Add query parameters to a query
0060 static void addQueryParametersToUrl(QUrl &url, const QList<QPair<QString, QString>> &parameters)
0061 {
0062 #if QT_VERSION < 0x050000
0063     url.setQueryItems(parameters);
0064 #else
0065     QUrlQuery query(url);
0066     query.setQueryItems(parameters);
0067     url.setQuery(query);
0068 #endif
0069 }
0070 
0071 O2::O2(QObject *parent)
0072     : O0BaseAuth(parent)
0073 {
0074     manager_ = new QNetworkAccessManager(this);
0075     replyServer_ = new O2ReplyServer(this);
0076     grantFlow_ = GrantFlowAuthorizationCode;
0077     localhostPolicy_ = QLatin1StringView(O2_CALLBACK_URL);
0078     qRegisterMetaType<QNetworkReply::NetworkError>("QNetworkReply::NetworkError");
0079     connect(replyServer_, &O2ReplyServer::verificationReceived, this, &O2::onVerificationReceived);
0080 }
0081 
0082 O2::GrantFlow O2::grantFlow() const
0083 {
0084     return grantFlow_;
0085 }
0086 
0087 void O2::setGrantFlow(O2::GrantFlow value)
0088 {
0089     grantFlow_ = value;
0090     Q_EMIT grantFlowChanged();
0091 }
0092 
0093 QString O2::username() const
0094 {
0095     return username_;
0096 }
0097 
0098 void O2::setUsername(const QString &value)
0099 {
0100     username_ = value;
0101     Q_EMIT usernameChanged();
0102 }
0103 
0104 QString O2::password() const
0105 {
0106     return password_;
0107 }
0108 
0109 void O2::setPassword(const QString &value)
0110 {
0111     password_ = value;
0112     Q_EMIT passwordChanged();
0113 }
0114 
0115 QString O2::scope() const
0116 {
0117     return scope_;
0118 }
0119 
0120 void O2::setScope(const QString &value)
0121 {
0122     scope_ = value;
0123     Q_EMIT scopeChanged();
0124 }
0125 
0126 QString O2::requestUrl() const
0127 {
0128     return requestUrl_.toString();
0129 }
0130 
0131 void O2::setRequestUrl(const QString &value)
0132 {
0133     requestUrl_ = QUrl(value);
0134     Q_EMIT requestUrlChanged();
0135 }
0136 
0137 QString O2::tokenUrl()
0138 {
0139     return tokenUrl_.toString();
0140 }
0141 
0142 void O2::setTokenUrl(const QString &value)
0143 {
0144     tokenUrl_ = QUrl(value);
0145     Q_EMIT tokenUrlChanged();
0146 }
0147 
0148 QString O2::refreshTokenUrl()
0149 {
0150     return refreshTokenUrl_.toString();
0151 }
0152 
0153 void O2::setRefreshTokenUrl(const QString &value)
0154 {
0155     refreshTokenUrl_ = QUrl(value);
0156     Q_EMIT refreshTokenUrlChanged();
0157 }
0158 
0159 void O2::link()
0160 {
0161     qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::link";
0162 
0163     if (linked()) {
0164         qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::link: Linked already";
0165         Q_EMIT linkingSucceeded();
0166         return;
0167     }
0168 
0169     setLinked(false);
0170     setToken(QLatin1StringView(""));
0171     setTokenSecret(QLatin1StringView(""));
0172     setExtraTokens(QVariantMap());
0173     setRefreshToken(QString());
0174     setExpires(0);
0175 
0176     if (grantFlow_ == GrantFlowAuthorizationCode) {
0177         // Start listening to authentication replies
0178         replyServer_->listen(QHostAddress::Any, localPort_);
0179 
0180         // Save redirect URI, as we have to reuse it when requesting the access token
0181         redirectUri_ = localhostPolicy_.arg(replyServer_->serverPort());
0182 
0183         // Assemble initial authentication URL
0184         QList<QPair<QString, QString>> parameters;
0185         parameters.append(qMakePair(QLatin1StringView(O2_OAUTH2_RESPONSE_TYPE),
0186                                     (grantFlow_ == GrantFlowAuthorizationCode) ? QLatin1StringView(O2_OAUTH2_GRANT_TYPE_CODE)
0187                                                                                : QLatin1StringView(O2_OAUTH2_GRANT_TYPE_TOKEN)));
0188         parameters.append(qMakePair(QLatin1StringView(O2_OAUTH2_CLIENT_ID), clientId_));
0189         parameters.append(qMakePair(QLatin1StringView(O2_OAUTH2_REDIRECT_URI), redirectUri_));
0190         parameters.append(qMakePair(QLatin1StringView(O2_OAUTH2_SCOPE), scope_));
0191         parameters.append(qMakePair(QLatin1StringView(O2_OAUTH2_API_KEY), apiKey_));
0192 
0193         // Show authentication URL with a web browser
0194         QUrl url(requestUrl_);
0195         addQueryParametersToUrl(url, parameters);
0196         qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::link: Emit openBrowser" << url.toString();
0197         Q_EMIT openBrowser(url);
0198     } else if (grantFlow_ == GrantFlowResourceOwnerPasswordCredentials) {
0199         QList<O0RequestParameter> parameters;
0200         parameters.append(O0RequestParameter(O2_OAUTH2_CLIENT_ID, clientId_.toUtf8()));
0201         parameters.append(O0RequestParameter(O2_OAUTH2_CLIENT_SECRET, clientSecret_.toUtf8()));
0202         parameters.append(O0RequestParameter(O2_OAUTH2_USERNAME, username_.toUtf8()));
0203         parameters.append(O0RequestParameter(O2_OAUTH2_PASSWORD, password_.toUtf8()));
0204         parameters.append(O0RequestParameter(O2_OAUTH2_GRANT_TYPE, O2_OAUTH2_GRANT_TYPE_PASSWORD));
0205         parameters.append(O0RequestParameter(O2_OAUTH2_SCOPE, scope_.toUtf8()));
0206         parameters.append(O0RequestParameter(O2_OAUTH2_API_KEY, apiKey_.toUtf8()));
0207         QByteArray payload = O0BaseAuth::createQueryParameters(parameters);
0208 
0209         QUrl url(tokenUrl_);
0210         QNetworkRequest tokenRequest(url);
0211         tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1StringView("application/x-www-form-urlencoded"));
0212         QNetworkReply *tokenReply = manager_->post(tokenRequest, payload);
0213 
0214         connect(tokenReply, &QNetworkReply::finished, this, &O2::onTokenReplyFinished, Qt::QueuedConnection);
0215         connect(tokenReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onTokenReplyError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
0216     }
0217 }
0218 
0219 void O2::unlink()
0220 {
0221     qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::unlink";
0222     setLinked(false);
0223     setToken(QString());
0224     setRefreshToken(QString());
0225     setExpires(0);
0226     setExtraTokens(QVariantMap());
0227     Q_EMIT linkingSucceeded();
0228 }
0229 
0230 void O2::onVerificationReceived(const QMultiMap<QString, QString> &response)
0231 {
0232     qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::onVerificationReceived:" << response;
0233     qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::onVerificationReceived: Emitting closeBrowser()";
0234     Q_EMIT closeBrowser();
0235 
0236     if (response.contains(QStringLiteral("error"))) {
0237         qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2::onVerificationReceived: Verification failed: " << response;
0238         Q_EMIT linkingFailed();
0239         return;
0240     }
0241 
0242     if (grantFlow_ == GrantFlowAuthorizationCode) {
0243         // Save access code
0244         setCode(response.value(QLatin1StringView(O2_OAUTH2_GRANT_TYPE_CODE)));
0245 
0246         // Exchange access code for access/refresh tokens
0247         QString query;
0248         if (!apiKey_.isEmpty()) {
0249             query = QString(QLatin1StringView("?") + QLatin1StringView(O2_OAUTH2_API_KEY) + QLatin1StringView("=") + apiKey_);
0250         }
0251         QNetworkRequest tokenRequest(QUrl(tokenUrl_.toString() + query));
0252         tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1StringView(O2_MIME_TYPE_XFORM));
0253         QMap<QString, QString> parameters;
0254         parameters.insert(QLatin1StringView(O2_OAUTH2_GRANT_TYPE_CODE), code());
0255         parameters.insert(QLatin1StringView(O2_OAUTH2_CLIENT_ID), clientId_);
0256         parameters.insert(QLatin1StringView(O2_OAUTH2_CLIENT_SECRET), clientSecret_);
0257         parameters.insert(QLatin1StringView(O2_OAUTH2_REDIRECT_URI), redirectUri_);
0258         parameters.insert(QLatin1StringView(O2_OAUTH2_GRANT_TYPE), QLatin1StringView(O2_AUTHORIZATION_CODE));
0259         QByteArray data = buildRequestBody(parameters);
0260         QNetworkReply *tokenReply = manager_->post(tokenRequest, data);
0261         timedReplies_.add(tokenReply);
0262         connect(tokenReply, &QNetworkReply::finished, this, &O2::onTokenReplyFinished, Qt::QueuedConnection);
0263         connect(tokenReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onTokenReplyError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
0264     } else {
0265         setToken(response.value(QLatin1StringView(O2_OAUTH2_ACCESS_TOKEN)));
0266         setRefreshToken(response.value(QLatin1StringView(O2_OAUTH2_REFRESH_TOKEN)));
0267     }
0268 }
0269 
0270 QString O2::code() const
0271 {
0272     QString key = QString::fromLatin1(O2_KEY_CODE).arg(clientId_);
0273     return store_->value(key);
0274 }
0275 
0276 void O2::setCode(const QString &c)
0277 {
0278     QString key = QString::fromLatin1(O2_KEY_CODE).arg(clientId_);
0279     store_->setValue(key, c);
0280 }
0281 
0282 void O2::onTokenReplyFinished()
0283 {
0284     qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::onTokenReplyFinished";
0285     auto tokenReply = qobject_cast<QNetworkReply *>(sender());
0286     if (tokenReply->error() == QNetworkReply::NoError) {
0287         QByteArray replyData = tokenReply->readAll();
0288         QVariantMap tokens = parseTokenResponse(replyData);
0289 
0290         // Check for mandatory tokens
0291         if (tokens.contains(QLatin1StringView(O2_OAUTH2_ACCESS_TOKEN))) {
0292             setToken(tokens.take(QLatin1StringView(O2_OAUTH2_ACCESS_TOKEN)).toString());
0293             bool ok = false;
0294             int expiresIn = tokens.take(QLatin1StringView(O2_OAUTH2_EXPIRES_IN)).toInt(&ok);
0295             if (ok) {
0296                 qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::onTokenReplyFinished: Token expires in" << expiresIn << "seconds";
0297                 setExpires(QDateTime::currentSecsSinceEpoch() + expiresIn);
0298             }
0299             setRefreshToken(tokens.take(QLatin1StringView(O2_OAUTH2_REFRESH_TOKEN)).toString());
0300             setExtraTokens(tokens);
0301             timedReplies_.remove(tokenReply);
0302             setLinked(true);
0303             Q_EMIT linkingSucceeded();
0304         } else {
0305             qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2::onTokenReplyFinished: oauth_token missing from response" << replyData;
0306             Q_EMIT linkingFailed();
0307         }
0308     }
0309     tokenReply->deleteLater();
0310 }
0311 
0312 void O2::onTokenReplyError(QNetworkReply::NetworkError error)
0313 {
0314     auto tokenReply = qobject_cast<QNetworkReply *>(sender());
0315     qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2::onTokenReplyError: " << error << ": " << tokenReply->errorString();
0316     qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::onTokenReplyError: " << tokenReply->readAll();
0317     setToken(QString());
0318     setRefreshToken(QString());
0319     timedReplies_.remove(tokenReply);
0320     Q_EMIT linkingFailed();
0321 }
0322 
0323 QByteArray O2::buildRequestBody(const QMap<QString, QString> &parameters)
0324 {
0325     QByteArray body;
0326     bool first = true;
0327     for (const QString &key : parameters.keys()) {
0328         if (first) {
0329             first = false;
0330         } else {
0331             body.append("&");
0332         }
0333         QString value = parameters.value(key);
0334         body.append(QUrl::toPercentEncoding(key) + QStringLiteral("=").toUtf8() + QUrl::toPercentEncoding(value));
0335     }
0336     return body;
0337 }
0338 
0339 int O2::expires()
0340 {
0341     const QString key = QString::fromLatin1(O2_KEY_EXPIRES).arg(clientId_);
0342     return store_->value(key).toInt();
0343 }
0344 
0345 void O2::setExpires(int v)
0346 {
0347     const QString key = QString::fromLatin1(O2_KEY_EXPIRES).arg(clientId_);
0348     store_->setValue(key, QString::number(v));
0349 }
0350 
0351 QString O2::refreshToken()
0352 {
0353     const QString key = QString::fromLatin1(O2_KEY_REFRESH_TOKEN).arg(clientId_);
0354     return store_->value(key);
0355 }
0356 
0357 void O2::setRefreshToken(const QString &v)
0358 {
0359     qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::setRefreshToken" << v.left(4) << "...";
0360     QString key = QString::fromLatin1(O2_KEY_REFRESH_TOKEN).arg(clientId_);
0361     store_->setValue(key, v);
0362 }
0363 
0364 void O2::refresh()
0365 {
0366     qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::refresh: Token: ..." << refreshToken().right(7);
0367 
0368     if (refreshToken().isEmpty()) {
0369         qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2::refresh: No refresh token";
0370         onRefreshError(QNetworkReply::AuthenticationRequiredError);
0371         return;
0372     }
0373     if (refreshTokenUrl_.isEmpty()) {
0374         qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2::refresh: Refresh token URL not set";
0375         onRefreshError(QNetworkReply::AuthenticationRequiredError);
0376         return;
0377     }
0378 
0379     QNetworkRequest refreshRequest(refreshTokenUrl_);
0380     refreshRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1StringView(O2_MIME_TYPE_XFORM));
0381     QMap<QString, QString> parameters;
0382     parameters.insert(QLatin1StringView(O2_OAUTH2_CLIENT_ID), clientId_);
0383     parameters.insert(QLatin1StringView(O2_OAUTH2_CLIENT_SECRET), clientSecret_);
0384     parameters.insert(QLatin1StringView(O2_OAUTH2_REFRESH_TOKEN), refreshToken());
0385     parameters.insert(QLatin1StringView(O2_OAUTH2_GRANT_TYPE), QLatin1StringView(O2_OAUTH2_REFRESH_TOKEN));
0386 
0387     QByteArray data = buildRequestBody(parameters);
0388     QNetworkReply *refreshReply = manager_->post(refreshRequest, data);
0389     timedReplies_.add(refreshReply);
0390     connect(refreshReply, &QNetworkReply::finished, this, &O2::onRefreshFinished, Qt::QueuedConnection);
0391     connect(refreshReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRefreshError(QNetworkReply::NetworkError)), Qt::QueuedConnection);
0392 }
0393 
0394 void O2::onRefreshFinished()
0395 {
0396     auto refreshReply = qobject_cast<QNetworkReply *>(sender());
0397     qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::onRefreshFinished: Error" << (int)refreshReply->error() << refreshReply->errorString();
0398     if (refreshReply->error() == QNetworkReply::NoError) {
0399         QByteArray reply = refreshReply->readAll();
0400         QVariantMap tokens = parseTokenResponse(reply);
0401         setToken(tokens.value(QLatin1StringView(O2_OAUTH2_ACCESS_TOKEN)).toString());
0402         setExpires(QDateTime::currentSecsSinceEpoch() + tokens.value(QLatin1StringView(O2_OAUTH2_EXPIRES_IN)).toInt());
0403         setRefreshToken(tokens.value(QLatin1StringView(O2_OAUTH2_REFRESH_TOKEN)).toString());
0404         timedReplies_.remove(refreshReply);
0405         setLinked(true);
0406         Q_EMIT linkingSucceeded();
0407         Q_EMIT refreshFinished(QNetworkReply::NoError);
0408         qCDebug(TOMBOYNOTESRESOURCE_LOG) << " New token expires in" << expires() << "seconds";
0409     }
0410     refreshReply->deleteLater();
0411 }
0412 
0413 void O2::onRefreshError(QNetworkReply::NetworkError error)
0414 {
0415     auto refreshReply = qobject_cast<QNetworkReply *>(sender());
0416     qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2::onRefreshError: " << error;
0417     unlink();
0418     timedReplies_.remove(refreshReply);
0419     Q_EMIT refreshFinished(error);
0420 }
0421 
0422 QString O2::localhostPolicy() const
0423 {
0424     return localhostPolicy_;
0425 }
0426 
0427 void O2::setLocalhostPolicy(const QString &value)
0428 {
0429     localhostPolicy_ = value;
0430 }
0431 
0432 QString O2::apiKey() const
0433 {
0434     return apiKey_;
0435 }
0436 
0437 void O2::setApiKey(const QString &value)
0438 {
0439     apiKey_ = value;
0440 }
0441 
0442 QByteArray O2::replyContent() const
0443 {
0444     return replyServer_->replyContent();
0445 }
0446 
0447 void O2::setReplyContent(const QByteArray &value)
0448 {
0449     replyServer_->setReplyContent(value);
0450 }
0451 
0452 bool O2::ignoreSslErrors()
0453 {
0454     return timedReplies_.ignoreSslErrors();
0455 }
0456 
0457 void O2::setIgnoreSslErrors(bool ignoreSslErrors)
0458 {
0459     timedReplies_.setIgnoreSslErrors(ignoreSslErrors);
0460 }
0461 
0462 #include "moc_o2.cpp"