1 /* This file is part of Clementine.
2    Copyright 2012-2014, John Maguire <john.maguire@gmail.com>
3    Copyright 2012, 2014, David Sansome <me@davidsansome.com>
4    Copyright 2014, Arnaud Bienner <arnaud.bienner@gmail.com>
5    Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
6 
7    Clementine is free software: you can redistribute it and/or modify
8    it under the terms of the GNU General Public License as published by
9    the Free Software Foundation, either version 3 of the License, or
10    (at your option) any later version.
11 
12    Clementine is distributed in the hope that it will be useful,
13    but WITHOUT ANY WARRANTY; without even the implied warranty of
14    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15    GNU General Public License for more details.
16 
17    You should have received a copy of the GNU General Public License
18    along with Clementine.  If not, see <http://www.gnu.org/licenses/>.
19 */
20 
21 #include "internet/core/oauthenticator.h"
22 
23 #include <QDesktopServices>
24 #include <QSslError>
25 #include <QStringList>
26 #include <QUrl>
27 #include <QUrlQuery>
28 #include <QJsonParseError>
29 #include <QJsonDocument>
30 #include <QJsonObject>
31 
32 #include "core/closure.h"
33 #include "core/logging.h"
34 #include "internet/core/localredirectserver.h"
35 
36 const char* OAuthenticator::kRemoteURL =
37     "https://clementine-data.appspot.com/skydrive";
38 
OAuthenticator(const QString & client_id,const QString & client_secret,RedirectStyle redirect,QObject * parent)39 OAuthenticator::OAuthenticator(const QString& client_id,
40                                const QString& client_secret,
41                                RedirectStyle redirect, QObject* parent)
42     : QObject(parent),
43       client_id_(client_id),
44       client_secret_(client_secret),
45       redirect_style_(redirect) {}
46 
StartAuthorisation(const QString & oauth_endpoint,const QString & token_endpoint,const QString & scope)47 void OAuthenticator::StartAuthorisation(const QString& oauth_endpoint,
48                                         const QString& token_endpoint,
49                                         const QString& scope) {
50   token_endpoint_ = QUrl(token_endpoint);
51   LocalRedirectServer* server = new LocalRedirectServer(this);
52   server->Listen();
53 
54   QUrl url = QUrl(oauth_endpoint);
55   QUrlQuery url_query;
56   url_query.addQueryItem("response_type", "code");
57   url_query.addQueryItem("client_id", client_id_);
58   QUrl redirect_url;
59   QUrlQuery redirect_url_query;
60 
61   const QString port = QString::number(server->url().port());
62 
63   if (redirect_style_ == RedirectStyle::REMOTE) {
64     redirect_url = QUrl(kRemoteURL);
65     redirect_url_query.addQueryItem("port", port);
66   } else if (redirect_style_ == RedirectStyle::REMOTE_WITH_STATE) {
67     redirect_url = QUrl(kRemoteURL);
68     url_query.addQueryItem("state", port);
69   } else {
70     redirect_url = server->url();
71   }
72 
73   url_query.addQueryItem("redirect_uri", redirect_url.toString());
74   if (!scope.isEmpty()) {  // Empty scope is valid for Dropbox.
75     url_query.addQueryItem("scope", scope);
76   }
77 
78   url.setQuery(url_query);
79   redirect_url.setQuery(redirect_url_query);
80 
81   NewClosure(server, SIGNAL(Finished()), this, &OAuthenticator::RedirectArrived,
82              server, redirect_url);
83 
84   QDesktopServices::openUrl(url);
85 }
86 
RedirectArrived(LocalRedirectServer * server,QUrl url)87 void OAuthenticator::RedirectArrived(LocalRedirectServer* server, QUrl url) {
88   server->deleteLater();
89   QUrl request_url = server->request_url();
90   RequestAccessToken(QUrlQuery(request_url).queryItemValue("code").toUtf8(), url);
91 }
92 
ParseHttpRequest(const QByteArray & request) const93 QByteArray OAuthenticator::ParseHttpRequest(const QByteArray& request) const {
94   QList<QByteArray> split = request.split('\r');
95   const QByteArray& request_line = split[0];
96   QByteArray path = request_line.split(' ')[1];
97   QByteArray code = path.split('=')[1];
98 
99   return code;
100 }
101 
RequestAccessToken(const QByteArray & code,const QUrl & url)102 void OAuthenticator::RequestAccessToken(const QByteArray& code,
103                                         const QUrl& url) {
104   typedef QPair<QString, QString> Param;
105   QList<Param> parameters;
106   parameters << Param("code", code) << Param("client_id", client_id_)
107              << Param("client_secret", client_secret_)
108              << Param("grant_type", "authorization_code")
109              // Even though we don't use this URI anymore, it must match the
110              // original one.
111              << Param("redirect_uri", url.toString());
112 
113   QStringList params;
114   for (const Param& p : parameters) {
115     params.append(QString("%1=%2").arg(
116         p.first, QString(QUrl::toPercentEncoding(p.second))));
117   }
118   QString post_data = params.join("&");
119   qLog(Debug) << post_data;
120 
121   QNetworkRequest request = QNetworkRequest(QUrl(token_endpoint_));
122   request.setHeader(QNetworkRequest::ContentTypeHeader,
123                     "application/x-www-form-urlencoded");
124 
125   QNetworkReply* reply = network_.post(request, post_data.toUtf8());
126   connect(reply, SIGNAL(sslErrors(QList<QSslError>)),
127           SLOT(SslErrors(QList<QSslError>)));
128   NewClosure(reply, SIGNAL(finished()), this,
129              SLOT(FetchAccessTokenFinished(QNetworkReply*)), reply);
130 }
131 
FetchAccessTokenFinished(QNetworkReply * reply)132 void OAuthenticator::FetchAccessTokenFinished(QNetworkReply* reply) {
133   reply->deleteLater();
134 
135   if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) != 200) {
136     qLog(Error) << "Failed to get access token" << reply->readAll();
137     return;
138   }
139 
140   QJsonParseError error;
141   QJsonDocument json_document = QJsonDocument::fromJson(reply->readAll(), &error);
142 
143   if (error.error != QJsonParseError::NoError) {
144     qLog(Error) << "Failed to parse oauth reply";
145     return;
146   }
147 
148   QJsonObject result = json_document.object();
149   access_token_ = result["access_token"].toString();
150   refresh_token_ = result["refresh_token"].toString();
151   SetExpiryTime(result["expires_in"].toInt());
152 
153   emit Finished();
154 }
155 
RefreshAuthorisation(const QString & token_endpoint,const QString & refresh_token)156 void OAuthenticator::RefreshAuthorisation(const QString& token_endpoint,
157                                           const QString& refresh_token) {
158   refresh_token_ = refresh_token;
159 
160   QUrl url(token_endpoint);
161 
162   typedef QPair<QString, QString> Param;
163   QList<Param> parameters;
164   parameters << Param("client_id", client_id_)
165              << Param("client_secret", client_secret_)
166              << Param("grant_type", "refresh_token")
167              << Param("refresh_token", refresh_token);
168   QStringList params;
169   for (const Param& p : parameters) {
170     params.append(QString("%1=%2").arg(
171         p.first, QString(QUrl::toPercentEncoding(p.second))));
172   }
173   QString post_data = params.join("&");
174 
175   QNetworkRequest request(url);
176   request.setHeader(QNetworkRequest::ContentTypeHeader,
177                     "application/x-www-form-urlencoded");
178   QNetworkReply* reply = network_.post(request, post_data.toUtf8());
179   NewClosure(reply, SIGNAL(finished()), this,
180              SLOT(RefreshAccessTokenFinished(QNetworkReply*)), reply);
181 }
182 
SetExpiryTime(int expires_in_seconds)183 void OAuthenticator::SetExpiryTime(int expires_in_seconds) {
184   // Set the expiry time with two minutes' grace.
185   expiry_time_ = QDateTime::currentDateTime().addSecs(expires_in_seconds - 120);
186   qLog(Debug) << "Current oauth access token expires at:" << expiry_time_;
187 }
188 
RefreshAccessTokenFinished(QNetworkReply * reply)189 void OAuthenticator::RefreshAccessTokenFinished(QNetworkReply* reply) {
190   reply->deleteLater();
191 
192   QJsonObject json_result = QJsonDocument::fromJson(reply->readAll()).object();
193 
194   access_token_ = json_result["access_token"].toString();
195   if (json_result.contains("refresh_token")) {
196     refresh_token_ = json_result["refresh_token"].toString();
197   }
198   SetExpiryTime(json_result["expires_in"].toInt());
199   emit Finished();
200 }
201 
SslErrors(const QList<QSslError> & errors)202 void OAuthenticator::SslErrors(const QList<QSslError>& errors) {
203   for (const QSslError& error : errors) {
204     qLog(Debug) << error.errorString();
205   }
206 }
207