/* * 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 "services/database/Db.hpp" #include "services/database/Session.hpp" #include "services/database/User.hpp" #include "utils/EnumSet.hpp" #include "utils/IConfig.hpp" #include "utils/Logger.hpp" #include "utils/Service.hpp" #include "utils/String.hpp" #include "utils/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" using namespace Database; namespace API::Subsonic { std::unique_ptr createSubsonicResource(Database::Db& db) { return std::make_unique(db); } namespace { std::unordered_map readConfigProtocolVersions() { std::unordered_map res; 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; } 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, EnumSet allowedUserTypes) { auto transaction{ context.dbSession.createSharedTransaction() }; User::pointer currentUser{ 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; EnumSet allowedUserTypes{ UserType::DEMO, UserType::REGULAR, 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", {handleNotImplemented}}, // 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, {UserType::ADMIN}}}, {"/createUser", {handleCreateUserRequest, {UserType::ADMIN}, &Utils::checkSetPasswordImplemented}}, {"/updateUser", {handleUpdateUserRequest, {UserType::ADMIN}}}, {"/deleteUser", {handleDeleteUserRequest, {UserType::ADMIN}}}, {"/changePassword", {handleChangePassword, {UserType::REGULAR, UserType::ADMIN}, &Utils::checkSetPasswordImplemented}}, // Bookmarks {"/getBookmarks", {handleGetBookmarks}}, {"/createBookmark", {handleCreateBookmark}}, {"/deleteBookmark", {handleDeleteBookmark}}, {"/getPlayQueue", {handleNotImplemented}}, {"/savePlayQueue", {handleNotImplemented}}, // Media library scanning {"/getScanStatus", {Scan::handleGetScanStatus, {UserType::ADMIN}}}, {"/startScan", {Scan::handleStartScan, {UserType::ADMIN}}}, }; using MediaRetrievalHandlerFunc = std::function; static std::unordered_map mediaRetrievalHandlers { // Media retrieval {"/download", handleDownload}, {"/stream", handleStream}, {"/getCoverArt", handleGetCoverArt}, }; } SubsonicResource::SubsonicResource(Db& db) : _serverProtocolVersionsByClient{ readConfigProtocolVersions() } , _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 (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()) { if (itEntryPoint->second.checkFunc) itEntryPoint->second.checkFunc(); checkUserTypeIsAllowed(requestContext, itEntryPoint->second.allowedUserTypes); Response resp{ (itEntryPoint->second.func)(requestContext) }; 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()) { 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::ParameterMap& parameters) { ClientInfo res; if (hasParameter(parameters, "t")) throw TokenAuthenticationNotSupportedForLDAPUsersError{}; // 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) }; const Database::UserId userId{ authenticateUser(request, clientInfo) }; return { parameters, _db.getTLSSession(), userId, clientInfo, getServerProtocolVersion(clientInfo.name) }; } Database::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo) { if (auto * authEnvService{ Service<::Auth::IEnvService>::get() }) { const auto checkResult{ authEnvService->processRequest(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(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