From 4248c9db1a3243e36a67ac61f45102e8b08212b1 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Mon, 20 May 2013 20:27:03 -0400 Subject: [PATCH] Import Hatchet plugin --- CMakeLists.txt | 1 + src/accounts/CMakeLists.txt | 11 +- src/accounts/hatchet/.gitignore | 23 + src/accounts/hatchet/CMakeLists.txt | 52 ++ .../hatchet/account/HatchetAccount.cpp | 381 ++++++++++++ src/accounts/hatchet/account/HatchetAccount.h | 129 ++++ .../hatchet/account/HatchetAccountConfig.cpp | 207 +++++++ .../hatchet/account/HatchetAccountConfig.h | 65 ++ .../hatchet/account/HatchetAccountConfig.ui | 150 +++++ .../hatchet/admin/certs/dreamcatcher.pem | 14 + .../hatchet/admin/certs/startcomroot.pem | 44 ++ .../admin/icons/hatchet-icon-512x512.png | Bin 0 -> 32731 bytes .../hatchet/cmake/Modules/FindQCA2.cmake | 37 ++ src/accounts/hatchet/resources.qrc | 7 + src/accounts/hatchet/sip/HatchetSip.cpp | 554 ++++++++++++++++++ src/accounts/hatchet/sip/HatchetSip.h | 95 +++ src/accounts/hatchet/sip/WebSocket.cpp | 291 +++++++++ src/accounts/hatchet/sip/WebSocket.h | 78 +++ .../hatchet/sip/WebSocketThreadController.cpp | 70 +++ .../hatchet/sip/WebSocketThreadController.h | 49 ++ src/accounts/hatchet/sip/hatchet_config.hpp | 72 +++ src/libtomahawk/utils/Closure.cpp | 2 +- src/libtomahawk/utils/Closure.h | 10 +- 23 files changed, 2334 insertions(+), 8 deletions(-) create mode 100644 src/accounts/hatchet/.gitignore create mode 100644 src/accounts/hatchet/CMakeLists.txt create mode 100644 src/accounts/hatchet/account/HatchetAccount.cpp create mode 100644 src/accounts/hatchet/account/HatchetAccount.h create mode 100644 src/accounts/hatchet/account/HatchetAccountConfig.cpp create mode 100644 src/accounts/hatchet/account/HatchetAccountConfig.h create mode 100644 src/accounts/hatchet/account/HatchetAccountConfig.ui create mode 100644 src/accounts/hatchet/admin/certs/dreamcatcher.pem create mode 100644 src/accounts/hatchet/admin/certs/startcomroot.pem create mode 100644 src/accounts/hatchet/admin/icons/hatchet-icon-512x512.png create mode 100644 src/accounts/hatchet/cmake/Modules/FindQCA2.cmake create mode 100644 src/accounts/hatchet/resources.qrc create mode 100644 src/accounts/hatchet/sip/HatchetSip.cpp create mode 100644 src/accounts/hatchet/sip/HatchetSip.h create mode 100644 src/accounts/hatchet/sip/WebSocket.cpp create mode 100644 src/accounts/hatchet/sip/WebSocket.h create mode 100644 src/accounts/hatchet/sip/WebSocketThreadController.cpp create mode 100644 src/accounts/hatchet/sip/WebSocketThreadController.h create mode 100644 src/accounts/hatchet/sip/hatchet_config.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0672c18d1..a080ce5c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,7 @@ add_definitions( "-DQT_STRICT_ITERATORS" ) option(BUILD_GUI "Build Tomahawk with GUI" ON) option(BUILD_RELEASE "Generate TOMAHAWK_VERSION without GIT info" OFF) option(BUILD_TESTS "Build Tomahawk with unit tests" ON) +option(BUILD_HATCHET "Build the Hatchet plugin" OFF) option(WITH_BREAKPAD "Build with breakpad integration" ON) option(WITH_CRASHREPORTER "Build with CrashReporter" ON) diff --git a/src/accounts/CMakeLists.txt b/src/accounts/CMakeLists.txt index a6604bef3..8359bc4df 100644 --- a/src/accounts/CMakeLists.txt +++ b/src/accounts/CMakeLists.txt @@ -3,16 +3,17 @@ include( ${CMAKE_CURRENT_LIST_DIR}/../../TomahawkAddPlugin.cmake ) file(GLOB SUBDIRECTORIES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "*") foreach(SUBDIRECTORY ${SUBDIRECTORIES}) if(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${SUBDIRECTORY}" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${SUBDIRECTORY}/CMakeLists.txt") - if(SUBDIRECTORY STREQUAL "xmpp") + if(SUBDIRECTORY STREQUAL "twitter") + elseif(SUBDIRECTORY STREQUAL "xmpp") if( JREEN_FOUND ) add_subdirectory( xmpp ) endif() - elseif(SUBDIRECTORY STREQUAL "twitter") - if(QTWEETLIB_FOUND AND BUILD_GUI) - add_subdirectory( twitter ) + elseif(SUBDIRECTORY STREQUAL "hatchet") + if(BUILD_HATCHET AND BUILD_GUI) + add_subdirectory(hatchet) endif() else() - add_subdirectory( ${SUBDIRECTORY} ) + add_subdirectory(${SUBDIRECTORY}) endif() endif() endforeach() diff --git a/src/accounts/hatchet/.gitignore b/src/accounts/hatchet/.gitignore new file mode 100644 index 000000000..b8c904e9b --- /dev/null +++ b/src/accounts/hatchet/.gitignore @@ -0,0 +1,23 @@ +*-build/* +build/* +.directory +*.a +*.o +._* +*.user +*.swp +*.swo +Makefile* +moc_* +*~ +*.sublime-project +*.sublime-workspace +/tomahawk +.kdev4 +*.kdev4 +*.kate-swp +clang/ +win/ +gcc/ +tags +.DS_Store diff --git a/src/accounts/hatchet/CMakeLists.txt b/src/accounts/hatchet/CMakeLists.txt new file mode 100644 index 000000000..7f2e83301 --- /dev/null +++ b/src/accounts/hatchet/CMakeLists.txt @@ -0,0 +1,52 @@ +cmake_minimum_required(VERSION 2.8) +CMAKE_POLICY(SET CMP0017 NEW) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake/Modules") + +if(NOT TOMAHAWK_LIBRARIES) + message(STATUS "BUILDING OUTSIDE TOMAHAWK") + find_package(Tomahawk REQUIRED) +else() + message(STATUS "BUILDING INSIDE TOMAHAWK") + set(TOMAHAWK_USE_FILE "${CMAKE_SOURCE_DIR}/TomahawkUse.cmake") +endif() +include( ${TOMAHAWK_USE_FILE} ) + +find_package(OpenSSL REQUIRED) +find_package(QCA2 REQUIRED) +find_package(websocketpp 0.2.99 REQUIRED) + +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR} + ${WEBSOCKETPP_INCLUDE_DIR} + ${TOMAHAWK_INCLUDE_DIRS} + ${QCA2_INCLUDE_DIR} +) + +add_definitions(-D_WEBSOCKETPP_CPP11_STL_) + +if(APPLE) + # http://stackoverflow.com/questions/7226753/osx-lion-xcode-4-1-how-do-i-setup-a-c0x-project/7236451#7236451 + add_definitions(-std=c++11 -stdlib=libc++ -U__STRICT_ANSI__) + set(PLATFORM_SPECIFIC_LINK_LIBRARIES "/usr/lib/libc++.dylib") +else() + add_definitions(-std=c++0x) +endif() + +tomahawk_add_plugin(hatchet + TYPE account + EXPORT_MACRO ACCOUNTDLLEXPORT_PRO + SOURCES + account/HatchetAccount.cpp + account/HatchetAccountConfig.cpp + sip/WebSocket.cpp + sip/WebSocketThreadController.cpp + sip/HatchetSip.cpp + UI + account/HatchetAccountConfig.ui + LINK_LIBRARIES + ${TOMAHAWK_LIBRARIES} + ${QCA2_LIBRARIES} + ${PLATFORM_SPECIFIC_LINK_LIBRARIES} +) + diff --git a/src/accounts/hatchet/account/HatchetAccount.cpp b/src/accounts/hatchet/account/HatchetAccount.cpp new file mode 100644 index 000000000..69cb60ec8 --- /dev/null +++ b/src/accounts/hatchet/account/HatchetAccount.cpp @@ -0,0 +1,381 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2012 Leo Franchi + * + * Tomahawk 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 3 of the License, or + * (at your option) any later version. + * + * Tomahawk 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 Tomahawk. If not, see . + */ + +#include "HatchetAccount.h" + +#include "HatchetAccountConfig.h" +#include "utils/Closure.h" +#include "utils/Logger.h" +#include "sip/HatchetSip.h" +#include "utils/TomahawkUtils.h" + +#include +#include +#include +#include +#include + +#include +#include + +using namespace Tomahawk; +using namespace Accounts; + +static QPixmap* s_icon = 0; +HatchetAccount* HatchetAccount::s_instance = 0; + +#define AUTH_SERVER "http://auth.toma.hk" + +HatchetAccountFactory::HatchetAccountFactory() +{ +#ifndef ENABLE_HEADLESS + if ( s_icon == 0 ) + s_icon = new QPixmap( ":/hatchet-account/hatchet-icon-512x512.png" ); +#endif +} + + +HatchetAccountFactory::~HatchetAccountFactory() +{ + +} + + +QPixmap +HatchetAccountFactory::icon() const +{ + return *s_icon; +} + + +Account* +HatchetAccountFactory::createAccount( const QString& pluginId ) +{ + return new HatchetAccount( pluginId.isEmpty() ? generateId( factoryId() ) : pluginId ); +} + + +// Hatchet account + +HatchetAccount::HatchetAccount( const QString& accountId ) + : Account( accountId ) +{ + s_instance = this; +} + + +HatchetAccount::~HatchetAccount() +{ + +} + + +HatchetAccount* +HatchetAccount::instance() +{ + return s_instance; +} + + +AccountConfigWidget* +HatchetAccount::configurationWidget() +{ + if ( m_configWidget.isNull() ) + m_configWidget = QWeakPointer( new HatchetAccountConfig( this ) ); + + return m_configWidget.data(); +} + + +void +HatchetAccount::authenticate() +{ + if ( connectionState() == Connected ) + return; + + if ( !authToken().isEmpty() ) + { + qDebug() << "Have saved credentials with auth token:" << authToken(); + if ( sipPlugin() ) + sipPlugin()->connectPlugin(); + } + else if ( !username().isEmpty() ) + { + // Need to re-prompt for password, since we don't save it! + } +} + + +void +HatchetAccount::deauthenticate() +{ + if ( !m_tomahawkSipPlugin.isNull() ) + sipPlugin()->disconnectPlugin(); + emit deauthenticated(); +} + + +void +HatchetAccount::setConnectionState( Account::ConnectionState connectionState ) +{ + m_state = connectionState; + + emit connectionStateChanged( connectionState ); +} + + +Account::ConnectionState +HatchetAccount::connectionState() const +{ + return m_state; +} + + +SipPlugin* +HatchetAccount::sipPlugin() +{ + if ( m_tomahawkSipPlugin.isNull() ) + { + tLog() << Q_FUNC_INFO; + m_tomahawkSipPlugin = QWeakPointer< HatchetSipPlugin >( new HatchetSipPlugin( this ) ); + connect( m_tomahawkSipPlugin.data(), SIGNAL( authUrlDiscovered( Tomahawk::Accounts::HatchetAccount::Service, QString ) ), + this, SLOT( authUrlDiscovered( Tomahawk::Accounts::HatchetAccount::Service, QString ) ) ); + + return m_tomahawkSipPlugin.data(); + } + return m_tomahawkSipPlugin.data(); +} + + +QPixmap +HatchetAccount::icon() const +{ + return *s_icon; +} + + +bool +HatchetAccount::isAuthenticated() const +{ + return credentials().contains( "authtoken" ); +} + + +QString +HatchetAccount::username() const +{ + return credentials().value( "username" ).toString(); +} + + +QByteArray +HatchetAccount::authToken() const +{ + return credentials().value( "authtoken" ).toByteArray(); +} + + +void +HatchetAccount::doRegister( const QString& username, const QString& password, const QString& email ) +{ + if ( username.isEmpty() || password.isEmpty() || email.isEmpty() ) + { + return; + } + + QVariantMap registerCmd; + registerCmd[ "command" ] = "register"; + registerCmd[ "email" ] = email; + registerCmd[ "password" ] = password; + registerCmd[ "username" ] = username; + + QNetworkReply* reply = buildRequest( "signup", registerCmd ); + connect( reply, SIGNAL( finished() ), this, SLOT( onRegisterFinished() ) ); +} + + +void +HatchetAccount::loginWithPassword( const QString& username, const QString& password ) +{ + if ( username.isEmpty() || password.isEmpty() ) + { + tLog() << "No tomahawk account username or pw, not logging in"; + return; + } + + QVariantMap params; + params[ "password" ] = password; + params[ "username" ] = username; + + QNetworkReply* reply = buildRequest( "login", params ); + NewClosure( reply, SIGNAL( finished() ), this, SLOT( onPasswordLoginFinished( QNetworkReply*, const QString& ) ), reply, username ); +} + + +void +HatchetAccount::fetchAccessTokens() +{ + if ( username().isEmpty() || authToken().isEmpty() ) + { + tLog() << "No authToken, not logging in"; + return; + } + + QVariantMap params; + params[ "authtoken" ] = authToken(); + params[ "username" ] = username(); + + tLog() << "Fetching access tokens"; + QNetworkReply* reply = buildRequest( "tokens", params ); + connect( reply, SIGNAL( finished() ), this, SLOT( onFetchAccessTokensFinished() ) ); +} + + +void +HatchetAccount::onRegisterFinished() +{ + QNetworkReply* reply = qobject_cast< QNetworkReply* >( sender() ); + Q_ASSERT( reply ); + bool ok; + const QVariantMap resp = parseReply( reply, ok ); + if ( !ok ) + { + emit registerFinished( false, resp.value( "errormsg" ).toString() ); + return; + } + + emit registerFinished( true, QString() ); +} + + +void +HatchetAccount::onPasswordLoginFinished( QNetworkReply* reply, const QString& username ) +{ + Q_ASSERT( reply ); + bool ok; + const QVariantMap resp = parseReply( reply, ok ); + if ( !ok ) + return; + + const QByteArray authenticationToken = resp.value( "message" ).toMap().value( "authtoken" ).toMap().value("token").toByteArray(); + + QVariantHash creds = credentials(); + creds[ "username" ] = username; + creds[ "authtoken" ] = authenticationToken; + setCredentials( creds ); + syncConfig(); + + if ( !authenticationToken.isEmpty() ) + { + // We're succesful! Now log in with our authtoken for access + fetchAccessTokens(); + } +} + + +void +HatchetAccount::onFetchAccessTokensFinished() +{ + QNetworkReply* reply = qobject_cast< QNetworkReply* >( sender() ); + Q_ASSERT( reply ); + bool ok; + const QVariantMap resp = parseReply( reply, ok ); + if ( !ok ) + { + if ( resp["code"].toInt() == 140 ) + { + tLog() << "Expired credentials, need to reauthenticate with password"; + QVariantHash creds = credentials(); + creds.remove( "authtoken" ); + setCredentials( creds ); + syncConfig(); + } + else + tLog() << "Unable to fetch access tokens"; + return; + } + + QVariantHash creds = credentials(); + creds[ "accesstokens" ] = resp[ "message" ].toMap()[ "accesstokens" ]; + setCredentials( creds ); + syncConfig(); + + emit accessTokensFetched(); +} + + +QString +HatchetAccount::authUrlForService( const Service &service ) const +{ + return m_extraAuthUrls.value( service, QString() ); +} + + +void +HatchetAccount::authUrlDiscovered( Service service, const QString &authUrl ) +{ + m_extraAuthUrls[ service ] = authUrl; +} + + +QNetworkReply* +HatchetAccount::buildRequest( const QString& command, const QVariantMap& params ) const +{ + QJson::Serializer s; + const QByteArray msgJson = s.serialize( params ); + + QNetworkRequest req( QUrl( QString( "%1/%2" ).arg( AUTH_SERVER ).arg( command ) ) ); + req.setHeader( QNetworkRequest::ContentTypeHeader, "application/json; charset=utf-8" ); + QNetworkReply* reply = TomahawkUtils::nam()->post( req, msgJson ); + + return reply; +} + + +QVariantMap +HatchetAccount::parseReply( QNetworkReply* reply, bool& okRet ) const +{ + QVariantMap resp; + + reply->deleteLater(); + + if ( reply->error() != QNetworkReply::NoError ) + { + tLog() << "Network error in command:" << reply->error() << reply->errorString(); + okRet = false; + return resp; + } + + QJson::Parser p; + bool ok; + resp = p.parse( reply, &ok ).toMap(); + + if ( !ok || resp.value( "error", false ).toBool() ) + { + tLog() << "Error from tomahawk server response, or in parsing from json:" << resp.value( "errormsg" ) << resp; + okRet = false; + return resp; + } + + tLog() << "Got reply" << resp.keys(); + tLog() << "Got reply" << resp.values(); + okRet = true; + return resp; +} + +Q_EXPORT_PLUGIN2( Tomahawk::Accounts::AccountFactory, Tomahawk::Accounts::HatchetAccountFactory ) diff --git a/src/accounts/hatchet/account/HatchetAccount.h b/src/accounts/hatchet/account/HatchetAccount.h new file mode 100644 index 000000000..3c8398f83 --- /dev/null +++ b/src/accounts/hatchet/account/HatchetAccount.h @@ -0,0 +1,129 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2012 Leo Franchi + * + * Tomahawk 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 3 of the License, or + * (at your option) any later version. + * + * Tomahawk 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 Tomahawk. If not, see . + */ + +#ifndef TOMAHAWK_ACCOUNT_H +#define TOMAHAWK_ACCOUNT_H + + +#include +#include + +class QNetworkReply; + +class HatchetSipPlugin; + +namespace Tomahawk +{ +namespace Accounts +{ + +class HatchetAccountConfig; + +class ACCOUNTDLLEXPORT HatchetAccountFactory : public AccountFactory +{ + Q_OBJECT + Q_INTERFACES( Tomahawk::Accounts::AccountFactory ) +public: + HatchetAccountFactory(); + virtual ~HatchetAccountFactory(); + + virtual QString factoryId() const { return "hatchetaccount"; } + virtual QString prettyName() const { return "Hatchet"; } + virtual QString description() const { return tr( "Connect to your Hatchet account" ); } + virtual bool isUnique() const { return true; } + AccountTypes types() const { return AccountTypes( SipType ); }; +// virtual bool allowUserCreation() const { return false; } +#ifndef ENABLE_HEADLESS + virtual QPixmap icon() const; +#endif + + + virtual Account* createAccount ( const QString& pluginId = QString() ); +}; + +class ACCOUNTDLLEXPORT HatchetAccount : public Account +{ + Q_OBJECT +public: + enum Service { + Facebook = 0 + }; + + HatchetAccount( const QString &accountId ); + virtual ~HatchetAccount(); + + static HatchetAccount* instance(); + + QPixmap icon() const; + + void authenticate(); + void deauthenticate(); + bool isAuthenticated() const; + + void setConnectionState( Account::ConnectionState connectionState ); + ConnectionState connectionState() const; + + + virtual Tomahawk::InfoSystem::InfoPluginPtr infoPlugin() { return Tomahawk::InfoSystem::InfoPluginPtr(); } + SipPlugin* sipPlugin(); + + AccountConfigWidget* configurationWidget(); + QWidget* aclWidget() { return 0; } + + QString username() const; + + void fetchAccessTokens(); + + QString authUrlForService( const Service& service ) const; + +signals: + void deauthenticated(); + void accessTokensFetched(); + void registerFinished( bool successful, const QString& msg ); + +private slots: + void onRegisterFinished(); + void onPasswordLoginFinished( QNetworkReply*, const QString& username ); + void onFetchAccessTokensFinished(); + + void authUrlDiscovered( Tomahawk::Accounts::HatchetAccount::Service service, const QString& authUrl ); +private: + QByteArray authToken() const; + + void doRegister( const QString& username, const QString& password, const QString& email ); + void loginWithPassword( const QString& username, const QString& password ); + + QNetworkReply* buildRequest( const QString& command, const QVariantMap& params ) const; + QVariantMap parseReply( QNetworkReply* reply, bool& ok ) const; + + QWeakPointer m_configWidget; + + Account::ConnectionState m_state; + + QWeakPointer< HatchetSipPlugin > m_tomahawkSipPlugin; + QHash< Service, QString > m_extraAuthUrls; + + static HatchetAccount* s_instance; + friend class HatchetAccountConfig; +}; + +} +} + + +#endif diff --git a/src/accounts/hatchet/account/HatchetAccountConfig.cpp b/src/accounts/hatchet/account/HatchetAccountConfig.cpp new file mode 100644 index 000000000..b1ea83edf --- /dev/null +++ b/src/accounts/hatchet/account/HatchetAccountConfig.cpp @@ -0,0 +1,207 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2012 Leo Franchi + * + * Tomahawk 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 3 of the License, or + * (at your option) any later version. + * + * Tomahawk 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 Tomahawk. If not, see . + */ + +#include "HatchetAccountConfig.h" +#include "HatchetAccount.h" +#include "utils/TomahawkUtils.h" +#include "utils/Logger.h" + +#include "ui_HatchetAccountConfig.h" + +using namespace Tomahawk; +using namespace Accounts; + +namespace { + enum ButtonAction { + Login, + Register, + Logout + }; +} + +HatchetAccountConfig::HatchetAccountConfig( HatchetAccount* account ) + : AccountConfigWidget( 0 ) + , m_ui( new Ui::HatchetAccountConfig ) + , m_account( account ) +{ + Q_ASSERT( m_account ); + + m_ui->setupUi( this ); + + m_ui->emailLabel->hide(); + m_ui->emailEdit->hide(); + + connect( m_ui->registerbutton, SIGNAL( clicked( bool ) ), this, SLOT( registerClicked() ) ); + connect( m_ui->loginOrRegisterButton, SIGNAL( clicked( bool ) ), this, SLOT( loginOrRegister() ) ); + + connect( m_ui->usernameEdit, SIGNAL( textChanged( QString ) ), this, SLOT( fieldsChanged() ) ); + connect( m_ui->passwordEdit, SIGNAL( textChanged( QString ) ), this, SLOT( fieldsChanged() ) ); + connect( m_ui->emailEdit, SIGNAL( textChanged( QString ) ), this, SLOT( fieldsChanged() ) ); + + connect( m_account, SIGNAL( registerFinished( bool, QString ) ), this, SLOT( registerFinished( bool, QString ) ) ); + connect( m_account, SIGNAL( deauthenticated() ), this, SLOT( showLoggedOut() ) ); + connect( m_account, SIGNAL( accessTokensFetched() ), this, SLOT( accountInfoUpdated() ) ); + + if ( !m_account->authToken().isEmpty() ) + accountInfoUpdated(); + else + { + m_ui->usernameEdit->setText( m_account->username() ); + showLoggedOut(); + } +} + +HatchetAccountConfig::~HatchetAccountConfig() +{ + +} + + +void +HatchetAccountConfig::registerClicked() +{ + m_ui->registerbutton->hide(); + + m_ui->emailLabel->show(); + m_ui->emailEdit->show(); + m_ui->loginOrRegisterButton->setText( tr( "Register" ) ); + m_ui->loginOrRegisterButton->setProperty( "action", Register ); + +} + + +void +HatchetAccountConfig::loginOrRegister() +{ + const ButtonAction action = static_cast< ButtonAction>( m_ui->loginOrRegisterButton->property( "action" ).toInt() ); + + if ( action == Login ) + { + // Log in mode + m_account->loginWithPassword( m_ui->usernameEdit->text(), m_ui->passwordEdit->text() ); + } + else if ( action == Register ) + { + // Register since the use clicked register and just entered his info + const QString username = m_ui->usernameEdit->text(); + const QString password = m_ui->passwordEdit->text(); + const QString email = m_ui->emailEdit->text(); + m_account->doRegister( username, password, email ); + } + else if ( action == Logout ) + { + // TODO + m_ui->usernameEdit->clear(); + m_ui->passwordEdit->clear(); + + QVariantHash creds = m_account->credentials(); + creds.clear(); + m_account->setCredentials( creds ); + m_account->sync(); + m_account->deauthenticate(); + } +} + + +void +HatchetAccountConfig::fieldsChanged() +{ + const QString username = m_ui->usernameEdit->text(); + const QString password = m_ui->passwordEdit->text(); + const QString email = m_ui->emailEdit->text(); + + const ButtonAction action = static_cast< ButtonAction>( m_ui->loginOrRegisterButton->property( "action" ).toInt() ); + + m_ui->loginOrRegisterButton->setEnabled( !username.isEmpty() && !password.isEmpty() && ( action == Login || !email.isEmpty() ) ); + + m_ui->errorLabel->clear(); + + if ( action == Login ) + m_ui->loginOrRegisterButton->setText( tr( "Login" ) ); + else if ( action == Register ) + m_ui->loginOrRegisterButton->setText( tr( "Register" ) ); +} + + +void +HatchetAccountConfig::registerFinished( bool success, const QString& error ) +{ + if ( success ) + { + showLoggedOut(); + m_ui->errorLabel->setText( tr( "An email has been sent to activate your account" ) ); + } + else + { + m_ui->loginOrRegisterButton->setText( "Failed" ); + m_ui->loginOrRegisterButton->setEnabled( false ); + m_ui->errorLabel->setText( error ); + } +} + + +void +HatchetAccountConfig::showLoggedIn() +{ + m_ui->registerbutton->hide(); + m_ui->usernameLabel->hide(); + m_ui->usernameEdit->hide(); + m_ui->emailLabel->hide(); + m_ui->emailEdit->hide(); + m_ui->passwordLabel->hide(); + m_ui->passwordEdit->hide(); + + m_ui->loggedInLabel->setText( tr( "Logged in as: %1" ).arg( m_account->username() ) ); + m_ui->loggedInLabel->show(); + + m_ui->errorLabel->clear(); + m_ui->errorLabel->hide(); + + m_ui->loginOrRegisterButton->setText( "Log out" ); + m_ui->loginOrRegisterButton->setProperty( "action", Logout ); +} + + +void +HatchetAccountConfig::showLoggedOut() +{ + m_ui->emailEdit->hide(); + m_ui->emailLabel->hide(); + + m_ui->registerbutton->show(); + m_ui->usernameLabel->show(); + m_ui->usernameEdit->show(); + m_ui->passwordLabel->show(); + m_ui->passwordEdit->show(); + + m_ui->loggedInLabel->clear(); + m_ui->loggedInLabel->hide(); + + m_ui->errorLabel->clear(); + + m_ui->loginOrRegisterButton->setText( "Login" ); + m_ui->loginOrRegisterButton->setProperty( "action", Login ); +} + + +void +HatchetAccountConfig::accountInfoUpdated() +{ + showLoggedIn(); + return; +} diff --git a/src/accounts/hatchet/account/HatchetAccountConfig.h b/src/accounts/hatchet/account/HatchetAccountConfig.h new file mode 100644 index 000000000..be125679c --- /dev/null +++ b/src/accounts/hatchet/account/HatchetAccountConfig.h @@ -0,0 +1,65 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2012 Leo Franchi + * + * Tomahawk 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 3 of the License, or + * (at your option) any later version. + * + * Tomahawk 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 Tomahawk. If not, see . + */ + +#ifndef TOMAHAWK_ACCOUNT_CONFIG_H +#define TOMAHAWK_ACCOUNT_CONFIG_H + +#include + +#include +#include + +class QNetworkReply; + +namespace Ui { + class HatchetAccountConfig; +}; + +namespace Tomahawk { +namespace Accounts { + +class HatchetAccount; + +class HatchetAccountConfig : public AccountConfigWidget +{ + Q_OBJECT +public: + explicit HatchetAccountConfig( HatchetAccount* account ); + virtual ~HatchetAccountConfig(); + +private slots: + void registerClicked(); + void loginOrRegister(); + + void registerFinished( bool success, const QString& error ); + + void fieldsChanged(); + + void showLoggedIn(); + void showLoggedOut(); + + void accountInfoUpdated(); +private: + Ui::HatchetAccountConfig* m_ui; + HatchetAccount* m_account; +}; + +} +} + +#endif diff --git a/src/accounts/hatchet/account/HatchetAccountConfig.ui b/src/accounts/hatchet/account/HatchetAccountConfig.ui new file mode 100644 index 000000000..a2e9413be --- /dev/null +++ b/src/accounts/hatchet/account/HatchetAccountConfig.ui @@ -0,0 +1,150 @@ + + + HatchetAccountConfig + + + + 0 + 0 + 301 + 404 + + + + Form + + + + + + + + + :/hatchet-account/hatchet-icon-512x512.png + + + Qt::AlignCenter + + + + + + + Connect to your Hatchet account + + + Qt::AlignCenter + + + + + + + Register for a free account + + + + + + + + 10 + 75 + true + + + + + + + Qt::AlignCenter + + + + + + + + + Email: + + + + + + + Enter email to use + + + + + + + Username + + + + + + + Hatchet username + + + + + + + Password: + + + + + + + QLineEdit::Password + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + true + + + + + + + Login + + + + + + + + + + + + diff --git a/src/accounts/hatchet/admin/certs/dreamcatcher.pem b/src/accounts/hatchet/admin/certs/dreamcatcher.pem new file mode 100644 index 000000000..6489d80bb --- /dev/null +++ b/src/accounts/hatchet/admin/certs/dreamcatcher.pem @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyT6j9B1hiRHwV96BSZJp +vLnGS0p6h1fxJiVGOjpcct/pA1rfVW66LTC7seYT6mF15flccIn5H8srAvH2a57S +uCMHDrHkQIoNDoOf+Ersw4tYzVYv+5h8IN9apYr6iqfT7rn3Fzb5oEB+GqcDurX3 +4aaDjm+Wq0QjJAZf0fOaKor0ASejkj6EywpaHLwj51DKtf6SYbDEf52vDOzsDDnx +2AzVeqzPNFnnau6/v7rCi081b33qOrbJBtJjSNAJcM3iVf465k8KP2VD8nX3wzwT +9H0TEp67UMm0lz2i6gSAyEcIR2FNdTjVa1ZHYF2tnOihFDu0H4U4/4mPswpNNdOp +mtc5WxxK5ouKqxD8ArSoclmZMybIBbL0lLRjo4VC/olfbd1RQ57L9ETtNIf0xxKU +aweUkfWyJfU5ueaSlBaGoKSGugqp50ql3Td0m2JnJmaopdSi12L3EdlaO0+/OL3f +0aXbtnsZXlIHoq7FBEYcLhCNbwEeceV5Cveb5Os8w2331jkgpcPoXQFOtIVyva2B +3RadI0aALIbX5Tt0/AcbLeq10BJH8Ny2RSrhF2cxkjXEwQ68OUbCv1U4Si+EL44E +tctg8zj9rB8SG+miE18cXaQEUxA/olOU/Axkm8dLLLTxqWsD3VQYDxBgBemaY1OT +O32yipB9ko1sLCx6ZaQQ56sCAwEAAQ== +-----END PUBLIC KEY----- diff --git a/src/accounts/hatchet/admin/certs/startcomroot.pem b/src/accounts/hatchet/admin/certs/startcomroot.pem new file mode 100644 index 000000000..960f2657b --- /dev/null +++ b/src/accounts/hatchet/admin/certs/startcomroot.pem @@ -0,0 +1,44 @@ +-----BEGIN CERTIFICATE----- +MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEW +MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg +Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM2WhcNMzYwOTE3MTk0NjM2WjB9 +MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi +U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh +cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk +pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf +OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C +Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT +Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi +HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM +Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w ++2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+ +Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3 +Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B +26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID +AQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE +FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9j +ZXJ0LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3Js +LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFM +BgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUHAgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0 +Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRwOi8vY2VydC5zdGFy +dGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYgU3Rh +cnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlh +YmlsaXR5LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2Yg +dGhlIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFp +bGFibGUgYXQgaHR0cDovL2NlcnQuc3RhcnRjb20ub3JnL3BvbGljeS5wZGYwEQYJ +YIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNT +TCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOCAgEAFmyZ +9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8 +jhvh3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUW +FjgKXlf2Ysd6AgXmvB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJz +ewT4F+irsfMuXGRuczE6Eri8sxHkfY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1 +ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3fsNrarnDy0RLrHiQi+fHLB5L +EUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZEoalHmdkrQYu +L6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq +yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuC +O3NJo2pXh5Tl1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6V +um0ABj6y6koQOdjQK/W/7HW/lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkySh +NOsF/5oirpt9P/FlUQqmMGqz9IgcgA38corog14= +-----END CERTIFICATE----- diff --git a/src/accounts/hatchet/admin/icons/hatchet-icon-512x512.png b/src/accounts/hatchet/admin/icons/hatchet-icon-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..6e8543ca383ffd1b4598c94f698eb63ff8c834c0 GIT binary patch literal 32731 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 m)a!8CydG||ON+okBk=#p%w!nP9gZLX0000 +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +if (NOT WIN32) + find_package(PkgConfig) + pkg_check_modules(PC_QCA2 qca2) + set(QCA2_DEFINITIONS ${PC_QCA2_CFLAGS_OTHER}) +endif (NOT WIN32) + +find_library(QCA2_LIBRARIES + NAMES qca + HINTS ${PC_QCA2_LIBDIR} ${PC_QCA2_LIBRARY_DIRS} +) + +find_path(QCA2_INCLUDE_DIR qca.h + HINTS ${PC_QCA2_INCLUDEDIR} ${PC_QCA2_INCLUDE_DIRS} + PATH_SUFFIXES QtCrypto + PATHS /usr/local/lib/qca.framework/Headers/ +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(QCA2 DEFAULT_MSG QCA2_LIBRARIES QCA2_INCLUDE_DIR) + +mark_as_advanced(QCA2_INCLUDE_DIR QCA2_LIBRARIES) diff --git a/src/accounts/hatchet/resources.qrc b/src/accounts/hatchet/resources.qrc new file mode 100644 index 000000000..7440ede9f --- /dev/null +++ b/src/accounts/hatchet/resources.qrc @@ -0,0 +1,7 @@ + + + admin/icons/hatchet-icon-512x512.png + admin/certs/dreamcatcher.pem + admin/certs/startcomroot.pem + + diff --git a/src/accounts/hatchet/sip/HatchetSip.cpp b/src/accounts/hatchet/sip/HatchetSip.cpp new file mode 100644 index 000000000..b2215d5c1 --- /dev/null +++ b/src/accounts/hatchet/sip/HatchetSip.cpp @@ -0,0 +1,554 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2012, Jeff Mitchell + * + * Tomahawk 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 3 of the License, or + * (at your option) any later version. + * + * Tomahawk 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 Tomahawk. If not, see . + */ + +#include "HatchetSip.h" + +#include "account/HatchetAccount.h" +#include "WebSocketThreadController.h" +//#include "WebSocket.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +HatchetSipPlugin::HatchetSipPlugin( Tomahawk::Accounts::Account *account ) + : SipPlugin( account ) + , m_sipState( Closed ) + , m_version( 0 ) + , m_publicKey( 0 ) +{ + tLog() << Q_FUNC_INFO; + + connect( m_account, SIGNAL( accessTokensFetched() ), this, SLOT( connectWebSocket() ) ); + connect( Servent::instance(), SIGNAL( dbSyncTriggered() ), this, SLOT( dbSyncTriggered() )); + + QFile pemFile( ":/hatchet-account/dreamcatcher.pem" ); + pemFile.open( QIODevice::ReadOnly ); + tLog() << Q_FUNC_INFO << "certs/dreamcatcher.pem: " << pemFile.readAll(); + pemFile.close(); + pemFile.open( QIODevice::ReadOnly ); + QCA::ConvertResult conversionResult; + QCA::PublicKey publicKey = QCA::PublicKey::fromPEM(pemFile.readAll(), &conversionResult); + if ( QCA::ConvertGood != conversionResult ) + { + tLog() << Q_FUNC_INFO << "INVALID PUBKEY READ"; + return; + } + m_publicKey = new QCA::PublicKey( publicKey ); +} + + +HatchetSipPlugin::~HatchetSipPlugin() +{ + if ( m_webSocketThreadController ) + { + m_webSocketThreadController->quit(); + m_webSocketThreadController->wait( 60000 ); + + delete m_webSocketThreadController; + m_webSocketThreadController = 0; + } + + m_sipState = Closed; + + hatchetAccount()->setConnectionState( Tomahawk::Accounts::Account::Disconnected ); +} + + +bool +HatchetSipPlugin::isValid() const +{ + return m_account->enabled() && m_account->isAuthenticated() && m_publicKey; +} + + +void +HatchetSipPlugin::sendSipInfo(const Tomahawk::peerinfo_ptr& receiver, const SipInfo& info) +{ + const QString dbid = receiver->data().toMap().value( "dbid" ).toString(); + tLog() << Q_FUNC_INFO << "Send local info to " << receiver->friendlyName() << "(" << dbid << ") we are" << info.nodeId() << "with offerkey " << info.key(); + + QVariantMap sendMap; + sendMap[ "command" ] = "authorize-peer"; + sendMap[ "dbid" ] = dbid; + sendMap[ "offerkey" ] = info.key(); + + + if ( !sendBytes( sendMap ) ) + tLog() << Q_FUNC_INFO << "Failed sending message"; +} + +Tomahawk::Accounts::HatchetAccount* +HatchetSipPlugin::hatchetAccount() const +{ + return qobject_cast< Tomahawk::Accounts::HatchetAccount* >( m_account ); +} + + +void +HatchetSipPlugin::connectPlugin() +{ + if ( !m_account->isAuthenticated() ) + { + //FIXME: Prompt user for password? + return; + } + + m_webSocketThreadController = QPointer< WebSocketThreadController >( new WebSocketThreadController( this ) ); + + hatchetAccount()->setConnectionState( Tomahawk::Accounts::Account::Connecting ); + hatchetAccount()->fetchAccessTokens(); +} + + +void +HatchetSipPlugin::disconnectPlugin() +{ + if ( m_webSocketThreadController ) + { + m_webSocketThreadController->quit(); + m_webSocketThreadController->wait( 60000 ); + + delete m_webSocketThreadController; + m_webSocketThreadController = 0; + } + + m_sipState = Closed; + m_version = 0; + + hatchetAccount()->setConnectionState( Tomahawk::Accounts::Account::Disconnected ); +} + + +///////////////////////////// Connection methods //////////////////////////////////// + + +void +HatchetSipPlugin::connectWebSocket() +{ + //Other things can request access tokens, so if we're already connected there's no need to pay attention +// if ( !m_ws.isNull() ) +// return; + + if ( !isValid() ) + { + tLog() << Q_FUNC_INFO << "Invalid state, not continuing with connection"; + return; + } + + QVariantList tokensCreds = m_account->credentials()[ "accesstokens" ].toList(); + //FIXME: Don't blindly pick the first one that matches? + QVariantMap connectVals; + foreach ( QVariant credObj, tokensCreds ) + { + QVariantMap creds = credObj.toMap(); + if ( creds.contains( "type" ) && creds[ "type" ].toString() == "dreamcatcher" ) + { + connectVals = creds; + m_userid = creds["userid"].toString(); + m_token = creds["token"].toString(); + break; + } + } + + QString url; + if ( !connectVals.isEmpty() ) + url = connectVals[ "host" ].toString() + ':' + connectVals[ "port" ].toString(); + + if ( url.isEmpty() ) + { + tLog() << Q_FUNC_INFO << "Unable to find a proper connection endpoint; bailing"; + disconnectPlugin(); + return; + } + else + tLog() << Q_FUNC_INFO << "Connecting to Dreamcatcher endpoint at: " << url; + + m_webSocketThreadController->setUrl( url ); +// connect( m_ws.data(), SIGNAL( opened() ), this, SLOT( onWsOpened() ) ); +// connect( m_ws.data(), SIGNAL( failed( QString ) ), this, SLOT( onWsFailed( QString ) ) ); +// connect( m_ws.data(), SIGNAL( closed( QString ) ), this, SLOT( onWsClosed( QString ) ) ); +// connect( m_ws.data(), SIGNAL( message( QString ) ), this, SLOT( onWsMessage( QString ) ) ); + m_webSocketThreadController->start(); +} + + +void +HatchetSipPlugin::webSocketConnected() +{ + tLog() << Q_FUNC_INFO << "WebSocket connected"; + + if ( m_token.isEmpty() || !m_account->credentials().contains( "username" ) ) + { + tLog() << Q_FUNC_INFO << "access token or username is empty, aborting"; + disconnectPlugin(); + return; + } + + hatchetAccount()->setConnectionState( Tomahawk::Accounts::Account::Connected ); + m_sipState = AcquiringVersion; + + m_uuid = QUuid::createUuid().toString(); + QCA::SecureArray sa( m_uuid.toLatin1() ); + QCA::SecureArray result = m_publicKey->encrypt( sa, QCA::EME_PKCS1_OAEP ); + + tLog() << Q_FUNC_INFO << "uuid:" << m_uuid << ", size of uuid:" << m_uuid.size() << ", size of sa:" << sa.size() << ", size of result:" << result.size(); + + QVariantMap nonceVerMap; + nonceVerMap[ "version" ] = VERSION; + nonceVerMap[ "nonce" ] = QString( result.toByteArray().toBase64() ); + sendBytes( nonceVerMap ); +} + + +void +HatchetSipPlugin::webSocketDisconnected() +{ + tLog() << Q_FUNC_INFO << "WebSocket disconnected"; + m_sipState = Closed; +} + + +bool +HatchetSipPlugin::sendBytes( const QVariantMap& jsonMap ) const +{ + tLog() << Q_FUNC_INFO; + if ( m_sipState == Closed ) + { + tLog() << Q_FUNC_INFO << "was told to send bytes on a closed connection, not gonna do it"; + return false; + } + + QJson::Serializer serializer; + QByteArray bytes = serializer.serialize( jsonMap ); + if ( bytes.isEmpty() ) + { + tLog() << Q_FUNC_INFO << "could not serialize register structure to JSON"; + return false; + } + + tLog() << Q_FUNC_INFO << "Sending bytes of size" << bytes.size(); + emit rawBytes( bytes ); + return true; +} + + +void +HatchetSipPlugin::onWsFailed( const QString &msg ) +{ + tLog() << Q_FUNC_INFO << "WebSocket failed with message: " << msg; + disconnectPlugin(); +} + + +void +HatchetSipPlugin::onWsClosed( const QString &msg ) +{ + tLog() << Q_FUNC_INFO << "WebSocket closed with message: " << msg; + disconnectPlugin(); +} + + +void +HatchetSipPlugin::messageReceived( const QByteArray &msg ) +{ + tLog() << Q_FUNC_INFO << "WebSocket message: " << msg; + + QJson::Parser parser; + bool ok; + QVariant jsonVariant = parser.parse( msg, &ok ); + if ( !jsonVariant.isValid() ) + { + tLog() << Q_FUNC_INFO << "Failed to parse message back from server"; + return; + } + + QVariantMap retMap = jsonVariant.toMap(); + + if ( m_sipState == AcquiringVersion ) + { + tLog() << Q_FUNC_INFO << "In acquiring version state, expecting version/nonce information"; + if ( !retMap.contains( "version" ) || !retMap.contains( "nonce" ) ) + { + tLog() << Q_FUNC_INFO << "Failed to acquire version or nonce information"; + disconnectPlugin(); + return; + } + bool ok = false; + int ver = retMap[ "version" ].toInt( &ok ); + if ( ver == 0 || !ok ) + { + tLog() << Q_FUNC_INFO << "Failed to acquire version information"; + disconnectPlugin(); + return; + } + + if ( retMap[ "nonce" ].toString() != m_uuid ) + { + tLog() << Q_FUNC_INFO << "Failed to validate nonce"; + disconnectPlugin(); + return; + } + + m_version = ver; + + QVariantMap registerMap; + registerMap[ "command" ] = "register"; + registerMap[ "userid" ] = m_userid; + registerMap[ "host" ] = Servent::instance()->externalAddress(); + registerMap[ "port" ] = Servent::instance()->externalPort(); + registerMap[ "token" ] = m_token; + registerMap[ "dbid" ] = Database::instance()->impl()->dbid(); + registerMap[ "alias" ] = QHostInfo::localHostName(); + + if ( !sendBytes( registerMap ) ) + { + tLog() << Q_FUNC_INFO << "Failed sending message"; + disconnectPlugin(); + return; + } + + m_sipState = Registering; + } + else if ( m_sipState == Registering ) + { + tLog() << Q_FUNC_INFO << "In registering state, checking status of registration"; + if ( retMap.contains( "status" ) && + retMap[ "status" ].toString() == "success" ) + { + tLog() << Q_FUNC_INFO << "Registered successfully"; + m_sipState = Connected; + hatchetAccount()->setConnectionState( Tomahawk::Accounts::Account::Connected ); + QTimer::singleShot(0, this, SLOT( dbSyncTriggered() ) ); + return; + } + else + { + tLog() << Q_FUNC_INFO << "Failed to register successfully"; + //m_ws.data()->stop(); + return; + } + } + else if ( m_sipState != Connected ) + { + // ...erm? + tLog() << Q_FUNC_INFO << "Got a message from a non connected socket?"; + return; + } + else if ( !retMap.contains( "command" ) || + !retMap[ "command" ].canConvert< QString >() ) + { + tLog() << Q_FUNC_INFO << "Unable to convert and/or interepret command from server"; + return; + } + + QString command = retMap[ "command" ].toString(); + + if ( command == "new-peer" ) + newPeer( retMap ); + else if ( command == "peer-authorization" ) + peerAuthorization( retMap ); + else if ( command == "synclastseen" ) + sendOplog( retMap ); +} + + +bool +HatchetSipPlugin::checkKeys( QStringList keys, const QVariantMap& map ) const +{ + foreach ( QString key, keys ) + { + if ( !map.contains( key ) ) + { + tLog() << Q_FUNC_INFO << "Did not find the value" << key << "in the new-peer structure"; + return false; + } + } + return true; +} + + +///////////////////////////// Peer handling methods //////////////////////////////////// + + +void +HatchetSipPlugin::newPeer( const QVariantMap& valMap ) +{ + const QString username = valMap[ "username" ].toString(); + const QString dbid = valMap[ "dbid" ].toString(); + const QString host = valMap[ "host" ].toString(); + unsigned int port = valMap[ "port" ].toUInt(); + + tLog() << Q_FUNC_INFO << "username:" << username << "dbid" << dbid; + + QStringList keys( QStringList() << "command" << "username" << "host" << "port" << "dbid" ); + if ( !checkKeys( keys, valMap ) ) + return; + + Tomahawk::peerinfo_ptr peerInfo = Tomahawk::PeerInfo::get( this, dbid, Tomahawk::PeerInfo::AutoCreate ); + peerInfo->setContactId( username ); + peerInfo->setFriendlyName( username ); + QVariantMap data; + data.insert( "dbid", QVariant::fromValue< QString >( dbid ) ); + peerInfo->setData( data ); + + + SipInfo sipInfo; + sipInfo.setNodeId( dbid ); + if( !host.isEmpty() && port != 0 ) + { + sipInfo.setHost( valMap[ "host" ].toString() ); + sipInfo.setPort( valMap[ "port" ].toUInt() ); + sipInfo.setVisible( true ); + } + else + { + sipInfo.setVisible( false ); + } + peerInfo->setSipInfo( sipInfo ); + + peerInfo->setStatus( Tomahawk::PeerInfo::Online ); +} + + +void +HatchetSipPlugin::peerAuthorization( const QVariantMap& valMap ) +{ + tLog() << Q_FUNC_INFO << "dbid:" << valMap[ "dbid" ].toString() << "offerkey" << valMap[ "offerkey" ].toString(); + + QStringList keys( QStringList() << "command" << "dbid" << "offerkey" ); + if ( !checkKeys( keys, valMap ) ) + return; + + + Tomahawk::peerinfo_ptr peerInfo = Tomahawk::PeerInfo::get( this, valMap[ "dbid" ].toString() ); + if( peerInfo.isNull() ) + { + tLog() << Q_FUNC_INFO << "Received a peer-authorization for a peer we don't know about"; + return; + } + + SipInfo sipInfo = peerInfo->sipInfo(); + sipInfo.setKey( valMap[ "offerkey" ].toString() ); + peerInfo->setSipInfo( sipInfo ); +} + + +///////////////////////////// Syncing methods //////////////////////////////////// + +void +HatchetSipPlugin::dbSyncTriggered() +{ + if ( m_sipState != Connected ) + return; + + if ( !SourceList::instance() || SourceList::instance()->getLocal().isNull() ) + return; + + QVariantMap sourceMap; + sourceMap[ "command" ] = "synctrigger"; + const Tomahawk::source_ptr src = SourceList::instance()->getLocal(); + sourceMap[ "name" ] = src->friendlyName(); + sourceMap[ "alias" ] = QHostInfo::localHostName(); + sourceMap[ "friendlyname" ] = src->dbFriendlyName(); + + if ( !sendBytes( sourceMap ) ) + { + tLog() << Q_FUNC_INFO << "Failed sending message"; + return; + } +} + + +void +HatchetSipPlugin::sendOplog( const QVariantMap& valMap ) const +{ + tLog() << Q_FUNC_INFO; + DatabaseCommand_loadOps* cmd = new DatabaseCommand_loadOps( SourceList::instance()->getLocal(), valMap[ "lastrevision" ].toString() ); + connect( cmd, SIGNAL( done( QString, QString, QList< dbop_ptr > ) ), SLOT( oplogFetched( QString, QString, QList< dbop_ptr > ) ) ); + Database::instance()->enqueue( QSharedPointer< DatabaseCommand >( cmd ) ); +} + + +void +HatchetSipPlugin::oplogFetched( const QString& sinceguid, const QString& /* lastguid */, const QList< dbop_ptr > ops ) const +{ + tLog() << Q_FUNC_INFO; + const uint_fast32_t byteMax = 1 << 25; + int currBytes = 0; + QVariantMap commandMap; + commandMap[ "command" ] = "oplog"; + commandMap[ "startingrevision" ] = sinceguid; + currBytes += 60; //baseline for quotes, keys, commas, colons, etc. + QVariantList revisions; + tLog() << Q_FUNC_INFO << "Found" << ops.size() << "ops"; + foreach( const dbop_ptr op, ops ) + { + currBytes += 80; //baseline for quotes, keys, commas, colons, etc. + QVariantMap revMap; + revMap[ "revision" ] = op->guid; + currBytes += op->guid.length(); + revMap[ "singleton" ] = op->singleton; + currBytes += 5; //true or false + revMap[ "command" ] = op->command; + currBytes += op->command.length(); + currBytes += 5; //true or false + if ( op->compressed ) + { + revMap[ "compressed" ] = true; + QByteArray b64 = op->payload.toBase64(); + revMap[ "payload" ] = op->payload.toBase64(); + currBytes += b64.length(); + } + else + { + revMap[ "compressed" ] = false; + revMap[ "payload" ] = op->payload; + currBytes += op->payload.length(); + } + if ( currBytes >= (int)(byteMax - 1000000) ) // tack on an extra 1M for safety as it seems qjson puts in spaces + break; + else + revisions << revMap; + } + tLog() << Q_FUNC_INFO << "Sending" << revisions.size() << "revisions"; + commandMap[ "revisions" ] = revisions; + + if ( !sendBytes( commandMap ) ) + { + tLog() << Q_FUNC_INFO << "Failed sending message, attempting to send a blank message to clear sync state"; + QVariantMap rescueMap; + rescueMap[ "command" ] = "oplog"; + if ( !sendBytes( rescueMap ) ) + { + tLog() << Q_FUNC_INFO << "Failed to send rescue map; state may be out-of-sync with server"; + //FIXME: Do we want to disconnect and reconnect at this point to try to get sending working and clear the server state? + } + } +} + + diff --git a/src/accounts/hatchet/sip/HatchetSip.h b/src/accounts/hatchet/sip/HatchetSip.h new file mode 100644 index 000000000..6317ec7c0 --- /dev/null +++ b/src/accounts/hatchet/sip/HatchetSip.h @@ -0,0 +1,95 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2012, Jeff Mitchell + * + * Tomahawk 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 3 of the License, or + * (at your option) any later version. + * + * Tomahawk 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 Tomahawk. If not, see . + */ + +#ifndef TOMAHAWK_SIP_H +#define TOMAHAWK_SIP_H + +#include "accounts/AccountDllMacro.h" +#include "database/Op.h" +#include "sip/SipPlugin.h" +#include "account/HatchetAccount.h" + +#include +#include + +class WebSocketThreadController; + +const int VERSION = 1; + +class ACCOUNTDLLEXPORT HatchetSipPlugin : public SipPlugin +{ + Q_OBJECT + + enum SipState { + AcquiringVersion, + Registering, + Connected, + Closed + }; + +public: + HatchetSipPlugin( Tomahawk::Accounts::Account *account ); + + virtual ~HatchetSipPlugin(); + + virtual bool isValid() const; + + virtual void sendSipInfo( const Tomahawk::peerinfo_ptr& receiver, const SipInfo& info ); + +public slots: + virtual void connectPlugin(); + void disconnectPlugin(); + void checkSettings() {} + void configurationChanged() {} + void addContact( const QString &, const QString& ) {} + void sendMsg( const QString&, const SipInfo& ) {} + void webSocketConnected(); + void webSocketDisconnected(); + +signals: + void connectWebSocket() const; + void disconnectWebSocket() const; + void authUrlDiscovered( Tomahawk::Accounts::HatchetAccount::Service service, const QString& authUrl ) const; + void rawBytes( QByteArray bytes ) const; + +private slots: + void dbSyncTriggered(); + void messageReceived( const QByteArray& msg ); + void connectWebSocket(); + void onWsFailed( const QString &msg ); + void onWsClosed( const QString &msg ); + void oplogFetched( const QString& sinceguid, const QString& lastguid, const QList< dbop_ptr > ops ) const; + +private: + bool sendBytes( const QVariantMap& jsonMap ) const; + bool checkKeys( QStringList keys, const QVariantMap& map ) const; + void newPeer( const QVariantMap& valMap ); + void peerAuthorization( const QVariantMap& valMap ); + void sendOplog( const QVariantMap& valMap ) const; + Tomahawk::Accounts::HatchetAccount* hatchetAccount() const; + + QPointer< WebSocketThreadController > m_webSocketThreadController; + QString m_token; + QString m_userid; + QString m_uuid; + SipState m_sipState; + int m_version; + QCA::PublicKey* m_publicKey; +}; + +#endif diff --git a/src/accounts/hatchet/sip/WebSocket.cpp b/src/accounts/hatchet/sip/WebSocket.cpp new file mode 100644 index 000000000..47223b491 --- /dev/null +++ b/src/accounts/hatchet/sip/WebSocket.cpp @@ -0,0 +1,291 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2012, Leo Franchi + * + * Tomahawk 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 3 of the License, or + * (at your option) any later version. + * + * Tomahawk 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 Tomahawk. If not, see . + */ +#include "WebSocket.h" + +#include "utils/Logger.h" + +#include + +#include + +typedef typename websocketpp::lib::error_code error_code; + +WebSocket::WebSocket( const QString& url ) + : QObject( nullptr ) + , m_url( url ) + , m_outputStream() + , m_lastSocketState( QAbstractSocket::UnconnectedState ) +{ + tLog() << Q_FUNC_INFO << "WebSocket constructing"; + m_client = std::unique_ptr< hatchet_client >( new hatchet_client() ); + m_client->set_message_handler( std::bind(&onMessage, this, std::placeholders::_1, std::placeholders::_2 ) ); + m_client->register_ostream( &m_outputStream ); +} + + +WebSocket::~WebSocket() +{ + if ( m_connection ) + m_connection.reset(); + + if ( m_socket ) + { + if ( m_socket->state() == QAbstractSocket::ConnectedState ) + { + QObject::disconnect( m_socket, SIGNAL( stateChanged( QAbstractSocket::SocketState ) ) ); + m_socket->disconnectFromHost(); + QObject::connect( m_socket, SIGNAL( disconnected() ), m_socket, SLOT( deleteLater() ) ); + } + else + m_socket->deleteLater(); + } + + m_client.reset(); +} + + +void +WebSocket::setUrl( const QString &url ) +{ + tLog() << Q_FUNC_INFO << "Setting url to" << url; + if ( m_url == url ) + return; + + if ( m_socket && m_socket->isEncrypted() ) + reconnectWs(); +} + + +void +WebSocket::connectWs() +{ + tLog() << Q_FUNC_INFO << "Connecting"; + if ( m_socket ) + { + if ( m_socket->isEncrypted() ) + return; + + if ( m_socket->state() == QAbstractSocket::ClosingState ) + QMetaObject::invokeMethod( this, "connectWs", Qt::QueuedConnection ); + + return; + } + + tLog() << Q_FUNC_INFO << "Establishing new connection"; + m_socket = QPointer< QSslSocket >( new QSslSocket( nullptr ) ); + m_socket->addCaCertificate( QSslCertificate::fromPath( ":/hatchet-account/startcomroot.pem").first() ); + QObject::connect( m_socket, SIGNAL( stateChanged( QAbstractSocket::SocketState ) ), SLOT( socketStateChanged( QAbstractSocket::SocketState ) ) ); + QObject::connect( m_socket, SIGNAL( sslErrors( const QList< QSslError >& ) ), SLOT( sslErrors( const QList< QSslError >& ) ) ); + QObject::connect( m_socket, SIGNAL( encrypted() ), SLOT( encrypted() ) ); + QObject::connect( m_socket, SIGNAL( readyRead() ), SLOT( socketReadyRead() ) ); + m_socket->connectToHostEncrypted( m_url.host(), m_url.port() ); +} + + +void +WebSocket::disconnectWs() +{ + tLog() << Q_FUNC_INFO << "Disconnecting"; + m_outputStream.seekg( std::ios_base::end ); + m_outputStream.seekp( std::ios_base::end ); + if ( m_connection ) + m_connection.reset(); + m_queuedMessagesToSend.empty(); + m_socket->disconnectFromHost(); +} + + +void +WebSocket::reconnectWs() +{ + tLog() << Q_FUNC_INFO << "Reconnecting"; + QMetaObject::invokeMethod( this, "disconnectWs", Qt::QueuedConnection ); + QMetaObject::invokeMethod( this, "connectWs", Qt::QueuedConnection ); +} + + +void +WebSocket::socketStateChanged( QAbstractSocket::SocketState state ) +{ + tLog() << Q_FUNC_INFO << "Socket state changed to" << state; + switch ( state ) + { + case QAbstractSocket::ClosingState: + if ( m_lastSocketState == QAbstractSocket::ClosingState ) + { + // It seems like it does not actually properly close, so force it + tLog() << Q_FUNC_INFO << "Got a double closing state, cleaning up and emitting disconnected"; + m_socket->deleteLater(); + m_lastSocketState = QAbstractSocket::UnconnectedState; + emit disconnected(); + return; + } + break; + case QAbstractSocket::UnconnectedState: + if ( m_lastSocketState == QAbstractSocket::UnconnectedState ) + return; + tLog() << Q_FUNC_INFO << "Socket now unconnected, cleaning up and emitting disconnected"; + m_socket->deleteLater(); + m_lastSocketState = QAbstractSocket::UnconnectedState; + emit disconnected(); + return; + default: + ; + } + m_lastSocketState = state; +} + + +void +WebSocket::sslErrors( const QList< QSslError >& errors ) +{ + tLog() << Q_FUNC_INFO << "Encountered errors when trying to connect via SSL"; + foreach( QSslError error, errors ) + tLog() << Q_FUNC_INFO << "Error: " << error.errorString(); + QMetaObject::invokeMethod( this, "disconnectWs", Qt::QueuedConnection ); +} + + +void +WebSocket::encrypted() +{ + tLog() << Q_FUNC_INFO << "Encrypted connection to Hatchet established"; + error_code ec; + // Adjust wss:// to ws:// in the URL so it doesn't complain that the transport isn't encrypted + QString url = m_url.toString(); + if ( url.startsWith( "wss") ) + url.remove( 2, 1 ); + m_connection = m_client->get_connection( url.toStdString(), ec ); + if ( !m_connection ) + { + tLog() << Q_FUNC_INFO << "Got error creating WS connection, error is:" << QString::fromStdString( ec.message() ); + disconnectWs(); + return; + } + m_client->connect( m_connection ); + QMetaObject::invokeMethod( this, "readOutput", Qt::QueuedConnection ); + emit connected(); +} + + +void +WebSocket::readOutput() +{ + if ( !m_connection ) + return; + + tLog() << Q_FUNC_INFO; + + std::string outputString = m_outputStream.str(); + if ( outputString.size() > 0 ) + { + m_outputStream.str(""); + + tLog() << Q_FUNC_INFO << "Got string of size" << outputString.size() << "from ostream"; + qint64 sizeWritten = m_socket->write( outputString.data(), outputString.size() ); + tLog() << Q_FUNC_INFO << "Wrote" << sizeWritten << "bytes to the socket"; + if ( sizeWritten == -1 ) + { + tLog() << Q_FUNC_INFO << "Error during writing, closing connection"; + QMetaObject::invokeMethod( this, "disconnectWs", Qt::QueuedConnection ); + return; + } + } + + if ( m_queuedMessagesToSend.size() ) + { + if ( m_connection->get_state() == websocketpp::session::state::open ) + { + foreach( QByteArray message, m_queuedMessagesToSend ) + { + tLog() << Q_FUNC_INFO << "Sending queued message of size" << message.size(); + m_connection->send( std::string( message.constData(), message.size() ), websocketpp::frame::opcode::TEXT ); + } + + m_queuedMessagesToSend.clear(); + QMetaObject::invokeMethod( this, "readOutput", Qt::QueuedConnection ); + } + else + QTimer::singleShot( 200, this, SLOT( readOutput() ) ); + } +} + +void +WebSocket::socketReadyRead() +{ + tLog() << Q_FUNC_INFO; + + if ( !m_socket || !m_socket->isEncrypted() ) + return; + + if ( !m_socket->isValid() ) + { + tLog() << Q_FUNC_INFO << "Socket appears to no longer be valid. Something is wrong; disconnecting"; + QMetaObject::invokeMethod( this, "disconnectWs", Qt::QueuedConnection ); + return; + } + + if ( qint64 bytes = m_socket->bytesAvailable() ) + { + tLog() << Q_FUNC_INFO << "Bytes available:" << bytes; + QByteArray buf; + buf.resize( bytes ); + qint64 bytesRead = m_socket->read( buf.data(), bytes ); + tLog() << Q_FUNC_INFO << "Bytes read:" << bytesRead; // << ", content is" << websocketpp::utility::to_hex( buf.constData(), bytesRead ).data(); + if ( bytesRead != bytes ) + { + tLog() << Q_FUNC_INFO << "Error occurred during socket read. Something is wrong; disconnecting"; + QMetaObject::invokeMethod( this, "disconnectWs", Qt::QueuedConnection ); + return; + } + std::stringstream ss( std::string( buf.constData(), bytesRead ) ); + ss >> *m_connection; + } + + QMetaObject::invokeMethod( this, "readOutput", Qt::QueuedConnection ); +} + + +void +WebSocket::encodeMessage( const QByteArray &bytes ) +{ + tLog() << Q_FUNC_INFO << "Encoding message"; //, message is" << bytes.constData(); + if ( !m_connection ) + { + tLog() << Q_FUNC_INFO << "Asked to send message but do not have a valid connection!"; + return; + } + + if ( m_connection->get_state() != websocketpp::session::state::open ) + { + tLog() << Q_FUNC_INFO << "Connection not yet open/upgraded, queueing work to send"; + m_queuedMessagesToSend.append( bytes ); + } + else + m_connection->send( std::string( bytes.constData() ), websocketpp::frame::opcode::TEXT ); + + QMetaObject::invokeMethod( this, "readOutput", Qt::QueuedConnection ); +} + +void +onMessage( WebSocket* ws, websocketpp::connection_hdl, hatchet_client::message_ptr msg ) +{ + tLog() << Q_FUNC_INFO << "Handling message"; + std::string payload = msg->get_payload(); + ws->decodedMessage( QByteArray( payload.data(), payload.length() ) ); +} diff --git a/src/accounts/hatchet/sip/WebSocket.h b/src/accounts/hatchet/sip/WebSocket.h new file mode 100644 index 000000000..715802299 --- /dev/null +++ b/src/accounts/hatchet/sip/WebSocket.h @@ -0,0 +1,78 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2012, Leo Franchi + * + * Tomahawk 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 3 of the License, or + * (at your option) any later version. + * + * Tomahawk 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 Tomahawk. If not, see . + */ +#ifndef WEBSOCKET__H +#define WEBSOCKET__H + +#include "DllMacro.h" + +#include "hatchet_config.hpp" +#include + +#include +#include +#include + +#include + +typedef typename websocketpp::client< websocketpp::config::hatchet_client > hatchet_client; + +class WebSocket; + +void onMessage( WebSocket* ws, websocketpp::connection_hdl, hatchet_client::message_ptr msg ); + +class DLLEXPORT WebSocket : public QObject +{ + Q_OBJECT +public: + explicit WebSocket( const QString& url ); + virtual ~WebSocket(); + +signals: + void connected(); + void disconnected(); + void decodedMessage( QByteArray bytes ); + +public slots: + void setUrl( const QString& url ); + void connectWs(); + void disconnectWs(); + void encodeMessage( const QByteArray& bytes ); + +private slots: + void socketStateChanged( QAbstractSocket::SocketState state ); + void sslErrors( const QList< QSslError >& errors ); + void encrypted(); + void reconnectWs(); + void readOutput(); + void socketReadyRead(); + +private: + Q_DISABLE_COPY( WebSocket ) + + friend void onMessage( WebSocket *ws, websocketpp::connection_hdl, hatchet_client::message_ptr msg ); + + QUrl m_url; + std::stringstream m_outputStream; + std::unique_ptr< hatchet_client > m_client; + hatchet_client::connection_ptr m_connection; + QPointer< QSslSocket > m_socket; + QAbstractSocket::SocketState m_lastSocketState; + QList< QByteArray > m_queuedMessagesToSend; +}; + +#endif diff --git a/src/accounts/hatchet/sip/WebSocketThreadController.cpp b/src/accounts/hatchet/sip/WebSocketThreadController.cpp new file mode 100644 index 000000000..a0448d7a2 --- /dev/null +++ b/src/accounts/hatchet/sip/WebSocketThreadController.cpp @@ -0,0 +1,70 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2012, Leo Franchi + * + * Tomahawk 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 3 of the License, or + * (at your option) any later version. + * + * Tomahawk 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 Tomahawk. If not, see . + */ +#include "WebSocketThreadController.h" +#include "WebSocket.h" + +#include "utils/Logger.h" + +WebSocketThreadController::WebSocketThreadController( QObject* sip ) + : QThread( nullptr ) + , m_sip( sip ) +{ +} + + +WebSocketThreadController::~WebSocketThreadController() +{ + if ( m_webSocket ) + { + delete m_webSocket; + m_webSocket = 0; + } +} + + +void +WebSocketThreadController::setUrl( const QString &url ) +{ + m_url = url; + if ( m_webSocket ) + { + QMetaObject::invokeMethod( m_webSocket, "setUrl", Qt::QueuedConnection, Q_ARG( QString, url )); + } +} + + +void +WebSocketThreadController::run() +{ + tLog() << Q_FUNC_INFO << "Starting"; + m_webSocket = QPointer< WebSocket >( new WebSocket( m_url ) ); + if ( m_webSocket && m_sip ) + { + tLog() << Q_FUNC_INFO << "Have a valid websocket and parent"; + connect( m_sip, SIGNAL( connectWebSocket() ), m_webSocket, SLOT( connectWs() ), Qt::QueuedConnection ); + connect( m_sip, SIGNAL( disconnectWebSocket() ), m_webSocket, SLOT( disconnectWs() ), Qt::QueuedConnection ); + connect( m_sip, SIGNAL( rawBytes( QByteArray ) ), m_webSocket, SLOT( encodeMessage( QByteArray ) ), Qt::QueuedConnection ); + connect( m_webSocket, SIGNAL( connected() ), m_sip, SLOT( webSocketConnected() ), Qt::QueuedConnection ); + connect( m_webSocket, SIGNAL( disconnected() ), m_sip, SLOT( webSocketDisconnected() ), Qt::QueuedConnection ); + connect( m_webSocket, SIGNAL( decodedMessage( QByteArray ) ), m_sip, SLOT( messageReceived( QByteArray ) ), Qt::QueuedConnection ); + QMetaObject::invokeMethod( m_webSocket, "connectWs", Qt::QueuedConnection ); + exec(); + delete m_webSocket; + m_webSocket = 0; + } +} diff --git a/src/accounts/hatchet/sip/WebSocketThreadController.h b/src/accounts/hatchet/sip/WebSocketThreadController.h new file mode 100644 index 000000000..a7511a4b3 --- /dev/null +++ b/src/accounts/hatchet/sip/WebSocketThreadController.h @@ -0,0 +1,49 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2012, Leo Franchi + * + * Tomahawk 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 3 of the License, or + * (at your option) any later version. + * + * Tomahawk 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 Tomahawk. If not, see . + */ +#ifndef WEBSOCKET_THREAD_CONTROLLER_H +#define WEBSOCKET_THREAD_CONTROLLER_H + +#include "DllMacro.h" + +#include +#include + +class WebSocket; + +class DLLEXPORT WebSocketThreadController : public QThread +{ + Q_OBJECT + +public: + explicit WebSocketThreadController( QObject* sip ); + virtual ~WebSocketThreadController(); + + void setUrl( const QString &url ); + +protected: + void run(); + +private: + Q_DISABLE_COPY( WebSocketThreadController ) + + QPointer< WebSocket > m_webSocket; + QPointer< QObject > m_sip; + QString m_url; +}; + +#endif diff --git a/src/accounts/hatchet/sip/hatchet_config.hpp b/src/accounts/hatchet/sip/hatchet_config.hpp new file mode 100644 index 000000000..8a33295bc --- /dev/null +++ b/src/accounts/hatchet/sip/hatchet_config.hpp @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2013, Peter Thorson. All rights reserved. + * Copyright (c) 2013, Jeff Mitchell . 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 WebSocket++ Project 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 PETER THORSON 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 WEBSOCKETPP_CONFIG_HATCHET_CLIENT_HPP +#define WEBSOCKETPP_CONFIG_HATCHET_CLIENT_HPP + +#include +#include +#include + +namespace websocketpp { +namespace config { + +struct hatchet_client : public core_client { + typedef hatchet_client type; + + typedef websocketpp::concurrency::none concurrency_type; + + typedef core_client::request_type request_type; + typedef core_client::response_type response_type; + + typedef core_client::message_type message_type; + typedef core_client::con_msg_manager_type con_msg_manager_type; + typedef core_client::endpoint_msg_manager_type endpoint_msg_manager_type; + + typedef core_client::alog_type alog_type; + typedef core_client::elog_type elog_type; + + typedef core_client::rng_type rng_type; + + struct transport_config { + typedef type::concurrency_type concurrency_type; + typedef type::alog_type alog_type; + typedef type::elog_type elog_type; + typedef type::request_type request_type; + typedef type::response_type response_type; + }; + + typedef websocketpp::transport::iostream::endpoint transport_type; + + //static const websocketpp::log::level alog_level = websocketpp::log::alevel::all; +}; + +} // namespace config +} // namespace websocketpp + +#endif // WEBSOCKETPP_CONFIG_HATCHET_CLIENT_HPP diff --git a/src/libtomahawk/utils/Closure.cpp b/src/libtomahawk/utils/Closure.cpp index daa1003b2..e77321d5d 100644 --- a/src/libtomahawk/utils/Closure.cpp +++ b/src/libtomahawk/utils/Closure.cpp @@ -54,7 +54,7 @@ Closure::Closure(QObject* sender, Closure::Closure(QObject* sender, const char* signal, - std::tr1::function callback) + function callback) : callback_(callback) { Connect(sender, signal); } diff --git a/src/libtomahawk/utils/Closure.h b/src/libtomahawk/utils/Closure.h index 7e1cdf53b..fa89257d5 100644 --- a/src/libtomahawk/utils/Closure.h +++ b/src/libtomahawk/utils/Closure.h @@ -21,7 +21,13 @@ #include "DllMacro.h" +#ifdef _WEBSOCKETPP_CPP11_STL_ +#include +using std::function; +#else #include +using std::tr1::function; +#endif #include #include @@ -64,7 +70,7 @@ class DLLEXPORT Closure : public QObject, boost::noncopyable { const ClosureArgumentWrapper* val3 = 0); Closure(QObject* sender, const char* signal, - std::tr1::function callback); + function callback); void setAutoDelete( bool autoDelete ) { autoDelete_ = autoDelete; } @@ -87,7 +93,7 @@ class DLLEXPORT Closure : public QObject, boost::noncopyable { void Connect(QObject* sender, const char* signal); QMetaMethod slot_; - std::tr1::function callback_; + function callback_; bool autoDelete_; QObject* outOfThreadReceiver_;