diff --git a/src/libs/subsonic/impl/RequestContext.hpp b/src/libs/subsonic/impl/RequestContext.hpp index ec10a226..cd41100b 100644 --- a/src/libs/subsonic/impl/RequestContext.hpp +++ b/src/libs/subsonic/impl/RequestContext.hpp @@ -38,6 +38,7 @@ namespace API::Subsonic const Wt::Http::ParameterMap& parameters; Database::Session& dbSession; std::string userName; + std::string clientName; }; } diff --git a/src/libs/subsonic/impl/Scan.cpp b/src/libs/subsonic/impl/Scan.cpp index 544f71dd..42716069 100644 --- a/src/libs/subsonic/impl/Scan.cpp +++ b/src/libs/subsonic/impl/Scan.cpp @@ -43,20 +43,20 @@ namespace API::Subsonic::Scan Response - handleGetScanStatus(RequestContext&) + handleGetScanStatus(RequestContext& context) { - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; response.addNode("scanStatus", createStatusResponseNode()); return response; } Response - handleStartScan(RequestContext&) + handleStartScan(RequestContext& context) { ServiceProvider::get()->requestImmediateScan(); - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; response.addNode("scanStatus", createStatusResponseNode()); return response; diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index ad53f93d..6d793f20 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -141,15 +141,15 @@ getClientInfo(const Wt::Http::ParameterMap& parameters) ClientInfo 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 > API_VERSION_MINOR) + if (res.version.minor > Response::getAPIMinorVersion(res.name)) throw ServerMustUpgradeError {}; - res.name = getMandatoryParameterAs(parameters, "c"); res.user = getMandatoryParameterAs(parameters, "u"); res.password = decodePasswordIfNeeded(getMandatoryParameterAs(parameters, "p")); @@ -493,9 +493,9 @@ userToResponseNode(const User::pointer& user) static Response -handlePingRequest(RequestContext&) +handlePingRequest(RequestContext& context) { - return Response::createOkResponse(); + return Response::createOkResponse(context); } static @@ -521,7 +521,7 @@ handleChangePassword(RequestContext& context) user.modify()->setPasswordHash(hash); user.modify()->clearAuthTokens(); - return Response::createOkResponse(); + return Response::createOkResponse(context); } static @@ -576,7 +576,7 @@ handleCreatePlaylistRequest(RequestContext& context) TrackListEntry::create(context.dbSession, track, tracklist ); } - return Response::createOkResponse(); + return Response::createOkResponse(context); } static @@ -599,7 +599,7 @@ handleCreateUserRequest(RequestContext& context) User::pointer user {User::create(context.dbSession, username, hash)}; - return Response::createOkResponse(); + return Response::createOkResponse(context); } static @@ -626,7 +626,7 @@ handleDeletePlaylistRequest(RequestContext& context) tracklist.remove(); - return Response::createOkResponse(); + return Response::createOkResponse(context); } static @@ -647,14 +647,14 @@ handleDeleteUserRequest(RequestContext& context) user.remove(); - return Response::createOkResponse(); + return Response::createOkResponse(context); } static Response -handleGetLicenseRequest(RequestContext&) +handleGetLicenseRequest(RequestContext& context) { - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& licenseNode {response.createNode("license")}; licenseNode.setAttribute("licenseExpires", "2025-09-03T14:46:43"); @@ -680,7 +680,7 @@ handleGetRandomSongsRequest(RequestContext& context) auto tracks {Track::getAllRandom(context.dbSession, {}, size)}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& randomSongsNode {response.createNode("randomSongs")}; for (const Track::pointer& track : tracks) @@ -757,7 +757,7 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) else throw NotImplementedGenericError {}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& albumListNode {response.createNode(id3 ? "albumList2" : "albumList")}; for (const Release::pointer& release : releases) @@ -800,7 +800,7 @@ handleGetAlbumRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node releaseNode {releaseToResponseNode(release, context.dbSession, user, true /* id3 */)}; auto tracks {release->getTracks()}; @@ -832,7 +832,7 @@ handleGetArtistRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node artistNode {artistToResponseNode(user, artist, true /* id3 */)}; auto releases {artist->getReleases()}; @@ -856,7 +856,7 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3) // Optional params std::size_t count {getParameterAs(context.parameters, "count").value_or(20)}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& artistInfoNode {response.createNode(id3 ? "artistInfo2" : "artistInfo")}; { @@ -909,7 +909,7 @@ static Response handleGetArtistsRequest(RequestContext& context) { - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& artistsNode {response.createNode("artists")}; Response::Node& indexNode {artistsNode.createArrayChild("index")}; @@ -951,7 +951,7 @@ handleGetMusicDirectoryRequest(RequestContext& context) // Mandatory params Id id {getMandatoryParameterAs(context.parameters, "id")}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& directoryNode {response.createNode("directory")}; directoryNode.setAttribute("id", IdToString(id)); @@ -1015,9 +1015,9 @@ handleGetMusicDirectoryRequest(RequestContext& context) static Response -handleGetMusicFoldersRequest(RequestContext&) +handleGetMusicFoldersRequest(RequestContext& context) { - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& musicFoldersNode {response.createNode("musicFolders")}; Response::Node& musicFolderNode {musicFoldersNode.createArrayChild("musicFolder")}; @@ -1031,7 +1031,7 @@ static Response handleGetGenresRequest(RequestContext& context) { - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& genresNode {response.createNode("genres")}; @@ -1053,7 +1053,7 @@ static Response handleGetIndexesRequest(RequestContext& context) { - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& artistsNode {response.createNode("indexes")}; Response::Node& indexNode {artistsNode.createArrayChild("index")}; @@ -1129,7 +1129,7 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3) Random::shuffleContainer(tracks); - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& similarSongsNode {response.createNode(id3 ? "similarSongs2" : "similarSongs")}; for (const Track::pointer& track : tracks) similarSongsNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); @@ -1161,7 +1161,7 @@ handleGetStarredRequestCommon(RequestContext& context, bool id3) if (!user) throw UserNotAuthorizedError {}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& starredNode {response.createNode(id3 ? "starred2" : "starred")}; { @@ -1236,7 +1236,7 @@ handleGetPlaylistRequest(RequestContext& context) if (!tracklist) throw RequestedDataNotFoundError {}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node playlistNode {tracklistToResponseNode(tracklist, context.dbSession)}; auto entries {tracklist->getEntries()}; @@ -1258,7 +1258,7 @@ handleGetPlaylistsRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& playlistsNode {response.createNode("playlists")}; auto tracklists {TrackList::getAll(context.dbSession, user, TrackList::Type::Playlist)}; @@ -1295,7 +1295,7 @@ handleGetSongsByGenreRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& songsByGenreNode {response.createNode("songsByGenre")}; bool more; @@ -1320,7 +1320,7 @@ handleGetUserRequest(RequestContext& context) if (!user) throw RequestedDataNotFoundError {}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; response.addNode("user", userToResponseNode(user)); return response; @@ -1332,7 +1332,7 @@ handleGetUsersRequest(RequestContext& context) { auto transaction {context.dbSession.createSharedTransaction()}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& usersNode {response.createNode("users")}; const auto users {User::getAll(context.dbSession)}; @@ -1365,7 +1365,7 @@ handleSearchRequestCommon(RequestContext& context, bool id3) if (!user) throw UserNotAuthorizedError {}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& searchResult2Node {response.createNode(id3 ? "searchResult3" : "searchResult2")}; bool more; @@ -1474,7 +1474,7 @@ handleStarRequest(RequestContext& context) user.modify()->starTrack(track); } - return Response::createOkResponse(); + return Response::createOkResponse(context); } static @@ -1531,7 +1531,7 @@ handleUnstarRequest(RequestContext& context) } - return Response::createOkResponse(); + return Response::createOkResponse(context); } static @@ -1563,7 +1563,7 @@ handleUpdateUserRequest(RequestContext& context) user.modify()->clearAuthTokens(); } - return Response::createOkResponse(); + return Response::createOkResponse(context); } static @@ -1627,7 +1627,7 @@ handleUpdatePlaylistRequest(RequestContext& context) TrackListEntry::create(context.dbSession, track, tracklist ); } - return Response::createOkResponse(); + return Response::createOkResponse(context); } static @@ -1642,7 +1642,7 @@ handleGetBookmarks(RequestContext& context) const auto bookmarks {TrackBookmark::getByUser(context.dbSession, user)}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& bookmarksNode {response.createNode("bookmarks")}; for (const TrackBookmark::pointer& bookmark : bookmarks) @@ -1687,7 +1687,7 @@ handleCreateBookmark(RequestContext& context) if (comment) bookmark.modify()->setComment(*comment); - return Response::createOkResponse(); + return Response::createOkResponse(context); } static @@ -1715,7 +1715,7 @@ handleDeleteBookmark(RequestContext& context) bookmark.remove(); - return Response::createOkResponse(); + return Response::createOkResponse(context); } static @@ -1891,12 +1891,15 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp // Optional parameters const ResponseFormat format {getParameterAs(parameters, "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml}; + std::string clientName; + try { - // Mandatory parameters const ClientInfo clientInfo {getClientInfo(parameters)}; + clientName = clientInfo.name; + SessionPool::ScopedSession dbSession {_sessionPool}; switch (ServiceProvider::get()->checkUserPassword(dbSession.get(), @@ -1911,7 +1914,7 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp throw LoginThrottledGenericError {}; } - RequestContext requestContext {parameters, dbSession.get(), clientInfo.user}; + RequestContext requestContext {parameters, dbSession.get(), clientInfo.user, clientInfo.name}; auto itEntryPoint {requestEntryPoints.find(requestPath)}; if (itEntryPoint != requestEntryPoints.end()) @@ -1945,21 +1948,12 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp LMS_LOG(API_SUBSONIC, ERROR) << "Unhandled command '" << requestPath << "'"; throw UnknownEntryPointGenericError {}; } - catch (const NotImplementedGenericError& e) - { - LMS_LOG(API_SUBSONIC, INFO) << "Command '" << requestPath << "'" - << ", params = [" << parameterMapToDebugString(request.getParameterMap()) << "]" - << ", code = " << static_cast(e.getCode()) << ", msg = '" << static_cast(e).getMessage() << "'"; - Response resp {Response::createFailedResponse(e)}; - resp.write(response.out(), format); - response.setMimeType(ResponseFormatToMimeType(format)); - } catch (const Error& e) { 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(e)}; + Response resp {Response::createFailedResponse(clientName, e)}; resp.write(response.out(), format); response.setMimeType(ResponseFormatToMimeType(format)); } diff --git a/src/libs/subsonic/impl/SubsonicResponse.cpp b/src/libs/subsonic/impl/SubsonicResponse.cpp index 893be143..9572d18e 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.cpp +++ b/src/libs/subsonic/impl/SubsonicResponse.cpp @@ -28,6 +28,7 @@ #include #include "utils/Exception.hpp" +#include "utils/String.hpp" namespace API::Subsonic { @@ -108,25 +109,25 @@ Response::Node::createArrayChild(const std::string& key) } Response -Response::createOkResponse() +Response::createOkResponse(const RequestContext& context) { Response response; Node& responseNode {response._root.createChild("subsonic-response")}; responseNode.setAttribute("status", "ok"); - responseNode.setAttribute("version", API_VERSION_STR); + responseNode.setAttribute("version", std::string {QUOTEME(API_VERSION_MAJOR) "."} + std::to_string(getAPIMinorVersion(context.clientName)) + ".0"); return response; } Response -Response::createFailedResponse(const Error& error) +Response::createFailedResponse(std::string_view clientName, const Error& error) { Response response; Node& responseNode {response._root.createChild("subsonic-response")}; responseNode.setAttribute("status", "failed"); - responseNode.setAttribute("version", API_VERSION_STR); + responseNode.setAttribute("version", std::string {QUOTEME(API_VERSION_MAJOR) "."} + std::to_string(getAPIMinorVersion(clientName)) + ".0"); Node& errorNode {responseNode.createChild("error")}; errorNode.setAttribute("code", std::to_string(static_cast(error.getCode()))); @@ -213,6 +214,16 @@ Response::writeXML(std::ostream& os) boost::property_tree::write_xml(os, root); } +unsigned +Response::getAPIMinorVersion(std::string_view clientName) +{ + // Audinaut does not lower its client API version in plain text authentication mode + if (clientName == "Audinaut") + return 13; + 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 8354cbf2..811d22c9 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.hpp +++ b/src/libs/subsonic/impl/SubsonicResponse.hpp @@ -24,15 +24,13 @@ #include #include +#include "RequestContext.hpp" + +#define API_VERSION_MAJOR 1 namespace API::Subsonic { -#define API_VERSION_MAJOR 1 -#define API_VERSION_MINOR 12 -#define API_VERSION_PATCH 0 -#define API_VERSION_STR "1.12.0" - enum class ResponseFormat { xml, @@ -201,8 +199,8 @@ class Response std::map> _childrenArrays; }; - static Response createOkResponse(); - static Response createFailedResponse(const Error& error); + static Response createOkResponse(const RequestContext& context); + static Response createFailedResponse(std::string_view clientName, const Error& error); virtual ~Response() {} Response(const Response&) = delete; @@ -215,6 +213,8 @@ class Response Node& createArrayNode(const std::string& key); void write(std::ostream& os, ResponseFormat format); + + static unsigned getAPIMinorVersion(std::string_view clientName); private: void writeJSON(std::ostream& os); diff --git a/src/libs/utils/include/utils/String.hpp b/src/libs/utils/include/utils/String.hpp index 2c4e6724..d4655931 100644 --- a/src/libs/utils/include/utils/String.hpp +++ b/src/libs/utils/include/utils/String.hpp @@ -25,8 +25,10 @@ #include #include -namespace StringUtils { +#define QUOTEME(x) QUOTEME_1(x) +#define QUOTEME_1(x) #x +namespace StringUtils { std::vector splitString(const std::string& string, const std::string& separators);