/* * 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 . */ #include "SubsonicResource.hpp" #include #include #include "services/auth/IPasswordService.hpp" #include "services/auth/IEnvService.hpp" #include "database/Db.hpp" #include "database/Session.hpp" #include "database/User.hpp" #include "core/EnumSet.hpp" #include "core/LiteralString.hpp" #include "core/IConfig.hpp" #include "core/ILogger.hpp" #include "core/ITraceLogger.hpp" #include "core/Service.hpp" #include "core/String.hpp" #include "core/Utils.hpp" #include "entrypoints/AlbumSongLists.hpp" #include "entrypoints/Browsing.hpp" #include "entrypoints/Bookmarks.hpp" #include "entrypoints/MediaAnnotation.hpp" #include "entrypoints/MediaLibraryScanning.hpp" #include "entrypoints/MediaRetrieval.hpp" #include "entrypoints/Playlists.hpp" #include "entrypoints/Searching.hpp" #include "entrypoints/System.hpp" #include "entrypoints/UserManagement.hpp" #include "ParameterParsing.hpp" #include "ProtocolVersion.hpp" #include "RequestContext.hpp" #include "SubsonicId.hpp" #include "SubsonicResponse.hpp" #include "Utils.hpp" namespace lms::api::subsonic { std::unique_ptr createSubsonicResource(db::Db& db) { return std::make_unique(db); } namespace { std::unordered_map readConfigProtocolVersions() { std::unordered_map res; core::Service::get()->visitStrings("api-subsonic-old-server-protocol-clients", [&](std::string_view client) { res.emplace(std::string{ client }, ProtocolVersion{ 1, 12, 0 }); }, { "DSub" }); return res; } std::unordered_set readOpenSubsonicDisabledClients() { std::unordered_set res; core::Service::get()->visitStrings("api-open-subsonic-disabled-clients", [&](std::string_view client) { res.emplace(std::string{ client }); }, { "DSub" }); return res; } std::unordered_set readDefaultCoverClients() { std::unordered_set res; core::Service::get()->visitStrings("api-subsonic-default-cover-clients", [&](std::string_view client) { res.emplace(std::string{ client }); }, { "DSub", "substreamer" }); return res; } std::string parameterMapToDebugString(const Wt::Http::ParameterMap& parameterMap) { auto censorValue = [](const std::string& type, const std::string& value) -> std::string { if (type == "p" || type == "password") return "*REDACTED*"; else return value; }; std::string res; for (const auto& params : parameterMap) { res += "{" + params.first + "="; if (params.second.size() == 1) { res += censorValue(params.first, params.second.front()); } else { res += "{"; for (const std::string& param : params.second) res += censorValue(params.first, param) + ","; res += "}"; } res += "}, "; } return res; } void checkUserTypeIsAllowed(RequestContext& context, core::EnumSet allowedUserTypes) { auto transaction{ context.dbSession.createReadTransaction() }; db::User::pointer currentUser{ db::User::find(context.dbSession, context.userId) }; if (!currentUser) throw RequestedDataNotFoundError{}; if (!allowedUserTypes.contains(currentUser->getType())) throw UserNotAuthorizedError{}; } Response handleNotImplemented(RequestContext&) { throw NotImplementedGenericError{}; } using RequestHandlerFunc = std::function; using CheckImplementedFunc = std::function; struct RequestEntryPointInfo { RequestHandlerFunc func; core::EnumSet allowedUserTypes{ db::UserType::DEMO, db::UserType::REGULAR, db::UserType::ADMIN }; CheckImplementedFunc checkFunc{}; }; static const std::unordered_map requestEntryPoints { // System {"/ping", {handlePingRequest}}, {"/getLicense", {handleGetLicenseRequest}}, {"/getOpenSubsonicExtensions", {handleGetOpenSubsonicExtensions}}, // Browsing {"/getMusicFolders", {handleGetMusicFoldersRequest}}, {"/getIndexes", {handleGetIndexesRequest}}, {"/getMusicDirectory", {handleGetMusicDirectoryRequest}}, {"/getGenres", {handleGetGenresRequest}}, {"/getArtists", {handleGetArtistsRequest}}, {"/getArtist", {handleGetArtistRequest}}, {"/getAlbum", {handleGetAlbumRequest}}, {"/getSong", {handleGetSongRequest}}, {"/getVideos", {handleNotImplemented}}, {"/getArtistInfo", {handleGetArtistInfoRequest}}, {"/getArtistInfo2", {handleGetArtistInfo2Request}}, {"/getAlbumInfo", {handleNotImplemented}}, {"/getAlbumInfo2", {handleNotImplemented}}, {"/getSimilarSongs", {handleGetSimilarSongsRequest}}, {"/getSimilarSongs2", {handleGetSimilarSongs2Request}}, {"/getTopSongs", {handleGetTopSongs}}, // Album/song lists {"/getAlbumList", {handleGetAlbumListRequest}}, {"/getAlbumList2", {handleGetAlbumList2Request}}, {"/getRandomSongs", {handleGetRandomSongsRequest}}, {"/getSongsByGenre", {handleGetSongsByGenreRequest}}, {"/getNowPlaying", {handleNotImplemented}}, {"/getStarred", {handleGetStarredRequest}}, {"/getStarred2", {handleGetStarred2Request}}, // Searching {"/search", {handleNotImplemented}}, {"/search2", {handleSearch2Request}}, {"/search3", {handleSearch3Request}}, // Playlists {"/getPlaylists", {handleGetPlaylistsRequest}}, {"/getPlaylist", {handleGetPlaylistRequest}}, {"/createPlaylist", {handleCreatePlaylistRequest}}, {"/updatePlaylist", {handleUpdatePlaylistRequest}}, {"/deletePlaylist", {handleDeletePlaylistRequest}}, // Media retrieval {"/hls", {handleNotImplemented}}, {"/getCaptions", {handleNotImplemented}}, {"/getLyrics", {handleNotImplemented}}, {"/getAvatar", {handleNotImplemented}}, // Media annotation {"/star", {handleStarRequest}}, {"/unstar", {handleUnstarRequest}}, {"/setRating", {handleNotImplemented}}, {"/scrobble", {handleScrobble}}, // Sharing {"/getShares", {handleNotImplemented}}, {"/createShares", {handleNotImplemented}}, {"/updateShare", {handleNotImplemented}}, {"/deleteShare", {handleNotImplemented}}, // Podcast {"/getPodcasts", {handleNotImplemented}}, {"/getNewestPodcasts", {handleNotImplemented}}, {"/refreshPodcasts", {handleNotImplemented}}, {"/createPodcastChannel", {handleNotImplemented}}, {"/deletePodcastChannel", {handleNotImplemented}}, {"/deletePodcastEpisode", {handleNotImplemented}}, {"/downloadPodcastEpisode", {handleNotImplemented}}, // Jukebox {"/jukeboxControl", {handleNotImplemented}}, // Internet radio {"/getInternetRadioStations", {handleNotImplemented}}, {"/createInternetRadioStation", {handleNotImplemented}}, {"/updateInternetRadioStation", {handleNotImplemented}}, {"/deleteInternetRadioStation", {handleNotImplemented}}, // Chat {"/getChatMessages", {handleNotImplemented}}, {"/addChatMessages", {handleNotImplemented}}, // User management {"/getUser", {handleGetUserRequest}}, {"/getUsers", {handleGetUsersRequest, {db::UserType::ADMIN}}}, {"/createUser", {handleCreateUserRequest, {db::UserType::ADMIN}, &utils::checkSetPasswordImplemented}}, {"/updateUser", {handleUpdateUserRequest, {db::UserType::ADMIN}}}, {"/deleteUser", {handleDeleteUserRequest, {db::UserType::ADMIN}}}, {"/changePassword", {handleChangePassword, {db::UserType::REGULAR, db::UserType::ADMIN}, &utils::checkSetPasswordImplemented}}, // Bookmarks {"/getBookmarks", {handleGetBookmarks}}, {"/createBookmark", {handleCreateBookmark}}, {"/deleteBookmark", {handleDeleteBookmark}}, {"/getPlayQueue", {handleNotImplemented}}, {"/savePlayQueue", {handleNotImplemented}}, // Media library scanning {"/getScanStatus", {Scan::handleGetScanStatus, {db::UserType::ADMIN}}}, {"/startScan", {Scan::handleStartScan, {db::UserType::ADMIN}}}, }; using MediaRetrievalHandlerFunc = std::function; static std::unordered_map mediaRetrievalHandlers { // Media retrieval {"/download", handleDownload}, {"/stream", handleStream}, {"/getCoverArt", handleGetCoverArt}, }; } SubsonicResource::SubsonicResource(db::Db& db) : _serverProtocolVersionsByClient{ readConfigProtocolVersions() } , _openSubsonicDisabledClients{ readOpenSubsonicDisabledClients() } , _defaultCoverClients{ readDefaultCoverClients() } , _db{ db } { } void SubsonicResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) { static std::atomic curRequestId{}; const std::size_t requestId{ curRequestId++ }; LMS_LOG(API_SUBSONIC, DEBUG, "Handling request " << requestId << " '" << request.pathInfo() << "', continuation = " << (request.continuation() ? "true" : "false") << ", params = " << parameterMapToDebugString(request.getParameterMap())); std::string requestPath{ request.pathInfo() }; if (core::stringUtils::stringEndsWith(requestPath, ".view")) requestPath.resize(requestPath.length() - 5); // Optional parameters const ResponseFormat format{ getParameterAs(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml }; ProtocolVersion protocolVersion{ defaultServerProtocolVersion }; try { // 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()) { LMS_SCOPED_TRACE_OVERVIEW("Subsonic", itEntryPoint->first); if (itEntryPoint->second.checkFunc) itEntryPoint->second.checkFunc(); checkUserTypeIsAllowed(requestContext, itEntryPoint->second.allowedUserTypes); const Response resp{ [&] { LMS_SCOPED_TRACE_DETAILED("Subsonic", "HandleRequest"); return itEntryPoint->second.func(requestContext); }()}; { LMS_SCOPED_TRACE_DETAILED("Subsonic", "WriteResponse"); resp.write(response.out(), format); response.setMimeType(std::string{ ResponseFormatToMimeType(format) }); } LMS_LOG(API_SUBSONIC, DEBUG, "Request " << requestId << " '" << requestPath << "' handled!"); return; } auto itStreamHandler{ mediaRetrievalHandlers.find(requestPath) }; if (itStreamHandler != mediaRetrievalHandlers.end()) { LMS_SCOPED_TRACE_OVERVIEW("Subsonic", itStreamHandler->first); itStreamHandler->second(requestContext, request, response); LMS_LOG(API_SUBSONIC, DEBUG, "Request " << requestId << " '" << requestPath << "' handled!"); return; } LMS_LOG(API_SUBSONIC, ERROR, "Unhandled command '" << requestPath << "'"); throw UnknownEntryPointGenericError{}; } 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(protocolVersion, e) }; resp.write(response.out(), format); response.setMimeType(std::string{ 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::Request& request) { const auto& parameters{ request.getParameterMap() }; ClientInfo res; if (hasParameter(parameters, "t")) throw TokenAuthenticationNotSupportedForLDAPUsersError{}; res.ipAddress = request.clientAddress(); // 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(request) }; const db::UserId userId{ authenticateUser(request, clientInfo) }; bool enableOpenSubsonic{ _openSubsonicDisabledClients.find(clientInfo.name) == std::cend(_openSubsonicDisabledClients) }; bool enableDefaultCover{ _defaultCoverClients.find(clientInfo.name) != std::cend(_openSubsonicDisabledClients) }; return { parameters, _db.getTLSSession(), userId, clientInfo, getServerProtocolVersion(clientInfo.name), enableOpenSubsonic, enableDefaultCover }; } db::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo) { // if the request if a continuation, the user is already authenticated if (request.continuation()) { db::Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; const auto user{ db::User::find(session, clientInfo.user) }; if (!user) throw UserNotAuthorizedError{}; return user->getId(); } if (auto * authEnvService{ core::Service::get() }) { const auto checkResult{ authEnvService->processRequest(request) }; if (checkResult.state != auth::IEnvService::CheckResult::State::Granted) throw UserNotAuthorizedError{}; return *checkResult.userId; } else if (auto * authPasswordService{ core::Service::get() }) { const auto checkResult{ authPasswordService->checkUserPassword(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 available to authenticate user" }; } } // namespace lms::api::subsonic