From 8e9806f35cbeeaa824a4e3858883e204473cc047 Mon Sep 17 00:00:00 2001 From: badaix Date: Tue, 21 Jan 2025 22:25:04 +0100 Subject: [PATCH] controlscript must be located in plugin_dir --- common/utils/string_utils.cpp | 27 +++++++++++++++- common/utils/string_utils.hpp | 5 ++- doc/json_rpc_api/control.md | 2 +- server/control_requests.cpp | 35 +++++++++++++++----- server/etc/snapserver.conf | 3 ++ server/server_settings.hpp | 3 +- server/snapserver.cpp | 3 ++ server/streamreader/pcm_stream.cpp | 2 +- server/streamreader/stream_control.cpp | 10 +++--- server/streamreader/stream_control.hpp | 5 +-- server/streamreader/stream_uri.cpp | 38 +++++++++++++++------- server/streamreader/stream_uri.hpp | 45 ++++++++++++++++++-------- test/test_main.cpp | 5 ++- 13 files changed, 137 insertions(+), 46 deletions(-) diff --git a/common/utils/string_utils.cpp b/common/utils/string_utils.cpp index cc9241eb..6edd0f16 100644 --- a/common/utils/string_utils.cpp +++ b/common/utils/string_utils.cpp @@ -1,6 +1,6 @@ /*** This file is part of snapcast - Copyright (C) 2014-2024 Johannes Pohl + Copyright (C) 2014-2025 Johannes Pohl This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -105,6 +105,31 @@ std::string trim_copy(const std::string& s) return trim(str); } + +std::string urlEncode(const std::string& str) +{ + std::ostringstream os; + for (std::string::const_iterator ci = str.begin(); ci != str.end(); ++ci) + { + if ((*ci >= 'a' && *ci <= 'z') || (*ci >= 'A' && *ci <= 'Z') || (*ci >= '0' && *ci <= '9')) + { // allowed + os << *ci; + } + else if (*ci == ' ') + { + os << '+'; + } + else + { + auto toHex = [](unsigned char x) { return static_cast(x + (x > 9 ? ('A' - 10) : '0')); }; + os << '%' << toHex(*ci >> 4) << toHex(*ci % 16); + } + } + + return os.str(); +} + + // decode %xx to char std::string uriDecode(const std::string& src) { diff --git a/common/utils/string_utils.hpp b/common/utils/string_utils.hpp index 81e2a947..534c903e 100644 --- a/common/utils/string_utils.hpp +++ b/common/utils/string_utils.hpp @@ -1,6 +1,6 @@ /*** This file is part of snapcast - Copyright (C) 2014-2024 Johannes Pohl + Copyright (C) 2014-2025 Johannes Pohl This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -54,6 +54,9 @@ std::string trim_copy(const std::string& s); /// decode %xx to char std::string uriDecode(const std::string& src); +/// @return uri encoded version of @p str +std::string urlEncode(const std::string& str); + /// Split string @p s at @p delim into @p left and @p right void split_left(const std::string& s, char delim, std::string& left, std::string& right); diff --git a/doc/json_rpc_api/control.md b/doc/json_rpc_api/control.md index 33e3f31e..94d24ffa 100644 --- a/doc/json_rpc_api/control.md +++ b/doc/json_rpc_api/control.md @@ -485,7 +485,7 @@ See [Plugin.Stream.Player.SetProperty](stream_plugin.md#pluginstreamplayersetpro ### Stream.AddStream Note: For security purposes, the RPC interface allows only adding streams of these types: `pipe`, `file`, `tcp`, `alsa`, `jack` and `meta`. -It is also not allowed to set the `controlscript` query parameter of `streamUri`. +The optional`controlscript` of the `streamUri` must be located in `[stream] plugin_dir` (configured in `snapserver.conf`, default `/usr/share/snapserver/plug-ins`), can be an absolute or relative path. #### Request diff --git a/server/control_requests.cpp b/server/control_requests.cpp index 41610a83..5c23f2e0 100644 --- a/server/control_requests.cpp +++ b/server/control_requests.cpp @@ -22,11 +22,13 @@ // local headers #include "common/aixlog.hpp" #include "common/message/server_settings.hpp" +#include "jsonrpcpp.hpp" #include "server.hpp" // 3rd party headers // standard headers +#include #include #include @@ -692,18 +694,35 @@ void StreamAddRequest::execute(const jsonrpcpp::request_ptr& request, AuthInfo& // Don't allow adding streams that start a user defined process: CVE-2023-36177 static constexpr std::array whitelist{"pipe", "file", "tcp", "alsa", "jack", "meta"}; - const std::string stream_uri = request->params().get("streamUri"); - const StreamUri parsedUri(stream_uri); + std::string stream_uri = request->params().get("streamUri"); + StreamUri parsed_uri(stream_uri); - if (std::find(whitelist.begin(), whitelist.end(), parsedUri.scheme) == whitelist.end()) - throw jsonrpcpp::InvalidParamsException("Adding '" + parsedUri.scheme + "' streams is not allowed", request->id()); + if (std::find(whitelist.begin(), whitelist.end(), parsed_uri.scheme) == whitelist.end()) + throw jsonrpcpp::InvalidParamsException("Adding '" + parsed_uri.scheme + "' streams is not allowed", request->id()); - // Don't allow settings the controlscript streamUri property - if (!parsedUri.getQuery("controlscript").empty()) - throw jsonrpcpp::InvalidParamsException("No 'controlscript' streamUri property allowed", request->id()); + std::filesystem::path script = parsed_uri.getQuery("controlscript"); + if (!script.empty()) + { + // script must be located in the [stream] plugin_dir + std::filesystem::path plugin_dir = getSettings().stream.plugin_dir; + // if script file name is relative, prepend the plugin_dir + if (!script.is_absolute()) + script = plugin_dir / script; + // convert to normalized absolute path + script = std::filesystem::weakly_canonical(script); + LOG(DEBUG, LOG_TAG) << "controlscript: " << script.native() << "\n"; + // check if script is directly located in plugin_dir + if (script.parent_path() != plugin_dir) + throw jsonrpcpp::InvalidParamsException("controlscript must be located in '" + plugin_dir.native() + "'"); + if (!std::filesystem::exists(script)) + throw jsonrpcpp::InvalidParamsException("controlscript '" + script.native() + "' does not exist"); + parsed_uri.query["controlscript"] = script; + LOG(DEBUG, LOG_TAG) << "Raw stream uri: " << stream_uri << "\n"; + stream_uri = parsed_uri.toString(); + } std::ignore = authinfo; - LOG(INFO, LOG_TAG) << "Stream.AddStream(" << request->params().get("streamUri") << ")\n"; + LOG(INFO, LOG_TAG) << "Stream.AddStream(" << stream_uri << ")\n"; // Add stream PcmStreamPtr stream = getStreamManager().addStream(stream_uri); diff --git a/server/etc/snapserver.conf b/server/etc/snapserver.conf index 1e08f815..ac8721cb 100644 --- a/server/etc/snapserver.conf +++ b/server/etc/snapserver.conf @@ -166,6 +166,9 @@ doc_root = /usr/share/snapserver/snapweb # meta: meta://///.../?name= source = pipe:///tmp/snapfifo?name=default +# Plugin directory, containing scripts, referred by "controlscript" +# plugin_dir = /usr/share/snapserver/plug-ins + # Default sample format: :: #sampleformat = 48000:16:2 diff --git a/server/server_settings.hpp b/server/server_settings.hpp index 7bbb81b9..d3935227 100644 --- a/server/server_settings.hpp +++ b/server/server_settings.hpp @@ -1,6 +1,6 @@ /*** This file is part of snapcast - Copyright (C) 2014-2024 Johannes Pohl + Copyright (C) 2014-2025 Johannes Pohl This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -92,6 +92,7 @@ struct ServerSettings struct Stream { size_t port{1704}; + std::filesystem::path plugin_dir{"/usr/share/snapserver/plug-ins"}; std::vector sources; std::string codec{"flac"}; int32_t bufferMs{1000}; diff --git a/server/snapserver.cpp b/server/snapserver.cpp index 68d0f928..2b68c697 100644 --- a/server/snapserver.cpp +++ b/server/snapserver.cpp @@ -113,6 +113,7 @@ int main(int argc, char* argv[]) settings.tcp.bind_to_address.front(), &settings.tcp.bind_to_address[0]); // stream settings + conf.add>("", "stream.plugin_dir", "stream plugin directory", settings.stream.plugin_dir, &settings.stream.plugin_dir); auto stream_bind_to_address = conf.add>("", "stream.bind_to_address", "address for the server to listen on", settings.stream.bind_to_address.front(), &settings.stream.bind_to_address[0]); conf.add>("", "stream.port", "which port the server should listen on", settings.stream.port, &settings.stream.port); @@ -296,6 +297,8 @@ int main(int argc, char* argv[]) if (!streamValue->is_set() && !sourceValue->is_set()) settings.stream.sources.push_back(sourceValue->value()); + settings.stream.plugin_dir = std::filesystem::weakly_canonical(settings.stream.plugin_dir); + LOG(INFO, LOG_TAG) << "Stream plugin directory: " << settings.stream.plugin_dir << "\n"; for (size_t n = 0; n < streamValue->count(); ++n) { LOG(INFO, LOG_TAG) << "Adding stream: " << streamValue->value(n) << "\n"; diff --git a/server/streamreader/pcm_stream.cpp b/server/streamreader/pcm_stream.cpp index 506d1c89..02b4ea8d 100644 --- a/server/streamreader/pcm_stream.cpp +++ b/server/streamreader/pcm_stream.cpp @@ -72,7 +72,7 @@ PcmStream::PcmStream(PcmStream::Listener* pcmListener, boost::asio::io_context& std::string params; if (uri_.query.find(kControlScriptParams) != uri_.query.end()) params = uri_.query[kControlScriptParams]; - stream_ctrl_ = std::make_unique(strand_, uri_.query[kControlScript], params); + stream_ctrl_ = std::make_unique(strand_, server_settings_.stream.plugin_dir, uri_.query[kControlScript], std::move(params)); } if (uri_.query.find(kUriChunkMs) != uri_.query.end()) diff --git a/server/streamreader/stream_control.cpp b/server/streamreader/stream_control.cpp index 54a5e420..319a8610 100644 --- a/server/streamreader/stream_control.cpp +++ b/server/streamreader/stream_control.cpp @@ -129,15 +129,15 @@ void StreamControl::onLog(std::string message) -ScriptStreamControl::ScriptStreamControl(const boost::asio::any_io_executor& executor, const std::string& script, const std::string& params) - : StreamControl(executor), script_(script), params_(params) +ScriptStreamControl::ScriptStreamControl(const boost::asio::any_io_executor& executor, const std::filesystem::path& plugin_dir, std::string script, + std::string params) + : StreamControl(executor), script_(std::move(script)), params_(std::move(params)) { namespace fs = utils::file; if (!fs::exists(script_)) { - std::string plugin_path = "/usr/share/snapserver/plug-ins/"; - if (fs::exists(plugin_path + script_)) - script_ = plugin_path + script_; + if (fs::exists(plugin_dir / script_)) + script_ = plugin_dir / script_; else throw SnapException("Control script not found: \"" + script_ + "\""); } diff --git a/server/streamreader/stream_control.hpp b/server/streamreader/stream_control.hpp index cffecd62..32578fee 100644 --- a/server/streamreader/stream_control.hpp +++ b/server/streamreader/stream_control.hpp @@ -28,6 +28,7 @@ #include // standard headers +#include #include #include @@ -78,10 +79,10 @@ private: class ScriptStreamControl : public StreamControl { public: - ScriptStreamControl(const boost::asio::any_io_executor& executor, const std::string& script, const std::string& params); + ScriptStreamControl(const boost::asio::any_io_executor& executor, const std::filesystem::path& plugin_dir, std::string script, std::string params); virtual ~ScriptStreamControl() = default; -protected: +private: /// Send a message to stdin of the process void doCommand(const jsonrpcpp::Request& request) override; void doStart(const std::string& stream_id, const ServerSettings& server_setttings) override; diff --git a/server/streamreader/stream_uri.cpp b/server/streamreader/stream_uri.cpp index d5da4c84..a9f645a9 100644 --- a/server/streamreader/stream_uri.cpp +++ b/server/streamreader/stream_uri.cpp @@ -1,6 +1,6 @@ /*** This file is part of snapcast - Copyright (C) 2014-2024 Johannes Pohl + Copyright (C) 2014-2025 Johannes Pohl This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -31,6 +31,8 @@ using namespace std; namespace strutils = utils::string; +static constexpr auto LOG_TAG = "StreamUri"; + namespace streamreader { @@ -40,34 +42,39 @@ StreamUri::StreamUri(const std::string& uri) } -void StreamUri::parse(const std::string& streamUri) +void StreamUri::parse(const std::string& stream_uri) { // https://en.wikipedia.org/wiki/Uniform_Resource_Identifier // scheme:[//[user:password@]host[:port]][/]path[?query][#fragment] // would be more elegant with regex. Not yet supported on my dev machine's gcc 4.8 :( - LOG(DEBUG) << "StreamUri: " << streamUri << "\n"; + LOG(DEBUG, LOG_TAG) << "StreamUri: " << stream_uri << "\n"; size_t pos; - uri = strutils::trim_copy(streamUri); + + // Remove leading and trailing quotes + uri = strutils::trim_copy(stream_uri); while (!uri.empty() && ((uri[0] == '\'') || (uri[0] == '"'))) uri = uri.substr(1); while (!uri.empty() && ((uri[uri.length() - 1] == '\'') || (uri[uri.length() - 1] == '"'))) uri = uri.substr(0, this->uri.length() - 1); // string decodedUri = strutils::uriDecode(uri); - // LOG(DEBUG) << "StreamUri decoded: " << decodedUri << "\n"; + // LOG(DEBUG, LOG_TAG) << "StreamUri decoded: " << decodedUri << "\n"; string tmp(uri); + // Parse scheme pos = tmp.find(':'); if (pos == string::npos) throw invalid_argument("missing ':'"); scheme = strutils::uriDecode(strutils::trim_copy(tmp.substr(0, pos))); tmp = tmp.substr(pos + 1); - LOG(TRACE) << "scheme: '" << scheme << "', tmp: '" << tmp << "'\n"; + LOG(TRACE, LOG_TAG) << "scheme: '" << scheme << "', tmp: '" << tmp << "'\n"; + // tmp = //[user:password@]host[:port][/]path[?query][#fragment] if (tmp.find("//") != 0) throw invalid_argument("missing host separator: '//'"); tmp = tmp.substr(2); + // tmp = [user:password@]host[:port][/]path[?query][#fragment] pos = tmp.find('/'); if (pos == string::npos) @@ -76,13 +83,15 @@ void StreamUri::parse(const std::string& streamUri) if (pos == string::npos) pos = tmp.length(); } + // [user:password@]host[:port][/]path[?query][#fragment] + // pos: ^ or ^ or ^ host = strutils::uriDecode(strutils::trim_copy(tmp.substr(0, pos))); tmp = tmp.substr(pos); path = tmp; pos = std::min(path.find('?'), path.find('#')); path = strutils::uriDecode(strutils::trim_copy(path.substr(0, pos))); - LOG(TRACE) << "host: '" << host << "', tmp: '" << tmp << "', path: '" << path << "'\n"; + LOG(TRACE, LOG_TAG) << "host: '" << host << "', tmp: '" << tmp << "', path: '" << path << "'\n"; string queryStr; pos = tmp.find('?'); @@ -90,7 +99,7 @@ void StreamUri::parse(const std::string& streamUri) { tmp = tmp.substr(pos + 1); queryStr = tmp; - LOG(TRACE) << "path: '" << path << "', tmp: '" << tmp << "', query: '" << queryStr << "'\n"; + LOG(TRACE, LOG_TAG) << "path: '" << path << "', tmp: '" << tmp << "', query: '" << queryStr << "'\n"; } pos = tmp.find('#'); @@ -99,7 +108,7 @@ void StreamUri::parse(const std::string& streamUri) queryStr = tmp.substr(0, pos); tmp = tmp.substr(pos + 1); fragment = strutils::uriDecode(strutils::trim_copy(tmp)); - LOG(TRACE) << "query: '" << queryStr << "', fragment: '" << fragment << "', tmp: '" << tmp << "'\n"; + LOG(TRACE, LOG_TAG) << "query: '" << queryStr << "', fragment: '" << fragment << "', tmp: '" << tmp << "'\n"; } vector keyValueList = strutils::split(queryStr, '&'); @@ -113,15 +122,16 @@ void StreamUri::parse(const std::string& streamUri) query[key] = value; } } - LOG(DEBUG) << "StreamUri.toString: " << toString() << "\n"; + LOG(DEBUG, LOG_TAG) << "StreamUri.toString: " << toString() << "\n"; } std::string StreamUri::toString() const { + // TODO: path must be properly be uri encoded // scheme:[//[user:password@]host[:port]][/]path[?query][#fragment] stringstream ss; - ss << scheme << "://" << host << "/" + path; + ss << scheme << "://" << host << path; if (!query.empty()) { ss << "?"; @@ -155,4 +165,10 @@ std::string StreamUri::getQuery(const std::string& key, const std::string& def) return iter->second; return def; } + +bool StreamUri::operator==(const StreamUri& other) const +{ + return (other.scheme == scheme) && (other.host == host) && (other.path == path) && (other.query == query) && (other.fragment == fragment); +} + } // namespace streamreader diff --git a/server/streamreader/stream_uri.hpp b/server/streamreader/stream_uri.hpp index bd08b3e3..d95419cb 100644 --- a/server/streamreader/stream_uri.hpp +++ b/server/streamreader/stream_uri.hpp @@ -1,6 +1,6 @@ /*** This file is part of snapcast - Copyright (C) 2014-2020 Johannes Pohl + Copyright (C) 2014-2025 Johannes Pohl This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -32,32 +32,49 @@ using json = nlohmann::json; namespace streamreader { -// scheme:[//[user:password@]host[:port]][/]path[?query][#fragment] +/// URI with the general format: +/// scheme:[//[user:password@]host[:port]][/]path[?query][#fragment] struct StreamUri { - StreamUri(const std::string& uri); + /// c'tor construct from string @p uri + explicit StreamUri(const std::string& uri); + + /// the complete uri std::string uri; + /// the scheme component (pipe, http, file, tcp, ...) std::string scheme; - /* struct Authority - { - std::string username; - std::string password; - std::string host; - size_t port; - }; - Authority authority; - */ + // struct Authority + // { + // std::string username; + // std::string password; + // std::string host; + // size_t port; + // }; + // Authority authority; + + /// the host component std::string host; + /// the path component std::string path; + /// the query component: "key = value" pairs std::map query; + /// the fragment component std::string fragment; - std::string id() const; + /// @return URI as json json toJson() const; + + /// @return value for a @p key or @p def, if key does not exist std::string getQuery(const std::string& key, const std::string& def = "") const; - void parse(const std::string& streamUri); + /// parse @p stream_uri string + void parse(const std::string& stream_uri); + + /// @return uri as string std::string toString() const; + + /// @return true if @p other is equal to this + bool operator==(const StreamUri& other) const; }; } // namespace streamreader diff --git a/test/test_main.cpp b/test/test_main.cpp index 0c696f15..a35bae96 100644 --- a/test/test_main.cpp +++ b/test/test_main.cpp @@ -1,6 +1,6 @@ /*** This file is part of snapcast - Copyright (C) 2014-2024 Johannes Pohl + Copyright (C) 2014-2025 Johannes Pohl This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -290,6 +290,9 @@ TEST_CASE("Uri") REQUIRE(uri.query["devicename"] == "Snapcast"); REQUIRE(uri.query["bitrate"] == "320"); REQUIRE(uri.query["killall"] == "false"); + REQUIRE(uri.toString().find("spotify:///librespot?") == 0); + StreamUri uri_from_str{uri.toString()}; + // REQUIRE(uri == uri_from_str); }