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>> ¶meters) 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> ¶meters) 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"