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 ¶m : 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> ¶ms) 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 ¶m : 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"