1 /*
2  * Strawberry Music Player
3  * Copyright 2019-2021, Jonas Kvinge <jonas@jkvinge.net>
4  *
5  * Strawberry is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * Strawberry is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with Strawberry.  If not, see <http://www.gnu.org/licenses/>.
17  *
18  */
19 
20 #include "config.h"
21 
22 #include <memory>
23 
24 #include <QtGlobal>
25 #include <QObject>
26 #include <QByteArray>
27 #include <QPair>
28 #include <QList>
29 #include <QMap>
30 #include <QString>
31 #include <QVariant>
32 #include <QUrl>
33 #include <QUrlQuery>
34 #include <QNetworkAccessManager>
35 #include <QNetworkRequest>
36 #include <QNetworkReply>
37 #include <QSslConfiguration>
38 #include <QSslSocket>
39 #include <QSslError>
40 #include <QCryptographicHash>
41 #include <QJsonValue>
42 #include <QJsonDocument>
43 #include <QJsonObject>
44 #include <QSettings>
45 #include <QSortFilterProxyModel>
46 #include <QtDebug>
47 
48 #include "core/utilities.h"
49 #include "core/application.h"
50 #include "core/player.h"
51 #include "core/logging.h"
52 #include "core/networkaccessmanager.h"
53 #include "core/database.h"
54 #include "core/song.h"
55 #include "collection/collectionbackend.h"
56 #include "collection/collectionmodel.h"
57 #include "subsonicservice.h"
58 #include "subsonicurlhandler.h"
59 #include "subsonicrequest.h"
60 #include "subsonicscrobblerequest.h"
61 #include "settings/settingsdialog.h"
62 #include "settings/subsonicsettingspage.h"
63 
64 const Song::Source SubsonicService::kSource = Song::Source_Subsonic;
65 const char *SubsonicService::kClientName = "Strawberry";
66 const char *SubsonicService::kApiVersion = "1.11.0";
67 const char *SubsonicService::kSongsTable = "subsonic_songs";
68 const char *SubsonicService::kSongsFtsTable = "subsonic_songs_fts";
69 const int SubsonicService::kMaxRedirects = 3;
70 
SubsonicService(Application * app,QObject * parent)71 SubsonicService::SubsonicService(Application *app, QObject *parent)
72     : InternetService(Song::Source_Subsonic, "Subsonic", "subsonic", SubsonicSettingsPage::kSettingsGroup, SettingsDialog::Page_Subsonic, app, parent),
73       app_(app),
74       url_handler_(new SubsonicUrlHandler(app, this)),
75       collection_backend_(nullptr),
76       collection_model_(nullptr),
77       collection_sort_model_(new QSortFilterProxyModel(this)),
78       http2_(false),
79       verify_certificate_(false),
80       download_album_covers_(true),
81       auth_method_(SubsonicSettingsPage::AuthMethod_MD5),
82       ping_redirects_(0) {
83 
84   app->player()->RegisterUrlHandler(url_handler_);
85 
86   // Backend
87 
88   collection_backend_ = new CollectionBackend();
89   collection_backend_->moveToThread(app_->database()->thread());
90   collection_backend_->Init(app_->database(), app->task_manager(), Song::Source_Subsonic, kSongsTable, kSongsFtsTable);
91 
92   // Model
93 
94   collection_model_ = new CollectionModel(collection_backend_, app_, this);
95   collection_sort_model_->setSourceModel(collection_model_);
96   collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
97   collection_sort_model_->setDynamicSortFilter(true);
98   collection_sort_model_->setSortLocaleAware(true);
99   collection_sort_model_->sort(0);
100 
101   SubsonicService::ReloadSettings();
102 
103 }
104 
~SubsonicService()105 SubsonicService::~SubsonicService() {
106 
107   while (!replies_.isEmpty()) {
108     QNetworkReply *reply = replies_.takeFirst();
109     QObject::disconnect(reply, nullptr, this, nullptr);
110     if (reply->isRunning()) reply->abort();
111     reply->deleteLater();
112   }
113 
114   collection_backend_->deleteLater();
115 
116 }
117 
Exit()118 void SubsonicService::Exit() {
119 
120   QObject::connect(collection_backend_, &CollectionBackend::ExitFinished, this, &SubsonicService::ExitFinished);
121   collection_backend_->ExitAsync();
122 
123 }
124 
ShowConfig()125 void SubsonicService::ShowConfig() {
126   app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Subsonic);
127 }
128 
ReloadSettings()129 void SubsonicService::ReloadSettings() {
130 
131   QSettings s;
132   s.beginGroup(SubsonicSettingsPage::kSettingsGroup);
133 
134   server_url_ = s.value("url").toUrl();
135   username_ = s.value("username").toString();
136   QByteArray password = s.value("password").toByteArray();
137   if (password.isEmpty()) password_.clear();
138   else password_ = QString::fromUtf8(QByteArray::fromBase64(password));
139 
140   http2_ = s.value("http2", false).toBool();
141   verify_certificate_ = s.value("verifycertificate", false).toBool();
142   download_album_covers_ = s.value("downloadalbumcovers", true).toBool();
143   auth_method_ = static_cast<SubsonicSettingsPage::AuthMethod>(s.value("authmethod", SubsonicSettingsPage::AuthMethod_MD5).toInt());
144 
145   s.endGroup();
146 
147 }
148 
SendPing()149 void SubsonicService::SendPing() {
150   SendPingWithCredentials(server_url_, username_, password_, auth_method_, false);
151 }
152 
SendPingWithCredentials(QUrl url,const QString & username,const QString & password,const SubsonicSettingsPage::AuthMethod auth_method,const bool redirect)153 void SubsonicService::SendPingWithCredentials(QUrl url, const QString &username, const QString &password, const SubsonicSettingsPage::AuthMethod auth_method, const bool redirect) {
154 
155   if (!network_ || !redirect) {
156     network_ = std::make_unique<QNetworkAccessManager>();
157 #if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0))
158     network_->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
159 #endif
160     ping_redirects_ = 0;
161   }
162 
163   ParamList params = ParamList() << Param("c", kClientName)
164                                  << Param("v", kApiVersion)
165                                  << Param("f", "json")
166                                  << Param("u", username);
167 
168   if (auth_method == SubsonicSettingsPage::AuthMethod_Hex) {
169     params << Param("p", QString("enc:" + password.toUtf8().toHex()));
170   }
171   else {
172     const QString salt = Utilities::CryptographicRandomString(20);
173     QCryptographicHash md5(QCryptographicHash::Md5);
174     md5.addData(password_.toUtf8());
175     md5.addData(salt.toUtf8());
176     params << Param("s", salt);
177     params << Param("t", md5.result().toHex());
178   }
179 
180   QUrlQuery url_query(url.query());
181   for (const Param &param : params) {
182     url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
183   }
184 
185   if (!redirect) {
186     if (!url.path().isEmpty() && url.path().right(1) == "/") {
187       url.setPath(url.path() + QString("rest/ping.view"));
188     }
189     else {
190       url.setPath(url.path() + QString("/rest/ping.view"));
191     }
192   }
193 
194   url.setQuery(url_query);
195 
196   QNetworkRequest req(url);
197 
198   if (url.scheme() == "https" && !verify_certificate_) {
199     QSslConfiguration sslconfig = QSslConfiguration::defaultConfiguration();
200     sslconfig.setPeerVerifyMode(QSslSocket::VerifyNone);
201     req.setSslConfiguration(sslconfig);
202   }
203 
204 #if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
205   req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
206 #else
207   req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
208 #endif
209 
210   req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
211 
212 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
213   req.setAttribute(QNetworkRequest::Http2AllowedAttribute, http2_);
214 #endif
215 
216   errors_.clear();
217   QNetworkReply *reply = network_->get(req);
218   replies_ << reply;
219   QObject::connect(reply, &QNetworkReply::sslErrors, this, &SubsonicService::HandlePingSSLErrors);
220   QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, url, username, password, auth_method]() { HandlePingReply(reply, url, username, password, auth_method); });
221 
222   //qLog(Debug) << "Subsonic: Sending request" << url << url.query();
223 
224 }
225 
HandlePingSSLErrors(const QList<QSslError> & ssl_errors)226 void SubsonicService::HandlePingSSLErrors(const QList<QSslError> &ssl_errors) {
227 
228   for (const QSslError &ssl_error : ssl_errors) {
229     errors_ += ssl_error.errorString();
230   }
231 
232 }
233 
HandlePingReply(QNetworkReply * reply,const QUrl & url,const QString & username,const QString & password,const SubsonicSettingsPage::AuthMethod auth_method)234 void SubsonicService::HandlePingReply(QNetworkReply *reply, const QUrl &url, const QString &username, const QString &password, const SubsonicSettingsPage::AuthMethod auth_method) {
235 
236   Q_UNUSED(url);
237 
238   if (!replies_.contains(reply)) return;
239   replies_.removeAll(reply);
240   QObject::disconnect(reply, nullptr, this, nullptr);
241   reply->deleteLater();
242 
243   if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) {
244     if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
245       // This is a network error, there is nothing more to do.
246       PingError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
247       return;
248     }
249     else {
250 
251       // Check for a valid redirect first.
252       if (
253           (
254           reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 301 ||
255           reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 302 ||
256           reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 307
257           )
258           &&
259           ping_redirects_ <= kMaxRedirects
260       )
261       {
262         QUrl redirect_url = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
263         if (!redirect_url.isEmpty()) {
264           ++ping_redirects_;
265           qLog(Debug) << "Redirecting ping request to" << redirect_url.toString(QUrl::RemoveQuery);
266           SendPingWithCredentials(redirect_url, username, password, auth_method, true);
267           return;
268         }
269       }
270 
271       // See if there is Json data containing "error" - then use that instead.
272       QByteArray data = reply->readAll();
273       QJsonParseError parse_error;
274       QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error);
275       if (parse_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
276         QJsonObject json_obj = json_doc.object();
277         if (!json_obj.isEmpty() && json_obj.contains("error")) {
278           QJsonValue json_error = json_obj["error"];
279           if (json_error.isObject()) {
280             json_obj = json_error.toObject();
281             if (!json_obj.isEmpty() && json_obj.contains("code") && json_obj.contains("message")) {
282               int code = json_obj["code"].toInt();
283               QString message = json_obj["message"].toString();
284               errors_ << QString("%1 (%2)").arg(message).arg(code);
285             }
286           }
287         }
288       }
289       if (errors_.isEmpty()) {
290         if (reply->error() != QNetworkReply::NoError) {
291           errors_ << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
292         }
293         else {
294           errors_ << QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
295         }
296       }
297       PingError();
298       return;
299     }
300   }
301 
302   errors_.clear();
303 
304   QByteArray data(reply->readAll());
305 
306   QJsonParseError json_error;
307   QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
308 
309   if (json_error.error != QJsonParseError::NoError) {
310     PingError("Ping reply from server missing Json data.");
311     return;
312   }
313 
314   if (json_doc.isEmpty()) {
315     PingError("Ping reply from server has empty Json document.");
316     return;
317   }
318 
319   if (!json_doc.isObject()) {
320     PingError("Ping reply from server has Json document that is not an object.", json_doc);
321     return;
322   }
323 
324   QJsonObject json_obj = json_doc.object();
325   if (json_obj.isEmpty()) {
326     PingError("Ping reply from server has empty Json object.", json_doc);
327     return;
328   }
329 
330   if (!json_obj.contains("subsonic-response")) {
331     PingError("Ping reply from server is missing subsonic-response", json_obj);
332     return;
333   }
334   QJsonValue value_response = json_obj["subsonic-response"];
335   if (!value_response.isObject()) {
336     PingError("Ping reply from server subsonic-response is not an object", value_response);
337     return;
338   }
339   QJsonObject obj_response = value_response.toObject();
340 
341   if (obj_response.contains("error")) {
342     QJsonValue value_error = obj_response["error"];
343     if (!value_error.isObject()) {
344       PingError("Authentication error reply from server is not an object", value_error);
345       return;
346     }
347     QJsonObject obj_error = value_error.toObject();
348     if (!obj_error.contains("code") || !obj_error.contains("message")) {
349       PingError("Authentication error reply from server is missing status or message", json_obj);
350       return;
351     }
352     //int status = obj_error["code"].toInt();
353     QString message = obj_error["message"].toString();
354     emit TestComplete(false, message);
355     emit TestFailure(message);
356     return;
357   }
358 
359   if (!obj_response.contains("status")) {
360     PingError("Ping reply from server is missing status", obj_response);
361     return;
362   }
363 
364   QString status = obj_response["status"].toString().toLower();
365   QString message = obj_response["message"].toString();
366 
367   if (status == "failed") {
368     emit TestComplete(false, message);
369     emit TestFailure(message);
370     return;
371   }
372   else if (status == "ok") {
373     emit TestComplete(true);
374     emit TestSuccess();
375     return;
376   }
377   else {
378     PingError("Ping reply status from server is unknown", json_obj);
379     return;
380   }
381 
382 }
383 
CheckConfiguration()384 void SubsonicService::CheckConfiguration() {
385 
386   if (server_url_.isEmpty()) {
387     emit TestComplete(false, "Missing Subsonic server url.");
388     return;
389   }
390   if (username_.isEmpty()) {
391     emit TestComplete(false, "Missing Subsonic username.");
392     return;
393   }
394   if (password_.isEmpty()) {
395     emit TestComplete(false, "Missing Subsonic password.");
396     return;
397   }
398 
399 }
400 
Scrobble(const QString & song_id,const bool submission,const QDateTime & time)401 void SubsonicService::Scrobble(const QString &song_id, const bool submission, const QDateTime &time) {
402 
403   if (!server_url().isValid() || username().isEmpty() || password().isEmpty()) {
404     return;
405   }
406 
407   if (!scrobble_request_) {
408     // We're doing requests every 30-240s the whole time, so keep reusing this instance
409     scrobble_request_ = std::make_shared<SubsonicScrobbleRequest>(this, url_handler_, app_);
410   }
411 
412   scrobble_request_->CreateScrobbleRequest(song_id, submission, time);
413 
414 }
415 
ResetSongsRequest()416 void SubsonicService::ResetSongsRequest() {
417 
418   if (songs_request_) {
419     QObject::disconnect(songs_request_.get(), nullptr, this, nullptr);
420     QObject::disconnect(this, nullptr, songs_request_.get(), nullptr);
421     songs_request_.reset();
422   }
423 
424 }
425 
GetSongs()426 void SubsonicService::GetSongs() {
427 
428   if (!server_url().isValid()) {
429     emit SongsResults(SongMap(), tr("Server URL is invalid."));
430     return;
431   }
432 
433   if (username().isEmpty() || password().isEmpty()) {
434     emit SongsResults(SongMap(), tr("Missing username or password."));
435     return;
436   }
437 
438   ResetSongsRequest();
439   songs_request_ = std::make_shared<SubsonicRequest>(this, url_handler_, app_);
440   QObject::connect(songs_request_.get(), &SubsonicRequest::Results, this, &SubsonicService::SongsResultsReceived);
441   QObject::connect(songs_request_.get(), &SubsonicRequest::UpdateStatus, this, &SubsonicService::SongsUpdateStatus);
442   QObject::connect(songs_request_.get(), &SubsonicRequest::ProgressSetMaximum, this, &SubsonicService::SongsProgressSetMaximum);
443   QObject::connect(songs_request_.get(), &SubsonicRequest::UpdateProgress, this, &SubsonicService::SongsUpdateProgress);
444 
445   songs_request_->GetAlbums();
446 
447 }
448 
SongsResultsReceived(const SongMap & songs,const QString & error)449 void SubsonicService::SongsResultsReceived(const SongMap &songs, const QString &error) {
450 
451   emit SongsResults(songs, error);
452 
453 }
454 
PingError(const QString & error,const QVariant & debug)455 void SubsonicService::PingError(const QString &error, const QVariant &debug) {
456 
457   if (!error.isEmpty()) errors_ << error;
458 
459   QString error_html;
460   for (const QString &e : errors_) {
461     qLog(Error) << "Subsonic:" << e;
462     error_html += e + "<br />";
463   }
464   if (debug.isValid()) qLog(Debug) << debug;
465 
466   emit TestFailure(error_html);
467   emit TestComplete(false, error_html);
468 
469   errors_.clear();
470 
471 }
472