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

0001 /*
0002     SPDX-License-Identifier: BSD-2-Clause
0003 */
0004 
0005 #include "debug.h"
0006 #include <QByteArray>
0007 #include <QCryptographicHash>
0008 #include <QDateTime>
0009 #include <QNetworkRequest>
0010 #include <QRandomGenerator>
0011 
0012 #if QT_VERSION >= 0x050000
0013 #include <QUrlQuery>
0014 #endif
0015 
0016 #if QT_VERSION >= 0x050100
0017 #include <QMessageAuthenticationCode>
0018 #endif
0019 
0020 #include "o2/o0globals.h"
0021 #include "o2/o0settingsstore.h"
0022 #include "o2/o1.h"
0023 #include "o2/o2replyserver.h"
0024 
0025 O1::O1(QObject *parent)
0026     : O0BaseAuth(parent)
0027 {
0028     setSignatureMethod(QLatin1StringView(O2_SIGNATURE_TYPE_HMAC_SHA1));
0029     manager_ = new QNetworkAccessManager(this);
0030     replyServer_ = new O2ReplyServer(this);
0031     qRegisterMetaType<QNetworkReply::NetworkError>("QNetworkReply::NetworkError");
0032     connect(replyServer_, &O2ReplyServer::verificationReceived, this, &O1::onVerificationReceived);
0033     setCallbackUrl(QLatin1StringView(O2_CALLBACK_URL));
0034 }
0035 
0036 QUrl O1::requestTokenUrl() const
0037 {
0038     return requestTokenUrl_;
0039 }
0040 
0041 void O1::setRequestTokenUrl(const QUrl &v)
0042 {
0043     requestTokenUrl_ = v;
0044     Q_EMIT requestTokenUrlChanged();
0045 }
0046 
0047 QList<O0RequestParameter> O1::requestParameters()
0048 {
0049     return requestParameters_;
0050 }
0051 
0052 void O1::setRequestParameters(const QList<O0RequestParameter> &v)
0053 {
0054     requestParameters_ = v;
0055 }
0056 
0057 QString O1::callbackUrl() const
0058 {
0059     return callbackUrl_;
0060 }
0061 
0062 void O1::setCallbackUrl(const QString &v)
0063 {
0064     callbackUrl_ = v;
0065 }
0066 
0067 QUrl O1::authorizeUrl() const
0068 {
0069     return authorizeUrl_;
0070 }
0071 
0072 void O1::setAuthorizeUrl(const QUrl &value)
0073 {
0074     authorizeUrl_ = value;
0075     Q_EMIT authorizeUrlChanged();
0076 }
0077 
0078 QUrl O1::accessTokenUrl() const
0079 {
0080     return accessTokenUrl_;
0081 }
0082 
0083 void O1::setAccessTokenUrl(const QUrl &value)
0084 {
0085     accessTokenUrl_ = value;
0086     Q_EMIT accessTokenUrlChanged();
0087 }
0088 
0089 QString O1::signatureMethod()
0090 {
0091     return signatureMethod_;
0092 }
0093 
0094 void O1::setSignatureMethod(const QString &value)
0095 {
0096     qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O1::setSignatureMethod: " << value;
0097     signatureMethod_ = value;
0098 }
0099 
0100 void O1::unlink()
0101 {
0102     qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O1::unlink";
0103     setLinked(false);
0104     setToken(QString());
0105     setTokenSecret(QString());
0106     setExtraTokens(QVariantMap());
0107     Q_EMIT linkingSucceeded();
0108 }
0109 
0110 #if QT_VERSION < 0x050100
0111 /// Calculate the HMAC variant of SHA1 hash.
0112 /// @author     http://qt-project.org/wiki/HMAC-SHA1.
0113 /// @copyright  Creative Commons Attribution-ShareAlike 2.5 Generic.
0114 static QByteArray hmacSha1(QByteArray key, QByteArray baseString)
0115 {
0116     int blockSize = 64;
0117     if (key.length() > blockSize) {
0118         key = QCryptographicHash::hash(key, QCryptographicHash::Sha1);
0119     }
0120     QByteArray innerPadding(blockSize, char(0x36));
0121     QByteArray outerPadding(blockSize, char(0x5c));
0122     for (int i = 0; i < key.length(); i++) {
0123         innerPadding[i] = innerPadding[i] ^ key.at(i);
0124         outerPadding[i] = outerPadding[i] ^ key.at(i);
0125     }
0126     QByteArray total = outerPadding;
0127     QByteArray part = innerPadding;
0128     part.append(baseString);
0129     total.append(QCryptographicHash::hash(part, QCryptographicHash::Sha1));
0130     QByteArray hashed = QCryptographicHash::hash(total, QCryptographicHash::Sha1);
0131     return hashed.toBase64();
0132 }
0133 
0134 #endif
0135 
0136 /// Get HTTP operation name.
0137 static QString getOperationName(QNetworkAccessManager::Operation op)
0138 {
0139     switch (op) {
0140     case QNetworkAccessManager::GetOperation:
0141         return QStringLiteral("GET");
0142     case QNetworkAccessManager::PostOperation:
0143         return QStringLiteral("POST");
0144     case QNetworkAccessManager::PutOperation:
0145         return QStringLiteral("PUT");
0146     case QNetworkAccessManager::DeleteOperation:
0147         return QStringLiteral("DEL");
0148     default:
0149         return {};
0150     }
0151 }
0152 
0153 /// Build a concatenated/percent-encoded string from a list of headers.
0154 QByteArray O1::encodeHeaders(const QList<O0RequestParameter> &headers)
0155 {
0156     return QUrl::toPercentEncoding(QString::fromLatin1(createQueryParameters(headers)));
0157 }
0158 
0159 /// Build a base string for signing.
0160 QByteArray O1::getRequestBase(const QList<O0RequestParameter> &oauthParams,
0161                               const QList<O0RequestParameter> &otherParams,
0162                               const QUrl &url,
0163                               QNetworkAccessManager::Operation op)
0164 {
0165     QByteArray base;
0166 
0167     // Initialize base string with the operation name (e.g. "GET") and the base URL
0168     base.append(getOperationName(op).toUtf8() + "&");
0169     base.append(QUrl::toPercentEncoding(url.toString(QUrl::RemoveQuery)) + "&");
0170 
0171     // Append a sorted+encoded list of all request parameters to the base string
0172     QList<O0RequestParameter> headers(oauthParams);
0173     headers.append(otherParams);
0174     std::sort(headers.begin(), headers.end());
0175     base.append(encodeHeaders(headers));
0176 
0177     return base;
0178 }
0179 
0180 QByteArray O1::sign(const QList<O0RequestParameter> &oauthParams,
0181                     const QList<O0RequestParameter> &otherParams,
0182                     const QUrl &url,
0183                     QNetworkAccessManager::Operation op,
0184                     const QString &consumerSecret,
0185                     const QString &tokenSecret)
0186 {
0187     QByteArray baseString = getRequestBase(oauthParams, otherParams, url, op);
0188     QByteArray secret = QUrl::toPercentEncoding(consumerSecret) + "&" + QUrl::toPercentEncoding(tokenSecret);
0189 #if QT_VERSION >= 0x050100
0190     return QMessageAuthenticationCode::hash(baseString, secret, QCryptographicHash::Sha1).toBase64();
0191 #else
0192     return hmacSha1(secret, baseString);
0193 #endif
0194 }
0195 
0196 QByteArray O1::buildAuthorizationHeader(const QList<O0RequestParameter> &oauthParams)
0197 {
0198     bool first = true;
0199     QByteArray ret("OAuth ");
0200     QList<O0RequestParameter> headers(oauthParams);
0201     std::sort(headers.begin(), headers.end());
0202     for (const O0RequestParameter &h : std::as_const(headers)) {
0203         if (first) {
0204             first = false;
0205         } else {
0206             ret.append(",");
0207         }
0208         ret.append(h.name);
0209         ret.append("=\"");
0210         ret.append(QUrl::toPercentEncoding(QString::fromLatin1(h.value)));
0211         ret.append("\"");
0212     }
0213     return ret;
0214 }
0215 
0216 QByteArray O1::generateSignature(const QList<O0RequestParameter> &headers,
0217                                  const QNetworkRequest &req,
0218                                  const QList<O0RequestParameter> &signingParameters,
0219                                  QNetworkAccessManager::Operation operation)
0220 {
0221     QByteArray signature;
0222     if (signatureMethod() == QLatin1StringView(O2_SIGNATURE_TYPE_HMAC_SHA1)) {
0223         signature = sign(headers, signingParameters, req.url(), operation, clientSecret(), tokenSecret());
0224     } else if (signatureMethod() == QLatin1StringView(O2_SIGNATURE_TYPE_PLAINTEXT)) {
0225         signature = clientSecret().toLatin1() + "&" + tokenSecret().toLatin1();
0226     }
0227     return signature;
0228 }
0229 
0230 void O1::link()
0231 {
0232     qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O1::link";
0233     if (linked()) {
0234         qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O1::link: Linked already";
0235         Q_EMIT linkingSucceeded();
0236         return;
0237     }
0238 
0239     setLinked(false);
0240     setToken(QString());
0241     setTokenSecret(QString());
0242     setExtraTokens(QVariantMap());
0243 
0244     // Start reply server
0245     replyServer_->listen(QHostAddress::Any, localPort());
0246 
0247     // Get any query parameters for the request
0248     QUrlQuery requestData;
0249     const auto params{requestParameters()};
0250     for (const O0RequestParameter &param : params) {
0251         requestData.addQueryItem(QString::fromLatin1(param.name), QString::fromLatin1(QUrl::toPercentEncoding(QString::fromLatin1(param.value))));
0252     }
0253 
0254     // Get the request url and add parameters
0255     QUrl requestUrl = requestTokenUrl();
0256     requestUrl.setQuery(requestData);
0257 
0258     // Create request
0259     QNetworkRequest request(requestUrl);
0260 
0261     // Create initial token request
0262     QList<O0RequestParameter> headers;
0263     headers.append(O0RequestParameter(O2_OAUTH_CALLBACK, callbackUrl().arg(replyServer_->serverPort()).toLatin1()));
0264     headers.append(O0RequestParameter(O2_OAUTH_CONSUMER_KEY, clientId().toLatin1()));
0265     headers.append(O0RequestParameter(O2_OAUTH_NONCE, nonce()));
0266     headers.append(O0RequestParameter(O2_OAUTH_TIMESTAMP, QString::number(QDateTime::currentDateTimeUtc().toSecsSinceEpoch()).toLatin1()));
0267     headers.append(O0RequestParameter(O2_OAUTH_VERSION, "1.0"));
0268     headers.append(O0RequestParameter(O2_OAUTH_SIGNATURE_METHOD, signatureMethod().toLatin1()));
0269     headers.append(O0RequestParameter(O2_OAUTH_SIGNATURE, generateSignature(headers, request, requestParameters(), QNetworkAccessManager::PostOperation)));
0270 
0271     // Clear request token
0272     requestToken_.clear();
0273     requestTokenSecret_.clear();
0274 
0275     // Post request
0276     request.setRawHeader(O2_HTTP_AUTHORIZATION_HEADER, buildAuthorizationHeader(headers));
0277     request.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1StringView(O2_MIME_TYPE_XFORM));
0278     QNetworkReply *reply = manager_->post(request, QByteArray());
0279     connect(reply, &QNetworkReply::errorOccurred, this, &O1::onTokenRequestError);
0280     connect(reply, &QNetworkReply::finished, this, &O1::onTokenRequestFinished);
0281 }
0282 
0283 void O1::onTokenRequestError(QNetworkReply::NetworkError error)
0284 {
0285     auto reply = qobject_cast<QNetworkReply *>(sender());
0286     qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O1::onTokenRequestError:" << (int)error << reply->errorString() << reply->readAll();
0287     Q_EMIT linkingFailed();
0288 }
0289 
0290 void O1::onTokenRequestFinished()
0291 {
0292     qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O1::onTokenRequestFinished";
0293     auto reply = qobject_cast<QNetworkReply *>(sender());
0294     reply->deleteLater();
0295     if (reply->error() != QNetworkReply::NoError) {
0296         qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O1::onTokenRequestFinished: " << reply->errorString();
0297         return;
0298     }
0299 
0300     // Get request token and secret
0301     QByteArray data = reply->readAll();
0302     QMap<QString, QString> response = parseResponse(data);
0303     requestToken_ = response.value(QLatin1StringView(O2_OAUTH_TOKEN), QString());
0304     requestTokenSecret_ = response.value(QLatin1StringView(O2_OAUTH_TOKEN_SECRET), QString());
0305     setToken(requestToken_);
0306     setTokenSecret(requestTokenSecret_);
0307 
0308     // Checking for "oauth_callback_confirmed" is present and set to true
0309     QString oAuthCbConfirmed = response.value(QLatin1StringView(O2_OAUTH_CALLBACK_CONFIRMED), QStringLiteral("false"));
0310     if (requestToken_.isEmpty() || requestTokenSecret_.isEmpty() || (oAuthCbConfirmed == QLatin1StringView("false"))) {
0311         qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O1::onTokenRequestFinished: No oauth_token, oauth_token_secret or oauth_callback_confirmed in response :"
0312                                            << data;
0313         Q_EMIT linkingFailed();
0314         return;
0315     }
0316 
0317     // Continue authorization flow in the browser
0318     QUrl url(authorizeUrl());
0319 #if QT_VERSION < 0x050000
0320     url.addQueryItem(O2_OAUTH_TOKEN, requestToken_);
0321     url.addQueryItem(O2_OAUTH_CALLBACK, callbackUrl().arg(replyServer_->serverPort()).toLatin1());
0322 #else
0323     QUrlQuery query(url);
0324     query.addQueryItem(QLatin1StringView(O2_OAUTH_TOKEN), requestToken_);
0325     query.addQueryItem(QLatin1StringView(O2_OAUTH_CALLBACK), QString::fromLatin1(callbackUrl().arg(replyServer_->serverPort()).toLatin1()));
0326     url.setQuery(query);
0327 #endif
0328     Q_EMIT openBrowser(url);
0329 }
0330 
0331 void O1::onVerificationReceived(const QMultiMap<QString, QString> &params)
0332 {
0333     qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O1::onVerificationReceived";
0334     Q_EMIT closeBrowser();
0335     verifier_ = params.value(QLatin1StringView(O2_OAUTH_VERFIER), QString());
0336     if (params.value(QLatin1StringView(O2_OAUTH_TOKEN)) == requestToken_) {
0337         // Exchange request token for access token
0338         exchangeToken();
0339     } else {
0340         qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O1::onVerificationReceived: oauth_token missing or doesn't match";
0341         Q_EMIT linkingFailed();
0342     }
0343 }
0344 
0345 void O1::exchangeToken()
0346 {
0347     qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O1::exchangeToken";
0348 
0349     // Create token exchange request
0350     QNetworkRequest request(accessTokenUrl());
0351     QList<O0RequestParameter> oauthParams;
0352     oauthParams.append(O0RequestParameter(O2_OAUTH_CONSUMER_KEY, clientId().toLatin1()));
0353     oauthParams.append(O0RequestParameter(O2_OAUTH_VERSION, "1.0"));
0354     oauthParams.append(O0RequestParameter(O2_OAUTH_TIMESTAMP, QString::number(QDateTime::currentDateTimeUtc().toSecsSinceEpoch()).toLatin1()));
0355     oauthParams.append(O0RequestParameter(O2_OAUTH_NONCE, nonce()));
0356     oauthParams.append(O0RequestParameter(O2_OAUTH_TOKEN, requestToken_.toLatin1()));
0357     oauthParams.append(O0RequestParameter(O2_OAUTH_VERFIER, verifier_.toLatin1()));
0358     oauthParams.append(O0RequestParameter(O2_OAUTH_SIGNATURE_METHOD, signatureMethod().toLatin1()));
0359     oauthParams.append(
0360         O0RequestParameter(O2_OAUTH_SIGNATURE, generateSignature(oauthParams, request, QList<O0RequestParameter>(), QNetworkAccessManager::PostOperation)));
0361 
0362     // Post request
0363     request.setRawHeader(O2_HTTP_AUTHORIZATION_HEADER, buildAuthorizationHeader(oauthParams));
0364     request.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1StringView(O2_MIME_TYPE_XFORM));
0365     QNetworkReply *reply = manager_->post(request, QByteArray());
0366     connect(reply, &QNetworkReply::errorOccurred, this, &O1::onTokenExchangeError);
0367     connect(reply, &QNetworkReply::finished, this, &O1::onTokenExchangeFinished);
0368 }
0369 
0370 void O1::onTokenExchangeError(QNetworkReply::NetworkError error)
0371 {
0372     auto reply = qobject_cast<QNetworkReply *>(sender());
0373     qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O1::onTokenExchangeError:" << (int)error << reply->errorString() << reply->readAll();
0374     Q_EMIT linkingFailed();
0375 }
0376 
0377 void O1::onTokenExchangeFinished()
0378 {
0379     qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O1::onTokenExchangeFinished";
0380 
0381     auto reply = qobject_cast<QNetworkReply *>(sender());
0382     reply->deleteLater();
0383     if (reply->error() != QNetworkReply::NoError) {
0384         qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O1::onTokenExchangeFinished: " << reply->errorString();
0385         return;
0386     }
0387 
0388     // Get access token and secret
0389     QByteArray data = reply->readAll();
0390     QMap<QString, QString> response = parseResponse(data);
0391     if (response.contains(QLatin1StringView(O2_OAUTH_TOKEN)) && response.contains(QLatin1StringView(O2_OAUTH_TOKEN_SECRET))) {
0392         setToken(response.take(QLatin1StringView(O2_OAUTH_TOKEN)));
0393         setTokenSecret(response.take(QLatin1StringView(O2_OAUTH_TOKEN_SECRET)));
0394         // Set extra tokens if any
0395         if (!response.isEmpty()) {
0396             QVariantMap extraTokens;
0397             for (const QString &key : std::as_const(response)) {
0398                 extraTokens.insert(key, response.value(key));
0399             }
0400             setExtraTokens(extraTokens);
0401         }
0402         setLinked(true);
0403         Q_EMIT linkingSucceeded();
0404     } else {
0405         qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O1::onTokenExchangeFinished: oauth_token or oauth_token_secret missing from response" << data;
0406         Q_EMIT linkingFailed();
0407     }
0408 }
0409 
0410 QMap<QString, QString> O1::parseResponse(const QByteArray &response)
0411 {
0412     QMap<QString, QString> ret;
0413     const auto responses{response.split('&')};
0414     for (const QByteArray &param : responses) {
0415         const QList<QByteArray> kv = param.split('=');
0416         if (kv.length() == 2) {
0417             ret.insert(QUrl::fromPercentEncoding(kv[0]), QUrl::fromPercentEncoding(kv[1]));
0418         }
0419     }
0420     return ret;
0421 }
0422 
0423 QByteArray O1::nonce()
0424 {
0425     QString u = QString::number(QDateTime::currentDateTimeUtc().toSecsSinceEpoch());
0426     u.append(QString::number(QRandomGenerator::global()->bounded(RAND_MAX)));
0427     return u.toLatin1();
0428 }
0429 
0430 #include "moc_o1.cpp"