diff --git a/CMakeLists.txt b/CMakeLists.txt index 95a057a97..6646d8047 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/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..5884d1a07 --- /dev/null +++ b/src/accounts/hatchet/account/HatchetAccount.cpp @@ -0,0 +1,417 @@ +/* === 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 + +#include +#include + +using namespace Tomahawk; +using namespace Accounts; + +static QPixmap* s_icon = 0; +HatchetAccount* HatchetAccount::s_instance = 0; + +const QString c_loginServer("https://mandella.hatchet.is/v1"); +const QString c_accessTokenServer("https://mandella.hatchet.is/v1"); + +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 ) + , m_publicKey( nullptr ) +{ + s_instance = this; + + QFile pemFile( ":/hatchet-account/mandella.pem" ); + pemFile.open( QIODevice::ReadOnly ); + tDebug() << Q_FUNC_INFO << "certs/mandella.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 ); +} + + +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(); +} + + +uint +HatchetAccount::authTokenExpiration() const +{ + bool ok; + return credentials().value( "expiration" ).toUInt( &ok ); +} + + +void +HatchetAccount::loginWithPassword( const QString& username, const QString& password, const QString &otp ) +{ + if ( username.isEmpty() || password.isEmpty() || !m_publicKey ) + { + tLog() << "No tomahawk account username or pw or public key, not logging in"; + return; + } + + m_uuid = QUuid::createUuid().toString(); + QCA::SecureArray sa( m_uuid.toLatin1() ); + QCA::SecureArray result = m_publicKey->encrypt( sa, QCA::EME_PKCS1_OAEP ); + + QVariantMap params; + params[ "password" ] = password; + params[ "username" ] = username; + if ( !otp.isEmpty() ) + params[ "otp" ] = otp; + params[ "client" ] = "Tomahawk (" + QHostInfo::localHostName() + ")"; + params[ "nonce" ] = QString( result.toByteArray().toBase64() ); + + QJson::Serializer s; + const QByteArray msgJson = s.serialize( params ); + + QNetworkRequest req( QUrl( c_loginServer + "/auth/credentials") ); + req.setHeader( QNetworkRequest::ContentTypeHeader, "application/json; charset=utf-8" ); + QNetworkReply* reply = TomahawkUtils::nam()->post( req, msgJson ); + + NewClosure( reply, SIGNAL( finished() ), this, SLOT( onPasswordLoginFinished( QNetworkReply*, const QString& ) ), reply, username ); +} + + +void +HatchetAccount::fetchAccessTokens( const QString& type ) +{ + if ( username().isEmpty() || authToken().isEmpty() ) + { + tLog() << "No authToken, not logging in"; + return; + } + + if ( authTokenExpiration() < ( QDateTime::currentMSecsSinceEpoch() / 1000 ) ) + tLog() << "Auth token has expired, but may still be valid on the server"; + + tLog() << "Fetching access tokens"; + QNetworkRequest req( QUrl( c_accessTokenServer + "/tokens/" + type + "?authtoken=" + authToken() ) ); + + QNetworkReply* reply = TomahawkUtils::nam()->get( req ); + + connect( reply, SIGNAL( finished() ), this, SLOT( onFetchAccessTokensFinished() ) ); +} + + +void +HatchetAccount::onPasswordLoginFinished( QNetworkReply* reply, const QString& username ) +{ + Q_ASSERT( reply ); + bool ok; + const QVariantMap resp = parseReply( reply, ok ); + if ( !ok ) + { + tLog() << Q_FUNC_INFO << "Error getting parsed reply from auth server"; + emit authError( "An error occurred reading the reply from the server"); + deauthenticate(); + return; + } + + if ( !resp.value( "error" ).toString().isEmpty() ) + { + tLog() << Q_FUNC_INFO << "Auth server returned an error"; + emit authError( resp.value( "error" ).toString() ); + deauthenticate(); + return; + } + + const QString nonce = resp.value( "data" ).toMap().value( "nonce" ).toString(); + if ( nonce != m_uuid ) + { + tLog() << Q_FUNC_INFO << "Auth server nonce value does not match!"; + emit authError( "The nonce value was incorrect. YOUR ACCOUNT MAY BE COMPROMISED." ); + deauthenticate(); + return; + } + + const QByteArray authenticationToken = resp.value( "data" ).toMap().value( "token" ).toByteArray(); + uint expiration = resp.value( "data" ).toMap().value( "expiration" ).toUInt( &ok ); + + QVariantHash creds = credentials(); + creds[ "username" ] = username; + creds[ "authtoken" ] = authenticationToken; + creds[ "expiration" ] = expiration; + setCredentials( creds ); + syncConfig(); + + if ( !authenticationToken.isEmpty() ) + { + if ( sipPlugin() ) + sipPlugin()->connectPlugin(); + } +} + + +void +HatchetAccount::onFetchAccessTokensFinished() +{ + tLog() << Q_FUNC_INFO; + QNetworkReply* reply = qobject_cast< QNetworkReply* >( sender() ); + Q_ASSERT( reply ); + bool ok; + const QVariantMap resp = parseReply( reply, ok ); + if ( !ok || !resp.value( "error" ).toString().isEmpty() ) + { + tLog() << Q_FUNC_INFO << "Auth server returned an error"; + if ( ok ) + emit authError( resp.value( "error" ).toString() ); + deauthenticate(); + return; + } + + QVariantHash creds = credentials(); + QStringList tokenTypesFound; + + tDebug() << Q_FUNC_INFO << "resp: " << resp; + + foreach( QVariant tokenVariant, resp[ "data" ].toMap()[ "tokens" ].toList() ) + { + QVariantMap tokenMap = tokenVariant.toMap(); + QString tokenTypeName = tokenMap[ "type" ].toString() + "tokens"; + if ( !tokenTypesFound.contains( tokenTypeName ) ) + { + creds[ tokenTypeName ] = QVariantList(); + tokenTypesFound.append( tokenTypeName ); + } + creds[ tokenTypeName ] = creds[ tokenTypeName ].toList() << tokenMap; + } + + tDebug() << Q_FUNC_INFO << "Creds: " << creds; + + setCredentials( creds ); + syncConfig(); + + tLog() << Q_FUNC_INFO << "Access tokens fetched successfully"; + + 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; +} + + +QVariantMap +HatchetAccount::parseReply( QNetworkReply* reply, bool& okRet ) const +{ + QVariantMap resp; + + reply->deleteLater(); + + bool ok; + int statusCode = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt( &ok ); + if ( reply->error() != QNetworkReply::NoError && statusCode != 400 && statusCode != 500 ) + { + tLog() << Q_FUNC_INFO << "Network error in command:" << reply->error() << reply->errorString(); + okRet = false; + return resp; + } + + QJson::Parser p; + resp = p.parse( reply, &ok ).toMap(); + + if ( !ok ) + { + tLog() << Q_FUNC_INFO << "Error parsing JSON from server"; + okRet = false; + return resp; + } + + if ( !resp.value( "error", "" ).toString().isEmpty() ) + { + tLog() << "Error from tomahawk server response, or in parsing from json:" << resp.value( "error" ).toString() << resp; + } + + tDebug() << Q_FUNC_INFO << "Got keys" << resp.keys(); + tDebug() << Q_FUNC_INFO << "Got values" << 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..6a3537bda --- /dev/null +++ b/src/accounts/hatchet/account/HatchetAccount.h @@ -0,0 +1,132 @@ +/* === 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 HATCHET_ACCOUNT_H +#define HATCHET_ACCOUNT_H + + +#include +#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( const QString& type = "dreamcatcher" ); + + QString authUrlForService( const Service& service ) const; + +signals: + void authError( QString error ); + void deauthenticated(); + void accessTokensFetched(); + +private slots: + void onPasswordLoginFinished( QNetworkReply*, const QString& username ); + void onFetchAccessTokensFinished(); + void authUrlDiscovered( Tomahawk::Accounts::HatchetAccount::Service service, const QString& authUrl ); + +private: + QByteArray authToken() const; + uint authTokenExpiration() const; + + void loginWithPassword( const QString& username, const QString& password, const QString &otp ); + + 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; + + QCA::PublicKey* m_publicKey; + QString m_uuid; +}; + +} +} + + +#endif diff --git a/src/accounts/hatchet/account/HatchetAccountConfig.cpp b/src/accounts/hatchet/account/HatchetAccountConfig.cpp new file mode 100644 index 000000000..cecd2016a --- /dev/null +++ b/src/accounts/hatchet/account/HatchetAccountConfig.cpp @@ -0,0 +1,182 @@ +/* === 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" + +#include + +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->label->setPixmap( m_ui->label->pixmap()->scaled( QSize( 128, 127 ), Qt::KeepAspectRatio, Qt::SmoothTransformation ) ); + + m_ui->loginButton->setDefault( true ); + connect( m_ui->loginButton, SIGNAL( clicked( bool ) ), this, SLOT( login() ) ); + + connect( m_ui->usernameEdit, SIGNAL( textChanged( QString ) ), this, SLOT( fieldsChanged() ) ); + connect( m_ui->passwordEdit, SIGNAL( textChanged( QString ) ), this, SLOT( fieldsChanged() ) ); + connect( m_ui->otpEdit, SIGNAL( textChanged( QString ) ), this, SLOT( fieldsChanged() ) ); + + connect( m_account, SIGNAL( authError( QString ) ), this, SLOT( authError( 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::login() +{ + const ButtonAction action = static_cast< ButtonAction>( m_ui->loginButton->property( "action" ).toInt() ); + + if ( action == Login ) + { + // Log in mode + m_account->loginWithPassword( m_ui->usernameEdit->text(), m_ui->passwordEdit->text(), m_ui->otpEdit->text() ); + } + else if ( action == Logout ) + { + // TODO + m_ui->usernameEdit->clear(); + m_ui->passwordEdit->clear(); + m_ui->otpEdit->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 ButtonAction action = static_cast< ButtonAction>( m_ui->loginButton->property( "action" ).toInt() ); + + m_ui->loginButton->setEnabled( !username.isEmpty() && !password.isEmpty() && action == Login ); + + m_ui->errorLabel->clear(); + + if ( action == Login ) + m_ui->loginButton->setText( tr( "Login" ) ); +} + + +void +HatchetAccountConfig::showLoggedIn() +{ + m_ui->usernameLabel->hide(); + m_ui->usernameEdit->hide(); + m_ui->otpLabel->hide(); + m_ui->otpEdit->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->loginButton->setText( "Log out" ); + m_ui->loginButton->setProperty( "action", Logout ); + m_ui->loginButton->setDefault( true ); +} + + +void +HatchetAccountConfig::showLoggedOut() +{ + m_ui->usernameLabel->show(); + m_ui->usernameEdit->show(); + m_ui->passwordLabel->show(); + m_ui->passwordEdit->show(); + m_ui->otpEdit->show(); + m_ui->otpLabel->show(); + + m_ui->loggedInLabel->clear(); + m_ui->loggedInLabel->hide(); + + m_ui->errorLabel->clear(); + + m_ui->loginButton->setText( "Login" ); + m_ui->loginButton->setProperty( "action", Login ); + m_ui->loginButton->setDefault( true ); +} + + +void +HatchetAccountConfig::accountInfoUpdated() +{ + showLoggedIn(); + return; +} + + +void +HatchetAccountConfig::authError( const QString &error ) +{ + QMessageBox::critical( this, "An error was encountered logging in:", error ); +} + + +void +HatchetAccountConfig::showEvent( QShowEvent *event ) +{ + AccountConfigWidget::showEvent( event ); + m_ui->loginButton->setDefault( true ); +} diff --git a/src/accounts/hatchet/account/HatchetAccountConfig.h b/src/accounts/hatchet/account/HatchetAccountConfig.h new file mode 100644 index 000000000..2cc5e6692 --- /dev/null +++ b/src/accounts/hatchet/account/HatchetAccountConfig.h @@ -0,0 +1,69 @@ +/* === 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 HATCHET_ACCOUNT_CONFIG_H +#define HATCHET_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 login(); + + void fieldsChanged(); + + void showLoggedIn(); + void showLoggedOut(); + + void accountInfoUpdated(); + + void authError( const QString& error ); + +protected: + //virtual void changeEvent( QEvent* event ); + virtual void showEvent( QShowEvent* event ); + +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..76d33a4a9 --- /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 + + + + + + + + 10 + 75 + true + + + + + + + Qt::AlignCenter + + + + + + + + + One-time +Password + + + + + + + Username + + + + + + + Hatchet username + + + + + + + Password: + + + + + + + + + + QLineEdit::Password + + + Hatchet password + + + + + + + (Only if configured) + + + + + + + + + + + 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/mandella.pem b/src/accounts/hatchet/admin/certs/mandella.pem new file mode 100644 index 000000000..b654270c7 --- /dev/null +++ b/src/accounts/hatchet/admin/certs/mandella.pem @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwqJWocwqceJBfO1Bbfdr +ozqYW+3T5VlmpBILw1OREDuUjdCO8mYvgfFTq5/JTZo/grLmd+LqntU3Y3E/u5Ij +Y92hRbPVmsQGIBcbTXuaasFLwjF9HAHaDcTbD0hS1mg1ZeO8r+uVvGmtdGDjulo/ +lCqo8PP2SEu7Bx1w2XQLQSiCnKVZ5Zk25JQDiiDzsBmdTPhOaihU3kyo32qTOovs +xXv9d6J/Uziv/DcuoIDKS1Uvn1h91QTn2wDdW6NcPgjHq/BD0o7cHeuI6d7XXZm3 +XM2TGQLl489h3kT3j6Tn30K3vXobYQvRXH76xjvYuWHlFaT/eSyEx0W/dIoN1Ot/ +88SEaR0hlgBhL1e9sLbQUXKhHooeWApWUDVtioQGRklcZZV3eP3YUUkvUmhfls8R +dgcI68cfZp0JFtbosHtJdzOcDwncKwJCUOP0XIx/5xn4wrAUJqjq08IcRG5s6GUn +lw/n8Tb/paG2Ip5XhzNYX8fSklj2C6/5jZP2jTnAnFVy314U3vFEqLe/kQxIPWen +pPlTqLSBq1J0HomxSUuizjSF4u1oW7TchCO+zV1EjOD3FEsBpWgkG9NIyqOtdUVo +QH4c5HsflspmSAnM3zuap9NHPjFo2Q1tqoqqA2arTzO5zb1d0IQxe193dZ4zm4vj +fb5OBuKW3xwYpJbezb2rHH8CAwEAAQ== +-----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 000000000..fabab44dd Binary files /dev/null and b/src/accounts/hatchet/admin/icons/hatchet-icon-512x512.png differ diff --git a/src/accounts/hatchet/cmake/Modules/FindQCA2.cmake b/src/accounts/hatchet/cmake/Modules/FindQCA2.cmake new file mode 100644 index 000000000..888937196 --- /dev/null +++ b/src/accounts/hatchet/cmake/Modules/FindQCA2.cmake @@ -0,0 +1,37 @@ +# - Try to find QCA2 (Qt Cryptography Architecture 2) +# Once done this will define +# +# QCA2_FOUND - system has QCA2 +# QCA2_INCLUDE_DIR - the QCA2 include directory +# QCA2_LIBRARIES - the libraries needed to use QCA2 +# QCA2_DEFINITIONS - Compiler switches required for using QCA2 +# +# use pkg-config to get the directories and then use these values +# in the FIND_PATH() and FIND_LIBRARY() calls + +# Copyright (c) 2006, Michael Larouche, +# +# 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..43ff7e63d --- /dev/null +++ b/src/accounts/hatchet/resources.qrc @@ -0,0 +1,8 @@ + + + admin/icons/hatchet-icon-512x512.png + admin/certs/mandella.pem + 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..8a965a49b --- /dev/null +++ b/src/accounts/hatchet/sip/HatchetSip.cpp @@ -0,0 +1,605 @@ +/* === 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 + +HatchetSipPlugin::HatchetSipPlugin( Tomahawk::Accounts::Account *account ) + : SipPlugin( account ) + , m_sipState( Closed ) + , m_version( 0 ) + , m_publicKey( nullptr ) + , m_reconnectTimer( this ) +{ + 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 ); + tDebug() << 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 ); + + m_reconnectTimer.setInterval( 0 ); + m_reconnectTimer.setSingleShot( true ); + connect( &m_reconnectTimer, SIGNAL( timeout() ), SLOT( connectPlugin() ) ); +} + + +HatchetSipPlugin::~HatchetSipPlugin() +{ + if ( m_webSocketThreadController ) + { + m_webSocketThreadController->quit(); + m_webSocketThreadController->wait( 60000 ); + + delete m_webSocketThreadController.data(); + } + + m_sipState = Closed; + + hatchetAccount()->setConnectionState( Tomahawk::Accounts::Account::Disconnected ); +} + + +bool +HatchetSipPlugin::isValid() const +{ + return m_account->enabled() && m_account->isAuthenticated() && m_publicKey; +} + + +Tomahawk::Accounts::HatchetAccount* +HatchetSipPlugin::hatchetAccount() const +{ + return qobject_cast< Tomahawk::Accounts::HatchetAccount* >( m_account ); +} + + +void +HatchetSipPlugin::connectPlugin() +{ + tLog() << Q_FUNC_INFO; + if ( !m_account->isAuthenticated() ) + { + tLog() << Q_FUNC_INFO << "Account not authenticated, not continuing"; + //FIXME: Prompt user for password? + return; + } + + hatchetAccount()->setConnectionState( Tomahawk::Accounts::Account::Connecting ); + hatchetAccount()->fetchAccessTokens(); +} + + +void +HatchetSipPlugin::disconnectPlugin() +{ + tLog() << Q_FUNC_INFO; + if ( m_webSocketThreadController && m_webSocketThreadController->isRunning() ) + emit disconnectWebSocket(); + else + webSocketDisconnected(); +} + + +///////////////////////////// Connection methods //////////////////////////////////// + + +void +HatchetSipPlugin::connectWebSocket() +{ + tLog() << Q_FUNC_INFO; + if ( m_webSocketThreadController ) + { + tLog() << Q_FUNC_INFO << "Already have a thread running, bailing"; + return; + } + + m_webSocketThreadController = QPointer< WebSocketThreadController >( new WebSocketThreadController( this ) ); + + if ( !m_webSocketThreadController ) + { + tLog() << Q_FUNC_INFO << "Could not create a new thread, bailing"; + disconnectPlugin(); + return; + } + + if ( !isValid() ) + { + tLog() << Q_FUNC_INFO << "Invalid state, not continuing with connection"; + return; + } + + m_token.clear(); + + QVariantList tokensCreds = m_account->credentials()[ "dreamcatchertokens" ].toList(); + + //FIXME: Don't blindly pick the first one that matches? Most likely, cycle through if the first one fails + QVariantMap connectVals; + foreach ( QVariant credObj, tokensCreds ) + { + QVariantMap creds = credObj.toMap(); + if ( creds.contains( "type" ) && creds[ "type" ].toString() == "dreamcatcher" ) + { + connectVals = creds; + m_token = creds[ "token" ].toString(); + break; + } + } + + QString url; + if ( !connectVals.isEmpty() ) + { + QString port = connectVals[ "port" ].toString(); + if ( port == "443" ) + url = "wss://"; + else + url = "ws://"; + url += connectVals[ "host" ].toString() + ':' + connectVals[ "port" ].toString(); + } + + if ( url.isEmpty() || m_token.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 ); + 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 ); + + tDebug() << 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_reconnectTimer.stop(); + + if ( m_webSocketThreadController ) + { + m_webSocketThreadController->quit(); + m_webSocketThreadController->wait( 60000 ); + + delete m_webSocketThreadController.data(); + } + + m_sipState = Closed; + m_version = 0; + + hatchetAccount()->setConnectionState( Tomahawk::Accounts::Account::Disconnected ); + + if ( hatchetAccount()->enabled() ) + { + // Work on the assumption that we were disconnected because Dreamcatcher shut down + // Reconnect after a time; use reasonable backoff + random + int interval = m_reconnectTimer.interval() <= 25000 ? m_reconnectTimer.interval() + 5000 : 30000; + interval += QCA::Random::randomInt() % 30; + m_reconnectTimer.setInterval( interval ); + m_reconnectTimer.start(); + } +} + + +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; + } + + tDebug() << Q_FUNC_INFO << "Sending bytes of size" << bytes.size(); + emit rawBytes( bytes ); + return true; +} + + +void +HatchetSipPlugin::messageReceived( const QByteArray &msg ) +{ + tDebug() << 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[ "token" ] = m_token; + registerMap[ "dbid" ] = Database::instance()->impl()->dbid(); + registerMap[ "alias" ] = QHostInfo::localHostName(); + + QList< SipInfo > sipinfos = Servent::instance()->getLocalSipInfos( "default", "default" ); + QVariantList hostinfos; + foreach ( SipInfo sipinfo, sipinfos ) + { + QVariantMap hostinfo; + hostinfo[ "host" ] = sipinfo.host(); + hostinfo[ "port" ] = sipinfo.port(); + hostinfos << hostinfo; + } + + registerMap[ "hostinfo" ] = hostinfos; + + 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 ); + m_reconnectTimer.setInterval( 0 ); + 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 >() ) + { + tDebug() << 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 QVariantList hostinfo = valMap[ "hostinfo" ].toList(); + const QString dbid = valMap[ "dbid" ].toString(); + + tDebug() << Q_FUNC_INFO << "username:" << username << "dbid" << dbid; + + QStringList keys( QStringList() << "command" << "username" << "hostinfo" << "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", dbid ); + peerInfo->setData( data ); + + QList< SipInfo > sipInfos; + + foreach ( const QVariant listItem, hostinfo ) + { + if ( !listItem.canConvert< QVariantMap >() ) + continue; + + QVariantMap hostpair = listItem.toMap(); + + if ( !hostpair.contains( "host" ) || !hostpair.contains( "port" ) ) + continue; + + const QString host = hostpair[ "host" ].toString(); + unsigned int port = hostpair[ "port" ].toUInt(); + + if ( host.isEmpty() || port == 0 ) + continue; + + SipInfo sipInfo; + sipInfo.setNodeId( dbid ); + sipInfo.setHost( host ); + sipInfo.setPort( port ); + sipInfo.setVisible( true ); + sipInfos << sipInfo; + } + + m_sipInfoHash[ dbid ] = sipInfos; + + peerInfo->setStatus( Tomahawk::PeerInfo::Online ); +} + + +void +HatchetSipPlugin::sendSipInfos(const Tomahawk::peerinfo_ptr& receiver, const QList< SipInfo >& infos) +{ + if ( infos.size() == 0 ) + { + tLog() << Q_FUNC_INFO << "Got no sipinfo data (list size 0)"; + return; + } + + const QString dbid = receiver->data().toMap().value( "dbid" ).toString(); + tDebug() << Q_FUNC_INFO << "Send local info to " << receiver->friendlyName() << "(" << dbid << ") we are" << infos[ 0 ].nodeId() << "with offerkey " << infos[ 0 ].key(); + + QVariantMap sendMap; + sendMap[ "command" ] = "authorize-peer"; + sendMap[ "dbid" ] = dbid; + sendMap[ "offerkey" ] = infos[ 0 ].key(); + + if ( !sendBytes( sendMap ) ) + tLog() << Q_FUNC_INFO << "Failed sending message"; +} + + +void +HatchetSipPlugin::peerAuthorization( const QVariantMap& valMap ) +{ + tDebug() << Q_FUNC_INFO << "dbid:" << valMap[ "dbid" ].toString() << "offerkey" << valMap[ "offerkey" ].toString(); + + QStringList keys( QStringList() << "command" << "dbid" << "offerkey" ); + if ( !checkKeys( keys, valMap ) ) + return; + + QString dbid = valMap[ "dbid" ].toString(); + + Tomahawk::peerinfo_ptr peerInfo = Tomahawk::PeerInfo::get( this, dbid ); + if( peerInfo.isNull() ) + { + tLog() << Q_FUNC_INFO << "Received a peer-authorization for a peer we don't know about"; + return; + } + + QList< SipInfo > sipInfos = m_sipInfoHash[ dbid ]; + for (int i = 0; i < sipInfos.size(); i++) + sipInfos[i].setKey( valMap[ "offerkey" ].toString() ); + peerInfo->setSipInfos( sipInfos ); + m_sipInfoHash.remove( dbid ); +} + + +///////////////////////////// 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 +{ + tDebug() << 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 ) +{ + tDebug() << 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; + tDebug() << 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; + } + tDebug() << 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; reconnecting"; + disconnectPlugin(); + } + } +} + + diff --git a/src/accounts/hatchet/sip/HatchetSip.h b/src/accounts/hatchet/sip/HatchetSip.h new file mode 100644 index 000000000..7ade8c967 --- /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 HATCHET_SIP_H +#define HATCHET_SIP_H + +#include "accounts/AccountDllMacro.h" +#include "database/Op.h" +#include "sip/SipPlugin.h" +#include "account/HatchetAccount.h" + +#include +#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 sendSipInfos( const Tomahawk::peerinfo_ptr& receiver, const QList< SipInfo >& infos ); + +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 oplogFetched( const QString& sinceguid, const QString& lastguid, const QList< dbop_ptr > ops ); + +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_uuid; + SipState m_sipState; + int m_version; + QCA::PublicKey* m_publicKey; + QTimer m_reconnectTimer; + QHash< QString, QList< SipInfo > > m_sipInfoHash; +}; + +#endif diff --git a/src/accounts/hatchet/sip/WebSocket.cpp b/src/accounts/hatchet/sip/WebSocket.cpp new file mode 100644 index 000000000..bbbe70b63 --- /dev/null +++ b/src/accounts/hatchet/sip/WebSocket.cpp @@ -0,0 +1,324 @@ +/* === 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 + +typedef typename websocketpp::lib::error_code error_code; + +WebSocket::WebSocket( const QString& url ) + : QObject( nullptr ) + , m_disconnecting( false ) + , m_url( url ) + , m_outputStream() + , m_lastSocketState( QAbstractSocket::UnconnectedState ) + , m_connectionTimer( this ) +{ + 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->set_close_handler( std::bind(&onClose, this, std::placeholders::_1 ) ); + m_client->register_ostream( &m_outputStream ); + + m_connectionTimer.setSingleShot( true ); + m_connectionTimer.setInterval( 30000 ); + connect( &m_connectionTimer, SIGNAL( timeout() ), SLOT( disconnectWs() ) ); +} + + +WebSocket::~WebSocket() +{ + if ( m_connection ) + m_connection.reset(); + + m_client.reset(); + + if ( m_socket ) + delete m_socket.data(); +} + + +void +WebSocket::setUrl( const QString &url ) +{ + tLog() << Q_FUNC_INFO << "Setting url to" << url; + if ( m_url == url ) + return; + + // We'll let automatic reconnection handle things + if ( m_socket && m_socket->isEncrypted() ) + disconnectWs(); +} + + +void +WebSocket::connectWs() +{ + tLog() << Q_FUNC_INFO << "Connecting"; + m_disconnecting = false; + 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() ); + m_connectionTimer.start(); +} + + +void +WebSocket::disconnectWs( websocketpp::close::status::value status, const QString &reason ) +{ + tLog() << Q_FUNC_INFO << "Disconnecting"; + m_disconnecting = true; + + error_code ec; + if ( m_connection ) + { + m_connection->close( status, reason.toAscii().constData(), ec ); + QMetaObject::invokeMethod( this, "readOutput", Qt::QueuedConnection ); + QTimer::singleShot( 5000, this, SLOT( disconnectSocket() ) ); //safety + return; + } + + disconnectSocket(); +} + + +void +WebSocket::disconnectSocket() +{ + if ( m_socket && m_socket->state() == QAbstractSocket::ConnectedState ) + m_socket->disconnectFromHost(); + else + QMetaObject::invokeMethod( this, "cleanup", Qt::QueuedConnection ); + + QTimer::singleShot( 5000, this, SLOT( cleanup() ) ); //safety +} + + +void +WebSocket::cleanup() +{ + tLog() << Q_FUNC_INFO << "Cleaning up"; + m_outputStream.seekg( std::ios_base::end ); + m_outputStream.seekp( std::ios_base::end ); + m_queuedMessagesToSend.empty(); + if ( m_connection ) + m_connection.reset(); + + emit disconnected(); +} + + +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; + QMetaObject::invokeMethod( this, "cleanup", Qt::QueuedConnection ); + 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; + QMetaObject::invokeMethod( this, "cleanup", Qt::QueuedConnection ); + 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 Dreamcatcher established"; + error_code ec; + + QUrl url(m_url); + websocketpp::uri_ptr uri(new websocketpp::uri(false, url.host().toStdString(), url.port(), "/")); + + m_connection = m_client->get_connection( uri, ec ); + if ( !m_connection || ec.value() != 0 ) + { + 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; + + tDebug() << Q_FUNC_INFO; + + std::string outputString = m_outputStream.str(); + if ( outputString.size() > 0 ) + { + m_outputStream.str(""); + + tDebug() << Q_FUNC_INFO << "Got string of size" << outputString.size() << "from ostream"; + qint64 sizeWritten = m_socket->write( outputString.data(), outputString.size() ); + tDebug() << 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 ) + { + tDebug() << 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 ); + m_connectionTimer.stop(); + } + else if ( !m_disconnecting ) + QTimer::singleShot( 200, this, SLOT( readOutput() ) ); + } + else + m_connectionTimer.stop(); +} + +void +WebSocket::socketReadyRead() +{ + tDebug() << 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() ) + { + tDebug() << Q_FUNC_INFO << "Bytes available:" << bytes; + QByteArray buf; + buf.resize( bytes ); + qint64 bytesRead = m_socket->read( buf.data(), bytes ); + tDebug() << 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 ) +{ + tDebug() << 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 ); + m_connectionTimer.start(); + } + 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 ) +{ + tDebug() << Q_FUNC_INFO << "Handling message"; + std::string payload = msg->get_payload(); + ws->decodedMessage( QByteArray( payload.data(), payload.length() ) ); +} + +void +onClose( WebSocket *ws, websocketpp::connection_hdl ) +{ + tDebug() << Q_FUNC_INFO << "Handling message"; + QMetaObject::invokeMethod( ws, "disconnectSocket", Qt::QueuedConnection ); +} diff --git a/src/accounts/hatchet/sip/WebSocket.h b/src/accounts/hatchet/sip/WebSocket.h new file mode 100644 index 000000000..62b23c897 --- /dev/null +++ b/src/accounts/hatchet/sip/WebSocket.h @@ -0,0 +1,84 @@ +/* === 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 + +#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 ); +void onClose( WebSocket* ws, websocketpp::connection_hdl ); + +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( websocketpp::close::status::value status = websocketpp::close::status::normal, const QString& reason = QString( "Disconnecting" ) ); + void encodeMessage( const QByteArray& bytes ); + +private slots: + void socketStateChanged( QAbstractSocket::SocketState state ); + void sslErrors( const QList< QSslError >& errors ); + void disconnectSocket(); + void cleanup(); + void encrypted(); + void readOutput(); + void socketReadyRead(); + +private: + Q_DISABLE_COPY( WebSocket ) + + friend void onMessage( WebSocket *ws, websocketpp::connection_hdl, hatchet_client::message_ptr msg ); + friend void onClose( WebSocket *ws, websocketpp::connection_hdl ); + + bool m_disconnecting; + 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; + QTimer m_connectionTimer; +}; + +#endif diff --git a/src/accounts/hatchet/sip/WebSocketThreadController.cpp b/src/accounts/hatchet/sip/WebSocketThreadController.cpp new file mode 100644 index 000000000..86beed07a --- /dev/null +++ b/src/accounts/hatchet/sip/WebSocketThreadController.cpp @@ -0,0 +1,71 @@ +/* === 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_webSocket( 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/Source.cpp b/src/libtomahawk/Source.cpp index ab397a769..2a87199b1 100644 --- a/src/libtomahawk/Source.cpp +++ b/src/libtomahawk/Source.cpp @@ -70,8 +70,12 @@ Source::Source( int id, const QString& nodeId ) if ( m_isLocal ) { - connect( Accounts::AccountManager::instance(), SIGNAL( connected( Tomahawk::Accounts::Account* ) ), SLOT( setOnline() ) ); - connect( Accounts::AccountManager::instance(), SIGNAL( disconnected( Tomahawk::Accounts::Account* ) ), SLOT( setOffline() ) ); + connect( Accounts::AccountManager::instance(), + SIGNAL( connected( Tomahawk::Accounts::Account* ) ), + SLOT( setOnline() ) ); + connect( Accounts::AccountManager::instance(), + SIGNAL( disconnected( Tomahawk::Accounts::Account*, Tomahawk::Accounts::AccountManager::DisconnectReason ) ), + SLOT( handleDisconnect( Tomahawk::Accounts::Account*, Tomahawk::Accounts::AccountManager::DisconnectReason ) ) ); } } @@ -330,6 +334,13 @@ Source::removeCollection( const collection_ptr& c ) emit collectionRemoved( c ); } +void +Source::handleDisconnect( Tomahawk::Accounts::Account*, Tomahawk::Accounts::AccountManager::DisconnectReason reason ) +{ + if ( reason == Tomahawk::Accounts::AccountManager::Disabled ) + setOffline(); +} + void Source::setOffline() diff --git a/src/libtomahawk/Source.h b/src/libtomahawk/Source.h index aaa6faf2f..f5fa9ba74 100644 --- a/src/libtomahawk/Source.h +++ b/src/libtomahawk/Source.h @@ -26,6 +26,7 @@ #include #include "Typedefs.h" +#include "accounts/AccountManager.h" #include "network/ControlConnection.h" #include "network/DbSyncConnection.h" #include "collection/Collection.h" @@ -136,6 +137,7 @@ private slots: void dbLoaded( unsigned int id, const QString& fname ); void updateIndexWhenSynced(); + void handleDisconnect( Tomahawk::Accounts::Account*, Tomahawk::Accounts::AccountManager::DisconnectReason reason ); void setOffline(); void setOnline(); diff --git a/src/libtomahawk/accounts/AccountManager.cpp b/src/libtomahawk/accounts/AccountManager.cpp index fdc26c216..b72d1f413 100644 --- a/src/libtomahawk/accounts/AccountManager.cpp +++ b/src/libtomahawk/accounts/AccountManager.cpp @@ -512,7 +512,8 @@ AccountManager::onStateChanged( Account::ConnectionState state ) if ( account->connectionState() == Account::Disconnected ) { m_connectedAccounts.removeAll( account ); - emit disconnected( account ); + DisconnectReason reason = account->enabled() ? Disconnected : Disabled; + emit disconnected( account, reason ); } else if ( account->connectionState() == Account::Connected ) { diff --git a/src/libtomahawk/accounts/AccountManager.h b/src/libtomahawk/accounts/AccountManager.h index 99963dd45..420b73859 100644 --- a/src/libtomahawk/accounts/AccountManager.h +++ b/src/libtomahawk/accounts/AccountManager.h @@ -21,7 +21,8 @@ #ifndef ACCOUNTMANAGER_H #define ACCOUNTMANAGER_H -#include +#include +#include #include "Typedefs.h" #include "DllMacro.h" @@ -42,6 +43,11 @@ class DLLEXPORT AccountManager : public QObject Q_OBJECT public: + enum DisconnectReason { + Disconnected, + Disabled + }; + static AccountManager* instance(); explicit AccountManager( QObject *parent ); @@ -61,7 +67,7 @@ public: void hookupAndEnable( Account* account, bool startup = false ); /// Hook up signals and start the plugin void removeAccount( Account* account ); - QList< Account* > accounts() const { return m_accounts; }; + QList< Account* > accounts() const { return m_accounts; } QList< Account* > accounts( Tomahawk::Accounts::AccountType type ) const { return m_accountsByAccountType[ type ]; } QList< Account* > accountsFromFactory( Tomahawk::Accounts::AccountFactory* factory ) const; @@ -105,7 +111,7 @@ signals: void removed( Tomahawk::Accounts::Account* ); void connected( Tomahawk::Accounts::Account* ); - void disconnected( Tomahawk::Accounts::Account* ); + void disconnected( Tomahawk::Accounts::Account*, Tomahawk::Accounts::AccountManager::DisconnectReason ); void authError( Tomahawk::Accounts::Account* ); void stateChanged( Account* p, Accounts::Account::ConnectionState state ); 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_;