From ff662cb341d354407a7ebc62dd39d7523d8eecd0 Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Tue, 4 Mar 2025 08:46:33 +0500 Subject: [PATCH 1/5] allow relative paths in config --- ChangeLog.md | 2 + cpp/server/deadbeef/plugin.cpp | 2 +- cpp/server/foobar2000/plugin.cpp | 2 +- cpp/server/settings.cpp | 69 ++++++++++++++++++++++++-------- cpp/server/settings.hpp | 16 ++++++-- cpp/server/static_controller.cpp | 50 +++-------------------- cpp/server/static_controller.hpp | 2 +- docs/advanced-config.md | 10 ++++- 8 files changed, 82 insertions(+), 71 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 9f05b393..da6ebbcb 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,4 +1,6 @@ # Changes in v0.11 (not released) +- Allow non-absolute paths in config (resolved relative to {player profile directory}/beefweb/) +- Fix ignoring of "clientConfigDir" setting ### DeaDBeeF - Provide universal .deb package to match `deadbeef-static_*.deb` - Fix deadlock with streamer diff --git a/cpp/server/deadbeef/plugin.cpp b/cpp/server/deadbeef/plugin.cpp index b74559a3..64767f53 100644 --- a/cpp/server/deadbeef/plugin.cpp +++ b/cpp/server/deadbeef/plugin.cpp @@ -59,7 +59,7 @@ void Plugin::reconfigure() settings->port = port_; settings->allowRemote = allowRemote_; - settings->musicDirsStr = parseValueList(musicDirs_, ';'); + settings->musicDirsOrig = parseValueList(musicDirs_, ';'); settings->authRequired = authRequired_; settings->authUser = authUser_; settings->authPassword = authPassword_; diff --git a/cpp/server/foobar2000/plugin.cpp b/cpp/server/foobar2000/plugin.cpp index a5c59133..0f8b71f4 100644 --- a/cpp/server/foobar2000/plugin.cpp +++ b/cpp/server/foobar2000/plugin.cpp @@ -37,7 +37,7 @@ void Plugin::reconfigure() settings->port = settings_store::port; settings->allowRemote = settings_store::allowRemote; - settings->musicDirsStr = settings_store::getMusicDirs(); + settings->musicDirsOrig = settings_store::getMusicDirs(); settings->authRequired = settings_store::authRequired; settings->authUser = settings_store::authUser; settings->authPassword = settings_store::authPassword; diff --git a/cpp/server/settings.cpp b/cpp/server/settings.cpp index 36129c6d..b12e9f10 100644 --- a/cpp/server/settings.cpp +++ b/cpp/server/settings.cpp @@ -135,9 +135,9 @@ void SettingsData::initialize(const Path& profileDir) { assert(!profileDir.empty()); - auto configDir = profileDir / MSRV_PATH_LITERAL(MSRV_PROJECT_ID); + baseDir = profileDir / MSRV_PATH_LITERAL(MSRV_PROJECT_ID); - loadFromFile(configDir / MSRV_PATH_LITERAL(MSRV_CONFIG_FILE)); + loadFromFile(baseDir / MSRV_PATH_LITERAL(MSRV_CONFIG_FILE)); auto envConfigFile = getEnvAsPath(MSRV_CONFIG_FILE_ENV); if (!envConfigFile.empty()) @@ -148,30 +148,26 @@ void SettingsData::initialize(const Path& profileDir) logError("ignoring non-absolute config file path: %s", envConfigFile.c_str()); } + webRoot = resolvePath(pathFromUtf8(webRootOrig)).lexically_normal(); + if (webRoot.empty()) { - webRoot = pathToUtf8(getDefaultWebRoot()); + webRoot = getDefaultWebRoot(); } - clientConfigDir = pathFromUtf8(clientConfigDirStr).lexically_normal().make_preferred(); - - if (!clientConfigDir.empty() && !clientConfigDir.is_absolute()) - { - logError("ignoring non-absolute client config dir: %s", clientConfigDirStr.c_str()); - clientConfigDir.clear(); - } + clientConfigDir = resolvePath(pathFromUtf8(clientConfigDirOrig)).lexically_normal(); if (clientConfigDir.empty()) { - clientConfigDir = configDir / MSRV_PATH_LITERAL(MSRV_CLIENT_CONFIG_DIR); + clientConfigDir = baseDir / MSRV_PATH_LITERAL(MSRV_CLIENT_CONFIG_DIR); } musicDirs.clear(); - musicDirs.reserve(musicDirsStr.size()); + musicDirs.reserve(musicDirsOrig.size()); - for (const auto& dir : musicDirsStr) + for (const auto& dir : musicDirsOrig) { - auto path = pathFromUtf8(dir).lexically_normal().make_preferred(); + auto path = resolvePath(pathFromUtf8(dir)).lexically_normal(); if (!path.is_absolute()) { @@ -181,6 +177,44 @@ void SettingsData::initialize(const Path& profileDir) musicDirs.emplace_back(std::move(path)); } + + urlMappings.clear(); + urlMappings.reserve(urlMappingsOrig.size()); + + for (const auto& kv : urlMappingsOrig) + { + if (kv.first.find(':') != std::string::npos) + { + logError("url mapping '%s' contains reserved character ':'", kv.first.c_str()); + continue; + } + + if (kv.first.empty() || kv.first == "/") + { + logError("root url mapping is not allowed, use 'webRoot' instead"); + continue; + } + + if (kv.second.empty()) + { + logError("url mapping '%s' has empty target", kv.first.c_str()); + continue; + } + + std::string prefix(kv.first); + + if (prefix.front() != '/') + prefix.insert(0, 1, '/'); + + if (prefix.back() != '/') + prefix.push_back('/'); + + auto path = resolvePath(pathFromUtf8(kv.second)).lexically_normal(); + + logInfo("using url mapping '%s' -> '%s'", kv.first.c_str(), kv.second.c_str()); + + urlMappings[std::move(prefix)] = std::move(path); + } } void SettingsData::loadFromFile(const Path& path) @@ -206,13 +240,14 @@ void SettingsData::loadFromJson(const Json& json) loadValue(json, &port, "port"); loadValue(json, &allowRemote, "allowRemote"); - loadValue(json, &musicDirsStr, "musicDirs"); - loadValue(json, &webRoot, "webRoot"); + loadValue(json, &musicDirsOrig, "musicDirs"); + loadValue(json, &webRootOrig, "webRoot"); loadValue(json, &authRequired, "authRequired"); loadValue(json, &authUser, "authUser"); loadValue(json, &authPassword, "authPassword"); loadValue(json, &responseHeaders, "responseHeaders"); - loadValue(json, &urlMappings, "urlMappings"); + loadValue(json, &urlMappingsOrig, "urlMappings"); + loadValue(json, &clientConfigDirOrig, "clientConfigDir"); loadPermissions(json); } diff --git a/cpp/server/settings.hpp b/cpp/server/settings.hpp index 0fdd0aff..20727c32 100644 --- a/cpp/server/settings.hpp +++ b/cpp/server/settings.hpp @@ -30,12 +30,14 @@ class SettingsData SettingsData(); ~SettingsData(); + Path baseDir; int port = MSRV_DEFAULT_PORT; bool allowRemote = true; - std::vector musicDirsStr; + std::vector musicDirsOrig; std::vector musicDirs; - std::string webRoot; - std::string clientConfigDirStr; + Path webRoot; + std::string webRootOrig; + std::string clientConfigDirOrig; Path clientConfigDir; ApiPermissions permissions = ApiPermissions::ALL; @@ -43,7 +45,8 @@ class SettingsData std::string authUser; std::string authPassword; std::unordered_map responseHeaders; - std::unordered_map urlMappings; + std::unordered_map urlMappings; + std::unordered_map urlMappingsOrig; static void migrate(const char* appName, const Path& profileDir); static const Path& getDefaultWebRoot(); @@ -57,6 +60,11 @@ class SettingsData bool isAllowedPath(const Path& path) const; void initialize(const Path& profileDir); + Path resolvePath(const Path& path) + { + return path.empty() || path.is_absolute() ? path : baseDir / path; + } + private: void loadFromJson(const Json& json); void loadFromFile(const Path& path); diff --git a/cpp/server/static_controller.cpp b/cpp/server/static_controller.cpp index 04f47482..077ff277 100644 --- a/cpp/server/static_controller.cpp +++ b/cpp/server/static_controller.cpp @@ -52,7 +52,7 @@ ResponsePtr StaticController::getFile() if (requestPath.empty()) return redirectToDirectory(); - auto filePath = (targetDir_ / pathFromUtf8(requestPath)).lexically_normal().make_preferred(); + auto filePath = (targetDir_ / pathFromUtf8(requestPath)).lexically_normal(); if (!isSubpath(targetDir_, filePath)) return Response::notFound(); @@ -89,19 +89,6 @@ void StaticController::defineRoutes( { for (auto& kv : settings->urlMappings) { - if (kv.first.empty() || kv.first == "/") - { - logError("root url mapping is not allowed, use 'webRoot' instead"); - continue; - } - - if (kv.second.empty()) - { - logError("url mapping '%s' has empty target", kv.first.c_str()); - continue; - } - - logInfo("using url mapping '%s' -> '%s'", kv.first.c_str(), kv.second.c_str()); defineRoutes(router, workQueue, kv.first, kv.second, contentTypes); } @@ -113,43 +100,16 @@ void StaticController::defineRoutes( Router* router, WorkQueue* workQueue, const std::string& urlPrefix, - const std::string& targetDir, + const Path& targetDir, const ContentTypeMap& contentTypes) { - if (urlPrefix.find(':') != std::string::npos) - { - logError("url mapping '%s' contains reserved character ':'", urlPrefix.c_str()); - return; - } - - std::string prefix; - - if (urlPrefix.empty() || urlPrefix.front() != '/') - prefix = "/" + urlPrefix; - else - prefix = urlPrefix; - - if (prefix.back() != '/') - prefix.push_back('/'); - - auto target = pathFromUtf8(targetDir).lexically_normal().make_preferred(); - - if (!target.is_absolute()) - { - logError("url mapping '%s' target should be absolute, got '%s'", urlPrefix.c_str(), targetDir.c_str()); - return; - } - auto routes = router->defineRoutes(); - routes.createWith([=](Request* request) { - return new StaticController(request, target, contentTypes); - }); - + routes.createWith([=](Request* request) { return new StaticController(request, targetDir, contentTypes); }); routes.useWorkQueue(workQueue); - routes.get(prefix, &StaticController::getFile); - routes.get(prefix + ":path*", &StaticController::getFile); + routes.get(urlPrefix, &StaticController::getFile); + routes.get(urlPrefix + ":path*", &StaticController::getFile); } } diff --git a/cpp/server/static_controller.hpp b/cpp/server/static_controller.hpp index a1adfcae..ed4eebbd 100644 --- a/cpp/server/static_controller.hpp +++ b/cpp/server/static_controller.hpp @@ -32,7 +32,7 @@ class StaticController : public ControllerBase Router* router, WorkQueue* workQueue, const std::string& urlPrefix, - const std::string& targetDir, + const Path& targetDir, const ContentTypeMap& contentTypes); private: diff --git a/docs/advanced-config.md b/docs/advanced-config.md index 920f2dd2..33829a46 100644 --- a/docs/advanced-config.md +++ b/docs/advanced-config.md @@ -29,6 +29,12 @@ The following options are available: } ``` +### Non-absolute paths + +Unless specified otherwise any path in configuration could be non-absolute. + +Such paths are resolved relative to `{player_profile_dir}/beefweb` directory. + ### Network settings `port: number` - Network port to use (same as in UI) @@ -49,7 +55,7 @@ The following options are available: ### Web server settings -`webRoot: string` - Root directory where static web content is located. This path has to be absolute. +`webRoot: string` - Root directory where static web content is located. `urlMappings: {string: string}` - Alternative web directories defined by URL prefix @@ -79,4 +85,4 @@ Please read [documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/CO ### Other settings -`clientConfigDir: string` - Path to directory where client configuration is stored. Must be absolute. +`clientConfigDir: string` - Path to directory where client configuration is stored. From 3483a87b09fb142cae70852c1e4d6fca613e1def Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Tue, 4 Mar 2025 08:50:02 +0500 Subject: [PATCH 2/5] avoid calling make_preferred() after lexically_normal() --- cpp/server/browser_controller.cpp | 2 +- cpp/server/playlists_controller.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/server/browser_controller.cpp b/cpp/server/browser_controller.cpp index 1a95c515..e3baad9c 100644 --- a/cpp/server/browser_controller.cpp +++ b/cpp/server/browser_controller.cpp @@ -83,7 +83,7 @@ ResponsePtr BrowserController::getRoots() ResponsePtr BrowserController::getEntries() { auto requestedPath = param("path"); - auto normalizedPath = pathFromUtf8(requestedPath).lexically_normal().make_preferred(); + auto normalizedPath = pathFromUtf8(requestedPath).lexically_normal(); if (!settings_->isAllowedPath(normalizedPath)) return Response::error(HttpStatus::S_403_FORBIDDEN, "listing directory is not allowed"); diff --git a/cpp/server/playlists_controller.cpp b/cpp/server/playlists_controller.cpp index 7afb8c27..c9dc3253 100644 --- a/cpp/server/playlists_controller.cpp +++ b/cpp/server/playlists_controller.cpp @@ -128,7 +128,7 @@ std::string PlaylistsController::validateAndNormalizeItem(const std::string& ite else path = pathFromUtf8(item); - path = path.lexically_normal().make_preferred(); + path = path.lexically_normal(); if (settings_->isAllowedPath(path)) return pathToUtf8(path); From 1c3b3f2d891dc52bdff3390af096fa99a6b2e6e0 Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Tue, 4 Mar 2025 09:07:10 +0500 Subject: [PATCH 3/5] playlist_controller: simplify code --- cpp/server/playlists_controller.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cpp/server/playlists_controller.cpp b/cpp/server/playlists_controller.cpp index c9dc3253..75014fee 100644 --- a/cpp/server/playlists_controller.cpp +++ b/cpp/server/playlists_controller.cpp @@ -133,10 +133,7 @@ std::string PlaylistsController::validateAndNormalizeItem(const std::string& ite if (settings_->isAllowedPath(path)) return pathToUtf8(path); - request()->response = Response::error(HttpStatus::S_403_FORBIDDEN, "item is not under allowed path: " + item); - request()->setProcessed(); - - throw InvalidRequestException(); + throw OperationForbiddenException("item is not under allowed path: " + item); } ResponsePtr PlaylistsController::addItems() From 6cd84e2b62dc59e5772831c7c313ccce39634466 Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Tue, 4 Mar 2025 09:11:00 +0500 Subject: [PATCH 4/5] allow relative music dirs --- cpp/server/settings.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cpp/server/settings.cpp b/cpp/server/settings.cpp index b12e9f10..5aef4012 100644 --- a/cpp/server/settings.cpp +++ b/cpp/server/settings.cpp @@ -165,17 +165,18 @@ void SettingsData::initialize(const Path& profileDir) musicDirs.clear(); musicDirs.reserve(musicDirsOrig.size()); + auto index = 0; for (const auto& dir : musicDirsOrig) { - auto path = resolvePath(pathFromUtf8(dir)).lexically_normal(); - - if (!path.is_absolute()) + if (dir.empty()) { - logError("ignoring non-absolute music dir: %s", dir.c_str()); + logError("skipping empty music directory at index %d", index); continue; } + auto path = resolvePath(pathFromUtf8(dir)).lexically_normal(); musicDirs.emplace_back(std::move(path)); + index++; } urlMappings.clear(); From e4c77b273591972537a6f4c4af46cc41530df531 Mon Sep 17 00:00:00 2001 From: Hyperblast Date: Tue, 4 Mar 2025 09:28:20 +0500 Subject: [PATCH 5/5] plugin.cpp: wrap config init in try/catch --- cpp/server/foobar2000/plugin.cpp | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/cpp/server/foobar2000/plugin.cpp b/cpp/server/foobar2000/plugin.cpp index 0f8b71f4..cedb89bd 100644 --- a/cpp/server/foobar2000/plugin.cpp +++ b/cpp/server/foobar2000/plugin.cpp @@ -12,8 +12,9 @@ Plugin::Plugin() host_(&player_) { assert(!current_); - reconfigure(); current_ = this; + + reconfigure(); } Plugin::~Plugin() @@ -33,19 +34,21 @@ Path Plugin::getProfileDir() void Plugin::reconfigure() { - auto settings = std::make_shared(); + tryCatchLog([&] { + auto settings = std::make_shared(); - settings->port = settings_store::port; - settings->allowRemote = settings_store::allowRemote; - settings->musicDirsOrig = settings_store::getMusicDirs(); - settings->authRequired = settings_store::authRequired; - settings->authUser = settings_store::authUser; - settings->authPassword = settings_store::authPassword; - settings->permissions = settings_store::getPermissions(); + settings->port = settings_store::port; + settings->allowRemote = settings_store::allowRemote; + settings->musicDirsOrig = settings_store::getMusicDirs(); + settings->authRequired = settings_store::authRequired; + settings->authUser = settings_store::authUser; + settings->authPassword = settings_store::authPassword; + settings->permissions = settings_store::getPermissions(); - settings->initialize(getProfileDir()); + settings->initialize(getProfileDir()); - host_.reconfigure(std::move(settings)); + host_.reconfigure(std::move(settings)); + }); } Plugin* Plugin::current_ = nullptr;