fix(api): return proper json objects (#3544)
This commit is contained in:
1
.github/workflows/CI.yml
vendored
1
.github/workflows/CI.yml
vendored
@@ -887,7 +887,6 @@ jobs:
|
|||||||
mingw-w64-ucrt-x86_64-graphviz
|
mingw-w64-ucrt-x86_64-graphviz
|
||||||
mingw-w64-ucrt-x86_64-MinHook
|
mingw-w64-ucrt-x86_64-MinHook
|
||||||
mingw-w64-ucrt-x86_64-miniupnpc
|
mingw-w64-ucrt-x86_64-miniupnpc
|
||||||
mingw-w64-ucrt-x86_64-nlohmann-json
|
|
||||||
mingw-w64-ucrt-x86_64-nodejs
|
mingw-w64-ucrt-x86_64-nodejs
|
||||||
mingw-w64-ucrt-x86_64-nsis
|
mingw-w64-ucrt-x86_64-nsis
|
||||||
mingw-w64-ucrt-x86_64-onevpl
|
mingw-w64-ucrt-x86_64-onevpl
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES
|
|||||||
${CMAKE_THREAD_LIBS_INIT}
|
${CMAKE_THREAD_LIBS_INIT}
|
||||||
enet
|
enet
|
||||||
libdisplaydevice::display_device
|
libdisplaydevice::display_device
|
||||||
|
nlohmann_json::nlohmann_json
|
||||||
opus
|
opus
|
||||||
${FFMPEG_LIBRARIES}
|
${FFMPEG_LIBRARIES}
|
||||||
${Boost_LIBRARIES}
|
${Boost_LIBRARIES}
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ list(PREPEND PLATFORM_LIBRARIES
|
|||||||
libstdc++.a
|
libstdc++.a
|
||||||
libwinpthread.a
|
libwinpthread.a
|
||||||
minhook::minhook
|
minhook::minhook
|
||||||
nlohmann_json::nlohmann_json
|
|
||||||
ntdll
|
ntdll
|
||||||
setupapi
|
setupapi
|
||||||
shlwapi
|
shlwapi
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/Simple-Web-Server")
|
|||||||
add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/libdisplaydevice")
|
add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/libdisplaydevice")
|
||||||
|
|
||||||
# common dependencies
|
# common dependencies
|
||||||
|
include("${CMAKE_MODULE_PATH}/dependencies/nlohmann_json.cmake")
|
||||||
find_package(OpenSSL REQUIRED)
|
find_package(OpenSSL REQUIRED)
|
||||||
find_package(PkgConfig REQUIRED)
|
find_package(PkgConfig REQUIRED)
|
||||||
find_package(Threads REQUIRED)
|
find_package(Threads REQUIRED)
|
||||||
|
|||||||
18
cmake/dependencies/nlohmann_json.cmake
Normal file
18
cmake/dependencies/nlohmann_json.cmake
Normal file
@@ -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()
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
# windows specific dependencies
|
# windows specific dependencies
|
||||||
|
|
||||||
# nlohmann_json
|
|
||||||
find_package(nlohmann_json CONFIG 3.11 REQUIRED)
|
|
||||||
|
|
||||||
# Make sure MinHook is installed
|
# Make sure MinHook is installed
|
||||||
find_library(MINHOOK_LIBRARY libMinHook.a REQUIRED)
|
find_library(MINHOOK_LIBRARY libMinHook.a REQUIRED)
|
||||||
find_path(MINHOOK_INCLUDE_DIR MinHook.h PATH_SUFFIXES include REQUIRED)
|
find_path(MINHOOK_INCLUDE_DIR MinHook.h PATH_SUFFIXES include REQUIRED)
|
||||||
|
|||||||
38
docs/api.md
38
docs/api.md
@@ -12,17 +12,23 @@ basic authentication with the admin username and password.
|
|||||||
## GET /api/apps
|
## GET /api/apps
|
||||||
@copydoc confighttp::getApps()
|
@copydoc confighttp::getApps()
|
||||||
|
|
||||||
## GET /api/logs
|
|
||||||
@copydoc confighttp::getLogs()
|
|
||||||
|
|
||||||
## POST /api/apps
|
## POST /api/apps
|
||||||
@copydoc confighttp::saveApp()
|
@copydoc confighttp::saveApp()
|
||||||
|
|
||||||
|
## POST /api/apps/close
|
||||||
|
@copydoc confighttp::closeApp()
|
||||||
|
|
||||||
## DELETE /api/apps/{index}
|
## DELETE /api/apps/{index}
|
||||||
@copydoc confighttp::deleteApp()
|
@copydoc confighttp::deleteApp()
|
||||||
|
|
||||||
## POST /api/covers/upload
|
## GET /api/clients/list
|
||||||
@copydoc confighttp::uploadCover()
|
@copydoc confighttp::getClients()
|
||||||
|
|
||||||
|
## POST /api/clients/unpair
|
||||||
|
@copydoc confighttp::unpair()
|
||||||
|
|
||||||
|
## POST /api/clients/unpair-all
|
||||||
|
@copydoc confighttp::unpairAll()
|
||||||
|
|
||||||
## GET /api/config
|
## GET /api/config
|
||||||
@copydoc confighttp::getConfig()
|
@copydoc confighttp::getConfig()
|
||||||
@@ -33,11 +39,11 @@ basic authentication with the admin username and password.
|
|||||||
## POST /api/config
|
## POST /api/config
|
||||||
@copydoc confighttp::saveConfig()
|
@copydoc confighttp::saveConfig()
|
||||||
|
|
||||||
## POST /api/restart
|
## POST /api/covers/upload
|
||||||
@copydoc confighttp::restart()
|
@copydoc confighttp::uploadCover()
|
||||||
|
|
||||||
## POST /api/reset-display-device-persistence
|
## GET /api/logs
|
||||||
@copydoc confighttp::resetDisplayDevicePersistence()
|
@copydoc confighttp::getLogs()
|
||||||
|
|
||||||
## POST /api/password
|
## POST /api/password
|
||||||
@copydoc confighttp::savePassword()
|
@copydoc confighttp::savePassword()
|
||||||
@@ -45,17 +51,11 @@ basic authentication with the admin username and password.
|
|||||||
## POST /api/pin
|
## POST /api/pin
|
||||||
@copydoc confighttp::savePin()
|
@copydoc confighttp::savePin()
|
||||||
|
|
||||||
## POST /api/clients/unpair-all
|
## POST /api/reset-display-device-persistence
|
||||||
@copydoc confighttp::unpairAll()
|
@copydoc confighttp::resetDisplayDevicePersistence()
|
||||||
|
|
||||||
## POST /api/clients/unpair
|
## POST /api/restart
|
||||||
@copydoc confighttp::unpair()
|
@copydoc confighttp::restart()
|
||||||
|
|
||||||
## GET /api/clients/list
|
|
||||||
@copydoc confighttp::listClients()
|
|
||||||
|
|
||||||
## POST /api/apps/close
|
|
||||||
@copydoc confighttp::closeApp()
|
|
||||||
|
|
||||||
<div class="section_buttons">
|
<div class="section_buttons">
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
This is an option for both prep-cmd and regular commands and will launch the process with the current user without a
|
||||||
UAC prompt.
|
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**
|
**Example**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "Game With AntiCheat that Requires Admin",
|
"name": "Game With AntiCheat that Requires Admin",
|
||||||
"output": "",
|
"output": "",
|
||||||
"cmd": "ping 127.0.0.1",
|
"cmd": "ping 127.0.0.1",
|
||||||
"exclude-global-prep-cmd": "false",
|
"exclude-global-prep-cmd": false,
|
||||||
"elevated": "true",
|
"elevated": true,
|
||||||
"prep-cmd": [
|
"prep-cmd": [
|
||||||
{
|
{
|
||||||
"do": "powershell.exe -command \"Start-Streaming\"",
|
"do": "powershell.exe -command \"Start-Streaming\"",
|
||||||
"undo": "powershell.exe -command \"Stop-Streaming\"",
|
"undo": "powershell.exe -command \"Stop-Streaming\"",
|
||||||
"elevated": "false"
|
"elevated": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"image-path": ""
|
"image-path": ""
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ dependencies=(
|
|||||||
"mingw-w64-ucrt-x86_64-graphviz" # Optional, for docs
|
"mingw-w64-ucrt-x86_64-graphviz" # Optional, for docs
|
||||||
"mingw-w64-ucrt-x86_64-MinHook"
|
"mingw-w64-ucrt-x86_64-MinHook"
|
||||||
"mingw-w64-ucrt-x86_64-miniupnpc"
|
"mingw-w64-ucrt-x86_64-miniupnpc"
|
||||||
"mingw-w64-ucrt-x86_64-nlohmann-json"
|
|
||||||
"mingw-w64-ucrt-x86_64-nodejs"
|
"mingw-w64-ucrt-x86_64-nodejs"
|
||||||
"mingw-w64-ucrt-x86_64-nsis"
|
"mingw-w64-ucrt-x86_64-nsis"
|
||||||
"mingw-w64-ucrt-x86_64-onevpl"
|
"mingw-w64-ucrt-x86_64-onevpl"
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ editing the `conf` file in a text editor. Use the examples as reference.
|
|||||||
<tr>
|
<tr>
|
||||||
<td>Example</td>
|
<td>Example</td>
|
||||||
<td colspan="2">@code{}
|
<td colspan="2">@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</td>
|
@endcode</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -8,16 +8,14 @@
|
|||||||
|
|
||||||
// standard includes
|
// standard includes
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
|
||||||
// lib includes
|
// lib includes
|
||||||
#include <boost/algorithm/string.hpp>
|
#include <boost/algorithm/string.hpp>
|
||||||
#include <boost/asio/ssl/context.hpp>
|
#include <boost/asio/ssl/context.hpp>
|
||||||
#include <boost/asio/ssl/context_base.hpp>
|
|
||||||
#include <boost/filesystem.hpp>
|
#include <boost/filesystem.hpp>
|
||||||
#include <boost/property_tree/json_parser.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include <boost/property_tree/ptree.hpp>
|
|
||||||
#include <boost/property_tree/xml_parser.hpp>
|
|
||||||
#include <Simple-Web-Server/crypto.hpp>
|
#include <Simple-Web-Server/crypto.hpp>
|
||||||
#include <Simple-Web-Server/server_https.hpp>
|
#include <Simple-Web-Server/server_https.hpp>
|
||||||
|
|
||||||
@@ -34,7 +32,6 @@
|
|||||||
#include "nvhttp.h"
|
#include "nvhttp.h"
|
||||||
#include "platform/common.h"
|
#include "platform/common.h"
|
||||||
#include "process.h"
|
#include "process.h"
|
||||||
#include "rtsp.h"
|
|
||||||
#include "utility.h"
|
#include "utility.h"
|
||||||
#include "uuid.h"
|
#include "uuid.h"
|
||||||
#include "version.h"
|
#include "version.h"
|
||||||
@@ -43,7 +40,6 @@ using namespace std::literals;
|
|||||||
|
|
||||||
namespace confighttp {
|
namespace confighttp {
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
namespace pt = boost::property_tree;
|
|
||||||
|
|
||||||
using https_server_t = SimpleWeb::Server<SimpleWeb::HTTPS>;
|
using https_server_t = SimpleWeb::Server<SimpleWeb::HTTPS>;
|
||||||
|
|
||||||
@@ -82,10 +78,11 @@ namespace confighttp {
|
|||||||
* @param response The HTTP response object.
|
* @param response The HTTP response object.
|
||||||
* @param output_tree The JSON tree to send.
|
* @param output_tree The JSON tree to send.
|
||||||
*/
|
*/
|
||||||
void send_response(resp_https_t response, const pt::ptree &output_tree) {
|
void send_response(resp_https_t response, const nlohmann::json &output_tree) {
|
||||||
std::ostringstream data;
|
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||||
pt::write_json(data, output_tree);
|
headers.emplace("Content-Type", "application/json");
|
||||||
response->write(data.str());
|
|
||||||
|
response->write(output_tree.dump(), headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -96,10 +93,20 @@ namespace confighttp {
|
|||||||
void send_unauthorized(resp_https_t response, req_https_t request) {
|
void send_unauthorized(resp_https_t response, req_https_t request) {
|
||||||
auto address = net::addr_to_normalized_string(request->remote_endpoint().address());
|
auto address = net::addr_to_normalized_string(request->remote_endpoint().address());
|
||||||
BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv;
|
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 {
|
const SimpleWeb::CaseInsensitiveMultimap headers {
|
||||||
|
{"Content-Type", "application/json"},
|
||||||
{"WWW-Authenticate", R"(Basic realm="Sunshine Gamestream Host", charset="UTF-8")"}
|
{"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) {
|
void not_found(resp_https_t response, [[maybe_unused]] req_https_t request) {
|
||||||
constexpr SimpleWeb::StatusCode code = SimpleWeb::StatusCode::client_error_not_found;
|
constexpr SimpleWeb::StatusCode code = SimpleWeb::StatusCode::client_error_not_found;
|
||||||
|
|
||||||
pt::ptree tree;
|
nlohmann::json tree;
|
||||||
tree.put("status_code", static_cast<int>(code));
|
tree["status_code"] = code;
|
||||||
tree.put("error", "Not Found");
|
tree["error"] = "Not Found";
|
||||||
|
|
||||||
std::ostringstream data;
|
|
||||||
pt::write_json(data, tree);
|
|
||||||
|
|
||||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||||
headers.emplace("Content-Type", "application/json");
|
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") {
|
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;
|
constexpr SimpleWeb::StatusCode code = SimpleWeb::StatusCode::client_error_bad_request;
|
||||||
|
|
||||||
pt::ptree tree;
|
nlohmann::json tree;
|
||||||
tree.put("status_code", static_cast<int>(code));
|
tree["status_code"] = code;
|
||||||
tree.put("status", false);
|
tree["status"] = false;
|
||||||
tree.put("error", error_message);
|
tree["error"] = error_message;
|
||||||
|
|
||||||
std::ostringstream data;
|
|
||||||
pt::write_json(data, tree);
|
|
||||||
|
|
||||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||||
headers.emplace("Content-Type", "application/json");
|
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);
|
print_req(request);
|
||||||
|
|
||||||
std::string content = file_handler::read_file(config::stream.file_apps.c_str());
|
try {
|
||||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
std::string content = file_handler::read_file(config::stream.file_apps.c_str());
|
||||||
headers.emplace("Content-Type", "application/json");
|
nlohmann::json file_tree = nlohmann::json::parse(content);
|
||||||
response->write(content, headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Legacy versions of Sunshine used strings for boolean and integers, let's convert them
|
||||||
* @brief Get the logs from the log file.
|
// List of keys to convert to boolean
|
||||||
* @param response The HTTP response object.
|
std::vector<std::string> boolean_keys = {
|
||||||
* @param request The HTTP request object.
|
"exclude-global-prep-cmd",
|
||||||
*
|
"elevated",
|
||||||
* @api_examples{/api/logs| GET| null}
|
"auto-detach",
|
||||||
*/
|
"wait-all"
|
||||||
void getLogs(resp_https_t response, req_https_t request) {
|
};
|
||||||
if (!authenticate(response, request)) {
|
|
||||||
return;
|
// List of keys to convert to integers
|
||||||
|
std::vector<std::string> 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<std::string>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (app.contains("prep-cmd")) {
|
||||||
|
for (auto &prep : app["prep-cmd"]) {
|
||||||
|
if (prep.contains("elevated") && prep["elevated"].is_string()) {
|
||||||
|
prep["elevated"] = prep["elevated"] == "true";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
std::stringstream ss;
|
||||||
ss << request->content.rdbuf();
|
ss << request->content.rdbuf();
|
||||||
|
|
||||||
BOOST_LOG(info) << config::stream.file_apps;
|
|
||||||
try {
|
try {
|
||||||
// TODO: Input Validation
|
// TODO: Input Validation
|
||||||
pt::ptree fileTree;
|
nlohmann::json output_tree;
|
||||||
pt::ptree inputTree;
|
nlohmann::json input_tree = nlohmann::json::parse(ss);
|
||||||
pt::ptree outputTree;
|
std::string file = file_handler::read_file(config::stream.file_apps.c_str());
|
||||||
pt::read_json(ss, inputTree);
|
BOOST_LOG(info) << file;
|
||||||
pt::read_json(config::stream.file_apps, fileTree);
|
nlohmann::json file_tree = nlohmann::json::parse(file);
|
||||||
|
|
||||||
if (inputTree.get_child("prep-cmd").empty()) {
|
if (input_tree["prep-cmd"].empty()) {
|
||||||
inputTree.erase("prep-cmd");
|
input_tree.erase("prep-cmd");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inputTree.get_child("detached").empty()) {
|
if (input_tree["detached"].empty()) {
|
||||||
inputTree.erase("detached");
|
input_tree.erase("detached");
|
||||||
}
|
}
|
||||||
|
|
||||||
auto &apps_node = fileTree.get_child("apps"s);
|
auto &apps_node = file_tree["apps"];
|
||||||
int index = inputTree.get<int>("index");
|
int index = input_tree["index"].get<int>(); // this will intentionally cause exception if the provided value is the wrong type
|
||||||
|
|
||||||
inputTree.erase("index");
|
input_tree.erase("index");
|
||||||
|
|
||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
apps_node.push_back(std::make_pair("", inputTree));
|
apps_node.push_back(input_tree);
|
||||||
} else {
|
} else {
|
||||||
// Unfortunately Boost PT does not allow to directly edit the array, copy should do the trick
|
nlohmann::json newApps = nlohmann::json::array();
|
||||||
pt::ptree newApps;
|
for (size_t i = 0; i < apps_node.size(); ++i) {
|
||||||
int i = 0;
|
|
||||||
for (const auto &[k, v] : apps_node) {
|
|
||||||
if (i == index) {
|
if (i == index) {
|
||||||
newApps.push_back(std::make_pair("", inputTree));
|
newApps.push_back(input_tree);
|
||||||
} else {
|
} else {
|
||||||
newApps.push_back(std::make_pair("", v));
|
newApps.push_back(apps_node[i]);
|
||||||
}
|
}
|
||||||
i++;
|
|
||||||
}
|
}
|
||||||
fileTree.erase("apps");
|
file_tree["apps"] = newApps;
|
||||||
fileTree.push_back(std::make_pair("apps", newApps));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort the apps array by name
|
// Sort the apps array by name
|
||||||
std::vector<pt::ptree> apps_vector;
|
std::sort(apps_node.begin(), apps_node.end(), [](const nlohmann::json &a, const nlohmann::json &b) {
|
||||||
for (const auto &[k, v] : fileTree.get_child("apps")) {
|
return a["name"].get<std::string>() < b["name"].get<std::string>();
|
||||||
apps_vector.push_back(v);
|
|
||||||
}
|
|
||||||
std::ranges::sort(apps_vector, [](const pt::ptree &a, const pt::ptree &b) {
|
|
||||||
return a.get<std::string>("name") < b.get<std::string>("name");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
pt::ptree sorted_apps;
|
file_handler::write_file(config::stream.file_apps.c_str(), file_tree.dump(4));
|
||||||
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);
|
|
||||||
proc::refresh(config::stream.file_apps);
|
proc::refresh(config::stream.file_apps);
|
||||||
|
|
||||||
outputTree.put("status", true);
|
output_tree["status"] = true;
|
||||||
send_response(response, outputTree);
|
send_response(response, output_tree);
|
||||||
} catch (std::exception &e) {
|
} catch (std::exception &e) {
|
||||||
BOOST_LOG(warning) << "SaveApp: "sv << e.what();
|
BOOST_LOG(warning) << "SaveApp: "sv << e.what();
|
||||||
bad_request(response, request, 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.
|
* @brief Delete an application.
|
||||||
* @param response The HTTP response object.
|
* @param response The HTTP response object.
|
||||||
@@ -604,13 +629,13 @@ namespace confighttp {
|
|||||||
|
|
||||||
print_req(request);
|
print_req(request);
|
||||||
|
|
||||||
pt::ptree outputTree;
|
|
||||||
try {
|
try {
|
||||||
pt::ptree fileTree;
|
nlohmann::json output_tree;
|
||||||
pt::ptree newApps;
|
nlohmann::json new_apps = nlohmann::json::array();
|
||||||
pt::read_json(config::stream.file_apps, fileTree);
|
std::string file = file_handler::read_file(config::stream.file_apps.c_str());
|
||||||
auto &apps_node = fileTree.get_child("apps"s);
|
nlohmann::json file_tree = nlohmann::json::parse(file);
|
||||||
int index = stoi(request->path_match[1]);
|
auto &apps_node = file_tree["apps"];
|
||||||
|
const int index = std::stoi(request->path_match[1]);
|
||||||
|
|
||||||
if (index < 0 || index >= static_cast<int>(apps_node.size())) {
|
if (index < 0 || index >= static_cast<int>(apps_node.size())) {
|
||||||
std::string error;
|
std::string error;
|
||||||
@@ -623,22 +648,19 @@ namespace confighttp {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unfortunately Boost PT does not allow to directly edit the array, copy should do the trick
|
for (size_t i = 0; i < apps_node.size(); ++i) {
|
||||||
int i = 0;
|
if (i != index) {
|
||||||
for (const auto &[k, v] : apps_node) {
|
new_apps.push_back(apps_node[i]);
|
||||||
if (i++ != index) {
|
|
||||||
newApps.push_back(std::make_pair("", v));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fileTree.erase("apps");
|
file_tree["apps"] = new_apps;
|
||||||
fileTree.push_back(std::make_pair("apps", newApps));
|
|
||||||
|
|
||||||
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);
|
proc::refresh(config::stream.file_apps);
|
||||||
|
|
||||||
outputTree.put("status", true);
|
output_tree["status"] = true;
|
||||||
outputTree.put("result", "application "s + std::to_string(index) + " deleted");
|
output_tree["result"] = "application " + std::to_string(index) + " deleted";
|
||||||
send_response(response, outputTree);
|
send_response(response, output_tree);
|
||||||
} catch (std::exception &e) {
|
} catch (std::exception &e) {
|
||||||
BOOST_LOG(warning) << "DeleteApp: "sv << e.what();
|
BOOST_LOG(warning) << "DeleteApp: "sv << e.what();
|
||||||
bad_request(response, request, 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 response The HTTP response object.
|
||||||
* @param request The HTTP request object.
|
* @param request The HTTP request object.
|
||||||
* The body for the post request should be JSON serialized in the following format:
|
* The body for the post request should be JSON serialized in the following format:
|
||||||
* @code{.json}
|
* @code{.json}
|
||||||
* {
|
* {
|
||||||
* "key": "igdb_<game_id>",
|
* "uuid": "<uuid>"
|
||||||
* "url": "https://images.igdb.com/igdb/image/upload/t_cover_big_2x/<slug>.png"
|
|
||||||
* }
|
* }
|
||||||
* @endcode
|
* @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)) {
|
if (!authenticate(response, request)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print_req(request);
|
||||||
|
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
std::stringstream configStream;
|
|
||||||
ss << request->content.rdbuf();
|
ss << request->content.rdbuf();
|
||||||
pt::ptree outputTree;
|
|
||||||
pt::ptree inputTree;
|
|
||||||
try {
|
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) {
|
} catch (std::exception &e) {
|
||||||
BOOST_LOG(warning) << "UploadCover: "sv << e.what();
|
BOOST_LOG(warning) << "Unpair: "sv << e.what();
|
||||||
bad_request(response, request, 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto key = inputTree.get("key", "");
|
print_req(request);
|
||||||
if (key.empty()) {
|
|
||||||
bad_request(response, request, "Cover key is required");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto url = inputTree.get("url", "");
|
|
||||||
|
|
||||||
const std::string coverdir = platf::appdata().string() + "/covers/";
|
nvhttp::erase_all_clients();
|
||||||
file_handler::make_directory(coverdir);
|
proc::proc.terminate();
|
||||||
|
|
||||||
std::basic_string path = coverdir + http::url_escape(key) + ".png";
|
nlohmann::json output_tree;
|
||||||
if (!url.empty()) {
|
output_tree["status"] = true;
|
||||||
if (http::url_get_host(url) != "images.igdb.com") {
|
send_response(response, output_tree);
|
||||||
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<std::string>("data"));
|
|
||||||
|
|
||||||
std::ofstream imgfile(path);
|
|
||||||
imgfile.write(data.data(), (int) data.size());
|
|
||||||
}
|
|
||||||
outputTree.put("status", true);
|
|
||||||
outputTree.put("path", path);
|
|
||||||
send_response(response, outputTree);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -722,18 +761,18 @@ namespace confighttp {
|
|||||||
|
|
||||||
print_req(request);
|
print_req(request);
|
||||||
|
|
||||||
pt::ptree outputTree;
|
nlohmann::json output_tree;
|
||||||
outputTree.put("status", true);
|
output_tree["status"] = true;
|
||||||
outputTree.put("platform", SUNSHINE_PLATFORM);
|
output_tree["platform"] = SUNSHINE_PLATFORM;
|
||||||
outputTree.put("version", PROJECT_VER);
|
output_tree["version"] = PROJECT_VER;
|
||||||
|
|
||||||
auto vars = config::parse_config(file_handler::read_file(config::sunshine.config_file.c_str()));
|
auto vars = config::parse_config(file_handler::read_file(config::sunshine.config_file.c_str()));
|
||||||
|
|
||||||
for (auto &[name, value] : vars) {
|
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);
|
print_req(request);
|
||||||
|
|
||||||
pt::ptree outputTree;
|
nlohmann::json output_tree;
|
||||||
outputTree.put("status", true);
|
output_tree["status"] = true;
|
||||||
outputTree.put("locale", config::sunshine.locale);
|
output_tree["locale"] = config::sunshine.locale;
|
||||||
send_response(response, outputTree);
|
send_response(response, output_tree);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -777,24 +816,24 @@ namespace confighttp {
|
|||||||
print_req(request);
|
print_req(request);
|
||||||
|
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
std::stringstream configStream;
|
|
||||||
ss << request->content.rdbuf();
|
ss << request->content.rdbuf();
|
||||||
try {
|
try {
|
||||||
// TODO: Input Validation
|
// TODO: Input Validation
|
||||||
pt::ptree inputTree;
|
std::stringstream config_stream;
|
||||||
pt::ptree outputTree;
|
nlohmann::json output_tree;
|
||||||
pt::read_json(ss, inputTree);
|
nlohmann::json input_tree = nlohmann::json::parse(ss);
|
||||||
for (const auto &[k, v] : inputTree) {
|
for (const auto &[k, v] : input_tree.items()) {
|
||||||
std::string value = inputTree.get<std::string>(k);
|
if (v.is_null() || (v.is_string() && v.get<std::string>().empty())) {
|
||||||
if (value.length() == 0 || value.compare("null") == 0) {
|
|
||||||
continue;
|
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<std::string>() : v.dump()) << std::endl;
|
||||||
}
|
}
|
||||||
file_handler::write_file(config::sunshine.config_file.c_str(), configStream.str());
|
file_handler::write_file(config::sunshine.config_file.c_str(), config_stream.str());
|
||||||
outputTree.put("status", true);
|
output_tree["status"] = true;
|
||||||
send_response(response, outputTree);
|
send_response(response, output_tree);
|
||||||
} catch (std::exception &e) {
|
} catch (std::exception &e) {
|
||||||
BOOST_LOG(warning) << "SaveConfig: "sv << e.what();
|
BOOST_LOG(warning) << "SaveConfig: "sv << e.what();
|
||||||
bad_request(response, request, 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 response The HTTP response object.
|
||||||
* @param request The HTTP request 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_<game_id>",
|
||||||
|
* "url": "https://images.igdb.com/igdb/image/upload/t_cover_big_2x/<slug>.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)) {
|
if (!authenticate(response, request)) {
|
||||||
return;
|
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
|
std::string key = input_tree.value("key", "");
|
||||||
platf::restart();
|
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<int>(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 response The HTTP response object.
|
||||||
* @param request The HTTP request 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)) {
|
if (!authenticate(response, request)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
print_req(request);
|
print_req(request);
|
||||||
|
|
||||||
pt::ptree outputTree;
|
std::string content = file_handler::read_file(config::sunshine.log_file.c_str());
|
||||||
outputTree.put("status", display_device::reset_persistence());
|
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||||
send_response(response, outputTree);
|
headers.emplace("Content-Type", "text/plain");
|
||||||
|
response->write(SimpleWeb::StatusCode::success_ok, content, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -864,23 +946,21 @@ namespace confighttp {
|
|||||||
|
|
||||||
std::vector<std::string> errors = {};
|
std::vector<std::string> errors = {};
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
std::stringstream configStream;
|
std::stringstream config_stream;
|
||||||
ss << request->content.rdbuf();
|
ss << request->content.rdbuf();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: Input Validation
|
// TODO: Input Validation
|
||||||
pt::ptree inputTree;
|
nlohmann::json output_tree;
|
||||||
pt::ptree outputTree;
|
nlohmann::json input_tree = nlohmann::json::parse(ss);
|
||||||
pt::read_json(ss, inputTree);
|
std::string username = input_tree.value("currentUsername", "");
|
||||||
auto username = inputTree.count("currentUsername") > 0 ? inputTree.get<std::string>("currentUsername") : "";
|
std::string newUsername = input_tree.value("newUsername", "");
|
||||||
auto newUsername = inputTree.get<std::string>("newUsername");
|
std::string password = input_tree.value("currentPassword", "");
|
||||||
auto password = inputTree.count("currentPassword") > 0 ? inputTree.get<std::string>("currentPassword") : "";
|
std::string newPassword = input_tree.value("newPassword", "");
|
||||||
auto newPassword = inputTree.count("newPassword") > 0 ? inputTree.get<std::string>("newPassword") : "";
|
std::string confirmPassword = input_tree.value("confirmNewPassword", "");
|
||||||
auto confirmPassword = inputTree.count("confirmNewPassword") > 0 ? inputTree.get<std::string>("confirmNewPassword") : "";
|
if (newUsername.empty()) {
|
||||||
if (newUsername.length() == 0) {
|
|
||||||
newUsername = username;
|
newUsername = username;
|
||||||
}
|
}
|
||||||
if (newUsername.length() == 0) {
|
if (newUsername.empty()) {
|
||||||
errors.emplace_back("Invalid Username");
|
errors.emplace_back("Invalid Username");
|
||||||
} else {
|
} else {
|
||||||
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
|
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
|
||||||
@@ -890,7 +970,7 @@ namespace confighttp {
|
|||||||
} else {
|
} else {
|
||||||
http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword);
|
http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword);
|
||||||
http::reload_user_creds(config::sunshine.credentials_file);
|
http::reload_user_creds(config::sunshine.credentials_file);
|
||||||
outputTree.put("status", true);
|
output_tree["status"] = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
errors.emplace_back("Invalid Current Credentials");
|
errors.emplace_back("Invalid Current Credentials");
|
||||||
@@ -906,7 +986,7 @@ namespace confighttp {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
send_response(response, outputTree);
|
send_response(response, output_tree);
|
||||||
} catch (std::exception &e) {
|
} catch (std::exception &e) {
|
||||||
BOOST_LOG(warning) << "SavePassword: "sv << e.what();
|
BOOST_LOG(warning) << "SavePassword: "sv << e.what();
|
||||||
bad_request(response, request, e.what());
|
bad_request(response, request, e.what());
|
||||||
@@ -936,16 +1016,20 @@ namespace confighttp {
|
|||||||
|
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
ss << request->content.rdbuf();
|
ss << request->content.rdbuf();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: Input Validation
|
nlohmann::json output_tree;
|
||||||
pt::ptree inputTree;
|
nlohmann::json input_tree = nlohmann::json::parse(ss);
|
||||||
pt::ptree outputTree;
|
const std::string name = input_tree.value("name", "");
|
||||||
pt::read_json(ss, inputTree);
|
const std::string pin = input_tree.value("pin", "");
|
||||||
std::string pin = inputTree.get<std::string>("pin");
|
|
||||||
std::string name = inputTree.get<std::string>("name");
|
int _pin = 0;
|
||||||
outputTree.put("status", nvhttp::pin(pin, name));
|
_pin = std::stoi(pin);
|
||||||
send_response(response, outputTree);
|
if (_pin < 0 || _pin > 9999) {
|
||||||
|
bad_request(response, request, "PIN must be between 0000 and 9999");
|
||||||
|
}
|
||||||
|
|
||||||
|
output_tree["status"] = nvhttp::pin(pin, name);
|
||||||
|
send_response(response, output_tree);
|
||||||
} catch (std::exception &e) {
|
} catch (std::exception &e) {
|
||||||
BOOST_LOG(warning) << "SavePin: "sv << e.what();
|
BOOST_LOG(warning) << "SavePin: "sv << e.what();
|
||||||
bad_request(response, request, e.what());
|
bad_request(response, request, e.what());
|
||||||
@@ -953,106 +1037,40 @@ namespace confighttp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Unpair all clients.
|
* @brief Reset the display device persistence.
|
||||||
* @param response The HTTP response object.
|
* @param response The HTTP response object.
|
||||||
* @param request The HTTP request object.
|
* @param request The HTTP request object.
|
||||||
*
|
*
|
||||||
* @api_examples{/api/clients/unpair-all| POST| null}
|
* @api_examples{/api/reset-display-device-persistence| POST| null}
|
||||||
*/
|
*/
|
||||||
void unpairAll(resp_https_t response, req_https_t request) {
|
void resetDisplayDevicePersistence(resp_https_t response, req_https_t request) {
|
||||||
if (!authenticate(response, request)) {
|
if (!authenticate(response, request)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
print_req(request);
|
print_req(request);
|
||||||
|
|
||||||
nvhttp::erase_all_clients();
|
nlohmann::json output_tree;
|
||||||
proc::proc.terminate();
|
output_tree["status"] = display_device::reset_persistence();
|
||||||
|
send_response(response, output_tree);
|
||||||
pt::ptree outputTree;
|
|
||||||
outputTree.put("status", true);
|
|
||||||
send_response(response, outputTree);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Unpair a client.
|
* @brief Restart Sunshine.
|
||||||
* @param response The HTTP response object.
|
* @param response The HTTP response object.
|
||||||
* @param request The HTTP request object.
|
* @param request The HTTP request object.
|
||||||
* The body for the post request should be JSON serialized in the following format:
|
|
||||||
* @code{.json}
|
|
||||||
* {
|
|
||||||
* "uuid": "<uuid>"
|
|
||||||
* }
|
|
||||||
* @endcode
|
|
||||||
*
|
*
|
||||||
* @api_examples{/api/unpair| POST| {"uuid":"1234"}}
|
* @api_examples{/api/restart| POST| null}
|
||||||
*/
|
*/
|
||||||
void unpair(resp_https_t response, req_https_t request) {
|
void restart(resp_https_t response, req_https_t request) {
|
||||||
if (!authenticate(response, request)) {
|
if (!authenticate(response, request)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
print_req(request);
|
print_req(request);
|
||||||
|
|
||||||
std::stringstream ss;
|
// We may not return from this call
|
||||||
ss << request->content.rdbuf();
|
platf::restart();
|
||||||
|
|
||||||
try {
|
|
||||||
// TODO: Input Validation
|
|
||||||
pt::ptree inputTree;
|
|
||||||
pt::ptree outputTree;
|
|
||||||
pt::read_json(ss, inputTree);
|
|
||||||
std::string uuid = inputTree.get<std::string>("uuid");
|
|
||||||
outputTree.put("status", nvhttp::unpair_client(uuid));
|
|
||||||
send_response(response, outputTree);
|
|
||||||
} catch (std::exception &e) {
|
|
||||||
BOOST_LOG(warning) << "Unpair: "sv << e.what();
|
|
||||||
bad_request(response, request, e.what());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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 listClients(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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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();
|
|
||||||
|
|
||||||
pt::ptree outputTree;
|
|
||||||
outputTree.put("status", true);
|
|
||||||
send_response(response, outputTree);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void start() {
|
void start() {
|
||||||
@@ -1095,7 +1113,7 @@ namespace confighttp {
|
|||||||
server.resource["^/api/password$"]["POST"] = savePassword;
|
server.resource["^/api/password$"]["POST"] = savePassword;
|
||||||
server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp;
|
server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp;
|
||||||
server.resource["^/api/clients/unpair-all$"]["POST"] = unpairAll;
|
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/clients/unpair$"]["POST"] = unpair;
|
||||||
server.resource["^/api/apps/close$"]["POST"] = closeApp;
|
server.resource["^/api/apps/close$"]["POST"] = closeApp;
|
||||||
server.resource["^/api/covers/upload$"]["POST"] = uploadCover;
|
server.resource["^/api/covers/upload$"]["POST"] = uploadCover;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
// standard includes
|
// standard includes
|
||||||
#include <functional>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
// local includes
|
// local includes
|
||||||
|
|||||||
@@ -765,14 +765,14 @@ namespace nvhttp {
|
|||||||
response->close_connection_after_response = true;
|
response->close_connection_after_response = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pt::ptree get_all_clients() {
|
nlohmann::json get_all_clients() {
|
||||||
pt::ptree named_cert_nodes;
|
nlohmann::json named_cert_nodes = nlohmann::json::array();
|
||||||
client_t &client = client_root;
|
client_t &client = client_root;
|
||||||
for (auto &named_cert : client.named_devices) {
|
for (auto &named_cert : client.named_devices) {
|
||||||
pt::ptree named_cert_node;
|
nlohmann::json named_cert_node;
|
||||||
named_cert_node.put("name"s, named_cert.name);
|
named_cert_node["name"] = named_cert.name;
|
||||||
named_cert_node.put("uuid"s, named_cert.uuid);
|
named_cert_node["uuid"] = named_cert.uuid;
|
||||||
named_cert_nodes.push_back(std::make_pair(""s, named_cert_node));
|
named_cert_nodes.push_back(named_cert_node);
|
||||||
}
|
}
|
||||||
|
|
||||||
return named_cert_nodes;
|
return named_cert_nodes;
|
||||||
@@ -1177,13 +1177,13 @@ namespace nvhttp {
|
|||||||
save_state();
|
save_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
int unpair_client(std::string uuid) {
|
bool unpair_client(const std::string_view uuid) {
|
||||||
int removed = 0;
|
bool removed = false;
|
||||||
client_t &client = client_root;
|
client_t &client = client_root;
|
||||||
for (auto it = client.named_devices.begin(); it != client.named_devices.end();) {
|
for (auto it = client.named_devices.begin(); it != client.named_devices.end();) {
|
||||||
if ((*it).uuid == uuid) {
|
if ((*it).uuid == uuid) {
|
||||||
it = client.named_devices.erase(it);
|
it = client.named_devices.erase(it);
|
||||||
removed++;
|
removed = true;
|
||||||
} else {
|
} else {
|
||||||
++it;
|
++it;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
// lib includes
|
// lib includes
|
||||||
#include <boost/property_tree/ptree.hpp>
|
#include <boost/property_tree/ptree.hpp>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
#include <Simple-Web-Server/server_https.hpp>
|
#include <Simple-Web-Server/server_https.hpp>
|
||||||
|
|
||||||
// local includes
|
// local includes
|
||||||
@@ -176,20 +177,21 @@ namespace nvhttp {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Remove single client.
|
* @brief Remove single client.
|
||||||
|
* @param uuid The UUID of the client to remove.
|
||||||
* @examples
|
* @examples
|
||||||
* nvhttp::unpair_client("4D7BB2DD-5704-A405-B41C-891A022932E1");
|
* nvhttp::unpair_client("4D7BB2DD-5704-A405-B41C-891A022932E1");
|
||||||
* @examples_end
|
* @examples_end
|
||||||
*/
|
*/
|
||||||
int unpair_client(std::string uniqueid);
|
bool unpair_client(std::string_view uuid);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get all paired clients.
|
* @brief Get all paired clients.
|
||||||
* @return The list of all paired clients.
|
* @return The list of all paired clients.
|
||||||
* @examples
|
* @examples
|
||||||
* boost::property_tree::ptree clients = nvhttp::get_all_clients();
|
* nlohmann::json clients = nvhttp::get_all_clients();
|
||||||
* @examples_end
|
* @examples_end
|
||||||
*/
|
*/
|
||||||
boost::property_tree::ptree get_all_clients();
|
nlohmann::json get_all_clients();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Remove all paired clients.
|
* @brief Remove all paired clients.
|
||||||
|
|||||||
@@ -234,8 +234,8 @@
|
|||||||
<!-- exit timeout -->
|
<!-- exit timeout -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="exitTimeout" class="form-label">{{ $t('apps.exit_timeout') }}</label>
|
<label for="exitTimeout" class="form-label">{{ $t('apps.exit_timeout') }}</label>
|
||||||
<input type="text" class="form-control monospace" id="exitTimeout" aria-describedby="exitTimeoutHelp"
|
<input type="number" class="form-control monospace" id="exitTimeout" aria-describedby="exitTimeoutHelp"
|
||||||
v-model="editForm['exit-timeout']" />
|
v-model="editForm['exit-timeout']" min="0" placeholder="5" />
|
||||||
<div id="exitTimeoutHelp" class="form-text">{{ $t('apps.exit_timeout_desc') }}</div>
|
<div id="exitTimeoutHelp" class="form-text">{{ $t('apps.exit_timeout_desc') }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
|||||||
@@ -343,10 +343,7 @@
|
|||||||
this.$forceUpdate()
|
this.$forceUpdate()
|
||||||
},
|
},
|
||||||
serialize() {
|
serialize() {
|
||||||
let config = JSON.parse(JSON.stringify(this.config));
|
return 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;
|
|
||||||
},
|
},
|
||||||
save() {
|
save() {
|
||||||
this.saved = false;
|
this.saved = false;
|
||||||
@@ -360,17 +357,8 @@
|
|||||||
Object.keys(tab.options).forEach(optionKey => {
|
Object.keys(tab.options).forEach(optionKey => {
|
||||||
let delete_value = false
|
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
|
// 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
|
delete_value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,14 +39,12 @@ const config = ref(props.config)
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Spatial AQ -->
|
<!-- Spatial AQ -->
|
||||||
<div class="mb-3">
|
<Checkbox class="mb-3"
|
||||||
<label for="nvenc_spatial_aq" class="form-label">{{ $t('config.nvenc_spatial_aq') }}</label>
|
id="nvenc_spatial_aq"
|
||||||
<select id="nvenc_spatial_aq" class="form-select" v-model="config.nvenc_spatial_aq">
|
locale-prefix="config"
|
||||||
<option value="disabled">{{ $t('config.nvenc_spatial_aq_disabled') }}</option>
|
v-model="config.nvenc_spatial_aq"
|
||||||
<option value="enabled">{{ $t('config.nvenc_spatial_aq_enabled') }}</option>
|
default="false"
|
||||||
</select>
|
></Checkbox>
|
||||||
<div class="form-text">{{ $t('config.nvenc_spatial_aq_desc') }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Single-frame VBV/HRD percentage increase -->
|
<!-- Single-frame VBV/HRD percentage increase -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
"env_vars_desc": "All commands get these environment variables by default:",
|
"env_vars_desc": "All commands get these environment variables by default:",
|
||||||
"env_xrandr_example": "Example - Xrandr for Resolution Automation:",
|
"env_xrandr_example": "Example - Xrandr for Resolution Automation:",
|
||||||
"exit_timeout": "Exit Timeout",
|
"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",
|
"find_cover": "Find Cover",
|
||||||
"global_prep_desc": "Enable/Disable the execution of Global Prep Commands for this application.",
|
"global_prep_desc": "Enable/Disable the execution of Global Prep Commands for this application.",
|
||||||
"global_prep_name": "Global Prep Commands",
|
"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_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": "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_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": "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_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)",
|
"nvenc_twopass_disabled": "Disabled (fastest, not recommended)",
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
{
|
{
|
||||||
"name": "Steam Big Picture",
|
"name": "Steam Big Picture",
|
||||||
"cmd": "steam://open/bigpicture",
|
"cmd": "steam://open/bigpicture",
|
||||||
"auto-detach": "true",
|
"auto-detach": true,
|
||||||
"wait-all": "true",
|
"wait-all": true,
|
||||||
"image-path": "steam.png"
|
"image-path": "steam.png"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user