From 257a10212780bfe12103275f46fcfddb65c2612a Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 31 Jan 2025 23:38:22 -0500 Subject: [PATCH] fix(api): return proper json objects (#3544) --- .github/workflows/CI.yml | 1 - cmake/compile_definitions/common.cmake | 1 + cmake/compile_definitions/windows.cmake | 1 - cmake/dependencies/common.cmake | 1 + cmake/dependencies/nlohmann_json.cmake | 18 + cmake/dependencies/windows.cmake | 3 - docs/api.md | 38 +- docs/app_examples.md | 9 +- docs/building.md | 1 - docs/configuration.md | 2 +- src/confighttp.cpp | 568 +++++++++--------- src/confighttp.h | 1 - src/nvhttp.cpp | 18 +- src/nvhttp.h | 8 +- src_assets/common/assets/web/apps.html | 4 +- src_assets/common/assets/web/config.html | 16 +- .../tabs/encoders/NvidiaNvencEncoder.vue | 14 +- .../assets/web/public/assets/locale/en.json | 4 +- src_assets/windows/assets/apps.json | 4 +- 19 files changed, 363 insertions(+), 349 deletions(-) create mode 100644 cmake/dependencies/nlohmann_json.cmake diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 0a583c061a5..945d2d5387d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -887,7 +887,6 @@ jobs: mingw-w64-ucrt-x86_64-graphviz mingw-w64-ucrt-x86_64-MinHook mingw-w64-ucrt-x86_64-miniupnpc - mingw-w64-ucrt-x86_64-nlohmann-json mingw-w64-ucrt-x86_64-nodejs mingw-w64-ucrt-x86_64-nsis mingw-w64-ucrt-x86_64-onevpl diff --git a/cmake/compile_definitions/common.cmake b/cmake/compile_definitions/common.cmake index 0c576426eb9..723a1a7a540 100644 --- a/cmake/compile_definitions/common.cmake +++ b/cmake/compile_definitions/common.cmake @@ -149,6 +149,7 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${CMAKE_THREAD_LIBS_INIT} enet libdisplaydevice::display_device + nlohmann_json::nlohmann_json opus ${FFMPEG_LIBRARIES} ${Boost_LIBRARIES} diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake index 3ee287e0162..b169dbb5f48 100644 --- a/cmake/compile_definitions/windows.cmake +++ b/cmake/compile_definitions/windows.cmake @@ -76,7 +76,6 @@ list(PREPEND PLATFORM_LIBRARIES libstdc++.a libwinpthread.a minhook::minhook - nlohmann_json::nlohmann_json ntdll setupapi shlwapi diff --git a/cmake/dependencies/common.cmake b/cmake/dependencies/common.cmake index 27da728b631..c92b4777b86 100644 --- a/cmake/dependencies/common.cmake +++ b/cmake/dependencies/common.cmake @@ -16,6 +16,7 @@ add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/Simple-Web-Server") add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/libdisplaydevice") # common dependencies +include("${CMAKE_MODULE_PATH}/dependencies/nlohmann_json.cmake") find_package(OpenSSL REQUIRED) find_package(PkgConfig REQUIRED) find_package(Threads REQUIRED) diff --git a/cmake/dependencies/nlohmann_json.cmake b/cmake/dependencies/nlohmann_json.cmake new file mode 100644 index 00000000000..c0029b42ecc --- /dev/null +++ b/cmake/dependencies/nlohmann_json.cmake @@ -0,0 +1,18 @@ +# +# Loads the nlohmann_json library giving the priority to the system package first, with a fallback to FetchContent. +# +include_guard(GLOBAL) + +find_package(nlohmann_json 3.11 QUIET GLOBAL) +if(NOT nlohmann_json_FOUND) + message(STATUS "nlohmann_json v3.11.x package not found in the system. Falling back to FetchContent.") + include(FetchContent) + + FetchContent_Declare( + json + URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz + URL_HASH MD5=c23a33f04786d85c29fda8d16b5f0efd + DOWNLOAD_EXTRACT_TIMESTAMP + ) + FetchContent_MakeAvailable(json) +endif() diff --git a/cmake/dependencies/windows.cmake b/cmake/dependencies/windows.cmake index 11a40ecf481..3faad7dfd41 100644 --- a/cmake/dependencies/windows.cmake +++ b/cmake/dependencies/windows.cmake @@ -1,8 +1,5 @@ # windows specific dependencies -# nlohmann_json -find_package(nlohmann_json CONFIG 3.11 REQUIRED) - # Make sure MinHook is installed find_library(MINHOOK_LIBRARY libMinHook.a REQUIRED) find_path(MINHOOK_INCLUDE_DIR MinHook.h PATH_SUFFIXES include REQUIRED) diff --git a/docs/api.md b/docs/api.md index e93f500c061..5313d5988f6 100644 --- a/docs/api.md +++ b/docs/api.md @@ -12,17 +12,23 @@ basic authentication with the admin username and password. ## GET /api/apps @copydoc confighttp::getApps() -## GET /api/logs -@copydoc confighttp::getLogs() - ## POST /api/apps @copydoc confighttp::saveApp() +## POST /api/apps/close +@copydoc confighttp::closeApp() + ## DELETE /api/apps/{index} @copydoc confighttp::deleteApp() -## POST /api/covers/upload -@copydoc confighttp::uploadCover() +## GET /api/clients/list +@copydoc confighttp::getClients() + +## POST /api/clients/unpair +@copydoc confighttp::unpair() + +## POST /api/clients/unpair-all +@copydoc confighttp::unpairAll() ## GET /api/config @copydoc confighttp::getConfig() @@ -33,11 +39,11 @@ basic authentication with the admin username and password. ## POST /api/config @copydoc confighttp::saveConfig() -## POST /api/restart -@copydoc confighttp::restart() +## POST /api/covers/upload +@copydoc confighttp::uploadCover() -## POST /api/reset-display-device-persistence -@copydoc confighttp::resetDisplayDevicePersistence() +## GET /api/logs +@copydoc confighttp::getLogs() ## POST /api/password @copydoc confighttp::savePassword() @@ -45,17 +51,11 @@ basic authentication with the admin username and password. ## POST /api/pin @copydoc confighttp::savePin() -## POST /api/clients/unpair-all -@copydoc confighttp::unpairAll() - -## POST /api/clients/unpair -@copydoc confighttp::unpair() - -## GET /api/clients/list -@copydoc confighttp::listClients() +## POST /api/reset-display-device-persistence +@copydoc confighttp::resetDisplayDevicePersistence() -## POST /api/apps/close -@copydoc confighttp::closeApp() +## POST /api/restart +@copydoc confighttp::restart()
diff --git a/docs/app_examples.md b/docs/app_examples.md index 0db2ad930de..0a814f75612 100644 --- a/docs/app_examples.md +++ b/docs/app_examples.md @@ -301,22 +301,19 @@ administrative privileges. Simply enable the elevated option in the WEB UI, or a This is an option for both prep-cmd and regular commands and will launch the process with the current user without a UAC prompt. -@note{It is important to write the values "true" and "false" as string values, not as the typical true/false -values in most JSON.} - **Example** ```json { "name": "Game With AntiCheat that Requires Admin", "output": "", "cmd": "ping 127.0.0.1", - "exclude-global-prep-cmd": "false", - "elevated": "true", + "exclude-global-prep-cmd": false, + "elevated": true, "prep-cmd": [ { "do": "powershell.exe -command \"Start-Streaming\"", "undo": "powershell.exe -command \"Stop-Streaming\"", - "elevated": "false" + "elevated": false } ], "image-path": "" diff --git a/docs/building.md b/docs/building.md index 3c27ac82baf..b7f5d402601 100644 --- a/docs/building.md +++ b/docs/building.md @@ -92,7 +92,6 @@ dependencies=( "mingw-w64-ucrt-x86_64-graphviz" # Optional, for docs "mingw-w64-ucrt-x86_64-MinHook" "mingw-w64-ucrt-x86_64-miniupnpc" - "mingw-w64-ucrt-x86_64-nlohmann-json" "mingw-w64-ucrt-x86_64-nodejs" "mingw-w64-ucrt-x86_64-nsis" "mingw-w64-ucrt-x86_64-onevpl" diff --git a/docs/configuration.md b/docs/configuration.md index fe8c22b93be..b0e384aabef 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -225,7 +225,7 @@ editing the `conf` file in a text editor. Use the examples as reference. Example @code{} - global_prep_cmd = [{"do":"nircmd.exe setdisplay 1280 720 32 144","undo":"nircmd.exe setdisplay 2560 1440 32 144"}] + global_prep_cmd = [{"do":"nircmd.exe setdisplay 1280 720 32 144","elevated":true,"undo":"nircmd.exe setdisplay 2560 1440 32 144"}] @endcode diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 5050a4ebb3b..4ba09b88a5a 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -8,16 +8,14 @@ // standard includes #include +#include #include // lib includes #include #include -#include #include -#include -#include -#include +#include #include #include @@ -34,7 +32,6 @@ #include "nvhttp.h" #include "platform/common.h" #include "process.h" -#include "rtsp.h" #include "utility.h" #include "uuid.h" #include "version.h" @@ -43,7 +40,6 @@ using namespace std::literals; namespace confighttp { namespace fs = std::filesystem; - namespace pt = boost::property_tree; using https_server_t = SimpleWeb::Server; @@ -82,10 +78,11 @@ namespace confighttp { * @param response The HTTP response object. * @param output_tree The JSON tree to send. */ - void send_response(resp_https_t response, const pt::ptree &output_tree) { - std::ostringstream data; - pt::write_json(data, output_tree); - response->write(data.str()); + void send_response(resp_https_t response, const nlohmann::json &output_tree) { + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Content-Type", "application/json"); + + response->write(output_tree.dump(), headers); } /** @@ -96,10 +93,20 @@ namespace confighttp { void send_unauthorized(resp_https_t response, req_https_t request) { auto address = net::addr_to_normalized_string(request->remote_endpoint().address()); BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv; + + constexpr SimpleWeb::StatusCode code = SimpleWeb::StatusCode::client_error_unauthorized; + + nlohmann::json tree; + tree["status_code"] = code; + tree["status"] = false; + tree["error"] = "Unauthorized"; + const SimpleWeb::CaseInsensitiveMultimap headers { + {"Content-Type", "application/json"}, {"WWW-Authenticate", R"(Basic realm="Sunshine Gamestream Host", charset="UTF-8")"} }; - response->write(SimpleWeb::StatusCode::client_error_unauthorized, headers); + + response->write(code, tree.dump(), headers); } /** @@ -176,17 +183,14 @@ namespace confighttp { void not_found(resp_https_t response, [[maybe_unused]] req_https_t request) { constexpr SimpleWeb::StatusCode code = SimpleWeb::StatusCode::client_error_not_found; - pt::ptree tree; - tree.put("status_code", static_cast(code)); - tree.put("error", "Not Found"); - - std::ostringstream data; - pt::write_json(data, tree); + nlohmann::json tree; + tree["status_code"] = code; + tree["error"] = "Not Found"; SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "application/json"); - response->write(code, data.str(), headers); + response->write(code, tree.dump(), headers); } /** @@ -198,18 +202,15 @@ namespace confighttp { void bad_request(resp_https_t response, [[maybe_unused]] req_https_t request, const std::string &error_message = "Bad Request") { constexpr SimpleWeb::StatusCode code = SimpleWeb::StatusCode::client_error_bad_request; - pt::ptree tree; - tree.put("status_code", static_cast(code)); - tree.put("status", false); - tree.put("error", error_message); - - std::ostringstream data; - pt::write_json(data, tree); + nlohmann::json tree; + tree["status_code"] = code; + tree["status"] = false; + tree["error"] = error_message; SimpleWeb::CaseInsensitiveMultimap headers; headers.emplace("Content-Type", "application/json"); - response->write(code, data.str(), headers); + response->write(code, tree.dump(), headers); } /** @@ -455,30 +456,50 @@ namespace confighttp { print_req(request); - std::string content = file_handler::read_file(config::stream.file_apps.c_str()); - SimpleWeb::CaseInsensitiveMultimap headers; - headers.emplace("Content-Type", "application/json"); - response->write(content, headers); - } + try { + std::string content = file_handler::read_file(config::stream.file_apps.c_str()); + nlohmann::json file_tree = nlohmann::json::parse(content); + + // Legacy versions of Sunshine used strings for boolean and integers, let's convert them + // List of keys to convert to boolean + std::vector boolean_keys = { + "exclude-global-prep-cmd", + "elevated", + "auto-detach", + "wait-all" + }; + + // List of keys to convert to integers + std::vector integer_keys = { + "exit-timeout" + }; + + // Walk fileTree and convert true/false strings to boolean or integer values + for (auto &app : file_tree["apps"]) { + for (const auto &key : boolean_keys) { + if (app.contains(key) && app[key].is_string()) { + app[key] = app[key] == "true"; + } + } + for (const auto &key : integer_keys) { + if (app.contains(key) && app[key].is_string()) { + app[key] = std::stoi(app[key].get()); + } + } + if (app.contains("prep-cmd")) { + for (auto &prep : app["prep-cmd"]) { + if (prep.contains("elevated") && prep["elevated"].is_string()) { + prep["elevated"] = prep["elevated"] == "true"; + } + } + } + } - /** - * @brief Get the logs from the log file. - * @param response The HTTP response object. - * @param request The HTTP request object. - * - * @api_examples{/api/logs| GET| null} - */ - void getLogs(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) { - return; + send_response(response, file_tree); + } catch (std::exception &e) { + BOOST_LOG(warning) << "GetApps: "sv << e.what(); + bad_request(response, request, e.what()); } - - print_req(request); - - std::string content = file_handler::read_file(config::sunshine.log_file.c_str()); - SimpleWeb::CaseInsensitiveMultimap headers; - headers.emplace("Content-Type", "text/plain"); - response->write(SimpleWeb::StatusCode::success_ok, content, headers); } /** @@ -522,74 +543,78 @@ namespace confighttp { std::stringstream ss; ss << request->content.rdbuf(); - - BOOST_LOG(info) << config::stream.file_apps; try { // TODO: Input Validation - pt::ptree fileTree; - pt::ptree inputTree; - pt::ptree outputTree; - pt::read_json(ss, inputTree); - pt::read_json(config::stream.file_apps, fileTree); - - if (inputTree.get_child("prep-cmd").empty()) { - inputTree.erase("prep-cmd"); + nlohmann::json output_tree; + nlohmann::json input_tree = nlohmann::json::parse(ss); + std::string file = file_handler::read_file(config::stream.file_apps.c_str()); + BOOST_LOG(info) << file; + nlohmann::json file_tree = nlohmann::json::parse(file); + + if (input_tree["prep-cmd"].empty()) { + input_tree.erase("prep-cmd"); } - if (inputTree.get_child("detached").empty()) { - inputTree.erase("detached"); + if (input_tree["detached"].empty()) { + input_tree.erase("detached"); } - auto &apps_node = fileTree.get_child("apps"s); - int index = inputTree.get("index"); + auto &apps_node = file_tree["apps"]; + int index = input_tree["index"].get(); // this will intentionally cause exception if the provided value is the wrong type - inputTree.erase("index"); + input_tree.erase("index"); if (index == -1) { - apps_node.push_back(std::make_pair("", inputTree)); + apps_node.push_back(input_tree); } else { - // Unfortunately Boost PT does not allow to directly edit the array, copy should do the trick - pt::ptree newApps; - int i = 0; - for (const auto &[k, v] : apps_node) { + nlohmann::json newApps = nlohmann::json::array(); + for (size_t i = 0; i < apps_node.size(); ++i) { if (i == index) { - newApps.push_back(std::make_pair("", inputTree)); + newApps.push_back(input_tree); } else { - newApps.push_back(std::make_pair("", v)); + newApps.push_back(apps_node[i]); } - i++; } - fileTree.erase("apps"); - fileTree.push_back(std::make_pair("apps", newApps)); + file_tree["apps"] = newApps; } // Sort the apps array by name - std::vector apps_vector; - for (const auto &[k, v] : fileTree.get_child("apps")) { - apps_vector.push_back(v); - } - std::ranges::sort(apps_vector, [](const pt::ptree &a, const pt::ptree &b) { - return a.get("name") < b.get("name"); + std::sort(apps_node.begin(), apps_node.end(), [](const nlohmann::json &a, const nlohmann::json &b) { + return a["name"].get() < b["name"].get(); }); - pt::ptree sorted_apps; - for (const auto &app : apps_vector) { - sorted_apps.push_back(std::make_pair("", app)); - } - fileTree.erase("apps"); - fileTree.add_child("apps", sorted_apps); - - pt::write_json(config::stream.file_apps, fileTree); + file_handler::write_file(config::stream.file_apps.c_str(), file_tree.dump(4)); proc::refresh(config::stream.file_apps); - outputTree.put("status", true); - send_response(response, outputTree); + output_tree["status"] = true; + send_response(response, output_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "SaveApp: "sv << e.what(); bad_request(response, request, e.what()); } } + /** + * @brief Close the currently running application. + * @param response The HTTP response object. + * @param request The HTTP request object. + * + * @api_examples{/api/apps/close| POST| null} + */ + void closeApp(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { + return; + } + + print_req(request); + + proc::proc.terminate(); + + nlohmann::json output_tree; + output_tree["status"] = true; + send_response(response, output_tree); + } + /** * @brief Delete an application. * @param response The HTTP response object. @@ -604,13 +629,13 @@ namespace confighttp { print_req(request); - pt::ptree outputTree; try { - pt::ptree fileTree; - pt::ptree newApps; - pt::read_json(config::stream.file_apps, fileTree); - auto &apps_node = fileTree.get_child("apps"s); - int index = stoi(request->path_match[1]); + nlohmann::json output_tree; + nlohmann::json new_apps = nlohmann::json::array(); + std::string file = file_handler::read_file(config::stream.file_apps.c_str()); + nlohmann::json file_tree = nlohmann::json::parse(file); + auto &apps_node = file_tree["apps"]; + const int index = std::stoi(request->path_match[1]); if (index < 0 || index >= static_cast(apps_node.size())) { std::string error; @@ -623,22 +648,19 @@ namespace confighttp { return; } - // Unfortunately Boost PT does not allow to directly edit the array, copy should do the trick - int i = 0; - for (const auto &[k, v] : apps_node) { - if (i++ != index) { - newApps.push_back(std::make_pair("", v)); + for (size_t i = 0; i < apps_node.size(); ++i) { + if (i != index) { + new_apps.push_back(apps_node[i]); } } - fileTree.erase("apps"); - fileTree.push_back(std::make_pair("apps", newApps)); + file_tree["apps"] = new_apps; - pt::write_json(config::stream.file_apps, fileTree); + file_handler::write_file(config::stream.file_apps.c_str(), file_tree.dump(4)); proc::refresh(config::stream.file_apps); - outputTree.put("status", true); - outputTree.put("result", "application "s + std::to_string(index) + " deleted"); - send_response(response, outputTree); + output_tree["status"] = true; + output_tree["result"] = "application " + std::to_string(index) + " deleted"; + send_response(response, output_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "DeleteApp: "sv << e.what(); bad_request(response, request, e.what()); @@ -646,66 +668,83 @@ namespace confighttp { } /** - * @brief Upload a cover image. + * @brief Get the list of paired clients. + * @param response The HTTP response object. + * @param request The HTTP request object. + * + * @api_examples{/api/clients/list| GET| null} + */ + void getClients(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { + return; + } + + print_req(request); + + const nlohmann::json named_certs = nvhttp::get_all_clients(); + + nlohmann::json output_tree; + output_tree["named_certs"] = named_certs; + output_tree["status"] = true; + send_response(response, output_tree); + } + + /** + * @brief Unpair a client. * @param response The HTTP response object. * @param request The HTTP request object. * The body for the post request should be JSON serialized in the following format: * @code{.json} * { - * "key": "igdb_", - * "url": "https://images.igdb.com/igdb/image/upload/t_cover_big_2x/.png" + * "uuid": "" * } * @endcode * - * @api_examples{/api/covers/upload| POST| {"key":"igdb_1234","url":"https://images.igdb.com/igdb/image/upload/t_cover_big_2x/abc123.png"}} + * @api_examples{/api/unpair| POST| {"uuid":"1234"}} */ - void uploadCover(resp_https_t response, req_https_t request) { + void unpair(resp_https_t response, req_https_t request) { if (!authenticate(response, request)) { return; } + print_req(request); + std::stringstream ss; - std::stringstream configStream; ss << request->content.rdbuf(); - pt::ptree outputTree; - pt::ptree inputTree; + try { - pt::read_json(ss, inputTree); + // TODO: Input Validation + nlohmann::json output_tree; + const nlohmann::json input_tree = nlohmann::json::parse(ss); + const std::string uuid = input_tree.value("uuid", ""); + output_tree["status"] = nvhttp::unpair_client(uuid); + send_response(response, output_tree); } catch (std::exception &e) { - BOOST_LOG(warning) << "UploadCover: "sv << e.what(); + BOOST_LOG(warning) << "Unpair: "sv << e.what(); bad_request(response, request, e.what()); - return; } + } - auto key = inputTree.get("key", ""); - if (key.empty()) { - bad_request(response, request, "Cover key is required"); + /** + * @brief Unpair all clients. + * @param response The HTTP response object. + * @param request The HTTP request object. + * + * @api_examples{/api/clients/unpair-all| POST| null} + */ + void unpairAll(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { return; } - auto url = inputTree.get("url", ""); - const std::string coverdir = platf::appdata().string() + "/covers/"; - file_handler::make_directory(coverdir); + print_req(request); - std::basic_string path = coverdir + http::url_escape(key) + ".png"; - if (!url.empty()) { - if (http::url_get_host(url) != "images.igdb.com") { - bad_request(response, request, "Only images.igdb.com is allowed"); - return; - } - if (!http::download_file(url, path)) { - bad_request(response, request, "Failed to download cover"); - return; - } - } else { - auto data = SimpleWeb::Crypto::Base64::decode(inputTree.get("data")); + nvhttp::erase_all_clients(); + proc::proc.terminate(); - std::ofstream imgfile(path); - imgfile.write(data.data(), (int) data.size()); - } - outputTree.put("status", true); - outputTree.put("path", path); - send_response(response, outputTree); + nlohmann::json output_tree; + output_tree["status"] = true; + send_response(response, output_tree); } /** @@ -722,18 +761,18 @@ namespace confighttp { print_req(request); - pt::ptree outputTree; - outputTree.put("status", true); - outputTree.put("platform", SUNSHINE_PLATFORM); - outputTree.put("version", PROJECT_VER); + nlohmann::json output_tree; + output_tree["status"] = true; + output_tree["platform"] = SUNSHINE_PLATFORM; + output_tree["version"] = PROJECT_VER; auto vars = config::parse_config(file_handler::read_file(config::sunshine.config_file.c_str())); for (auto &[name, value] : vars) { - outputTree.put(std::move(name), std::move(value)); + output_tree[name] = std::move(value); } - send_response(response, outputTree); + send_response(response, output_tree); } /** @@ -748,10 +787,10 @@ namespace confighttp { print_req(request); - pt::ptree outputTree; - outputTree.put("status", true); - outputTree.put("locale", config::sunshine.locale); - send_response(response, outputTree); + nlohmann::json output_tree; + output_tree["status"] = true; + output_tree["locale"] = config::sunshine.locale; + send_response(response, output_tree); } /** @@ -777,24 +816,24 @@ namespace confighttp { print_req(request); std::stringstream ss; - std::stringstream configStream; ss << request->content.rdbuf(); try { // TODO: Input Validation - pt::ptree inputTree; - pt::ptree outputTree; - pt::read_json(ss, inputTree); - for (const auto &[k, v] : inputTree) { - std::string value = inputTree.get(k); - if (value.length() == 0 || value.compare("null") == 0) { + std::stringstream config_stream; + nlohmann::json output_tree; + nlohmann::json input_tree = nlohmann::json::parse(ss); + for (const auto &[k, v] : input_tree.items()) { + if (v.is_null() || (v.is_string() && v.get().empty())) { continue; } - configStream << k << " = " << value << std::endl; + // v.dump() will dump valid json, which we do not want for strings in the config right now + // we should migrate the config file to straight json and get rid of all this nonsense + config_stream << k << " = " << (v.is_string() ? v.get() : v.dump()) << std::endl; } - file_handler::write_file(config::sunshine.config_file.c_str(), configStream.str()); - outputTree.put("status", true); - send_response(response, outputTree); + file_handler::write_file(config::sunshine.config_file.c_str(), config_stream.str()); + output_tree["status"] = true; + send_response(response, output_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "SaveConfig: "sv << e.what(); bad_request(response, request, e.what()); @@ -802,40 +841,83 @@ namespace confighttp { } /** - * @brief Restart Sunshine. + * @brief Upload a cover image. * @param response The HTTP response object. * @param request The HTTP request object. + * The body for the post request should be JSON serialized in the following format: + * @code{.json} + * { + * "key": "igdb_", + * "url": "https://images.igdb.com/igdb/image/upload/t_cover_big_2x/.png" + * } + * @endcode * - * @api_examples{/api/restart| POST| null} + * @api_examples{/api/covers/upload| POST| {"key":"igdb_1234","url":"https://images.igdb.com/igdb/image/upload/t_cover_big_2x/abc123.png"}} */ - void restart(resp_https_t response, req_https_t request) { + void uploadCover(resp_https_t response, req_https_t request) { if (!authenticate(response, request)) { return; } - print_req(request); + std::stringstream ss; + ss << request->content.rdbuf(); + try { + nlohmann::json output_tree; + nlohmann::json input_tree = nlohmann::json::parse(ss); - // We may not return from this call - platf::restart(); + std::string key = input_tree.value("key", ""); + if (key.empty()) { + bad_request(response, request, "Cover key is required"); + return; + } + std::string url = input_tree.value("url", ""); + + const std::string coverdir = platf::appdata().string() + "/covers/"; + file_handler::make_directory(coverdir); + + std::basic_string path = coverdir + http::url_escape(key) + ".png"; + if (!url.empty()) { + if (http::url_get_host(url) != "images.igdb.com") { + bad_request(response, request, "Only images.igdb.com is allowed"); + return; + } + if (!http::download_file(url, path)) { + bad_request(response, request, "Failed to download cover"); + return; + } + } else { + auto data = SimpleWeb::Crypto::Base64::decode(input_tree.value("data", "")); + + std::ofstream imgfile(path); + imgfile.write(data.data(), static_cast(data.size())); + } + output_tree["status"] = true; + output_tree["path"] = path; + send_response(response, output_tree); + } catch (std::exception &e) { + BOOST_LOG(warning) << "UploadCover: "sv << e.what(); + bad_request(response, request, e.what()); + } } /** - * @brief Reset the display device persistence. + * @brief Get the logs from the log file. * @param response The HTTP response object. * @param request The HTTP request object. * - * @api_examples{/api/reset-display-device-persistence| POST| null} + * @api_examples{/api/logs| GET| null} */ - void resetDisplayDevicePersistence(resp_https_t response, req_https_t request) { + void getLogs(resp_https_t response, req_https_t request) { if (!authenticate(response, request)) { return; } print_req(request); - pt::ptree outputTree; - outputTree.put("status", display_device::reset_persistence()); - send_response(response, outputTree); + std::string content = file_handler::read_file(config::sunshine.log_file.c_str()); + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Content-Type", "text/plain"); + response->write(SimpleWeb::StatusCode::success_ok, content, headers); } /** @@ -864,23 +946,21 @@ namespace confighttp { std::vector errors = {}; std::stringstream ss; - std::stringstream configStream; + std::stringstream config_stream; ss << request->content.rdbuf(); - try { // TODO: Input Validation - pt::ptree inputTree; - pt::ptree outputTree; - pt::read_json(ss, inputTree); - auto username = inputTree.count("currentUsername") > 0 ? inputTree.get("currentUsername") : ""; - auto newUsername = inputTree.get("newUsername"); - auto password = inputTree.count("currentPassword") > 0 ? inputTree.get("currentPassword") : ""; - auto newPassword = inputTree.count("newPassword") > 0 ? inputTree.get("newPassword") : ""; - auto confirmPassword = inputTree.count("confirmNewPassword") > 0 ? inputTree.get("confirmNewPassword") : ""; - if (newUsername.length() == 0) { + nlohmann::json output_tree; + nlohmann::json input_tree = nlohmann::json::parse(ss); + std::string username = input_tree.value("currentUsername", ""); + std::string newUsername = input_tree.value("newUsername", ""); + std::string password = input_tree.value("currentPassword", ""); + std::string newPassword = input_tree.value("newPassword", ""); + std::string confirmPassword = input_tree.value("confirmNewPassword", ""); + if (newUsername.empty()) { newUsername = username; } - if (newUsername.length() == 0) { + if (newUsername.empty()) { errors.emplace_back("Invalid Username"); } else { auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string(); @@ -890,7 +970,7 @@ namespace confighttp { } else { http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword); http::reload_user_creds(config::sunshine.credentials_file); - outputTree.put("status", true); + output_tree["status"] = true; } } else { errors.emplace_back("Invalid Current Credentials"); @@ -906,7 +986,7 @@ namespace confighttp { return; } - send_response(response, outputTree); + send_response(response, output_tree); } catch (std::exception &e) { BOOST_LOG(warning) << "SavePassword: "sv << e.what(); bad_request(response, request, e.what()); @@ -936,123 +1016,61 @@ namespace confighttp { std::stringstream ss; ss << request->content.rdbuf(); - try { - // TODO: Input Validation - pt::ptree inputTree; - pt::ptree outputTree; - pt::read_json(ss, inputTree); - std::string pin = inputTree.get("pin"); - std::string name = inputTree.get("name"); - outputTree.put("status", nvhttp::pin(pin, name)); - send_response(response, outputTree); - } catch (std::exception &e) { - BOOST_LOG(warning) << "SavePin: "sv << e.what(); - bad_request(response, request, e.what()); - } - } - - /** - * @brief Unpair all clients. - * @param response The HTTP response object. - * @param request The HTTP request object. - * - * @api_examples{/api/clients/unpair-all| POST| null} - */ - void unpairAll(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) { - return; - } - - print_req(request); - - nvhttp::erase_all_clients(); - proc::proc.terminate(); - - pt::ptree outputTree; - outputTree.put("status", true); - send_response(response, outputTree); - } - - /** - * @brief Unpair a client. - * @param response The HTTP response object. - * @param request The HTTP request object. - * The body for the post request should be JSON serialized in the following format: - * @code{.json} - * { - * "uuid": "" - * } - * @endcode - * - * @api_examples{/api/unpair| POST| {"uuid":"1234"}} - */ - void unpair(resp_https_t response, req_https_t request) { - if (!authenticate(response, request)) { - return; - } - - print_req(request); - - std::stringstream ss; - ss << request->content.rdbuf(); + nlohmann::json output_tree; + nlohmann::json input_tree = nlohmann::json::parse(ss); + const std::string name = input_tree.value("name", ""); + const std::string pin = input_tree.value("pin", ""); + + int _pin = 0; + _pin = std::stoi(pin); + if (_pin < 0 || _pin > 9999) { + bad_request(response, request, "PIN must be between 0000 and 9999"); + } - try { - // TODO: Input Validation - pt::ptree inputTree; - pt::ptree outputTree; - pt::read_json(ss, inputTree); - std::string uuid = inputTree.get("uuid"); - outputTree.put("status", nvhttp::unpair_client(uuid)); - send_response(response, outputTree); + output_tree["status"] = nvhttp::pin(pin, name); + send_response(response, output_tree); } catch (std::exception &e) { - BOOST_LOG(warning) << "Unpair: "sv << e.what(); + BOOST_LOG(warning) << "SavePin: "sv << e.what(); bad_request(response, request, e.what()); } } /** - * @brief Get the list of paired clients. + * @brief Reset the display device persistence. * @param response The HTTP response object. * @param request The HTTP request object. * - * @api_examples{/api/clients/list| GET| null} + * @api_examples{/api/reset-display-device-persistence| POST| null} */ - void listClients(resp_https_t response, req_https_t request) { + void resetDisplayDevicePersistence(resp_https_t response, req_https_t request) { if (!authenticate(response, request)) { return; } print_req(request); - const pt::ptree named_certs = nvhttp::get_all_clients(); - - pt::ptree outputTree; - outputTree.put("status", false); - outputTree.add_child("named_certs", named_certs); - outputTree.put("status", true); - send_response(response, outputTree); + nlohmann::json output_tree; + output_tree["status"] = display_device::reset_persistence(); + send_response(response, output_tree); } /** - * @brief Close the currently running application. + * @brief Restart Sunshine. * @param response The HTTP response object. * @param request The HTTP request object. * - * @api_examples{/api/apps/close| POST| null} + * @api_examples{/api/restart| POST| null} */ - void closeApp(resp_https_t response, req_https_t request) { + void restart(resp_https_t response, req_https_t request) { if (!authenticate(response, request)) { return; } print_req(request); - proc::proc.terminate(); - - pt::ptree outputTree; - outputTree.put("status", true); - send_response(response, outputTree); + // We may not return from this call + platf::restart(); } void start() { @@ -1095,7 +1113,7 @@ namespace confighttp { server.resource["^/api/password$"]["POST"] = savePassword; server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp; server.resource["^/api/clients/unpair-all$"]["POST"] = unpairAll; - server.resource["^/api/clients/list$"]["GET"] = listClients; + server.resource["^/api/clients/list$"]["GET"] = getClients; server.resource["^/api/clients/unpair$"]["POST"] = unpair; server.resource["^/api/apps/close$"]["POST"] = closeApp; server.resource["^/api/covers/upload$"]["POST"] = uploadCover; diff --git a/src/confighttp.h b/src/confighttp.h index af5174aa403..9925603927c 100644 --- a/src/confighttp.h +++ b/src/confighttp.h @@ -5,7 +5,6 @@ #pragma once // standard includes -#include #include // local includes diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 647ec83f9c6..6b518bf91e9 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -765,14 +765,14 @@ namespace nvhttp { response->close_connection_after_response = true; } - pt::ptree get_all_clients() { - pt::ptree named_cert_nodes; + nlohmann::json get_all_clients() { + nlohmann::json named_cert_nodes = nlohmann::json::array(); client_t &client = client_root; for (auto &named_cert : client.named_devices) { - pt::ptree named_cert_node; - named_cert_node.put("name"s, named_cert.name); - named_cert_node.put("uuid"s, named_cert.uuid); - named_cert_nodes.push_back(std::make_pair(""s, named_cert_node)); + nlohmann::json named_cert_node; + named_cert_node["name"] = named_cert.name; + named_cert_node["uuid"] = named_cert.uuid; + named_cert_nodes.push_back(named_cert_node); } return named_cert_nodes; @@ -1177,13 +1177,13 @@ namespace nvhttp { save_state(); } - int unpair_client(std::string uuid) { - int removed = 0; + bool unpair_client(const std::string_view uuid) { + bool removed = false; client_t &client = client_root; for (auto it = client.named_devices.begin(); it != client.named_devices.end();) { if ((*it).uuid == uuid) { it = client.named_devices.erase(it); - removed++; + removed = true; } else { ++it; } diff --git a/src/nvhttp.h b/src/nvhttp.h index 3ffbcafc493..636337071b9 100644 --- a/src/nvhttp.h +++ b/src/nvhttp.h @@ -10,6 +10,7 @@ // lib includes #include +#include #include // local includes @@ -176,20 +177,21 @@ namespace nvhttp { /** * @brief Remove single client. + * @param uuid The UUID of the client to remove. * @examples * nvhttp::unpair_client("4D7BB2DD-5704-A405-B41C-891A022932E1"); * @examples_end */ - int unpair_client(std::string uniqueid); + bool unpair_client(std::string_view uuid); /** * @brief Get all paired clients. * @return The list of all paired clients. * @examples - * boost::property_tree::ptree clients = nvhttp::get_all_clients(); + * nlohmann::json clients = nvhttp::get_all_clients(); * @examples_end */ - boost::property_tree::ptree get_all_clients(); + nlohmann::json get_all_clients(); /** * @brief Remove all paired clients. diff --git a/src_assets/common/assets/web/apps.html b/src_assets/common/assets/web/apps.html index de59d30e6ca..73e5b274ca8 100644 --- a/src_assets/common/assets/web/apps.html +++ b/src_assets/common/assets/web/apps.html @@ -234,8 +234,8 @@

{{ $t('apps.applications_title') }}

- +
{{ $t('apps.exit_timeout_desc') }}
diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index 5fb7e3ccf74..38b40da43ee 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -343,10 +343,7 @@

{{ $t('config.configuration') }}

this.$forceUpdate() }, serialize() { - let config = JSON.parse(JSON.stringify(this.config)); - config.global_prep_cmd = JSON.stringify(config.global_prep_cmd); - config.dd_mode_remapping = JSON.stringify(config.dd_mode_remapping); - return config; + return JSON.parse(JSON.stringify(this.config)); }, save() { this.saved = false; @@ -360,17 +357,8 @@

{{ $t('config.configuration') }}

Object.keys(tab.options).forEach(optionKey => { let delete_value = false - if (["global_prep_cmd", "dd_mode_remapping"].includes(optionKey)) { - const config_value = config[optionKey] - const default_value = JSON.stringify(tab.options[optionKey]) - - if (config_value === default_value) { - delete_value = true - } - } - // todo: add proper type checking - if (String(config[optionKey]) === String(tab.options[optionKey])) { + if (JSON.stringify(config[optionKey]) === JSON.stringify(tab.options[optionKey])) { delete_value = true } diff --git a/src_assets/common/assets/web/configs/tabs/encoders/NvidiaNvencEncoder.vue b/src_assets/common/assets/web/configs/tabs/encoders/NvidiaNvencEncoder.vue index 15ae8dc6789..a6bd2a00275 100644 --- a/src_assets/common/assets/web/configs/tabs/encoders/NvidiaNvencEncoder.vue +++ b/src_assets/common/assets/web/configs/tabs/encoders/NvidiaNvencEncoder.vue @@ -39,14 +39,12 @@ const config = ref(props.config)
-
- - -
{{ $t('config.nvenc_spatial_aq_desc') }}
-
+
diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json index 3f6ddfc6b37..2735ff3ba30 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json @@ -65,7 +65,7 @@ "env_vars_desc": "All commands get these environment variables by default:", "env_xrandr_example": "Example - Xrandr for Resolution Automation:", "exit_timeout": "Exit Timeout", - "exit_timeout_desc": "Number of seconds to wait for all app processes to gracefully exit when requested to quit. If unset, the default is to wait up to 5 seconds. If set to zero or a negative value, the app will be immediately terminated.", + "exit_timeout_desc": "Number of seconds to wait for all app processes to gracefully exit when requested to quit. If unset, the default is to wait up to 5 seconds. If set to 0, the app will be immediately terminated.", "find_cover": "Find Cover", "global_prep_desc": "Enable/Disable the execution of Global Prep Commands for this application.", "global_prep_name": "Global Prep Commands", @@ -279,8 +279,6 @@ "nvenc_realtime_hags_desc": "Currently NVIDIA drivers may freeze in encoder when HAGS is enabled, realtime priority is used and VRAM utilization is close to maximum. Disabling this option lowers the priority to high, sidestepping the freeze at the cost of reduced capture performance when the GPU is heavily loaded.", "nvenc_spatial_aq": "Spatial AQ", "nvenc_spatial_aq_desc": "Assign higher QP values to flat regions of the video. Recommended to enable when streaming at lower bitrates.", - "nvenc_spatial_aq_disabled": "Disabled (faster, default)", - "nvenc_spatial_aq_enabled": "Enabled (slower)", "nvenc_twopass": "Two-pass mode", "nvenc_twopass_desc": "Adds preliminary encoding pass. This allows to detect more motion vectors, better distribute bitrate across the frame and more strictly adhere to bitrate limits. Disabling it is not recommended since this can lead to occasional bitrate overshoot and subsequent packet loss.", "nvenc_twopass_disabled": "Disabled (fastest, not recommended)", diff --git a/src_assets/windows/assets/apps.json b/src_assets/windows/assets/apps.json index 30b6ada06dd..c8d4cb74952 100644 --- a/src_assets/windows/assets/apps.json +++ b/src_assets/windows/assets/apps.json @@ -8,8 +8,8 @@ { "name": "Steam Big Picture", "cmd": "steam://open/bigpicture", - "auto-detach": "true", - "wait-all": "true", + "auto-detach": true, + "wait-all": true, "image-path": "steam.png" } ]