API Subsonic: added authentication support

This commit is contained in:
emeric
2019-04-05 10:54:36 +02:00
parent 3ee937a409
commit 35b3d631ef
9 changed files with 107 additions and 14 deletions
+5
View File
@@ -28,6 +28,9 @@ namespace API::Subsonic
boost::optional<Id> boost::optional<Id>
IdFromString(const std::string& id) IdFromString(const std::string& id)
{ {
if (id == "root")
return Id{Id::Type::Root};
std::vector<std::string> values {splitString(id, "-")}; std::vector<std::string> values {splitString(id, "-")};
if (values.size() != 2) if (values.size() != 2)
{ {
@@ -69,6 +72,8 @@ IdToString(const Id& id)
switch (id.type) switch (id.type)
{ {
case Id::Type::Root:
return "root";
case Id::Type::Artist: case Id::Type::Artist:
res = "artist-"; res = "artist-";
break; break;
+2 -1
View File
@@ -28,13 +28,14 @@ struct Id
{ {
enum class Type enum class Type
{ {
Root, // Where all artists artistless albums reside
Track, Track,
Release, Release,
Artist, Artist,
}; };
Type type; Type type;
Database::IdType id; Database::IdType id {};
}; };
boost::optional<Id> IdFromString(const std::string& id); boost::optional<Id> IdFromString(const std::string& id);
+54 -9
View File
@@ -22,6 +22,7 @@
#include <numeric> #include <numeric>
#include <random> #include <random>
#include <Wt/Auth/Identity.h>
#include <Wt/WLocalDateTime.h> #include <Wt/WLocalDateTime.h>
#include "av/AvTranscoder.hpp" #include "av/AvTranscoder.hpp"
@@ -164,6 +165,13 @@ getClientInfo(const Wt::Http::ParameterMap& parameters)
param = getParameterAs<std::string>(parameters, "p"); param = getParameterAs<std::string>(parameters, "p");
if (!param) if (!param)
return {}; return {};
if (param->find("enc:") == 0)
{
param = stringFromHex(param->substr(4));
if (!param)
return {};
}
res->password = *param; res->password = *param;
// Optional parameters // Optional parameters
@@ -173,6 +181,20 @@ getClientInfo(const Wt::Http::ParameterMap& parameters)
return res; return res;
} }
static
bool
checkPassword(Database::Handler& db, const ClientInfo& clientInfo)
{
auto authUser {db.getUserDatabase().findWithIdentity(Wt::Auth::Identity::LoginName, clientInfo.user)};
if (!authUser.isValid())
{
LMS_LOG(API_SUBSONIC, ERROR) << "Cannot find user '" << clientInfo.user << "'";
return false;
}
return db.getPasswordService().verifyPassword(authUser, clientInfo.password) == Wt::Auth::PasswordResult::PasswordValid;
}
SubsonicResource::SubsonicResource(Wt::Dbo::SqlConnectionPool& connectionPool) SubsonicResource::SubsonicResource(Wt::Dbo::SqlConnectionPool& connectionPool)
: _db {connectionPool} : _db {connectionPool}
{ {
@@ -206,10 +228,6 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp
LMS_LOG(API_SUBSONIC, DEBUG) << "\t'" << value << "'"; LMS_LOG(API_SUBSONIC, DEBUG) << "\t'" << value << "'";
} }
std::string s{std::istreambuf_iterator<char>(request.in()), {}};
LMS_LOG(API_SUBSONIC, DEBUG) << "BODY = '" << s << "'";
auto clientInfo {getClientInfo(parameters)}; auto clientInfo {getClientInfo(parameters)};
if (!clientInfo) if (!clientInfo)
{ {
@@ -223,6 +241,9 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp
std::unique_lock<std::mutex> lock{mutex}; // For now just handle request s one by one std::unique_lock<std::mutex> lock{mutex}; // For now just handle request s one by one
if (!checkPassword(_db, *clientInfo))
throw Error {Error::Code::WrongUsernameOrPassword};
auto itHandler {requestHandlers.find(request.path())}; auto itHandler {requestHandlers.find(request.path())};
if (itHandler != requestHandlers.end()) if (itHandler != requestHandlers.end())
{ {
@@ -321,9 +342,15 @@ releaseToResponseNode(const Database::Release::pointer& release)
if (!artists.empty()) if (!artists.empty())
{ {
if (artists.size() > 1) if (artists.size() > 1)
{
albumNode.setAttribute("artist", "Various Artists"); albumNode.setAttribute("artist", "Various Artists");
albumNode.setAttribute("parent", IdToString({Id::Type::Root}));
}
else else
{
albumNode.setAttribute("artist", artists.front()->getName()); albumNode.setAttribute("artist", artists.front()->getName());
albumNode.setAttribute("parent", IdToString({Id::Type::Artist, artists.front().id()}));
}
} }
return albumNode; return albumNode;
@@ -338,6 +365,7 @@ artistToResponseNode(const Database::Artist::pointer& artist)
artistNode.setAttribute("id", IdToString({Id::Type::Artist, artist.id()})); artistNode.setAttribute("id", IdToString({Id::Type::Artist, artist.id()}));
artistNode.setAttribute("name", artist->getName()); artistNode.setAttribute("name", artist->getName());
artistNode.setAttribute("albumCount", std::to_string(artist->getReleases().size())); artistNode.setAttribute("albumCount", std::to_string(artist->getReleases().size()));
artistNode.setAttribute("parent", IdToString({Id::Type::Root}));
return artistNode; return artistNode;
} }
@@ -512,6 +540,19 @@ handleGetMusicDirectoryRequest(const Wt::Http::ParameterMap& request, Database::
switch (id->type) switch (id->type)
{ {
case Id::Type::Root:
{
Wt::Dbo::Transaction transaction {db.getSession()};
directoryNode.setAttribute("name", "Music");
auto artists {Database::Artist::getAll(db.getSession())};
for (const Database::Artist::pointer& artist : artists)
directoryNode.addArrayChild("child", artistToResponseNode(artist));
break;
}
case Id::Type::Artist: case Id::Type::Artist:
{ {
Wt::Dbo::Transaction transaction {db.getSession()}; Wt::Dbo::Transaction transaction {db.getSession()};
@@ -520,6 +561,8 @@ handleGetMusicDirectoryRequest(const Wt::Http::ParameterMap& request, Database::
if (!artist) if (!artist)
throw Error {Error::Code::RequestedDataNotFound}; throw Error {Error::Code::RequestedDataNotFound};
directoryNode.setAttribute("name", artist->getName());
auto releases {artist->getReleases()}; auto releases {artist->getReleases()};
for (const Database::Release::pointer& release : releases) for (const Database::Release::pointer& release : releases)
directoryNode.addArrayChild("child", releaseToResponseNode(release)); directoryNode.addArrayChild("child", releaseToResponseNode(release));
@@ -535,6 +578,8 @@ handleGetMusicDirectoryRequest(const Wt::Http::ParameterMap& request, Database::
if (!release) if (!release)
throw Error {Error::Code::RequestedDataNotFound}; throw Error {Error::Code::RequestedDataNotFound};
directoryNode.setAttribute("name", release->getName());
auto tracks {release->getTracks()}; auto tracks {release->getTracks()};
for (const Database::Track::pointer& track : tracks) for (const Database::Track::pointer& track : tracks)
directoryNode.addArrayChild("child", trackToResponseNode(track)); directoryNode.addArrayChild("child", trackToResponseNode(track));
@@ -556,7 +601,7 @@ handleGetMusicFoldersRequest(const Wt::Http::ParameterMap& request, Database::Ha
Response::Node& musicFoldersNode {response.createNode("musicFolders")}; Response::Node& musicFoldersNode {response.createNode("musicFolders")};
Response::Node& musicFolderNode {musicFoldersNode.createArrayChild("musicFolder")}; Response::Node& musicFolderNode {musicFoldersNode.createArrayChild("musicFolder")};
musicFolderNode.setAttribute("id", "1"); musicFolderNode.setAttribute("id", IdToString({Id::Type::Root}));
musicFolderNode.setAttribute("name", "Music"); musicFolderNode.setAttribute("name", "Music");
return response; return response;
@@ -624,7 +669,7 @@ Response
handleGetStarredRequest(const Wt::Http::ParameterMap& request, Database::Handler& db) handleGetStarredRequest(const Wt::Http::ParameterMap& request, Database::Handler& db)
{ {
Response response {Response::createOkResponse()}; Response response {Response::createOkResponse()};
response.createArrayNode("starred"); response.createNode("starred");
return response; return response;
} }
@@ -633,7 +678,7 @@ Response
handleGetStarred2Request(const Wt::Http::ParameterMap& request, Database::Handler& db) handleGetStarred2Request(const Wt::Http::ParameterMap& request, Database::Handler& db)
{ {
Response response {Response::createOkResponse()}; Response response {Response::createOkResponse()};
response.createArrayNode("starred2"); response.createNode("starred2");
return response; return response;
} }
@@ -642,7 +687,7 @@ Response
handleGetPlaylistsRequest(const Wt::Http::ParameterMap& request, Database::Handler& db) handleGetPlaylistsRequest(const Wt::Http::ParameterMap& request, Database::Handler& db)
{ {
Response response {Response::createOkResponse()}; Response response {Response::createOkResponse()};
response.createArrayNode("playlists"); response.createNode("playlists");
return response; return response;
} }
@@ -813,7 +858,7 @@ handleGetCoverArt(const Wt::Http::Request& request, Database::Handler& db, Wt::H
cover = getServices().coverArtGrabber->getFromRelease(db.getSession(), id->id, Image::Format::JPEG, *size); cover = getServices().coverArtGrabber->getFromRelease(db.getSession(), id->id, Image::Format::JPEG, *size);
break; break;
default: default:
throw Error {"bad id format"}; throw Error {"Bad id format"};
} }
response.setMimeType( Image::format_to_mimeType(Image::Format::JPEG) ); response.setMimeType( Image::format_to_mimeType(Image::Format::JPEG) );
+8 -2
View File
@@ -19,6 +19,8 @@
#include "SubsonicResponse.hpp" #include "SubsonicResponse.hpp"
#include <regex>
#include <boost/property_tree/json_parser.hpp> #include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/xml_parser.hpp> #include <boost/property_tree/xml_parser.hpp>
@@ -227,9 +229,13 @@ Response::write(std::ostream& os, ResponseFormat format)
boost::property_tree::write_xml(os, root); boost::property_tree::write_xml(os, root);
break; break;
case ResponseFormat::json: case ResponseFormat::json:
boost::property_tree::write_json(os, root); {
// property_tree does not support empty json array
std::ostringstream oss;
boost::property_tree::write_json(oss, root);
os << std::regex_replace(oss.str(), std::regex {R"(\[[\r\n]*\s*\"\"[\r\n]*\s*\])"}, R"(\{\})");
break; break;
}
} }
} }
+1
View File
@@ -93,6 +93,7 @@ class Response
Node& createNode(const std::string& key); Node& createNode(const std::string& key);
Node& createArrayNode(const std::string& key); Node& createArrayNode(const std::string& key);
void writeJson(std::ostream& os);
void write(std::ostream& os, ResponseFormat format); void write(std::ostream& os, ResponseFormat format);
private: private:
+1 -1
View File
@@ -47,7 +47,7 @@ namespace Database {
namespace { namespace {
Wt::Auth::AuthService authService; Wt::Auth::AuthService authService;
Wt::Auth::PasswordService passwordService(authService); Wt::Auth::PasswordService passwordService {authService};
} }
+1 -1
View File
@@ -50,7 +50,7 @@ class Handler
Wt::Dbo::ptr<User> createUser(const Wt::Auth::User& authUser); Wt::Dbo::ptr<User> createUser(const Wt::Auth::User& authUser);
Wt::Auth::AbstractUserDatabase& getUserDatabase(); Wt::Auth::AbstractUserDatabase& getUserDatabase();
Wt::Auth::Login& getLogin() { return _login; } Wt::Auth::Login& getLogin() { return _login; } // TODO move
// Long living shared associated services // Long living shared associated services
static void configureAuth(); static void configureAuth();
+32
View File
@@ -116,4 +116,36 @@ replaceInString(std::string str, const std::string& from, const std::string& to)
return str; return str;
} }
boost::optional<std::string>
stringFromHex(const std::string& str)
{
static const char lut[] {"0123456789ABCDEF"};
if (str.length() % 2 != 0)
return boost::none;
std::string res;
res.reserve(str.length() / 2);
auto it {std::cbegin(str)};
while (it != std::cend(str))
{
unsigned val {};
auto itHigh {std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++)))};
auto itLow {std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++)))};
if (itHigh == std::cend(lut) || itLow == std::cend(lut))
return {};
val = std::distance(std::cbegin(lut), itHigh) << 4;
val += std::distance(std::cbegin(lut), itLow );
res.push_back(static_cast<char>(val));
}
return res;
}
+3
View File
@@ -64,6 +64,9 @@ boost::optional<T> readAs(const std::string& str)
std::string std::string
replaceInString(std::string str, const std::string& from, const std::string& to); replaceInString(std::string str, const std::string& from, const std::string& to);
boost::optional<std::string>
stringFromHex(const std::string& str);
// warning: not efficient // warning: not efficient
template<class In, class Out, class U = typename std::iterator_traits<In>::value_type> template<class In, class Out, class U = typename std::iterator_traits<In>::value_type>
void uniqueAndSortedByOccurence(In first, In last, Out out) void uniqueAndSortedByOccurence(In first, In last, Out out)