diff --git a/approot/admin-user.xml b/approot/admin-user.xml index e20f21a6..ec00334a 100644 --- a/approot/admin-user.xml +++ b/approot/admin-user.xml @@ -6,16 +6,16 @@

${title}

- ${} + ${}
-
- ${
} + ${} ${}
-
diff --git a/approot/messages.xml b/approot/messages.xml index a1962507..3f7cc85c 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -13,7 +13,10 @@ Logout Not a directory Password +Bad login / password combination +Login throttled, please try again later Confirm password +Password too weak Passwords don't match Are you sure? Another session has been open. Reopen this one? @@ -61,7 +64,7 @@ Demo account Demo account already exists! Demo password must be the login name! -Last login attempt +Last login User already exists! New user New user created! @@ -144,10 +147,5 @@ Settings New settings saved! - - - - - diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index a3b22cc4..64db088b 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -13,7 +13,10 @@ Logout N'est pas un répertoire Mot de passe +Mauvaise combinaison login / mot de passe +Trop de tentatives de connexion, veuillez réessayer plus tard Confirmation du mot de passe +Mot de passe trop faible Les mots de passe ne correspondent pas Êtes-vous sûr ? Une autre session a été ouverte. Reouvrir celle-ci ? @@ -61,7 +64,7 @@ Compte de démonstration Le compte de démonstration existe déjà ! Le password doit être égal au login ! -Dernière tentative de login +Date du dernier login L'utilisateur existe déjà ! Nouvel utilisateur Nouvel utilisateur créé ! @@ -90,7 +93,7 @@ Artistes divers -Artists similaires +Artistes similaires Ajouts récents @@ -149,17 +152,4 @@ Oui Non - - -Invalide - -Mot de passe invalide -Trop court -Trop long -Pas assez de caractères ou de classes différentes pour cette longueur de mot de passe -Pas assez de caractères ou de classes différentes -Basé sur des informations personnelles -Basé sur un mot de dictionnaire -Basé sur une séquence de caractères - diff --git a/conf/lms.conf b/conf/lms.conf index 649582e7..9d04c3c1 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -32,3 +32,6 @@ log-config = "* -debug -info:WebRequest"; # Turn on this option to allow the demo account creation/use demo = false; +# Max entries in the logging throttler (1 entry per client) +login-throttler-max-entries = 10000; + diff --git a/src/Makefile.am b/src/Makefile.am index b353bddd..ef7b2d8e 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -7,6 +7,10 @@ lms_SOURCES = \ $(srcdir)/api/subsonic/SubsonicResource.hpp \ $(srcdir)/api/subsonic/SubsonicResponse.cpp \ $(srcdir)/api/subsonic/SubsonicResponse.hpp \ + $(srcdir)/auth/AuthService.cpp \ + $(srcdir)/auth/AuthService.hpp \ + $(srcdir)/auth/LoginThrottler.cpp \ + $(srcdir)/auth/LoginThrottler.hpp \ $(srcdir)/av/AvInfo.cpp \ $(srcdir)/av/AvInfo.hpp \ $(srcdir)/av/AvTranscoder.cpp \ @@ -134,6 +138,8 @@ lms_SOURCES = \ $(srcdir)/utils/Exception.hpp \ $(srcdir)/utils/Logger.cpp \ $(srcdir)/utils/Logger.hpp \ + $(srcdir)/utils/NetAddress.cpp \ + $(srcdir)/utils/NetAddress.hpp \ $(srcdir)/utils/Path.cpp \ $(srcdir)/utils/Path.hpp \ $(srcdir)/utils/Utils.cpp \ diff --git a/src/api/subsonic/SubsonicResource.cpp b/src/api/subsonic/SubsonicResource.cpp index e1bf1215..07028201 100644 --- a/src/api/subsonic/SubsonicResource.cpp +++ b/src/api/subsonic/SubsonicResource.cpp @@ -24,9 +24,9 @@ #include #include -#include #include +#include "auth/AuthService.hpp" #include "av/AvTranscoder.hpp" #include "cover/CoverArtGrabber.hpp" #include "database/Artist.hpp" @@ -42,6 +42,8 @@ #include "SubsonicId.hpp" #include "SubsonicResponse.hpp" +using namespace Database; + static const std::string genreClusterName {"GENRE"}; // Files are always reported to be in the same format static const std::size_t reportedBitrate {128}; @@ -115,33 +117,41 @@ struct ClientInfo struct RequestContext { const Wt::Http::ParameterMap& parameters; - Database::Session& dbSession; + Session& dbSession; std::string userName; }; -// TODO handle multiple databases -static thread_local std::map> dbSessions; +using SessionMap = std::map>; +static std::map dbSessions; static -Database::Session& +Session& getOrCreateDbSession(Database::Database& db) { static std::mutex mutex; - std::unique_lock lock {mutex}; + SessionMap* sessionMap {}; - auto it {dbSessions.find(std::this_thread::get_id())}; - if (it != dbSessions.end()) + { + std::unique_lock lock {mutex}; + sessionMap = &dbSessions[std::this_thread::get_id()]; + } + + auto it {sessionMap->find(&db)}; + if (it != std::end(*sessionMap)) return *it->second; - auto res {dbSessions.emplace(std::this_thread::get_id(), db.createSession())}; + auto res {sessionMap->emplace(&db, db.createSession())}; assert(res.second); + + LMS_LOG(API_SUBSONIC, DEBUG) << "Created db session"; + return *(res.first->second); } static void -cleanDbSessions() +clearDbSessions() { dbSessions.clear(); } @@ -326,7 +336,7 @@ SubsonicResource::SubsonicResource(Database::Database& db) SubsonicResource::~SubsonicResource() { LMS_LOG(API_SUBSONIC, DEBUG) << "Cleaning db sessions..."; - cleanDbSessions(); + clearDbSessions(); } std::vector @@ -379,7 +389,7 @@ std::string parameterMapToDebugString(const Wt::Http::ParameterMap& parameterMap void SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) { - static std::atomic curRequestId{}; + static std::atomic curRequestId {}; const std::size_t requestId {curRequestId++}; @@ -392,12 +402,21 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp try { - Database::Session& dbSession {getOrCreateDbSession(_db)}; + Session& dbSession {getOrCreateDbSession(_db)}; const ClientInfo clientInfo {getClientInfo(parameters)}; - if (!dbSession.checkUserPassword(clientInfo.user, clientInfo.password)) - throw Error {Error::Code::WrongUsernameOrPassword}; + switch (getService()->checkUserPassword(dbSession, + boost::asio::ip::address::from_string(request.clientAddress()), + clientInfo.user, clientInfo.password)) + { + case Auth::AuthService::PasswordCheckResult::Match: + break; + case Auth::AuthService::PasswordCheckResult::Mismatch: + throw Error {Error::Code::WrongUsernameOrPassword}; + case Auth::AuthService::PasswordCheckResult::Throttled: + throw Error {Error::CustomType::LoginThrottled}; + } RequestContext requestContext {.parameters = parameters, .dbSession = dbSession, .userName = clientInfo.user}; @@ -455,7 +474,7 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp static std::string -getArtistNames(const std::vector& artists) +getArtistNames(const std::vector& artists) { if (artists.size() == 1) return artists.front()->getName(); @@ -464,7 +483,7 @@ getArtistNames(const std::vector& artists) names.resize(artists.size()); std::transform(std::cbegin(artists), std::cend(artists), std::begin(names), - [](const Database::Artist::pointer& artist) + [](const Artist::pointer& artist) { return artist->getName(); }); @@ -474,7 +493,7 @@ getArtistNames(const std::vector& artists) static std::string -getTrackPath(const Database::Track::pointer& track) +getTrackPath(const Track::pointer& track) { std::string path; @@ -503,7 +522,7 @@ getTrackPath(const Database::Track::pointer& track) static Response::Node -trackToResponseNode(const Database::Track::pointer& track, Database::Session& dbSession, const Database::User::pointer& user) +trackToResponseNode(const Track::pointer& track, Session& dbSession, const User::pointer& user) { Response::Node trackResponse; @@ -547,7 +566,7 @@ trackToResponseNode(const Database::Track::pointer& track, Database::Session& db trackResponse.setAttribute("starred", reportedStarredDate); // Report the first GENRE for this track - Database::ClusterType::pointer clusterType {Database::ClusterType::getByName(dbSession, genreClusterName)}; + ClusterType::pointer clusterType {ClusterType::getByName(dbSession, genreClusterName)}; if (clusterType) { auto clusters {track->getClusterGroups({clusterType}, 1)}; @@ -560,7 +579,7 @@ trackToResponseNode(const Database::Track::pointer& track, Database::Session& db static Response::Node -releaseToResponseNode(const Database::Release::pointer& release, Database::Session& dbSession, const Database::User::pointer& user, bool id3) +releaseToResponseNode(const Release::pointer& release, Session& dbSession, const User::pointer& user, bool id3) { Response::Node albumNode; @@ -613,7 +632,7 @@ releaseToResponseNode(const Database::Release::pointer& release, Database::Sessi if (id3) { // Report the first GENRE for this track - Database::ClusterType::pointer clusterType {Database::ClusterType::getByName(dbSession, genreClusterName)}; + ClusterType::pointer clusterType {ClusterType::getByName(dbSession, genreClusterName)}; if (clusterType) { auto clusters {release->getClusterGroups({clusterType}, 1)}; @@ -630,7 +649,7 @@ releaseToResponseNode(const Database::Release::pointer& release, Database::Sessi static Response::Node -artistToResponseNode(const Database::User::pointer& user, const Database::Artist::pointer& artist, bool id3) +artistToResponseNode(const User::pointer& user, const Artist::pointer& artist, bool id3) { Response::Node artistNode; @@ -648,7 +667,7 @@ artistToResponseNode(const Database::User::pointer& user, const Database::Artist static Response::Node -clusterToResponseNode(const Database::Cluster::pointer& cluster) +clusterToResponseNode(const Cluster::pointer& cluster) { Response::Node clusterNode; @@ -683,19 +702,19 @@ handleCreatePlaylistRequest(RequestContext& context) if (!name && !id) throw Error {Error::Code::RequiredParameterMissing}; - auto transaction {context.dbSession.createSharedTransaction()}; + auto transaction {context.dbSession.createUniqueTransaction()}; - Database::User::pointer user {context.dbSession.getUser(context.userName)}; + User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; - Database::TrackList::pointer tracklist; + TrackList::pointer tracklist; if (id) { - tracklist = Database::TrackList::getById(context.dbSession, id->value); + tracklist = TrackList::getById(context.dbSession, id->value); if (!tracklist || tracklist->getUser() != user - || tracklist->getType() != Database::TrackList::Type::Playlist) + || tracklist->getType() != TrackList::Type::Playlist) { throw Error {Error::Code::RequestedDataNotFound}; } @@ -705,16 +724,16 @@ handleCreatePlaylistRequest(RequestContext& context) } else { - tracklist = Database::TrackList::create(context.dbSession, *name, Database::TrackList::Type::Playlist, false, user); + tracklist = TrackList::create(context.dbSession, *name, TrackList::Type::Playlist, false, user); } for (const Id& trackId : trackIds) { - Database::Track::pointer track {Database::Track::getById(context.dbSession, trackId.value)}; + Track::pointer track {Track::getById(context.dbSession, trackId.value)}; if (!track) continue; - Database::TrackListEntry::create(context.dbSession, track, tracklist ); + TrackListEntry::create(context.dbSession, track, tracklist ); } return Response::createOkResponse(); @@ -730,14 +749,14 @@ handleDeletePlaylistRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - Database::User::pointer user {context.dbSession.getUser(context.userName)}; + User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; - Database::TrackList::pointer tracklist {Database::TrackList::getById(context.dbSession, id.value)}; + TrackList::pointer tracklist {TrackList::getById(context.dbSession, id.value)}; if (!tracklist || tracklist->getUser() != user - || tracklist->getType() != Database::TrackList::Type::Playlist) + || tracklist->getType() != TrackList::Type::Playlist) { throw Error {Error::Code::RequestedDataNotFound}; } @@ -769,53 +788,21 @@ handleGetRandomSongsRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - Database::User::pointer user {context.dbSession.getUser(context.userName)}; + User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; - auto tracks {Database::Track::getAllRandom(context.dbSession, size)}; + auto tracks {Track::getAllRandom(context.dbSession, size)}; Response response {Response::createOkResponse()}; Response::Node& randomSongsNode {response.createNode("randomSongs")}; - for (const Database::Track::pointer& track : tracks) + for (const Track::pointer& track : tracks) randomSongsNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); return response; } -static -std::vector getRandomAlbums(Database::Session& dbSession, std::size_t offset, std::size_t size) -{ - std::vector res; - - std::size_t nbReleases {Database::Release::getCount(dbSession)}; - if (offset > nbReleases) - return res; - - if (offset + size > nbReleases) - size = nbReleases - offset; - - std::vector indexes; - indexes.resize(nbReleases); - std::iota(std::begin(indexes), std::end(indexes), 1); - - // As random results are paginated, we need to set a seed for it - std::seed_seq seed {1337}; - std::mt19937 generator{seed}; - - std::shuffle(std::begin(indexes), std::end(indexes), generator); - std::for_each(std::next(std::begin(indexes), offset), std::next(std::begin(indexes), offset + size), - [&](std::size_t offset) - { - auto release {Database::Release::getAll(dbSession, offset, 1)}; - if (!release.empty()) - res.emplace_back(release.front()); - }); - - return res; -} - static Response handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) @@ -827,40 +814,41 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) std::size_t size {getParameterAs(context.parameters, "size").get_value_or(10)}; std::size_t offset {getParameterAs(context.parameters, "offset").get_value_or(0)}; - std::vector releases; + std::vector releases; auto transaction {context.dbSession.createSharedTransaction()}; - Database::User::pointer user {context.dbSession.getUser(context.userName)}; + User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; if (type == "random") { - releases = getRandomAlbums(context.dbSession, offset, size); + // Random results are paginated, but there is no acceptable way to handle the pagination params without repeating some albums + releases = Release::getAllRandom(context.dbSession, size); } else if (type == "newest") { auto after {Wt::WLocalDateTime::currentServerDateTime().toUTC().addMonths(-6)}; - releases = Database::Release::getLastAdded(context.dbSession, after, offset, size); + releases = Release::getLastAdded(context.dbSession, after, offset, size); } else if (type == "alphabeticalByName") { - releases = Database::Release::getAll(context.dbSession, offset, size); + releases = Release::getAll(context.dbSession, offset, size); } else if (type == "byGenre") { // Mandatory param std::string genre {getMandatoryParameterAs(context.parameters, "genre")}; - Database::ClusterType::pointer clusterType {Database::ClusterType::getByName(context.dbSession, genreClusterName)}; + ClusterType::pointer clusterType {ClusterType::getByName(context.dbSession, genreClusterName)}; if (clusterType) { - Database::Cluster::pointer cluster {clusterType->getCluster(genre)}; + Cluster::pointer cluster {clusterType->getCluster(genre)}; if (cluster) { bool more; - releases = Database::Release::getByFilter(context.dbSession, {cluster.id()}, {}, offset, size, more); + releases = Release::getByFilter(context.dbSession, {cluster.id()}, {}, offset, size, more); } } } @@ -870,7 +858,7 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) Response response {Response::createOkResponse()}; Response::Node& albumListNode {response.createNode(id3 ? "albumList2" : "albumList")}; - for (const Database::Release::pointer& release : releases) + for (const Release::pointer& release : releases) albumListNode.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3)); return response; @@ -899,11 +887,11 @@ handleGetAlbumRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - Database::Release::pointer release {Database::Release::getById(context.dbSession, id.value)}; + Release::pointer release {Release::getById(context.dbSession, id.value)}; if (!release) throw Error {Error::Code::RequestedDataNotFound}; - Database::User::pointer user {context.dbSession.getUser(context.userName)}; + User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; @@ -911,7 +899,7 @@ handleGetAlbumRequest(RequestContext& context) Response::Node releaseNode {releaseToResponseNode(release, context.dbSession, user, true /* id3 */)}; auto tracks {release->getTracks()}; - for (const Database::Track::pointer& track : tracks) + for (const Track::pointer& track : tracks) releaseNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); response.addNode("album", std::move(releaseNode)); @@ -930,8 +918,8 @@ handleGetArtistRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - Database::Artist::pointer artist {Database::Artist::getById(context.dbSession, id.value)}; - Database::User::pointer user {context.dbSession.getUser(context.userName)}; + Artist::pointer artist {Artist::getById(context.dbSession, id.value)}; + User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!artist || !user) throw Error {Error::Code::RequestedDataNotFound}; @@ -940,7 +928,7 @@ handleGetArtistRequest(RequestContext& context) Response::Node artistNode {artistToResponseNode(user, artist, true /* id3 */)}; auto releases {artist->getReleases()}; - for (const Database::Release::pointer& release : releases) + for (const Release::pointer& release : releases) artistNode.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, true /* id3 */)); response.addNode("artist", std::move(artistNode)); @@ -960,27 +948,35 @@ Response handleGetArtistInfoRequestCommon(RequestContext& context, bool id3) // Optional params std::size_t count {getParameterAs(context.parameters, "count").get_value_or(10)}; - auto transaction {context.dbSession.createSharedTransaction()}; - - Database::Artist::pointer artist {Database::Artist::getById(context.dbSession, id.value)}; - Database::User::pointer user {context.dbSession.getUser(context.userName)}; - - if (!artist || !user) - throw Error {Error::Code::RequestedDataNotFound}; - Response response {Response::createOkResponse()}; Response::Node& artistInfoNode {response.createNode(id3 ? "artistInfo2" : "artistInfo")}; - if (!artist->getMBID().empty()) - artistInfoNode.createChild("musicBrainzId").setValue(artist->getMBID()); - - auto similarArtistsId {getService()->getSimilarArtists(context.dbSession, artist.id(), count)}; - for ( const auto& similarArtistId : similarArtistsId ) { - Database::Artist::pointer similarArtist {Database::Artist::getById(context.dbSession, similarArtistId)}; + auto transaction {context.dbSession.createSharedTransaction()}; - if (similarArtist) - artistInfoNode.addArrayChild("similarArtist", artistToResponseNode(user, similarArtist, id3)); + Artist::pointer artist {Artist::getById(context.dbSession, id.value)}; + if (!artist) + throw Error {Error::Code::RequestedDataNotFound}; + + if (!artist->getMBID().empty()) + artistInfoNode.createChild("musicBrainzId").setValue(artist->getMBID()); + } + + auto similarArtistsId {getService()->getSimilarArtists(context.dbSession, id.value, count)}; + + { + auto transaction {context.dbSession.createSharedTransaction()}; + + User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + if (!user) + throw Error {Error::Code::RequestedDataNotFound}; + + for ( const auto& similarArtistId : similarArtistsId ) + { + Artist::pointer similarArtist {Artist::getById(context.dbSession, similarArtistId)}; + if (similarArtist) + artistInfoNode.addArrayChild("similarArtist", artistToResponseNode(user, similarArtist, id3)); + } } return response; @@ -1006,12 +1002,12 @@ handleGetArtistsRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - Database::User::pointer user {context.dbSession.getUser(context.userName)}; + User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; - auto artists {Database::Artist::getAll(context.dbSession)}; - for (const Database::Artist::pointer& artist : artists) + auto artists {Artist::getAll(context.dbSession)}; + for (const Artist::pointer& artist : artists) indexNode.addArrayChild("artist", artistToResponseNode(user, artist, true /* id3 */)); return response; @@ -1031,7 +1027,7 @@ handleGetMusicDirectoryRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - Database::User::pointer user {context.dbSession.getUser(context.userName)}; + User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; @@ -1041,8 +1037,8 @@ handleGetMusicDirectoryRequest(RequestContext& context) { directoryNode.setAttribute("name", "Music"); - auto artists {Database::Artist::getAll(context.dbSession)}; - for (const Database::Artist::pointer& artist : artists) + auto artists {Artist::getAll(context.dbSession)}; + for (const Artist::pointer& artist : artists) directoryNode.addArrayChild("child", artistToResponseNode(user, artist, false /* no id3 */)); break; @@ -1050,14 +1046,14 @@ handleGetMusicDirectoryRequest(RequestContext& context) case Id::Type::Artist: { - auto artist {Database::Artist::getById(context.dbSession, id.value)}; + auto artist {Artist::getById(context.dbSession, id.value)}; if (!artist) throw Error {Error::Code::RequestedDataNotFound}; directoryNode.setAttribute("name", makeNameFilesystemCompatible(artist->getName())); auto releases {artist->getReleases()}; - for (const Database::Release::pointer& release : releases) + for (const Release::pointer& release : releases) directoryNode.addArrayChild("child", releaseToResponseNode(release, context.dbSession, user, false /* no id3 */)); break; @@ -1065,14 +1061,14 @@ handleGetMusicDirectoryRequest(RequestContext& context) case Id::Type::Release: { - auto release {Database::Release::getById(context.dbSession, id.value)}; + auto release {Release::getById(context.dbSession, id.value)}; if (!release) throw Error {Error::Code::RequestedDataNotFound}; directoryNode.setAttribute("name", makeNameFilesystemCompatible(release->getName())); auto tracks {release->getTracks()}; - for (const Database::Track::pointer& track : tracks) + for (const Track::pointer& track : tracks) directoryNode.addArrayChild("child", trackToResponseNode(track, context.dbSession, user)); break; @@ -1107,12 +1103,12 @@ handleGetGenresRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - auto clusterType {Database::ClusterType::getByName(context.dbSession, genreClusterName)}; + auto clusterType {ClusterType::getByName(context.dbSession, genreClusterName)}; if (clusterType) { auto clusters {clusterType->getClusters()}; - for (const Database::Cluster::pointer& cluster : clusters) + for (const Cluster::pointer& cluster : clusters) genresNode.addArrayChild("genre", clusterToResponseNode(cluster)); } @@ -1130,12 +1126,12 @@ handleGetIndexesRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - Database::User::pointer user {context.dbSession.getUser(context.userName)}; + User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; - auto artists {Database::Artist::getAll(context.dbSession)}; - for (const Database::Artist::pointer& artist : artists) + auto artists {Artist::getAll(context.dbSession)}; + for (const Artist::pointer& artist : artists) indexNode.addArrayChild("artist", artistToResponseNode(user, artist, false /* no id3 */)); return response; @@ -1154,8 +1150,8 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3) auto transaction {context.dbSession.createSharedTransaction()}; - Database::Artist::pointer artist {Database::Artist::getById(context.dbSession, id.value)}; - Database::User::pointer user {context.dbSession.getUser(context.userName)}; + Artist::pointer artist {Artist::getById(context.dbSession, id.value)}; + User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user || !artist) throw Error {Error::Code::RequestedDataNotFound}; @@ -1166,7 +1162,7 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3) auto similarArtistsId {getService()->getSimilarArtists(context.dbSession, artist.id(), 5)}; for ( const auto& similarArtistId : similarArtistsId ) { - Database::Artist::pointer similarArtist {Database::Artist::getById(context.dbSession, similarArtistId)}; + Artist::pointer similarArtist {Artist::getById(context.dbSession, similarArtistId)}; if (!similarArtist) continue; @@ -1183,7 +1179,7 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3) Response response {Response::createOkResponse()}; Response::Node& similarSongsNode {response.createNode(id3 ? "similarSongs2" : "similarSongs")}; - for (const Database::Track::pointer& track : tracks) + for (const Track::pointer& track : tracks) similarSongsNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); return response; @@ -1208,7 +1204,7 @@ handleGetStarredRequestCommon(RequestContext& context, bool id3) { auto transaction {context.dbSession.createSharedTransaction()}; - Database::User::pointer user {context.dbSession.getUser(context.userName)}; + User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; @@ -1217,19 +1213,19 @@ handleGetStarredRequestCommon(RequestContext& context, bool id3) { auto artists {user->getStarredArtists()}; - for (const Database::Artist::pointer& artist : artists) + for (const Artist::pointer& artist : artists) starredNode.addArrayChild("artist", artistToResponseNode(user, artist, id3)); } { auto releases {user->getStarredReleases()}; - for (const Database::Release::pointer& release : releases) + for (const Release::pointer& release : releases) starredNode.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3)); } { auto tracks {user->getStarredTracks()}; - for (const Database::Track::pointer& track : tracks) + for (const Track::pointer& track : tracks) starredNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); } @@ -1250,7 +1246,7 @@ handleGetStarred2Request(RequestContext& context) } Response::Node -tracklistToResponseNode(const Database::TrackList::pointer& tracklist, Database::Session& dbSession) +tracklistToResponseNode(const TrackList::pointer& tracklist, Session& dbSession) { Response::Node playlistNode; @@ -1260,7 +1256,7 @@ tracklistToResponseNode(const Database::TrackList::pointer& tracklist, Database: playlistNode.setAttribute("duration", std::to_string(std::chrono::duration_cast(tracklist->getDuration()).count())); playlistNode.setAttribute("public", tracklist->isPublic() ? "true" : "false"); playlistNode.setAttribute("created", ""); - playlistNode.setAttribute("owner", dbSession.getUserLoginName(tracklist->getUser())); + playlistNode.setAttribute("owner", tracklist->getUser()->getLoginName()); return playlistNode; } @@ -1275,8 +1271,8 @@ handleGetPlaylistRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - Database::User::pointer user {context.dbSession.getUser(context.userName)}; - Database::TrackList::pointer tracklist {Database::TrackList::getById(context.dbSession, id.value)}; + User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + TrackList::pointer tracklist {TrackList::getById(context.dbSession, id.value)}; if (!user || !tracklist) throw Error {Error::Code::RequestedDataNotFound}; @@ -1284,7 +1280,7 @@ handleGetPlaylistRequest(RequestContext& context) Response::Node playlistNode {tracklistToResponseNode(tracklist, context.dbSession)}; auto entries {tracklist->getEntries()}; - for (const Database::TrackListEntry::pointer& entry : entries) + for (const TrackListEntry::pointer& entry : entries) playlistNode.addArrayChild("entry", trackToResponseNode(entry->getTrack(), context.dbSession, user)); response.addNode("playlist", playlistNode ); @@ -1297,15 +1293,15 @@ handleGetPlaylistsRequest(RequestContext& context) { auto transaction {context.dbSession.createSharedTransaction()}; - Database::User::pointer user {context.dbSession.getUser(context.userName)}; + User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; Response response {Response::createOkResponse()}; Response::Node& playlistsNode {response.createNode("playlists")}; - auto tracklists {Database::TrackList::getAll(context.dbSession, user, Database::TrackList::Type::Playlist)}; - for (const Database::TrackList::pointer& tracklist : tracklists) + auto tracklists {TrackList::getAll(context.dbSession, user, TrackList::Type::Playlist)}; + for (const TrackList::pointer& tracklist : tracklists) playlistsNode.addArrayChild("playlist", tracklistToResponseNode(tracklist, context.dbSession)); return response; @@ -1325,7 +1321,7 @@ handleGetSongsByGenreRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - auto clusterType {Database::ClusterType::getByName(context.dbSession, genreClusterName)}; + auto clusterType {ClusterType::getByName(context.dbSession, genreClusterName)}; if (!clusterType) throw Error {Error::Code::RequestedDataNotFound}; @@ -1333,7 +1329,7 @@ handleGetSongsByGenreRequest(RequestContext& context) if (!cluster) throw Error {Error::Code::RequestedDataNotFound}; - Database::User::pointer user {context.dbSession.getUser(context.userName)}; + User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; @@ -1341,8 +1337,8 @@ handleGetSongsByGenreRequest(RequestContext& context) Response::Node& songsByGenreNode {response.createNode("songsByGenre")}; bool more; - auto tracks {Database::Track::getByFilter(context.dbSession, {cluster.id()}, {}, offset, size, more)}; - for (const Database::Track::pointer& track : tracks) + auto tracks {Track::getByFilter(context.dbSession, {cluster.id()}, {}, offset, size, more)}; + for (const Track::pointer& track : tracks) songsByGenreNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); return response; @@ -1367,7 +1363,7 @@ handleSearchRequestCommon(RequestContext& context, bool id3) auto transaction {context.dbSession.createSharedTransaction()}; - Database::User::pointer user {context.dbSession.getUser(context.userName)}; + User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; @@ -1376,20 +1372,20 @@ handleSearchRequestCommon(RequestContext& context, bool id3) bool more; { - auto artists {Database::Artist::getByFilter(context.dbSession, {}, keywords, artistOffset, artistCount, more)}; - for (const Database::Artist::pointer& artist : artists) + auto artists {Artist::getByFilter(context.dbSession, {}, keywords, artistOffset, artistCount, more)}; + for (const Artist::pointer& artist : artists) searchResult2Node.addArrayChild("artist", artistToResponseNode(user, artist, id3)); } { - auto releases {Database::Release::getByFilter(context.dbSession, {}, keywords, albumOffset, albumCount, more)}; - for (const Database::Release::pointer& release : releases) + auto releases {Release::getByFilter(context.dbSession, {}, keywords, albumOffset, albumCount, more)}; + for (const Release::pointer& release : releases) searchResult2Node.addArrayChild("album", releaseToResponseNode(release, context.dbSession, user, id3)); } { - auto tracks {Database::Track::getByFilter(context.dbSession, {}, keywords, songOffset, songCount, more)}; - for (const Database::Track::pointer& track : tracks) + auto tracks {Track::getByFilter(context.dbSession, {}, keywords, songOffset, songCount, more)}; + for (const Track::pointer& track : tracks) searchResult2Node.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); } @@ -1448,13 +1444,13 @@ handleStarRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - Database::User::pointer user {context.dbSession.getUser(context.userName)}; + User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; for (const Id& id : params.artistIds) { - Database::Artist::pointer artist {Database::Artist::getById(context.dbSession, id.value)}; + Artist::pointer artist {Artist::getById(context.dbSession, id.value)}; if (!artist) continue; @@ -1463,7 +1459,7 @@ handleStarRequest(RequestContext& context) for (const Id& id : params.releaseIds) { - Database::Release::pointer release {Database::Release::getById(context.dbSession, id.value)}; + Release::pointer release {Release::getById(context.dbSession, id.value)}; if (!release) continue; @@ -1472,7 +1468,7 @@ handleStarRequest(RequestContext& context) for (const Id& id : params.trackIds) { - Database::Track::pointer track {Database::Track::getById(context.dbSession, id.value)}; + Track::pointer track {Track::getById(context.dbSession, id.value)}; if (!track) continue; @@ -1501,13 +1497,13 @@ handleUnstarRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - Database::User::pointer user {context.dbSession.getUser(context.userName)}; + User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; for (const Id& id : params.artistIds) { - Database::Artist::pointer artist {Database::Artist::getById(context.dbSession, id.value)}; + Artist::pointer artist {Artist::getById(context.dbSession, id.value)}; if (!artist) continue; @@ -1516,7 +1512,7 @@ handleUnstarRequest(RequestContext& context) for (const Id& id : params.releaseIds) { - Database::Release::pointer release {Database::Release::getById(context.dbSession, id.value)}; + Release::pointer release {Release::getById(context.dbSession, id.value)}; if (!release) continue; @@ -1525,7 +1521,7 @@ handleUnstarRequest(RequestContext& context) for (const Id& id : params.trackIds) { - Database::Track::pointer track {Database::Track::getById(context.dbSession, id.value)}; + Track::pointer track {Track::getById(context.dbSession, id.value)}; if (!track) continue; @@ -1556,14 +1552,14 @@ handleUpdatePlaylistRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - Database::User::pointer user {context.dbSession.getUser(context.userName)}; + User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; - Database::TrackList::pointer tracklist {Database::TrackList::getById(context.dbSession, id.value)}; + TrackList::pointer tracklist {TrackList::getById(context.dbSession, id.value)}; if (!tracklist || tracklist->getUser() != user - || tracklist->getType() != Database::TrackList::Type::Playlist) + || tracklist->getType() != TrackList::Type::Playlist) { throw Error {Error::Code::RequestedDataNotFound}; } @@ -1589,11 +1585,11 @@ handleUpdatePlaylistRequest(RequestContext& context) // Add tracks for (const Id& trackIdToAdd : trackIdsToAdd) { - Database::Track::pointer track {Database::Track::getById(context.dbSession, trackIdToAdd.value)}; + Track::pointer track {Track::getById(context.dbSession, trackIdToAdd.value)}; if (!track) continue; - Database::TrackListEntry::create(context.dbSession, track, tracklist ); + TrackListEntry::create(context.dbSession, track, tracklist ); } return Response::createOkResponse(); @@ -1613,7 +1609,7 @@ createTranscoder(RequestContext& context) { auto transaction {context.dbSession.createSharedTransaction()}; - Database::User::pointer user {context.dbSession.getUser(context.userName)}; + User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; if (!user) throw Error {Error::Code::RequestedDataNotFound}; @@ -1623,7 +1619,7 @@ createTranscoder(RequestContext& context) *maxBitRate = clamp(*maxBitRate, std::size_t {48}, user->getMaxAudioTranscodeBitrate() / 1000); - auto track {Database::Track::getById(context.dbSession, id.value)}; + auto track {Track::getById(context.dbSession, id.value)}; if (!track) throw Error {Error::Code::RequestedDataNotFound}; diff --git a/src/api/subsonic/SubsonicResponse.cpp b/src/api/subsonic/SubsonicResponse.cpp index ceb93e04..58873dd5 100644 --- a/src/api/subsonic/SubsonicResponse.cpp +++ b/src/api/subsonic/SubsonicResponse.cpp @@ -52,6 +52,8 @@ ErrorCodeToString(Error::Code error) { switch (error) { + case Error::Code::Generic: + return "Generic Error."; case Error::Code::RequiredParameterMissing: return "Required parameter is missing."; case Error::Code::ClientMustUpgrade: @@ -64,9 +66,9 @@ ErrorCodeToString(Error::Code error) return "User is not authorized for the given operation."; case Error::Code::RequestedDataNotFound: return "The requested data was not found."; - default: - return "Unknown error"; } + + return "Unknown error"; } static @@ -81,9 +83,11 @@ CustomTypeToString(Error::CustomType type) return "Not implemented"; case Error::CustomType::InternalError: return "Internal error"; - default: - return "Unknown custom error"; + case Error::CustomType::LoginThrottled: + return "Login throttled, too many attempts"; } + + return "Unknown custom error"; } Error::Error(Code code) diff --git a/src/api/subsonic/SubsonicResponse.hpp b/src/api/subsonic/SubsonicResponse.hpp index 23da6648..2112ee34 100644 --- a/src/api/subsonic/SubsonicResponse.hpp +++ b/src/api/subsonic/SubsonicResponse.hpp @@ -50,6 +50,7 @@ class Error { BadId, NotImplemented, + LoginThrottled, InternalError, }; diff --git a/src/auth/AuthService.cpp b/src/auth/AuthService.cpp new file mode 100644 index 00000000..e88d68f9 --- /dev/null +++ b/src/auth/AuthService.cpp @@ -0,0 +1,105 @@ +/* + * 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 . + */ + +/* This file contains some classes in order to get info from file using the libavconv */ + +#include "AuthService.hpp" + +#include +#include +#include +#include + +#include "database/Session.hpp" +#include "utils/Utils.hpp" +#include "utils/Logger.hpp" + +namespace Auth { + +AuthService::AuthService(std::size_t maxThrottlerEntries) +: _loginThrottler {maxThrottlerEntries} +{ +} + +AuthService::PasswordCheckResult +AuthService::checkUserPassword(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& loginName, const std::string& password) +{ + if (_loginThrottler.isClientThrottled(clientAddress)) + return PasswordCheckResult::Throttled; + + Database::User::PasswordHash passwordHash; + { + auto transaction {session.createSharedTransaction()}; + + const Database::User::pointer user {Database::User::getByLoginName(session, loginName)}; + if (!user) + { + _loginThrottler.onBadClientAttempt(clientAddress); + return PasswordCheckResult::Mismatch; + } + + passwordHash = user->getPasswordHash(); + } + + const Wt::Auth::BCryptHashFunction hashFunc {6}; + if (hashFunc.verify(password, passwordHash.salt, passwordHash.hash)) + { + _loginThrottler.onGoodClientAttempt(clientAddress); + return PasswordCheckResult::Match; + } + else + { + _loginThrottler.onBadClientAttempt(clientAddress); + return PasswordCheckResult::Mismatch; + } +} + +Database::User::PasswordHash +AuthService::hashPassword(const std::string& password) const +{ + std::array buffer; + fillRandom(buffer); + + std::ostringstream oss; + for (std::uint8_t b : buffer) + oss << std::hex << std::setfill('0') << std::setw(2) << static_cast(b); + + const std::string salt {Wt::Utils::base64Encode(oss.str(), false)}; + + const Wt::Auth::BCryptHashFunction hashFunc {6}; + return {salt, hashFunc.compute(password, salt)}; +} + +bool +AuthService::evaluatePasswordStrength(const std::string& loginName, const std::string& password) const +{ + Wt::Auth::PasswordStrengthValidator validator; + validator.setMinimumLength(Wt::Auth::PasswordStrengthType::OneCharClass, 4); + validator.setMinimumLength(Wt::Auth::PasswordStrengthType::TwoCharClass, 4); + validator.setMinimumLength(Wt::Auth::PasswordStrengthType::PassPhrase, 4); + validator.setMinimumLength(Wt::Auth::PasswordStrengthType::ThreeCharClass, 4); + validator.setMinimumLength(Wt::Auth::PasswordStrengthType::FourCharClass, 4); + validator.setMinimumPassPhraseWords(1); + validator.setMinimumMatchLength(3); + + return validator.evaluateStrength(password, loginName, "").isValid(); +} + +} // namespace Auth + diff --git a/src/auth/AuthService.hpp b/src/auth/AuthService.hpp new file mode 100644 index 00000000..7aecea54 --- /dev/null +++ b/src/auth/AuthService.hpp @@ -0,0 +1,70 @@ +/* + * 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 . + */ + +/* This file contains some classes in order to get info from file using the libavconv */ + +#pragma once + +#include + +#include + +#include "LoginThrottler.hpp" +#include "database/User.hpp" + +namespace Database +{ + class Session; +} + + +namespace Auth { + + class AuthService + { + public: + + AuthService(std::size_t maxThrottlerEntries); + + AuthService() = default; + ~AuthService() = default; + + AuthService(const AuthService&) = delete; + AuthService& operator=(const AuthService&) = delete; + AuthService(AuthService&&) = delete; + AuthService& operator=(AuthService&&) = delete; + + + // Password services + enum class PasswordCheckResult + { + Match, + Mismatch, + Throttled, + }; + PasswordCheckResult checkUserPassword(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& loginName, const std::string& password); + Database::User::PasswordHash hashPassword(const std::string& password) const; + bool evaluatePasswordStrength(const std::string& loginName, const std::string& password) const; + + private: + + LoginThrottler _loginThrottler; + }; + +} diff --git a/src/auth/LoginThrottler.cpp b/src/auth/LoginThrottler.cpp new file mode 100644 index 00000000..55087e5e --- /dev/null +++ b/src/auth/LoginThrottler.cpp @@ -0,0 +1,124 @@ +/* + * 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 . + */ + +/* This file contains some classes in order to get info from file using the libavconv */ + +#include "LoginThrottler.hpp" + +#include "utils/Logger.hpp" +#include "utils/Utils.hpp" + +namespace Auth { + +static +boost::asio::ip::address_v6 +getAddressWithMask(const boost::asio::ip::address_v6& address, std::size_t prefix) +{ + assert(prefix % 8 == 0); + + std::array truncatedBytes; + + auto bytes {address.to_bytes()}; + std::copy(std::cbegin(bytes), std::next(std::cbegin(bytes), prefix / 8), truncatedBytes.begin()); + + return boost::asio::ip::address_v6 {truncatedBytes}; +} + +static +boost::asio::ip::address +getAddressToThrottle(const boost::asio::ip::address& address) +{ + return address.is_v6() ? getAddressWithMask(address.to_v6(), 64) : address; +} + +void +LoginThrottler::removeOutdatedEntries() +{ + const Wt::WDateTime now {Wt::WDateTime::currentDateTime()}; + + for (auto it {std::begin(_attemptsInfo)}; it != std::end(_attemptsInfo); ) + { + if (it->second.nextValidAttempt <= now) + it = _attemptsInfo.erase(it); + else + ++it; + } +} + +void +LoginThrottler::onBadClientAttempt(const boost::asio::ip::address& address) +{ + const boost::asio::ip::address clientAddress {getAddressToThrottle(address)}; + + const Wt::WDateTime now {Wt::WDateTime::currentDateTime()}; + + std::unique_lock lock {_mutex}; + + if (_attemptsInfo.size() >= _maxEntries) + removeOutdatedEntries(); + // If still full, kill one random entry + if (_attemptsInfo.size() >= _maxEntries) + _attemptsInfo.erase(pickRandom(_attemptsInfo)); + + AttemptsInfo& attemptsInfo {_attemptsInfo[address]}; + + attemptsInfo.nbSuccessiveBadAttempts++; + + if (attemptsInfo.nbSuccessiveBadAttempts >= 50) + attemptsInfo.nextValidAttempt = now.addSecs(60); + if (attemptsInfo.nbSuccessiveBadAttempts >= 20) + attemptsInfo.nextValidAttempt = now.addSecs(10); + else if (attemptsInfo.nbSuccessiveBadAttempts >= 10) + attemptsInfo.nextValidAttempt = now.addSecs(5); + else if (attemptsInfo.nbSuccessiveBadAttempts >= 5) + attemptsInfo.nextValidAttempt = now.addSecs(2); + else if (attemptsInfo.nbSuccessiveBadAttempts >= 2) + attemptsInfo.nextValidAttempt = now.addSecs(1); + else + attemptsInfo.nextValidAttempt = now; + + LMS_LOG(AUTH, INFO) << "Registering bad attempt for '" << clientAddress.to_string() << "' (" << attemptsInfo.nbSuccessiveBadAttempts << " successive bad attempts)"; +} + +void +LoginThrottler::onGoodClientAttempt(const boost::asio::ip::address& address) +{ + const boost::asio::ip::address clientAddress {getAddressToThrottle(address)}; + + std::unique_lock lock {_mutex}; + + _attemptsInfo.erase(address); +} + +bool +LoginThrottler::isClientThrottled(const boost::asio::ip::address& address) const +{ + const boost::asio::ip::address clientAddress {getAddressToThrottle(address)}; + + std::shared_lock lock {_mutex}; + + auto it {_attemptsInfo.find(address)}; + if (it == _attemptsInfo.end()) + return false; + + return it->second.nextValidAttempt > Wt::WDateTime::currentDateTime(); +} + +} // Auth + diff --git a/src/auth/LoginThrottler.hpp b/src/auth/LoginThrottler.hpp new file mode 100644 index 00000000..ea2ca433 --- /dev/null +++ b/src/auth/LoginThrottler.hpp @@ -0,0 +1,62 @@ +/* + * 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 . + */ + +/* This file contains some classes in order to get info from file using the libavconv */ + +#pragma once + +#include +#include +#include + +#include + +#include "utils/NetAddress.hpp" +#include "utils/Exception.hpp" + +namespace Auth { + +class LoginThrottler +{ + public: + LoginThrottler(std::size_t maxEntries) : _maxEntries {maxEntries} {} + + bool isClientThrottled(const boost::asio::ip::address& address) const; + void onBadClientAttempt(const boost::asio::ip::address& address); + void onGoodClientAttempt(const boost::asio::ip::address& address); + + private: + + void removeOutdatedEntries(); + + struct AttemptsInfo + { + std::size_t nbSuccessiveBadAttempts {}; + Wt::WDateTime nextValidAttempt; + }; + + const std::size_t _maxEntries; + + mutable std::shared_timed_mutex _mutex; + std::unordered_map _attemptsInfo; +}; + + +} // Auth + diff --git a/src/database/Session.cpp b/src/database/Session.cpp index 3c18507f..73426482 100644 --- a/src/database/Session.cpp +++ b/src/database/Session.cpp @@ -19,15 +19,6 @@ #include "Session.hpp" -#include -#include -#include -#include -#include -#include -#include -#include - #include "utils/Exception.hpp" #include "utils/Logger.hpp" @@ -44,12 +35,7 @@ namespace Database { -#define LMS_DATABASE_VERSION 4 - -namespace { - Wt::Auth::AuthService authService; - Wt::Auth::PasswordService passwordService {authService}; -} +#define LMS_DATABASE_VERSION 5 using Version = std::size_t; @@ -112,36 +98,6 @@ Session::doDatabaseMigrationIfNeeded() switch (version) { - case 3: - - LMS_LOG(DB, INFO) << "Migrating database from version 3..."; - - _session.execute(R"(CREATE TABLE IF NOT EXISTS "user_artist_starred" ( - "user_id" bigint, - "artist_id" bigint, - primary key ("user_id", "artist_id"), - constraint "fk_user_artist_starred_key1" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred, - constraint "fk_user_artist_starred_key2" foreign key ("artist_id") references "artist" ("id") deferrable initially deferred);)"); - _session.execute(R"(CREATE INDEX "user_artist_starred_user" on "user_artist_starred" ("user_id");)"); - _session.execute(R"(CREATE INDEX "user_artist_starred_artist" on "user_artist_starred" ("artist_id");)"); - _session.execute(R"(CREATE TABLE IF NOT EXISTS "user_release_starred" ( - "user_id" bigint, - "release_id" bigint, - primary key ("user_id", "release_id"), - constraint "fk_user_release_starred_key1" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred, - constraint "fk_user_release_starred_key2" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred);)"); - _session.execute(R"(CREATE INDEX "user_release_starred_user" on "user_release_starred" ("user_id");)"); - _session.execute(R"(CREATE INDEX "user_release_starred_release" on "user_release_starred" ("release_id");)"); - _session.execute(R"(CREATE TABLE IF NOT EXISTS "user_track_starred" ( - "user_id" bigint, - "track_id" bigint, - primary key ("user_id", "track_id"), - constraint "fk_user_track_starred_key1" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred, - constraint "fk_user_track_starred_key2" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred);)"); - _session.execute(R"(CREATE INDEX "user_track_starred_user" on "user_track_starred" ("user_id");)"); - _session.execute(R"(CREATE INDEX "user_track_starred_track" on "user_track_starred" ("track_id");)"); - break; - default: LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration"; throw LmsException {outdatedMsg}; @@ -150,51 +106,6 @@ Session::doDatabaseMigrationIfNeeded() VersionInfo::get(*this).modify()->setVersion(LMS_DATABASE_VERSION); } - -void -Session::configureAuth(void) -{ - authService.setEmailVerificationEnabled(false); - authService.setAuthTokensEnabled(true, "lmsauth"); - authService.setAuthTokenValidity(24 * 60 * 365); // A year - authService.setIdentityPolicy(Wt::Auth::IdentityPolicy::LoginName); - authService.setRandomTokenLength(32); - -#if WT_VERSION < 0X04000300 - authService.setTokenHashFunction(new Wt::Auth::BCryptHashFunction(8)); -#else - authService.setTokenHashFunction(std::make_unique(8)); -#endif - - auto verifier = std::make_unique(); - verifier->addHashFunction(std::make_unique(8)); - passwordService.setVerifier(std::move(verifier)); - passwordService.setAttemptThrottlingEnabled(true); - - auto strengthValidator = std::make_unique(); - // Reduce some constraints... - strengthValidator->setMinimumLength( Wt::Auth::PasswordStrengthType::PassPhrase, 4); - strengthValidator->setMinimumLength( Wt::Auth::PasswordStrengthType::OneCharClass, 4); - strengthValidator->setMinimumLength( Wt::Auth::PasswordStrengthType::TwoCharClass, 4); - strengthValidator->setMinimumLength( Wt::Auth::PasswordStrengthType::ThreeCharClass, 4 ); - strengthValidator->setMinimumLength( Wt::Auth::PasswordStrengthType::FourCharClass, 4 ); - - passwordService.setStrengthValidator(std::move(strengthValidator)); -} - -const Wt::Auth::AuthService& -Session::getAuthService() -{ - return authService; -} - -const Wt::Auth::PasswordService& -Session::getPasswordService() -{ - return passwordService; -} - - Session::Session(std::shared_timed_mutex& mutex, Wt::Dbo::SqlConnectionPool& connectionPool) : _mutex {mutex} { @@ -202,75 +113,69 @@ Session::Session(std::shared_timed_mutex& mutex, Wt::Dbo::SqlConnectionPool& con _session.mapClass("version_info"); _session.mapClass("artist"); + _session.mapClass("auth_token"); _session.mapClass("cluster"); _session.mapClass("cluster_type"); - _session.mapClass("tracklist"); - _session.mapClass("tracklist_entry"); _session.mapClass("release"); - _session.mapClass("track"); - _session.mapClass("track_artist_link"); - _session.mapClass("track_features"); - _session.mapClass("scan_settings"); _session.mapClass("similarity_settings"); _session.mapClass("similarity_settings_feature"); - - _session.mapClass("auth_info"); - _session.mapClass("auth_identity"); - _session.mapClass("auth_token"); + _session.mapClass("track"); + _session.mapClass("track_artist_link"); + _session.mapClass("track_features"); + _session.mapClass("tracklist"); + _session.mapClass("tracklist_entry"); _session.mapClass("user"); - - _users = std::make_unique(_session); } -// TODO make this per database -static thread_local bool hasSharedLock {false}; -static thread_local bool hasUniqueLock {false}; + +enum class OwnedLock +{ + None, + Shared, + Unique, +}; + +static thread_local std::map lockDebug; UniqueTransaction::UniqueTransaction(std::shared_timed_mutex& mutex, Wt::Dbo::Session& session) : _lock {mutex}, _transaction {session} { - assert(!hasSharedLock); - assert(!hasUniqueLock); - hasUniqueLock = true; - LMS_LOG(DB, DEBUG) << "UniqueTransaction ACQUIRED"; + assert(lockDebug[_lock.mutex()] == OwnedLock::None); + lockDebug[_lock.mutex()] = OwnedLock::Unique; } UniqueTransaction::~UniqueTransaction() { - assert(hasUniqueLock); - hasUniqueLock = false; - LMS_LOG(DB, DEBUG) << "UniqueTransaction RELEASED"; + assert(lockDebug[_lock.mutex()] == OwnedLock::Unique); + lockDebug[_lock.mutex()] = OwnedLock::None; } SharedTransaction::SharedTransaction(std::shared_timed_mutex& mutex, Wt::Dbo::Session& session) : _lock {mutex}, _transaction {session} { - assert(!hasSharedLock); - assert(!hasUniqueLock); - hasSharedLock = true; - LMS_LOG(DB, DEBUG) << "SharedTransaction ACQUIRED"; + assert(lockDebug[_lock.mutex()] == OwnedLock::None); + lockDebug[_lock.mutex()] = OwnedLock::Shared; } SharedTransaction::~SharedTransaction() { - assert(hasSharedLock); - hasSharedLock = false; - LMS_LOG(DB, DEBUG) << "SharedTransaction RELEASED"; + assert(lockDebug[_lock.mutex()] == OwnedLock::Shared); + lockDebug[_lock.mutex()] = OwnedLock::None; } void Session::checkUniqueLocked() { - assert(hasUniqueLock); + assert(lockDebug[&_mutex] == OwnedLock::Unique); } void Session::checkSharedLocked() { - assert(hasUniqueLock || hasSharedLock); + assert(lockDebug[&_mutex] != OwnedLock::None); } std::unique_ptr @@ -307,6 +212,7 @@ Session::prepareTables() _session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)"); _session.execute("CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)"); _session.execute("CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)"); + _session.execute("CREATE INDEX IF NOT EXISTS auth_token_user_idx ON auth_token(user_id)"); _session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)"); _session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)"); _session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)"); @@ -346,112 +252,4 @@ Session::optimize() _session.execute("ANALYZE"); } -std::string -Session::getUserLoginName(Wt::Dbo::ptr user) -{ - const Wt::Auth::User authUser {_users->findWithId(std::to_string(user.id()))}; - if (!authUser.isValid()) - throw LmsException {"Invalid user state"}; - - return authUser.identity(Wt::Auth::Identity::LoginName).toUTF8(); -} - -bool -Session::checkUserPassword(const std::string& loginName, const std::string& password) -{ - auto transaction {createUniqueTransaction()}; - - auto authUser {_users->findWithIdentity(Wt::Auth::Identity::LoginName, loginName)}; - if (!authUser.isValid()) - return false; // TODO const time? - - return passwordService.verifyPassword(authUser, password) == Wt::Auth::PasswordResult::PasswordValid; -} - -void -Session::updateUserPassword(Wt::Dbo::ptr user, const std::string& password) -{ - const Wt::Auth::User authUser {_users->findWithId(std::to_string(user.id()))}; - if (!authUser.isValid()) - throw LmsException {"Bad user state"}; - passwordService.updatePassword(authUser, password); -} - -Wt::WDateTime -Session::getUserLastLoginAttempt(Wt::Dbo::ptr user) -{ - const Wt::Auth::User authUser {_users->findWithId(std::to_string(user.id()))}; - if (!authUser.isValid()) - throw LmsException {"Bad user state"}; - - return authUser.lastLoginAttempt(); -} - -void -Session::removeUser(Database::User::pointer user) -{ - checkUniqueLocked(); - - auto authUser = _users->findWithId(std::to_string(user.id())); - _users->deleteUser(authUser); - user.remove(); -} - -Wt::Auth::AbstractUserDatabase& -Session::getUserDatabase() -{ - return *_users; -} - -User::pointer -Session::createUser(const std::string& loginName, const std::string& password) -{ - Wt::Auth::User authUser {_users->registerNew()}; - if (!authUser.isValid()) - { - LMS_LOG(DB, ERROR) << "Invalid authUser"; - return {}; - } - User::pointer user {User::create(*this)}; - Wt::Dbo::ptr authInfo = _users->find(authUser); - authInfo.modify()->setUser(user); - - authUser.setIdentity(Wt::Auth::Identity::LoginName, loginName); - passwordService.updatePassword(authUser, password); - - return user; -} - -User::pointer -Session::getLoggedUser() -{ - if (_login.loggedIn()) - return getUser(_login.user()); - else - return User::pointer(); -} - -User::pointer -Session::getUser(const Wt::Auth::User& authUser) -{ - if (!authUser.isValid()) { - LMS_LOG(DB, ERROR) << "Session::getUser: invalid authUser"; - return User::pointer(); - } - - Wt::Dbo::ptr authInfo = _users->find(authUser); - - return authInfo->user(); -} - -User::pointer -Session::getUser(const std::string& loginName) -{ - auto authUser {getUserDatabase().findWithIdentity(Wt::Auth::Identity::LoginName, loginName)}; - if (!authUser.isValid()) - return User::pointer {}; - - return getUser(authUser); -} - } // namespace Database diff --git a/src/database/Session.hpp b/src/database/Session.hpp index fe144d0a..1b983395 100644 --- a/src/database/Session.hpp +++ b/src/database/Session.hpp @@ -26,19 +26,9 @@ #include #include -#include - -#include -#include -#include - -#include "User.hpp" namespace Database { -using AuthInfo = Wt::Auth::Dbo::AuthInfo; -using UserDatabase = Wt::Auth::Dbo::UserDatabase; - class UniqueTransaction { public: @@ -81,24 +71,6 @@ class Session void optimize(); - // User management - Wt::Dbo::ptr getLoggedUser(); // get the current user, may return empty - Wt::Dbo::ptr getUser(const std::string& loginName); - std::string getUserLoginName(Wt::Dbo::ptr user); - Wt::Dbo::ptr createUser(const std::string& loginName, const std::string& password); - void removeUser(Wt::Dbo::ptr user); - bool checkUserPassword(const std::string& loginName, const std::string& password); - void updateUserPassword(Wt::Dbo::ptr user, const std::string& password); - Wt::WDateTime getUserLastLoginAttempt(Wt::Dbo::ptr user); - - Wt::Auth::AbstractUserDatabase& getUserDatabase(); - Wt::Auth::Login& getLogin() { return _login; } // TODO move - - // Long living shared associated services - static void configureAuth(); - static const Wt::Auth::AuthService& getAuthService(); - static const Wt::Auth::PasswordService& getPasswordService(); - Wt::Dbo::Session& getDboSession() { return _session; } private: @@ -109,13 +81,8 @@ class Session void doDatabaseMigrationIfNeeded(); void prepareTables(); // need to run only once at startup - Wt::Dbo::ptr getUser(const Wt::Auth::User& authUser); - std::shared_timed_mutex& _mutex; Wt::Dbo::Session _session; - std::unique_ptr _users; - Wt::Auth::Login _login; - }; } // namespace Database diff --git a/src/database/User.cpp b/src/database/User.cpp index 9f5aa28b..fd35a866 100644 --- a/src/database/User.cpp +++ b/src/database/User.cpp @@ -19,6 +19,7 @@ #include "User.hpp" +#include "utils/Logger.hpp" #include "Artist.hpp" #include "Release.hpp" #include "Session.hpp" @@ -27,6 +28,36 @@ namespace Database { + +AuthToken::AuthToken(const std::string& value, const Wt::WDateTime& expiry, Wt::Dbo::ptr user) +: _value {value} +, _expiry {expiry} +, _user {user} +{ + +} + +AuthToken::pointer +AuthToken::create(Session& session, const std::string& value, const Wt::WDateTime& expiry, Wt::Dbo::ptr user) +{ + session.checkUniqueLocked(); + + auto res {session.getDboSession().add(std::make_unique(value, expiry, user))}; + + session.getDboSession().flush(); + + return res; +} + +AuthToken::pointer +AuthToken::getByValue(Session& session, const std::string& value) +{ + session.checkSharedLocked(); + + return session.getDboSession().find() + .where("value = ?").bind(value); +} + static const std::string playedListName {"__played_tracks__"}; static const std::string queuedListName {"__queued_tracks__"}; @@ -46,6 +77,14 @@ User::User() } +User::User(const std::string& loginName, const PasswordHash& passwordHash) +: User() +{ + _loginName = loginName; + _passwordHash = passwordHash.hash; + _passwordSalt = passwordHash.salt; +} + std::vector User::getAll(Session& session) { @@ -65,11 +104,11 @@ User::getDemo(Session& session) } User::pointer -User::create(Session& session) +User::create(Session& session, const std::string& loginName, const PasswordHash& passwordHash) { session.checkUniqueLocked(); - User::pointer user {session.getDboSession().add(std::make_unique())}; + User::pointer user {session.getDboSession().add(std::make_unique(loginName, passwordHash))}; TrackList::create(session, playedListName, TrackList::Type::Internal, false, user); TrackList::create(session, queuedListName, TrackList::Type::Internal, false, user); @@ -85,6 +124,13 @@ User::getById(Session& session, IdType id) return session.getDboSession().find().where("id = ?").bind( id ); } +User::pointer +User::getByLoginName(Session& session, const std::string& name) +{ + return session.getDboSession().find() + .where("login_name = ?").bind(name); +} + void User::setAudioTranscodeBitrate(Bitrate bitrate) { @@ -99,6 +145,12 @@ User::setMaxAudioTranscodeBitrate(Bitrate bitrate) _audioTranscodeBitrate = _maxAudioTranscodeBitrate; } +void +User::clearAuthTokens() +{ + _authTokens.clear(); +} + Bitrate User::getAudioTranscodeBitrate(void) const { diff --git a/src/database/User.hpp b/src/database/User.hpp index 26121296..a0376078 100644 --- a/src/database/User.hpp +++ b/src/database/User.hpp @@ -22,6 +22,7 @@ #include #include +#include #include "Types.hpp" @@ -46,6 +47,41 @@ enum class AudioFormat using Bitrate = std::size_t; +class User; +class AuthToken +{ + public: + + using pointer = Wt::Dbo::ptr; + + AuthToken() = default; + AuthToken(const std::string& value, const Wt::WDateTime& expiry, Wt::Dbo::ptr user); + + // Utility + static pointer create(Session& session, const std::string& value, const Wt::WDateTime&expiry, Wt::Dbo::ptr user); + static void removeExpiredTokens(Session& session, Wt::WDateTime now); + static pointer getByValue(Session& session, const std::string& value); + + // Accessors + Wt::Dbo::ptr getUser() const { return _user; } + const Wt::WDateTime& getExpiry() const { return _expiry; } + + template + void persist(Action& a) + { + Wt::Dbo::field(a, _value, "value"); + Wt::Dbo::field(a, _expiry, "expiry"); + Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); + } + + private: + + std::string _value; + Wt::WDateTime _expiry; + + Wt::Dbo::ptr _user; +}; + class User : public Wt::Dbo::Dbo { public: @@ -61,21 +97,34 @@ class User : public Wt::Dbo::Dbo DEMO }; + struct PasswordHash + { + std::string salt; + std::string hash; + }; + // list of audio parameters static const std::set audioTranscodeAllowedBitrates; User(); + User(const std::string& loginName, const PasswordHash& passwordHash); // utility - static pointer create(Session& session); + static pointer create(Session& session, const std::string& loginName, const PasswordHash& passwordHash); - // accessors static pointer getById(Session& session, IdType id); - static pointer getByLoginName(const std::string& loginName); + static pointer getByLoginName(Session& session, const std::string& loginName); static std::vector getAll(Session& session); static pointer getDemo(Session& session); + // accessors + const std::string& getLoginName() const { return _loginName; } + PasswordHash getPasswordHash() const { return PasswordHash {_passwordSalt, _passwordHash}; } + Wt::WDateTime getLastLogin() const { return _lastLogin; } + // write + void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; } + void setPasswordHash(const PasswordHash& passwordHash) { _passwordSalt = passwordHash.salt; _passwordHash = passwordHash.hash; } void setType(Type type) { _type = type; } void setAudioTranscodeEnable(bool value) { _audioTranscodeEnable = value; } void setAudioTranscodeFormat(AudioFormat format) { _audioTranscodeFormat = format; } @@ -84,6 +133,7 @@ class User : public Wt::Dbo::Dbo void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; } void setRadio(bool val) { _radio = val; } void setRepeatAll(bool val) { _repeatAll = val; } + void clearAuthTokens(); // read bool isAdmin() const { return _type == Type::ADMIN; } @@ -115,22 +165,27 @@ class User : public Wt::Dbo::Dbo std::vector> getStarredTracks() const; template - void persist(Action& a) - { - Wt::Dbo::field(a, _type, "type"); - Wt::Dbo::field(a, _maxAudioTranscodeBitrate, "max_audio_bitrate"); - Wt::Dbo::field(a, _audioTranscodeEnable, "audio_transcode_enable"); - Wt::Dbo::field(a, _audioTranscodeBitrate, "audio_transcode_bitrate"); - Wt::Dbo::field(a, _audioTranscodeFormat, "audio_transcode_format"); - // User's dynamic data - Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos"); - Wt::Dbo::field(a, _repeatAll, "repeat_all"); - Wt::Dbo::field(a, _radio, "radio"); - Wt::Dbo::hasMany(a, _tracklists, Wt::Dbo::ManyToOne, "user"); - Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_artist_starred", "", Wt::Dbo::OnDeleteCascade); - Wt::Dbo::hasMany(a, _starredReleases, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade); - Wt::Dbo::hasMany(a, _starredTracks, Wt::Dbo::ManyToMany, "user_track_starred", "", Wt::Dbo::OnDeleteCascade); - } + void persist(Action& a) + { + Wt::Dbo::field(a, _type, "type"); + Wt::Dbo::field(a, _loginName, "login_name"); + Wt::Dbo::field(a, _passwordSalt, "password_salt"); + Wt::Dbo::field(a, _passwordHash, "password_hash"); + Wt::Dbo::field(a, _lastLogin, "last_login"); + Wt::Dbo::field(a, _maxAudioTranscodeBitrate, "max_audio_bitrate"); + Wt::Dbo::field(a, _audioTranscodeEnable, "audio_transcode_enable"); + Wt::Dbo::field(a, _audioTranscodeBitrate, "audio_transcode_bitrate"); + Wt::Dbo::field(a, _audioTranscodeFormat, "audio_transcode_format"); + // User's dynamic data + Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos"); + Wt::Dbo::field(a, _repeatAll, "repeat_all"); + Wt::Dbo::field(a, _radio, "radio"); + Wt::Dbo::hasMany(a, _tracklists, Wt::Dbo::ManyToOne, "user"); + Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_artist_starred", "", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::hasMany(a, _starredReleases, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::hasMany(a, _starredTracks, Wt::Dbo::ManyToMany, "user_track_starred", "", Wt::Dbo::OnDeleteCascade); + Wt::Dbo::hasMany(a, _authTokens, Wt::Dbo::ManyToOne, "user"); + } private: @@ -138,16 +193,21 @@ class User : public Wt::Dbo::Dbo static const AudioFormat defaultAudioTranscodeFormat {AudioFormat::OGG_OPUS}; static const Bitrate defaultAudioTranscodeBitrate {128000}; + std::string _loginName; + std::string _passwordSalt; + std::string _passwordHash; + Wt::WDateTime _lastLogin; + // Admin defined settings int _maxAudioTranscodeBitrate; Type _type {Type::REGULAR}; // User defined settings - bool _audioTranscodeEnable {defaultAudioTranscodeEnable}; - AudioFormat _audioTranscodeFormat {defaultAudioTranscodeFormat}; - int _audioTranscodeBitrate {defaultAudioTranscodeBitrate}; + bool _audioTranscodeEnable {defaultAudioTranscodeEnable}; + AudioFormat _audioTranscodeFormat {defaultAudioTranscodeFormat}; + int _audioTranscodeBitrate {defaultAudioTranscodeBitrate}; - // User's dynamic data + // User's dynamic data (UI) int _curPlayingTrackPos {}; // Current track position in queue bool _repeatAll {}; bool _radio {}; @@ -156,6 +216,7 @@ class User : public Wt::Dbo::Dbo Wt::Dbo::collection> _starredArtists; Wt::Dbo::collection> _starredReleases; Wt::Dbo::collection> _starredTracks; + Wt::Dbo::collection> _authTokens; }; diff --git a/src/main/main.cpp b/src/main/main.cpp index 1e3ab455..e2aa0298 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -26,6 +26,7 @@ #include "api/subsonic/SubsonicResource.hpp" #include "av/AvInfo.hpp" #include "av/AvTranscoder.hpp" +#include "auth/AuthService.hpp" #include "cover/CoverArtGrabber.hpp" #include "image/Image.hpp" #include "scanner/MediaScanner.hpp" @@ -121,7 +122,6 @@ int main(int argc, char* argv[]) Image::init(argv[0]); Av::AvInit(); Av::Transcoder::init(); - Database::Session::configureAuth(); // Initializing a connection pool to the database that will be shared along services Database::Database database {Config::instance().getPath("working-dir") / "lms.db"}; @@ -129,6 +129,7 @@ int main(int argc, char* argv[]) UserInterface::LmsApplicationGroupContainer appGroups; // Service initialization order is important + ServiceProvider::create(Config::instance().getULong("login-throttler-max-entriees", 10000)); Scanner::MediaScanner& mediaScanner {ServiceProvider::create(database.createSession())}; Similarity::FeaturesScannerAddon similarityFeaturesScannerAddon {database.createSession()}; diff --git a/src/scanner/MediaScanner.cpp b/src/scanner/MediaScanner.cpp index 3f141517..d165c2d0 100644 --- a/src/scanner/MediaScanner.cpp +++ b/src/scanner/MediaScanner.cpp @@ -408,11 +408,10 @@ MediaScanner::scan(boost::system::error_code err) scanMediaDirectory(_mediaDirectory, forceScan, stats); LMS_LOG(DBUPDATER, INFO) << "scaning media directory '" << _mediaDirectory.string() << "' DONE"; + removeOrphanEntries(); + if (_running) - { - removeOrphanEntries(); checkDuplicatedAudioFiles(stats); - } LMS_LOG(DBUPDATER, INFO) << "Scan " << (_running ? "complete" : "aborted") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.scanErrors << ", not imported = " << stats.incompleteScans << "), duplicates = " << stats.nbDuplicates() << " (hash = " << stats.duplicateHashes << ", mbid = " << stats.duplicateMBID << ")"; diff --git a/src/similarity/cluster/SimilarityClusterSearcher.cpp b/src/similarity/cluster/SimilarityClusterSearcher.cpp index 40eee04c..bc48f500 100644 --- a/src/similarity/cluster/SimilarityClusterSearcher.cpp +++ b/src/similarity/cluster/SimilarityClusterSearcher.cpp @@ -167,6 +167,9 @@ getSimilarArtists(Database::Session& dbSession, Database::IdType artistId, std:: continue; res.push_back(trackArtist.id()); + + if (res.size() == maxCount) + break; } if (res.size() == maxCount) diff --git a/src/ui/Auth.cpp b/src/ui/Auth.cpp index b3ea96a2..754a9768 100644 --- a/src/ui/Auth.cpp +++ b/src/ui/Auth.cpp @@ -19,82 +19,227 @@ #include "Auth.hpp" +#include + #include #include #include #include +#include "auth/AuthService.hpp" +#include "main/Service.hpp" #include "utils/Logger.hpp" +#include "utils/Utils.hpp" #include "common/Validators.hpp" #include "LmsApplication.hpp" namespace UserInterface { -Auth::Auth() -: Wt::WTemplateFormView(Wt::WString::tr("Lms.Auth.template")) -{ - _model = std::make_shared(LmsApp->getDbSession().getAuthService(), LmsApp->getDbSession().getUserDatabase()); - _model->addPasswordAuth(&Database::Session::getPasswordService()); +static const std::string authCookieName {"LmsAuth"}; - // LoginName - setFormWidget(Wt::Auth::AuthModel::LoginNameField, std::make_unique()); +static +std::string +createSecret() +{ + std::array buffer; + fillRandom(buffer); + + std::ostringstream oss; + for (std::uint8_t b : buffer) + oss << std::hex << std::setfill('0') << std::setw(2) << static_cast(b); + + return oss.str(); +} + + +static +void +createAuthToken(Database::IdType userId) +{ + + const std::string secret {createSecret()}; + const Wt::WDateTime now {Wt::WDateTime::currentDateTime()}; + const Wt::WDateTime expiry {now.addYears(1)}; { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - auto demoUser = Database::User::getDemo(LmsApp->getDbSession()); - if (demoUser) - { - const std::string userName {LmsApp->getDbSession().getUserLoginName(demoUser)}; - _model->setValue(Wt::Auth::AuthModel::LoginNameField, userName ); - _model->setValue(Wt::Auth::AuthModel::PasswordField, userName); - } + Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), userId)}; + Database::AuthToken::create(LmsApp->getDbSession(), secret, expiry, user); + + LMS_LOG(UI, DEBUG) << "Created auth token for user '" << user->getLoginName() << "', expiry = " << expiry.toString(); } + LmsApp->setCookie(authCookieName, + secret, + expiry.toTime_t() - now.toTime_t(), + "", + "", + LmsApp->environment().urlScheme() == "https"); +} + + +boost::optional +processAuthToken(const Wt::WEnvironment& env) +{ + const std::string* authCookie {env.getCookie(authCookieName)}; + if (!authCookie) + return boost::none; + + Database::IdType userId {}; + { + auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; + + Database::AuthToken::pointer authToken {Database::AuthToken::getByValue(LmsApp->getDbSession(), *authCookie)}; + if (!authToken) + { + LMS_LOG(UI, INFO) << "Client '" << env.clientAddress() << "' presented a token that has not been found"; + return boost::none; + } + + if (authToken->getExpiry() < Wt::WDateTime::currentDateTime()) + { + LMS_LOG(UI, INFO) << "Expired auth token for user '" << authToken->getUser()->getLoginName() << "'!"; + authToken.remove(); + return boost::none; + } + + LMS_LOG(UI, DEBUG) << "Found auth token for user '" << authToken->getUser()->getLoginName() << "'!"; + userId = authToken->getUser().id(); + + authToken.remove(); + } + + createAuthToken(userId); + + return userId; +} + +class AuthModel : public Wt::WFormModel +{ + public: + + // Associate each field with a unique string literal. + static const Field LoginNameField; + static const Field PasswordField; + static const Field RememberMeField; + + AuthModel() + { + addField(LoginNameField); + addField(PasswordField); + addField(RememberMeField); + + setValidator(LoginNameField, createNameValidator()); + setValidator(PasswordField, createMandatoryValidator()); + } + + + void saveData() + { + { + auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; + + Database::User::pointer user {Database::User::getByLoginName(LmsApp->getDbSession(), valueText(LoginNameField).toUTF8())}; + user.modify()->setLastLogin(Wt::WDateTime::currentDateTime()); + _userId = user.id(); + } + + if (Wt::asNumber(value(RememberMeField))) + createAuthToken(*_userId); + } + + bool validateField(Field field) + { + Wt::WString error; + + if (field == PasswordField) + { + switch (getService<::Auth::AuthService>()->checkUserPassword( + LmsApp->getDbSession(), + boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()), + valueText(LoginNameField).toUTF8(), + valueText(PasswordField).toUTF8())) + { + case ::Auth::AuthService::PasswordCheckResult::Match: + break; + case ::Auth::AuthService::PasswordCheckResult::Mismatch: + error = Wt::WString::tr("Lms.password-bad-login-combination"); + break; + case ::Auth::AuthService::PasswordCheckResult::Throttled: + error = Wt::WString::tr("Lms.password-client-throttled"); + break; + } + } + else + { + return Wt::WFormModel::validateField(field); + } + + setValidation(field, Wt::WValidator::Result( error.empty() ? Wt::ValidationState::Valid : Wt::ValidationState::Invalid, error)); + + return (validation(field).state() == Wt::ValidationState::Valid); + } + + boost::optional getUserId() const { return _userId; } + + private: + + boost::optional _userId; +}; + +const AuthModel::Field AuthModel::LoginNameField {"login-name"}; +const AuthModel::Field AuthModel::PasswordField {"password"}; +const AuthModel::Field AuthModel::RememberMeField {"remember-me"}; + + +Auth::Auth() +: Wt::WTemplateFormView {Wt::WString::tr("Lms.Auth.template")} +{ + auto model {std::make_shared()}; + + auto processAuth = [=]() + { + updateModel(model.get()); + + if (model->validate()) + { + model->saveData(); + userLoggedIn.emit(*model->getUserId()); + } + else + updateView(model.get()); + }; + + // LoginName + setFormWidget(AuthModel::LoginNameField, std::make_unique()); + // Password auto password = std::make_unique(); password->setEchoMode(Wt::EchoMode::Password); - password->enterPressed().connect(this, &Auth::processAuth); - setFormWidget(Wt::Auth::AuthModel::PasswordField, std::move(password)); + password->enterPressed().connect(this, processAuth); + setFormWidget(AuthModel::PasswordField, std::move(password)); - // Remember Me - setFormWidget(Wt::Auth::AuthModel::RememberMeField, std::make_unique()); + // Remember me + setFormWidget(AuthModel::RememberMeField, std::make_unique()); - Wt::WPushButton* loginBtn = bindNew("login-btn", Wt::WString::tr("Lms.login")); - loginBtn->clicked().connect(this, &Auth::processAuth); - - LmsApp->getDbSession().getLogin().changed().connect(std::bind([=] { - if (LmsApp->getDbSession().getLogin().loggedIn()) - this->setHidden(true); - })); + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - updateView(_model.get()); - - Wt::Auth::User user = _model->processAuthToken(); - if (user.isValid()) - { - LMS_LOG(UI, DEBUG) << "Valid user found from auth token (id = " << user.id() << ")"; - _model->loginUser(LmsApp->getDbSession().getLogin(), user, Wt::Auth::LoginState::Weak); + Database::User::pointer demoUser {Database::User::getDemo(LmsApp->getDbSession())}; + if (demoUser) + { + model->setValue(AuthModel::LoginNameField, demoUser->getLoginName()); + model->setValue(AuthModel::PasswordField, demoUser->getLoginName()); + } } -} -void -Auth::processAuth() -{ - updateModel(_model.get()); + Wt::WPushButton* loginBtn {bindNew("login-btn", Wt::WString::tr("Lms.login"))}; + loginBtn->clicked().connect(this, processAuth); - if (_model->validate()) - _model->login(LmsApp->getDbSession().getLogin()); - else - updateView(_model.get()); -} + updateView(model.get()); -void -Auth::logout() -{ - _model->logout(LmsApp->getDbSession().getLogin()); } } // namespace UserInterface diff --git a/src/ui/Auth.hpp b/src/ui/Auth.hpp index 07df5557..1d94e2f4 100644 --- a/src/ui/Auth.hpp +++ b/src/ui/Auth.hpp @@ -19,22 +19,24 @@ #pragma once +#include + #include -#include + +#include "database/Types.hpp" namespace UserInterface { + +// If success, returns the authenticated user id +boost::optional processAuthToken(const Wt::WEnvironment& env); + class Auth : public Wt::WTemplateFormView { public: Auth(); - void logout(); - - private: - void processAuth(); - - std::shared_ptr _model; + Wt::Signal userLoggedIn; }; diff --git a/src/ui/LmsApplication.cpp b/src/ui/LmsApplication.cpp index 381a2a9b..80d27985 100644 --- a/src/ui/LmsApplication.cpp +++ b/src/ui/LmsApplication.cpp @@ -28,13 +28,13 @@ #include #include #include -#include #include "config/config.h" #include "cover/CoverArtGrabber.hpp" #include "database/Artist.hpp" #include "database/Cluster.hpp" #include "database/Release.hpp" +#include "database/User.hpp" #include "explore/Explore.hpp" #include "main/Service.hpp" #include "utils/Logger.hpp" @@ -46,6 +46,7 @@ #include "admin/UsersView.hpp" #include "resource/ImageResource.hpp" #include "resource/AudioResource.hpp" +#include "Auth.hpp" #include "MediaPlayer.hpp" #include "PlayHistoryView.hpp" #include "PlayQueueView.hpp" @@ -66,6 +67,39 @@ LmsApplication::instance() return reinterpret_cast(Wt::WApplication::instance()); } +Wt::Dbo::ptr +LmsApplication::getUser() const +{ + if (!_userId) + return {}; + + return Database::User::getById(*_dbSession, *_userId); +} + +bool +LmsApplication::isUserAdmin() const +{ + auto transaction {_dbSession->createSharedTransaction()}; + + return getUser()->isAdmin(); +} + +bool +LmsApplication::isUserDemo() const +{ + auto transaction {_dbSession->createSharedTransaction()}; + + return getUser()->isDemo(); +} + +std::string +LmsApplication::getUserLoginName() const +{ + auto transaction {_dbSession->createSharedTransaction()}; + + return getUser()->getLoginName(); +} + LmsApplication::LmsApplication(const Wt::WEnvironment& env, std::unique_ptr dbSession, LmsApplicationGroupContainer& appGroups) @@ -131,25 +165,35 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, if (firstConnection) { root()->addWidget(std::make_unique()); + return; + } + + auto userId {processAuthToken(env)}; + if (userId) + { + handleUserLoggedIn(*userId); } else { - LmsApp->getDbSession().getLogin().changed().connect(this, &LmsApplication::handleAuthEvent); - _auth = root()->addNew(); + Auth* auth {root()->addNew()}; + auth->userLoggedIn.connect(this, &LmsApplication::handleUserLoggedIn); } } void LmsApplication::finalize() { - LmsApplicationInfo info = LmsApplicationInfo::fromEnvironment(environment()); - - getApplicationGroup().postOthers([info] + if (_userId) { - LmsApp->getEvents().appClosed(info); - }); + LmsApplicationInfo info = LmsApplicationInfo::fromEnvironment(environment()); - getApplicationGroup().leave(); + getApplicationGroup().postOthers([info] + { + LmsApp->getEvents().appClosed(info); + }); + + getApplicationGroup().leave(); + } preQuit().emit(); } @@ -280,28 +324,37 @@ handlePathChange(Wt::WStackedWidget* stack, bool isAdmin) LmsApplicationGroup& LmsApplication::getApplicationGroup() { - return _appGroups.get(_userIdentity); + return _appGroups.get(*_userId); } void -LmsApplication::handleAuthEvent() +LmsApplication::handleUserLoggedOut() { - if (!getDbSession().getLogin().loggedIn()) - { - LMS_LOG(UI, INFO) << "User '" << _userIdentity << " 'logged out, session = " << sessionId(); + LMS_LOG(UI, INFO) << "User '" << getUserLoginName() << " 'logged out"; - goHomeAndQuit(); - return; + { + auto transaction {_dbSession->createUniqueTransaction()}; + getUser().modify()->clearAuthTokens(); } + setConfirmCloseMessage(""); + goHomeAndQuit(); +} + +void +LmsApplication::handleUserLoggedIn(Database::IdType userId) +{ + _userId = userId; + + root()->clear(); + try { // post([this] // { - _userIdentity = getAuthUser().identity(Wt::Auth::Identity::LoginName); const LmsApplicationInfo info {LmsApplicationInfo::fromEnvironment(environment())}; - LMS_LOG(UI, INFO) << "User '" << _userIdentity << "' logged in from '" << environment().clientAddress() << "', user agent = " << environment().userAgent() << ", session = " << sessionId(); + LMS_LOG(UI, INFO) << "User '" << getUserLoginName() << "' logged in from '" << environment().clientAddress() << "', user agent = " << environment().userAgent(); getApplicationGroup().join(info); getApplicationGroup().postOthers([info] @@ -310,7 +363,8 @@ LmsApplication::handleAuthEvent() }); createHome(); - triggerUpdate(); + +// triggerUpdate(); // }); } catch (std::exception& e) @@ -323,12 +377,6 @@ LmsApplication::handleAuthEvent() void LmsApplication::createHome() { - - { - auto transaction {_dbSession->createSharedTransaction()}; - _isAdmin = LmsApp->getUser()->isAdmin(); - } - _imageResource = std::make_shared(); _audioResource = std::make_shared(); @@ -337,8 +385,8 @@ LmsApplication::createHome() Wt::WTemplate* main {root()->addWidget(std::make_unique(Wt::WString::tr("Lms.template")))}; // Navbar - Wt::WNavigationBar* navbar = main->bindNew("navbar-top"); - navbar->setTitle("LMS", Wt::WLink(Wt::LinkType::InternalPath, "/artists")); + Wt::WNavigationBar* navbar {main->bindNew("navbar-top")}; + navbar->setTitle("LMS", Wt::WLink {Wt::LinkType::InternalPath, "/artists"}); navbar->setResponsive(true); Wt::WMenu* menu {navbar->addMenu(std::make_unique())}; @@ -370,7 +418,7 @@ LmsApplication::createHome() Wt::WMenu* rightMenu = navbar->addMenu(std::make_unique(), Wt::AlignmentFlag::Right); std::size_t itemCounter = 0; - if (_isAdmin) + if (isUserAdmin()) { auto menuItem = rightMenu->insertItem(itemCounter++, Wt::WString::tr("Lms.administration")); menuItem->setSelectable(false); @@ -395,11 +443,7 @@ LmsApplication::createHome() { auto menuItem = rightMenu->insertItem(itemCounter++, Wt::WString::tr("Lms.logout")); menuItem->setSelectable(true); - menuItem->triggered().connect(std::bind([=] - { - setConfirmCloseMessage(""); - _auth->logout(); - })); + menuItem->triggered().connect(this, &LmsApplication::handleUserLoggedOut); } // Contents @@ -412,7 +456,7 @@ LmsApplication::createHome() mainStack->addNew(); // Admin stuff - if (_isAdmin) + if (isUserAdmin()) { mainStack->addNew(); mainStack->addNew(); @@ -493,7 +537,7 @@ LmsApplication::createHome() _events.dbScanned.connect([=] (Scanner::MediaScanner::Stats stats) { - if (_isAdmin) + if (isUserAdmin()) { notifyMsg(MsgType::Info, Wt::WString::tr("Lms.Admin.Database.scan-complete") .arg(static_cast(stats.nbFiles())) @@ -509,7 +553,7 @@ LmsApplication::createHome() _events.appOpen.connect([=] (LmsApplicationInfo info) { // Only one active session by user - if (!LmsApp->getUser()->isDemo()) + if (!LmsApp->isUserDemo()) { setConfirmCloseMessage(""); quit(Wt::WString::tr("Lms.quit-other-session")); @@ -518,10 +562,10 @@ LmsApplication::createHome() internalPathChanged().connect(std::bind([=] { - handlePathChange(mainStack, _isAdmin); + handlePathChange(mainStack, isUserAdmin()); })); - handlePathChange(mainStack, _isAdmin); + handlePathChange(mainStack, isUserAdmin()); } void diff --git a/src/ui/LmsApplication.hpp b/src/ui/LmsApplication.hpp index e4690fae..2d35867f 100644 --- a/src/ui/LmsApplication.hpp +++ b/src/ui/LmsApplication.hpp @@ -28,7 +28,6 @@ #include "scanner/MediaScanner.hpp" #include "LmsApplicationGroup.hpp" -#include "Auth.hpp" namespace Database { class Artist; @@ -41,6 +40,7 @@ namespace UserInterface { class AudioResource; class ImageResource; +class Auth; // Events that can be listen from anywhere in the application struct Events @@ -83,9 +83,10 @@ class LmsApplication : public Wt::WApplication std::shared_ptr getAudioResource() { return _audioResource; } Database::Session& getDbSession() { return *_dbSession.get();} - const Wt::Auth::User& getAuthUser() { return getDbSession().getLogin().user(); } - Wt::Dbo::ptr getUser() { return getDbSession().getLoggedUser(); } - Wt::WString getUserIdentity() { return _userIdentity; } + Wt::Dbo::ptr getUser() const; + bool isUserAdmin() const; // user must be logged in prior this call + bool isUserDemo() const; // user must be logged in prior this call + std::string getUserLoginName() const; // user must be logged in prior this call Events& getEvents() { return _events; } @@ -109,8 +110,10 @@ class LmsApplication : public Wt::WApplication LmsApplicationGroup& getApplicationGroup(); - // Events - void handleAuthEvent(); + // Signal slots + void handleUserLoggedOut(); + void handleUserLoggedIn(Database::IdType userId); + void notify(const Wt::WEvent& event) override; void finalize() override; @@ -120,11 +123,9 @@ class LmsApplication : public Wt::WApplication std::unique_ptr _dbSession; LmsApplicationGroupContainer& _appGroups; Events _events; - Wt::WString _userIdentity; - Auth* _auth {}; + boost::optional _userId {}; std::shared_ptr _imageResource; std::shared_ptr _audioResource; - bool _isAdmin {}; }; diff --git a/src/ui/LmsApplicationGroup.cpp b/src/ui/LmsApplicationGroup.cpp index 36e0647b..ba5ed2a3 100644 --- a/src/ui/LmsApplicationGroup.cpp +++ b/src/ui/LmsApplicationGroup.cpp @@ -37,7 +37,7 @@ LmsApplicationInfo::fromEnvironment(const Wt::WEnvironment& env) void LmsApplicationGroup::join(LmsApplicationInfo info) { - std::lock_guard lock(_mutex); + std::unique_lock lock {_mutex}; _apps.emplace(wApp->sessionId(), std::move(info)); } @@ -45,7 +45,7 @@ LmsApplicationGroup::join(LmsApplicationInfo info) void LmsApplicationGroup::leave() { - std::lock_guard lock(_mutex); + std::unique_lock lock {_mutex}; _apps.erase(wApp->sessionId()); } @@ -55,7 +55,7 @@ LmsApplicationGroup::getOtherSessionIds() const { std::vector res; - std::lock_guard lock(_mutex); + std::unique_lock lock {_mutex}; for (auto const& app : _apps) { if (app.first != wApp->sessionId()) @@ -79,11 +79,11 @@ LmsApplicationGroup::postOthers(std::function func) const } LmsApplicationGroup& -LmsApplicationGroupContainer::get(Wt::WString identity) +LmsApplicationGroupContainer::get(Database::IdType userId) { - std::lock_guard lock(_mutex); + std::unique_lock lock {_mutex}; - return _apps[identity]; + return _apps[userId]; } } // UserInterface diff --git a/src/ui/LmsApplicationGroup.hpp b/src/ui/LmsApplicationGroup.hpp index 326fe63a..ae579e91 100644 --- a/src/ui/LmsApplicationGroup.hpp +++ b/src/ui/LmsApplicationGroup.hpp @@ -25,6 +25,8 @@ #include #include +#include "database/Types.hpp" + namespace UserInterface { @@ -56,10 +58,10 @@ class LmsApplicationGroup class LmsApplicationGroupContainer { public: - LmsApplicationGroup& get(Wt::WString identity); + LmsApplicationGroup& get(Database::IdType userId); private: - std::map _apps; + std::map _apps; std::mutex _mutex; }; diff --git a/src/ui/PlayHistoryView.cpp b/src/ui/PlayHistoryView.cpp index 2df44771..9fa9fc43 100644 --- a/src/ui/PlayHistoryView.cpp +++ b/src/ui/PlayHistoryView.cpp @@ -26,6 +26,7 @@ #include "database/Track.hpp" #include "database/TrackList.hpp" +#include "database/User.hpp" #include "LmsApplication.hpp" diff --git a/src/ui/PlayQueueView.cpp b/src/ui/PlayQueueView.cpp index be06290d..3fccf9e8 100644 --- a/src/ui/PlayQueueView.cpp +++ b/src/ui/PlayQueueView.cpp @@ -24,6 +24,7 @@ #include "database/Track.hpp" #include "database/TrackList.hpp" +#include "database/User.hpp" #include "main/Service.hpp" #include "similarity/SimilaritySearcher.hpp" #include "utils/Logger.hpp" @@ -131,7 +132,14 @@ PlayQueue::PlayQueue() { LmsApp->post([=] { - load(LmsApp->getUser()->getCurPlayingTrackPos(), false); + std::size_t trackPos {}; + + { + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + trackPos = LmsApp->getUser()->getCurPlayingTrackPos(); + } + + load(trackPos, false); }); trackList = LmsApp->getUser()->getQueuedTrackList(LmsApp->getDbSession()); } diff --git a/src/ui/SettingsView.cpp b/src/ui/SettingsView.cpp index b0a8e0c6..baec3f3a 100644 --- a/src/ui/SettingsView.cpp +++ b/src/ui/SettingsView.cpp @@ -24,12 +24,15 @@ #include #include #include +#include #include #include "common/Validators.hpp" #include "common/ValueStringModel.hpp" +#include "auth/AuthService.hpp" +#include "main/Service.hpp" #include "utils/Logger.hpp" #include "LmsApplication.hpp" @@ -69,20 +72,27 @@ class SettingsModel : public Wt::WFormModel void saveData() { + Database::User::PasswordHash passwordHash; + + if (!valueText(PasswordField).empty()) + passwordHash = getService<::Auth::AuthService>()->hashPassword(valueText(PasswordField).toUTF8()); + auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; - LmsApp->getUser().modify()->setAudioTranscodeEnable(Wt::asNumber(value(TranscodeEnableField))); + Database::User::pointer user {LmsApp->getUser()}; + + user.modify()->setAudioTranscodeEnable(Wt::asNumber(value(TranscodeEnableField))); auto transcodeBitrateRow {_transcodeBitrateModel->getRowFromString(valueText(TranscodeBitrateField))}; if (transcodeBitrateRow) - LmsApp->getUser().modify()->setAudioTranscodeBitrate(_transcodeBitrateModel->getValue(*transcodeBitrateRow)); + user.modify()->setAudioTranscodeBitrate(_transcodeBitrateModel->getValue(*transcodeBitrateRow)); auto transcodeFormatRow {_transcodeFormatModel->getRowFromString(valueText(TranscodeFormatField))}; if (transcodeFormatRow) - LmsApp->getUser().modify()->setAudioTranscodeFormat(_transcodeFormatModel->getValue(*transcodeFormatRow)); + user.modify()->setAudioTranscodeFormat(_transcodeFormatModel->getValue(*transcodeFormatRow)); if (!valueText(PasswordField).empty()) - Session::getPasswordService().updatePassword(LmsApp->getAuthUser(), valueText(PasswordField)); + user.modify()->setPasswordHash(passwordHash); } void loadData() @@ -115,11 +125,8 @@ class SettingsModel : public Wt::WFormModel { if (!valueText(PasswordField).empty()) { - // Evaluate the strength of the password - auto res = Session::getPasswordService().strengthValidator()->evaluateStrength(valueText(PasswordField), LmsApp->getUserIdentity(), ""); - - if (!res.isValid()) - error = res.message(); + if (!getService<::Auth::AuthService>()->evaluatePasswordStrength(LmsApp->getUserLoginName(), valueText(PasswordField).toUTF8())) + error = Wt::WString::tr("Lms.password-too-weak"); } else return Wt::WFormModel::validateField(field); diff --git a/src/ui/admin/InitWizardView.cpp b/src/ui/admin/InitWizardView.cpp index da35e79d..21cb5d20 100644 --- a/src/ui/admin/InitWizardView.cpp +++ b/src/ui/admin/InitWizardView.cpp @@ -22,8 +22,9 @@ #include #include #include -#include +#include "auth/AuthService.hpp" +#include "main/Service.hpp" #include "utils/Exception.hpp" #include "utils/Logger.hpp" @@ -54,6 +55,8 @@ class InitWizardModel : public Wt::WFormModel void saveData() { + const Database::User::PasswordHash passwordHash {getService<::Auth::AuthService>()->hashPassword(valueText(PasswordField).toUTF8())}; + auto transaction(LmsApp->getDbSession().createUniqueTransaction()); // Check if a user already exist @@ -61,7 +64,7 @@ class InitWizardModel : public Wt::WFormModel if (!Database::User::getAll(LmsApp->getDbSession()).empty()) throw LmsException("Admin user already created"); - Database::User::pointer user {LmsApp->getDbSession().createUser(valueText(AdminLoginField).toUTF8(), valueText(PasswordField).toUTF8())}; + Database::User::pointer user {Database::User::create(LmsApp->getDbSession(), valueText(AdminLoginField).toUTF8(), passwordHash)}; user.modify()->setType(Database::User::Type::ADMIN); } @@ -74,11 +77,8 @@ class InitWizardModel : public Wt::WFormModel if (!valueText(PasswordField).empty()) { // Evaluate the strength of the password - auto res = Database::Session::getPasswordService().strengthValidator()->evaluateStrength(valueText(PasswordField), - valueText(AdminLoginField), ""); - - if (!res.isValid()) - error = res.message(); + if (!getService<::Auth::AuthService>()->evaluatePasswordStrength(valueText(AdminLoginField).toUTF8(), valueText(PasswordField).toUTF8())) + error = Wt::WString::tr("Lms.password-too-weak"); } else return Wt::WFormModel::validateField(field); @@ -126,7 +126,7 @@ InitWizardView::InitWizardView() setFormWidget(InitWizardModel::PasswordConfirmField, std::move(passwordConfirmEdit)); Wt::WPushButton* saveButton = bindNew("create-btn", Wt::WString::tr("Lms.create")); - saveButton->clicked().connect(std::bind([=] + saveButton->clicked().connect([=] { updateModel(model.get()); @@ -138,7 +138,7 @@ InitWizardView::InitWizardView() } updateView(model.get()); - })); + }); updateView(model.get()); } diff --git a/src/ui/admin/UserView.cpp b/src/ui/admin/UserView.cpp index 65d4a997..5a159921 100644 --- a/src/ui/admin/UserView.cpp +++ b/src/ui/admin/UserView.cpp @@ -28,7 +28,9 @@ #include +#include "auth/AuthService.hpp" #include "database/User.hpp" +#include "main/Service.hpp" #include "utils/Config.hpp" #include "utils/Exception.hpp" #include "utils/Logger.hpp" @@ -78,6 +80,10 @@ class UserModel : public Wt::WFormModel void saveData() { + boost::optional passwordHash; + if (!valueText(PasswordField).empty()) + passwordHash = getService<::Auth::AuthService>()->hashPassword(valueText(PasswordField).toUTF8()); + auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; if (_userId) @@ -86,8 +92,11 @@ class UserModel : public Wt::WFormModel Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *_userId)}; // Account - if (!valueText(PasswordField).empty()) - LmsApp->getDbSession().updateUserPassword(user, valueText(PasswordField).toUTF8()); + if (passwordHash) + { + user.modify()->setPasswordHash(*passwordHash); + user.modify()->clearAuthTokens(); + } auto transcodeBitrateLimitRow {_bitrateModel->getRowFromString(valueText(AudioTranscodeBitrateLimitField))}; if (transcodeBitrateLimitRow) @@ -96,7 +105,7 @@ class UserModel : public Wt::WFormModel else { // Create user - Database::User::pointer user = LmsApp->getDbSession().createUser(valueText(LoginField).toUTF8(), valueText(PasswordField).toUTF8()); + Database::User::pointer user {Database::User::create(LmsApp->getDbSession(), valueText(LoginField).toUTF8(), *passwordHash)}; auto transcodeBitrateLimitRow {_bitrateModel->getRowFromString(valueText(AudioTranscodeBitrateLimitField))}; if (transcodeBitrateLimitRow ) @@ -125,17 +134,17 @@ class UserModel : public Wt::WFormModel setValue(AudioTranscodeBitrateLimitField, _bitrateModel->getString(*transcodeBitrateLimitRow)); } - Wt::WString getLoginName() const + std::string getLoginName() const { if (_userId) { auto transaction {LmsApp->getDbSession().createSharedTransaction()}; const Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *_userId)}; - return LmsApp->getDbSession().getUserLoginName(user); + return user->getLoginName(); } else - return valueText(LoginField); + return valueText(LoginField).toUTF8(); } bool validateField(Field field) @@ -144,7 +153,9 @@ class UserModel : public Wt::WFormModel if (field == LoginField) { - const Database::User::pointer user {LmsApp->getDbSession().getUser(valueText(LoginField).toUTF8())}; + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + const Database::User::pointer user {Database::User::getByLoginName(LmsApp->getDbSession(), valueText(LoginField).toUTF8())}; if (user) error = Wt::WString::tr("Lms.Admin.User.user-already-exists"); } @@ -154,17 +165,15 @@ class UserModel : public Wt::WFormModel { if (Wt::asNumber(value(DemoField))) { - //Demo account: password must be the same as the login name + // Demo account: password must be the same as the login name if (valueText(PasswordField) != getLoginName()) error = Wt::WString::tr("Lms.Admin.User.demo-password-invalid"); } else { // Evaluate the strength of the password for non demo accounts - auto res = Database::Session::getPasswordService().strengthValidator()->evaluateStrength(valueText(PasswordField), getLoginName(), ""); - - if (!res.isValid()) - error = res.message(); + if (!getService<::Auth::AuthService>()->evaluatePasswordStrength(getLoginName(), valueText(PasswordField).toUTF8())) + error = Wt::WString::tr("Lms.password-too-weak"); } } } @@ -231,13 +240,12 @@ UserView::refreshView() auto transaction {LmsApp->getDbSession().createSharedTransaction()}; const Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *userId)}; - const std::string loginName {LmsApp->getDbSession().getUserLoginName(user)}; - t->bindString("title", Wt::WString::tr("Lms.Admin.User.user-edit").arg(loginName), Wt::TextFormat::Plain); - t->setCondition("if-has-last-login-attempt", true); + t->bindString("title", Wt::WString::tr("Lms.Admin.User.user-edit").arg(user->getLoginName()), Wt::TextFormat::Plain); + t->setCondition("if-has-last-login", true); - Wt::WLineEdit *lastLoginAttempt {t->bindNew("last-login-attempt")}; - lastLoginAttempt->setText(LmsApp->getDbSession().getUserLastLoginAttempt(user).toString()); - lastLoginAttempt->setEnabled(false); + Wt::WLineEdit *lastLogin {t->bindNew("last-login")}; + lastLogin->setText(user->getLastLogin().toString()); + lastLogin->setEnabled(false); } else { diff --git a/src/ui/admin/UsersView.cpp b/src/ui/admin/UsersView.cpp index d276dc13..76b2e6a7 100644 --- a/src/ui/admin/UsersView.cpp +++ b/src/ui/admin/UsersView.cpp @@ -68,7 +68,7 @@ UsersView::refreshView() Wt::WTemplate* entry {_container->addNew(Wt::WString::tr("Lms.Admin.Users.template.entry"))}; - entry->bindString("name", LmsApp->getDbSession().getUserLoginName(user), Wt::TextFormat::Plain); + entry->bindString("name", user->getLoginName(), Wt::TextFormat::Plain); // Create tag if (user->isAdmin() || user->isDemo()) @@ -104,7 +104,7 @@ UsersView::refreshView() Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), userId)}; if (user) - LmsApp->getDbSession().removeUser(user); + user.remove(); _container->removeWidget(entry); } diff --git a/src/ui/explore/ArtistsInfoView.cpp b/src/ui/explore/ArtistsInfoView.cpp index fcf635a4..ec1bebbd 100644 --- a/src/ui/explore/ArtistsInfoView.cpp +++ b/src/ui/explore/ArtistsInfoView.cpp @@ -23,6 +23,7 @@ #include "database/Artist.hpp" #include "database/TrackList.hpp" +#include "database/User.hpp" #include "utils/Utils.hpp" #include "ArtistLink.hpp" #include "LmsApplication.hpp" diff --git a/src/ui/explore/Filters.cpp b/src/ui/explore/Filters.cpp index a3489737..078f48a3 100644 --- a/src/ui/explore/Filters.cpp +++ b/src/ui/explore/Filters.cpp @@ -99,17 +99,24 @@ Filters::showDialog() const std::string type {typeCombo->valueText().toUTF8()}; const std::string value {valueCombo->valueText().toUTF8()}; - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + // TODO use a model to store the cluster.id() values + Database::IdType clusterId {}; - Database::ClusterType::pointer clusterType {Database::ClusterType::getByName(LmsApp->getDbSession(), type)}; - if (!clusterType) - return; + { + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - Database::Cluster::pointer cluster {clusterType->getCluster(value)}; - if (!cluster) - return; + Database::ClusterType::pointer clusterType {Database::ClusterType::getByName(LmsApp->getDbSession(), type)}; + if (!clusterType) + return; - add(cluster.id()); + Database::Cluster::pointer cluster {clusterType->getCluster(value)}; + if (!cluster) + return; + + clusterId = cluster.id(); + } + + add(clusterId); }); dialog->show(); @@ -118,23 +125,29 @@ Filters::showDialog() void Filters::add(Database::IdType clusterId) { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - Database::Cluster::pointer cluster {Database::Cluster::getById(LmsApp->getDbSession(), clusterId)}; - if (!cluster) - return; + Wt::WTemplate* filter {}; - auto res {_filterIds.insert(clusterId)}; - if (!res.second) - return; + { + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - auto filter {_filters->addWidget(LmsApp->createCluster(cluster, true))}; - filter->clicked().connect(std::bind([=] + Database::Cluster::pointer cluster {Database::Cluster::getById(LmsApp->getDbSession(), clusterId)}; + if (!cluster) + return; + + auto res {_filterIds.insert(clusterId)}; + if (!res.second) + return; + + filter = _filters->addWidget(LmsApp->createCluster(cluster, true)); + } + + filter->clicked().connect([=] { _filters->removeWidget(filter); _filterIds.erase(clusterId); _sigUpdated.emit(); - })); + }); LmsApp->notifyMsg(MsgType::Info, Wt::WString::tr("Lms.Explore.filter-added"), std::chrono::seconds {2}); diff --git a/src/ui/explore/ReleasesInfoView.cpp b/src/ui/explore/ReleasesInfoView.cpp index c3dcfdfb..14ccb04d 100644 --- a/src/ui/explore/ReleasesInfoView.cpp +++ b/src/ui/explore/ReleasesInfoView.cpp @@ -23,6 +23,7 @@ #include "database/Release.hpp" #include "database/TrackList.hpp" +#include "database/User.hpp" #include "resource/ImageResource.hpp" #include "ReleaseLink.hpp" #include "LmsApplication.hpp" diff --git a/src/ui/explore/TracksInfoView.cpp b/src/ui/explore/TracksInfoView.cpp index 4f2a97a0..4c4cd471 100644 --- a/src/ui/explore/TracksInfoView.cpp +++ b/src/ui/explore/TracksInfoView.cpp @@ -24,6 +24,7 @@ #include "database/Track.hpp" #include "database/TrackList.hpp" +#include "database/User.hpp" #include "utils/Utils.hpp" diff --git a/src/ui/resource/AudioResource.cpp b/src/ui/resource/AudioResource.cpp index 52b950a0..92537e90 100644 --- a/src/ui/resource/AudioResource.cpp +++ b/src/ui/resource/AudioResource.cpp @@ -24,6 +24,7 @@ #include "utils/Logger.hpp" #include "database/Track.hpp" +#include "database/User.hpp" #include "LmsApplication.hpp" diff --git a/src/utils/Logger.cpp b/src/utils/Logger.cpp index d1103a6d..c25c1b40 100644 --- a/src/utils/Logger.cpp +++ b/src/utils/Logger.cpp @@ -24,6 +24,7 @@ std::string getModuleName(Module mod) switch (mod) { case Module::API_SUBSONIC: return "API_SUBSONIC"; + case Module::AUTH: return "AUTH"; case Module::AV: return "AV"; case Module::COVER: return "COVER"; case Module::DB: return "DB"; diff --git a/src/utils/Logger.hpp b/src/utils/Logger.hpp index 5a7585f8..a2ce9d4a 100644 --- a/src/utils/Logger.hpp +++ b/src/utils/Logger.hpp @@ -36,6 +36,7 @@ enum class Severity enum class Module { API_SUBSONIC, + AUTH, AV, COVER, DB, diff --git a/src/utils/NetAddress.cpp b/src/utils/NetAddress.cpp new file mode 100644 index 00000000..723de502 --- /dev/null +++ b/src/utils/NetAddress.cpp @@ -0,0 +1,44 @@ +/* + * 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 "NetAddress.hpp" + +namespace std +{ + std::size_t hash::operator()(const boost::asio::ip::address& ipAddr) const + { + if (ipAddr.is_v4()) + return ipAddr.to_v4().to_ulong(); + + if (ipAddr.is_v6()) + { + const auto& range {ipAddr.to_v6().to_bytes()}; + std::size_t res {}; + + for (auto b : range) + res ^= std::hash{}(static_cast(b)); + + return res; + } + + return std::hash{}(ipAddr.to_string()); + } + +} + diff --git a/src/utils/NetAddress.hpp b/src/utils/NetAddress.hpp new file mode 100644 index 00000000..ae142775 --- /dev/null +++ b/src/utils/NetAddress.hpp @@ -0,0 +1,31 @@ +/* + * 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 + +namespace std +{ + template<> struct hash + { + std::size_t operator()(const boost::asio::ip::address& ipAddr) const; + }; + +} + + diff --git a/src/utils/Path.hpp b/src/utils/Path.hpp index 8ebcf95a..4b1131f6 100644 --- a/src/utils/Path.hpp +++ b/src/utils/Path.hpp @@ -1,4 +1,3 @@ - /* * Copyright (C) 2016 Emeric Poupon * diff --git a/src/utils/Utils.cpp b/src/utils/Utils.cpp index eab83c5f..e2fea7fd 100644 --- a/src/utils/Utils.cpp +++ b/src/utils/Utils.cpp @@ -156,4 +156,3 @@ stringFromHex(const std::string& str) } - diff --git a/src/utils/Utils.hpp b/src/utils/Utils.hpp index 1cc5644c..1097f89f 100644 --- a/src/utils/Utils.hpp +++ b/src/utils/Utils.hpp @@ -114,3 +114,29 @@ shuffleContainer(Container& container) std::shuffle(std::begin(container), std::end(container), randGenerator); } +template +void +fillRandom(Container& container) +{ + using value_type = typename Container::value_type; + static_assert(std::is_integral::value, "Integral required"); + + std::random_device rd; + std::mt19937_64 randGenerator {rd()}; + std::uniform_int_distribution<> dist {std::numeric_limits::min(), std::numeric_limits::max()}; + + std::for_each(std::begin(container), std::end(container), [&](auto& elem) { elem = dist(randGenerator); }); +} + +template +typename Container::iterator +pickRandom(Container& container) +{ + auto now {std::chrono::system_clock::now()}; + std::mt19937 randGenerator (std::chrono::duration_cast(now.time_since_epoch()).count()); + std::uniform_int_distribution<> dist {0, static_cast(container.size())}; + + return std::next(std::begin(container), dist(randGenerator )); +} + +