/* * 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 "core/EnumSet.hpp" #include "core/IConfig.hpp" #include "core/ILogger.hpp" #include "core/ITraceLogger.hpp" #include "core/LiteralString.hpp" #include "core/Service.hpp" #include "core/String.hpp" #include "core/Utils.hpp" #include "database/Db.hpp" #include "database/Session.hpp" #include "database/User.hpp" #include "services/auth/IEnvService.hpp" #include "services/auth/IPasswordService.hpp" #include "ParameterParsing.hpp" #include "ProtocolVersion.hpp" #include "RequestContext.hpp" #include "SubsonicId.hpp" #include "SubsonicResponse.hpp" #include "Utils.hpp" #include "entrypoints/AlbumSongLists.hpp" #include "entrypoints/Bookmarks.hpp" #include "entrypoints/Browsing.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" 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) { if (!allowedUserTypes.contains(context.user->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{}; }; 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", { handleNotImplemented } }, { "/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", { handleGetLyrics } }, { "/getLyricsBySongId", { handleGetLyricsBySongId } }, { "/getAvatar", { handleNotImplemented } }, // Media annotation { "/star", { handleStarRequest } }, { "/unstar", { handleUnstarRequest } }, { "/setRating", { handleSetRating } }, { "/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 } }, { "/startScan", { Scan::handleStartScan, { db::UserType::ADMIN } } }, }; using MediaRetrievalHandlerFunc = std::function; const std::unordered_map mediaRetrievalHandlers{ // Media retrieval { "/download", handleDownload }, { "/stream", handleStream }, { "/getCoverArt", handleGetCoverArt }, }; struct TLSMonotonicMemoryResourceCleaner { TLSMonotonicMemoryResourceCleaner() = default; ~TLSMonotonicMemoryResourceCleaner() { TLSMonotonicMemoryResource::getInstance().reset(); } private: TLSMonotonicMemoryResourceCleaner(const TLSMonotonicMemoryResourceCleaner&) = delete; TLSMonotonicMemoryResourceCleaner& operator=(const TLSMonotonicMemoryResourceCleaner&) = delete; }; } // namespace SubsonicResource::SubsonicResource(db::Db& db) : _serverProtocolVersionsByClient{ readConfigProtocolVersions() } , _openSubsonicDisabledClients{ readOpenSubsonicDisabledClients() } , _defaultReleaseCoverClients{ 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++ }; TLSMonotonicMemoryResourceCleaner memoryResourceCleaner; 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.contains(clientInfo.name) }; bool enableDefaultCover{ _defaultReleaseCoverClients.contains(clientInfo.name) }; const ResponseFormat format{ getParameterAs(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml }; db::User::pointer user; { db::Session& session{ _db.getTLSSession() }; auto transaction{ session.createReadTransaction() }; user = db::User::find(session, userId); if (!user) throw UserNotAuthorizedError{}; } return RequestContext{ .parameters = parameters, .dbSession = _db.getTLSSession(), .user = user, .clientInfo = clientInfo, .serverProtocolVersion = getServerProtocolVersion(clientInfo.name), .responseFormat = format, .enableOpenSubsonic = enableOpenSubsonic, .enableDefaultCover = 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