From 145f1efaf5c148de35d507fe3039396441a4594d Mon Sep 17 00:00:00 2001 From: John Maguire Date: Thu, 12 Jul 2012 14:09:20 +0200 Subject: [PATCH] Basic support for Google Drive & OAuth. --- data/data.qrc | 1 + data/providers/googledrive.png | Bin 0 -> 2148 bytes src/CMakeLists.txt | 4 + src/engines/gstenginepipeline.cpp | 15 +++ src/internet/googledriveservice.cpp | 75 ++++++++++++++ src/internet/googledriveservice.h | 35 +++++++ src/internet/internetmodel.cpp | 2 + src/internet/oauthenticator.cpp | 147 ++++++++++++++++++++++++++++ src/internet/oauthenticator.h | 38 +++++++ 9 files changed, 317 insertions(+) create mode 100644 data/providers/googledrive.png create mode 100644 src/internet/googledriveservice.cpp create mode 100644 src/internet/googledriveservice.h create mode 100644 src/internet/oauthenticator.cpp create mode 100644 src/internet/oauthenticator.h diff --git a/data/data.qrc b/data/data.qrc index 707ca59a3..06d0aa79d 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -284,6 +284,7 @@ providers/echonest.png providers/songkick.png providers/twitter.png + providers/googledrive.png lumberjacksong.txt schema/schema-18.sql star-off.png diff --git a/data/providers/googledrive.png b/data/providers/googledrive.png new file mode 100644 index 0000000000000000000000000000000000000000..590dde867f4bd46b2d836b9e5e8a8e2940b13ff0 GIT binary patch literal 2148 zcmV-q2%GnbP)Mwm4`kVS#<|WZ|&Ja z)7Sg^{(x6EwE!{Ccy2hdr(yVDgw5q`A5X6~gvQm&p}BteWj@L^9@9aOpn>K-nseX- zaB@{^fTG`^scsE^@yrHD6pEB_U=0@f=bgdA4eS|MZ+~9jy($in@im%8UBCsx$tzW8 zwkkp7A@@A%jLkxNKE7HRc!|bW0%ZLBWWCngg&RAdwZ@AKKP?(jPY!Q;Njx1 zFGLTmnhr5~LmjA2?p1}m#YJ9NMCpeMedTnMc#0l$_EYg-{opao9e_p37G;T;I z?wi`Hit03t8f&hA61_xFFMoCcI|`mMy3m%3e8y`i|}+ z*x*aRdXGR2m2?&wMyeh}>VRe&1*yw2f;VUT0x{c!l?e`xdowT#QECk_-AMHgXMuq3 zAQ%qA_M8lmon-0V1xcSVQBRT>lrxv+W4JKafw{|Fm^az+br3p2fAy=MY-8u4_Z9Kw@O5 zmJo^s^?VuD1b`xit$o{1wuFiJD7{e&BJ3&5r_{p7;nifJ8@FUPS;pRu9HWf2Y$np| z(Yb%U>NgkS zk=Ap^CYH03n*E@DfQ%khyBU%UOQgvL-O!Ip3<_kL0c2!XAn9JDLGxxT9~xA0;X`hC zj}zcE7zV%r=s&vyZaMd;(?EKWg66sag;F)3E(u-(-MjwVbUQ{x-9kw@AlKCJKG4&uqzrhw3vUjz$;uAD^a29}EgOX!)Er^(f8V1LceNIEu4Q zU{nH8?DLBst5gH!D#HCC@Sn5w<{%8bXI-zc(@R!@;{+%iftQy|w3SC9xIeH-7&D6gnHo_^J~nd>h3|2)l18IQQo?BVJe5 zLCGbBmlDnIUcY_kN_Xbcf?hwn4T{~{T;zial#(?*uV)fwn1)WmWJ40XFFAxbd^A4C zR=lxAP->Ru?>KSG%r4I~i$qa+y!vR`ArlWFYkU$yE0E`YjXXusZ(ein?dJbV+;rf7 a?!aG7`ph;%20c*#0000url().host().contains("googleusercontent.com") && + instance->url().hasFragment()) { + QByteArray authorization = QString("Bearer %1").arg( + instance->url().fragment()).toAscii(); + GstStructure* headers = gst_structure_new( + "extra-headers", + "Authorization", G_TYPE_STRING, + authorization.constData(), + NULL); + g_object_set(element, "extra-headers", headers, NULL); + gst_structure_free(headers); + } } void GstEnginePipeline::TransitionToNext() { diff --git a/src/internet/googledriveservice.cpp b/src/internet/googledriveservice.cpp new file mode 100644 index 000000000..15d845bca --- /dev/null +++ b/src/internet/googledriveservice.cpp @@ -0,0 +1,75 @@ +#include "googledriveservice.h" + +#include + +#include "core/closure.h" +#include "internetmodel.h" +#include "oauthenticator.h" + +namespace { + +const char* kGoogleDriveFiles = "https://www.googleapis.com/drive/v2/files"; + +} + +GoogleDriveService::GoogleDriveService(Application* app, InternetModel* parent) + : InternetService("Google Drive", app, parent, parent), + root_(NULL), + oauth_(new OAuthenticator(this)) { + connect(oauth_, SIGNAL(AccessTokenAvailable(QString)), SLOT(AccessTokenAvailable(QString))); +} + +QStandardItem* GoogleDriveService::CreateRootItem() { + root_ = new QStandardItem(QIcon(":providers/googledrive.png"), "Google Drive"); + root_->setData(true, InternetModel::Role_CanLazyLoad); + return root_; +} + +void GoogleDriveService::LazyPopulate(QStandardItem* item) { + switch (item->data(InternetModel::Role_Type).toInt()) { + case InternetModel::Type_Service: + Connect(); + break; + default: + break; + } +} + +void GoogleDriveService::Connect() { + oauth_->StartAuthorisation(); +} + +void GoogleDriveService::AccessTokenAvailable(const QString& token) { + access_token_ = token; + QUrl url = QUrl(kGoogleDriveFiles); + url.addQueryItem("q", "mimeType = 'audio/mpeg'"); + + QNetworkRequest request = QNetworkRequest(url); + request.setRawHeader( + "Authorization", QString("Bearer %1").arg(token).toUtf8()); + QNetworkReply* reply = network_.get(request); + NewClosure(reply, SIGNAL(finished()), this, SLOT(ListFilesFinished(QNetworkReply*)), reply); +} + +void GoogleDriveService::ListFilesFinished(QNetworkReply* reply) { + reply->deleteLater(); + + QJson::Parser parser; + bool ok = false; + QVariantMap result = parser.parse(reply, &ok).toMap(); + if (!ok) { + qLog(Error) << "Failed to request files from Google Drive"; + return; + } + + QVariantList items = result["items"].toList(); + foreach (const QVariant& v, items) { + QVariantMap file = v.toMap(); + Song song; + song.set_title(file["title"].toString()); + QString url = file["downloadUrl"].toString() + "#" + access_token_; + song.set_url(url); + song.set_filesize(file["fileSize"].toInt()); + root_->appendRow(CreateSongItem(song)); + } +} diff --git a/src/internet/googledriveservice.h b/src/internet/googledriveservice.h new file mode 100644 index 000000000..79ee1b68d --- /dev/null +++ b/src/internet/googledriveservice.h @@ -0,0 +1,35 @@ +#ifndef GOOGLEDRIVESERVICE_H +#define GOOGLEDRIVESERVICE_H + +#include "internetservice.h" + +#include "core/network.h" + +class QStandardItem; + +class OAuthenticator; + +class GoogleDriveService : public InternetService { + Q_OBJECT + public: + GoogleDriveService(Application* app, InternetModel* parent); + + QStandardItem* CreateRootItem(); + void LazyPopulate(QStandardItem* item); + + private slots: + void AccessTokenAvailable(const QString& token); + void ListFilesFinished(QNetworkReply* reply); + + private: + void Connect(); + + QStandardItem* root_; + OAuthenticator* oauth_; + + QString access_token_; + + NetworkAccessManager network_; +}; + +#endif diff --git a/src/internet/internetmodel.cpp b/src/internet/internetmodel.cpp index a12a91e8d..baf692033 100644 --- a/src/internet/internetmodel.cpp +++ b/src/internet/internetmodel.cpp @@ -17,6 +17,7 @@ #include "digitallyimportedservicebase.h" #include "icecastservice.h" +#include "googledriveservice.h" #include "jamendoservice.h" #include "magnatuneservice.h" #include "internetmimedata.h" @@ -64,6 +65,7 @@ InternetModel::InternetModel(Application* app, QObject* parent) #ifdef HAVE_LIBLASTFM AddService(new LastFMService(app, this)); #endif + AddService(new GoogleDriveService(app, this)); AddService(new GroovesharkService(app, this)); AddService(new MagnatuneService(app, this)); AddService(new PodcastService(app, this)); diff --git a/src/internet/oauthenticator.cpp b/src/internet/oauthenticator.cpp new file mode 100644 index 000000000..f3f297d6a --- /dev/null +++ b/src/internet/oauthenticator.cpp @@ -0,0 +1,147 @@ +#include "oauthenticator.h" + +#include +#include +#include +#include + +#include + +#include "core/closure.h" + +namespace { + +const char* kGoogleOAuthEndpoint = "https://accounts.google.com/o/oauth2/auth"; +const char* kGoogleOAuthTokenEndpoint = + "https://accounts.google.com/o/oauth2/token"; +const char* kGoogleDriveFiles = "https://www.googleapis.com/drive/v2/files"; + +const char* kClientId = "679260893280.apps.googleusercontent.com"; +const char* kClientSecret = "l3cWb8efUZsrBI4wmY3uKl6i"; + +} // namespace + +OAuthenticator::OAuthenticator(QObject* parent) + : QObject(parent) { +} + +void OAuthenticator::StartAuthorisation() { + server_.listen(QHostAddress::LocalHost); + const quint16 port = server_.serverPort(); + + NewClosure(&server_, SIGNAL(newConnection()), this, SLOT(NewConnection())); + + QUrl url = QUrl(kGoogleOAuthEndpoint); + url.addQueryItem("response_type", "code"); + url.addQueryItem("client_id", kClientId); + url.addQueryItem("redirect_uri", QString("http://localhost:%1").arg(port)); + url.addQueryItem("scope", "https://www.googleapis.com/auth/drive.readonly"); + + QDesktopServices::openUrl(url); +} + +void OAuthenticator::NewConnection() { + QTcpSocket* socket = server_.nextPendingConnection(); + server_.close(); + + QByteArray buffer; + + NewClosure(socket, SIGNAL(readyRead()), + this, SLOT(RedirectArrived(QTcpSocket*, QByteArray)), socket, buffer); + + // Everything is bon. + socket->write("HTTP/1.0 200 OK\r\n"); + socket->flush(); +} + +void OAuthenticator::RedirectArrived(QTcpSocket* socket, QByteArray buffer) { + buffer.append(socket->readAll()); + + if (socket->atEnd() || buffer.endsWith("\r\n\r\n")) { + socket->deleteLater(); + const QByteArray& code = ParseHttpRequest(buffer); + qLog(Debug) << "Code:" << code; + RequestAccessToken(code, socket->localPort()); + } else { + NewClosure(socket, SIGNAL(readyReady()), + this, SLOT(RedirectArrived(QTcpSocket*, QByteArray)), socket, buffer); + } +} + +QByteArray OAuthenticator::ParseHttpRequest(const QByteArray& request) const { + QList split = request.split('\r'); + const QByteArray& request_line = split[0]; + QByteArray path = request_line.split(' ')[1]; + QByteArray code = path.split('=')[1]; + + return code; +} + +void OAuthenticator::RequestAccessToken(const QByteArray& code, quint16 port) { + typedef QPair Param; + QList parameters; + parameters << Param("code", code) + << Param("client_id", kClientId) + << Param("client_secret", kClientSecret) + << Param("grant_type", "authorization_code") + // Even though we don't use this URI anymore, it must match the + // original one. + << Param("redirect_uri", QString("http://localhost:%1").arg(port)); + + QStringList params; + foreach (const Param& p, parameters) { + params.append(QString("%1=%2").arg(p.first, QString(QUrl::toPercentEncoding(p.second)))); + } + QString post_data = params.join("&"); + qLog(Debug) << post_data; + + QNetworkRequest request = QNetworkRequest(QUrl(kGoogleOAuthTokenEndpoint)); + request.setHeader(QNetworkRequest::ContentTypeHeader, + "application/x-www-form-urlencoded"); + + QNetworkReply* reply = network_.post(request, post_data.toUtf8()); + NewClosure(reply, SIGNAL(finished()), this, + SLOT(FetchAccessTokenFinished(QNetworkReply*)), reply); +} + +void OAuthenticator::FetchAccessTokenFinished(QNetworkReply* reply) { + reply->deleteLater(); + + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) != 200) { + qLog(Error) << "Failed to get access token" + << reply->readAll(); + return; + } + + QJson::Parser parser; + bool ok = false; + QVariantMap result = parser.parse(reply, &ok).toMap(); + if (!ok) { + qLog(Error) << "Failed to parse oauth reply"; + return; + } + + qLog(Debug) << result; + + access_token_ = result["access_token"].toString(); + refresh_token_ = result["refresh_token"].toString(); + + emit AccessTokenAvailable(access_token_); +} + +void OAuthenticator::ListFiles(const QString& access_token) { + QUrl url = QUrl(kGoogleDriveFiles); + url.addQueryItem("q", "mimeType = 'audio/mpeg'"); + QNetworkRequest request = QNetworkRequest(url); + request.setRawHeader( + "Authorization", QString("Bearer %1").arg(access_token).toUtf8()); + qLog(Debug) << "Header:" << request.rawHeader("Authorization"); + QNetworkReply* reply = network_.get(request); + NewClosure(reply, SIGNAL(finished()), this, SLOT(ListFilesResponse(QNetworkReply*)), reply); +} + +void OAuthenticator::ListFilesResponse(QNetworkReply* reply) { + reply->deleteLater(); + + qLog(Debug) << reply->readAll(); +} diff --git a/src/internet/oauthenticator.h b/src/internet/oauthenticator.h new file mode 100644 index 000000000..114197a80 --- /dev/null +++ b/src/internet/oauthenticator.h @@ -0,0 +1,38 @@ +#ifndef OAUTHENTICATOR_H +#define OAUTHENTICATOR_H + +#include +#include + +#include "core/network.h" + +class QTcpSocket; + +class OAuthenticator : public QObject { + Q_OBJECT + public: + explicit OAuthenticator(QObject* parent = 0); + void StartAuthorisation(); + + signals: + void AccessTokenAvailable(QString token); + + private slots: + void NewConnection(); + void RedirectArrived(QTcpSocket* socket, QByteArray buffer); + void FetchAccessTokenFinished(QNetworkReply* reply); + void ListFilesResponse(QNetworkReply* reply); + + private: + QByteArray ParseHttpRequest(const QByteArray& request) const; + void RequestAccessToken(const QByteArray& code, quint16 port); + void ListFiles(const QString& access_token); + + QTcpServer server_; + NetworkAccessManager network_; + + QString access_token_; + QString refresh_token_; +}; + +#endif