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