diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,19 @@ add_subdirectory(src) +##### libraries for OAuth #### +# Should we use OAuth libraries provided by pipacs(3rd party libraries)? +##### +option(USE_o2 "OAuth2 libraries by pipacs" ON) + +if (USE_o2) + INCLUDE_DIRECTORIES(${PROJECT_SOURCE_DIR}/src_o2) + add_subdirectory(src_o2) + set(EXTRA_LIBS ${EXTRA_LIBS} src_o2) +endif (USE_o2) + +######### + if (BUILD_TESTING) add_subdirectory(autotests) add_subdirectory(tests) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -21,7 +21,6 @@ rocketchatbackend.cpp notification.cpp messagequeue.cpp - authentication.cpp rocketchatmessage.cpp typingnotification.cpp changetemporarystatus.cpp @@ -52,6 +51,21 @@ textconverter.cpp loadrecenthistorymanager.cpp unityservicemanager.cpp + + #OAuth + google.cpp + ../src_o2/o0settingsstore.cpp + ../src_o2/o2google.cpp + ../src_o2/o0globals.h + ../src_o2/o0baseauth.cpp + ../src_o2/o0settingsstore.cpp + ../src_o2/o0simplecrypt.h + ../src_o2/o2simplecrypt.cpp + ../src_o2/o2reply.cpp + ../src_o2/o0abstractstore.h + ../src_o2/o2.cpp + ../src_o2/o1timedreply.cpp + ../src_o2/o2replyserver.cpp ) ecm_qt_declare_logging_category(Ruqola_core_srcs HEADER ruqola_debug.h IDENTIFIER RUQOLA_LOG CATEGORY_NAME org.kde.ruqola) diff --git a/src/authentication.h b/src/authentication.h deleted file mode 100644 --- a/src/authentication.h +++ /dev/null @@ -1,57 +0,0 @@ -/* - - * Copyright 2016 Riccardo Iaconelli - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation; either version 2 of - * the License or (at your option) version 3 or any later version - * accepted by the membership of KDE e.V. (or its successor approved - * by the membership of KDE e.V.), which shall act as a proxy - * defined in Section 14 of version 3 of the license. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#ifndef AUTHENTICATION_H -#define AUTHENTICATION_H - -#include - -class Authentication -{ -public: - Authentication(); - - /** - * @brief Extract info from Google Json API - */ - void getDataFromJson(); - - /** - * @brief Call DDPClient's @method method with OAuth params - */ - void OAuthLogin(); - - /** - * @brief Make requests to Google on behalf of user using access token - */ - void sendApiRequest(); - -private slots: - void onGranted(); - -private: - bool m_authGranted = false; - QString m_clientID; - QString m_clientSecret; -}; - -#endif // AUTHENTICATION_H diff --git a/src/authentication.cpp b/src/authentication.cpp deleted file mode 100644 --- a/src/authentication.cpp +++ /dev/null @@ -1,110 +0,0 @@ -/* - - * Copyright 2016 Riccardo Iaconelli - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation; either version 2 of - * the License or (at your option) version 3 or any later version - * accepted by the membership of KDE e.V. (or its successor approved - * by the membership of KDE e.V.), which shall act as a proxy - * defined in Section 14 of version 3 of the license. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#include "ruqola.h" -#include "authentication.h" -#include "ddpapi/ddpclient.h" -#include "ruqola_debug.h" -#include "rocketchataccount.h" - -#include -#include - -Authentication::Authentication() -{ - getDataFromJson(); -} - -void Authentication::getDataFromJson() -{ - QFile f(QStringLiteral(":/client_secret.json")); - - QString val; - if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { - val = QString::fromLatin1(f.readAll()); - } else { - qCWarning(RUQOLA_LOG) << "Impossible to read client_secret.json"; - //TODO exit ? - } - - QJsonDocument document = QJsonDocument::fromJson(val.toUtf8()); - QJsonObject object = document.object(); - const auto settingsObject = object[QStringLiteral("web")].toObject(); - const QUrl authUri(settingsObject[QStringLiteral("auth_uri")].toString()); - const QUrl tokenUri(settingsObject[QStringLiteral("token_uri")].toString()); - const auto clientID = settingsObject[QStringLiteral("client_id")].toString(); - const auto clientSecret(settingsObject[QStringLiteral("client_secret")].toString()); - const auto redirectUrls = settingsObject[QStringLiteral("redirect_uris")].toArray(); - const QUrl redirectUrl(redirectUrls[0].toString()); - -/* - QString clientID = QString("143580046552-s4rmnq5mg008u76id0d3rl63od985hc6.apps.googleusercontent.com"); - QString clientSecret = QString("nyVm19iOjjtldcCZJ-7003xg"); - QString redirectUrl = QString("http://localhost:8080/cb/_oauth/google?close"); -*/ - - QSettings s; - s.setValue(QStringLiteral("clientID"), clientID); - m_clientID = clientID; - s.setValue(QStringLiteral("clientSecret"), clientSecret); - m_clientSecret = clientSecret; - s.setValue(QStringLiteral("redirectUrl"), redirectUrl); -} - -void Authentication::OAuthLogin() -{ - QJsonObject auth; - QJsonObject authKeys; - authKeys[QStringLiteral("credentialToken")] = m_clientID; - authKeys[QStringLiteral("credentialSecret")] = m_clientSecret; - - auth[QStringLiteral("oauth")] = authKeys; - qCDebug(RUQOLA_LOG) << "-------------------------"; - qCDebug(RUQOLA_LOG) << "-------------------------"; - qCDebug(RUQOLA_LOG) << "OAuth Json" << auth; - Ruqola::self()->rocketChatAccount()->ddp()->method(QStringLiteral("login"), QJsonDocument(auth)); - - QJsonArray requestPermissions; - requestPermissions.append(QStringLiteral("email")); - - QUuid state; - state = state.createUuid(); - QSettings s; - s.setValue(QStringLiteral("stateRandomNumber"), state); - - QJsonObject loginUrlParameters; - loginUrlParameters[QStringLiteral("client_id")] = m_clientID; - loginUrlParameters[QStringLiteral("response_type")] = QStringLiteral("code"); - loginUrlParameters[QStringLiteral("scope")] = QStringLiteral("openID profile email"); - loginUrlParameters[QStringLiteral("state")] = state.toString(); - - QJsonObject json; - json[QStringLiteral("requestPermissions")] = requestPermissions; - json[QStringLiteral("requestOfflineToken")] = true; - json[QStringLiteral("loginUrlParameters")] = loginUrlParameters; - json[QStringLiteral("loginHint")] = s.value(QStringLiteral("username")).toString(); - json[QStringLiteral("loginStyle")] = QStringLiteral("redirect"); - json[QStringLiteral("redirectUrl")] = s.value(QStringLiteral("redirectUrl")).toString(); - -// qCDebug(RUQOLA_LOG) << "OAuth Json" << json; -// Ruqola::self()->ddp()->method("login", QJsonDocument(json)); -} diff --git a/src/client_secret.json b/src/client_secret.json --- a/src/client_secret.json +++ b/src/client_secret.json @@ -1,10 +1,10 @@ {"web": - {"client_id":"143580046552-s4rmnq5mg008u76id0d3rl63od985hc6.apps.googleusercontent.com", + {"client_id":"788579520714-51vdlc1d4o5a86i0l77lp75ujmadu2qg.apps.googleusercontent.com", "project_id":"ruqola-161705", "auth_uri":"https://accounts.google.com/o/oauth2/auth", "token_uri":"https://accounts.google.com/o/oauth2/token", "auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs", - "client_secret":"nyVm19iOjjtldcCZJ-7003xg", + "client_secret":"rQ-k1MxaCvYDbRxrsBtTMPoE", "redirect_uris":["http://localhost:8080/cb/_oauth/google?close"] } } diff --git a/src/ddpapi/ddpclient.h b/src/ddpapi/ddpclient.h --- a/src/ddpapi/ddpclient.h +++ b/src/ddpapi/ddpclient.h @@ -54,8 +54,9 @@ enum LoginType { Password, - Google + GoogleOAuth }; + Q_ENUM(LoginType) explicit DDPClient(RocketChatAccount *account = nullptr, QObject *parent = nullptr); ~DDPClient(); @@ -92,6 +93,11 @@ void subscribe(const QString &collection, const QJsonArray ¶ms); /** + * @brief Calls method to log in the user with credentialToken and credentialSecret got from Google.cpp + */ + Q_INVOKABLE void loginGoogle(); + + /** * @brief Calls method to log in the user with valid username and password */ Q_INVOKABLE void login(); @@ -134,6 +140,11 @@ */ QString cachePath() const; + /** + *@brief used to set loginType to GoogleOAuth when user presses "Login with Google" button + * */ + Q_INVOKABLE void setLoginType(LoginType t); + quint64 leaveRoom(const QString &roomID); quint64 hideRoom(const QString &roomID); quint64 clearUnreadMessages(const QString &roomID); @@ -197,7 +208,7 @@ void setLoginStatus(LoginStatus l); LoginType loginType() const; - Q_INVOKABLE void setLoginType(LoginType t); + QString mUrl; AbstractWebSocket *mWebSocket = nullptr; diff --git a/src/ddpapi/ddpclient.cpp b/src/ddpapi/ddpclient.cpp --- a/src/ddpapi/ddpclient.cpp +++ b/src/ddpapi/ddpclient.cpp @@ -31,6 +31,8 @@ #include "messagequeue.h" #include "ruqolalogger.h" #include "rocketchatbackend.h" +#include "google.h" +#include "config-ruqola.h" #include #include @@ -539,7 +541,12 @@ if (root.value(QLatin1String("error")).toObject().value(QLatin1String("error")).toInt() == 403) { qCDebug(RUQOLA_DDPAPI_LOG) << "Wrong password or token expired"; - login(); // Let's keep trying to log in + if (loginType() == DDPClient::LoginType::GoogleOAuth) { + loginGoogle(); // Let's keep trying to log in + } else { + login(); // Let's keep trying to log in + } + } else { const QString token = root.value(QLatin1String("result")).toObject().value(QLatin1String("token")).toString(); mRocketChatAccount->settings()->setAuthToken(token); @@ -556,7 +563,11 @@ setLoginStatus(DDPClient::LoggingIn); //Ruqola::self()->authentication()->OAuthLogin(); - login(); // Try to resume auth token login + if (loginType() == DDPClient::LoginType::GoogleOAuth) { + loginGoogle(); // Let's keep trying to log in + } else { + login(); // Let's keep trying to log in + } } else if (messageType == QLatin1String("error")) { qCDebug(RUQOLA_DDPAPI_LOG) << "ERROR!!" << message; } else if (messageType == QLatin1String("ping")) { @@ -584,6 +595,21 @@ return method(QStringLiteral("loadHistory"), QJsonDocument(params), process_backlog); } +void DDPClient::loginGoogle() +{ +#ifdef USE_o2 //Only, if set this variable to ON(in cmakeLists.txt in root dir) we can use OAuth2 (by default it is ON) + Google *api = new Google(this); + api->doOAuth(O2::GrantFlowAuthorizationCode); + + //When this signal is emitted from google.cpp it means it has called the login 'method' + //by sending credentialToken and credentialSecret + connect(api, &Google::loginMethodCalled, [=] { + m_loginJob = api->oauthLoginJob; + }); +#endif + +} + void DDPClient::login() { if (!mRocketChatAccount->settings()->password().isEmpty()) { diff --git a/src/google.h b/src/google.h new file mode 100644 --- /dev/null +++ b/src/google.h @@ -0,0 +1,71 @@ +/* + * Copyright 2016 Riccardo Iaconelli + * Copyright 2018 Veluri Mithun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef Google_H +#define Google_H + +#include +#include "config-ruqola.h" + +#ifdef USE_o2 //Only, if set this variable to ON(in cmakeLists.txt in root dir) we can use OAuth2 (by default it is ON) +#include "../src_o2/o2google.h" +#include "../src_o2/o0baseauth.h" + + +class Google : public QObject +{ + Q_OBJECT + +public: + explicit Google(QObject *parent = nullptr); + void getDataFromJson(); + + unsigned oauthLoginJob; +Q_SIGNALS: + void extraTokensReady(const QVariantMap &extraTokens); + void linkingFailed(); + void linkingSucceeded(); + void loginMethodCalled(); + +public Q_SLOTS: + void doOAuth(O2::GrantFlow grantFlowType); + void validateToken(); + +private Q_SLOTS: + void onLinkedChanged(); + void onLinkingSucceeded(); + void onOpenBrowser(const QUrl &url); + void onCloseBrowser(); + void onFinished(); + + void OAuthLoginMethodParameter(); +private: + O2Google *p_o2Google = nullptr; + QString m_clientID; + QString m_clientSecret; + QString m_authUri; + QString m_tokenUri; + QString m_accessToken; + bool m_isValidToken = false; +}; +#endif // USE_o2 +#endif // Google_H diff --git a/src/google.cpp b/src/google.cpp new file mode 100644 --- /dev/null +++ b/src/google.cpp @@ -0,0 +1,209 @@ +/* + * Copyright 2016 Riccardo Iaconelli + * Copyright 2018 Veluri Mithun + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "google.h" +#include "ruqola.h" +#include "ruqola_debug.h" +#include "ruqola_ddpapi_debug.h" +#include "ddpapi/ddpclient.h" +#include "rocketchataccount.h" +#include "config-ruqola.h" + +#ifdef USE_o2 //Only, if set this variable to ON(in cmakeLists.txt in root dir) we can use OAuth2 (by default it is ON) +#include "../src_o2/o0globals.h" +#include "../src_o2/o0settingsstore.h" +#include "../src_o2/o2google.h" + + +Google::Google(QObject *parent) + :QObject(parent) { + p_o2Google = new O2Google(this); + + getDataFromJson(); + + p_o2Google->setClientId(m_clientID); + p_o2Google->setClientSecret(m_clientSecret); + p_o2Google->setLocalPort(8888); //it is from redirect url(http://127.0.0.1:8888/) + p_o2Google->setRequestUrl(m_authUri); // Use the desktop login UI + p_o2Google->setScope(QStringLiteral("email")); + + // Create a store object for writing the received tokens + O0SettingsStore *store = new O0SettingsStore(O2_ENCRYPTION_KEY); + store->setGroupKey(QStringLiteral("Google")); + p_o2Google->setStore(store); + + connect(p_o2Google, &O2Google::linkedChanged, this, &Google::onLinkedChanged); + connect(p_o2Google, SIGNAL(linkingFailed()), this, SIGNAL(linkingFailed())); + connect(p_o2Google, &O2Google::linkingSucceeded, this, &Google::onLinkingSucceeded); + connect(p_o2Google, &O2Google::openBrowser, this, &Google::onOpenBrowser); + connect(p_o2Google, &O2Google::closeBrowser, this, &Google::onCloseBrowser); + connect(p_o2Google, &O2Google::linkingSucceeded, this, &Google::OAuthLoginMethodParameter); +} + +void Google::getDataFromJson() +{ + QFile f(QStringLiteral(":/client_secret.json")); + + QString val; + if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { + val = QString::fromLatin1(f.readAll()); + } else { + qWarning(RUQOLA_LOG()) << "Impossible to read client_secret.json"; + //TODO exit ? + } + + //******github******* + //38a607244195a0d7af8 > clientID + //bb617841568d7c1e0c0888f292cf69b7b11d327e3 > clientSecret + //https://github.com/login/oauth/authorize + //https://github.com/login/oauth/access_token + QJsonDocument document = QJsonDocument::fromJson(val.toUtf8()); + QJsonObject object = document.object(); + const auto settingsObject = object[QStringLiteral("web")].toObject(); + const auto authUri(settingsObject[QStringLiteral("auth_uri")].toString()); + const auto clientID = settingsObject[QStringLiteral("client_id")].toString(); + const auto clientSecret(settingsObject[QStringLiteral("client_secret")].toString()); + + m_clientID = clientID; + m_clientSecret = clientSecret; + m_authUri = authUri; + m_tokenUri = QStringLiteral("https://accounts.google.com/o/oauth2/token"); +} + +void Google::doOAuth(O2::GrantFlow grantFlowType) { + qDebug(RUQOLA_LOG()) << QStringLiteral("Starting OAuth 2 with grant flow type:") << QStringLiteral("Authorization Grant Flow") + << QStringLiteral("..."); + p_o2Google->setGrantFlow(grantFlowType); + p_o2Google->unlink(); + + //TODO: refresh the token if it is expired(not valid) + validateToken(); + if (m_isValidToken) { + OAuthLoginMethodParameter(); + } else { + p_o2Google->link(); + } + + +} + +//currently not used +void Google::validateToken() { + if (!p_o2Google->linked()) { + qWarning(RUQOLA_LOG()) << "ERROR: Application is not linked!"; + Q_EMIT linkingFailed(); + return; + } + + QString accessToken = p_o2Google->token(); + QString debugUrlStr = QString(m_tokenUri).arg(accessToken); + QNetworkRequest request = QNetworkRequest(QUrl(debugUrlStr)); + QNetworkAccessManager *mgr = new QNetworkAccessManager(this); + QNetworkReply *reply = mgr->get(request); + connect(reply, &QNetworkReply::finished, this, &Google::onFinished); + qDebug(RUQOLA_LOG()) << QStringLiteral("Validating user token. Please wait..."); +} + +void Google::onOpenBrowser(const QUrl &url) { + QDesktopServices::openUrl(url); +} + +void Google::onCloseBrowser() { + //TODO: close the browser +} + +void Google::onLinkedChanged() { + qDebug(RUQOLA_LOG()) << QStringLiteral("Link changed!"); +} + +void Google::onLinkingSucceeded() { + O2Google *o1t = qobject_cast(sender()); + if (!o1t->linked()) { + return; + } + m_accessToken = o1t->token(); + QVariantMap extraTokens = o1t->extraTokens(); + if (!extraTokens.isEmpty()) { + Q_EMIT extraTokensReady(extraTokens); + qDebug(RUQOLA_LOG()) << QStringLiteral("Extra tokens in response:"); + foreach (const QString &key, extraTokens.keys()) { + qDebug(RUQOLA_LOG()) << key << QStringLiteral(":") + << (extraTokens.value(key).toString().left(3) + QStringLiteral("...")); + } + } +} + +//currently not used +void Google::onFinished() { + QNetworkReply *reply = qobject_cast(sender()); + if (!reply) { + qWarning(RUQOLA_LOG()) << QStringLiteral("NULL reply!"); + Q_EMIT linkingFailed(); + return; + } + + reply->deleteLater(); + if (reply->error() != QNetworkReply::NoError) { + qWarning(RUQOLA_LOG()) << QStringLiteral("Reply error:") << reply->error(); + qWarning(RUQOLA_LOG()) << QStringLiteral("Reason:") << reply->errorString(); + Q_EMIT linkingFailed(); + return; + } + + QByteArray replyData = reply->readAll(); + bool valid = !replyData.contains("error"); + if (valid) { + qDebug(RUQOLA_LOG()) << QStringLiteral("Token is valid"); + Q_EMIT linkingSucceeded(); + m_isValidToken = true; + } else { + qDebug(RUQOLA_LOG()) << QStringLiteral("Token is invalid"); + Q_EMIT linkingFailed(); + } +} + +void Google::OAuthLoginMethodParameter() +{ + QJsonObject auth; + QJsonObject authKeys;// + authKeys[QStringLiteral("credentialToken")] = m_accessToken; + authKeys[QStringLiteral("credentialSecret")] = m_clientSecret; + + auth[QStringLiteral("oauth")] = authKeys; + qDebug(RUQOLA_DDPAPI_LOG()) << "-------------------------"; + qDebug(RUQOLA_DDPAPI_LOG()) << "-------------------------"; + qDebug(RUQOLA_DDPAPI_LOG()) << "OAuth Json" << auth; + + oauthLoginJob = Ruqola::self()->rocketChatAccount()->ddp()->method(QStringLiteral("login"), QJsonDocument(auth)); + Q_EMIT loginMethodCalled(); +} + +#endif//USE_o2 diff --git a/src/qml/Login.qml b/src/qml/Login.qml --- a/src/qml/Login.qml +++ b/src/qml/Login.qml @@ -1,5 +1,4 @@ /* - * Copyright 2016 Riccardo Iaconelli * Copyright (c) 2017-2018 Montel Laurent * @@ -21,24 +20,23 @@ * */ - import QtQuick 2.9 import QtQuick.Controls 1.3 +import KDE.Ruqola.Ruqola 1.0 import org.kde.kirigami 2.1 as Kirigami import KDE.Ruqola.DDPClient 1.0 import KDE.Ruqola.RocketChatAccount 1.0 Kirigami.Page { id: loginForm property QtObject rcAccount + property QtObject rocketChatAccount: Ruqola.rocketChatAccount() property alias username: usernameField.text; property alias password: passField.text; property alias serverUrl: urlField.text; property alias accountName: nameField.text; - signal accepted() - - + signal accepted() implicitHeight: 400 implicitWidth: 300 @@ -137,10 +135,29 @@ enabled: (passField.text && urlField.text && usernameField.text) onClicked: loginForm.accepted() isDefault: true - } + } Item { id: spacer3 + width: 30 + height: 30 + } + + Button { + id: googleButton + + width: parent.width + text: i18n("Login with Google") + enabled: true + onClicked: { + loginForm.accepted() + rocketChatAccount.setLoginTypeToGoogleOAuth() + } + isDefault: true + } + + Item { + id: spacer4 width: 30 height: 30 diff --git a/src/rocketchataccount.h b/src/rocketchataccount.h --- a/src/rocketchataccount.h +++ b/src/rocketchataccount.h @@ -123,6 +123,7 @@ Q_INVOKABLE void downloadFile(const QString &downloadFileUrl, const QUrl &localFile); Q_INVOKABLE void starMessage(const QString &messageId, const QString &rid, bool starred); Q_INVOKABLE void uploadFile(const QString &description, const QUrl &fileUrl); + Q_INVOKABLE void setLoginTypeToGoogleOAuth(); void loadEmoji(const QJsonObject &obj); void parsePublicSettings(const QJsonObject &obj); diff --git a/src/rocketchataccount.cpp b/src/rocketchataccount.cpp --- a/src/rocketchataccount.cpp +++ b/src/rocketchataccount.cpp @@ -286,6 +286,11 @@ return mDdp; } +void RocketChatAccount::setLoginTypeToGoogleOAuth() +{ + ddp()->setLoginType(DDPClient::LoginType::GoogleOAuth); +} + DDPClient::LoginStatus RocketChatAccount::loginStatus() { if (mDdp) { diff --git a/src_o2/CMakeLists.txt b/src_o2/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/src_o2/CMakeLists.txt @@ -0,0 +1,49 @@ +cmake_minimum_required(VERSION 2.8.11) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") + +find_package(Qt5Keychain REQUIRED) +find_package(Qt5Core REQUIRED) +find_package(Qt5Network REQUIRED) + + +set( o2_SRCS + o2.cpp + o2reply.cpp + o2replyserver.cpp + o2simplecrypt.cpp + o0settingsstore.cpp + o0baseauth.cpp + o2google.cpp + ) + +set( o2_HDRS + o2.h + o2reply.h + o2replyserver.h + o2requestor.h + o0abstractstore.h + o0baseauth.h + o0export.h + o0globals.h + o0settingsstore.h + o0simplecrypt.h + o2google.h + ) + + +add_library( o2 ${o2_SRCS} ${o2_HDRS} ) + + +target_link_libraries( o2 Qt5::Core Qt5::Network Qt5::Keychain) + +if (WIN32) + add_definations(-DO2_DLL_EXPORT) +endif() + +set_target_properties(o2 + PROPERTIES OUTPUT_NAME o2core VERSION ${RUQOLA_LIB_VERSION} SOVERSION ${RUQOLA_LIB_SOVERSION} + ) + diff --git a/src_o2/o0abstractstore.h b/src_o2/o0abstractstore.h new file mode 100644 --- /dev/null +++ b/src_o2/o0abstractstore.h @@ -0,0 +1,24 @@ +#ifndef O0ABSTRACTSTORE_H +#define O0ABSTRACTSTORE_H + +#include +#include + +#include "o0export.h" + +/// Storage for strings. +class O0_EXPORT O0AbstractStore: public QObject { + Q_OBJECT + +public: + explicit O0AbstractStore(QObject *parent = 0): QObject(parent) { + } + + /// Retrieve a string value by key. + virtual QString value(const QString &key, const QString &defaultValue = QString()) = 0; + + /// Set a string value for a key. + virtual void setValue(const QString &key, const QString &value) = 0; +}; + +#endif // O0ABSTRACTSTORE_H diff --git a/src_o2/o0baseauth.h b/src_o2/o0baseauth.h new file mode 100644 --- /dev/null +++ b/src_o2/o0baseauth.h @@ -0,0 +1,136 @@ +#ifndef O0BASEAUTH_H +#define O0BASEAUTH_H + +#include +#include +#include +#include +#include +#include + +#include "o0export.h" +#include "o0abstractstore.h" +#include "o0requestparameter.h" + +class O2ReplyServer; + +/// Base class of OAuth authenticators +class O0_EXPORT O0BaseAuth : public QObject { + Q_OBJECT + +public: + explicit O0BaseAuth(QObject *parent = 0, O0AbstractStore *store = 0); + +public: + /// Are we authenticated? + Q_PROPERTY(bool linked READ linked WRITE setLinked NOTIFY linkedChanged) + bool linked(); + + /// Authentication token. + Q_PROPERTY(QString token READ token NOTIFY tokenChanged) + QString token(); + + /// Authentication token secret. + Q_PROPERTY(QString tokenSecret READ tokenSecret NOTIFY tokenSecretChanged) + QString tokenSecret(); + + /// Provider-specific extra tokens, available after a successful authentication + Q_PROPERTY(QVariantMap extraTokens READ extraTokens NOTIFY extraTokensChanged) + QVariantMap extraTokens(); + + /// Client application ID. + /// O1 instances with the same (client ID, client secret) share the same "linked", "token" and "tokenSecret" properties. + Q_PROPERTY(QString clientId READ clientId WRITE setClientId NOTIFY clientIdChanged) + QString clientId(); + void setClientId(const QString &value); + + /// Client application secret. + /// O1 instances with the same (client ID, client secret) share the same "linked", "token" and "tokenSecret" properties. + Q_PROPERTY(QString clientSecret READ clientSecret WRITE setClientSecret NOTIFY clientSecretChanged) + QString clientSecret(); + void setClientSecret(const QString &value); + + /// Should we use a reply server (default) or an external web interceptor? + Q_PROPERTY(bool useExternalWebInterceptor READ useExternalWebInterceptor WRITE setUseExternalWebInterceptor) + bool useExternalWebInterceptor(); + void setUseExternalWebInterceptor(bool inUseExternalWebInterceptor); + + /// Page content on local host after successful oauth. + /// Provide it in case you do not want to close the browser, but display something + Q_PROPERTY(QByteArray replyContent READ replyContent WRITE setReplyContent) + QByteArray replyContent() const; + void setReplyContent(const QByteArray &value); + + /// TCP port number to use in local redirections. + /// The OAuth "redirect_uri" will be set to "http://localhost:/". + /// If localPort is set to 0 (default), O2 will replace it with a free one. + Q_PROPERTY(int localPort READ localPort WRITE setLocalPort NOTIFY localPortChanged) + int localPort(); + void setLocalPort(int value); + + /// Sets the storage object to use for storing the OAuth tokens on a peristent medium + void setStore(O0AbstractStore *store); + + /// Construct query string from list of headers + static QByteArray createQueryParameters(const QList ¶meters); + +public Q_SLOTS: + /// Authenticate. + Q_INVOKABLE virtual void link() = 0; + + /// De-authenticate. + Q_INVOKABLE virtual void unlink() = 0; + +Q_SIGNALS: + /// Emitted when client needs to open a web browser window, with the given URL. + void openBrowser(const QUrl &url); + + /// Emitted when client can close the browser window. + void closeBrowser(); + + /// Emitted when authentication/deauthentication succeeded. + void linkingSucceeded(); + + /// Emitted when authentication/deauthentication failed. + void linkingFailed(); + + // Property change signals + + void linkedChanged(); + void clientIdChanged(); + void clientSecretChanged(); + void localPortChanged(); + void tokenChanged(); + void tokenSecretChanged(); + void extraTokensChanged(); + +protected: + /// Set authentication token. + void setToken(const QString &v); + + /// Set authentication token secret. + void setTokenSecret(const QString &v); + + /// Set the linked state + void setLinked(bool v); + + /// Set extra tokens found in OAuth response + void setExtraTokens(QVariantMap extraTokens); + +protected: + QString clientId_; + QString clientSecret_; + QString redirectUri_; + QString requestToken_; + QString requestTokenSecret_; + QUrl requestTokenUrl_; + QUrl authorizeUrl_; + QUrl accessTokenUrl_; + quint16 localPort_; + O0AbstractStore *store_; + QVariantMap extraTokens_; + bool useExternalWebInterceptor_; + O2ReplyServer *replyServer_; +}; + +#endif // O0BASEAUTH diff --git a/src_o2/o0baseauth.cpp b/src_o2/o0baseauth.cpp new file mode 100644 --- /dev/null +++ b/src_o2/o0baseauth.cpp @@ -0,0 +1,150 @@ +#include +#include +#include + +#include "o0baseauth.h" +#include "o0globals.h" +#include "o0settingsstore.h" +#include "o2replyserver.h" + +static const quint16 DefaultLocalPort = 1965; + +O0BaseAuth::O0BaseAuth(QObject *parent, O0AbstractStore *store): QObject(parent), store_(0), useExternalWebInterceptor_(false), replyServer_(NULL) { + localPort_ = DefaultLocalPort; + setStore(store); +} + +void O0BaseAuth::setStore(O0AbstractStore *store) { + if (store_) { + store_->deleteLater(); + } + if (store) { + store_ = store; + store_->setParent(this); + } else { + store_ = new O0SettingsStore(O2_ENCRYPTION_KEY, this); + return; + } +} + +bool O0BaseAuth::linked() { + QString key = QString(O2_KEY_LINKED).arg(clientId_); + bool result = !store_->value(key).isEmpty(); + qDebug() << "O0BaseAuth::linked:" << (result? "Yes": "No"); + return result; +} + +void O0BaseAuth::setLinked(bool v) { + qDebug() << "O0BaseAuth::setLinked:" << (v? "true": "false"); + bool oldValue = linked(); + QString key = QString(O2_KEY_LINKED).arg(clientId_); + store_->setValue(key, v? QStringLiteral("1"): QStringLiteral("")); + if (oldValue != v) { + Q_EMIT linkedChanged(); + } +} + +QString O0BaseAuth::tokenSecret() { + QString key = QString(O2_KEY_TOKEN_SECRET).arg(clientId_); + return store_->value(key); +} + +void O0BaseAuth::setTokenSecret(const QString &v) { + QString key = QString(O2_KEY_TOKEN_SECRET).arg(clientId_); + store_->setValue(key, v); + Q_EMIT tokenSecretChanged(); +} + +QString O0BaseAuth::token() { + QString key = QString(O2_KEY_TOKEN).arg(clientId_); + return store_->value(key); +} + +void O0BaseAuth::setToken(const QString &v) { + QString key = QString(O2_KEY_TOKEN).arg(clientId_); + store_->setValue(key, v); + Q_EMIT tokenChanged(); +} + +QString O0BaseAuth::clientId() { + return clientId_; +} + +void O0BaseAuth::setClientId(const QString &value) { + clientId_ = value; + Q_EMIT clientIdChanged(); +} + +QString O0BaseAuth::clientSecret() { + return clientSecret_; +} + +void O0BaseAuth::setClientSecret(const QString &value) { + clientSecret_ = value; + Q_EMIT clientSecretChanged(); +} + +bool O0BaseAuth::useExternalWebInterceptor() { + return useExternalWebInterceptor_; +} + +void O0BaseAuth::setUseExternalWebInterceptor(bool useExternalWebInterceptor) { + useExternalWebInterceptor_ = useExternalWebInterceptor; +} + +QByteArray O0BaseAuth::replyContent() const { + if(replyServer_ != NULL) { + return replyServer_->replyContent(); + } + + return QByteArray(); +} + +void O0BaseAuth::setReplyContent(const QByteArray &value) { + if(replyServer_ != NULL) { + return replyServer_->setReplyContent(value); + } +} + +int O0BaseAuth::localPort() { + return localPort_; +} + +void O0BaseAuth::setLocalPort(int value) { + qDebug() << "O0BaseAuth::setLocalPort:" << value; + localPort_ = value; + Q_EMIT localPortChanged(); +} + +QVariantMap O0BaseAuth::extraTokens() { + QString key = QString(O2_KEY_EXTRA_TOKENS).arg(clientId_); + QString value = store_->value(key); + QByteArray bytes = QByteArray::fromBase64(value.toLatin1()); + QDataStream stream(&bytes, QIODevice::ReadOnly); + stream >> extraTokens_; + return extraTokens_; +} + +void O0BaseAuth::setExtraTokens(QVariantMap extraTokens) { + extraTokens_ = extraTokens; + QByteArray bytes; + QDataStream stream(&bytes, QIODevice::WriteOnly); + stream << extraTokens; + QString key = QString(O2_KEY_EXTRA_TOKENS).arg(clientId_); + store_->setValue(key, QLatin1String(bytes.toBase64())); + Q_EMIT extraTokensChanged(); +} + +QByteArray O0BaseAuth::createQueryParameters(const QList ¶meters) { + QByteArray ret; + bool first = true; + foreach (O0RequestParameter h, parameters) { + if (first) { + first = false; + } else { + ret.append("&"); + } + ret.append(QUrl::toPercentEncoding(QLatin1String(h.name)) + "=" + QUrl::toPercentEncoding(QLatin1String(h.name))); + } + return ret; +} diff --git a/src_o2/o0export.h b/src_o2/o0export.h new file mode 100644 --- /dev/null +++ b/src_o2/o0export.h @@ -0,0 +1,15 @@ +#pragma once + +#ifndef O0_EXPORT +// For exporting symbols from Windows' DLLs +#ifdef _WIN32 + #ifdef O2_DLL_EXPORT + #define O0_EXPORT __declspec(dllexport) + #else + #define O0_EXPORT __declspec(dllimport) + #endif +#else + #define O0_EXPORT +#endif + +#endif // O0_EXPORT diff --git a/src_o2/o0globals.h b/src_o2/o0globals.h new file mode 100644 --- /dev/null +++ b/src_o2/o0globals.h @@ -0,0 +1,90 @@ +/* +* Copyright (c) 2012, Akos Polster +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef O0GLOBALS_H +#define O0GLOBALS_H + +#include + +// Common constants +const QString O2_ENCRYPTION_KEY = QStringLiteral("12345678"); +const QString O2_CALLBACK_URL = QStringLiteral("http://127.0.0.1:%1/"); +const QString O2_MIME_TYPE_XFORM = QStringLiteral("application/x-www-form-urlencoded"); +const QString O2_MIME_TYPE_JSON = QStringLiteral("application/json"); + +// QSettings key names +const QString O2_KEY_TOKEN = QStringLiteral("token.%1"); +const QString O2_KEY_TOKEN_SECRET = QStringLiteral("tokensecret.%1"); +const QString O2_KEY_CODE = QStringLiteral("code.%1"); +const QString O2_KEY_EXPIRES = QStringLiteral("expires.%1"); +const QString O2_KEY_REFRESH_TOKEN = QStringLiteral("refreshtoken.%1"); +const QString O2_KEY_LINKED = QStringLiteral("linked.%1"); +const QString O2_KEY_EXTRA_TOKENS = QStringLiteral("extratokens.%1"); + +// OAuth 1/1.1 Request Parameters +const QString O2_OAUTH_CALLBACK = QStringLiteral("oauth_callback"); +const QString O2_OAUTH_CONSUMER_KEY = QStringLiteral("oauth_consumer_key"); +const QString O2_OAUTH_NONCE = QStringLiteral("oauth_nonce"); +const QString O2_OAUTH_SIGNATURE = QStringLiteral("oauth_signature"); +const QString O2_OAUTH_SIGNATURE_METHOD = QStringLiteral("oauth_signature_method"); +const QString O2_OAUTH_TIMESTAMP = QStringLiteral("oauth_timestamp"); +const QString O2_OAUTH_VERSION = QStringLiteral("oauth_version"); +// OAuth 1/1.1 Response Parameters +const QString O2_OAUTH_TOKEN = QStringLiteral("oauth_token"); +const QString O2_OAUTH_TOKEN_SECRET = QStringLiteral("oauth_token_secret"); +const QString O2_OAUTH_CALLBACK_CONFIRMED = QStringLiteral("oauth_callback_confirmed"); +const QString O2_OAUTH_VERFIER = QStringLiteral("oauth_verifier"); + +// OAuth 2 Request Parameters +const QString O2_OAUTH2_RESPONSE_TYPE = QStringLiteral("response_type"); +const QString O2_OAUTH2_CLIENT_ID = QStringLiteral("client_id"); +const QString O2_OAUTH2_CLIENT_SECRET = QStringLiteral("client_secret"); +const QString O2_OAUTH2_USERNAME = QStringLiteral("username"); +const QString O2_OAUTH2_PASSWORD = QStringLiteral("password"); +const QString O2_OAUTH2_REDIRECT_URI = QStringLiteral("redirect_uri"); +const QString O2_OAUTH2_SCOPE = QStringLiteral("scope"); +const QString O2_OAUTH2_GRANT_TYPE_CODE = QStringLiteral("code"); +const QString O2_OAUTH2_GRANT_TYPE_TOKEN = QStringLiteral("token"); +const QString O2_OAUTH2_GRANT_TYPE_PASSWORD = QStringLiteral("password"); +const QString O2_OAUTH2_GRANT_TYPE = QStringLiteral("grant_type"); +const QString O2_OAUTH2_API_KEY = QStringLiteral("api_key"); + +// OAuth 2 Response Parameters +const QString O2_OAUTH2_ACCESS_TOKEN = QStringLiteral("access_token"); +const QString O2_OAUTH2_REFRESH_TOKEN = QStringLiteral("refresh_token"); +const QString O2_OAUTH2_EXPIRES_IN = QStringLiteral("expires_in"); + +// OAuth signature types +const QString O2_SIGNATURE_TYPE_HMAC_SHA1 = QStringLiteral("HMAC-SHA1"); +const QString O2_SIGNATURE_TYPE_PLAINTEXT = QStringLiteral("PLAINTEXT"); + +// Parameter values +const QString O2_AUTHORIZATION_CODE = QStringLiteral("authorization_code"); + +// Standard HTTP headers +const QString O2_HTTP_AUTHORIZATION_HEADER = QStringLiteral("Authorization"); + +#endif // O0GLOBALS_H diff --git a/src_o2/o0keychainstore.h b/src_o2/o0keychainstore.h new file mode 100644 --- /dev/null +++ b/src_o2/o0keychainstore.h @@ -0,0 +1,60 @@ +// +// Created by michaelpollind on 3/13/17. +// +#ifndef O2_O0KEYCHAINSTORE_H +#define O2_O0KEYCHAINSTORE_H + +#include +#include "o0abstractstore.h" +#include + +namespace QKeychain { +class Job; +} + +/// Calling persist(), fetchFromKeychain() and clearFromKeychain() member +/// functions is the responsibility of the user of this class. +/// This is important to minimize the number of keychain accesses (and +/// potentially the number of user password prompts). +/// For example: fetchFromKeychain() can be called immediately after +/// creating a keychain store; persist() - after a successful authorization; +/// clearFromKeychain() - when the user logs out from the service. +class O0_EXPORT o0keyChainStore : public O0AbstractStore{ + Q_OBJECT +public: + explicit o0keyChainStore(const QString& app,const QString& name,QObject *parent = 0); + + /// Retrieve a string value by key. + QString value(const QString &key, const QString &defaultValue = QString()); + + /// Set a string value for a key. + void setValue(const QString &key, const QString &value); + + // The functions below return QKeychain::Error casted to int. They don't + // return the enumerator directly because it can not be forward-declared reliably, + // and including into this header may be undesirable. + // Note that if 0 is returned, then there was no error. + + int persist(); + int fetchFromKeychain(); + int clearFromKeychain(); + + /// @return true if @p errorCode is equal to QKeychain::EntryNotFound. + /// @note This function can be used to single out one type of an error + /// returned from the functions above without including . + /// The EntryNotFound error type is special because it can be considered + /// not an error if returned from clearFromKeychain(). + static bool isEntryNotFoundError(int errorCode); + +private: + void initJob(QKeychain::Job &job) const; + int executeJob(QKeychain::Job &job, const char *actionName) const; + + QString app_; + QString name_; + QMap pairs_; + +}; + + +#endif //O2_O0KEYCHAINSTORE_H diff --git a/src_o2/o0keychainstore.cpp b/src_o2/o0keychainstore.cpp new file mode 100644 --- /dev/null +++ b/src_o2/o0keychainstore.cpp @@ -0,0 +1,79 @@ +// +// Created by michaelpollind on 3/13/17. +// +#include "o0keychainstore.h" + +#include +#include +#include +#include +#include + +using namespace QKeychain; + +o0keyChainStore::o0keyChainStore(const QString& app,const QString& name,QObject *parent): + O0AbstractStore(parent), app_(app),name_(name),pairs_() +{ +} + +QString o0keyChainStore::value(const QString &key, const QString &defaultValue) { + return pairs_.value(key, defaultValue); +} + +void o0keyChainStore::setValue(const QString &key, const QString &value) { + pairs_.insert(key,value); +} + +int o0keyChainStore::persist() { + WritePasswordJob job(app_); + initJob(job); + + QByteArray data; + QDataStream ds(&data,QIODevice::ReadWrite); + ds << pairs_; + job.setBinaryData(data); + + return executeJob(job, "persist"); +} + +int o0keyChainStore::fetchFromKeychain() { + ReadPasswordJob job(app_); + initJob(job); + const int result = executeJob(job, "fetch"); + if (result == 0) { // success + QByteArray data; + data.append(job.binaryData()); + QDataStream ds(&data, QIODevice::ReadOnly); + ds >> pairs_; + } + return result; +} + +int o0keyChainStore::clearFromKeychain() { + DeletePasswordJob job(app_); + initJob(job); + return executeJob(job, "clear"); +} + +bool o0keyChainStore::isEntryNotFoundError(int errorCode) { + return errorCode == QKeychain::EntryNotFound; +} + +void o0keyChainStore::initJob(QKeychain::Job &job) const { + job.setAutoDelete(false); + job.setKey(name_); +} + +int o0keyChainStore::executeJob(QKeychain::Job &job, const char *actionName) const { + QEventLoop loop; + job.connect( &job, SIGNAL(finished(QKeychain::Job*)), &loop, SLOT(quit()) ); + job.start(); + loop.exec(); + + const QKeychain::Error errorCode = job.error(); + if (errorCode != QKeychain::NoError) { + qWarning() << "keychain store could not" << actionName << name_ << ":" + << job.errorString() << "(" << errorCode << ")."; + } + return errorCode; +} diff --git a/src_o2/o0requestparameter.h b/src_o2/o0requestparameter.h new file mode 100644 --- /dev/null +++ b/src_o2/o0requestparameter.h @@ -0,0 +1,16 @@ +#ifndef O0REQUESTPARAMETER_H +#define O0REQUESTPARAMETER_H + +#include "o0export.h" + +/// Request parameter (name-value pair) participating in authentication. +struct O0_EXPORT O0RequestParameter { + O0RequestParameter(const QByteArray &n, const QByteArray &v): name(n), value(v) {} + bool operator <(const O0RequestParameter &other) const { + return (name == other.name)? (value < other.value): (name < other.name); + } + QByteArray name; + QByteArray value; +}; + +#endif // O0REQUESTPARAMETER_H diff --git a/src_o2/o0settingsstore.h b/src_o2/o0settingsstore.h new file mode 100644 --- /dev/null +++ b/src_o2/o0settingsstore.h @@ -0,0 +1,43 @@ +#ifndef O0SETTINGSSTORE_H +#define O0SETTINGSSTORE_H + +#include +#include + +#include "o0baseauth.h" +#include "o0abstractstore.h" +#include "o0simplecrypt.h" + +/// Persistent storage for authentication tokens, using QSettings. +class O0_EXPORT O0SettingsStore: public O0AbstractStore { + Q_OBJECT + +public: + /// Constructor + explicit O0SettingsStore(const QString &encryptionKey, QObject *parent = 0); + + /// Construct with an explicit QSettings instance + explicit O0SettingsStore(QSettings *settings, const QString &encryptionKey, QObject *parent = 0); + + /// Group key prefix + Q_PROPERTY(QString groupKey READ groupKey WRITE setGroupKey NOTIFY groupKeyChanged) + QString groupKey() const; + void setGroupKey(const QString &groupKey); + + /// Get a string value for a key + QString value(const QString &key, const QString &defaultValue = QString()); + + /// Set a string value for a key + void setValue(const QString &key, const QString &value); + +Q_SIGNALS: + // Property change signals + void groupKeyChanged(); + +protected: + QSettings* settings_; + QString groupKey_; + O0SimpleCrypt crypt_; +}; + +#endif // O0SETTINGSSTORE_H diff --git a/src_o2/o0settingsstore.cpp b/src_o2/o0settingsstore.cpp new file mode 100644 --- /dev/null +++ b/src_o2/o0settingsstore.cpp @@ -0,0 +1,54 @@ +#include +#include +#include + +#include "o0settingsstore.h" + +static quint64 getHash(const QString &encryptionKey) { + return QCryptographicHash::hash(encryptionKey.toLatin1(), QCryptographicHash::Sha1).toULongLong(); +} + +O0SettingsStore::O0SettingsStore(const QString &encryptionKey, QObject *parent): + O0AbstractStore(parent), crypt_(getHash(encryptionKey)) { + settings_ = new QSettings(this); +} + +O0SettingsStore::O0SettingsStore(QSettings *settings, const QString &encryptionKey, QObject *parent): + O0AbstractStore(parent), crypt_(getHash(encryptionKey)) { + settings_ = settings; + settings_->setParent(this); +} + +QString O0SettingsStore::groupKey() const { + return groupKey_; +} + +void O0SettingsStore::setGroupKey(const QString &groupKey) { + if (groupKey_ == groupKey) { + return; + } + groupKey_ = groupKey; + Q_EMIT groupKeyChanged(); +} + +QString O0SettingsStore::value(const QString &key, const QString &defaultValue) { + QString fullKey = groupKey_.isEmpty() ? key : (groupKey_ + QLatin1Char('/') + key); + if (!settings_->contains(fullKey)) { + return defaultValue; + } + return crypt_.decryptToString(settings_->value(fullKey).toString()); +} + +void O0SettingsStore::setValue(const QString &key, const QString &value) { + QString fullKey = groupKey_.isEmpty() ? key : (groupKey_ + QLatin1Char('/') + key); + settings_->setValue(fullKey, crypt_.encryptToString(value)); + + const QSettings::Status status = settings_->status(); + if (status != QSettings::NoError) { + qCritical() << "O0SettingsStore QSettings error:" << status; + if (status == QSettings::AccessError) { + qCritical() << "Did you forget to set organization name and application name " + "in QSettings or QCoreApplication?"; + } + } +} diff --git a/src_o2/o0simplecrypt.h b/src_o2/o0simplecrypt.h new file mode 100644 --- /dev/null +++ b/src_o2/o0simplecrypt.h @@ -0,0 +1,227 @@ +/* +Copyright (c) 2011, Andre Somers +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Rathenau Instituut, Andre Somers nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL ANDRE SOMERS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef SIMPLECRYPT_H +#define SIMPLECRYPT_H +#include +#include +#include + +#include "o0baseauth.h" + +/** + @short Simple encryption and decryption of strings and byte arrays + + This class provides a simple implementation of encryption and decryption + of strings and byte arrays. + + @warning The encryption provided by this class is NOT strong encryption. It may + help to shield things from curious eyes, but it will NOT stand up to someone + determined to break the encryption. Don't say you were not warned. + + The class uses a 64 bit key. Simply create an instance of the class, set the key, + and use the encryptToString() method to calculate an encrypted version of the input string. + To decrypt that string again, use an instance of SimpleCrypt initialized with + the same key, and call the decryptToString() method with the encrypted string. If the key + matches, the decrypted version of the string will be returned again. + + If you do not provide a key, or if something else is wrong, the encryption and + decryption function will return an empty string or will return a string containing nonsense. + lastError() will return a value indicating if the method was succesful, and if not, why not. + + SimpleCrypt is prepared for the case that the encryption and decryption + algorithm is changed in a later version, by prepending a version identifier to the cypertext. + */ +class O0_EXPORT O0SimpleCrypt +{ +public: + /** + CompressionMode describes if compression will be applied to the data to be + encrypted. + */ + enum CompressionMode { + CompressionAuto, /*!< Only apply compression if that results in a shorter plaintext. */ + CompressionAlways, /*!< Always apply compression. Note that for short inputs, a compression may result in longer data */ + CompressionNever /*!< Never apply compression. */ + }; + /** + IntegrityProtectionMode describes measures taken to make it possible to detect problems with the data + or wrong decryption keys. + + Measures involve adding a checksum or a cryptograhpic hash to the data to be encrypted. This + increases the length of the resulting cypertext, but makes it possible to check if the plaintext + appears to be valid after decryption. + */ + enum IntegrityProtectionMode { + ProtectionNone, /*!< The integerity of the encrypted data is not protected. It is not really possible to detect a wrong key, for instance. */ + ProtectionChecksum,/*!< A simple checksum is used to verify that the data is in order. If not, an empty string is returned. */ + ProtectionHash /*!< A cryptographic hash is used to verify the integrity of the data. This method produces a much stronger, but longer check */ + }; + /** + Error describes the type of error that occured. + */ + enum Error { + ErrorNoError, /*!< No error occurred. */ + ErrorNoKeySet, /*!< No key was set. You can not encrypt or decrypt without a valid key. */ + ErrorUnknownVersion, /*!< The version of this data is unknown, or the data is otherwise not valid. */ + ErrorIntegrityFailed /*!< The integrity check of the data failed. Perhaps the wrong key was used. */ + }; + + /** + Constructor. + + Constructs a SimpleCrypt instance without a valid key set on it. + */ + O0SimpleCrypt(); + /** + Constructor. + + Constructs a SimpleCrypt instance and initializes it with the given @arg key. + */ + explicit O0SimpleCrypt(quint64 key); + + /** + (Re-) initializes the key with the given @arg key. + */ + void setKey(quint64 key); + /** + Returns true if SimpleCrypt has been initialized with a key. + */ + bool hasKey() const {return !m_keyParts.isEmpty();} + + /** + Sets the compression mode to use when encrypting data. The default mode is Auto. + + Note that decryption is not influenced by this mode, as the decryption recognizes + what mode was used when encrypting. + */ + void setCompressionMode(CompressionMode mode) {m_compressionMode = mode;} + /** + Returns the CompressionMode that is currently in use. + */ + CompressionMode compressionMode() const {return m_compressionMode;} + + /** + Sets the integrity mode to use when encrypting data. The default mode is Checksum. + + Note that decryption is not influenced by this mode, as the decryption recognizes + what mode was used when encrypting. + */ + void setIntegrityProtectionMode(IntegrityProtectionMode mode) {m_protectionMode = mode;} + /** + Returns the IntegrityProtectionMode that is currently in use. + */ + IntegrityProtectionMode integrityProtectionMode() const {return m_protectionMode;} + + /** + Returns the last error that occurred. + */ + Error lastError() const {return m_lastError;} + + /** + Encrypts the @arg plaintext string with the key the class was initialized with, and returns + a cyphertext the result. The result is a base64 encoded version of the binary array that is the + actual result of the string, so it can be stored easily in a text format. + */ + QString encryptToString(const QString& plaintext) ; + /** + Encrypts the @arg plaintext QByteArray with the key the class was initialized with, and returns + a cyphertext the result. The result is a base64 encoded version of the binary array that is the + actual result of the encryption, so it can be stored easily in a text format. + */ + QString encryptToString(QByteArray plaintext) ; + /** + Encrypts the @arg plaintext string with the key the class was initialized with, and returns + a binary cyphertext in a QByteArray the result. + + This method returns a byte array, that is useable for storing a binary format. If you need + a string you can store in a text file, use encryptToString() instead. + */ + QByteArray encryptToByteArray(const QString& plaintext) ; + /** + Encrypts the @arg plaintext QByteArray with the key the class was initialized with, and returns + a binary cyphertext in a QByteArray the result. + + This method returns a byte array, that is useable for storing a binary format. If you need + a string you can store in a text file, use encryptToString() instead. + */ + QByteArray encryptToByteArray(QByteArray plaintext) ; + + /** + Decrypts a cyphertext string encrypted with this class with the set key back to the + plain text version. + + If an error occured, such as non-matching keys between encryption and decryption, + an empty string or a string containing nonsense may be returned. + */ + QString decryptToString(const QString& cyphertext) ; + /** + Decrypts a cyphertext string encrypted with this class with the set key back to the + plain text version. + + If an error occured, such as non-matching keys between encryption and decryption, + an empty string or a string containing nonsense may be returned. + */ + QByteArray decryptToByteArray(const QString& cyphertext) ; + /** + Decrypts a cyphertext binary encrypted with this class with the set key back to the + plain text version. + + If an error occured, such as non-matching keys between encryption and decryption, + an empty string or a string containing nonsense may be returned. + */ + QString decryptToString(QByteArray cypher) ; + /** + Decrypts a cyphertext binary encrypted with this class with the set key back to the + plain text version. + + If an error occured, such as non-matching keys between encryption and decryption, + an empty string or a string containing nonsense may be returned. + */ + QByteArray decryptToByteArray(QByteArray cypher) ; + + //enum to describe options that have been used for the encryption. Currently only one, but + //that only leaves room for future extensions like adding a cryptographic hash... + enum CryptoFlag{CryptoFlagNone = 0, + CryptoFlagCompression = 0x01, + CryptoFlagChecksum = 0x02, + CryptoFlagHash = 0x04 + }; + Q_DECLARE_FLAGS(CryptoFlags, CryptoFlag) +private: + + void splitKey(); + + quint64 m_key; + QVector m_keyParts; + CompressionMode m_compressionMode; + IntegrityProtectionMode m_protectionMode; + Error m_lastError; +}; +Q_DECLARE_OPERATORS_FOR_FLAGS(O0SimpleCrypt::CryptoFlags) + +#endif // SimpleCrypt_H diff --git a/src_o2/o1.h b/src_o2/o1.h new file mode 100644 --- /dev/null +++ b/src_o2/o1.h @@ -0,0 +1,135 @@ +#ifndef O1_H +#define O1_H + +#include +#include +#include + +#include "o0export.h" +#include "o0baseauth.h" + +/// Simple OAuth 1.0 authenticator. +class O0_EXPORT O1: public O0BaseAuth { + Q_OBJECT + +public: + /// HTTP User-Agent header + /// Set user agent to a value unique for your application (https://tools.ietf.org/html/rfc7231#section-5.5.3) + /// if you see the following error in the application log: + /// O1::onTokenRequestError: 201 "Error transferring requestTokenUrl() - server replied: Forbidden" "Bad bot" + Q_PROPERTY(QByteArray userAgent READ userAgent WRITE setUserAgent) + QByteArray userAgent() const; + void setUserAgent(const QByteArray &value); + + /// Signature method + Q_PROPERTY(QString signatureMethod READ signatureMethod WRITE setSignatureMethod NOTIFY signatureMethodChanged) + QString signatureMethod(); + void setSignatureMethod(const QString &value); + + /// Token request URL. + Q_PROPERTY(QUrl requestTokenUrl READ requestTokenUrl WRITE setRequestTokenUrl NOTIFY requestTokenUrlChanged) + QUrl requestTokenUrl(); + void setRequestTokenUrl(const QUrl &value); + + /// Parameters to pass with request URL. + Q_PROPERTY(QList requestParameters READ requestParameters WRITE setRequestParameters); + QList requestParameters(); + void setRequestParameters(const QList &value); + + /// Callback URL. + /// It should contain a `%1` place marker, to be replaced by `O0BaseAuth::localPort()`. + /// Defaults to `O2_CALLBACK_URL`. + Q_PROPERTY(QString callbackUrl READ callbackUrl WRITE setCallbackUrl) + QString callbackUrl(); + void setCallbackUrl(const QString &value); + + /// Authorization URL. + Q_PROPERTY(QUrl authorizeUrl READ authorizeUrl WRITE setAuthorizeUrl NOTIFY authorizeUrlChanged) + QUrl authorizeUrl(); + void setAuthorizeUrl(const QUrl &value); + + /// Access token URL. + Q_PROPERTY(QUrl accessTokenUrl READ accessTokenUrl WRITE setAccessTokenUrl NOTIFY accessTokenUrlChanged) + QUrl accessTokenUrl(); + void setAccessTokenUrl(const QUrl &value); + + /// Constructor. + explicit O1(QObject *parent = 0, QNetworkAccessManager *manager = 0, O0AbstractStore *store = 0); + + /// Parse a URL-encoded response string. + static QMap parseResponse(const QByteArray &response); + + /// Build the value of the "Authorization:" header. + static QByteArray buildAuthorizationHeader(const QList &oauthParams); + + /// Add common configuration (headers) to @p req. + void decorateRequest(QNetworkRequest &req, const QList &oauthParams); + + /// Create unique bytes to prevent replay attacks. + static QByteArray nonce(); + + /// Generate signature string depending on signature method type + QByteArray generateSignature(const QList headers, const QNetworkRequest &req, const QList &signingParameters, QNetworkAccessManager::Operation operation); + + /// Calculate the HMAC-SHA1 signature of a request. + /// @param oauthParams OAuth parameters. + /// @param otherParams Other parameters participating in signing. + /// @param URL Request URL. May contain query parameters, but they will not be used for signing. + /// @param op HTTP operation. + /// @param consumerSecret Consumer (application) secret. + /// @param tokenSecret Authorization token secret (empty if not yet available). + /// @return Signature that can be used as the value of the "oauth_signature" parameter. + static QByteArray sign(const QList &oauthParams, const QList &otherParams, const QUrl &url, QNetworkAccessManager::Operation op, const QString &consumerSecret, const QString &tokenSecret); + + /// Build a base string for signing. + static QByteArray getRequestBase(const QList &oauthParams, const QList &otherParams, const QUrl &url, QNetworkAccessManager::Operation op); + + /// Build a concatenated/percent-encoded string from a list of headers. + static QByteArray encodeHeaders(const QList &headers); + +public Q_SLOTS: + /// Authenticate. + Q_INVOKABLE virtual void link(); + + /// De-authenticate. + Q_INVOKABLE virtual void unlink(); + +Q_SIGNALS: + void requestTokenUrlChanged(); + void authorizeUrlChanged(); + void accessTokenUrlChanged(); + void signatureMethodChanged(); + +public Q_SLOTS: + /// Handle verification received from the reply server. + virtual void onVerificationReceived(QMap params); + +protected Q_SLOTS: + /// Handle token request error. + virtual void onTokenRequestError(QNetworkReply::NetworkError error); + + /// Handle token request finished. + virtual void onTokenRequestFinished(); + + /// Handle token exchange error. + void onTokenExchangeError(QNetworkReply::NetworkError error); + + /// Handle token exchange finished. + void onTokenExchangeFinished(); + +protected: + /// Exchange temporary token to authentication token + void exchangeToken(); + + QByteArray userAgent_; + QUrl requestUrl_; + QList requestParameters_; + QString callbackUrl_; + QUrl tokenUrl_; + QUrl refreshTokenUrl_; + QString verifier_; + QString signatureMethod_; + QNetworkAccessManager *manager_; +}; + +#endif // O1_H diff --git a/src_o2/o1.cpp b/src_o2/o1.cpp new file mode 100644 --- /dev/null +++ b/src_o2/o1.cpp @@ -0,0 +1,420 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if QT_VERSION >= 0x050000 +#include +#endif + +#if QT_VERSION >= 0x050100 +#include +#endif + +#include "o1.h" +#include "o2replyserver.h" +#include "o0globals.h" +#include "o0settingsstore.h" + +O1::O1(QObject *parent, QNetworkAccessManager *manager, O0AbstractStore *store): O0BaseAuth(parent, store) { + setSignatureMethod(O2_SIGNATURE_TYPE_HMAC_SHA1); + manager_ = manager ? manager : new QNetworkAccessManager(this); + qRegisterMetaType("QNetworkReply::NetworkError"); + + setCallbackUrl(O2_CALLBACK_URL); +} + +QByteArray O1::userAgent() const { + return userAgent_; +} + +void O1::setUserAgent(const QByteArray &v) { + userAgent_ = v; +} + +QUrl O1::requestTokenUrl() { + return requestTokenUrl_; +} + +void O1::setRequestTokenUrl(const QUrl &v) { + requestTokenUrl_ = v; + Q_EMIT requestTokenUrlChanged(); +} + +QList O1::requestParameters() { + return requestParameters_; +} + +void O1::setRequestParameters(const QList &v) { + requestParameters_ = v; +} + +QString O1::callbackUrl() { + return callbackUrl_; +} + +void O1::setCallbackUrl(const QString &v) { + callbackUrl_ = v; +} + +QUrl O1::authorizeUrl() { + return authorizeUrl_; +} + +void O1::setAuthorizeUrl(const QUrl &value) { + authorizeUrl_ = value; + Q_EMIT authorizeUrlChanged(); +} + +QUrl O1::accessTokenUrl() { + return accessTokenUrl_; +} + +void O1::setAccessTokenUrl(const QUrl &value) { + accessTokenUrl_ = value; + Q_EMIT accessTokenUrlChanged(); +} + +QString O1::signatureMethod() { + return signatureMethod_; +} + +void O1::setSignatureMethod(const QString &value) { + qDebug() << "O1::setSignatureMethod: " << value; + signatureMethod_ = value; +} + +void O1::unlink() { + qDebug() << "O1::unlink"; + setLinked(false); + setToken(""); + setTokenSecret(""); + setExtraTokens(QVariantMap()); + Q_EMIT linkingSucceeded(); +} + +#if QT_VERSION < 0x050100 +/// Calculate the HMAC variant of SHA1 hash. +/// @author http://qt-project.org/wiki/HMAC-SHA1. +/// @copyright Creative Commons Attribution-ShareAlike 2.5 Generic. +static QByteArray hmacSha1(QByteArray key, QByteArray baseString) { + int blockSize = 64; + if (key.length() > blockSize) { + key = QCryptographicHash::hash(key, QCryptographicHash::Sha1); + } + QByteArray innerPadding(blockSize, char(0x36)); + QByteArray outerPadding(blockSize, char(0x5c)); + for (int i = 0; i < key.length(); i++) { + innerPadding[i] = innerPadding[i] ^ key.at(i); + outerPadding[i] = outerPadding[i] ^ key.at(i); + } + QByteArray total = outerPadding; + QByteArray part = innerPadding; + part.append(baseString); + total.append(QCryptographicHash::hash(part, QCryptographicHash::Sha1)); + QByteArray hashed = QCryptographicHash::hash(total, QCryptographicHash::Sha1); + return hashed.toBase64(); +} +#endif + +/// Get HTTP operation name. +static QString getOperationName(QNetworkAccessManager::Operation op) { + switch (op) { + case QNetworkAccessManager::GetOperation: return "GET"; + case QNetworkAccessManager::PostOperation: return "POST"; + case QNetworkAccessManager::PutOperation: return "PUT"; + case QNetworkAccessManager::DeleteOperation: return "DEL"; + default: return ""; + } +} + +/// Build a concatenated/percent-encoded string from a list of headers. +QByteArray O1::encodeHeaders(const QList &headers) { + return QUrl::toPercentEncoding(createQueryParameters(headers)); +} + +/// Build a base string for signing. +QByteArray O1::getRequestBase(const QList &oauthParams, const QList &otherParams, const QUrl &url, QNetworkAccessManager::Operation op) { + QByteArray base; + + // Initialize base string with the operation name (e.g. "GET") and the base URL + base.append(getOperationName(op).toUtf8() + "&"); + base.append(QUrl::toPercentEncoding(url.toString(QUrl::RemoveQuery)) + "&"); + + // Append a sorted+encoded list of all request parameters to the base string + QList headers(oauthParams); + headers.append(otherParams); + std::sort(headers.begin(), headers.end()); + base.append(encodeHeaders(headers)); + + return base; +} + +QByteArray O1::sign(const QList &oauthParams, const QList &otherParams, const QUrl &url, QNetworkAccessManager::Operation op, const QString &consumerSecret, const QString &tokenSecret) { + QByteArray baseString = getRequestBase(oauthParams, otherParams, url, op); + QByteArray secret = QUrl::toPercentEncoding(consumerSecret) + "&" + QUrl::toPercentEncoding(tokenSecret); +#if QT_VERSION >= 0x050100 + return QMessageAuthenticationCode::hash(baseString, secret, QCryptographicHash::Sha1).toBase64(); +#else + return hmacSha1(secret, baseString); +#endif +} + +QByteArray O1::buildAuthorizationHeader(const QList &oauthParams) { + bool first = true; + QByteArray ret("OAuth "); + QList headers(oauthParams); + std::sort(headers.begin(), headers.end()); + foreach (O0RequestParameter h, headers) { + if (first) { + first = false; + } else { + ret.append(","); + } + ret.append(h.name); + ret.append("=\""); + ret.append(QUrl::toPercentEncoding(h.value)); + ret.append("\""); + } + return ret; +} + +void O1::decorateRequest(QNetworkRequest &req, const QList &oauthParams) { + req.setRawHeader(O2_HTTP_AUTHORIZATION_HEADER, buildAuthorizationHeader(oauthParams)); + if (!userAgent_.isEmpty()) { +#if QT_VERSION >= 0x050000 + req.setHeader(QNetworkRequest::UserAgentHeader, userAgent_); +#else + req.setRawHeader("User-Agent", userAgent_); +#endif + } +} + +QByteArray O1::generateSignature(const QList headers, const QNetworkRequest &req, const QList &signingParameters, QNetworkAccessManager::Operation operation) { + QByteArray signature; + if (signatureMethod() == O2_SIGNATURE_TYPE_HMAC_SHA1) { + signature = sign(headers, signingParameters, req.url(), operation, clientSecret(), tokenSecret()); + } else if (signatureMethod() == O2_SIGNATURE_TYPE_PLAINTEXT) { + signature = clientSecret().toLatin1() + "&" + tokenSecret().toLatin1(); + } + return signature; +} + +void O1::link() { + qDebug() << "O1::link"; + + // Create the reply server if it doesn't exist + // and we don't use an external web interceptor + if(!useExternalWebInterceptor_) { + if(replyServer_ == NULL) { + replyServer_ = new O2ReplyServer(this); + connect(replyServer_, SIGNAL(verificationReceived(QMap)), this, SLOT(onVerificationReceived(QMap))); + } + } + + if (linked()) { + qDebug() << "O1::link: Linked already"; + Q_EMIT linkingSucceeded(); + return; + } + + setLinked(false); + setToken(""); + setTokenSecret(""); + setExtraTokens(QVariantMap()); + + if (!useExternalWebInterceptor_) { + // Start reply server + if (!replyServer_->isListening()) + replyServer_->listen(QHostAddress::Any, localPort()); + } + + // Get any query parameters for the request +#if QT_VERSION >= 0x050000 + QUrlQuery requestData; +#else + QUrl requestData = requestTokenUrl(); +#endif + O0RequestParameter param("", ""); + foreach(param, requestParameters()) + requestData.addQueryItem(QString(param.name), QUrl::toPercentEncoding(QString(param.value))); + + // Get the request url and add parameters +#if QT_VERSION >= 0x050000 + QUrl requestUrl = requestTokenUrl(); + requestUrl.setQuery(requestData); + // Create request + QNetworkRequest request(requestUrl); +#else + // Create request + QNetworkRequest request(requestData); +#endif + + // Create initial token request + QList headers; + headers.append(O0RequestParameter(O2_OAUTH_CALLBACK, callbackUrl().arg(localPort()).toLatin1())); + headers.append(O0RequestParameter(O2_OAUTH_CONSUMER_KEY, clientId().toLatin1())); + headers.append(O0RequestParameter(O2_OAUTH_NONCE, nonce())); + headers.append(O0RequestParameter(O2_OAUTH_TIMESTAMP, QString::number(QDateTime::currentDateTimeUtc().toTime_t()).toLatin1())); + headers.append(O0RequestParameter(O2_OAUTH_VERSION, "1.0")); + headers.append(O0RequestParameter(O2_OAUTH_SIGNATURE_METHOD, signatureMethod().toLatin1())); + headers.append(O0RequestParameter(O2_OAUTH_SIGNATURE, generateSignature(headers, request, requestParameters(), QNetworkAccessManager::PostOperation))); + + // Clear request token + requestToken_.clear(); + requestTokenSecret_.clear(); + + // Post request + decorateRequest(request, headers); + request.setHeader(QNetworkRequest::ContentTypeHeader, O2_MIME_TYPE_XFORM); + QNetworkReply *reply = manager_->post(request, QByteArray()); + connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onTokenRequestError(QNetworkReply::NetworkError))); + connect(reply, SIGNAL(finished()), this, SLOT(onTokenRequestFinished())); +} + +void O1::onTokenRequestError(QNetworkReply::NetworkError error) { + QNetworkReply *reply = qobject_cast(sender()); + qWarning() << "O1::onTokenRequestError:" << (int)error << reply->errorString() << reply->readAll(); + Q_EMIT linkingFailed(); +} + +void O1::onTokenRequestFinished() { + qDebug() << "O1::onTokenRequestFinished"; + QNetworkReply *reply = qobject_cast(sender()); + qDebug() << QString( "Request: %1" ).arg(reply->request().url().toString()); + reply->deleteLater(); + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "O1::onTokenRequestFinished: " << reply->errorString(); + return; + } + + // Get request token and secret + QByteArray data = reply->readAll(); + QMap response = parseResponse(data); + requestToken_ = response.value(O2_OAUTH_TOKEN, ""); + requestTokenSecret_ = response.value(O2_OAUTH_TOKEN_SECRET, ""); + setToken(requestToken_); + setTokenSecret(requestTokenSecret_); + + // Checking for "oauth_callback_confirmed" is present and set to true + QString oAuthCbConfirmed = response.value(O2_OAUTH_CALLBACK_CONFIRMED, "false"); + if (requestToken_.isEmpty() || requestTokenSecret_.isEmpty() || (oAuthCbConfirmed == "false")) { + qWarning() << "O1::onTokenRequestFinished: No oauth_token, oauth_token_secret or oauth_callback_confirmed in response :" << data; + Q_EMIT linkingFailed(); + return; + } + + // Continue authorization flow in the browser + QUrl url(authorizeUrl()); +#if QT_VERSION < 0x050000 + url.addQueryItem(O2_OAUTH_TOKEN, requestToken_); + url.addQueryItem(O2_OAUTH_CALLBACK, callbackUrl().arg(localPort()).toLatin1()); +#else + QUrlQuery query(url); + query.addQueryItem(O2_OAUTH_TOKEN, requestToken_); + query.addQueryItem(O2_OAUTH_CALLBACK, callbackUrl().arg(localPort()).toLatin1()); + url.setQuery(query); +#endif + Q_EMIT openBrowser(url); +} + +void O1::onVerificationReceived(QMap params) { + qDebug() << "O1::onVerificationReceived"; + Q_EMIT closeBrowser(); + verifier_ = params.value(O2_OAUTH_VERFIER, ""); + if (params.value(O2_OAUTH_TOKEN) == requestToken_) { + // Exchange request token for access token + exchangeToken(); + } else { + qWarning() << "O1::onVerificationReceived: oauth_token missing or doesn't match"; + Q_EMIT linkingFailed(); + } +} + +void O1::exchangeToken() { + qDebug() << "O1::exchangeToken"; + + // Create token exchange request + QNetworkRequest request(accessTokenUrl()); + QList oauthParams; + oauthParams.append(O0RequestParameter(O2_OAUTH_CONSUMER_KEY, clientId().toLatin1())); + oauthParams.append(O0RequestParameter(O2_OAUTH_VERSION, "1.0")); + oauthParams.append(O0RequestParameter(O2_OAUTH_TIMESTAMP, QString::number(QDateTime::currentDateTimeUtc().toTime_t()).toLatin1())); + oauthParams.append(O0RequestParameter(O2_OAUTH_NONCE, nonce())); + oauthParams.append(O0RequestParameter(O2_OAUTH_TOKEN, requestToken_.toLatin1())); + oauthParams.append(O0RequestParameter(O2_OAUTH_VERFIER, verifier_.toLatin1())); + oauthParams.append(O0RequestParameter(O2_OAUTH_SIGNATURE_METHOD, signatureMethod().toLatin1())); + oauthParams.append(O0RequestParameter(O2_OAUTH_SIGNATURE, generateSignature(oauthParams, request, QList(), QNetworkAccessManager::PostOperation))); + + // Post request + decorateRequest(request, oauthParams); + request.setHeader(QNetworkRequest::ContentTypeHeader, O2_MIME_TYPE_XFORM); + QNetworkReply *reply = manager_->post(request, QByteArray()); + connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onTokenExchangeError(QNetworkReply::NetworkError))); + connect(reply, SIGNAL(finished()), this, SLOT(onTokenExchangeFinished())); +} + +void O1::onTokenExchangeError(QNetworkReply::NetworkError error) { + QNetworkReply *reply = qobject_cast(sender()); + qWarning() << "O1::onTokenExchangeError:" << (int)error << reply->errorString() << reply->readAll(); + Q_EMIT linkingFailed(); +} + +void O1::onTokenExchangeFinished() { + qDebug() << "O1::onTokenExchangeFinished"; + + QNetworkReply *reply = qobject_cast(sender()); + reply->deleteLater(); + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "O1::onTokenExchangeFinished: " << reply->errorString(); + return; + } + + // Get access token and secret + QByteArray data = reply->readAll(); + QMap response = parseResponse(data); + if (response.contains(O2_OAUTH_TOKEN) && response.contains(O2_OAUTH_TOKEN_SECRET)) { + setToken(response.take(O2_OAUTH_TOKEN)); + setTokenSecret(response.take(O2_OAUTH_TOKEN_SECRET)); + // Set extra tokens if any + if (!response.isEmpty()) { + QVariantMap extraTokens; + foreach (QString key, response.keys()) { + extraTokens.insert(key, response.value(key)); + } + setExtraTokens(extraTokens); + } + setLinked(true); + Q_EMIT linkingSucceeded(); + } else { + qWarning() << "O1::onTokenExchangeFinished: oauth_token or oauth_token_secret missing from response" << data; + Q_EMIT linkingFailed(); + } +} + +QMap O1::parseResponse(const QByteArray &response) { + QMap ret; + foreach (QByteArray param, response.split('&')) { + QList kv = param.split('='); + if (kv.length() == 2) { + ret.insert(QUrl::fromPercentEncoding(kv[0]), QUrl::fromPercentEncoding(kv[1])); + } + } + return ret; +} + +QByteArray O1::nonce() { + static bool firstTime = true; + if (firstTime) { + firstTime = false; + qsrand(QTime::currentTime().msec()); + } + QString u = QString::number(QDateTime::currentDateTimeUtc().toTime_t()); + u.append(QString::number(qrand())); + return u.toLatin1(); +} diff --git a/src_o2/o1requestor.h b/src_o2/o1requestor.h new file mode 100644 --- /dev/null +++ b/src_o2/o1requestor.h @@ -0,0 +1,62 @@ +#ifndef O1REQUESTOR_H +#define O1REQUESTOR_H + +#include +#include +#include + +#include "o0export.h" +#include "o1.h" + +class QNetworkAccessManager; +class QNetworkReply; +class O1; + +/// Makes authenticated requests using OAuth 1.0. +class O0_EXPORT O1Requestor: public QObject { + Q_OBJECT + +public: + explicit O1Requestor(QNetworkAccessManager *manager, O1 *authenticator, QObject *parent = 0); + +public Q_SLOTS: + /// Make a GET request. + /// @param req Network request. + /// @param signingParameters Extra (non-OAuth) parameters participating in signing. + /// @return Reply. + QNetworkReply *get(const QNetworkRequest &req, const QList &signingParameters); + + /// Make a POST request. + /// @param req Network request. + /// @param signingParameters Extra (non-OAuth) parameters participating in signing. + /// @param data Request payload. + /// @return Reply. + QNetworkReply *post(const QNetworkRequest &req, const QList &signingParameters, const QByteArray &data); + + /// Make a POST request. + /// @param req Network request. + /// @param signingParameters Extra (non-OAuth) parameters participating in signing. + /// @param multiPart HTTPMultiPart. + /// @return Reply. + QNetworkReply *post(const QNetworkRequest &req, const QList &signingParameters, QHttpMultiPart *multiPart); + + /// Make a PUT request. + /// @param req Network request. + /// @param signingParameters Extra (non-OAuth) parameters participating in signing. + /// @param data Request payload. + /// @return Reply. + QNetworkReply *put(const QNetworkRequest &req, const QList &signingParameters, const QByteArray &data); + +protected: + /// Return new request based on the original, with the "Authentication:" header added. + QNetworkRequest setup(const QNetworkRequest &request, const QList &signingParameters, QNetworkAccessManager::Operation operation); + + /// Augment reply with a timer. + QNetworkReply *addTimer(QNetworkReply *reply); + + QNetworkAccessManager *manager_; + O1 *authenticator_; +}; + + +#endif // O1REQUESTOR_H diff --git a/src_o2/o1requestor.cpp b/src_o2/o1requestor.cpp new file mode 100644 --- /dev/null +++ b/src_o2/o1requestor.cpp @@ -0,0 +1,57 @@ +#include +#include +#include +#include + +#include "o1requestor.h" +#include "o1timedreply.h" +#include "o0globals.h" + +O1Requestor::O1Requestor(QNetworkAccessManager *manager, O1 *authenticator, QObject *parent): QObject(parent) { + manager_ = manager; + authenticator_ = authenticator; +} + +QNetworkReply *O1Requestor::get(const QNetworkRequest &req, const QList &signingParameters) { + QNetworkRequest request = setup(req, signingParameters, QNetworkAccessManager::GetOperation); + return addTimer(manager_->get(request)); +} + +QNetworkReply *O1Requestor::post(const QNetworkRequest &req, const QList &signingParameters, const QByteArray &data) { + QNetworkRequest request = setup(req, signingParameters, QNetworkAccessManager::PostOperation); + return addTimer(manager_->post(request, data)); +} + +QNetworkReply *O1Requestor::post(const QNetworkRequest &req, const QList &signingParameters, QHttpMultiPart * multiPart) { + QNetworkRequest request = setup(req, signingParameters, QNetworkAccessManager::PostOperation); + return addTimer(manager_->post(request, multiPart)); +} + +QNetworkReply *O1Requestor::put(const QNetworkRequest &req, const QList &signingParameters, const QByteArray &data) { + QNetworkRequest request = setup(req, signingParameters, QNetworkAccessManager::PutOperation); + return addTimer(manager_->put(request, data)); +} + +QNetworkReply *O1Requestor::addTimer(QNetworkReply *reply) { + (void)new O1TimedReply(reply); + return reply; +} + +QNetworkRequest O1Requestor::setup(const QNetworkRequest &req, const QList &signingParameters, QNetworkAccessManager::Operation operation) { + // Collect OAuth parameters + QList oauthParams; + oauthParams.append(O0RequestParameter(O2_OAUTH_CONSUMER_KEY, authenticator_->clientId().toLatin1())); + oauthParams.append(O0RequestParameter(O2_OAUTH_VERSION, "1.0")); + oauthParams.append(O0RequestParameter(O2_OAUTH_TOKEN, authenticator_->token().toLatin1())); + oauthParams.append(O0RequestParameter(O2_OAUTH_SIGNATURE_METHOD, authenticator_->signatureMethod().toLatin1())); + oauthParams.append(O0RequestParameter(O2_OAUTH_NONCE, O1::nonce())); + oauthParams.append(O0RequestParameter(O2_OAUTH_TIMESTAMP, QString::number(QDateTime::currentDateTimeUtc().toTime_t()).toLatin1())); + + // Add signature parameter + oauthParams.append(O0RequestParameter(O2_OAUTH_SIGNATURE, authenticator_->generateSignature(oauthParams, req, signingParameters, operation))); + + // Return a copy of the original request with authorization header set + QNetworkRequest request(req); + authenticator_->decorateRequest(request, oauthParams); + return request; +} diff --git a/src_o2/o1timedreply.h b/src_o2/o1timedreply.h new file mode 100644 --- /dev/null +++ b/src_o2/o1timedreply.h @@ -0,0 +1,27 @@ +#ifndef O1TIMEDREPLY_H +#define O1TIMEDREPLY_H + +#include +#include + +#include "o0export.h" + +/// A timer connected to a network reply. +class O0_EXPORT O1TimedReply: public QTimer { + Q_OBJECT + +public: + explicit O1TimedReply(QNetworkReply *parent, int pTimeout=60*1000); + +Q_SIGNALS: + /// Emitted when we have timed out waiting for the network reply. + void error(QNetworkReply::NetworkError); + /// Emitted when the network reply has responded. + void finished(); + +private Q_SLOTS: + void onFinished(); + void onTimeout(); +}; + +#endif diff --git a/src_o2/o1timedreply.cpp b/src_o2/o1timedreply.cpp new file mode 100644 --- /dev/null +++ b/src_o2/o1timedreply.cpp @@ -0,0 +1,20 @@ +#include +#include + +#include "o1timedreply.h" + +O1TimedReply::O1TimedReply(QNetworkReply *parent, int pTimeout): QTimer(parent) { + setSingleShot(true); + setInterval(pTimeout); + connect(this, SIGNAL(timeout()), this, SLOT(onTimeout())); + connect(parent, SIGNAL(finished()), this, SLOT(onFinished())); +} + +void O1TimedReply::onFinished() { + stop(); + Q_EMIT finished(); +} + +void O1TimedReply::onTimeout() { + Q_EMIT error(QNetworkReply::TimeoutError); +} diff --git a/src_o2/o2.h b/src_o2/o2.h new file mode 100644 --- /dev/null +++ b/src_o2/o2.h @@ -0,0 +1,173 @@ +#ifndef O2_H +#define O2_H + +#include +#include +#include +#include + +#include "o0export.h" +#include "o0baseauth.h" +#include "o2reply.h" +#include "o0abstractstore.h" + +/// Simple OAuth2 authenticator. +class O0_EXPORT O2: public O0BaseAuth { + Q_OBJECT + Q_ENUMS(GrantFlow) + +public: + /// Authorization flow types. + enum GrantFlow { + GrantFlowAuthorizationCode, ///< @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1 + GrantFlowImplicit, ///< @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.2 + GrantFlowResourceOwnerPasswordCredentials, + }; + + /// Authorization flow. + Q_PROPERTY(GrantFlow grantFlow READ grantFlow WRITE setGrantFlow NOTIFY grantFlowChanged) + GrantFlow grantFlow(); + void setGrantFlow(GrantFlow value); + + /// Resource owner username. + /// O2 instances with the same (username, password) share the same "linked" and "token" properties. + Q_PROPERTY(QString username READ username WRITE setUsername NOTIFY usernameChanged) + QString username(); + void setUsername(const QString &value); + + /// Resource owner password. + /// O2 instances with the same (username, password) share the same "linked" and "token" properties. + Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged) + QString password(); + void setPassword(const QString &value); + + /// Scope of authentication. + Q_PROPERTY(QString scope READ scope WRITE setScope NOTIFY scopeChanged) + QString scope(); + void setScope(const QString &value); + + /// Localhost policy. By default it's value is http://127.0.0.1:%1/, however some services may + /// require the use of http://localhost:%1/ or any other value. + Q_PROPERTY(QString localhostPolicy READ localhostPolicy WRITE setLocalhostPolicy) + QString localhostPolicy() const; + void setLocalhostPolicy(const QString &value); + + /// API key. + Q_PROPERTY(QString apiKey READ apiKey WRITE setApiKey) + QString apiKey(); + void setApiKey(const QString &value); + + /// Allow ignoring SSL errors? + /// E.g. SurveyMonkey fails on Mac due to SSL error. Ignoring the error circumvents the problem + Q_PROPERTY(bool ignoreSslErrors READ ignoreSslErrors WRITE setIgnoreSslErrors) + bool ignoreSslErrors(); + void setIgnoreSslErrors(bool ignoreSslErrors); + + /// Request URL. + Q_PROPERTY(QString requestUrl READ requestUrl WRITE setRequestUrl NOTIFY requestUrlChanged) + QString requestUrl(); + void setRequestUrl(const QString &value); + + /// User-defined extra parameters to append to request URL + Q_PROPERTY(QVariantMap extraRequestParams READ extraRequestParams WRITE setExtraRequestParams NOTIFY extraRequestParamsChanged) + QVariantMap extraRequestParams(); + void setExtraRequestParams(const QVariantMap &value); + + /// Token request URL. + Q_PROPERTY(QString tokenUrl READ tokenUrl WRITE setTokenUrl NOTIFY tokenUrlChanged) + QString tokenUrl(); + void setTokenUrl(const QString &value); + + /// Token refresh URL. + Q_PROPERTY(QString refreshTokenUrl READ refreshTokenUrl WRITE setRefreshTokenUrl NOTIFY refreshTokenUrlChanged) + QString refreshTokenUrl(); + void setRefreshTokenUrl(const QString &value); + +public: + /// Constructor. + /// @param parent Parent object. + explicit O2(QObject *parent = 0, QNetworkAccessManager *manager = 0, O0AbstractStore *store = 0); + + /// Get authentication code. + QString code(); + + /// Get refresh token. + QString refreshToken(); + + /// Get token expiration time (seconds from Epoch). + int expires(); + +public Q_SLOTS: + /// Authenticate. + Q_INVOKABLE virtual void link(); + + /// De-authenticate. + Q_INVOKABLE virtual void unlink(); + + /// Refresh token. + Q_INVOKABLE void refresh(); + + /// Handle situation where reply server has opted to close its connection + void serverHasClosed(bool paramsfound = false); + +Q_SIGNALS: + /// Emitted when a token refresh has been completed or failed. + void refreshFinished(QNetworkReply::NetworkError error); + + // Property change signals + void grantFlowChanged(); + void scopeChanged(); + void usernameChanged(); + void passwordChanged(); + void requestUrlChanged(); + void extraRequestParamsChanged(); + void refreshTokenUrlChanged(); + void tokenUrlChanged(); + +public Q_SLOTS: + /// Handle verification response. + virtual void onVerificationReceived(QMap); + +protected Q_SLOTS: + /// Handle completion of a token request. + virtual void onTokenReplyFinished(); + + /// Handle failure of a token request. + virtual void onTokenReplyError(QNetworkReply::NetworkError error); + + /// Handle completion of a refresh request. + virtual void onRefreshFinished(); + + /// Handle failure of a refresh request. + virtual void onRefreshError(QNetworkReply::NetworkError error); + +protected: + /// Build HTTP request body. + QByteArray buildRequestBody(const QMap ¶meters); + + /// Set authentication code. + void setCode(const QString &v); + + /// Set refresh token. + void setRefreshToken(const QString &v); + + /// Set token expiration time. + void setExpires(int v); + +protected: + QString username_; + QString password_; + QUrl requestUrl_; + QVariantMap extraReqParams_; + QUrl tokenUrl_; + QUrl refreshTokenUrl_; + QString scope_; + QString code_; + QString localhostPolicy_; + QString apiKey_; + QNetworkAccessManager *manager_; + O2ReplyList timedReplies_; + GrantFlow grantFlow_; +}; + +#endif // O2_H diff --git a/src_o2/o2.cpp b/src_o2/o2.cpp new file mode 100644 --- /dev/null +++ b/src_o2/o2.cpp @@ -0,0 +1,510 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if QT_VERSION >= 0x050000 +#include +#include +#include +#else +#include +#include +#endif + +#include "o2.h" +#include "o2replyserver.h" +#include "o0globals.h" +#include "o0settingsstore.h" + +/// Parse JSON data into a QVariantMap +static QVariantMap parseTokenResponse(const QByteArray &data) { +#if QT_VERSION >= 0x050000 + QJsonParseError err; + QJsonDocument doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError) { + qWarning() << "parseTokenResponse: Failed to parse token response due to err:" << err.errorString(); + return QVariantMap(); + } + + if (!doc.isObject()) { + qWarning() << "parseTokenResponse: Token response is not an object"; + return QVariantMap(); + } + + return doc.object().toVariantMap(); +#else + QScriptEngine engine; + QScriptValue value = engine.evaluate("(" + QString(data) + ")"); + QScriptValueIterator it(value); + QVariantMap map; + + while (it.hasNext()) { + it.next(); + map.insert(it.name(), it.value().toVariant()); + } + + return map; +#endif +} + +/// Add query parameters to a query +static void addQueryParametersToUrl(QUrl &url, QList > parameters) { +#if QT_VERSION < 0x050000 + url.setQueryItems(parameters); +#else + QUrlQuery query(url); + query.setQueryItems(parameters); + url.setQuery(query); +#endif +} + +O2::O2(QObject *parent, QNetworkAccessManager *manager, O0AbstractStore *store): O0BaseAuth(parent, store) { + manager_ = manager ? manager : new QNetworkAccessManager(this); + grantFlow_ = GrantFlowAuthorizationCode; + localhostPolicy_ = QString(O2_CALLBACK_URL); + qRegisterMetaType("QNetworkReply::NetworkError"); +} + +O2::GrantFlow O2::grantFlow() { + return grantFlow_; +} + +void O2::setGrantFlow(O2::GrantFlow value) { + grantFlow_ = value; + Q_EMIT grantFlowChanged(); +} + +QString O2::username() { + return username_; +} + +void O2::setUsername(const QString &value) { + username_ = value; + Q_EMIT usernameChanged(); +} + +QString O2::password() { + return password_; +} + +void O2::setPassword(const QString &value) { + password_ = value; + Q_EMIT passwordChanged(); +} + +QString O2::scope() { + return scope_; +} + +void O2::setScope(const QString &value) { + scope_ = value; + Q_EMIT scopeChanged(); +} + +QString O2::requestUrl() { + return requestUrl_.toString(); +} + +void O2::setRequestUrl(const QString &value) { + requestUrl_ = QUrl(value); + Q_EMIT requestUrlChanged(); +} + +QVariantMap O2::extraRequestParams() +{ + return extraReqParams_; +} + +void O2::setExtraRequestParams(const QVariantMap &value) +{ + extraReqParams_ = value; + Q_EMIT extraRequestParamsChanged(); +} + +QString O2::tokenUrl() { + return tokenUrl_.toString(); +} + +void O2::setTokenUrl(const QString &value) { + tokenUrl_= QUrl(value); + Q_EMIT tokenUrlChanged(); +} + +QString O2::refreshTokenUrl() { + return refreshTokenUrl_.toString(); +} + +void O2::setRefreshTokenUrl(const QString &value) { + refreshTokenUrl_ = QUrl(value); + Q_EMIT refreshTokenUrlChanged(); +} + +void O2::link() { + qDebug() << "O2::link"; + + // Create the reply server if it doesn't exist + // and we don't use an external web interceptor + if(!useExternalWebInterceptor_) { + if(replyServer_ == NULL) { + replyServer_ = new O2ReplyServer(this); + connect(replyServer_, SIGNAL(verificationReceived(QMap)), this, SLOT(onVerificationReceived(QMap))); + connect(replyServer_, SIGNAL(serverClosed(bool)), this, SLOT(serverHasClosed(bool))); + } + } + + if (linked()) { + qDebug() << "O2::link: Linked already"; + Q_EMIT linkingSucceeded(); + return; + } + + setLinked(false); + setToken(QStringLiteral("")); + setTokenSecret(QStringLiteral("")); + setExtraTokens(QVariantMap()); + setRefreshToken(QString()); + setExpires(0); + + if (grantFlow_ == GrantFlowAuthorizationCode || grantFlow_ == GrantFlowImplicit) { + + if (useExternalWebInterceptor_) { + // Save redirect URI, as we have to reuse it when requesting the access token + redirectUri_ = localhostPolicy_.arg(localPort()); + } else { + // Start listening to authentication replies + if (!replyServer_->isListening()) { + if (replyServer_->listen(QHostAddress::Any, localPort_)) { + qDebug() << "O2::link: Reply server listening on port" << localPort(); + } else { + qWarning() << "O2::link: Reply server failed to start listening on port" << localPort(); + Q_EMIT linkingFailed(); + return; + } + } + + // Save redirect URI, as we have to reuse it when requesting the access token + redirectUri_ = localhostPolicy_.arg(replyServer_->serverPort()); + } + + // Assemble intial authentication URL + QList > parameters; + parameters.append(qMakePair(QString(O2_OAUTH2_RESPONSE_TYPE), + (grantFlow_ == GrantFlowAuthorizationCode)? QString(O2_OAUTH2_GRANT_TYPE_CODE): QString(O2_OAUTH2_GRANT_TYPE_TOKEN))); + parameters.append(qMakePair(QString(O2_OAUTH2_CLIENT_ID), clientId_)); + parameters.append(qMakePair(QString(O2_OAUTH2_REDIRECT_URI), redirectUri_)); + parameters.append(qMakePair(QString(O2_OAUTH2_SCOPE), scope_.replace( QStringLiteral(" "), QStringLiteral("+")))); + if ( !apiKey_.isEmpty() ) + parameters.append(qMakePair(QString(O2_OAUTH2_API_KEY), apiKey_)); + foreach (QString key, extraRequestParams().keys()) { + parameters.append(qMakePair(key, extraRequestParams().value(key).toString())); + } + // Show authentication URL with a web browser + QUrl url(requestUrl_); + addQueryParametersToUrl(url, parameters); + qDebug() << "O2::link: Emit openBrowser" << url.toString(); + Q_EMIT openBrowser(url); + } else if (grantFlow_ == GrantFlowResourceOwnerPasswordCredentials) { + QList parameters; + parameters.append(O0RequestParameter(O2_OAUTH2_CLIENT_ID.toUtf8(), clientId_.toUtf8())); + if ( !clientSecret_.isEmpty() ) + parameters.append(O0RequestParameter(O2_OAUTH2_CLIENT_SECRET.toUtf8(), clientSecret_.toUtf8())); + parameters.append(O0RequestParameter(O2_OAUTH2_USERNAME.toUtf8(), username_.toUtf8())); + parameters.append(O0RequestParameter(O2_OAUTH2_PASSWORD.toUtf8(), password_.toUtf8())); + parameters.append(O0RequestParameter(O2_OAUTH2_GRANT_TYPE.toUtf8(), O2_OAUTH2_GRANT_TYPE_PASSWORD.toUtf8())); + parameters.append(O0RequestParameter(O2_OAUTH2_SCOPE.toUtf8(), scope_.toUtf8())); + if ( !apiKey_.isEmpty() ) + parameters.append(O0RequestParameter(O2_OAUTH2_API_KEY.toUtf8(), apiKey_.toUtf8())); + foreach (QString key, extraRequestParams().keys()) { + parameters.append(O0RequestParameter(key.toUtf8(), extraRequestParams().value(key).toByteArray())); + } + QByteArray payload = O0BaseAuth::createQueryParameters(parameters); + + qDebug() << "O2::link: Sending token request for resource owner flow"; + QUrl url(tokenUrl_); + QNetworkRequest tokenRequest(url); + tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded")); + QNetworkReply *tokenReply = manager_->post(tokenRequest, payload); + + connect(tokenReply, SIGNAL(finished()), this, SLOT(onTokenReplyFinished()), Qt::QueuedConnection); + connect(tokenReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onTokenReplyError(QNetworkReply::NetworkError)), Qt::QueuedConnection); + } +} + +void O2::unlink() { + qDebug() << "O2::unlink"; + setLinked(false); + setToken(QString()); + setRefreshToken(QString()); + setExpires(0); + setExtraTokens(QVariantMap()); + Q_EMIT linkingSucceeded(); +} + +void O2::onVerificationReceived(const QMap response) { + qDebug() << "O2::onVerificationReceived:" << response; + qDebug() << "O2::onVerificationReceived: Emitting closeBrowser()"; + Q_EMIT closeBrowser(); + + if (response.contains(QLatin1String("error"))) { + qWarning() << "O2::onVerificationReceived: Verification failed:" << response; + Q_EMIT linkingFailed(); + return; + } + + if (grantFlow_ == GrantFlowAuthorizationCode) { + // Save access code + setCode(response.value(QString(O2_OAUTH2_GRANT_TYPE_CODE))); + + // Exchange access code for access/refresh tokens + QString query; + if(!apiKey_.isEmpty()) + query = QString(QStringLiteral("?") + QString(O2_OAUTH2_API_KEY) + QStringLiteral("=") + apiKey_); + QNetworkRequest tokenRequest(QUrl(tokenUrl_.toString() + query)); + tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, O2_MIME_TYPE_XFORM); + tokenRequest.setRawHeader(QStringLiteral("Accept").toUtf8(), O2_MIME_TYPE_JSON.toUtf8()); + QMap parameters; + parameters.insert(O2_OAUTH2_GRANT_TYPE_CODE, code()); + parameters.insert(O2_OAUTH2_CLIENT_ID, clientId_); + parameters.insert(O2_OAUTH2_CLIENT_SECRET, clientSecret_); + parameters.insert(O2_OAUTH2_REDIRECT_URI, redirectUri_); + parameters.insert(O2_OAUTH2_GRANT_TYPE, O2_AUTHORIZATION_CODE); + QByteArray data = buildRequestBody(parameters); + + qDebug() << QStringLiteral("O2::onVerificationReceived: Exchange access code data:\n%1").arg(QLatin1String(data)); + + QNetworkReply *tokenReply = manager_->post(tokenRequest, data); + timedReplies_.add(tokenReply); + connect(tokenReply, SIGNAL(finished()), this, SLOT(onTokenReplyFinished()), Qt::QueuedConnection); + connect(tokenReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onTokenReplyError(QNetworkReply::NetworkError)), Qt::QueuedConnection); + } else if (grantFlow_ == GrantFlowImplicit) { + // Check for mandatory tokens + if (response.contains(O2_OAUTH2_ACCESS_TOKEN)) { + qDebug() << "O2::onVerificationReceived: Access token returned for implicit flow"; + setToken(response.value(O2_OAUTH2_ACCESS_TOKEN)); + if (response.contains(O2_OAUTH2_EXPIRES_IN)) { + bool ok = false; + int expiresIn = response.value(O2_OAUTH2_EXPIRES_IN).toInt(&ok); + if (ok) { + qDebug() << "O2::onVerificationReceived: Token expires in" << expiresIn << "seconds"; + setExpires((int)(QDateTime::currentMSecsSinceEpoch() / 1000 + expiresIn)); + } + } + setLinked(true); + Q_EMIT linkingSucceeded(); + } else { + qWarning() << "O2::onVerificationReceived: Access token missing from response for implicit flow"; + Q_EMIT linkingFailed(); + } + } else { + setToken(response.value(O2_OAUTH2_ACCESS_TOKEN)); + setRefreshToken(response.value(O2_OAUTH2_REFRESH_TOKEN)); + } +} + +QString O2::code() { + QString key = QString(O2_KEY_CODE).arg(clientId_); + return store_->value(key); +} + +void O2::setCode(const QString &c) { + QString key = QString(O2_KEY_CODE).arg(clientId_); + store_->setValue(key, c); +} + +void O2::onTokenReplyFinished() { + qDebug() << "O2::onTokenReplyFinished"; + QNetworkReply *tokenReply = qobject_cast(sender()); + if (!tokenReply) + { + qDebug() << "O2::onTokenReplyFinished: reply is null"; + return; + } + if (tokenReply->error() == QNetworkReply::NoError) { + QByteArray replyData = tokenReply->readAll(); + + // Dump replyData + // SENSITIVE DATA in RelWithDebInfo or Debug builds + //qDebug() << "O2::onTokenReplyFinished: replyData\n"; + //qDebug() << QString( replyData ); + + QVariantMap tokens = parseTokenResponse(replyData); + + // Dump tokens + qDebug() << "O2::onTokenReplyFinished: Tokens returned:\n"; + foreach (QString key, tokens.keys()) { + // SENSITIVE DATA in RelWithDebInfo or Debug builds, so it is truncated first + qDebug() << key << ": "<< tokens.value( key ).toString().left( 3 ) << "..."; + } + + // Check for mandatory tokens + if (tokens.contains(O2_OAUTH2_ACCESS_TOKEN)) { + qDebug() << "O2::onTokenReplyFinished: Access token returned"; + setToken(tokens.take(O2_OAUTH2_ACCESS_TOKEN).toString()); + bool ok = false; + int expiresIn = tokens.take(O2_OAUTH2_EXPIRES_IN).toInt(&ok); + if (ok) { + qDebug() << "O2::onTokenReplyFinished: Token expires in" << expiresIn << "seconds"; + setExpires((int)(QDateTime::currentMSecsSinceEpoch() / 1000 + expiresIn)); + } + setRefreshToken(tokens.take(O2_OAUTH2_REFRESH_TOKEN).toString()); + setExtraTokens(tokens); + timedReplies_.remove(tokenReply); + setLinked(true); + Q_EMIT linkingSucceeded(); + } else { + qWarning() << "O2::onTokenReplyFinished: Access token missing from response"; + Q_EMIT linkingFailed(); + } + } + tokenReply->deleteLater(); +} + +void O2::onTokenReplyError(QNetworkReply::NetworkError error) { + QNetworkReply *tokenReply = qobject_cast(sender()); + qWarning() << "O2::onTokenReplyError: " << error << ": " << tokenReply->errorString(); + qDebug() << "O2::onTokenReplyError: " << tokenReply->readAll(); + setToken(QString()); + setRefreshToken(QString()); + timedReplies_.remove(tokenReply); + Q_EMIT linkingFailed(); +} + +QByteArray O2::buildRequestBody(const QMap ¶meters) { + QByteArray body; + bool first = true; + foreach (QString key, parameters.keys()) { + if (first) { + first = false; + } else { + body.append("&"); + } + QString value = parameters.value(key); + body.append(QUrl::toPercentEncoding(key) + QStringLiteral("=").toUtf8() + QUrl::toPercentEncoding(value)); + } + return body; +} + +int O2::expires() { + QString key = QString(O2_KEY_EXPIRES).arg(clientId_); + return store_->value(key).toInt(); +} + +void O2::setExpires(int v) { + QString key = QString(O2_KEY_EXPIRES).arg(clientId_); + store_->setValue(key, QString::number(v)); +} + +QString O2::refreshToken() { + QString key = QString(O2_KEY_REFRESH_TOKEN).arg(clientId_); + return store_->value(key); +} + +void O2::setRefreshToken(const QString &v) { + qDebug() << "O2::setRefreshToken" << v.left(4) << "..."; + QString key = QString(O2_KEY_REFRESH_TOKEN).arg(clientId_); + store_->setValue(key, v); +} + +void O2::refresh() { + qDebug() << "O2::refresh: Token: ..." << refreshToken().right(7); + + if (refreshToken().isEmpty()) { + qWarning() << "O2::refresh: No refresh token"; + onRefreshError(QNetworkReply::AuthenticationRequiredError); + return; + } + if (refreshTokenUrl_.isEmpty()) { + qWarning() << "O2::refresh: Refresh token URL not set"; + onRefreshError(QNetworkReply::AuthenticationRequiredError); + return; + } + + QNetworkRequest refreshRequest(refreshTokenUrl_); + refreshRequest.setHeader(QNetworkRequest::ContentTypeHeader, O2_MIME_TYPE_XFORM); + QMap parameters; + parameters.insert(O2_OAUTH2_CLIENT_ID, clientId_); + parameters.insert(O2_OAUTH2_CLIENT_SECRET, clientSecret_); + parameters.insert(O2_OAUTH2_REFRESH_TOKEN, refreshToken()); + parameters.insert(O2_OAUTH2_GRANT_TYPE, O2_OAUTH2_REFRESH_TOKEN); + + QByteArray data = buildRequestBody(parameters); + QNetworkReply *refreshReply = manager_->post(refreshRequest, data); + timedReplies_.add(refreshReply); + connect(refreshReply, SIGNAL(finished()), this, SLOT(onRefreshFinished()), Qt::QueuedConnection); + connect(refreshReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRefreshError(QNetworkReply::NetworkError)), Qt::QueuedConnection); +} + +void O2::onRefreshFinished() { + QNetworkReply *refreshReply = qobject_cast(sender()); + + if (refreshReply->error() == QNetworkReply::NoError) { + QByteArray reply = refreshReply->readAll(); + QVariantMap tokens = parseTokenResponse(reply); + setToken(tokens.value(O2_OAUTH2_ACCESS_TOKEN).toString()); + setExpires((int)(QDateTime::currentMSecsSinceEpoch() / 1000 + tokens.value(O2_OAUTH2_EXPIRES_IN).toInt())); + QString refreshToken = tokens.value(O2_OAUTH2_REFRESH_TOKEN).toString(); + if(!refreshToken.isEmpty()) { + setRefreshToken(refreshToken); + } + else { + qDebug() << "No new refresh token. Keep the old one."; + } + timedReplies_.remove(refreshReply); + setLinked(true); + Q_EMIT linkingSucceeded(); + Q_EMIT refreshFinished(QNetworkReply::NoError); + qDebug() << " New token expires in" << expires() << "seconds"; + } else { + qDebug() << "O2::onRefreshFinished: Error" << (int)refreshReply->error() << refreshReply->errorString(); + } + refreshReply->deleteLater(); +} + +void O2::onRefreshError(QNetworkReply::NetworkError error) { + QNetworkReply *refreshReply = qobject_cast(sender()); + qWarning() << "O2::onRefreshError: " << error; + unlink(); + timedReplies_.remove(refreshReply); + Q_EMIT refreshFinished(error); +} + +void O2::serverHasClosed(bool paramsfound) +{ + if ( !paramsfound ) { + // server has probably timed out after receiving first response + Q_EMIT linkingFailed(); + } +} + +QString O2::localhostPolicy() const { + return localhostPolicy_; +} + +void O2::setLocalhostPolicy(const QString &value) { + localhostPolicy_ = value; +} + +QString O2::apiKey() { + return apiKey_; +} + +void O2::setApiKey(const QString &value) { + apiKey_ = value; +} + +bool O2::ignoreSslErrors() { + return timedReplies_.ignoreSslErrors(); +} + +void O2::setIgnoreSslErrors(bool ignoreSslErrors) { + timedReplies_.setIgnoreSslErrors(ignoreSslErrors); +} diff --git a/src_o2/o2google.h b/src_o2/o2google.h new file mode 100644 --- /dev/null +++ b/src_o2/o2google.h @@ -0,0 +1,17 @@ +// +// Created by michaelpollind on 3/13/17. +// + +#ifndef O2_O2GOOGLE_H +#define O2_O2GOOGLE_H + +#include "o2.h" + +class O2Google : public O2 { + Q_OBJECT +public: + explicit O2Google(QObject *parent = 0); +}; + + +#endif //O2_O2GOOGLE_H diff --git a/src_o2/o2google.cpp b/src_o2/o2google.cpp new file mode 100644 --- /dev/null +++ b/src_o2/o2google.cpp @@ -0,0 +1,15 @@ +// +// Created by michaelpollind on 3/13/17. +// + +#include "o2google.h" + +static QString GftEndpoint = QStringLiteral("https://accounts.google.com/o/oauth2/auth"); +static QString GftTokenUrl = QStringLiteral("https://accounts.google.com/o/oauth2/token"); +static QString GftRefreshUrl = QStringLiteral("https://accounts.google.com/o/oauth2/token"); + +O2Google::O2Google(QObject *parent) : O2(parent){ + setRequestUrl(GftEndpoint); + setTokenUrl(GftTokenUrl); + setRefreshTokenUrl(GftRefreshUrl); +} diff --git a/src_o2/o2reply.h b/src_o2/o2reply.h new file mode 100644 --- /dev/null +++ b/src_o2/o2reply.h @@ -0,0 +1,61 @@ +#ifndef O2TIMEDREPLYLIST_H +#define O2TIMEDREPLYLIST_H + +#include +#include +#include +#include +#include +#include + +#include "o0export.h" + +/// A network request/reply pair that can time out. +class O0_EXPORT O2Reply: public QTimer { + Q_OBJECT + +public: + O2Reply(QNetworkReply *reply, int timeOut = 60 * 1000, QObject *parent = 0); + +Q_SIGNALS: + void error(QNetworkReply::NetworkError); + +public Q_SLOTS: + /// When time out occurs, the QNetworkReply's error() signal is triggered. + void onTimeOut(); + +public: + QNetworkReply *reply; +}; + +/// List of O2Replies. +class O2ReplyList { +public: + O2ReplyList() { ignoreSslErrors_ = false; } + + /// Destructor. + /// Deletes all O2Reply instances in the list. + virtual ~O2ReplyList(); + + /// Create a new O2Reply from a QNetworkReply, and add it to this list. + void add(QNetworkReply *reply); + + /// Add an O2Reply to the list, while taking ownership of it. + void add(O2Reply *reply); + + /// Remove item from the list that corresponds to a QNetworkReply. + void remove(QNetworkReply *reply); + + /// Find an O2Reply in the list, corresponding to a QNetworkReply. + /// @return Matching O2Reply or NULL. + O2Reply *find(QNetworkReply *reply); + + bool ignoreSslErrors(); + void setIgnoreSslErrors(bool ignoreSslErrors); + +protected: + QList replies_; + bool ignoreSslErrors_; +}; + +#endif // O2TIMEDREPLYLIST_H diff --git a/src_o2/o2reply.cpp b/src_o2/o2reply.cpp new file mode 100644 --- /dev/null +++ b/src_o2/o2reply.cpp @@ -0,0 +1,58 @@ +#include +#include + +#include "o2reply.h" + +O2Reply::O2Reply(QNetworkReply *r, int timeOut, QObject *parent): QTimer(parent), reply(r) { + setSingleShot(true); + connect(this, SIGNAL(error(QNetworkReply::NetworkError)), reply, SIGNAL(error(QNetworkReply::NetworkError)), Qt::QueuedConnection); + connect(this, SIGNAL(timeout()), this, SLOT(onTimeOut()), Qt::QueuedConnection); + start(timeOut); +} + +void O2Reply::onTimeOut() { + Q_EMIT error(QNetworkReply::TimeoutError); +} + +O2ReplyList::~O2ReplyList() { + foreach (O2Reply *timedReply, replies_) { + delete timedReply; + } +} + +void O2ReplyList::add(QNetworkReply *reply) { + if (reply && ignoreSslErrors()) + reply->ignoreSslErrors(); + add(new O2Reply(reply)); +} + +void O2ReplyList::add(O2Reply *reply) { + replies_.append(reply); +} + +void O2ReplyList::remove(QNetworkReply *reply) { + O2Reply *o2Reply = find(reply); + if (o2Reply) { + o2Reply->stop(); + (void)replies_.removeOne(o2Reply); + } +} + +O2Reply *O2ReplyList::find(QNetworkReply *reply) { + foreach (O2Reply *timedReply, replies_) { + if (timedReply->reply == reply) { + return timedReply; + } + } + return 0; +} + +bool O2ReplyList::ignoreSslErrors() +{ + return ignoreSslErrors_; +} + +void O2ReplyList::setIgnoreSslErrors(bool ignoreSslErrors) +{ + ignoreSslErrors_ = ignoreSslErrors; +} diff --git a/src_o2/o2replyserver.h b/src_o2/o2replyserver.h new file mode 100644 --- /dev/null +++ b/src_o2/o2replyserver.h @@ -0,0 +1,50 @@ +#ifndef O2REPLYSERVER_H +#define O2REPLYSERVER_H + +#include +#include +#include +#include + +#include "o0export.h" + +/// HTTP server to process authentication response. +class O0_EXPORT O2ReplyServer: public QTcpServer { + Q_OBJECT + +public: + explicit O2ReplyServer(QObject *parent = 0); + + /// Page content on local host after successful oauth - in case you do not want to close the browser, but display something + Q_PROPERTY(QByteArray replyContent READ replyContent WRITE setReplyContent) + QByteArray replyContent(); + void setReplyContent(const QByteArray &value); + + /// Seconds to keep listening *after* first response for a callback with token content + Q_PROPERTY(int timeout READ timeout WRITE setTimeout) + int timeout(); + void setTimeout(int timeout); + + /// Maximum number of callback tries to accept, in case some don't have token content (favicons, etc.) + Q_PROPERTY(int callbackTries READ callbackTries WRITE setCallbackTries) + int callbackTries(); + void setCallbackTries(int maxtries); + +Q_SIGNALS: + void verificationReceived(QMap); + void serverClosed(bool); // whether it has found parameters + +public Q_SLOTS: + void onIncomingConnection(); + void onBytesReady(); + QMap parseQueryParams(QByteArray *data); + void closeServer(QTcpSocket *socket = 0, bool hasparameters = false); + +protected: + QByteArray replyContent_; + int timeout_; + int maxtries_; + int tries_; +}; + +#endif // O2REPLYSERVER_H diff --git a/src_o2/o2replyserver.cpp b/src_o2/o2replyserver.cpp new file mode 100755 --- /dev/null +++ b/src_o2/o2replyserver.cpp @@ -0,0 +1,168 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if QT_VERSION >= 0x050000 +#include +#endif + +#include "o2replyserver.h" + +O2ReplyServer::O2ReplyServer(QObject *parent): QTcpServer(parent), + timeout_(15), maxtries_(3), tries_(0) { + qDebug() << "O2ReplyServer: Starting"; + connect(this, SIGNAL(newConnection()), this, SLOT(onIncomingConnection())); + replyContent_ = ""; +} + +void O2ReplyServer::onIncomingConnection() { + qDebug() << "O2ReplyServer::onIncomingConnection: Receiving..."; + QTcpSocket *socket = nextPendingConnection(); + connect(socket, SIGNAL(readyRead()), this, SLOT(onBytesReady()), Qt::UniqueConnection); + connect(socket, SIGNAL(disconnected()), socket, SLOT(deleteLater())); + + // Wait for a bit *after* first response, then close server if no useable data has arrived + // Helps with implicit flow, where a URL fragment may need processed by local user-agent and + // sent as secondary query string callback, or additional requests make it through first, + // like for favicons, etc., before such secondary callbacks are fired + QTimer *timer = new QTimer(socket); + timer->setObjectName(QStringLiteral("timeoutTimer")); + connect(timer, SIGNAL(timeout()), this, SLOT(closeServer())); + timer->setSingleShot(true); + timer->setInterval(timeout() * 1000); + connect(socket, SIGNAL(readyRead()), timer, SLOT(start())); +} + +void O2ReplyServer::onBytesReady() { + if (!isListening()) { + // server has been closed, stop processing queued connections + return; + } + qDebug() << "O2ReplyServer::onBytesReady: Processing request"; + // NOTE: on first call, the timeout timer is started + QTcpSocket *socket = qobject_cast(sender()); + if (!socket) { + qWarning() << "O2ReplyServer::onBytesReady: No socket available"; + return; + } + QByteArray reply; + reply.append("HTTP/1.0 200 OK \r\n"); + reply.append("Content-Type: text/html; charset=\"utf-8\"\r\n"); + reply.append(QStringLiteral("Content-Length: %1\r\n\r\n").arg(replyContent_.size()).toLatin1()); + reply.append(replyContent_); + socket->write(reply); + qDebug() << "O2ReplyServer::onBytesReady: Sent reply"; + + QByteArray data = socket->readAll(); + QMap queryParams = parseQueryParams(&data); + if (queryParams.isEmpty()) { + if (tries_ < maxtries_ ) { + qDebug() << "O2ReplyServer::onBytesReady: No query params found, waiting for more callbacks"; + ++tries_; + return; + } else { + tries_ = 0; + qWarning() << "O2ReplyServer::onBytesReady: No query params found, maximum callbacks received"; + closeServer(socket, false); + return; + } + } + qDebug() << "O2ReplyServer::onBytesReady: Query params found, closing server"; + closeServer(socket, true); + Q_EMIT verificationReceived(queryParams); +} + +QMap O2ReplyServer::parseQueryParams(QByteArray *data) { + qDebug() << "O2ReplyServer::parseQueryParams"; + + //qDebug() << QString("O2ReplyServer::parseQueryParams data:\n%1").arg(QString(*data)); + + QString splitGetLine = QString(QLatin1String(*data)).split(QStringLiteral("\r\n")).first(); + splitGetLine.remove(QStringLiteral("GET ")); + splitGetLine.remove(QStringLiteral("HTTP/1.1")); + splitGetLine.remove(QStringLiteral("\r\n")); + splitGetLine.prepend(QStringLiteral("http://localhost")); + QUrl getTokenUrl(splitGetLine); + + QList< QPair > tokens; +#if QT_VERSION < 0x050000 + tokens = getTokenUrl.queryItems(); +#else + QUrlQuery query(getTokenUrl); + tokens = query.queryItems(); +#endif + QMultiMap queryParams; + QPair tokenPair; + foreach (tokenPair, tokens) { + // FIXME: We are decoding key and value again. This helps with Google OAuth, but is it mandated by the standard? + QString key = QUrl::fromPercentEncoding(QByteArray().append(tokenPair.first.trimmed().toLatin1())); + QString value = QUrl::fromPercentEncoding(QByteArray().append(tokenPair.second.trimmed().toLatin1())); + queryParams.insert(key, value); + } + return queryParams; +} + +void O2ReplyServer::closeServer(QTcpSocket *socket, bool hasparameters) +{ + if (!isListening()) { + return; + } + + qDebug() << "O2ReplyServer::closeServer: Initiating"; + int port = serverPort(); + + if (!socket && sender()) { + QTimer *timer = qobject_cast(sender()); + if (timer) { + qWarning() << "O2ReplyServer::closeServer: Closing due to timeout"; + timer->stop(); + socket = qobject_cast(timer->parent()); + timer->deleteLater(); + } + } + if (socket) { + QTimer *timer = socket->findChild(QStringLiteral("timeoutTimer")); + if (timer) { + qDebug() << "O2ReplyServer::closeServer: Stopping socket's timeout timer"; + timer->stop(); + } + socket->disconnectFromHost(); + } + close(); + qDebug() << "O2ReplyServer::closeServer: Closed, no longer listening on port" << port; + Q_EMIT serverClosed(hasparameters); +} + +QByteArray O2ReplyServer::replyContent() { + return replyContent_; +} + +void O2ReplyServer::setReplyContent(const QByteArray &value) { + replyContent_ = value; +} + +int O2ReplyServer::timeout() +{ + return timeout_; +} + +void O2ReplyServer::setTimeout(int timeout) +{ + timeout_ = timeout; +} + +int O2ReplyServer::callbackTries() +{ + return maxtries_; +} + +void O2ReplyServer::setCallbackTries(int maxtries) +{ + maxtries_ = maxtries; +} diff --git a/src_o2/o2requestor.h b/src_o2/o2requestor.h new file mode 100644 --- /dev/null +++ b/src_o2/o2requestor.h @@ -0,0 +1,90 @@ +#ifndef O2REQUESTOR_H +#define O2REQUESTOR_H + +#include +#include +#include +#include +#include +#include + +#include "o0export.h" +#include "o2reply.h" + +class O2; + +/// Makes authenticated requests. +class O0_EXPORT O2Requestor: public QObject { + Q_OBJECT + +public: + explicit O2Requestor(QNetworkAccessManager *manager, O2 *authenticator, QObject *parent = 0); + ~O2Requestor(); + + /// Some services require the access token to be sent as a Authentication HTTP header. + /// This is the case for Twitch and Mixer. + /// When the access token expires and is refreshed, O2Requestor::retry() needs to update the Authentication HTTP header. + /// In order to do so, O2Requestor needs to know the format of the Authentication HTTP header. + void setAccessTokenInAuthenticationHTTPHeaderFormat(const QString &value); + +public Q_SLOTS: + /// Make a GET request. + /// @return Request ID or -1 if there are too many requests in the queue. + int get(const QNetworkRequest &req); + + /// Make a POST request. + /// @return Request ID or -1 if there are too many requests in the queue. + int post(const QNetworkRequest &req, const QByteArray &data); + + /// Make a PUT request. + /// @return Request ID or -1 if there are too many requests in the queue. + int put(const QNetworkRequest &req, const QByteArray &data); + +Q_SIGNALS: + /// Emitted when a request has been completed or failed. + void finished(int id, QNetworkReply::NetworkError error, QByteArray data); + + /// Emitted when an upload has progressed. + void uploadProgress(int id, qint64 bytesSent, qint64 bytesTotal); + +protected Q_SLOTS: + /// Handle refresh completion. + void onRefreshFinished(QNetworkReply::NetworkError error); + + /// Handle request finished. + void onRequestFinished(); + + /// Handle request error. + void onRequestError(QNetworkReply::NetworkError error); + + /// Re-try request (after successful token refresh). + void retry(); + + /// Finish the request, Q_EMIT finished() signal. + void finish(); + + /// Handle upload progress. + void onUploadProgress(qint64 uploaded, qint64 total); + +protected: + int setup(const QNetworkRequest &request, QNetworkAccessManager::Operation operation); + + enum Status { + Idle, Requesting, ReRequesting + }; + + QNetworkAccessManager *manager_; + O2 *authenticator_; + QNetworkRequest request_; + QByteArray data_; + QNetworkReply *reply_; + Status status_; + int id_; + QNetworkAccessManager::Operation operation_; + QUrl url_; + O2ReplyList timedReplies_; + QNetworkReply::NetworkError error_; + QString accessTokenInAuthenticationHTTPHeaderFormat_; +}; + +#endif // O2REQUESTOR_H diff --git a/src_o2/o2requestor.cpp b/src_o2/o2requestor.cpp new file mode 100644 --- /dev/null +++ b/src_o2/o2requestor.cpp @@ -0,0 +1,209 @@ +#include +#include +#if QT_VERSION >= 0x050000 +#include +#endif + +#include "o2requestor.h" +#include "o2.h" +#include "o0globals.h" + +O2Requestor::O2Requestor(QNetworkAccessManager *manager, O2 *authenticator, QObject *parent): QObject(parent), reply_(NULL), status_(Idle) { + manager_ = manager; + authenticator_ = authenticator; + if (authenticator) { + timedReplies_.setIgnoreSslErrors(authenticator->ignoreSslErrors()); + } + qRegisterMetaType("QNetworkReply::NetworkError"); + connect(authenticator, SIGNAL(refreshFinished(QNetworkReply::NetworkError)), this, SLOT(onRefreshFinished(QNetworkReply::NetworkError)), Qt::QueuedConnection); +} + +O2Requestor::~O2Requestor() { +} + +void O2Requestor::setAccessTokenInAuthenticationHTTPHeaderFormat(const QString &value) { + accessTokenInAuthenticationHTTPHeaderFormat_ = value; +} + +int O2Requestor::get(const QNetworkRequest &req) { + if (-1 == setup(req, QNetworkAccessManager::GetOperation)) { + return -1; + } + reply_ = manager_->get(request_); + timedReplies_.add(reply_); + connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); + connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); + return id_; +} + +int O2Requestor::post(const QNetworkRequest &req, const QByteArray &data) { + if (-1 == setup(req, QNetworkAccessManager::PostOperation)) { + return -1; + } + data_ = data; + reply_ = manager_->post(request_, data_); + timedReplies_.add(reply_); + connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); + connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); + connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); + return id_; +} + +int O2Requestor::put(const QNetworkRequest &req, const QByteArray &data) { + if (-1 == setup(req, QNetworkAccessManager::PutOperation)) { + return -1; + } + data_ = data; + reply_ = manager_->put(request_, data_); + timedReplies_.add(reply_); + connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); + connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); + connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); + return id_; +} + +void O2Requestor::onRefreshFinished(QNetworkReply::NetworkError error) { + if (status_ != Requesting) { + qWarning() << "O2Requestor::onRefreshFinished: No pending request"; + return; + } + if (QNetworkReply::NoError == error) { + QTimer::singleShot(100, this, SLOT(retry())); + } else { + error_ = error; + QTimer::singleShot(10, this, SLOT(finish())); + } +} + +void O2Requestor::onRequestFinished() { + QNetworkReply *senderReply = qobject_cast(sender()); + QNetworkReply::NetworkError error = senderReply->error(); + if (status_ == Idle) { + return; + } + if (reply_ != senderReply) { + return; + } + if (error == QNetworkReply::NoError) { + QTimer::singleShot(10, this, SLOT(finish())); + } +} + +void O2Requestor::onRequestError(QNetworkReply::NetworkError error) { + qWarning() << "O2Requestor::onRequestError: Error" << (int)error; + if (status_ == Idle) { + return; + } + if (reply_ != qobject_cast(sender())) { + return; + } + int httpStatus = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + qWarning() << "O2Requestor::onRequestError: HTTP status" << httpStatus << reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); + if ((status_ == Requesting) && (httpStatus == 401)) { + // Call O2::refresh. Note the O2 instance might live in a different thread + if (QMetaObject::invokeMethod(authenticator_, "refresh")) { + return; + } + qCritical() << "O2Requestor::onRequestError: Invoking remote refresh failed"; + } + error_ = error; + QTimer::singleShot(10, this, SLOT(finish())); +} + +void O2Requestor::onUploadProgress(qint64 uploaded, qint64 total) { + if (status_ == Idle) { + qWarning() << "O2Requestor::onUploadProgress: No pending request"; + return; + } + if (reply_ != qobject_cast(sender())) { + return; + } + Q_EMIT uploadProgress(id_, uploaded, total); +} + +int O2Requestor::setup(const QNetworkRequest &req, QNetworkAccessManager::Operation operation) { + static int currentId; + QUrl url; + + if (status_ != Idle) { + qWarning() << "O2Requestor::setup: Another request pending"; + return -1; + } + + request_ = req; + operation_ = operation; + id_ = currentId++; + url_ = url = req.url(); +#if QT_VERSION < 0x050000 + url.addQueryItem(O2_OAUTH2_ACCESS_TOKEN, authenticator_->token()); +#else + QUrlQuery query(url); + query.addQueryItem(O2_OAUTH2_ACCESS_TOKEN, authenticator_->token()); + url.setQuery(query); +#endif + request_.setUrl(url); + + // If the service require the access token to be sent as a Authentication HTTP header, we add the access token. + if(!accessTokenInAuthenticationHTTPHeaderFormat_.isEmpty()) { + request_.setRawHeader(O2_HTTP_AUTHORIZATION_HEADER, accessTokenInAuthenticationHTTPHeaderFormat_.arg(authenticator_->token()).toLatin1()); + } + + status_ = Requesting; + error_ = QNetworkReply::NoError; + return id_; +} + +void O2Requestor::finish() { + QByteArray data; + if (status_ == Idle) { + qWarning() << "O2Requestor::finish: No pending request"; + return; + } + data = reply_->readAll(); + status_ = Idle; + timedReplies_.remove(reply_); + reply_->disconnect(this); + reply_->deleteLater(); + Q_EMIT finished(id_, error_, data); +} + +void O2Requestor::retry() { + if (status_ != Requesting) { + qWarning() << "O2Requestor::retry: No pending request"; + return; + } + timedReplies_.remove(reply_); + reply_->disconnect(this); + reply_->deleteLater(); + QUrl url = url_; +#if QT_VERSION < 0x050000 + url.addQueryItem(O2_OAUTH2_ACCESS_TOKEN, authenticator_->token()); +#else + QUrlQuery query(url); + query.addQueryItem(O2_OAUTH2_ACCESS_TOKEN, authenticator_->token()); + url.setQuery(query); +#endif + request_.setUrl(url); + + // If the service require the access token to be sent as a Authentication HTTP header, + // we update the access token when retrying. + if(!accessTokenInAuthenticationHTTPHeaderFormat_.isEmpty()) { + request_.setRawHeader(O2_HTTP_AUTHORIZATION_HEADER, accessTokenInAuthenticationHTTPHeaderFormat_.arg(authenticator_->token()).toLatin1()); + } + + status_ = ReRequesting; + switch (operation_) { + case QNetworkAccessManager::GetOperation: + reply_ = manager_->get(request_); + break; + case QNetworkAccessManager::PostOperation: + reply_ = manager_->post(request_, data_); + break; + default: + reply_ = manager_->put(request_, data_); + } + timedReplies_.add(reply_); + connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError)), Qt::QueuedConnection); + connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()), Qt::QueuedConnection); + connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); +} diff --git a/src_o2/o2simplecrypt.cpp b/src_o2/o2simplecrypt.cpp new file mode 100644 --- /dev/null +++ b/src_o2/o2simplecrypt.cpp @@ -0,0 +1,254 @@ +/* +Copyright (c) 2011, Andre Somers +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Rathenau Instituut, Andre Somers nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL ANDRE SOMERS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +#include "o0simplecrypt.h" +#include +#include +#include +#include +#include +#include + +O0SimpleCrypt::O0SimpleCrypt(): + m_key(0), + m_compressionMode(CompressionAuto), + m_protectionMode(ProtectionChecksum), + m_lastError(ErrorNoError) +{ + qsrand(uint(QDateTime::currentMSecsSinceEpoch() & 0xFFFF)); +} + +O0SimpleCrypt::O0SimpleCrypt(quint64 key): + m_key(key), + m_compressionMode(CompressionAuto), + m_protectionMode(ProtectionChecksum), + m_lastError(ErrorNoError) +{ + qsrand(uint(QDateTime::currentMSecsSinceEpoch() & 0xFFFF)); + splitKey(); +} + +void O0SimpleCrypt::setKey(quint64 key) +{ + m_key = key; + splitKey(); +} + +void O0SimpleCrypt::splitKey() +{ + m_keyParts.clear(); + m_keyParts.resize(8); + for (int i=0;i<8;i++) { + quint64 part = m_key; + for (int j=i; j>0; j--) + part = part >> 8; + part = part & 0xff; + m_keyParts[i] = static_cast(part); + } +} + +QByteArray O0SimpleCrypt::encryptToByteArray(const QString& plaintext) +{ + QByteArray plaintextArray = plaintext.toUtf8(); + return encryptToByteArray(plaintextArray); +} + +QByteArray O0SimpleCrypt::encryptToByteArray(QByteArray plaintext) +{ + if (m_keyParts.isEmpty()) { + qWarning() << "No key set."; + m_lastError = ErrorNoKeySet; + return QByteArray(); + } + + + QByteArray ba = plaintext; + + CryptoFlags flags = CryptoFlagNone; + if (m_compressionMode == CompressionAlways) { + ba = qCompress(ba, 9); //maximum compression + flags |= CryptoFlagCompression; + } else if (m_compressionMode == CompressionAuto) { + QByteArray compressed = qCompress(ba, 9); + if (compressed.count() < ba.count()) { + ba = compressed; + flags |= CryptoFlagCompression; + } + } + + QByteArray integrityProtection; + if (m_protectionMode == ProtectionChecksum) { + flags |= CryptoFlagChecksum; + QDataStream s(&integrityProtection, QIODevice::WriteOnly); + s << qChecksum(ba.constData(), ba.length()); + } else if (m_protectionMode == ProtectionHash) { + flags |= CryptoFlagHash; + QCryptographicHash hash(QCryptographicHash::Sha1); + hash.addData(ba); + + integrityProtection += hash.result(); + } + + //prepend a random char to the string + char randomChar = char(qrand() & 0xFF); + ba = randomChar + integrityProtection + ba; + + int pos(0); + char lastChar(0); + + int cnt = ba.count(); + + while (pos < cnt) { + ba[pos] = ba.at(pos) ^ m_keyParts.at(pos % 8) ^ lastChar; + lastChar = ba.at(pos); + ++pos; + } + + QByteArray resultArray; + resultArray.append(char(0x03)); //version for future updates to algorithm + resultArray.append(char(flags)); //encryption flags + resultArray.append(ba); + + m_lastError = ErrorNoError; + return resultArray; +} + +QString O0SimpleCrypt::encryptToString(const QString& plaintext) +{ + QByteArray plaintextArray = plaintext.toUtf8(); + QByteArray cypher = encryptToByteArray(plaintextArray); + QString cypherString = QString::fromLatin1(cypher.toBase64()); + return cypherString; +} + +QString O0SimpleCrypt::encryptToString(QByteArray plaintext) +{ + QByteArray cypher = encryptToByteArray(plaintext); + QString cypherString = QString::fromLatin1(cypher.toBase64()); + return cypherString; +} + +QString O0SimpleCrypt::decryptToString(const QString &cyphertext) +{ + QByteArray cyphertextArray = QByteArray::fromBase64(cyphertext.toLatin1()); + QByteArray plaintextArray = decryptToByteArray(cyphertextArray); + QString plaintext = QString::fromUtf8(plaintextArray, plaintextArray.size()); + + return plaintext; +} + +QString O0SimpleCrypt::decryptToString(QByteArray cypher) +{ + QByteArray ba = decryptToByteArray(cypher); + QString plaintext = QString::fromUtf8(ba, ba.size()); + + return plaintext; +} + +QByteArray O0SimpleCrypt::decryptToByteArray(const QString& cyphertext) +{ + QByteArray cyphertextArray = QByteArray::fromBase64(cyphertext.toLatin1()); + QByteArray ba = decryptToByteArray(cyphertextArray); + + return ba; +} + +QByteArray O0SimpleCrypt::decryptToByteArray(QByteArray cypher) +{ + if (m_keyParts.isEmpty()) { + qWarning() << "No key set."; + m_lastError = ErrorNoKeySet; + return QByteArray(); + } + + if (!cypher.length()) { + m_lastError = ErrorUnknownVersion; + return QByteArray(); + } + + QByteArray ba = cypher; + + char version = ba.at(0); + + if (version !=3) { //we only work with version 3 + m_lastError = ErrorUnknownVersion; + qWarning() << "Invalid version or not a cyphertext."; + return QByteArray(); + } + + CryptoFlags flags = CryptoFlags(ba.at(1)); + + ba = ba.mid(2); + int pos(0); + int cnt(ba.count()); + char lastChar = 0; + + while (pos < cnt) { + char currentChar = ba[pos]; + ba[pos] = ba.at(pos) ^ lastChar ^ m_keyParts.at(pos % 8); + lastChar = currentChar; + ++pos; + } + + ba = ba.mid(1); //chop off the random number at the start + + bool integrityOk(true); + if (flags.testFlag(CryptoFlagChecksum)) { + if (ba.length() < 2) { + m_lastError = ErrorIntegrityFailed; + return QByteArray(); + } + quint16 storedChecksum; + { + QDataStream s(&ba, QIODevice::ReadOnly); + s >> storedChecksum; + } + ba = ba.mid(2); + quint16 checksum = qChecksum(ba.constData(), ba.length()); + integrityOk = (checksum == storedChecksum); + } else if (flags.testFlag(CryptoFlagHash)) { + if (ba.length() < 20) { + m_lastError = ErrorIntegrityFailed; + return QByteArray(); + } + QByteArray storedHash = ba.left(20); + ba = ba.mid(20); + QCryptographicHash hash(QCryptographicHash::Sha1); + hash.addData(ba); + integrityOk = (hash.result() == storedHash); + } + + if (!integrityOk) { + m_lastError = ErrorIntegrityFailed; + return QByteArray(); + } + + if (flags.testFlag(CryptoFlagCompression)) + ba = qUncompress(ba); + + m_lastError = ErrorNoError; + return ba; +}