Introduces backend API endpoints for ViGEmBus status and installation, updates Windows build scripts to handle ViGEmBus versioning and installer download, and integrates ViGEmBus status and installation controls into the web UI. Removes legacy PowerShell scripts for gamepad driver management and related NSIS installer commands.
1387 lines
46 KiB
C++
1387 lines
46 KiB
C++
/**
|
|
* @file src/confighttp.cpp
|
|
* @brief Definitions for the Web UI Config HTTP server.
|
|
*
|
|
* @todo Authentication, better handling of routes common to nvhttp, cleanup
|
|
*/
|
|
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
|
|
|
|
// standard includes
|
|
#include <filesystem>
|
|
#include <format>
|
|
#include <fstream>
|
|
#include <set>
|
|
|
|
// lib includes
|
|
#include <boost/algorithm/string.hpp>
|
|
#include <boost/asio/ssl/context.hpp>
|
|
#include <boost/filesystem.hpp>
|
|
#include <nlohmann/json.hpp>
|
|
#include <Simple-Web-Server/crypto.hpp>
|
|
#include <Simple-Web-Server/server_https.hpp>
|
|
|
|
#ifdef _WIN32
|
|
#include "platform/windows/misc.h"
|
|
|
|
#include <vector>
|
|
#include <Windows.h>
|
|
#endif
|
|
|
|
// local includes
|
|
#include "config.h"
|
|
#include "confighttp.h"
|
|
#include "crypto.h"
|
|
#include "display_device.h"
|
|
#include "file_handler.h"
|
|
#include "globals.h"
|
|
#include "httpcommon.h"
|
|
#include "logging.h"
|
|
#include "network.h"
|
|
#include "nvhttp.h"
|
|
#include "platform/common.h"
|
|
#include "process.h"
|
|
#include "utility.h"
|
|
#include "uuid.h"
|
|
|
|
using namespace std::literals;
|
|
|
|
namespace confighttp {
|
|
namespace fs = std::filesystem;
|
|
|
|
using https_server_t = SimpleWeb::Server<SimpleWeb::HTTPS>;
|
|
|
|
using args_t = SimpleWeb::CaseInsensitiveMultimap;
|
|
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>;
|
|
using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Request>;
|
|
|
|
enum class op_e {
|
|
ADD, ///< Add client
|
|
REMOVE ///< Remove client
|
|
};
|
|
|
|
/**
|
|
* @brief Log the request details.
|
|
* @param request The HTTP request object.
|
|
*/
|
|
void print_req(const req_https_t &request) {
|
|
BOOST_LOG(debug) << "METHOD :: "sv << request->method;
|
|
BOOST_LOG(debug) << "DESTINATION :: "sv << request->path;
|
|
|
|
for (auto &[name, val] : request->header) {
|
|
BOOST_LOG(debug) << name << " -- " << (name == "Authorization" ? "CREDENTIALS REDACTED" : val);
|
|
}
|
|
|
|
BOOST_LOG(debug) << " [--] "sv;
|
|
|
|
for (auto &[name, val] : request->parse_query_string()) {
|
|
BOOST_LOG(debug) << name << " -- " << val;
|
|
}
|
|
|
|
BOOST_LOG(debug) << " [--] "sv;
|
|
}
|
|
|
|
/**
|
|
* @brief Send a response.
|
|
* @param response The HTTP response object.
|
|
* @param output_tree The JSON tree to send.
|
|
*/
|
|
void send_response(resp_https_t response, const nlohmann::json &output_tree) {
|
|
SimpleWeb::CaseInsensitiveMultimap headers;
|
|
headers.emplace("Content-Type", "application/json");
|
|
headers.emplace("X-Frame-Options", "DENY");
|
|
headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
|
|
response->write(output_tree.dump(), headers);
|
|
}
|
|
|
|
/**
|
|
* @brief Send a 401 Unauthorized response.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
*/
|
|
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")"},
|
|
{"X-Frame-Options", "DENY"},
|
|
{"Content-Security-Policy", "frame-ancestors 'none';"}
|
|
};
|
|
|
|
response->write(code, tree.dump(), headers);
|
|
}
|
|
|
|
/**
|
|
* @brief Send a redirect response.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
* @param path The path to redirect to.
|
|
*/
|
|
void send_redirect(resp_https_t response, req_https_t request, const char *path) {
|
|
auto address = net::addr_to_normalized_string(request->remote_endpoint().address());
|
|
BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv;
|
|
const SimpleWeb::CaseInsensitiveMultimap headers {
|
|
{"Location", path},
|
|
{"X-Frame-Options", "DENY"},
|
|
{"Content-Security-Policy", "frame-ancestors 'none';"}
|
|
};
|
|
response->write(SimpleWeb::StatusCode::redirection_temporary_redirect, headers);
|
|
}
|
|
|
|
/**
|
|
* @brief Authenticate the user.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
* @return True if the user is authenticated, false otherwise.
|
|
*/
|
|
bool authenticate(resp_https_t response, req_https_t request) {
|
|
auto address = net::addr_to_normalized_string(request->remote_endpoint().address());
|
|
auto ip_type = net::from_address(address);
|
|
|
|
if (ip_type > http::origin_web_ui_allowed) {
|
|
BOOST_LOG(info) << "Web UI: ["sv << address << "] -- denied"sv;
|
|
response->write(SimpleWeb::StatusCode::client_error_forbidden);
|
|
return false;
|
|
}
|
|
|
|
// If credentials are shown, redirect the user to a /welcome page
|
|
if (config::sunshine.username.empty()) {
|
|
send_redirect(response, request, "/welcome");
|
|
return false;
|
|
}
|
|
|
|
auto fg = util::fail_guard([&]() {
|
|
send_unauthorized(response, request);
|
|
});
|
|
|
|
auto auth = request->header.find("authorization");
|
|
if (auth == request->header.end()) {
|
|
return false;
|
|
}
|
|
|
|
auto &rawAuth = auth->second;
|
|
auto authData = SimpleWeb::Crypto::Base64::decode(rawAuth.substr("Basic "sv.length()));
|
|
|
|
auto index = (int) authData.find(':');
|
|
if (index >= authData.size() - 1) {
|
|
return false;
|
|
}
|
|
|
|
auto username = authData.substr(0, index);
|
|
auto password = authData.substr(index + 1);
|
|
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
|
|
|
|
if (!boost::iequals(username, config::sunshine.username) || hash != config::sunshine.password) {
|
|
return false;
|
|
}
|
|
|
|
fg.disable();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Send a 404 Not Found response.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
*/
|
|
void not_found(resp_https_t response, [[maybe_unused]] req_https_t request) {
|
|
constexpr SimpleWeb::StatusCode code = SimpleWeb::StatusCode::client_error_not_found;
|
|
|
|
nlohmann::json tree;
|
|
tree["status_code"] = code;
|
|
tree["error"] = "Not Found";
|
|
|
|
SimpleWeb::CaseInsensitiveMultimap headers;
|
|
headers.emplace("Content-Type", "application/json");
|
|
headers.emplace("X-Frame-Options", "DENY");
|
|
headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
|
|
|
|
response->write(code, tree.dump(), headers);
|
|
}
|
|
|
|
/**
|
|
* @brief Send a 400 Bad Request response.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
* @param error_message The error message to include in the response.
|
|
*/
|
|
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;
|
|
|
|
nlohmann::json tree;
|
|
tree["status_code"] = code;
|
|
tree["status"] = false;
|
|
tree["error"] = error_message;
|
|
|
|
SimpleWeb::CaseInsensitiveMultimap headers;
|
|
headers.emplace("Content-Type", "application/json");
|
|
headers.emplace("X-Frame-Options", "DENY");
|
|
headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
|
|
|
|
response->write(code, tree.dump(), headers);
|
|
}
|
|
|
|
/**
|
|
* @brief Validate the request content type and send bad request when mismatch.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
* @param contentType The expected content type
|
|
*/
|
|
bool check_content_type(resp_https_t response, req_https_t request, const std::string_view &contentType) {
|
|
auto requestContentType = request->header.find("content-type");
|
|
if (requestContentType == request->header.end()) {
|
|
bad_request(response, request, "Content type not provided");
|
|
return false;
|
|
}
|
|
// Extract the media type part before any parameters (e.g., charset)
|
|
std::string actualContentType = requestContentType->second;
|
|
size_t semicolonPos = actualContentType.find(';');
|
|
if (semicolonPos != std::string::npos) {
|
|
actualContentType = actualContentType.substr(0, semicolonPos);
|
|
}
|
|
|
|
// Trim whitespace and convert to lowercase for case-insensitive comparison
|
|
boost::algorithm::trim(actualContentType);
|
|
boost::algorithm::to_lower(actualContentType);
|
|
|
|
std::string expectedContentType(contentType);
|
|
boost::algorithm::to_lower(expectedContentType);
|
|
|
|
if (actualContentType != expectedContentType) {
|
|
bad_request(response, request, "Content type mismatch");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Get the index page.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
* @todo combine these functions into a single function that accepts the page, i.e "index", "pin", "apps"
|
|
*/
|
|
void getIndexPage(resp_https_t response, req_https_t request) {
|
|
if (!authenticate(response, request)) {
|
|
return;
|
|
}
|
|
|
|
print_req(request);
|
|
|
|
std::string content = file_handler::read_file(WEB_DIR "index.html");
|
|
SimpleWeb::CaseInsensitiveMultimap headers;
|
|
headers.emplace("Content-Type", "text/html; charset=utf-8");
|
|
headers.emplace("X-Frame-Options", "DENY");
|
|
headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
|
|
response->write(content, headers);
|
|
}
|
|
|
|
/**
|
|
* @brief Get the PIN page.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
*/
|
|
void getPinPage(resp_https_t response, req_https_t request) {
|
|
if (!authenticate(response, request)) {
|
|
return;
|
|
}
|
|
|
|
print_req(request);
|
|
|
|
std::string content = file_handler::read_file(WEB_DIR "pin.html");
|
|
SimpleWeb::CaseInsensitiveMultimap headers;
|
|
headers.emplace("Content-Type", "text/html; charset=utf-8");
|
|
headers.emplace("X-Frame-Options", "DENY");
|
|
headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
|
|
response->write(content, headers);
|
|
}
|
|
|
|
/**
|
|
* @brief Get the apps page.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
*/
|
|
void getAppsPage(resp_https_t response, req_https_t request) {
|
|
if (!authenticate(response, request)) {
|
|
return;
|
|
}
|
|
|
|
print_req(request);
|
|
|
|
std::string content = file_handler::read_file(WEB_DIR "apps.html");
|
|
SimpleWeb::CaseInsensitiveMultimap headers;
|
|
headers.emplace("Content-Type", "text/html; charset=utf-8");
|
|
headers.emplace("X-Frame-Options", "DENY");
|
|
headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
|
|
headers.emplace("Access-Control-Allow-Origin", "https://images.igdb.com/");
|
|
response->write(content, headers);
|
|
}
|
|
|
|
/**
|
|
* @brief Get the clients page.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
*/
|
|
void getClientsPage(resp_https_t response, req_https_t request) {
|
|
if (!authenticate(response, request)) {
|
|
return;
|
|
}
|
|
|
|
print_req(request);
|
|
|
|
std::string content = file_handler::read_file(WEB_DIR "clients.html");
|
|
SimpleWeb::CaseInsensitiveMultimap headers;
|
|
headers.emplace("Content-Type", "text/html; charset=utf-8");
|
|
headers.emplace("X-Frame-Options", "DENY");
|
|
headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
|
|
response->write(content, headers);
|
|
}
|
|
|
|
/**
|
|
* @brief Get the configuration page.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
*/
|
|
void getConfigPage(resp_https_t response, req_https_t request) {
|
|
if (!authenticate(response, request)) {
|
|
return;
|
|
}
|
|
|
|
print_req(request);
|
|
|
|
std::string content = file_handler::read_file(WEB_DIR "config.html");
|
|
SimpleWeb::CaseInsensitiveMultimap headers;
|
|
headers.emplace("Content-Type", "text/html; charset=utf-8");
|
|
headers.emplace("X-Frame-Options", "DENY");
|
|
headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
|
|
response->write(content, headers);
|
|
}
|
|
|
|
/**
|
|
* @brief Get the password page.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
*/
|
|
void getPasswordPage(resp_https_t response, req_https_t request) {
|
|
if (!authenticate(response, request)) {
|
|
return;
|
|
}
|
|
|
|
print_req(request);
|
|
|
|
std::string content = file_handler::read_file(WEB_DIR "password.html");
|
|
SimpleWeb::CaseInsensitiveMultimap headers;
|
|
headers.emplace("Content-Type", "text/html; charset=utf-8");
|
|
headers.emplace("X-Frame-Options", "DENY");
|
|
headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
|
|
response->write(content, headers);
|
|
}
|
|
|
|
/**
|
|
* @brief Get the welcome page.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
*/
|
|
void getWelcomePage(resp_https_t response, req_https_t request) {
|
|
print_req(request);
|
|
if (!config::sunshine.username.empty()) {
|
|
send_redirect(response, request, "/");
|
|
return;
|
|
}
|
|
std::string content = file_handler::read_file(WEB_DIR "welcome.html");
|
|
SimpleWeb::CaseInsensitiveMultimap headers;
|
|
headers.emplace("Content-Type", "text/html; charset=utf-8");
|
|
headers.emplace("X-Frame-Options", "DENY");
|
|
headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
|
|
response->write(content, headers);
|
|
}
|
|
|
|
/**
|
|
* @brief Get the troubleshooting page.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
*/
|
|
void getTroubleshootingPage(resp_https_t response, req_https_t request) {
|
|
if (!authenticate(response, request)) {
|
|
return;
|
|
}
|
|
|
|
print_req(request);
|
|
|
|
std::string content = file_handler::read_file(WEB_DIR "troubleshooting.html");
|
|
SimpleWeb::CaseInsensitiveMultimap headers;
|
|
headers.emplace("Content-Type", "text/html; charset=utf-8");
|
|
headers.emplace("X-Frame-Options", "DENY");
|
|
headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
|
|
response->write(content, headers);
|
|
}
|
|
|
|
/**
|
|
* @brief Get the favicon image.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
* @todo combine function with getSunshineLogoImage and possibly getNodeModules
|
|
* @todo use mime_types map
|
|
*/
|
|
void getFaviconImage(resp_https_t response, req_https_t request) {
|
|
print_req(request);
|
|
|
|
std::ifstream in(WEB_DIR "images/sunshine.ico", std::ios::binary);
|
|
SimpleWeb::CaseInsensitiveMultimap headers;
|
|
headers.emplace("Content-Type", "image/x-icon");
|
|
headers.emplace("X-Frame-Options", "DENY");
|
|
headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
|
|
response->write(SimpleWeb::StatusCode::success_ok, in, headers);
|
|
}
|
|
|
|
/**
|
|
* @brief Get the Sunshine logo image.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
* @todo combine function with getFaviconImage and possibly getNodeModules
|
|
* @todo use mime_types map
|
|
*/
|
|
void getSunshineLogoImage(resp_https_t response, req_https_t request) {
|
|
print_req(request);
|
|
|
|
std::ifstream in(WEB_DIR "images/logo-sunshine-45.png", std::ios::binary);
|
|
SimpleWeb::CaseInsensitiveMultimap headers;
|
|
headers.emplace("Content-Type", "image/png");
|
|
headers.emplace("X-Frame-Options", "DENY");
|
|
headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
|
|
response->write(SimpleWeb::StatusCode::success_ok, in, headers);
|
|
}
|
|
|
|
/**
|
|
* @brief Check if a path is a child of another path.
|
|
* @param base The base path.
|
|
* @param query The path to check.
|
|
* @return True if the path is a child of the base path, false otherwise.
|
|
*/
|
|
bool isChildPath(fs::path const &base, fs::path const &query) {
|
|
auto relPath = fs::relative(base, query);
|
|
return *(relPath.begin()) != fs::path("..");
|
|
}
|
|
|
|
/**
|
|
* @brief Get an asset from the node_modules directory.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
*/
|
|
void getNodeModules(resp_https_t response, req_https_t request) {
|
|
print_req(request);
|
|
fs::path webDirPath(WEB_DIR);
|
|
fs::path nodeModulesPath(webDirPath / "assets");
|
|
|
|
// .relative_path is needed to shed any leading slash that might exist in the request path
|
|
auto filePath = fs::weakly_canonical(webDirPath / fs::path(request->path).relative_path());
|
|
|
|
// Don't do anything if file does not exist or is outside the assets directory
|
|
if (!isChildPath(filePath, nodeModulesPath)) {
|
|
BOOST_LOG(warning) << "Someone requested a path " << filePath << " that is outside the assets folder";
|
|
bad_request(response, request);
|
|
return;
|
|
}
|
|
if (!fs::exists(filePath)) {
|
|
not_found(response, request);
|
|
return;
|
|
}
|
|
|
|
auto relPath = fs::relative(filePath, webDirPath);
|
|
// get the mime type from the file extension mime_types map
|
|
// remove the leading period from the extension
|
|
auto mimeType = mime_types.find(relPath.extension().string().substr(1));
|
|
// check if the extension is in the map at the x position
|
|
if (mimeType == mime_types.end()) {
|
|
bad_request(response, request);
|
|
return;
|
|
}
|
|
|
|
// if it is, set the content type to the mime type
|
|
SimpleWeb::CaseInsensitiveMultimap headers;
|
|
headers.emplace("Content-Type", mimeType->second);
|
|
headers.emplace("X-Frame-Options", "DENY");
|
|
headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
|
|
std::ifstream in(filePath.string(), std::ios::binary);
|
|
response->write(SimpleWeb::StatusCode::success_ok, in, headers);
|
|
}
|
|
|
|
/**
|
|
* @brief Get the list of available applications.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
*
|
|
* @api_examples{/api/apps| GET| null}
|
|
*/
|
|
void getApps(resp_https_t response, req_https_t request) {
|
|
if (!authenticate(response, request)) {
|
|
return;
|
|
}
|
|
|
|
print_req(request);
|
|
|
|
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<std::string> boolean_keys = {
|
|
"exclude-global-prep-cmd",
|
|
"elevated",
|
|
"auto-detach",
|
|
"wait-all"
|
|
};
|
|
|
|
// 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());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Save an application. To save a new application the index must be `-1`. To update an existing application, you must provide the current index of the application.
|
|
* @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}
|
|
* {
|
|
* "name": "Application Name",
|
|
* "output": "Log Output Path",
|
|
* "cmd": "Command to run the application",
|
|
* "index": -1,
|
|
* "exclude-global-prep-cmd": false,
|
|
* "elevated": false,
|
|
* "auto-detach": true,
|
|
* "wait-all": true,
|
|
* "exit-timeout": 5,
|
|
* "prep-cmd": [
|
|
* {
|
|
* "do": "Command to prepare",
|
|
* "undo": "Command to undo preparation",
|
|
* "elevated": false
|
|
* }
|
|
* ],
|
|
* "detached": [
|
|
* "Detached command"
|
|
* ],
|
|
* "image-path": "Full path to the application image. Must be a png file."
|
|
* }
|
|
* @endcode
|
|
*
|
|
* @api_examples{/api/apps| POST| {"name":"Hello, World!","index":-1}}
|
|
*/
|
|
void saveApp(resp_https_t response, req_https_t request) {
|
|
if (!check_content_type(response, request, "application/json")) {
|
|
return;
|
|
}
|
|
if (!authenticate(response, request)) {
|
|
return;
|
|
}
|
|
|
|
print_req(request);
|
|
|
|
std::stringstream ss;
|
|
ss << request->content.rdbuf();
|
|
try {
|
|
// TODO: Input Validation
|
|
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 (input_tree["detached"].empty()) {
|
|
input_tree.erase("detached");
|
|
}
|
|
|
|
auto &apps_node = file_tree["apps"];
|
|
int index = input_tree["index"].get<int>(); // this will intentionally cause exception if the provided value is the wrong type
|
|
|
|
input_tree.erase("index");
|
|
|
|
if (index == -1) {
|
|
apps_node.push_back(input_tree);
|
|
} else {
|
|
nlohmann::json newApps = nlohmann::json::array();
|
|
for (size_t i = 0; i < apps_node.size(); ++i) {
|
|
if (i == index) {
|
|
newApps.push_back(input_tree);
|
|
} else {
|
|
newApps.push_back(apps_node[i]);
|
|
}
|
|
}
|
|
file_tree["apps"] = newApps;
|
|
}
|
|
|
|
// Sort the apps array by name
|
|
std::sort(apps_node.begin(), apps_node.end(), [](const nlohmann::json &a, const nlohmann::json &b) {
|
|
return a["name"].get<std::string>() < b["name"].get<std::string>();
|
|
});
|
|
|
|
file_handler::write_file(config::stream.file_apps.c_str(), file_tree.dump(4));
|
|
proc::refresh(config::stream.file_apps);
|
|
|
|
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 (!check_content_type(response, request, "application/json")) {
|
|
return;
|
|
}
|
|
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.
|
|
* @param request The HTTP request object.
|
|
*
|
|
* @api_examples{/api/apps/9999| DELETE| null}
|
|
*/
|
|
void deleteApp(resp_https_t response, req_https_t request) {
|
|
// Skip check_content_type() for this endpoint since the request body is not used.
|
|
|
|
if (!authenticate(response, request)) {
|
|
return;
|
|
}
|
|
|
|
print_req(request);
|
|
|
|
try {
|
|
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<int>(apps_node.size())) {
|
|
std::string error;
|
|
if (const int max_index = static_cast<int>(apps_node.size()) - 1; max_index < 0) {
|
|
error = "No applications to delete";
|
|
} else {
|
|
error = std::format("'index' {} out of range, max index is {}", index, max_index);
|
|
}
|
|
bad_request(response, request, error);
|
|
return;
|
|
}
|
|
|
|
for (size_t i = 0; i < apps_node.size(); ++i) {
|
|
if (i != index) {
|
|
new_apps.push_back(apps_node[i]);
|
|
}
|
|
}
|
|
file_tree["apps"] = new_apps;
|
|
|
|
file_handler::write_file(config::stream.file_apps.c_str(), file_tree.dump(4));
|
|
proc::refresh(config::stream.file_apps);
|
|
|
|
output_tree["status"] = true;
|
|
output_tree["result"] = std::format("application {} deleted", index);
|
|
send_response(response, output_tree);
|
|
} catch (std::exception &e) {
|
|
BOOST_LOG(warning) << "DeleteApp: "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 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}
|
|
* {
|
|
* "uuid": "<uuid>"
|
|
* }
|
|
* @endcode
|
|
*
|
|
* @api_examples{/api/unpair| POST| {"uuid":"1234"}}
|
|
*/
|
|
void unpair(resp_https_t response, req_https_t request) {
|
|
if (!check_content_type(response, request, "application/json")) {
|
|
return;
|
|
}
|
|
if (!authenticate(response, request)) {
|
|
return;
|
|
}
|
|
|
|
print_req(request);
|
|
|
|
std::stringstream ss;
|
|
ss << request->content.rdbuf();
|
|
|
|
try {
|
|
// 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) << "Unpair: "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 (!check_content_type(response, request, "application/json")) {
|
|
return;
|
|
}
|
|
if (!authenticate(response, request)) {
|
|
return;
|
|
}
|
|
|
|
print_req(request);
|
|
|
|
nvhttp::erase_all_clients();
|
|
proc::proc.terminate();
|
|
|
|
nlohmann::json output_tree;
|
|
output_tree["status"] = true;
|
|
send_response(response, output_tree);
|
|
}
|
|
|
|
/**
|
|
* @brief Get the configuration settings.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
*
|
|
* @api_examples{/api/config| GET| null}
|
|
*/
|
|
void getConfig(resp_https_t response, req_https_t request) {
|
|
if (!authenticate(response, request)) {
|
|
return;
|
|
}
|
|
|
|
print_req(request);
|
|
|
|
nlohmann::json output_tree;
|
|
output_tree["status"] = true;
|
|
output_tree["platform"] = SUNSHINE_PLATFORM;
|
|
output_tree["version"] = PROJECT_VERSION;
|
|
|
|
auto vars = config::parse_config(file_handler::read_file(config::sunshine.config_file.c_str()));
|
|
|
|
for (auto &[name, value] : vars) {
|
|
output_tree[name] = std::move(value);
|
|
}
|
|
|
|
send_response(response, output_tree);
|
|
}
|
|
|
|
/**
|
|
* @brief Get the locale setting. This endpoint does not require authentication.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
*
|
|
* @api_examples{/api/configLocale| GET| null}
|
|
*/
|
|
void getLocale(resp_https_t response, req_https_t request) {
|
|
// we need to return the locale whether authenticated or not
|
|
|
|
print_req(request);
|
|
|
|
nlohmann::json output_tree;
|
|
output_tree["status"] = true;
|
|
output_tree["locale"] = config::sunshine.locale;
|
|
send_response(response, output_tree);
|
|
}
|
|
|
|
/**
|
|
* @brief Save the configuration settings.
|
|
* @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": "value"
|
|
* }
|
|
* @endcode
|
|
*
|
|
* @attention{It is recommended to ONLY save the config settings that differ from the default behavior.}
|
|
*
|
|
* @api_examples{/api/config| POST| {"key":"value"}}
|
|
*/
|
|
void saveConfig(resp_https_t response, req_https_t request) {
|
|
if (!check_content_type(response, request, "application/json")) {
|
|
return;
|
|
}
|
|
if (!authenticate(response, request)) {
|
|
return;
|
|
}
|
|
|
|
print_req(request);
|
|
|
|
std::stringstream ss;
|
|
ss << request->content.rdbuf();
|
|
try {
|
|
// TODO: Input Validation
|
|
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<std::string>().empty())) {
|
|
continue;
|
|
}
|
|
|
|
// 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(), 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());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @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_<game_id>",
|
|
* "url": "https://images.igdb.com/igdb/image/upload/t_cover_big_2x/<slug>.png"
|
|
* }
|
|
* @endcode
|
|
*
|
|
* @api_examples{/api/covers/upload| POST| {"key":"igdb_1234","url":"https://images.igdb.com/igdb/image/upload/t_cover_big_2x/abc123.png"}}
|
|
*/
|
|
void uploadCover(resp_https_t response, req_https_t request) {
|
|
if (!check_content_type(response, request, "application/json")) {
|
|
return;
|
|
}
|
|
if (!authenticate(response, request)) {
|
|
return;
|
|
}
|
|
|
|
std::stringstream ss;
|
|
ss << request->content.rdbuf();
|
|
try {
|
|
nlohmann::json output_tree;
|
|
nlohmann::json input_tree = nlohmann::json::parse(ss);
|
|
|
|
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<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 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;
|
|
}
|
|
|
|
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");
|
|
headers.emplace("X-Frame-Options", "DENY");
|
|
headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
|
|
response->write(SimpleWeb::StatusCode::success_ok, content, headers);
|
|
}
|
|
|
|
/**
|
|
* @brief Update existing credentials.
|
|
* @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}
|
|
* {
|
|
* "currentUsername": "Current Username",
|
|
* "currentPassword": "Current Password",
|
|
* "newUsername": "New Username",
|
|
* "newPassword": "New Password",
|
|
* "confirmNewPassword": "Confirm New Password"
|
|
* }
|
|
* @endcode
|
|
*
|
|
* @api_examples{/api/password| POST| {"currentUsername":"admin","currentPassword":"admin","newUsername":"admin","newPassword":"admin","confirmNewPassword":"admin"}}
|
|
*/
|
|
void savePassword(resp_https_t response, req_https_t request) {
|
|
if (!check_content_type(response, request, "application/json")) {
|
|
return;
|
|
}
|
|
if (!config::sunshine.username.empty() && !authenticate(response, request)) {
|
|
return;
|
|
}
|
|
|
|
print_req(request);
|
|
|
|
std::vector<std::string> errors = {};
|
|
std::stringstream ss;
|
|
std::stringstream config_stream;
|
|
ss << request->content.rdbuf();
|
|
try {
|
|
// TODO: Input Validation
|
|
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.empty()) {
|
|
errors.emplace_back("Invalid Username");
|
|
} else {
|
|
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
|
|
if (config::sunshine.username.empty() || (boost::iequals(username, config::sunshine.username) && hash == config::sunshine.password)) {
|
|
if (newPassword.empty() || newPassword != confirmPassword) {
|
|
errors.emplace_back("Password Mismatch");
|
|
} else {
|
|
http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword);
|
|
http::reload_user_creds(config::sunshine.credentials_file);
|
|
output_tree["status"] = true;
|
|
}
|
|
} else {
|
|
errors.emplace_back("Invalid Current Credentials");
|
|
}
|
|
}
|
|
|
|
if (!errors.empty()) {
|
|
// join the errors array
|
|
std::string error = std::accumulate(errors.begin(), errors.end(), std::string(), [](const std::string &a, const std::string &b) {
|
|
return a.empty() ? b : a + ", " + b;
|
|
});
|
|
bad_request(response, request, error);
|
|
return;
|
|
}
|
|
|
|
send_response(response, output_tree);
|
|
} catch (std::exception &e) {
|
|
BOOST_LOG(warning) << "SavePassword: "sv << e.what();
|
|
bad_request(response, request, e.what());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Send a pin code to the host. The pin is generated from the Moonlight client during the pairing process.
|
|
* @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}
|
|
* {
|
|
* "pin": "<pin>",
|
|
* "name": "Friendly Client Name"
|
|
* }
|
|
* @endcode
|
|
*
|
|
* @api_examples{/api/pin| POST| {"pin":"1234","name":"My PC"}}
|
|
*/
|
|
void savePin(resp_https_t response, req_https_t request) {
|
|
if (!check_content_type(response, request, "application/json")) {
|
|
return;
|
|
}
|
|
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);
|
|
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");
|
|
}
|
|
|
|
output_tree["status"] = nvhttp::pin(pin, name);
|
|
send_response(response, output_tree);
|
|
} catch (std::exception &e) {
|
|
BOOST_LOG(warning) << "SavePin: "sv << e.what();
|
|
bad_request(response, request, e.what());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Reset the display device persistence.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
*
|
|
* @api_examples{/api/reset-display-device-persistence| POST| null}
|
|
*/
|
|
void resetDisplayDevicePersistence(resp_https_t response, req_https_t request) {
|
|
if (!check_content_type(response, request, "application/json")) {
|
|
return;
|
|
}
|
|
if (!authenticate(response, request)) {
|
|
return;
|
|
}
|
|
|
|
print_req(request);
|
|
|
|
nlohmann::json output_tree;
|
|
output_tree["status"] = display_device::reset_persistence();
|
|
send_response(response, output_tree);
|
|
}
|
|
|
|
/**
|
|
* @brief Restart Sunshine.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
*
|
|
* @api_examples{/api/restart| POST| null}
|
|
*/
|
|
void restart(resp_https_t response, req_https_t request) {
|
|
if (!check_content_type(response, request, "application/json")) {
|
|
return;
|
|
}
|
|
if (!authenticate(response, request)) {
|
|
return;
|
|
}
|
|
|
|
print_req(request);
|
|
|
|
// We may not return from this call
|
|
platf::restart();
|
|
}
|
|
|
|
/**
|
|
* @brief Get ViGEmBus driver version and installation status.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
*
|
|
* @api_examples{/api/vigembus/status| GET| null}
|
|
*/
|
|
void getViGEmBusStatus(resp_https_t response, req_https_t request) {
|
|
if (!authenticate(response, request)) {
|
|
return;
|
|
}
|
|
|
|
print_req(request);
|
|
|
|
nlohmann::json output_tree;
|
|
|
|
#ifdef _WIN32
|
|
std::string version_str;
|
|
bool installed = false;
|
|
bool version_compatible = false;
|
|
|
|
// Check if ViGEmBus driver exists
|
|
std::filesystem::path driver_path = std::filesystem::path(std::getenv("SystemRoot") ? std::getenv("SystemRoot") : "C:\\Windows") / "System32" / "drivers" / "ViGEmBus.sys";
|
|
|
|
if (std::filesystem::exists(driver_path)) {
|
|
installed = platf::getFileVersionInfo(driver_path, version_str);
|
|
if (installed) {
|
|
// Parse version string to check compatibility (>= 1.17.0.0)
|
|
std::vector<std::string> version_parts;
|
|
std::stringstream ss(version_str);
|
|
std::string part;
|
|
while (std::getline(ss, part, '.')) {
|
|
version_parts.push_back(part);
|
|
}
|
|
|
|
if (version_parts.size() >= 2) {
|
|
int major = std::stoi(version_parts[0]);
|
|
int minor = std::stoi(version_parts[1]);
|
|
version_compatible = (major > 1) || (major == 1 && minor >= 17);
|
|
}
|
|
}
|
|
}
|
|
|
|
output_tree["installed"] = installed;
|
|
output_tree["version"] = version_str;
|
|
output_tree["version_compatible"] = version_compatible;
|
|
output_tree["packaged_version"] = VIGEMBUS_PACKAGED_VERSION;
|
|
#else
|
|
output_tree["error"] = "ViGEmBus is only available on Windows";
|
|
output_tree["installed"] = false;
|
|
output_tree["version"] = "";
|
|
output_tree["version_compatible"] = false;
|
|
output_tree["packaged_version"] = "";
|
|
#endif
|
|
|
|
send_response(response, output_tree);
|
|
}
|
|
|
|
/**
|
|
* @brief Install ViGEmBus driver with elevated permissions.
|
|
* @param response The HTTP response object.
|
|
* @param request The HTTP request object.
|
|
*
|
|
* @api_examples{/api/vigembus/install| POST| null}
|
|
*/
|
|
void installViGEmBus(resp_https_t response, req_https_t request) {
|
|
if (!check_content_type(response, request, "application/json")) {
|
|
return;
|
|
}
|
|
if (!authenticate(response, request)) {
|
|
return;
|
|
}
|
|
|
|
print_req(request);
|
|
|
|
nlohmann::json output_tree;
|
|
|
|
#ifdef _WIN32
|
|
// Get the path to the vigembus installer
|
|
const std::filesystem::path installer_path = platf::appdata().parent_path() / "scripts" / "vigembus_installer.exe";
|
|
|
|
if (!std::filesystem::exists(installer_path)) {
|
|
output_tree["status"] = false;
|
|
output_tree["error"] = "ViGEmBus installer not found";
|
|
send_response(response, output_tree);
|
|
return;
|
|
}
|
|
|
|
// Run the installer with elevated permissions
|
|
std::error_code ec;
|
|
boost::filesystem::path working_dir = boost::filesystem::path(installer_path.string()).parent_path();
|
|
boost::process::v1::environment env = boost::this_process::environment();
|
|
|
|
// Run with elevated permissions, non-interactive
|
|
const std::string install_cmd = std::format("{} /quiet", installer_path.string());
|
|
auto child = platf::run_command(true, false, install_cmd, working_dir, env, nullptr, ec, nullptr);
|
|
|
|
if (ec) {
|
|
output_tree["status"] = false;
|
|
output_tree["error"] = "Failed to start installer: " + ec.message();
|
|
send_response(response, output_tree);
|
|
return;
|
|
}
|
|
|
|
// Wait for the installer to complete
|
|
child.wait(ec);
|
|
|
|
if (ec) {
|
|
output_tree["status"] = false;
|
|
output_tree["error"] = "Installer failed: " + ec.message();
|
|
} else {
|
|
int exit_code = child.exit_code();
|
|
output_tree["status"] = (exit_code == 0);
|
|
output_tree["exit_code"] = exit_code;
|
|
if (exit_code != 0) {
|
|
output_tree["error"] = std::format("Installer exited with code {}", exit_code);
|
|
}
|
|
}
|
|
#else
|
|
output_tree["status"] = false;
|
|
output_tree["error"] = "ViGEmBus installation is only available on Windows";
|
|
#endif
|
|
|
|
send_response(response, output_tree);
|
|
}
|
|
|
|
void start() {
|
|
platf::set_thread_name("confighttp");
|
|
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
|
|
|
|
auto port_https = net::map_port(PORT_HTTPS);
|
|
auto address_family = net::af_from_enum_string(config::sunshine.address_family);
|
|
|
|
https_server_t server {config::nvhttp.cert, config::nvhttp.pkey};
|
|
server.default_resource["DELETE"] = [](resp_https_t response, req_https_t request) {
|
|
bad_request(response, request);
|
|
};
|
|
server.default_resource["PATCH"] = [](resp_https_t response, req_https_t request) {
|
|
bad_request(response, request);
|
|
};
|
|
server.default_resource["POST"] = [](resp_https_t response, req_https_t request) {
|
|
bad_request(response, request);
|
|
};
|
|
server.default_resource["PUT"] = [](resp_https_t response, req_https_t request) {
|
|
bad_request(response, request);
|
|
};
|
|
server.default_resource["GET"] = not_found;
|
|
server.resource["^/$"]["GET"] = getIndexPage;
|
|
server.resource["^/pin/?$"]["GET"] = getPinPage;
|
|
server.resource["^/apps/?$"]["GET"] = getAppsPage;
|
|
server.resource["^/clients/?$"]["GET"] = getClientsPage;
|
|
server.resource["^/config/?$"]["GET"] = getConfigPage;
|
|
server.resource["^/password/?$"]["GET"] = getPasswordPage;
|
|
server.resource["^/welcome/?$"]["GET"] = getWelcomePage;
|
|
server.resource["^/troubleshooting/?$"]["GET"] = getTroubleshootingPage;
|
|
server.resource["^/api/pin$"]["POST"] = savePin;
|
|
server.resource["^/api/apps$"]["GET"] = getApps;
|
|
server.resource["^/api/logs$"]["GET"] = getLogs;
|
|
server.resource["^/api/apps$"]["POST"] = saveApp;
|
|
server.resource["^/api/config$"]["GET"] = getConfig;
|
|
server.resource["^/api/config$"]["POST"] = saveConfig;
|
|
server.resource["^/api/configLocale$"]["GET"] = getLocale;
|
|
server.resource["^/api/restart$"]["POST"] = restart;
|
|
server.resource["^/api/reset-display-device-persistence$"]["POST"] = resetDisplayDevicePersistence;
|
|
server.resource["^/api/vigembus/status$"]["GET"] = getViGEmBusStatus;
|
|
server.resource["^/api/vigembus/install$"]["POST"] = installViGEmBus;
|
|
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"] = getClients;
|
|
server.resource["^/api/clients/unpair$"]["POST"] = unpair;
|
|
server.resource["^/api/apps/close$"]["POST"] = closeApp;
|
|
server.resource["^/api/covers/upload$"]["POST"] = uploadCover;
|
|
server.resource["^/images/sunshine.ico$"]["GET"] = getFaviconImage;
|
|
server.resource["^/images/logo-sunshine-45.png$"]["GET"] = getSunshineLogoImage;
|
|
server.resource["^/assets\\/.+$"]["GET"] = getNodeModules;
|
|
server.config.reuse_address = true;
|
|
server.config.address = net::get_bind_address(address_family);
|
|
server.config.port = port_https;
|
|
|
|
auto accept_and_run = [&](auto *server) {
|
|
try {
|
|
platf::set_thread_name("confighttp::tcp");
|
|
server->start([](unsigned short port) {
|
|
BOOST_LOG(info) << "Configuration UI available at [https://localhost:"sv << port << "]";
|
|
});
|
|
} catch (boost::system::system_error &err) {
|
|
// It's possible the exception gets thrown after calling server->stop() from a different thread
|
|
if (shutdown_event->peek()) {
|
|
return;
|
|
}
|
|
|
|
BOOST_LOG(fatal) << "Couldn't start Configuration HTTPS server on port ["sv << port_https << "]: "sv << err.what();
|
|
shutdown_event->raise(true);
|
|
return;
|
|
}
|
|
};
|
|
std::thread tcp {accept_and_run, &server};
|
|
|
|
// Wait for any event
|
|
shutdown_event->view();
|
|
|
|
server.stop();
|
|
|
|
tcp.join();
|
|
}
|
|
} // namespace confighttp
|