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 <QString>
30 #include <QUrl>
31 #include <QUrlQuery>
32 #include <QNetworkAccessManager>
33 #include <QNetworkRequest>
34 #include <QNetworkReply>
35 #include <QCryptographicHash>
36 #include <QSslConfiguration>
37 #include <QSslSocket>
38 #include <QSslError>
39 #include <QJsonDocument>
40 #include <QJsonObject>
41 #include <QJsonValue>
42 
43 #include "core/utilities.h"
44 #include "subsonicservice.h"
45 #include "subsonicbaserequest.h"
46 
47 #include "settings/subsonicsettingspage.h"
48 
SubsonicBaseRequest(SubsonicService * service,QObject * parent)49 SubsonicBaseRequest::SubsonicBaseRequest(SubsonicService *service, QObject *parent)
50     : QObject(parent),
51       service_(service),
52       network_(new QNetworkAccessManager) {
53 
54 #if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0))
55   network_->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
56 #endif
57 
58 }
59 
CreateUrl(const QUrl & server_url,const SubsonicSettingsPage::AuthMethod auth_method,const QString & username,const QString & password,const QString & ressource_name,const ParamList & params_provided)60 QUrl SubsonicBaseRequest::CreateUrl(const QUrl &server_url, const SubsonicSettingsPage::AuthMethod auth_method, const QString &username, const QString &password, const QString &ressource_name, const ParamList &params_provided) {
61 
62   ParamList params = ParamList() << params_provided
63                                  << Param("c", SubsonicService::kClientName)
64                                  << Param("v", SubsonicService::kApiVersion)
65                                  << Param("f", "json")
66                                  << Param("u", username);
67 
68   if (auth_method == SubsonicSettingsPage::AuthMethod_Hex) {
69     params << Param("p", QString("enc:" + password.toUtf8().toHex()));
70   }
71   else {
72     const QString salt = Utilities::CryptographicRandomString(20);
73     QCryptographicHash md5(QCryptographicHash::Md5);
74     md5.addData(password.toUtf8());
75     md5.addData(salt.toUtf8());
76     params << Param("s", salt);
77     params << Param("t", md5.result().toHex());
78   }
79 
80   QUrlQuery url_query;
81   for (const Param &param : params) {
82     url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
83   }
84 
85   QUrl url(server_url);
86 
87   if (!url.path().isEmpty() && url.path().right(1) == "/") {
88     url.setPath(url.path() + QString("rest/") + ressource_name + QString(".view"));
89   }
90   else {
91     url.setPath(url.path() + QString("/rest/") + ressource_name + QString(".view"));
92   }
93 
94   url.setQuery(url_query);
95 
96   return url;
97 
98 }
99 
CreateGetRequest(const QString & ressource_name,const ParamList & params_provided) const100 QNetworkReply *SubsonicBaseRequest::CreateGetRequest(const QString &ressource_name, const ParamList &params_provided) const {
101 
102   QUrl url = CreateUrl(server_url(), auth_method(), username(), password(), ressource_name, params_provided);
103   QNetworkRequest req(url);
104 
105   if (url.scheme() == "https" && !verify_certificate()) {
106     QSslConfiguration sslconfig = QSslConfiguration::defaultConfiguration();
107     sslconfig.setPeerVerifyMode(QSslSocket::VerifyNone);
108     req.setSslConfiguration(sslconfig);
109   }
110 
111   req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
112 #if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
113   req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
114 #else
115   req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
116 #endif
117 
118 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
119   req.setAttribute(QNetworkRequest::Http2AllowedAttribute, http2());
120 #endif
121 
122   QNetworkReply *reply = network_->get(req);
123   QObject::connect(reply, &QNetworkReply::sslErrors, this, &SubsonicBaseRequest::HandleSSLErrors);
124 
125   //qLog(Debug) << "Subsonic: Sending request" << url;
126 
127   return reply;
128 
129 }
130 
HandleSSLErrors(const QList<QSslError> & ssl_errors)131 void SubsonicBaseRequest::HandleSSLErrors(const QList<QSslError> &ssl_errors) {
132 
133   for (const QSslError &ssl_error : ssl_errors) {
134     Error(ssl_error.errorString());
135   }
136 
137 }
138 
GetReplyData(QNetworkReply * reply)139 QByteArray SubsonicBaseRequest::GetReplyData(QNetworkReply *reply) {
140 
141   QByteArray data;
142 
143   if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
144     data = reply->readAll();
145   }
146   else {
147     if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
148       // This is a network error, there is nothing more to do.
149       Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
150     }
151     else {
152 
153       // See if there is Json data containing "error" - then use that instead.
154       data = reply->readAll();
155       QString error;
156       QJsonParseError parse_error;
157       QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error);
158       if (parse_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
159         QJsonObject json_obj = json_doc.object();
160         if (!json_obj.isEmpty() && json_obj.contains("error")) {
161           QJsonValue json_error = json_obj["error"];
162           if (json_error.isObject()) {
163             json_obj = json_error.toObject();
164             if (!json_obj.isEmpty() && json_obj.contains("code") && json_obj.contains("message")) {
165               int code = json_obj["code"].toInt();
166               QString message = json_obj["message"].toString();
167               error = QString("%1 (%2)").arg(message).arg(code);
168             }
169           }
170         }
171       }
172       if (error.isEmpty()) {
173         if (reply->error() != QNetworkReply::NoError) {
174           error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
175         }
176         else {
177           error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
178         }
179       }
180       Error(error);
181     }
182   }
183 
184   return data;
185 
186 }
187 
ExtractJsonObj(QByteArray & data)188 QJsonObject SubsonicBaseRequest::ExtractJsonObj(QByteArray &data) {
189 
190   QJsonParseError json_error;
191   QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
192 
193   if (json_error.error != QJsonParseError::NoError) {
194     Error("Reply from server missing Json data.", data);
195     return QJsonObject();
196   }
197 
198   if (json_doc.isEmpty()) {
199     Error("Received empty Json document.", data);
200     return QJsonObject();
201   }
202 
203   if (!json_doc.isObject()) {
204     Error("Json document is not an object.", json_doc);
205     return QJsonObject();
206   }
207 
208   QJsonObject json_obj = json_doc.object();
209   if (json_obj.isEmpty()) {
210     Error("Received empty Json object.", json_doc);
211     return QJsonObject();
212   }
213 
214   if (!json_obj.contains("subsonic-response")) {
215     Error("Json reply is missing subsonic-response.", json_obj);
216     return QJsonObject();
217   }
218 
219   QJsonValue json_response = json_obj["subsonic-response"];
220   if (!json_response.isObject()) {
221     Error("Json response is not an object.", json_response);
222     return QJsonObject();
223   }
224   json_obj = json_response.toObject();
225 
226   return json_obj;
227 
228 }
229 
ErrorsToHTML(const QStringList & errors)230 QString SubsonicBaseRequest::ErrorsToHTML(const QStringList &errors) {
231 
232   QString error_html;
233   for (const QString &error : errors) {
234     error_html += error + "<br />";
235   }
236   return error_html;
237 
238 }
239