+/**
+ * KQOAuth - An OAuth authentication library for Qt.
+ *
+ * Author: Johan Paul (johan.paul@d-pointer.com)
+ * http://www.d-pointer.com
+ *
+ * KQOAuth is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * KQOAuth is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with KQOAuth. If not, see <http://www.gnu.org/licenses/>.
+ */
+#include <QByteArray>
+#include <QDateTime>
+#include <QCryptographicHash>
+#include <QPair>
+#include <QStringList>
+
+#include <QtDebug>
+#include <QtAlgorithms>
+
+#include "kqoauthrequest.h"
+#include "kqoauthrequest_p.h"
+#include "kqoauthutils.h"
+#include "kqoauthglobals.h"
+
+
+//////////// Private d_ptr implementation /////////
+
+KQOAuthRequestPrivate::KQOAuthRequestPrivate() :
+ timeout(0)
+{
+
+}
+
+KQOAuthRequestPrivate::~KQOAuthRequestPrivate()
+{
+
+}
+
+// This method will not include the "oauthSignature" paramater, since it is calculated from these parameters.
+void KQOAuthRequestPrivate::prepareRequest() {
+
+ // If parameter list is not empty, we don't want to insert these values by
+ // accident a second time. So giving up.
+ if( !requestParameters.isEmpty() ) {
+ return;
+ }
+
+ switch ( requestType ) {
+ case KQOAuthRequest::TemporaryCredentials:
+ requestParameters.append( qMakePair( OAUTH_KEY_CALLBACK, oauthCallbackUrl.toString()) ); // This is so ugly that it is almost beautiful.
+ requestParameters.append( qMakePair( OAUTH_KEY_SIGNATURE_METHOD, oauthSignatureMethod) );
+ requestParameters.append( qMakePair( OAUTH_KEY_CONSUMER_KEY, oauthConsumerKey ));
+ requestParameters.append( qMakePair( OAUTH_KEY_VERSION, oauthVersion ));
+ requestParameters.append( qMakePair( OAUTH_KEY_TIMESTAMP, this->oauthTimestamp() ));
+ requestParameters.append( qMakePair( OAUTH_KEY_NONCE, this->oauthNonce() ));
+ break;
+
+ case KQOAuthRequest::AccessToken:
+ requestParameters.append( qMakePair( OAUTH_KEY_SIGNATURE_METHOD, oauthSignatureMethod ));
+ requestParameters.append( qMakePair( OAUTH_KEY_CONSUMER_KEY, oauthConsumerKey ));
+ requestParameters.append( qMakePair( OAUTH_KEY_VERSION, oauthVersion ));
+ requestParameters.append( qMakePair( OAUTH_KEY_TIMESTAMP, this->oauthTimestamp() ));
+ requestParameters.append( qMakePair( OAUTH_KEY_NONCE, this->oauthNonce() ));
+ requestParameters.append( qMakePair( OAUTH_KEY_VERIFIER, oauthVerifier ));
+ requestParameters.append( qMakePair( OAUTH_KEY_TOKEN, oauthToken ));
+ break;
+
+ case KQOAuthRequest::AuthorizedRequest:
+ requestParameters.append( qMakePair( OAUTH_KEY_SIGNATURE_METHOD, oauthSignatureMethod ));
+ requestParameters.append( qMakePair( OAUTH_KEY_CONSUMER_KEY, oauthConsumerKey ));
+ requestParameters.append( qMakePair( OAUTH_KEY_VERSION, oauthVersion ));
+ requestParameters.append( qMakePair( OAUTH_KEY_TIMESTAMP, this->oauthTimestamp() ));
+ requestParameters.append( qMakePair( OAUTH_KEY_NONCE, this->oauthNonce() ));
+ requestParameters.append( qMakePair( OAUTH_KEY_TOKEN, oauthToken ));
+ break;
+
+ default:
+ break;
+ }
+}
+
+void KQOAuthRequestPrivate::signRequest() {
+ QString signature = this->oauthSignature();
+ requestParameters.append( qMakePair( OAUTH_KEY_SIGNATURE, signature) );
+}
+
+QString KQOAuthRequestPrivate::oauthSignature() {
+ /**
+ * http://oauth.net/core/1.0/#anchor16
+ * The HMAC-SHA1 signature method uses the HMAC-SHA1 signature algorithm as defined in [RFC2104] where the
+ * Signature Base String is the text and the key is the concatenated values (each first encoded per Parameter
+ * Encoding) of the Consumer Secret and Token Secret, separated by an ‘&’ character (ASCII code 38) even if empty.
+ **/
+ QByteArray baseString = this->requestBaseString();
+
+ QString secret = QString(QUrl::toPercentEncoding(oauthConsumerSecretKey)) + "&" + QString(QUrl::toPercentEncoding(oauthTokenSecret));
+ QString signature = KQOAuthUtils::hmac_sha1(baseString, secret);
+
+ if (debugOutput) {
+ qDebug() << "========== KQOAuthRequest has the following signature:";
+ qDebug() << " * Signature : " << QUrl::toPercentEncoding(signature) << "\n";
+ }
+ return QString( QUrl::toPercentEncoding(signature) );
+}
+
+bool normalizedParameterSort(const QPair<QString, QString> &left, const QPair<QString, QString> &right) {
+ QString keyLeft = left.first;
+ QString valueLeft = left.second;
+ QString keyRight = right.first;
+ QString valueRight = right.second;
+
+ if(keyLeft == keyRight) {
+ return (valueLeft < valueRight);
+ } else {
+ return (keyLeft < keyRight);
+ }
+}
+QByteArray KQOAuthRequestPrivate::requestBaseString() {
+ QByteArray baseString;
+
+ // Every request has these as the commont parameters.
+ baseString.append( oauthHttpMethodString.toUtf8() + "&"); // HTTP method
+ baseString.append( QUrl::toPercentEncoding( oauthRequestEndpoint.toString(QUrl::RemoveQuery) ) + "&" ); // The path and query components
+
+ QList< QPair<QString, QString> > baseStringParameters;
+ baseStringParameters.append(requestParameters);
+ baseStringParameters.append(additionalParameters);
+
+ // Sort the request parameters. These parameters have been
+ // initialized earlier.
+ qSort(baseStringParameters.begin(),
+ baseStringParameters.end(),
+ normalizedParameterSort
+ );
+
+ // Last append the request parameters correctly encoded.
+ baseString.append( encodedParamaterList(baseStringParameters) );
+
+ if (debugOutput) {
+ qDebug() << "========== KQOAuthRequest has the following base string:";
+ qDebug() << baseString << "\n";
+ }
+
+ return baseString;
+}
+
+QByteArray KQOAuthRequestPrivate::encodedParamaterList(const QList< QPair<QString, QString> > ¶meters) {
+ QByteArray resultList;
+
+ bool first = true;
+ QPair<QString, QString> parameter;
+
+ // Do the debug output.
+ if (debugOutput) {
+ qDebug() << "========== KQOAuthRequest has the following parameters:";
+ }
+ foreach (parameter, parameters) {
+ if(!first) {
+ resultList.append( "&" );
+ } else {
+ first = false;
+ }
+
+ // Here we don't need to explicitely encode the strings to UTF-8 since
+ // QUrl::toPercentEncoding() takes care of that for us.
+ resultList.append( QUrl::toPercentEncoding(parameter.first) // Parameter key
+ + "="
+ + QUrl::toPercentEncoding(parameter.second) // Parameter value
+ );
+ if (debugOutput) {
+ qDebug() << " * "
+ << parameter.first
+ << " : "
+ << parameter.second;
+ }
+ }
+ if (debugOutput) {
+ qDebug() << "\n";
+ }
+
+ return QUrl::toPercentEncoding(resultList);
+}
+
+QString KQOAuthRequestPrivate::oauthTimestamp() const {
+ // This is basically for unit tests only. In most cases we don't set the nonce beforehand.
+ if (!oauthTimestamp_.isEmpty()) {
+ return oauthTimestamp_;
+ }
+
+#if QT_VERSION >= 0x040700
+ return QString::number(QDateTime::currentDateTimeUtc().toTime_t());
+#else
+ return QString::number(QDateTime::currentDateTime().toUTC().toTime_t());
+#endif
+
+}
+
+QString KQOAuthRequestPrivate::oauthNonce() const {
+ // This is basically for unit tests only. In most cases we don't set the nonce beforehand.
+ if (!oauthNonce_.isEmpty()) {
+ return oauthNonce_;
+ }
+
+ return QString::number(qrand());
+}
+
+bool KQOAuthRequestPrivate::validateRequest() const {
+ switch ( requestType ) {
+ case KQOAuthRequest::TemporaryCredentials:
+ if (oauthRequestEndpoint.isEmpty()
+ || oauthConsumerKey.isEmpty()
+ || oauthNonce_.isEmpty()
+ || oauthSignatureMethod.isEmpty()
+ || oauthTimestamp_.isEmpty()
+ || oauthVersion.isEmpty())
+ {
+ return false;
+ }
+ return true;
+
+ case KQOAuthRequest::AccessToken:
+ if (oauthRequestEndpoint.isEmpty()
+ || oauthVerifier.isEmpty()
+ || oauthConsumerKey.isEmpty()
+ || oauthNonce_.isEmpty()
+ || oauthSignatureMethod.isEmpty()
+ || oauthTimestamp_.isEmpty()
+ || oauthToken.isEmpty()
+ || oauthTokenSecret.isEmpty()
+ || oauthVersion.isEmpty())
+ {
+ return false;
+ }
+ return true;
+
+ case KQOAuthRequest::AuthorizedRequest:
+ if (oauthRequestEndpoint.isEmpty()
+ || oauthConsumerKey.isEmpty()
+ || oauthNonce_.isEmpty()
+ || oauthSignatureMethod.isEmpty()
+ || oauthTimestamp_.isEmpty()
+ || oauthToken.isEmpty()
+ || oauthTokenSecret.isEmpty()
+ || oauthVersion.isEmpty())
+ {
+ return false;
+ }
+ return true;
+
+ default:
+ return false;
+ }
+
+ // We should not come here.
+ return false;
+}
+
+//////////// Public implementation ////////////////
+
+KQOAuthRequest::KQOAuthRequest(QObject *parent) :
+ QObject(parent),
+ d_ptr(new KQOAuthRequestPrivate)
+{
+ d_ptr->debugOutput = false; // No debug output by default.
+ qsrand(QTime::currentTime().msec()); // We need to seed the nonce random number with something.
+ // However, we cannot do this while generating the nonce since
+ // we might get the same seed. So initializing here should be fine.
+}
+
+KQOAuthRequest::~KQOAuthRequest()
+{
+ delete d_ptr;
+}
+
+void KQOAuthRequest::initRequest(KQOAuthRequest::RequestType type, const QUrl &requestEndpoint) {
+ Q_D(KQOAuthRequest);
+
+ if (!requestEndpoint.isValid()) {
+ qWarning() << "Endpoint URL is not valid. Ignoring. This request might not work.";
+ return;
+ }
+
+ if (type < 0 || type > KQOAuthRequest::AuthorizedRequest) {
+ qWarning() << "Invalid request type. Ignoring. This request might not work.";
+ return;
+ }
+
+ // Clear the request
+ clearRequest();
+
+ // Set smart defaults.
+ d->requestType = type;
+ d->oauthRequestEndpoint = requestEndpoint;
+ d->oauthTimestamp_ = d->oauthTimestamp();
+ d->oauthNonce_ = d->oauthNonce();
+ this->setSignatureMethod(KQOAuthRequest::HMAC_SHA1);
+ this->setHttpMethod(KQOAuthRequest::POST);
+ d->oauthVersion = "1.0"; // Currently supports only version 1.0
+
+ d->contentType = "application/x-www-form-urlencoded";
+}
+
+void KQOAuthRequest::setConsumerKey(const QString &consumerKey) {
+ Q_D(KQOAuthRequest);
+ d->oauthConsumerKey = consumerKey;
+}
+
+void KQOAuthRequest::setConsumerSecretKey(const QString &consumerSecretKey) {
+ Q_D(KQOAuthRequest);
+ d->oauthConsumerSecretKey = consumerSecretKey;
+}
+
+void KQOAuthRequest::setCallbackUrl(const QUrl &callbackUrl) {
+ Q_D(KQOAuthRequest);
+
+ d->oauthCallbackUrl = callbackUrl;
+}
+
+void KQOAuthRequest::setSignatureMethod(KQOAuthRequest::RequestSignatureMethod requestMethod) {
+ Q_D(KQOAuthRequest);
+ QString requestMethodString;
+
+ switch (requestMethod) {
+ case KQOAuthRequest::PLAINTEXT:
+ requestMethodString = "PLAINTEXT";
+ break;
+ case KQOAuthRequest::HMAC_SHA1:
+ requestMethodString = "HMAC-SHA1";
+ break;
+ case KQOAuthRequest::RSA_SHA1:
+ requestMethodString = "RSA-SHA1";
+ break;
+ default:
+ // We should not come here
+ qWarning() << "Invalid signature method set.";
+ break;
+ }
+
+ d->oauthSignatureMethod = requestMethodString;
+}
+
+void KQOAuthRequest::setTokenSecret(const QString &tokenSecret) {
+ Q_D(KQOAuthRequest);
+
+ d->oauthTokenSecret = tokenSecret;
+}
+
+void KQOAuthRequest::setToken(const QString &token) {
+ Q_D(KQOAuthRequest);
+
+ d->oauthToken = token;
+}
+
+void KQOAuthRequest::setVerifier(const QString &verifier) {
+ Q_D(KQOAuthRequest);
+
+ d->oauthVerifier = verifier;
+}
+
+
+void KQOAuthRequest::setHttpMethod(KQOAuthRequest::RequestHttpMethod httpMethod) {
+ Q_D(KQOAuthRequest);
+
+ QString requestHttpMethodString;
+
+ switch (httpMethod) {
+ case KQOAuthRequest::GET:
+ requestHttpMethodString = "GET";
+ break;
+ case KQOAuthRequest::POST:
+ requestHttpMethodString = "POST";
+ break;
+ default:
+ qWarning() << "Invalid HTTP method set.";
+ break;
+ }
+
+ d->oauthHttpMethod = httpMethod;
+ d->oauthHttpMethodString = requestHttpMethodString;
+}
+
+KQOAuthRequest::RequestHttpMethod KQOAuthRequest::httpMethod() const {
+ Q_D(const KQOAuthRequest);
+
+ return d->oauthHttpMethod;
+}
+
+void KQOAuthRequest::setAdditionalParameters(const KQOAuthParameters &additionalParams) {
+ Q_D(KQOAuthRequest);
+
+ QList<QString> additionalKeys = additionalParams.keys();
+ QList<QString> additionalValues = additionalParams.values();
+
+ int i=0;
+ foreach(QString key, additionalKeys) {
+ QString value = additionalValues.at(i);
+ d->additionalParameters.append( qMakePair(key, value) );
+ i++;
+ }
+}
+
+KQOAuthParameters KQOAuthRequest::additionalParameters() const {
+ Q_D(const KQOAuthRequest);
+
+ QMultiMap<QString, QString> additionalParams;
+ for(int i=0; i<d->additionalParameters.size(); i++) {
+ additionalParams.insert(d->additionalParameters.at(i).first,
+ d->additionalParameters.at(i).second);
+ }
+
+ return additionalParams;
+}
+
+KQOAuthRequest::RequestType KQOAuthRequest::requestType() const {
+ Q_D(const KQOAuthRequest);
+ return d->requestType;
+}
+
+QUrl KQOAuthRequest::requestEndpoint() const {
+ Q_D(const KQOAuthRequest);
+ return d->oauthRequestEndpoint;
+}
+
+QList<QByteArray> KQOAuthRequest::requestParameters() {
+ Q_D(KQOAuthRequest);
+
+ QList<QByteArray> requestParamList;
+
+ d->prepareRequest();
+ if (!isValid() ) {
+ qWarning() << "Request is not valid! I will still sign it, but it will probably not work.";
+ }
+ d->signRequest();
+
+ QPair<QString, QString> requestParam;
+ QString param;
+ QString value;
+ foreach (requestParam, d->requestParameters) {
+ param = requestParam.first;
+ value = requestParam.second;
+ requestParamList.append(QString(param + "=\"" + value +"\"").toUtf8());
+ }
+
+ return requestParamList;
+}
+
+QString KQOAuthRequest::contentType()
+{
+ Q_D(const KQOAuthRequest);
+ return d->contentType;
+}
+
+void KQOAuthRequest::setContentType(const QString &contentType)
+{
+ Q_D(KQOAuthRequest);
+ d->contentType = contentType;
+}
+
+QByteArray KQOAuthRequest::rawData()
+{
+ Q_D(const KQOAuthRequest);
+ return d->postRawData;
+}
+
+void KQOAuthRequest::setRawData(const QByteArray &rawData)
+{
+ Q_D(KQOAuthRequest);
+ d->postRawData = rawData;
+}
+
+QByteArray KQOAuthRequest::requestBody() const {
+ Q_D(const KQOAuthRequest);
+
+ QByteArray postBodyContent;
+ bool first = true;
+ for(int i=0; i < d->additionalParameters.size(); i++) {
+ if(!first) {
+ postBodyContent.append("&");
+ } else {
+ first = false;
+ }
+
+ QString key = d->additionalParameters.at(i).first;
+ QString value = d->additionalParameters.at(i).second;
+
+ postBodyContent.append(QUrl::toPercentEncoding(key) + QString("=").toUtf8() +
+ QUrl::toPercentEncoding(value));
+ }
+ return postBodyContent;
+}
+
+bool KQOAuthRequest::isValid() const {
+ Q_D(const KQOAuthRequest);
+
+ return d->validateRequest();
+}
+
+void KQOAuthRequest::setTimeout(int timeoutMilliseconds) {
+ Q_D(KQOAuthRequest);
+ d->timeout = timeoutMilliseconds;
+}
+
+void KQOAuthRequest::clearRequest() {
+ Q_D(KQOAuthRequest);
+
+ d->oauthRequestEndpoint = "";
+ d->oauthHttpMethodString = "";
+ d->oauthConsumerKey = "";
+ d->oauthConsumerSecretKey = "";
+ d->oauthToken = "";
+ d->oauthTokenSecret = "";
+ d->oauthSignatureMethod = "";
+ d->oauthCallbackUrl = "";
+ d->oauthVerifier = "";
+ d->oauthTimestamp_ = "";
+ d->oauthNonce_ = "";
+ d->requestParameters.clear();
+ d->additionalParameters.clear();
+ d->timeout = 0;
+}
+
+void KQOAuthRequest::setEnableDebugOutput(bool enabled) {
+ Q_D(KQOAuthRequest);
+ d->debugOutput = enabled;
+}
+
+/**
+ * Protected implementations for inherited classes
+ */
+bool KQOAuthRequest::validateXAuthRequest() const {
+ Q_D(const KQOAuthRequest);
+
+ if (d->oauthRequestEndpoint.isEmpty()
+ || d->oauthConsumerKey.isEmpty()
+ || d->oauthNonce_.isEmpty()
+ || d->oauthSignatureMethod.isEmpty()
+ || d->oauthTimestamp_.isEmpty()
+ || d->oauthVersion.isEmpty())
+ {
+ return false;
+ }
+ return true;
+}
+
+
+/**
+ * Private implementations for friend classes
+ */
+QString KQOAuthRequest::consumerKeyForManager() const {
+ Q_D(const KQOAuthRequest);
+ return d->oauthConsumerKey;
+}
+
+QString KQOAuthRequest::consumerKeySecretForManager() const {
+ Q_D(const KQOAuthRequest);
+ return d->oauthConsumerSecretKey;
+}
+
+QUrl KQOAuthRequest::callbackUrlForManager() const {
+ Q_D(const KQOAuthRequest);
+ return d->oauthCallbackUrl;
+}
+
+void KQOAuthRequest::requestTimerStart()
+{
+ Q_D(KQOAuthRequest);
+ if (d->timeout > 0) {
+ connect(&(d->timer), SIGNAL(timeout()), this, SIGNAL(requestTimedout()));
+ d->timer.start(d->timeout);
+ }
+}
+
+void KQOAuthRequest::requestTimerStop()
+{
+ Q_D(KQOAuthRequest);
+ if (d->timeout > 0) {
+ disconnect(&(d->timer), SIGNAL(timeout()), this, SIGNAL(requestTimedout()));
+ d->timer.stop();
+ }
+}