diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 539805fd..19f2ce99 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -2,6 +2,7 @@ set(SERVER_SOURCES authinfo.cpp config.cpp control_server.cpp + control_requests.cpp control_session_tcp.cpp control_session_http.cpp control_session_ws.cpp diff --git a/server/control_requests.cpp b/server/control_requests.cpp new file mode 100644 index 00000000..6601ea75 --- /dev/null +++ b/server/control_requests.cpp @@ -0,0 +1,897 @@ +/*** + This file is part of snapcast + Copyright (C) 2014-2024 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 + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 this program. If not, see . +***/ + +// prototype/interface header file +#include "control_requests.hpp" + +// local headers +#include "common/aixlog.hpp" +#include "common/message/server_settings.hpp" +#include "server.hpp" + +// 3rd party headers + +// standard headers +#include + + +static constexpr auto LOG_TAG = "ControlRequest"; + + +Request::Request(const Server& server, const std::string& method, const std::string& ressource) : server_(server), method_(method), ressource_(ressource) +{ +} + +bool Request::hasPermission(const AuthInfo& authinfo) const +{ + return authinfo.hasPermission(ressource_); +} + +const std::string& Request::method() const +{ + return method_; +} + + +const StreamServer& Request::getStreamServer() const +{ + return *server_.streamServer_; +} + +const StreamManager& Request::getStreamManager() const +{ + return *server_.streamManager_; +} + +const ServerSettings& Request::getSettings() const +{ + return server_.settings_; +} + + + +ControlRequestFactory::ControlRequestFactory(const Server& server) +{ + auto add_request = [&](std::shared_ptr&& request) { request_map_[request->method()] = std::move(request); }; + + // Client requests + add_request(std::make_shared(server)); + add_request(std::make_shared(server)); + add_request(std::make_shared(server)); + add_request(std::make_shared(server)); + + // Group requests + add_request(std::make_shared(server)); + add_request(std::make_shared(server)); + add_request(std::make_shared(server)); + add_request(std::make_shared(server)); + add_request(std::make_shared(server)); + + // Stream requests + add_request(std::make_shared(server)); + add_request(std::make_shared(server)); + add_request(std::make_shared(server)); + add_request(std::make_shared(server)); + + // Server requests + add_request(std::make_shared(server)); + add_request(std::make_shared(server)); + add_request(std::make_shared(server)); + add_request(std::make_shared(server)); + add_request(std::make_shared(server)); +} + + +std::shared_ptr ControlRequestFactory::getRequest(const std::string& method) const +{ + auto iter = request_map_.find(method); + if (iter != request_map_.end()) + return iter->second; + return nullptr; + // if (method == "Client.GetStatus") + // return nullptr; + // else if (method == "Client.SetVolume") + // return nullptr; + // else if (method == "Client.SetLatency") + // return nullptr; + // else if (method == "Client.SetName") + // return nullptr; + // else if (method == "Group.GetStatus") + // return nullptr; + // else if (method == "Group.SetName") + // return nullptr; + // else if (method == "Group.SetMute") + // return nullptr; + // else if (method == "Group.SetStream") + // return nullptr; + // else if (method == "Group.SetClients") + // return nullptr; + // else if (method == "Server.GetRPCVersion") + // return std::make_unique(server); + // else if (method == "Server.GetStatus") + // return std::make_unique(server); + // else if (method == "Server.DeleteClient") + // return std::make_unique(server); + // else if (method == "Server.Authenticate") + // return nullptr; + // else if (method == "Server.GetToken") + // return nullptr; + // else if (method == "Stream.AddStream") + // return nullptr; + // else if (method == "Stream.RemoveStream") + // return nullptr; + // else + // return nullptr; +} + + +///////////////////////////////////////// Client requests ///////////////////////////////////////// + + +ClientRequest::ClientRequest(const Server& server, const std::string& method, const std::string& ressource) : Request(server, method, ressource) +{ +} + +ClientInfoPtr ClientRequest::getClient(const jsonrpcpp::request_ptr& request) +{ + if (!request->params().has("id")) + throw jsonrpcpp::InvalidParamsException("Parameter 'id' is missing", request->id()); + + ClientInfoPtr clientInfo = Config::instance().getClientInfo(request->params().get("id")); + if (clientInfo == nullptr) + throw jsonrpcpp::InternalErrorException("Client not found", request->id()); + return clientInfo; +} + + +void ClientRequest::updateClient(const jsonrpcpp::request_ptr& request) +{ + ClientInfoPtr clientInfo = getClient(request); + session_ptr session = getStreamServer().getStreamSession(clientInfo->id); + if (session != nullptr) + { + auto serverSettings = std::make_shared(); + serverSettings->setBufferMs(getSettings().stream.bufferMs); + serverSettings->setVolume(clientInfo->config.volume.percent); + GroupPtr group = Config::instance().getGroupFromClient(clientInfo); + serverSettings->setMuted(clientInfo->config.volume.muted || group->muted); + serverSettings->setLatency(clientInfo->config.latency); + session->send(serverSettings); + } +} + + +ClientGetStatusRequest::ClientGetStatusRequest(const Server& server) : ClientRequest(server, "Client.GetStatus", "xxx") +{ +} + +void ClientGetStatusRequest::execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) +{ + // clang-format off + // Request: {"id":8,"jsonrpc":"2.0","method":"Client.GetStatus","params":{"id":"00:21:6a:7d:74:fc"}} + // Response: {"id":8,"jsonrpc":"2.0","result":{"client":{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":74}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488026416,"usec":135973},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}}} + // clang-format on + + std::ignore = authinfo; + + Json result; + result["client"] = getClient(request)->toJson(); + auto response = std::make_shared(*request, result); + on_response(std::move(response), nullptr); +} + + + +ClientSetVolumeRequest::ClientSetVolumeRequest(const Server& server) : ClientRequest(server, "Client.SetVolume", "xxx") +{ +} + +void ClientSetVolumeRequest::execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) +{ + // clang-format off + // Request: {"id":8,"jsonrpc":"2.0","method":"Client.SetVolume","params":{"id":"00:21:6a:7d:74:fc","volume":{"muted":false,"percent":74}}} + // Response: {"id":8,"jsonrpc":"2.0","result":{"volume":{"muted":false,"percent":74}}} + // Notification: {"jsonrpc":"2.0","method":"Client.OnVolumeChanged","params":{"id":"00:21:6a:7d:74:fc","volume":{"muted":false,"percent":74}}} + // clang-format on + + std::ignore = authinfo; + auto client_info = getClient(request); + client_info->config.volume.fromJson(request->params().get("volume")); + Json result; + result["volume"] = client_info->config.volume.toJson(); + + auto response = std::make_shared(*request, result); + auto notification = std::make_shared("Client.OnVolumeChanged", + jsonrpcpp::Parameter("id", client_info->id, "volume", client_info->config.volume.toJson())); + on_response(std::move(response), std::move(notification)); + updateClient(request); +} + + + +ClientSetLatencyRequest::ClientSetLatencyRequest(const Server& server) : ClientRequest(server, "Client.SetLatency", "xxx") +{ +} + +void ClientSetLatencyRequest::execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) +{ + // clang-format off + // Request: {"id":7,"jsonrpc":"2.0","method":"Client.SetLatency","params":{"id":"00:21:6a:7d:74:fc#2","latency":10}} + // Response: {"id":7,"jsonrpc":"2.0","result":{"latency":10}} + // Notification: {"jsonrpc":"2.0","method":"Client.OnLatencyChanged","params":{"id":"00:21:6a:7d:74:fc#2","latency":10}} + // clang-format on + + std::ignore = authinfo; + int latency = request->params().get("latency"); + if (latency < -10000) + latency = -10000; + else if (latency > getSettings().stream.bufferMs) + latency = getSettings().stream.bufferMs; + auto client_info = getClient(request); + client_info->config.latency = latency; //, -10000, settings_.stream.bufferMs); + Json result; + result["latency"] = client_info->config.latency; + + auto response = std::make_shared(*request, result); + auto notification = std::make_shared("Client.OnLatencyChanged", + jsonrpcpp::Parameter("id", client_info->id, "latency", client_info->config.latency)); + on_response(std::move(response), std::move(notification)); + updateClient(request); +} + + + +ClientSetNameRequest::ClientSetNameRequest(const Server& server) : ClientRequest(server, "Client.SetName", "xxx") +{ +} + +void ClientSetNameRequest::execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) +{ + // clang-format off + // Request: {"id":6,"jsonrpc":"2.0","method":"Client.SetName","params":{"id":"00:21:6a:7d:74:fc#2","name":"Laptop"}} + // Response: {"id":6,"jsonrpc":"2.0","result":{"name":"Laptop"}} + // Notification: {"jsonrpc":"2.0","method":"Client.OnNameChanged","params":{"id":"00:21:6a:7d:74:fc#2","name":"Laptop"}} + // clang-format on + + std::ignore = authinfo; + auto client_info = getClient(request); + client_info->config.name = request->params().get("name"); + Json result; + result["name"] = client_info->config.name; + + auto response = std::make_shared(*request, result); + auto notification = + std::make_shared("Client.OnNameChanged", jsonrpcpp::Parameter("id", client_info->id, "name", client_info->config.name)); + on_response(std::move(response), std::move(notification)); + updateClient(request); +} + + + +///////////////////////////////////////// Group requests ////////////////////////////////////////// + + + +GroupRequest::GroupRequest(const Server& server, const std::string& method, const std::string& ressource) : Request(server, method, ressource) +{ +} + +GroupPtr GroupRequest::getGroup(const jsonrpcpp::request_ptr& request) +{ + if (!request->params().has("id")) + throw jsonrpcpp::InvalidParamsException("Parameter 'id' is missing", request->id()); + + GroupPtr group = Config::instance().getGroup(request->params().get("id")); + if (group == nullptr) + throw jsonrpcpp::InternalErrorException("Group not found", request->id()); + return group; +} + + + +GroupGetStatusRequest::GroupGetStatusRequest(const Server& server) : GroupRequest(server, "Group.GetStatus", "xxx") +{ +} + +void GroupGetStatusRequest::execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) +{ + // clang-format off + // Request: {"id":5,"jsonrpc":"2.0","method":"Group.GetStatus","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1"}} + // Response: {"id":5,"jsonrpc":"2.0","result":{"group":{"clients":[{"config":{"instance":2,"latency":10,"name":"Laptop","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488026485,"usec":644997},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":74}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488026481,"usec":223747},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":true,"name":"","stream_id":"stream 1"}}} + // clang-format on + + std::ignore = authinfo; + Json result; + result["group"] = getGroup(request)->toJson(); + auto response = std::make_shared(*request, result); + on_response(std::move(response), nullptr); +} + + +GroupSetNameRequest::GroupSetNameRequest(const Server& server) : GroupRequest(server, "Group.SetName", "xxx") +{ +} + +void GroupSetNameRequest::execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) +{ + // clang-format off + // Request: {"id":6,"jsonrpc":"2.0","method":"Group.SetName","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","name":"Laptop"}} + // Response: {"id":6,"jsonrpc":"2.0","result":{"name":"MediaPlayer"}} + // Notification: {"jsonrpc":"2.0","method":"Group.OnNameChanged","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","MediaPlayer":"Laptop"}} + // clang-format on + + std::ignore = authinfo; + Json result; + auto group = getGroup(request); + group->name = request->params().get("name"); + result["name"] = group->name; + + auto response = std::make_shared(*request, result); + auto notification = std::make_shared("Group.OnNameChanged", jsonrpcpp::Parameter("id", group->id, "name", group->name)); + on_response(std::move(response), std::move(notification)); +} + + +GroupSetMuteRequest::GroupSetMuteRequest(const Server& server) : GroupRequest(server, "Group.SetMute", "xxx") +{ +} + +void GroupSetMuteRequest::execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) +{ + // clang-format off + // Request: {"id":5,"jsonrpc":"2.0","method":"Group.SetMute","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","mute":true}} + // Response: {"id":5,"jsonrpc":"2.0","result":{"mute":true}} + // Notification: {"jsonrpc":"2.0","method":"Group.OnMute","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","mute":true}} + // clang-format on + + std::ignore = authinfo; + bool muted = request->params().get("mute"); + auto group = getGroup(request); + group->muted = muted; + + // Update clients + for (const auto& client : group->clients) + { + session_ptr session = getStreamServer().getStreamSession(client->id); + if (session != nullptr) + { + auto serverSettings = std::make_shared(); + serverSettings->setBufferMs(getSettings().stream.bufferMs); + serverSettings->setVolume(client->config.volume.percent); + GroupPtr group = Config::instance().getGroupFromClient(client); + serverSettings->setMuted(client->config.volume.muted || group->muted); + serverSettings->setLatency(client->config.latency); + session->send(serverSettings); + } + } + + Json result; + result["mute"] = group->muted; + + auto response = std::make_shared(*request, result); + auto notification = std::make_shared("Group.OnMute", jsonrpcpp::Parameter("id", group->id, "mute", group->muted)); + on_response(std::move(response), std::move(notification)); +} + + +GroupSetStreamRequest::GroupSetStreamRequest(const Server& server) : GroupRequest(server, "Group.SetStream", "xxx") +{ +} + +void GroupSetStreamRequest::execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) +{ + // clang-format off + // Request: {"id":4,"jsonrpc":"2.0","method":"Group.SetStream","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","stream_id":"stream 1"}} + // Response: {"id":4,"jsonrpc":"2.0","result":{"stream_id":"stream 1"}} + // Notification: {"jsonrpc":"2.0","method":"Group.OnStreamChanged","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","stream_id":"stream 1"}} + // clang-format on + + std::ignore = authinfo; + auto streamId = request->params().get("stream_id"); + PcmStreamPtr stream = getStreamManager().getStream(streamId); + if (stream == nullptr) + throw jsonrpcpp::InternalErrorException("Stream not found", request->id()); + + auto group = getGroup(request); + group->streamId = streamId; + + // Update clients + for (const auto& client : group->clients) + { + session_ptr session = getStreamServer().getStreamSession(client->id); + if (session && (session->pcmStream() != stream)) + { + // session->send(stream->getMeta()); + session->send(stream->getHeader()); + session->setPcmStream(stream); + } + } + + // Notify others + Json result; + result["stream_id"] = group->streamId; + + auto response = std::make_shared(*request, result); + auto notification = std::make_shared("Group.OnStreamChanged", jsonrpcpp::Parameter("id", group->id, "stream_id", group->streamId)); + on_response(std::move(response), std::move(notification)); +} + + +GroupSetClientsRequest::GroupSetClientsRequest(const Server& server) : GroupRequest(server, "Group.SetClients", "xxx") +{ +} + +void GroupSetClientsRequest::execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) +{ + // clang-format off + // Request: {"id":3,"jsonrpc":"2.0","method":"Group.SetClients","params":{"clients":["00:21:6a:7d:74:fc#2","00:21:6a:7d:74:fc"],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1"}} + // Response: {"id":3,"jsonrpc":"2.0","result":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025901,"usec":864472},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":100}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025905,"usec":45238},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}} + // Notification: {"jsonrpc":"2.0","method":"Server.OnUpdate","params":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025901,"usec":864472},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":100}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025905,"usec":45238},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}} + // clang-format on + + std::ignore = authinfo; + std::vector clients = request->params().get("clients"); + + // Remove clients from group + auto group = getGroup(request); + for (auto iter = group->clients.begin(); iter != group->clients.end();) + { + auto client = *iter; + if (find(clients.begin(), clients.end(), client->id) != clients.end()) + { + ++iter; + continue; + } + iter = group->clients.erase(iter); + GroupPtr newGroup = Config::instance().addClientInfo(client); + newGroup->streamId = group->streamId; + } + + // Add clients to group + PcmStreamPtr stream = getStreamManager().getStream(group->streamId); + for (const auto& clientId : clients) + { + ClientInfoPtr client = Config::instance().getClientInfo(clientId); + if (!client) + continue; + GroupPtr oldGroup = Config::instance().getGroupFromClient(client); + if (oldGroup && (oldGroup->id == group->id)) + continue; + + if (oldGroup) + { + oldGroup->removeClient(client); + Config::instance().remove(oldGroup); + } + + group->addClient(client); + + // assign new stream + session_ptr session = getStreamServer().getStreamSession(client->id); + if (session && stream && (session->pcmStream() != stream)) + { + // session->send(stream->getMeta()); + session->send(stream->getHeader()); + session->setPcmStream(stream); + } + } + + if (group->empty()) + Config::instance().remove(group); + + json server = Config::instance().getServerStatus(getStreamManager().toJson()); + Json result; + result["server"] = server; + + auto response = std::make_shared(*request, result); + // Notify others: since at least two groups are affected, send a complete server update + auto notification = std::make_shared("Server.OnUpdate", jsonrpcpp::Parameter("server", server)); + on_response(std::move(response), std::move(notification)); +} + + + +///////////////////////////////////////// Stream requests ///////////////////////////////////////// + + + +StreamRequest::StreamRequest(const Server& server, const std::string& method, const std::string& ressource) : Request(server, method, ressource) +{ +} + +PcmStreamPtr StreamRequest::getStream(const StreamManager& stream_manager, const jsonrpcpp::request_ptr& request) +{ + PcmStreamPtr stream = stream_manager.getStream(getStreamId(request)); + if (stream == nullptr) + throw jsonrpcpp::InternalErrorException("Stream not found", request->id()); + return stream; +} + + +std::string StreamRequest::getStreamId(const jsonrpcpp::request_ptr& request) +{ + if (!request->params().has("id")) + throw jsonrpcpp::InvalidParamsException("Parameter 'id' is missing", request->id()); + + return request->params().get("id"); +} + + +StreamControlRequest::StreamControlRequest(const Server& server) : StreamRequest(server, "Stream.Control", "xxx") +{ +} + +void StreamControlRequest::execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) +{ + // clang-format off + // Request: {"id":4,"jsonrpc":"2.0","method":"Stream.Control","params":{"id":"Spotify", "command": "next", params: {}}} + // Response: {"id":4,"jsonrpc":"2.0","result":{"id":"Spotify"}} + // + // Request: {"id":4,"jsonrpc":"2.0","method":"Stream.Control","params":{"id":"Spotify", "command": "seek", "param": "60000"}} + // Response: {"id":4,"jsonrpc":"2.0","result":{"id":"Spotify"}} + // clang-format on + + std::ignore = authinfo; + LOG(INFO, LOG_TAG) << "Stream.Control id: " << request->params().get("id") << ", command: " << request->params().get("command") + << ", params: " << (request->params().has("params") ? request->params().get("params") : "") << "\n"; + + // Find stream + PcmStreamPtr stream = getStream(getStreamManager(), request); + + if (!request->params().has("command")) + throw jsonrpcpp::InvalidParamsException("Parameter 'commmand' is missing", request->id()); + + auto command = request->params().get("command"); + + auto handle_response = [request, on_response, command](const snapcast::ErrorCode& ec) + { + auto log_level = AixLog::Severity::debug; + if (ec) + log_level = AixLog::Severity::error; + LOG(log_level, LOG_TAG) << "Response to '" << command << "': " << ec << ", message: " << ec.detailed_message() << ", msg: " << ec.message() + << ", category: " << ec.category().name() << "\n"; + std::shared_ptr response; + if (ec) + response = std::make_shared(request->id(), jsonrpcpp::Error(ec.detailed_message(), ec.value())); + else + response = std::make_shared(request->id(), "ok"); + // LOG(DEBUG, LOG_TAG) << response->to_json().dump() << "\n"; + on_response(std::move(response), nullptr); + }; + + if (command == "setPosition") + { + if (!request->params().has("params") || !request->params().get("params").contains("position")) + throw jsonrpcpp::InvalidParamsException("setPosition requires parameter 'position'"); + auto seconds = request->params().get("params")["position"].get(); + stream->setPosition(std::chrono::milliseconds(static_cast(seconds * 1000)), + [handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); + } + else if (command == "seek") + { + if (!request->params().has("params") || !request->params().get("params").contains("offset")) + throw jsonrpcpp::InvalidParamsException("Seek requires parameter 'offset'"); + auto offset = request->params().get("params")["offset"].get(); + stream->seek(std::chrono::milliseconds(static_cast(offset * 1000)), [handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); + } + else if (command == "next") + { + stream->next([handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); + } + else if (command == "previous") + { + stream->previous([handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); + } + else if (command == "pause") + { + stream->pause([handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); + } + else if (command == "playPause") + { + stream->playPause([handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); + } + else if (command == "stop") + { + stream->stop([handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); + } + else if (command == "play") + { + stream->play([handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); + } + else + throw jsonrpcpp::InvalidParamsException("Command '" + command + "' not supported", request->id()); +} + + +StreamSetPropertyRequest::StreamSetPropertyRequest(const Server& server) : StreamRequest(server, "Stream.SetProperty", "xxx") +{ +} + +void StreamSetPropertyRequest::execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) +{ + LOG(INFO, LOG_TAG) << "Stream.SetProperty id: " << request->params().get("id") << ", property: " << request->params().get("property") + << ", value: " << request->params().get("value") << "\n"; + + std::ignore = authinfo; + // Find stream + std::string streamId = getStreamId(request); + PcmStreamPtr stream = getStream(getStreamManager(), request); + + if (!request->params().has("property")) + throw jsonrpcpp::InvalidParamsException("Parameter 'property' is missing", request->id()); + + if (!request->params().has("value")) + throw jsonrpcpp::InvalidParamsException("Parameter 'value' is missing", request->id()); + + auto name = request->params().get("property"); + auto value = request->params().get("value"); + LOG(INFO, LOG_TAG) << "Stream '" << streamId << "' set property: " << name << " = " << value << "\n"; + + auto handle_response = [request, on_response](const std::string& command, const snapcast::ErrorCode& ec) + { + LOG(ERROR, LOG_TAG) << "Result for '" << command << "': " << ec << ", message: " << ec.detailed_message() << ", msg: " << ec.message() + << ", category: " << ec.category().name() << "\n"; + std::shared_ptr response; + if (ec) + response = std::make_shared(request->id(), jsonrpcpp::Error(ec.detailed_message(), ec.value())); + else + response = std::make_shared(request->id(), "ok"); + on_response(response, nullptr); + }; + + if (name == "loopStatus") + { + auto val = value.get(); + LoopStatus loop_status = loop_status_from_string(val); + if (loop_status == LoopStatus::kUnknown) + throw jsonrpcpp::InvalidParamsException("Value for loopStatus must be one of 'none', 'track', 'playlist'", request->id()); + stream->setLoopStatus(loop_status, [handle_response, name](const snapcast::ErrorCode& ec) { handle_response(name, ec); }); + } + else if (name == "shuffle") + { + if (!value.is_boolean()) + throw jsonrpcpp::InvalidParamsException("Value for shuffle must be bool", request->id()); + stream->setShuffle(value.get(), [handle_response, name](const snapcast::ErrorCode& ec) { handle_response(name, ec); }); + } + else if (name == "volume") + { + if (!value.is_number_integer()) + throw jsonrpcpp::InvalidParamsException("Value for volume must be an int", request->id()); + stream->setVolume(value.get(), [handle_response, name](const snapcast::ErrorCode& ec) { handle_response(name, ec); }); + } + else if (name == "mute") + { + if (!value.is_boolean()) + throw jsonrpcpp::InvalidParamsException("Value for mute must be bool", request->id()); + stream->setMute(value.get(), [handle_response, name](const snapcast::ErrorCode& ec) { handle_response(name, ec); }); + } + else if (name == "rate") + { + if (!value.is_number_float()) + throw jsonrpcpp::InvalidParamsException("Value for rate must be float", request->id()); + stream->setRate(value.get(), [handle_response, name](const snapcast::ErrorCode& ec) { handle_response(name, ec); }); + } + else + throw jsonrpcpp::InvalidParamsException("Property '" + name + "' not supported", request->id()); +} + + +StreamAddRequest::StreamAddRequest(const Server& server) : StreamRequest(server, "Stream.AddStream", "xxx") +{ +} + +void StreamAddRequest::execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) +{ + // clang-format off + // Request: {"id":4,"jsonrpc":"2.0","method":"Stream.AddStream","params":{"streamUri":"uri"}} + // Response: {"id":4,"jsonrpc":"2.0","result":{"id":"Spotify"}} + // clang-format on + + std::ignore = authinfo; + LOG(INFO, LOG_TAG) << "Stream.AddStream(" << request->params().get("streamUri") << ")\n"; + + // Add stream + std::string streamUri = request->params().get("streamUri"); + PcmStreamPtr stream = getStreamManager().addStream(streamUri); + if (stream == nullptr) + throw jsonrpcpp::InternalErrorException("Stream not created", request->id()); + stream->start(); // We start the stream, otherwise it would be silent + + // Setup response + Json result; + result["id"] = stream->getId(); + + auto response = std::make_shared(*request, result); + on_response(std::move(response), nullptr); +} + + +StreamRemoveRequest::StreamRemoveRequest(const Server& server) : StreamRequest(server, "Stream.RemoveStream", "xxx") +{ +} + +void StreamRemoveRequest::execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) +{ + // clang-format off + // Request: {"id":4,"jsonrpc":"2.0","method":"Stream.RemoveStream","params":{"id":"Spotify"}} + // Response: {"id":4,"jsonrpc":"2.0","result":{"id":"Spotify"}} + // clang-format on + + std::ignore = authinfo; + LOG(INFO, LOG_TAG) << "Stream.RemoveStream(" << request->params().get("id") << ")\n"; + + // Find stream + std::string streamId = getStreamId(request); + getStreamManager().removeStream(streamId); + + // Setup response + Json result; + result["id"] = streamId; + auto response = std::make_shared(*request, result); + on_response(std::move(response), nullptr); +} + + + +///////////////////////////////////////// Server requests ///////////////////////////////////////// + + + +ServerGetRpcVersionRequest::ServerGetRpcVersionRequest(const Server& server) : Request(server, "Server.GetRPCVersion", "xxx") +{ +} + +void ServerGetRpcVersionRequest::execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) +{ + // clang-format off + // Request: {"id":8,"jsonrpc":"2.0","method":"Server.GetRPCVersion"} + // Response: {"id":8,"jsonrpc":"2.0","result":{"major":2,"minor":0,"patch":0}} + // clang-format on + + std::ignore = authinfo; + Json result; + // : backwards incompatible change + result["major"] = 23; + // : feature addition to the API + result["minor"] = 0; + // : bugfix release + result["patch"] = 0; + auto response = std::make_shared(*request, result); + on_response(std::move(response), nullptr); +} + + + +ServerGetStatusRequest::ServerGetStatusRequest(const Server& server) : Request(server, "Server.GetStatus", "xxx") +{ +} + +void ServerGetStatusRequest::execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) +{ + // clang-format off + // Request: {"id":1,"jsonrpc":"2.0","method":"Server.GetStatus"} + // Response: {"id":1,"jsonrpc":"2.0","result":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025696,"usec":578142},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":81}},"connected":true,"host":{"arch":"x86_64","ip":"192.168.0.54","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025696,"usec":611255},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}} + // clang-format on + + std::ignore = authinfo; + Json result; + result["server"] = Config::instance().getServerStatus(getStreamManager().toJson()); + auto response = std::make_shared(*request, result); + on_response(std::move(response), nullptr); +} + + + +ServerDeleteClientRequest::ServerDeleteClientRequest(const Server& server) : Request(server, "Server.DeleteClient", "xxx") +{ +} + +void ServerDeleteClientRequest::execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) +{ + // clang-format off + // Request: {"id":2,"jsonrpc":"2.0","method":"Server.DeleteClient","params":{"id":"00:21:6a:7d:74:fc"}} + // Response: {"id":2,"jsonrpc":"2.0","result":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025751,"usec":654777},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}} + // Notification: {"jsonrpc":"2.0","method":"Server.OnUpdate","params":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025751,"usec":654777},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}} + // clang-format on + + std::ignore = authinfo; + ClientInfoPtr clientInfo = Config::instance().getClientInfo(request->params().get("id")); + if (clientInfo == nullptr) + throw jsonrpcpp::InternalErrorException("Client not found", request->id()); + + Config::instance().remove(clientInfo); + + json server = Config::instance().getServerStatus(getStreamManager().toJson()); + Json result; + result["server"] = server; + + auto response = std::make_shared(*request, result); + auto notification = std::make_shared("Server.OnUpdate", jsonrpcpp::Parameter("server", server)); + on_response(std::move(response), std::move(notification)); +} + + +ServerAuthenticateRequest::ServerAuthenticateRequest(const Server& server) : Request(server, "Server.Authenticate", "xxx") +{ +} + +void ServerAuthenticateRequest::execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) +{ + // clang-format off + // Request: {"id":8,"jsonrpc":"2.0","method":"Server.Authenticate","params":{"scheme":"Basic","param":"YmFkYWl4OnNlY3JldA=="}} + // Response: {"id":8,"jsonrpc":"2.0","result":"ok"} + // Request: {"id":8,"jsonrpc":"2.0","method":"Server.Authenticate","params":{"scheme":"Bearer","param":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTg1NjQ1MTYsImlhdCI6MTcxODUyODUxNiwic3ViIjoiQmFkYWl4In0.gHrMVp7jTAg8aCSg3cttcfIxswqmOPuqVNOb5p79Cn0NmAqRmLXtDLX4QjOoOqqb66ezBBeikpNjPi_aO18YPoNmX9fPxSwcObTHBupnm5eugEpneMPDASFUSE2hg8rrD_OEoAVxx6hCLln7Z3ILyWDmR6jcmy7z0bp0BiAqOywUrFoVIsnlDZRs3wOaap5oS9J2oaA_gNi_7OuvAhrydn26LDhm0KiIqEcyIholkpRHrDYODkz98h2PkZdZ2U429tTvVhzDBJ1cBq2Zq3cvuMZT6qhwaUc8eYA8fUJ7g65iP4o2OZtUzlfEUqX1TKyuWuSK6CUlsZooNE-MSCT7_w"}} + // Response: {"id":8,"jsonrpc":"2.0","result":"ok"} + // clang-format on + + if (!request->params().has("scheme")) + throw jsonrpcpp::InvalidParamsException("Parameter 'scheme' is missing", request->id()); + if (!request->params().has("param")) + throw jsonrpcpp::InvalidParamsException("Parameter 'param' is missing", request->id()); + + auto scheme = request->params().get("scheme"); + auto param = request->params().get("param"); + LOG(INFO, LOG_TAG) << "Authorization scheme: " << scheme << ", param: " << param << "\n"; + auto ec = authinfo.authenticate(scheme, param); + + std::shared_ptr response; + if (ec) + response = std::make_shared(request->id(), jsonrpcpp::Error(ec.detailed_message(), ec.value())); + else + response = std::make_shared(request->id(), "ok"); + // LOG(DEBUG, LOG_TAG) << response->to_json().dump() << "\n"; + + on_response(std::move(response), nullptr); +} + + +ServerGetTokenRequest::ServerGetTokenRequest(const Server& server) : Request(server, "Server.GetToken", "xxx") +{ +} + +void ServerGetTokenRequest::execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) +{ + // clang-format off + // Request: {"id":8,"jsonrpc":"2.0","method":"Server.GetToken","params":{"username":"Badaix","password":"secret"}} + // Response: {"id":8,"jsonrpc":"2.0","result":{"token":""}} + // clang-format on + if (!request->params().has("username")) + throw jsonrpcpp::InvalidParamsException("Parameter 'username' is missing", request->id()); + if (!request->params().has("password")) + throw jsonrpcpp::InvalidParamsException("Parameter 'password' is missing", request->id()); + + auto username = request->params().get("username"); + auto password = request->params().get("password"); + LOG(INFO, LOG_TAG) << "GetToken username: " << username << ", password: " << password << "\n"; + auto token = authinfo.getToken(username, password); + + std::shared_ptr response; + if (token.hasError()) + { + response = std::make_shared(request->id(), jsonrpcpp::Error(token.getError().detailed_message(), token.getError().value())); + } + else + { + Json result; + result["token"] = token.takeValue(); + response = std::make_shared(*request, result); + } + // LOG(DEBUG, LOG_TAG) << response->to_json().dump() << "\n"; + + on_response(std::move(response), nullptr); +} diff --git a/server/control_requests.hpp b/server/control_requests.hpp new file mode 100644 index 00000000..570cd9ef --- /dev/null +++ b/server/control_requests.hpp @@ -0,0 +1,322 @@ +/*** + This file is part of snapcast + Copyright (C) 2014-2024 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 + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 this program. If not, see . +***/ + +#pragma once + + +// local headers +#include "authinfo.hpp" +#include "config.hpp" +#include "jsonrpcpp.hpp" +#include "stream_server.hpp" +#include "streamreader/stream_manager.hpp" + +// 3rd party headers + +// standard headers +#include +#include + +class Server; + +/// Base class of a Snapserver control request +class Request +{ +public: + // TODO: revise handler names + /// Response handler for json control requests, returning a @p response and/or a @p notification broadcast + using OnResponse = std::function; + + /// No default c'tor + Request() = delete; + + /// c'tor + explicit Request(const Server& server, const std::string& method, const std::string& ressource); + + /// d'tor + virtual ~Request() = default; + + /// Execute the Request + virtual void execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) = 0; + + /// @return true if the user has the permission for the request + bool hasPermission(const AuthInfo& authinfo) const; + + /// @return the name of the method + const std::string& method() const; + +protected: + /// @return the server's stream server + const StreamServer& getStreamServer() const; + /// @return the server's stream manager + const StreamManager& getStreamManager() const; + /// @return server settings + const ServerSettings& getSettings() const; + + +private: + /// the server + const Server& server_; + /// the command + std::string method_; + /// the ressource + std::string ressource_; +}; + + +/// Control request factory +class ControlRequestFactory +{ +public: + /// c'tor + explicit ControlRequestFactory(const Server& server); + /// @return Request instance to handle @p method + std::shared_ptr getRequest(const std::string& method) const; + +private: + /// storage for all available requests + std::map> request_map_; +}; + + + +/// Base for "Client." requests +class ClientRequest : public Request +{ +public: + /// c'tor + ClientRequest(const Server& server, const std::string& method, const std::string& ressource); + +protected: + /// update the client that is referenced in the @p request + void updateClient(const jsonrpcpp::request_ptr& request); + + /// @return the client referenced in the request + static ClientInfoPtr getClient(const jsonrpcpp::request_ptr& request); +}; + + +/// Base for "Client.GetStatus" requests +class ClientGetStatusRequest : public ClientRequest +{ +public: + /// c'tor + explicit ClientGetStatusRequest(const Server& server); + void execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) override; +}; + + +/// Base for "Client.SetVolume" requests +class ClientSetVolumeRequest : public ClientRequest +{ +public: + /// c'tor + explicit ClientSetVolumeRequest(const Server& server); + void execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) override; +}; + + +/// Base for "Client.SetLatency" requests +class ClientSetLatencyRequest : public ClientRequest +{ +public: + /// c'tor + explicit ClientSetLatencyRequest(const Server& server); + void execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) override; +}; + + +/// Base for "Client.SetName" requests +class ClientSetNameRequest : public ClientRequest +{ +public: + /// c'tor + explicit ClientSetNameRequest(const Server& server); + void execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) override; +}; + + + +/// Base for "Group." requests +class GroupRequest : public Request +{ +public: + /// c'tor + GroupRequest(const Server& server, const std::string& method, const std::string& ressource); + +protected: + /// @return the group referenced in the request + static GroupPtr getGroup(const jsonrpcpp::request_ptr& request); +}; + + +/// "Group.GetStatus" request +class GroupGetStatusRequest : public GroupRequest +{ +public: + /// c'tor + explicit GroupGetStatusRequest(const Server& server); + void execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) override; +}; + + +/// "Group.SetName" request +class GroupSetNameRequest : public GroupRequest +{ +public: + /// c'tor + explicit GroupSetNameRequest(const Server& server); + void execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) override; +}; + + +/// "Group.SetMute" request +class GroupSetMuteRequest : public GroupRequest +{ +public: + /// c'tor + explicit GroupSetMuteRequest(const Server& server); + void execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) override; +}; + + +/// "Group.SetStream" request +class GroupSetStreamRequest : public GroupRequest +{ +public: + /// c'tor + explicit GroupSetStreamRequest(const Server& server); + void execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) override; +}; + + +/// "Group.SetClients" request +class GroupSetClientsRequest : public GroupRequest +{ +public: + /// c'tor + explicit GroupSetClientsRequest(const Server& server); + void execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) override; +}; + + + +/// Base for "Stream." requests +class StreamRequest : public Request +{ +public: + /// c'tor + StreamRequest(const Server& server, const std::string& method, const std::string& ressource); + +protected: + /// @return the stream referenced in the request + static PcmStreamPtr getStream(const StreamManager& stream_manager, const jsonrpcpp::request_ptr& request); + /// @return the stream id referenced in the request + static std::string getStreamId(const jsonrpcpp::request_ptr& request); +}; + + +/// "Stream.Control" request +class StreamControlRequest : public StreamRequest +{ +public: + /// c'tor + explicit StreamControlRequest(const Server& server); + void execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) override; +}; + + +/// "Stream.SetProperty" request +class StreamSetPropertyRequest : public StreamRequest +{ +public: + /// c'tor + explicit StreamSetPropertyRequest(const Server& server); + void execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) override; +}; + + +/// "Stream.AddStream" request +class StreamAddRequest : public StreamRequest +{ +public: + /// c'tor + explicit StreamAddRequest(const Server& server); + void execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) override; +}; + + +/// "Stream.RemoveStream" request +class StreamRemoveRequest : public StreamRequest +{ +public: + /// c'tor + explicit StreamRemoveRequest(const Server& server); + void execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) override; +}; + + + +/// "Server.GetRPCVersion" request +class ServerGetRpcVersionRequest : public Request +{ +public: + /// c'tor + explicit ServerGetRpcVersionRequest(const Server& server); + void execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) override; +}; + + +/// "Server.GetStatus" request +class ServerGetStatusRequest : public Request +{ +public: + /// c'tor + explicit ServerGetStatusRequest(const Server& server); + void execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) override; +}; + + +/// "Server.DeleteClient" request +class ServerDeleteClientRequest : public Request +{ +public: + /// c'tor + explicit ServerDeleteClientRequest(const Server& server); + void execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) override; +}; + + +/// "Server.Authenticate" request +class ServerAuthenticateRequest : public Request +{ +public: + /// c'tor + explicit ServerAuthenticateRequest(const Server& server); + void execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) override; +}; + + +/// "Server.GetToken" request +class ServerGetTokenRequest : public Request +{ +public: + /// c'tor + explicit ServerGetTokenRequest(const Server& server); + void execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) override; +}; diff --git a/server/server.cpp b/server/server.cpp index dcbc54a0..d2b02cea 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -21,13 +21,11 @@ // local headers #include "common/aixlog.hpp" -#include "common/base64.h" #include "common/jwt.hpp" #include "common/message/client_info.hpp" #include "common/message/hello.hpp" #include "common/message/server_settings.hpp" #include "common/message/time.hpp" -#include "common/utils/string_utils.hpp" #include "config.hpp" #include "jsonrpcpp.hpp" @@ -36,6 +34,7 @@ // standard headers #include #include +#include using namespace std; @@ -45,15 +44,14 @@ using json = nlohmann::json; static constexpr auto LOG_TAG = "Server"; + + Server::Server(boost::asio::io_context& io_context, const ServerSettings& serverSettings) - : io_context_(io_context), config_timer_(io_context), settings_(serverSettings) + : io_context_(io_context), config_timer_(io_context), settings_(serverSettings), request_factory_(*this) { } -Server::~Server() = default; - - void Server::onNewSession(std::shared_ptr session) { LOG(DEBUG, LOG_TAG) << "onNewSession\n"; @@ -134,562 +132,34 @@ void Server::onDisconnect(StreamSession* streamSession) } -void Server::processRequest(const jsonrpcpp::request_ptr request, AuthInfo& authinfo, const OnResponse& on_response) const +void Server::processRequest(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const Request::OnResponse& on_response) const { - jsonrpcpp::entity_ptr response = nullptr; - jsonrpcpp::notification_ptr notification; - try + auto req = request_factory_.getRequest(request->method()); + if (req) { - // LOG(INFO, LOG_TAG) << "Server::processRequest method: " << request->method << ", " << "id: " << request->id() << "\n"; - Json result; - - if (request->method().find("Client.") == 0) + try { - ClientInfoPtr clientInfo = Config::instance().getClientInfo(request->params().get("id")); - if (clientInfo == nullptr) - throw jsonrpcpp::InternalErrorException("Client not found", request->id()); - - if (request->method() == "Client.GetStatus") - { - // clang-format off - // Request: {"id":8,"jsonrpc":"2.0","method":"Client.GetStatus","params":{"id":"00:21:6a:7d:74:fc"}} - // Response: {"id":8,"jsonrpc":"2.0","result":{"client":{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":74}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488026416,"usec":135973},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}}} - // clang-format on - result["client"] = clientInfo->toJson(); - } - else if (request->method() == "Client.SetVolume") - { - // clang-format off - // Request: {"id":8,"jsonrpc":"2.0","method":"Client.SetVolume","params":{"id":"00:21:6a:7d:74:fc","volume":{"muted":false,"percent":74}}} - // Response: {"id":8,"jsonrpc":"2.0","result":{"volume":{"muted":false,"percent":74}}} - // Notification: {"jsonrpc":"2.0","method":"Client.OnVolumeChanged","params":{"id":"00:21:6a:7d:74:fc","volume":{"muted":false,"percent":74}}} - // clang-format on - - clientInfo->config.volume.fromJson(request->params().get("volume")); - result["volume"] = clientInfo->config.volume.toJson(); - notification = std::make_shared( - "Client.OnVolumeChanged", jsonrpcpp::Parameter("id", clientInfo->id, "volume", clientInfo->config.volume.toJson())); - } - else if (request->method() == "Client.SetLatency") - { - // clang-format off - // Request: {"id":7,"jsonrpc":"2.0","method":"Client.SetLatency","params":{"id":"00:21:6a:7d:74:fc#2","latency":10}} - // Response: {"id":7,"jsonrpc":"2.0","result":{"latency":10}} - // Notification: {"jsonrpc":"2.0","method":"Client.OnLatencyChanged","params":{"id":"00:21:6a:7d:74:fc#2","latency":10}} - // clang-format on - int latency = request->params().get("latency"); - if (latency < -10000) - latency = -10000; - else if (latency > settings_.stream.bufferMs) - latency = settings_.stream.bufferMs; - clientInfo->config.latency = latency; //, -10000, settings_.stream.bufferMs); - result["latency"] = clientInfo->config.latency; - notification = std::make_shared("Client.OnLatencyChanged", - jsonrpcpp::Parameter("id", clientInfo->id, "latency", clientInfo->config.latency)); - } - else if (request->method() == "Client.SetName") - { - // clang-format off - // Request: {"id":6,"jsonrpc":"2.0","method":"Client.SetName","params":{"id":"00:21:6a:7d:74:fc#2","name":"Laptop"}} - // Response: {"id":6,"jsonrpc":"2.0","result":{"name":"Laptop"}} - // Notification: {"jsonrpc":"2.0","method":"Client.OnNameChanged","params":{"id":"00:21:6a:7d:74:fc#2","name":"Laptop"}} - // clang-format on - clientInfo->config.name = request->params().get("name"); - result["name"] = clientInfo->config.name; - notification = std::make_shared("Client.OnNameChanged", - jsonrpcpp::Parameter("id", clientInfo->id, "name", clientInfo->config.name)); - } - else - throw jsonrpcpp::MethodNotFoundException(request->id()); - - - if (request->method().find("Client.Set") == 0) - { - /// Update client - session_ptr session = streamServer_->getStreamSession(clientInfo->id); - if (session != nullptr) - { - auto serverSettings = make_shared(); - serverSettings->setBufferMs(settings_.stream.bufferMs); - serverSettings->setVolume(clientInfo->config.volume.percent); - GroupPtr group = Config::instance().getGroupFromClient(clientInfo); - serverSettings->setMuted(clientInfo->config.volume.muted || group->muted); - serverSettings->setLatency(clientInfo->config.latency); - session->send(serverSettings); - } - } + req->execute(request, authinfo, on_response); } - else if (request->method().find("Group.") == 0) + catch (const jsonrpcpp::RequestException& e) { - GroupPtr group = Config::instance().getGroup(request->params().get("id")); - if (group == nullptr) - throw jsonrpcpp::InternalErrorException("Group not found", request->id()); - - if (request->method() == "Group.GetStatus") - { - // clang-format off - // Request: {"id":5,"jsonrpc":"2.0","method":"Group.GetStatus","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1"}} - // Response: {"id":5,"jsonrpc":"2.0","result":{"group":{"clients":[{"config":{"instance":2,"latency":10,"name":"Laptop","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488026485,"usec":644997},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":74}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488026481,"usec":223747},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":true,"name":"","stream_id":"stream 1"}}} - // clang-format on - result["group"] = group->toJson(); - } - else if (request->method() == "Group.SetName") - { - // clang-format off - // Request: {"id":6,"jsonrpc":"2.0","method":"Group.SetName","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","name":"Laptop"}} - // Response: {"id":6,"jsonrpc":"2.0","result":{"name":"MediaPlayer"}} - // Notification: {"jsonrpc":"2.0","method":"Group.OnNameChanged","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","MediaPlayer":"Laptop"}} - // clang-format on - group->name = request->params().get("name"); - result["name"] = group->name; - notification = std::make_shared("Group.OnNameChanged", jsonrpcpp::Parameter("id", group->id, "name", group->name)); - } - else if (request->method() == "Group.SetMute") - { - // clang-format off - // Request: {"id":5,"jsonrpc":"2.0","method":"Group.SetMute","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","mute":true}} - // Response: {"id":5,"jsonrpc":"2.0","result":{"mute":true}} - // Notification: {"jsonrpc":"2.0","method":"Group.OnMute","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","mute":true}} - // clang-format on - bool muted = request->params().get("mute"); - group->muted = muted; - - /// Update clients - for (const auto& client : group->clients) - { - session_ptr session = streamServer_->getStreamSession(client->id); - if (session != nullptr) - { - auto serverSettings = make_shared(); - serverSettings->setBufferMs(settings_.stream.bufferMs); - serverSettings->setVolume(client->config.volume.percent); - GroupPtr group = Config::instance().getGroupFromClient(client); - serverSettings->setMuted(client->config.volume.muted || group->muted); - serverSettings->setLatency(client->config.latency); - session->send(serverSettings); - } - } - - result["mute"] = group->muted; - notification = std::make_shared("Group.OnMute", jsonrpcpp::Parameter("id", group->id, "mute", group->muted)); - } - else if (request->method() == "Group.SetStream") - { - // clang-format off - // Request: {"id":4,"jsonrpc":"2.0","method":"Group.SetStream","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","stream_id":"stream 1"}} - // Response: {"id":4,"jsonrpc":"2.0","result":{"stream_id":"stream 1"}} - // Notification: {"jsonrpc":"2.0","method":"Group.OnStreamChanged","params":{"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","stream_id":"stream 1"}} - // clang-format on - string streamId = request->params().get("stream_id"); - PcmStreamPtr stream = streamManager_->getStream(streamId); - if (stream == nullptr) - throw jsonrpcpp::InternalErrorException("Stream not found", request->id()); - - group->streamId = streamId; - - // Update clients - for (const auto& client : group->clients) - { - session_ptr session = streamServer_->getStreamSession(client->id); - if (session && (session->pcmStream() != stream)) - { - // session->send(stream->getMeta()); - session->send(stream->getHeader()); - session->setPcmStream(stream); - } - } - - // Notify others - result["stream_id"] = group->streamId; - notification = - std::make_shared("Group.OnStreamChanged", jsonrpcpp::Parameter("id", group->id, "stream_id", group->streamId)); - } - else if (request->method() == "Group.SetClients") - { - // clang-format off - // Request: {"id":3,"jsonrpc":"2.0","method":"Group.SetClients","params":{"clients":["00:21:6a:7d:74:fc#2","00:21:6a:7d:74:fc"],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1"}} - // Response: {"id":3,"jsonrpc":"2.0","result":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025901,"usec":864472},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":100}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025905,"usec":45238},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}} - // Notification: {"jsonrpc":"2.0","method":"Server.OnUpdate","params":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025901,"usec":864472},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":100}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025905,"usec":45238},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}} - // clang-format on - vector clients = request->params().get("clients"); - // Remove clients from group - for (auto iter = group->clients.begin(); iter != group->clients.end();) - { - auto client = *iter; - if (find(clients.begin(), clients.end(), client->id) != clients.end()) - { - ++iter; - continue; - } - iter = group->clients.erase(iter); - GroupPtr newGroup = Config::instance().addClientInfo(client); - newGroup->streamId = group->streamId; - } - - // Add clients to group - PcmStreamPtr stream = streamManager_->getStream(group->streamId); - for (const auto& clientId : clients) - { - ClientInfoPtr client = Config::instance().getClientInfo(clientId); - if (!client) - continue; - GroupPtr oldGroup = Config::instance().getGroupFromClient(client); - if (oldGroup && (oldGroup->id == group->id)) - continue; - - if (oldGroup) - { - oldGroup->removeClient(client); - Config::instance().remove(oldGroup); - } - - group->addClient(client); - - // assign new stream - session_ptr session = streamServer_->getStreamSession(client->id); - if (session && stream && (session->pcmStream() != stream)) - { - // session->send(stream->getMeta()); - session->send(stream->getHeader()); - session->setPcmStream(stream); - } - } - - if (group->empty()) - Config::instance().remove(group); - - json server = Config::instance().getServerStatus(streamManager_->toJson()); - result["server"] = server; - - // Notify others: since at least two groups are affected, send a complete server update - notification = std::make_shared("Server.OnUpdate", jsonrpcpp::Parameter("server", server)); - } - else - throw jsonrpcpp::MethodNotFoundException(request->id()); + LOG(ERROR, LOG_TAG) << "Server::onMessageReceived JsonRequestException: " << e.to_json().dump() << ", message: " << request->to_json().dump() + << "\n"; + auto response = std::make_shared(e); + on_response(std::move(response), nullptr); } - else if (request->method().find("Server.") == 0) + catch (const exception& e) { - if (request->method().find("Server.GetRPCVersion") == 0) - { - // Request: {"id":8,"jsonrpc":"2.0","method":"Server.GetRPCVersion"} - // Response: {"id":8,"jsonrpc":"2.0","result":{"major":2,"minor":0,"patch":0}} - // : backwards incompatible change - result["major"] = 2; - // : feature addition to the API - result["minor"] = 0; - // : bugfix release - result["patch"] = 0; - } - else if (request->method() == "Server.GetStatus") - { - // clang-format off - // Request: {"id":1,"jsonrpc":"2.0","method":"Server.GetStatus"} - // Response: {"id":1,"jsonrpc":"2.0","result":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025696,"usec":578142},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}},{"config":{"instance":1,"latency":0,"name":"","volume":{"muted":false,"percent":81}},"connected":true,"host":{"arch":"x86_64","ip":"192.168.0.54","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc","lastSeen":{"sec":1488025696,"usec":611255},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}} - // clang-format on - result["server"] = Config::instance().getServerStatus(streamManager_->toJson()); - } - else if (request->method() == "Server.DeleteClient") - { - // clang-format off - // Request: {"id":2,"jsonrpc":"2.0","method":"Server.DeleteClient","params":{"id":"00:21:6a:7d:74:fc"}} - // Response: {"id":2,"jsonrpc":"2.0","result":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025751,"usec":654777},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}} - // Notification: {"jsonrpc":"2.0","method":"Server.OnUpdate","params":{"server":{"groups":[{"clients":[{"config":{"instance":2,"latency":6,"name":"123 456","volume":{"muted":false,"percent":48}},"connected":true,"host":{"arch":"x86_64","ip":"127.0.0.1","mac":"00:21:6a:7d:74:fc","name":"T400","os":"Linux Mint 17.3 Rosa"},"id":"00:21:6a:7d:74:fc#2","lastSeen":{"sec":1488025751,"usec":654777},"snapclient":{"name":"Snapclient","protocolVersion":2,"version":"0.10.0"}}],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1","muted":false,"name":"","stream_id":"stream 2"}],"server":{"host":{"arch":"x86_64","ip":"","mac":"","name":"T400","os":"Linux Mint 17.3 Rosa"},"snapserver":{"controlProtocolVersion":1,"name":"Snapserver","protocolVersion":1,"version":"0.10.0"}},"streams":[{"id":"stream 1","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 1","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 1","scheme":"pipe"}},{"id":"stream 2","status":"idle","uri":{"fragment":"","host":"","path":"/tmp/snapfifo","query":{"chunk_ms":"20","codec":"flac","name":"stream 2","sampleformat":"48000:16:2"},"raw":"pipe:///tmp/snapfifo?name=stream 2","scheme":"pipe"}}]}}} - // clang-format on - ClientInfoPtr clientInfo = Config::instance().getClientInfo(request->params().get("id")); - if (clientInfo == nullptr) - throw jsonrpcpp::InternalErrorException("Client not found", request->id()); - - Config::instance().remove(clientInfo); - - json server = Config::instance().getServerStatus(streamManager_->toJson()); - result["server"] = server; - - /// Notify others - notification = std::make_shared("Server.OnUpdate", jsonrpcpp::Parameter("server", server)); - } - else if (request->method() == "Server.Authenticate") - { - // clang-format off - // Request: {"id":8,"jsonrpc":"2.0","method":"Server.Authenticate","params":{"scheme":"Basic","param":"YmFkYWl4OnNlY3JldA=="}} - // Response: {"id":8,"jsonrpc":"2.0","result":"ok"} - // Request: {"id":8,"jsonrpc":"2.0","method":"Server.Authenticate","params":{"scheme":"Bearer","param":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTg1NjQ1MTYsImlhdCI6MTcxODUyODUxNiwic3ViIjoiQmFkYWl4In0.gHrMVp7jTAg8aCSg3cttcfIxswqmOPuqVNOb5p79Cn0NmAqRmLXtDLX4QjOoOqqb66ezBBeikpNjPi_aO18YPoNmX9fPxSwcObTHBupnm5eugEpneMPDASFUSE2hg8rrD_OEoAVxx6hCLln7Z3ILyWDmR6jcmy7z0bp0BiAqOywUrFoVIsnlDZRs3wOaap5oS9J2oaA_gNi_7OuvAhrydn26LDhm0KiIqEcyIholkpRHrDYODkz98h2PkZdZ2U429tTvVhzDBJ1cBq2Zq3cvuMZT6qhwaUc8eYA8fUJ7g65iP4o2OZtUzlfEUqX1TKyuWuSK6CUlsZooNE-MSCT7_w"}} - // Response: {"id":8,"jsonrpc":"2.0","result":"ok"} - // clang-format on - if (!request->params().has("scheme")) - throw jsonrpcpp::InvalidParamsException("Parameter 'scheme' is missing", request->id()); - if (!request->params().has("param")) - throw jsonrpcpp::InvalidParamsException("Parameter 'param' is missing", request->id()); - - auto scheme = request->params().get("scheme"); - auto param = request->params().get("param"); - LOG(INFO, LOG_TAG) << "Authorization scheme: " << scheme << ", param: " << param << "\n"; - auto ec = authinfo.authenticate(scheme, param); - - if (ec) - response = make_shared(request->id(), jsonrpcpp::Error(ec.detailed_message(), ec.value())); - else - response = make_shared(request->id(), "ok"); - // LOG(DEBUG, LOG_TAG) << response->to_json().dump() << "\n"; - } - else if (request->method() == "Server.GetToken") - { - // clang-format off - // Request: {"id":8,"jsonrpc":"2.0","method":"Server.GetToken","params":{"username":"Badaix","password":"secret"}} - // Response: {"id":8,"jsonrpc":"2.0","result":{"token":""}} - // clang-format on - if (!request->params().has("username")) - throw jsonrpcpp::InvalidParamsException("Parameter 'username' is missing", request->id()); - if (!request->params().has("password")) - throw jsonrpcpp::InvalidParamsException("Parameter 'password' is missing", request->id()); - - auto username = request->params().get("username"); - auto password = request->params().get("password"); - LOG(INFO, LOG_TAG) << "GetToken username: " << username << ", password: " << password << "\n"; - auto token = authinfo.getToken(username, password); - - if (token.hasError()) - response = make_shared(request->id(), jsonrpcpp::Error(token.getError().detailed_message(), token.getError().value())); - else - result["token"] = token.takeValue(); - // LOG(DEBUG, LOG_TAG) << response->to_json().dump() << "\n"; - } - else - throw jsonrpcpp::MethodNotFoundException(request->id()); + LOG(ERROR, LOG_TAG) << "Server::onMessageReceived exception: " << e.what() << ", message: " << request->to_json().dump() << "\n"; + auto response = std::make_shared(e.what(), request->id()); + on_response(std::move(response), nullptr); } - else if (request->method().find("Stream.") == 0) - { - // if (request->method().find("Stream.SetMeta") == 0) - // { - // clang-format off - // Request: {"id":4,"jsonrpc":"2.0","method":"Stream.SetMeta","params":{"id":"Spotify", "metadata": {"album": "some album", "artist": "some artist", "track": "some track"...}}} - // Response: {"id":4,"jsonrpc":"2.0","result":{"id":"Spotify"}} - // clang-format on - - // LOG(INFO, LOG_TAG) << "Stream.SetMeta id: " << request->params().get("id") << ", meta: " << request->params().get("metadata") << - // "\n"; - - // // Find stream - // string streamId = request->params().get("id"); - // PcmStreamPtr stream = streamManager_->getStream(streamId); - // if (stream == nullptr) - // throw jsonrpcpp::InternalErrorException("Stream not found", request->id()); - - // // Set metadata from request - // stream->setMetadata(request->params().get("metadata")); - - // // Setup response - // result["id"] = streamId; - // } - if (request->method().find("Stream.Control") == 0) - { - // clang-format off - // Request: {"id":4,"jsonrpc":"2.0","method":"Stream.Control","params":{"id":"Spotify", "command": "next", params: {}}} - // Response: {"id":4,"jsonrpc":"2.0","result":{"id":"Spotify"}} - // - // Request: {"id":4,"jsonrpc":"2.0","method":"Stream.Control","params":{"id":"Spotify", "command": "seek", "param": "60000"}} - // Response: {"id":4,"jsonrpc":"2.0","result":{"id":"Spotify"}} - // clang-format on - - LOG(INFO, LOG_TAG) << "Stream.Control id: " << request->params().get("id") << ", command: " << request->params().get("command") - << ", params: " << (request->params().has("params") ? request->params().get("params") : "") << "\n"; - - // Find stream - auto streamId = request->params().get("id"); - PcmStreamPtr stream = streamManager_->getStream(streamId); - if (stream == nullptr) - throw jsonrpcpp::InternalErrorException("Stream not found", request->id()); - - if (!request->params().has("command")) - throw jsonrpcpp::InvalidParamsException("Parameter 'commmand' is missing", request->id()); - - auto command = request->params().get("command"); - - auto handle_response = [request, on_response, command](const snapcast::ErrorCode& ec) - { - auto log_level = AixLog::Severity::debug; - if (ec) - log_level = AixLog::Severity::error; - LOG(log_level, LOG_TAG) << "Response to '" << command << "': " << ec << ", message: " << ec.detailed_message() << ", msg: " << ec.message() - << ", category: " << ec.category().name() << "\n"; - std::shared_ptr response; - if (ec) - response = make_shared(request->id(), jsonrpcpp::Error(ec.detailed_message(), ec.value())); - else - response = make_shared(request->id(), "ok"); - // LOG(DEBUG, LOG_TAG) << response->to_json().dump() << "\n"; - on_response(response, nullptr); - }; - - if (command == "setPosition") - { - if (!request->params().has("params") || !request->params().get("params").contains("position")) - throw jsonrpcpp::InvalidParamsException("setPosition requires parameter 'position'"); - auto seconds = request->params().get("params")["position"].get(); - stream->setPosition(std::chrono::milliseconds(static_cast(seconds * 1000)), - [handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); - } - else if (command == "seek") - { - if (!request->params().has("params") || !request->params().get("params").contains("offset")) - throw jsonrpcpp::InvalidParamsException("Seek requires parameter 'offset'"); - auto offset = request->params().get("params")["offset"].get(); - stream->seek(std::chrono::milliseconds(static_cast(offset * 1000)), - [handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); - } - else if (command == "next") - { - stream->next([handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); - } - else if (command == "previous") - { - stream->previous([handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); - } - else if (command == "pause") - { - stream->pause([handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); - } - else if (command == "playPause") - { - stream->playPause([handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); - } - else if (command == "stop") - { - stream->stop([handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); - } - else if (command == "play") - { - stream->play([handle_response](const snapcast::ErrorCode& ec) { handle_response(ec); }); - } - else - throw jsonrpcpp::InvalidParamsException("Command '" + command + "' not supported", request->id()); - - return; - } - else if (request->method().find("Stream.SetProperty") == 0) - { - LOG(INFO, LOG_TAG) << "Stream.SetProperty id: " << request->params().get("id") - << ", property: " << request->params().get("property") << ", value: " << request->params().get("value") << "\n"; - - // Find stream - string streamId = request->params().get("id"); - PcmStreamPtr stream = streamManager_->getStream(streamId); - if (stream == nullptr) - throw jsonrpcpp::InternalErrorException("Stream not found", request->id()); - - if (!request->params().has("property")) - throw jsonrpcpp::InvalidParamsException("Parameter 'property' is missing", request->id()); - - if (!request->params().has("value")) - throw jsonrpcpp::InvalidParamsException("Parameter 'value' is missing", request->id()); - - auto name = request->params().get("property"); - auto value = request->params().get("value"); - LOG(INFO, LOG_TAG) << "Stream '" << streamId << "' set property: " << name << " = " << value << "\n"; - - auto handle_response = [request, on_response](const std::string& command, const snapcast::ErrorCode& ec) - { - LOG(ERROR, LOG_TAG) << "Result for '" << command << "': " << ec << ", message: " << ec.detailed_message() << ", msg: " << ec.message() - << ", category: " << ec.category().name() << "\n"; - std::shared_ptr response; - if (ec) - response = make_shared(request->id(), jsonrpcpp::Error(ec.detailed_message(), ec.value())); - else - response = make_shared(request->id(), "ok"); - on_response(response, nullptr); - }; - - if (name == "loopStatus") - { - auto val = value.get(); - LoopStatus loop_status = loop_status_from_string(val); - if (loop_status == LoopStatus::kUnknown) - throw jsonrpcpp::InvalidParamsException("Value for loopStatus must be one of 'none', 'track', 'playlist'", request->id()); - stream->setLoopStatus(loop_status, [handle_response, name](const snapcast::ErrorCode& ec) { handle_response(name, ec); }); - } - else if (name == "shuffle") - { - if (!value.is_boolean()) - throw jsonrpcpp::InvalidParamsException("Value for shuffle must be bool", request->id()); - stream->setShuffle(value.get(), [handle_response, name](const snapcast::ErrorCode& ec) { handle_response(name, ec); }); - } - else if (name == "volume") - { - if (!value.is_number_integer()) - throw jsonrpcpp::InvalidParamsException("Value for volume must be an int", request->id()); - stream->setVolume(value.get(), [handle_response, name](const snapcast::ErrorCode& ec) { handle_response(name, ec); }); - } - else if (name == "mute") - { - if (!value.is_boolean()) - throw jsonrpcpp::InvalidParamsException("Value for mute must be bool", request->id()); - stream->setMute(value.get(), [handle_response, name](const snapcast::ErrorCode& ec) { handle_response(name, ec); }); - } - else if (name == "rate") - { - if (!value.is_number_float()) - throw jsonrpcpp::InvalidParamsException("Value for rate must be float", request->id()); - stream->setRate(value.get(), [handle_response, name](const snapcast::ErrorCode& ec) { handle_response(name, ec); }); - } - else - throw jsonrpcpp::InvalidParamsException("Property '" + name + "' not supported", request->id()); - - return; - } - else if (request->method() == "Stream.AddStream") - { - // clang-format off - // Request: {"id":4,"jsonrpc":"2.0","method":"Stream.AddStream","params":{"streamUri":"uri"}} - // Response: {"id":4,"jsonrpc":"2.0","result":{"id":"Spotify"}} - // clang-format on - - LOG(INFO, LOG_TAG) << "Stream.AddStream(" << request->params().get("streamUri") << ")" - << "\n"; - - // Find stream - string streamUri = request->params().get("streamUri"); - PcmStreamPtr stream = streamManager_->addStream(streamUri); - if (stream == nullptr) - throw jsonrpcpp::InternalErrorException("Stream not created", request->id()); - stream->start(); // We start the stream, otherwise it would be silent - // Setup response - result["id"] = stream->getId(); - } - else if (request->method() == "Stream.RemoveStream") - { - // clang-format off - // Request: {"id":4,"jsonrpc":"2.0","method":"Stream.RemoveStream","params":{"id":"Spotify"}} - // Response: {"id":4,"jsonrpc":"2.0","result":{"id":"Spotify"}} - // clang-format on - - LOG(INFO, LOG_TAG) << "Stream.RemoveStream(" << request->params().get("id") << ")" - << "\n"; - - // Find stream - string streamId = request->params().get("id"); - streamManager_->removeStream(streamId); - // Setup response - result["id"] = streamId; - } - else - throw jsonrpcpp::MethodNotFoundException(request->id()); - } - else - throw jsonrpcpp::MethodNotFoundException(request->id()); - - if (!response) - response = std::make_shared(*request, result); } - catch (const jsonrpcpp::RequestException& e) + else { - LOG(ERROR, LOG_TAG) << "Server::onMessageReceived JsonRequestException: " << e.to_json().dump() << ", message: " << request->to_json().dump() << "\n"; - response = std::make_shared(e); + LOG(ERROR, LOG_TAG) << "Method not found: " << request->method() << "\n"; + throw jsonrpcpp::MethodNotFoundException(request->id()); } - catch (const exception& e) - { - LOG(ERROR, LOG_TAG) << "Server::onMessageReceived exception: " << e.what() << ", message: " << request->to_json().dump() << "\n"; - response = std::make_shared(e.what(), request->id()); - } - on_response(std::move(response), std::move(notification)); } diff --git a/server/server.hpp b/server/server.hpp index 9e23a417..767c1718 100644 --- a/server/server.hpp +++ b/server/server.hpp @@ -23,6 +23,7 @@ #include "authinfo.hpp" #include "common/message/message.hpp" #include "common/queue.hpp" +#include "control_requests.hpp" #include "control_server.hpp" #include "jsonrpcpp.hpp" #include "server_settings.hpp" @@ -30,6 +31,7 @@ #include "stream_session.hpp" #include "streamreader/stream_manager.hpp" + // 3rd party headers #include #include @@ -38,9 +40,6 @@ #include -using namespace streamreader; - -using boost::asio::ip::tcp; using acceptor_ptr = std::unique_ptr; using session_ptr = std::shared_ptr; @@ -51,14 +50,13 @@ using session_ptr = std::shared_ptr; */ class Server : public StreamMessageReceiver, public ControlMessageReceiver, public PcmStream::Listener { -public: - // TODO: revise handler names - /// Response handler for json control requests, returning a @p response and/or a @p notification broadcast - using OnResponse = std::function; + friend class Request; +public: /// c'tor Server(boost::asio::io_context& io_context, const ServerSettings& serverSettings); - virtual ~Server(); + /// d'tor + virtual ~Server() = default; /// Start the server (control server, stream server and stream manager) void start(); @@ -86,7 +84,7 @@ private: void onResync(const PcmStream* pcmStream, double ms) override; private: - void processRequest(const jsonrpcpp::request_ptr request, AuthInfo& authinfo, const OnResponse& on_response) const; + void processRequest(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const Request::OnResponse& on_response) const; /// Save the server state deferred to prevent blocking and lower disk io /// @param deferred the delay after the last call to saveConfig void saveConfig(const std::chrono::milliseconds& deferred = std::chrono::seconds(2)); @@ -99,4 +97,5 @@ private: std::unique_ptr controlServer_; std::unique_ptr streamServer_; std::unique_ptr streamManager_; + ControlRequestFactory request_factory_; };