Subsonic API: fixed unwanted termination when bad client parameters are received

This commit is contained in:
emeric
2026-03-10 15:51:49 +01:00
parent 10707d8f84
commit f9ae188be3
4 changed files with 80 additions and 69 deletions
@@ -107,5 +107,4 @@ namespace lms::api::subsonic
{ {
return _isOpenSubsonicEnabled; return _isOpenSubsonicEnabled;
} }
} // namespace lms::api::subsonic } // namespace lms::api::subsonic
@@ -77,5 +77,4 @@ namespace lms::api::subsonic
const ProtocolVersion _serverProtocolVersion; const ProtocolVersion _serverProtocolVersion;
const bool _isOpenSubsonicEnabled; const bool _isOpenSubsonicEnabled;
}; };
} // namespace lms::api::subsonic } // namespace lms::api::subsonic
+76 -65
View File
@@ -36,7 +36,6 @@
#include "services/auth/IPasswordService.hpp" #include "services/auth/IPasswordService.hpp"
#include "ParameterParsing.hpp" #include "ParameterParsing.hpp"
#include "ProtocolVersion.hpp"
#include "RequestContext.hpp" #include "RequestContext.hpp"
#include "SubsonicResponse.hpp" #include "SubsonicResponse.hpp"
#include "endpoints/AlbumSongLists.hpp" #include "endpoints/AlbumSongLists.hpp"
@@ -291,91 +290,48 @@ namespace lms::api::subsonic
const std::size_t requestId{ curRequestId++ }; const std::size_t requestId{ curRequestId++ };
TLSMonotonicMemoryResourceCleaner memoryResourceCleaner; TLSMonotonicMemoryResourceCleaner memoryResourceCleaner;
LMS_LOG(API_SUBSONIC, DEBUG, "Handling request " << requestId << " '" << request.pathInfo() << "', continuation = " << (request.continuation() ? "true" : "false") << ", params = " << parameterMapToDebugString(request.getParameterMap())); constexpr std::string_view optionalSuffix{ ".view" };
std::string requestPath{ request.pathInfo() }; std::string requestPath{ request.pathInfo() };
if (core::stringUtils::stringEndsWith(requestPath, ".view")) if (core::stringUtils::stringEndsWith(requestPath, optionalSuffix))
requestPath.resize(requestPath.length() - 5); requestPath.resize(requestPath.length() - optionalSuffix.size());
RequestContext requestContext{ request, _db.getTLSSession(), _config }; LMS_LOG(API_SUBSONIC, DEBUG, "Handling request " << requestId << " to '" << requestPath << " with params = " << parameterMapToDebugString(request.getParameterMap()) << "', continuation = " << (request.continuation() ? "true" : "false"));
// First check for media retrieval endpoints
auto itStreamHandler{ mediaRetrievalHandlers.find(requestPath) };
if (itStreamHandler != mediaRetrievalHandlers.end())
{
try
{
LMS_SCOPED_TRACE_OVERVIEW("Subsonic", itStreamHandler->first);
handleMediaRetrievalRequest(itStreamHandler->second, requestContext, request, response);
LMS_LOG(API_SUBSONIC, DEBUG, "Request " << requestId << " '" << requestPath << "' handled!");
}
catch (const Error& e)
{
LMS_LOG(API_SUBSONIC, ERROR, "Error while processing request '" << requestId << "', code = " << static_cast<int>(e.getCode()) << ", msg = '" << e.getMessage() << "'");
}
return;
}
// Now check other endpoints
try try
{ {
if (auto itEntryPoint{ requestEntryPoints.find(requestPath) }; itEntryPoint != requestEntryPoints.end()) if (!handleMediaRetrievalRequest(requestPath, request, response))
{ handleRequest(requestPath, request, response);
LMS_SCOPED_TRACE_OVERVIEW("Subsonic", itEntryPoint->first);
db::User::pointer user; LMS_LOG(API_SUBSONIC, DEBUG, "Request " << requestId << " to '" << requestPath << "' handled!");
if (itEntryPoint->second.authMode == AuthenticationMode::Authenticated)
{
user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request));
checkUserTypeIsAllowed(user, itEntryPoint->second.allowedUserTypes);
requestContext.setUser(user);
}
const Response resp{ [&] {
LMS_SCOPED_TRACE_DETAILED("Subsonic", "HandleRequest");
return itEntryPoint->second.func(requestContext);
}() };
{
LMS_SCOPED_TRACE_DETAILED("Subsonic", "WriteResponse");
resp.write(response.out(), requestContext.getResponseFormat());
response.setMimeType(std::string{ ResponseFormatToMimeType(requestContext.getResponseFormat()) });
}
LMS_LOG(API_SUBSONIC, DEBUG, "Request " << requestId << " '" << requestPath << "' handled!");
return;
}
// do not disclose unhandled commands for unauthenticated users
authenticateUser(request);
LMS_LOG(API_SUBSONIC, ERROR, "Unhandled command '" << requestPath << "'");
throw UnknownEntryPointGenericError{};
} }
catch (const Error& e) catch (const Error& e)
{ {
LMS_LOG(API_SUBSONIC, ERROR, "Error while processing request '" << requestPath << "'" << ", params = [" << parameterMapToDebugString(request.getParameterMap()) << "]" << ", code = " << static_cast<int>(e.getCode()) << ", msg = '" << e.getMessage() << "'"); LMS_LOG(API_SUBSONIC, ERROR, "Error while processing request " << requestId << " to '" << requestPath << "' with params = " << parameterMapToDebugString(request.getParameterMap()) << ": code = " << static_cast<int>(e.getCode()) << ", msg = '" << e.getMessage() << "'");
Response resp{ Response::createFailedResponse(requestContext.getServerProtocolVersion(), e) };
resp.write(response.out(), requestContext.getResponseFormat());
response.setMimeType(std::string{ ResponseFormatToMimeType(requestContext.getResponseFormat()) });
} }
} }
void SubsonicResource::handleMediaRetrievalRequest(const MediaRetrievalHandlerFunc& handler, RequestContext& requestContext, const Wt::Http::Request& request, Wt::Http::Response& response) bool SubsonicResource::handleMediaRetrievalRequest(const std::string& requestPath, const Wt::Http::Request& request, Wt::Http::Response& response)
{ {
auto itStreamHandler{ mediaRetrievalHandlers.find(requestPath) };
if (itStreamHandler == mediaRetrievalHandlers.end())
return false;
LMS_SCOPED_TRACE_OVERVIEW("Subsonic", itStreamHandler->first);
try try
{ {
// Media retrieval endpoints are always authenticated RequestContext requestContext{ request, _db.getTLSSession(), _config };
// Optimization: no need to reauth user for each continuation
// Media retrieval endpoints are always authenticated but we don't reauth user for a continuation
db::User::pointer user; db::User::pointer user;
if (!request.continuation()) if (!request.continuation())
user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request)); user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request));
requestContext.setUser(user); requestContext.setUser(user);
handler(requestContext, request, response); itStreamHandler->second(requestContext, request, response);
return true;
} }
catch (const UserNotAuthorizedError&) catch (const UserNotAuthorizedError&)
{ {
@@ -409,6 +365,61 @@ namespace lms::api::subsonic
} }
} }
void SubsonicResource::handleRequest(const std::string& requestPath, const Wt::Http::Request& request, Wt::Http::Response& response)
{
auto writeResponse{ [&](const Response& resp, ResponseFormat format) {
LMS_SCOPED_TRACE_DETAILED("Subsonic", "WriteResponse");
resp.write(response.out(), format);
response.setMimeType(std::string{ ResponseFormatToMimeType(format) });
} };
std::optional<RequestContext> requestContext;
try
{
requestContext.emplace(request, _db.getTLSSession(), _config);
}
catch (const Error& e)
{
writeResponse(Response::createFailedResponse(defaultServerProtocolVersion, e), ResponseFormat::xml);
throw;
}
try
{
if (auto itEntryPoint{ requestEntryPoints.find(requestPath) }; itEntryPoint != requestEntryPoints.end())
{
LMS_SCOPED_TRACE_OVERVIEW("Subsonic", itEntryPoint->first);
db::User::pointer user;
if (itEntryPoint->second.authMode == AuthenticationMode::Authenticated)
{
user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request));
checkUserTypeIsAllowed(user, itEntryPoint->second.allowedUserTypes);
requestContext->setUser(user);
}
const Response resp{ [&] {
LMS_SCOPED_TRACE_DETAILED("Subsonic", "HandleRequest");
return itEntryPoint->second.func(*requestContext);
}() };
writeResponse(resp, requestContext->getResponseFormat());
return;
}
// do not disclose unhandled commands for unauthenticated users
authenticateUser(request);
LMS_LOG(API_SUBSONIC, ERROR, "Unhandled command '" << requestPath << "'");
throw UnknownEntryPointGenericError{};
}
catch (const Error& e)
{
Response resp{ Response::createFailedResponse(requestContext->getServerProtocolVersion(), e) };
writeResponse(resp, requestContext->getResponseFormat());
throw;
}
}
db::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request) db::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request)
{ {
const auto& parameters{ request.getParameterMap() }; const auto& parameters{ request.getParameterMap() };
+4 -2
View File
@@ -18,6 +18,8 @@
*/ */
#pragma once #pragma once
#include <string>
#include <Wt/Http/Request.h> #include <Wt/Http/Request.h>
#include <Wt/Http/Response.h> #include <Wt/Http/Response.h>
#include <Wt/WResource.h> #include <Wt/WResource.h>
@@ -43,8 +45,8 @@ namespace lms::api::subsonic
private: private:
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
using MediaRetrievalHandlerFunc = std::function<void(RequestContext&, const Wt::Http::Request&, Wt::Http::Response&)>; bool handleMediaRetrievalRequest(const std::string& requestPath, const Wt::Http::Request& request, Wt::Http::Response& response);
void handleMediaRetrievalRequest(const MediaRetrievalHandlerFunc& handler, RequestContext& requestContext, const Wt::Http::Request& request, Wt::Http::Response& response); void handleRequest(const std::string& requestPath, const Wt::Http::Request& request, Wt::Http::Response& response);
db::UserId authenticateUser(const Wt::Http::Request& request); db::UserId authenticateUser(const Wt::Http::Request& request);