diff --git a/README.md b/README.md
index a20d26d8..b765f0ac 100644
--- a/README.md
+++ b/README.md
@@ -48,13 +48,13 @@ __Notes on the self-organizing map__:
* to enable the audio similarity source, you have to enable it first in the administration panel.
## Subsonic API
-The API version implemented is 1.12.0 and has been tested on _Android_ using the official application, _Ultrasonic_ and _DSub_.
+The API version implemented is 1.16.0 and has been tested on _Android_ using _Subsonic Player_, _Ultrasonic_ and _DSub_.
-Since _LMS_ uses metadata tags to organize music, a compatibility mode is used to navigate through the collection using the directory browsing commands.
+Since _LMS_ uses metadata tags to organize music, a compatibility mode is used to navigate through the collection when using the directory browsing commands.
The Subsonic API is enabled by default.
-__Note__: since _LMS_ may store hashed and salted passwords or may forward authentication requests to external services, it cannot handle the __token authentication__ method defined from version 1.13.0.
+__Note__: since _LMS_ may store hashed and salted passwords or may forward authentication requests to external services, it cannot handle the __token authentication__ method. You may need to check your client to make sure to use the __password__ authentication method.
## About tags
_LMS_ relies exclusively on tags to organize your music collection.
diff --git a/conf/lms.conf b/conf/lms.conf
index 550db81c..ab0ba1c9 100644
--- a/conf/lms.conf
+++ b/conf/lms.conf
@@ -55,6 +55,10 @@ login-throttler-max-entries = 10000;
# API
api-subsonic = true;
+# Use this list to make the reported server version to 1.12.0 depending on the client's name
+# Main usage is to make auto detections for the 'p' (password) parameter work
+api-subsonic-report-old-server-protocol = ("DSub");
+
# Turn on this option to allow the demo account creation/use
demo = false;
diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt
index 4c87d7cd..98093601 100644
--- a/src/libs/subsonic/CMakeLists.txt
+++ b/src/libs/subsonic/CMakeLists.txt
@@ -1,5 +1,6 @@
add_library(lmssubsonic SHARED
+ impl/ProtocolVersion.cpp
impl/Scan.cpp
impl/Stream.cpp
impl/SubsonicId.cpp
diff --git a/src/libs/subsonic/impl/ClientInfo.hpp b/src/libs/subsonic/impl/ClientInfo.hpp
new file mode 100644
index 00000000..9c2dafd0
--- /dev/null
+++ b/src/libs/subsonic/impl/ClientInfo.hpp
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2021 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include
+#include "ProtocolVersion.hpp"
+
+namespace API::Subsonic
+{
+ struct ClientInfo
+ {
+ std::string name;
+ std::string user;
+ std::string password;
+ ProtocolVersion version;
+ };
+}
diff --git a/src/libs/subsonic/impl/ProtocolVersion.cpp b/src/libs/subsonic/impl/ProtocolVersion.cpp
new file mode 100644
index 00000000..4a02a4e3
--- /dev/null
+++ b/src/libs/subsonic/impl/ProtocolVersion.cpp
@@ -0,0 +1,56 @@
+/*
+ * copyright (c) 2021 emeric poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "ProtocolVersion.hpp"
+
+namespace StringUtils
+{
+ template<>
+ std::optional
+ readAs(std::string_view str)
+ {
+ // Expects "X.Y.Z"
+ const auto numbers {StringUtils::splitString(str, ".")};
+ if (numbers.size() < 2 || numbers.size() > 3)
+ return std::nullopt;
+
+ API::Subsonic::ProtocolVersion version;
+
+ auto number {StringUtils::readAs(numbers[0])};
+ if (!number)
+ return std::nullopt;
+ version.major = *number;
+
+ number = {StringUtils::readAs(numbers[1])};
+ if (!number)
+ return std::nullopt;
+ version.minor = *number;
+
+ if (numbers.size() == 3)
+ {
+ number = {StringUtils::readAs(numbers[2])};
+ if (!number)
+ return std::nullopt;
+ version.patch = *number;
+ }
+
+ return version;
+ }
+}
+
diff --git a/src/libs/subsonic/impl/ProtocolVersion.hpp b/src/libs/subsonic/impl/ProtocolVersion.hpp
new file mode 100644
index 00000000..fc046e22
--- /dev/null
+++ b/src/libs/subsonic/impl/ProtocolVersion.hpp
@@ -0,0 +1,40 @@
+/*
+ * copyright (c) 2021 emeric poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include "utils/String.hpp"
+
+namespace API::Subsonic
+{
+ struct ProtocolVersion
+ {
+ unsigned major {};
+ unsigned minor {};
+ unsigned patch {};
+ };
+
+ static inline constexpr ProtocolVersion defaultServerProtocolVersion {1, 16, 0};
+}
+
+namespace StringUtils
+{
+ template<> std::optional readAs(std::string_view str);
+}
+
diff --git a/src/libs/subsonic/impl/RequestContext.hpp b/src/libs/subsonic/impl/RequestContext.hpp
index d2949a8b..9d73bcb4 100644
--- a/src/libs/subsonic/impl/RequestContext.hpp
+++ b/src/libs/subsonic/impl/RequestContext.hpp
@@ -24,6 +24,8 @@
#include
#include "database/Types.hpp"
+#include "ClientInfo.hpp"
+#include "ProtocolVersion.hpp"
namespace Database
{
@@ -37,7 +39,8 @@ namespace API::Subsonic
const Wt::Http::ParameterMap& parameters;
Database::Session& dbSession;
Database::UserId userId;
- std::string clientName;
+ ClientInfo clientInfo;
+ ProtocolVersion serverProtocolVersion;
};
}
diff --git a/src/libs/subsonic/impl/Scan.cpp b/src/libs/subsonic/impl/Scan.cpp
index 9d57fb17..dee3c082 100644
--- a/src/libs/subsonic/impl/Scan.cpp
+++ b/src/libs/subsonic/impl/Scan.cpp
@@ -52,7 +52,7 @@ namespace API::Subsonic::Scan
Response
handleGetScanStatus(RequestContext& context)
{
- Response response {Response::createOkResponse(context)};
+ Response response {Response::createOkResponse(context.serverProtocolVersion)};
response.addNode("scanStatus", createStatusResponseNode());
return response;
@@ -63,7 +63,7 @@ namespace API::Subsonic::Scan
{
Service::get()->requestImmediateScan(false);
- Response response {Response::createOkResponse(context)};
+ Response response {Response::createOkResponse(context.serverProtocolVersion)};
response.addNode("scanStatus", createStatusResponseNode());
return response;
diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp
index a3f8b3f4..dc9a904d 100644
--- a/src/libs/subsonic/impl/SubsonicResource.cpp
+++ b/src/libs/subsonic/impl/SubsonicResource.cpp
@@ -16,7 +16,8 @@
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see .
*/
-#include "subsonic/SubsonicResource.hpp"
+
+#include "SubsonicResource.hpp"
#include
#include
@@ -39,12 +40,14 @@
#include "database/User.hpp"
#include "recommendation/IEngine.hpp"
#include "scrobbling/IScrobbling.hpp"
+#include "utils/IConfig.hpp"
#include "utils/Logger.hpp"
#include "utils/Random.hpp"
#include "utils/Service.hpp"
#include "utils/String.hpp"
#include "utils/Utils.hpp"
#include "ParameterParsing.hpp"
+#include "ProtocolVersion.hpp"
#include "RequestContext.hpp"
#include "Scan.hpp"
#include "Stream.hpp"
@@ -58,56 +61,18 @@ static const std::string reportedStarredDate {"2000-01-01T00:00:00"};
static const std::string reportedDummyDate {"2000-01-01T00:00:00"};
static const unsigned long long reportedDummyDateULong {946684800000ULL}; // 2000-01-01T00:00:00 UTC
-namespace API::Subsonic
-{
- struct ClientVersion
- {
- unsigned major {};
- unsigned minor {};
- unsigned patch {};
- };
-}
-namespace StringUtils
-{
- template<>
- std::optional
- readAs(std::string_view str)
- {
- // Expects "X.Y.Z"
- const auto numbers {StringUtils::splitString(str, ".")};
- if (numbers.size() < 2 || numbers.size() > 3)
- return std::nullopt;
-
- API::Subsonic::ClientVersion version;
-
- auto number {StringUtils::readAs(numbers[0])};
- if (!number)
- return std::nullopt;
- version.major = *number;
-
- number = {StringUtils::readAs(numbers[1])};
- if (!number)
- return std::nullopt;
- version.minor = *number;
-
- if (numbers.size() == 3)
- {
- number = {StringUtils::readAs(numbers[2])};
- if (!number)
- return std::nullopt;
- version.patch = *number;
- }
-
- return version;
- }
-
-}
namespace API::Subsonic
{
+std::unique_ptr
+createSubsonicResource(Database::Db& db)
+{
+ return std::make_unique(db);
+}
+
static
void
checkSetPasswordImplemented()
@@ -140,38 +105,24 @@ decodePasswordIfNeeded(const std::string& password)
return password;
}
-struct ClientInfo
-{
- std::string name;
- std::string user;
- std::string password;
- ClientVersion version;
-};
-
static
-ClientInfo
-getClientInfo(const Wt::Http::ParameterMap& parameters)
+std::unordered_map
+readConfigProtocolVersions()
{
- ClientInfo res;
+ std::unordered_map res;
- // Mandatory parameters
- res.name = getMandatoryParameterAs(parameters, "c");
- res.version = getMandatoryParameterAs(parameters, "v");
- if (res.version.major > API_VERSION_MAJOR)
- throw ServerMustUpgradeError {};
- if (res.version.major < API_VERSION_MAJOR)
- throw ClientMustUpgradeError {};
- if (res.version.minor > Response::getAPIMinorVersion(res.name))
- throw ServerMustUpgradeError {};
-
- res.user = getMandatoryParameterAs(parameters, "u");
- res.password = decodePasswordIfNeeded(getMandatoryParameterAs(parameters, "p"));
+ Service::get()->visitStrings("api-subsonic-report-old-server-protocol",
+ [&](std::string_view client)
+ {
+ res.emplace(std::string {client}, ProtocolVersion {1, 12, 0});
+ }, {"DSub"});
return res;
}
SubsonicResource::SubsonicResource(Db& db)
-: _db {db}
+: _serverProtocolVersionsByClient {readConfigProtocolVersions()}
+, _db {db}
{
}
@@ -530,7 +481,7 @@ static
Response
handlePingRequest(RequestContext& context)
{
- return Response::createOkResponse(context);
+ return Response::createOkResponse(context.serverProtocolVersion);
}
static
@@ -570,7 +521,7 @@ handleChangePassword(RequestContext& context)
throw UserNotAuthorizedError {};
}
- return Response::createOkResponse(context);
+ return Response::createOkResponse(context.serverProtocolVersion);
}
static
@@ -620,7 +571,7 @@ handleCreatePlaylistRequest(RequestContext& context)
TrackListEntry::create(context.dbSession, track, tracklist );
}
- return Response::createOkResponse(context);
+ return Response::createOkResponse(context.serverProtocolVersion);
}
static
@@ -671,7 +622,7 @@ handleCreateUserRequest(RequestContext& context)
throw UserNotAuthorizedError {};
}
- return Response::createOkResponse(context);
+ return Response::createOkResponse(context.serverProtocolVersion);
}
static
@@ -696,7 +647,7 @@ handleDeletePlaylistRequest(RequestContext& context)
tracklist.remove();
- return Response::createOkResponse(context);
+ return Response::createOkResponse(context.serverProtocolVersion);
}
static
@@ -717,14 +668,14 @@ handleDeleteUserRequest(RequestContext& context)
user.remove();
- return Response::createOkResponse(context);
+ return Response::createOkResponse(context.serverProtocolVersion);
}
static
Response
handleGetLicenseRequest(RequestContext& context)
{
- Response response {Response::createOkResponse(context)};
+ Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& licenseNode {response.createNode("license")};
licenseNode.setAttribute("licenseExpires", "2025-09-03T14:46:43");
@@ -750,7 +701,7 @@ handleGetRandomSongsRequest(RequestContext& context)
auto tracks {Track::getAllRandom(context.dbSession, {}, size)};
- Response response {Response::createOkResponse(context)};
+ Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& randomSongsNode {response.createNode("randomSongs")};
for (const Track::pointer& track : tracks)
@@ -839,7 +790,7 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
else
throw NotImplementedGenericError {};
- Response response {Response::createOkResponse(context)};
+ Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& albumListNode {response.createNode(id3 ? "albumList2" : "albumList")};
for (const Release::pointer& release : releases)
@@ -879,7 +830,7 @@ handleGetAlbumRequest(RequestContext& context)
if (!user)
throw UserNotAuthorizedError {};
- Response response {Response::createOkResponse(context)};
+ Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node releaseNode {releaseToResponseNode(release, context.dbSession, user, true /* id3 */)};
auto tracks {release->getTracks()};
@@ -908,7 +859,7 @@ handleGetArtistRequest(RequestContext& context)
if (!user)
throw UserNotAuthorizedError {};
- Response response {Response::createOkResponse(context)};
+ Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node artistNode {artistToResponseNode(user, artist, true /* id3 */)};
auto releases {artist->getReleases()};
@@ -930,7 +881,7 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3)
// Optional params
std::size_t count {getParameterAs(context.parameters, "count").value_or(20)};
- Response response {Response::createOkResponse(context)};
+ Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& artistInfoNode {response.createNode(id3 ? "artistInfo2" : "artistInfo")};
{
@@ -986,7 +937,7 @@ static
Response
handleGetArtistsRequest(RequestContext& context)
{
- Response response {Response::createOkResponse(context)};
+ Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& artistsNode {response.createNode("artists")};
artistsNode.setAttribute("ignoredArticles", "");
@@ -1040,7 +991,7 @@ handleGetMusicDirectoryRequest(RequestContext& context)
if (!root && !artistId && !releaseId && !trackId)
throw BadParameterGenericError {"id"};
- Response response {Response::createOkResponse(context)};
+ Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& directoryNode {response.createNode("directory")};
auto transaction {context.dbSession.createSharedTransaction()};
@@ -1097,7 +1048,7 @@ static
Response
handleGetMusicFoldersRequest(RequestContext& context)
{
- Response response {Response::createOkResponse(context)};
+ Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& musicFoldersNode {response.createNode("musicFolders")};
Response::Node& musicFolderNode {musicFoldersNode.createArrayChild("musicFolder")};
@@ -1111,7 +1062,7 @@ static
Response
handleGetGenresRequest(RequestContext& context)
{
- Response response {Response::createOkResponse(context)};
+ Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& genresNode {response.createNode("genres")};
@@ -1133,7 +1084,7 @@ static
Response
handleGetIndexesRequest(RequestContext& context)
{
- Response response {Response::createOkResponse(context)};
+ Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& artistsNode {response.createNode("indexes")};
artistsNode.setAttribute("ignoredArticles", "");
@@ -1216,7 +1167,7 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
Random::shuffleContainer(tracks);
- Response response {Response::createOkResponse(context)};
+ Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& similarSongsNode {response.createNode(id3 ? "similarSongs2" : "similarSongs")};
for (const Track::pointer& track : tracks)
similarSongsNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user));
@@ -1248,7 +1199,7 @@ handleGetStarredRequestCommon(RequestContext& context, bool id3)
if (!user)
throw UserNotAuthorizedError {};
- Response response {Response::createOkResponse(context)};
+ Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& starredNode {response.createNode(id3 ? "starred2" : "starred")};
{
@@ -1324,7 +1275,7 @@ handleGetPlaylistRequest(RequestContext& context)
if (!tracklist)
throw RequestedDataNotFoundError {};
- Response response {Response::createOkResponse(context)};
+ Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node playlistNode {tracklistToResponseNode(tracklist, context.dbSession)};
auto entries {tracklist->getEntries()};
@@ -1346,7 +1297,7 @@ handleGetPlaylistsRequest(RequestContext& context)
if (!user)
throw UserNotAuthorizedError {};
- Response response {Response::createOkResponse(context)};
+ Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& playlistsNode {response.createNode("playlists")};
auto tracklists {TrackList::getAll(context.dbSession, user, TrackList::Type::Playlist)};
@@ -1383,7 +1334,7 @@ handleGetSongsByGenreRequest(RequestContext& context)
if (!user)
throw UserNotAuthorizedError {};
- Response response {Response::createOkResponse(context)};
+ Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& songsByGenreNode {response.createNode("songsByGenre")};
bool more;
@@ -1408,7 +1359,7 @@ handleGetUserRequest(RequestContext& context)
if (!user)
throw RequestedDataNotFoundError {};
- Response response {Response::createOkResponse(context)};
+ Response response {Response::createOkResponse(context.serverProtocolVersion)};
response.addNode("user", userToResponseNode(user));
return response;
@@ -1420,7 +1371,7 @@ handleGetUsersRequest(RequestContext& context)
{
auto transaction {context.dbSession.createSharedTransaction()};
- Response response {Response::createOkResponse(context)};
+ Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& usersNode {response.createNode("users")};
const auto users {User::getAll(context.dbSession)};
@@ -1453,7 +1404,7 @@ handleSearchRequestCommon(RequestContext& context, bool id3)
if (!user)
throw UserNotAuthorizedError {};
- Response response {Response::createOkResponse(context)};
+ Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& searchResult2Node {response.createNode(id3 ? "searchResult3" : "searchResult2")};
bool more;
@@ -1538,7 +1489,7 @@ handleStarRequest(RequestContext& context)
user.modify()->starTrack(track);
}
- return Response::createOkResponse(context);
+ return Response::createOkResponse(context.serverProtocolVersion);
}
static
@@ -1595,7 +1546,7 @@ handleUnstarRequest(RequestContext& context)
}
- return Response::createOkResponse(context);
+ return Response::createOkResponse(context.serverProtocolVersion);
}
static
@@ -1639,7 +1590,7 @@ handleScrobble(RequestContext& context)
}
}
- return Response::createOkResponse(context);
+ return Response::createOkResponse(context.serverProtocolVersion);
}
static
@@ -1682,7 +1633,7 @@ handleUpdateUserRequest(RequestContext& context)
}
}
- return Response::createOkResponse(context);
+ return Response::createOkResponse(context.serverProtocolVersion);
}
static
@@ -1741,7 +1692,7 @@ handleUpdatePlaylistRequest(RequestContext& context)
TrackListEntry::create(context.dbSession, track, tracklist);
}
- return Response::createOkResponse(context);
+ return Response::createOkResponse(context.serverProtocolVersion);
}
static
@@ -1756,7 +1707,7 @@ handleGetBookmarks(RequestContext& context)
const auto bookmarks {TrackBookmark::getByUser(context.dbSession, user)};
- Response response {Response::createOkResponse(context)};
+ Response response {Response::createOkResponse(context.serverProtocolVersion)};
Response::Node& bookmarksNode {response.createNode("bookmarks")};
for (const TrackBookmark::pointer& bookmark : bookmarks)
@@ -1798,7 +1749,7 @@ handleCreateBookmark(RequestContext& context)
if (comment)
bookmark.modify()->setComment(*comment);
- return Response::createOkResponse(context);
+ return Response::createOkResponse(context.serverProtocolVersion);
}
static
@@ -1824,7 +1775,7 @@ handleDeleteBookmark(RequestContext& context)
bookmark.remove();
- return Response::createOkResponse(context);
+ return Response::createOkResponse(context.serverProtocolVersion);
}
static
@@ -1867,7 +1818,7 @@ struct RequestEntryPointInfo
CheckImplementedFunc checkFunc {};
};
-static std::unordered_map requestEntryPoints
+static const std::unordered_map requestEntryPoints
{
// System
{"ping", {handlePingRequest}},
@@ -1981,38 +1932,6 @@ static std::unordered_map mediaRetrieval
{"getCoverArt", handleGetCoverArt},
};
-static
-Database::UserId
-authenticateUser(const Wt::Http::Request &request, const ClientInfo& clientInfo, Session& dbSession)
-{
- if (auto *authEnvService {Service<::Auth::IEnvService>::get()})
- {
- const auto checkResult {authEnvService->processRequest(dbSession, request)};
- if (checkResult.state != ::Auth::IEnvService::CheckResult::State::Granted)
- throw UserNotAuthorizedError {};
-
- return *checkResult.userId;
- }
- else if (auto *authPasswordService {Service<::Auth::IPasswordService>::get()})
- {
- const auto checkResult {authPasswordService->checkUserPassword(dbSession,
- boost::asio::ip::address::from_string(request.clientAddress()),
- clientInfo.user, clientInfo.password)};
-
- switch (checkResult.state)
- {
- case Auth::IPasswordService::CheckResult::State::Granted:
- return *checkResult.userId;
- break;
- case Auth::IPasswordService::CheckResult::State::Denied:
- throw WrongUsernameOrPasswordError {};
- case Auth::IPasswordService::CheckResult::State::Throttled:
- throw LoginThrottledGenericError {};
- }
- }
-
- throw InternalErrorGenericError {"No service avalaible to authenticate user"};
-}
void
SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response)
@@ -2027,24 +1946,16 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp
if (StringUtils::stringEndsWith(requestPath, ".view"))
requestPath.resize(requestPath.length() - 5);
- const Wt::Http::ParameterMap& parameters {request.getParameterMap()};
-
// Optional parameters
- const ResponseFormat format {getParameterAs(parameters, "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml};
+ const ResponseFormat format {getParameterAs(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml};
- std::string clientName;
+ ProtocolVersion protocolVersion {defaultServerProtocolVersion};
try
{
- // Mandatory parameters
- const ClientInfo clientInfo {getClientInfo(parameters)};
-
- clientName = clientInfo.name;
-
- Session& dbSession {_db.getTLSSession()};
-
- const Database::UserId userId {authenticateUser(request, clientInfo, dbSession)};
- RequestContext requestContext {parameters, dbSession, userId, clientInfo.name};
+ // We need to parse client a soon as possible to make sure to answer with the right protocol version
+ protocolVersion = getServerProtocolVersion(getMandatoryParameterAs(request.getParameterMap(), "c"));
+ RequestContext requestContext {buildRequestContext(request)};
auto itEntryPoint {requestEntryPoints.find(requestPath)};
if (itEntryPoint != requestEntryPoints.end())
@@ -2079,11 +1990,97 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp
LMS_LOG(API_SUBSONIC, ERROR) << "Error while processing request '" << requestPath << "'"
<< ", params = [" << parameterMapToDebugString(request.getParameterMap()) << "]"
<< ", code = " << static_cast(e.getCode()) << ", msg = '" << e.getMessage() << "'";
- Response resp {Response::createFailedResponse(clientName, e)};
+ Response resp {Response::createFailedResponse(protocolVersion, e)};
resp.write(response.out(), format);
response.setMimeType(ResponseFormatToMimeType(format));
}
}
+ProtocolVersion
+SubsonicResource::getServerProtocolVersion(const std::string& clientName) const
+{
+ auto it {_serverProtocolVersionsByClient.find(clientName)};
+ if (it == std::cend(_serverProtocolVersionsByClient))
+ return defaultServerProtocolVersion;
+
+ return it->second;
+}
+
+void
+SubsonicResource::checkProtocolVersion(ProtocolVersion client, ProtocolVersion server)
+{
+ if (client.major > server.major)
+ throw ServerMustUpgradeError {};
+ if (client.major < server.major)
+ throw ClientMustUpgradeError {};
+ if (client.minor > server.minor)
+ throw ServerMustUpgradeError {};
+ else if (client.minor == server.minor)
+ {
+ if (client.patch > server.patch)
+ throw ServerMustUpgradeError {};
+ }
+}
+
+ClientInfo
+SubsonicResource::getClientInfo(const Wt::Http::ParameterMap& parameters)
+{
+ ClientInfo res;
+
+ // Mandatory parameters
+ res.name = getMandatoryParameterAs(parameters, "c");
+ res.version = getMandatoryParameterAs(parameters, "v");
+ res.user = getMandatoryParameterAs(parameters, "u");
+ res.password = decodePasswordIfNeeded(getMandatoryParameterAs(parameters, "p"));
+
+ return res;
+}
+
+RequestContext
+SubsonicResource::buildRequestContext(const Wt::Http::Request& request)
+{
+ const Wt::Http::ParameterMap& parameters {request.getParameterMap()};
+
+ const ClientInfo clientInfo {getClientInfo(parameters)};
+
+ Session& dbSession {_db.getTLSSession()};
+
+ const Database::UserId userId {authenticateUser(request, clientInfo, dbSession)};
+
+ return {parameters, dbSession, userId, clientInfo, getServerProtocolVersion(clientInfo.name)};
+}
+
+Database::UserId
+SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo, Session& dbSession)
+{
+ if (auto *authEnvService {Service<::Auth::IEnvService>::get()})
+ {
+ const auto checkResult {authEnvService->processRequest(dbSession, request)};
+ if (checkResult.state != ::Auth::IEnvService::CheckResult::State::Granted)
+ throw UserNotAuthorizedError {};
+
+ return *checkResult.userId;
+ }
+ else if (auto *authPasswordService {Service<::Auth::IPasswordService>::get()})
+ {
+ const auto checkResult {authPasswordService->checkUserPassword(dbSession,
+ boost::asio::ip::address::from_string(request.clientAddress()),
+ clientInfo.user, clientInfo.password)};
+
+ switch (checkResult.state)
+ {
+ case Auth::IPasswordService::CheckResult::State::Granted:
+ return *checkResult.userId;
+ break;
+ case Auth::IPasswordService::CheckResult::State::Denied:
+ throw WrongUsernameOrPasswordError {};
+ case Auth::IPasswordService::CheckResult::State::Throttled:
+ throw LoginThrottledGenericError {};
+ }
+ }
+
+ throw InternalErrorGenericError {"No service avalaible to authenticate user"};
+}
+
} // namespace api::subsonic
diff --git a/src/libs/subsonic/impl/SubsonicResource.hpp b/src/libs/subsonic/impl/SubsonicResource.hpp
new file mode 100644
index 00000000..fd53029b
--- /dev/null
+++ b/src/libs/subsonic/impl/SubsonicResource.hpp
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2019 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+#pragma once
+
+#include
+#include
+#include
+
+#include "database/Types.hpp"
+#include "ClientInfo.hpp"
+#include "RequestContext.hpp"
+
+namespace Database
+{
+ class Db;
+}
+
+namespace API::Subsonic
+{
+
+ class SubsonicResource final : public Wt::WResource
+ {
+ public:
+ SubsonicResource(Database::Db& db);
+
+ private:
+ void handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) override;
+ ProtocolVersion getServerProtocolVersion(const std::string& clientName) const;
+
+ static void checkProtocolVersion(ProtocolVersion client, ProtocolVersion server);
+ ClientInfo getClientInfo(const Wt::Http::ParameterMap& parameters);
+ RequestContext buildRequestContext(const Wt::Http::Request& request);
+ Database::UserId authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo, Database::Session& dbSession);
+
+ const std::unordered_map _serverProtocolVersionsByClient;
+ Database::Db& _db;
+ };
+
+} // namespace
diff --git a/src/libs/subsonic/impl/SubsonicResponse.cpp b/src/libs/subsonic/impl/SubsonicResponse.cpp
index 47c47fcb..38f6b631 100644
--- a/src/libs/subsonic/impl/SubsonicResponse.cpp
+++ b/src/libs/subsonic/impl/SubsonicResponse.cpp
@@ -29,6 +29,7 @@
#include "utils/Exception.hpp"
#include "utils/String.hpp"
+#include "ProtocolVersion.hpp"
namespace API::Subsonic
{
@@ -102,26 +103,32 @@ Response::Node::createArrayChild(const std::string& key)
return _childrenArrays[key].back();
}
+void
+Response::Node::setVersionAttribute(ProtocolVersion protocolVersion)
+{
+ setAttribute("version", std::to_string(protocolVersion.major) + "." + std::to_string(protocolVersion.minor) + "." + std::to_string(protocolVersion.patch));
+}
+
Response
-Response::createOkResponse(const RequestContext& context)
+Response::createOkResponse(ProtocolVersion protocolVersion)
{
Response response;
Node& responseNode {response._root.createChild("subsonic-response")};
responseNode.setAttribute("status", "ok");
- responseNode.setAttribute("version", std::string {QUOTEME(API_VERSION_MAJOR) "."} + std::to_string(getAPIMinorVersion(context.clientName)) + ".0");
+ responseNode.setVersionAttribute(protocolVersion);
return response;
}
Response
-Response::createFailedResponse(std::string_view clientName, const Error& error)
+Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& error)
{
Response response;
Node& responseNode {response._root.createChild("subsonic-response")};
responseNode.setAttribute("status", "failed");
- responseNode.setAttribute("version", std::string {QUOTEME(API_VERSION_MAJOR) "."} + std::to_string(getAPIMinorVersion(clientName)) + ".0");
+ responseNode.setVersionAttribute(protocolVersion);
Node& errorNode {responseNode.createChild("error")};
errorNode.setAttribute("code", std::to_string(static_cast(error.getCode())));
@@ -214,18 +221,6 @@ Response::writeXML(std::ostream& os)
boost::property_tree::write_xml(os, root);
}
-unsigned
-Response::getAPIMinorVersion(std::string_view clientName)
-{
- // Some clients do not rely on version to enable the clear text password auth scheme
- if (clientName == "Audinaut")
- return 16;
- else if (clientName == "Sublime Music")
- return 16;
- else
- return 12;
-}
-
void
Response::writeJSON(std::ostream& os)
{
diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp
index dfc6b19b..c0d49b1e 100644
--- a/src/libs/subsonic/impl/SubsonicResponse.hpp
+++ b/src/libs/subsonic/impl/SubsonicResponse.hpp
@@ -27,8 +27,6 @@
#include "RequestContext.hpp"
-#define API_VERSION_MAJOR 1
-
namespace API::Subsonic
{
@@ -205,6 +203,9 @@ class Response
void addArrayChild(const std::string& key, Node node);
private:
+
+ void setVersionAttribute(ProtocolVersion version);
+
friend class Response;
using Value = std::variant;
std::map _attributes;
@@ -213,8 +214,8 @@ class Response
std::map> _childrenArrays;
};
- static Response createOkResponse(const RequestContext& context);
- static Response createFailedResponse(std::string_view clientName, const Error& error);
+ static Response createOkResponse(ProtocolVersion protocolVersion);
+ static Response createFailedResponse(ProtocolVersion protocolVersion, const Error& error);
virtual ~Response() {}
Response(const Response&) = delete;
@@ -228,9 +229,7 @@ class Response
void write(std::ostream& os, ResponseFormat format);
- static unsigned getAPIMinorVersion(std::string_view clientName);
private:
-
void writeJSON(std::ostream& os);
void writeXML(std::ostream& os);
diff --git a/src/libs/subsonic/include/subsonic/SubsonicResource.hpp b/src/libs/subsonic/include/subsonic/SubsonicResource.hpp
index 6d3e14b9..2354f787 100644
--- a/src/libs/subsonic/include/subsonic/SubsonicResource.hpp
+++ b/src/libs/subsonic/include/subsonic/SubsonicResource.hpp
@@ -18,8 +18,9 @@
*/
#pragma once
+#include
+
#include
-#include
namespace Database
{
@@ -28,18 +29,5 @@ namespace Database
namespace API::Subsonic
{
-
-class SubsonicResource final : public Wt::WResource
-{
- public:
- SubsonicResource(Database::Db& db);
-
- static std::string getPath() { return "rest/"; }
- private:
-
- void handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) override;
-
- Database::Db& _db;
-};
-
+ std::unique_ptr createSubsonicResource(Database::Db& db);
} // namespace
diff --git a/src/libs/utils/impl/Config.cpp b/src/libs/utils/impl/Config.cpp
index ba19cc0a..e53b90c5 100644
--- a/src/libs/utils/impl/Config.cpp
+++ b/src/libs/utils/impl/Config.cpp
@@ -50,7 +50,8 @@ Config::Config(const std::filesystem::path& p)
std::string_view
Config::getString(std::string_view setting, std::string_view def)
{
- try {
+ try
+ {
return static_cast(_config.lookup(std::string {setting}));
}
catch (libconfig::ConfigException&)
@@ -59,10 +60,30 @@ Config::getString(std::string_view setting, std::string_view def)
}
}
+void
+Config::visitStrings(std::string_view setting, std::function _func, std::initializer_list defs)
+{
+ try
+ {
+ const libconfig::Setting& values {_config.lookup(std::string {setting})};
+ for (int i {}; i < values.getLength(); ++i)
+ _func(static_cast(values[i]));
+ }
+ catch (const libconfig::SettingNotFoundException&)
+ {
+ for (std::string_view def : defs)
+ _func(def);
+ }
+ catch (libconfig::ConfigException&)
+ {
+ }
+}
+
std::filesystem::path
Config::getPath(std::string_view setting, const std::filesystem::path& path)
{
- try {
+ try
+ {
const char* res {_config.lookup(std::string {setting})};
return std::filesystem::path {std::string(res)};
}
@@ -75,7 +96,8 @@ Config::getPath(std::string_view setting, const std::filesystem::path& path)
unsigned long
Config::getULong(std::string_view setting, unsigned long def)
{
- try {
+ try
+ {
return static_cast(_config.lookup(std::string {setting}));
}
catch (libconfig::ConfigException&)
@@ -87,7 +109,8 @@ Config::getULong(std::string_view setting, unsigned long def)
long
Config::getLong(std::string_view setting, long def)
{
- try {
+ try
+ {
return _config.lookup(std::string {setting});
}
catch (libconfig::ConfigException&)
@@ -99,7 +122,8 @@ Config::getLong(std::string_view setting, long def)
bool
Config::getBool(std::string_view setting, bool def)
{
- try {
+ try
+ {
return _config.lookup(std::string {setting});
}
catch (libconfig::ConfigException&)
@@ -108,4 +132,3 @@ Config::getBool(std::string_view setting, bool def)
}
}
-
diff --git a/src/libs/utils/impl/Config.hpp b/src/libs/utils/impl/Config.hpp
index 7dce0423..cbbb2542 100644
--- a/src/libs/utils/impl/Config.hpp
+++ b/src/libs/utils/impl/Config.hpp
@@ -36,6 +36,7 @@ class Config final : public IConfig
// Default values are returned in case of setting not found
std::string_view getString(std::string_view setting, std::string_view def = "") override;
+ void visitStrings(std::string_view setting, std::function _func, std::initializer_list defs) override;
std::filesystem::path getPath(std::string_view setting, const std::filesystem::path& def = std::filesystem::path()) override;
unsigned long getULong(std::string_view setting, unsigned long def = 0) override;
long getLong(std::string_view setting, long def = 0) override;
diff --git a/src/libs/utils/include/utils/IConfig.hpp b/src/libs/utils/include/utils/IConfig.hpp
index 409d3702..975e5d6f 100644
--- a/src/libs/utils/include/utils/IConfig.hpp
+++ b/src/libs/utils/include/utils/IConfig.hpp
@@ -18,8 +18,9 @@
*/
#pragma once
-#include
#include
+#include
+#include
// Used to get config values from configuration files
class IConfig
@@ -30,10 +31,11 @@ class IConfig
// Default values are returned in case of setting not found
virtual std::string_view getString(std::string_view setting, std::string_view def = "") = 0;
+ virtual void visitStrings(std::string_view setting, std::function _func, std::initializer_list def = {}) = 0;
virtual std::filesystem::path getPath(std::string_view setting, const std::filesystem::path& def = std::filesystem::path()) = 0;
- virtual unsigned long getULong(std::string_view setting, unsigned long def = 0) = 0;
- virtual long getLong(std::string_view setting, long def = 0) = 0;
- virtual bool getBool(std::string_view setting, bool def = false) = 0;
+ virtual unsigned long getULong(std::string_view setting, unsigned long def = 0) = 0;
+ virtual long getLong(std::string_view setting, long def = 0) = 0;
+ virtual bool getBool(std::string_view setting, bool def = false) = 0;
};
diff --git a/src/lms/main.cpp b/src/lms/main.cpp
index ae7b34e1..98c0cec6 100644
--- a/src/lms/main.cpp
+++ b/src/lms/main.cpp
@@ -267,11 +267,14 @@ int main(int argc, char* argv[])
Service scrobblingService {Scrobbling::createScrobbling(ioContext, database)};
- API::Subsonic::SubsonicResource subsonicResource {database};
+ std::unique_ptr subsonicResource;
// bind API resources
if (config->getBool("api-subsonic", true))
- server.addResource(&subsonicResource, subsonicResource.getPath());
+ {
+ subsonicResource = API::Subsonic::createSubsonicResource(database);
+ server.addResource(subsonicResource.get(), "rest/");
+ }
// bind UI entry point
server.addEntryPoint(Wt::EntryPointType::Application,