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 ¶ms_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 ¶m : 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 ¶ms_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