refactor(confighttp): HTML page handlers into generic getPage function (#4645)

This commit is contained in:
David Lane
2026-03-08 18:36:22 -04:00
committed by GitHub
parent 3fbbe88b79
commit f04f6a2bde
13 changed files with 1250 additions and 275 deletions
+340 -269
View File
@@ -7,10 +7,11 @@
#define BOOST_BIND_GLOBAL_PLACEHOLDERS
// standard includes
#include <algorithm>
#include <filesystem>
#include <format>
#include <fstream>
#include <set>
#include <string_view>
// lib includes
#include <boost/algorithm/string.hpp>
@@ -51,14 +52,29 @@ namespace confighttp {
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>;
using resp_https_t = std::shared_ptr<SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>;
using req_https_t = std::shared_ptr<SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Request>;
using https_handler_t = std::function<void(resp_https_t, req_https_t)>;
enum class op_e {
ADD, ///< Add client
REMOVE ///< Remove client
};
// CSRF token management
struct csrf_token_t {
std::string token;
std::chrono::steady_clock::time_point expiration;
};
// Store CSRF tokens with thread safety
std::map<std::string, csrf_token_t, std::less<>> csrf_tokens; // NOSONAR(cpp:S5421) - intentionally mutable global
std::mutex csrf_tokens_mutex; // NOSONAR(cpp:S5421) - intentionally mutable global
// CSRF token configuration
constexpr auto CSRF_TOKEN_SIZE = 32; // 32 bytes = 256 bits
constexpr auto CSRF_TOKEN_LIFETIME = std::chrono::hours(1); // Tokens valid for 1 hour
/**
* @brief Log the request details.
* @param request The HTTP request object.
@@ -85,7 +101,7 @@ namespace confighttp {
* @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) {
void send_response(const resp_https_t &response, const nlohmann::json &output_tree) {
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "application/json");
headers.emplace("X-Frame-Options", "DENY");
@@ -98,11 +114,11 @@ namespace confighttp {
* @param response The HTTP response object.
* @param request The HTTP request object.
*/
void send_unauthorized(resp_https_t response, req_https_t request) {
void send_unauthorized(const resp_https_t &response, const 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;
constexpr auto code = SimpleWeb::StatusCode::client_error_unauthorized;
nlohmann::json tree;
tree["status_code"] = code;
@@ -125,7 +141,7 @@ namespace confighttp {
* @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) {
void send_redirect(const resp_https_t &response, const 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 {
@@ -142,11 +158,10 @@ namespace confighttp {
* @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) {
bool authenticate(const resp_https_t &response, const 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) {
if (const auto ip_type = net::from_address(address); 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;
@@ -162,24 +177,23 @@ namespace confighttp {
send_unauthorized(response, request);
});
auto auth = request->header.find("authorization");
const auto auth = request->header.find("authorization");
if (auth == request->header.end()) {
return false;
}
auto &rawAuth = auth->second;
const auto &rawAuth = auth->second;
auto authData = SimpleWeb::Crypto::Base64::decode(rawAuth.substr("Basic "sv.length()));
auto index = (int) authData.find(':');
const auto index = static_cast<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();
const auto username = authData.substr(0, index);
const auto password = authData.substr(index + 1);
if (!boost::iequals(username, config::sunshine.username) || hash != config::sunshine.password) {
if (const auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string(); !boost::iequals(username, config::sunshine.username) || hash != config::sunshine.password) {
return false;
}
@@ -193,8 +207,8 @@ namespace confighttp {
* @param request The HTTP request object.
* @param error_message The error message to include in the response.
*/
void not_found(resp_https_t response, [[maybe_unused]] req_https_t request, const std::string &error_message = "Not Found") {
constexpr SimpleWeb::StatusCode code = SimpleWeb::StatusCode::client_error_not_found;
void not_found(const resp_https_t &response, [[maybe_unused]] const req_https_t &request, const std::string &error_message) {
constexpr auto code = SimpleWeb::StatusCode::client_error_not_found;
nlohmann::json tree;
tree["status_code"] = code;
@@ -214,8 +228,8 @@ namespace confighttp {
* @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;
void bad_request(const resp_https_t &response, [[maybe_unused]] const req_https_t &request, const std::string &error_message) {
constexpr auto code = SimpleWeb::StatusCode::client_error_bad_request;
nlohmann::json tree;
tree["status_code"] = code;
@@ -231,21 +245,20 @@ namespace confighttp {
}
/**
* @brief Validate the request content type and send bad request when mismatch.
* @brief Validate the request content type and send a bad request when mismatched.
* @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");
bool check_content_type(const resp_https_t &response, const req_https_t &request, const std::string_view &contentType) {
const 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) {
if (const size_t semicolonPos = actualContentType.find(';'); semicolonPos != std::string::npos) {
actualContentType = actualContentType.substr(0, semicolonPos);
}
@@ -264,12 +277,144 @@ namespace confighttp {
}
/**
* @brief Validates the application index and sends error response if invalid.
* @brief Get a unique client identifier for CSRF token management.
* @param request The HTTP request object.
* @return A unique identifier based on username or IP address.
*/
std::string get_client_id(const req_https_t &request) {
// Try to use the authenticated username as client ID
if (const auto auth = request->header.find("authorization"); !config::sunshine.username.empty() && auth != request->header.end()) {
if (const auto &rawAuth = auth->second; rawAuth.rfind("Basic "sv, 0) == 0) {
auto authData = SimpleWeb::Crypto::Base64::decode(rawAuth.substr("Basic "sv.length()));
if (const auto index = static_cast<int>(authData.find(':')); index < authData.size() - 1) {
return authData.substr(0, index); // Return username
}
}
}
// Fall back to IP address if no username
return net::addr_to_normalized_string(request->remote_endpoint().address());
}
/**
* @brief Generate a new CSRF token for a client.
* @param client_id A unique identifier for the client (e.g., session ID or username).
* @return The generated CSRF token.
*/
std::string generate_csrf_token(const std::string &client_id) {
// Generate a cryptographically secure random token
std::string token = crypto::rand_alphabet(CSRF_TOKEN_SIZE);
std::scoped_lock lock(csrf_tokens_mutex);
// Clean up expired tokens first
const auto now = std::chrono::steady_clock::now();
std::erase_if(csrf_tokens, [&now](const auto &entry) {
return entry.second.expiration < now;
});
// Store the token with expiration
csrf_tokens[client_id] = csrf_token_t {
token,
now + CSRF_TOKEN_LIFETIME
};
return token;
}
/**
* @brief Validate a stored CSRF token for a client against a provided token string.
* @param response The HTTP response object.
* @param request The HTTP request object.
* @param client_id A unique identifier for the client.
* @param provided_token The token string to validate.
* @return True if the token is valid, false otherwise.
*/
bool validate_stored_csrf_token(const resp_https_t &response, const req_https_t &request, const std::string_view client_id, const std::string_view provided_token) {
std::scoped_lock lock(csrf_tokens_mutex);
const auto token_it = csrf_tokens.find(client_id);
if (token_it == csrf_tokens.end()) {
bad_request(response, request, "Invalid CSRF token");
return false;
}
if (const auto now = std::chrono::steady_clock::now(); token_it->second.expiration < now) {
csrf_tokens.erase(token_it);
bad_request(response, request, "CSRF token expired");
return false;
}
if (token_it->second.token != provided_token) {
bad_request(response, request, "Invalid CSRF token");
return false;
}
return true;
}
bool validate_csrf_token(const resp_https_t &response, const req_https_t &request, const std::string &client_id) {
// Helper function to check if a URL starts with any allowed origin
auto is_allowed_origin = [](const std::string_view url) {
return std::ranges::any_of(config::sunshine.csrf_allowed_origins, [&url](const std::string &allowed_origin) {
// Ensure exact prefix match (with ":" or "/" after to prevent malicious.com matching allowed.com)
if (url.rfind(allowed_origin, 0) != 0) { // rfind with pos=0 checks if the url starts with allowed_origin
return false;
}
// Check that it's followed by ":" (port) or "/" (path) or is an exact match
const size_t len = allowed_origin.length();
return url.length() == len || url[len] == ':' || url[len] == '/';
});
};
// Check if the request is from the same origin (Origin or Referer header matches configured allowed origins)
const auto origin_it = request->header.find("Origin");
if (origin_it != request->header.end() && is_allowed_origin(origin_it->second)) {
// Same origin request - allow without CSRF token
return true;
}
// If we have a Referer header, check if it's same-origin
const auto referer_it = request->header.find("Referer");
if (referer_it != request->header.end() && is_allowed_origin(referer_it->second)) {
// Same origin request - allow without CSRF token
return true;
}
// If neither Origin nor Referer is present, this cannot be a browser-initiated CSRF attack.
// Non-browser clients (e.g. curl, scripts) never send these headers, and a malicious web page
// cannot cause a non-browser client to make requests on a user's behalf.
if (origin_it == request->header.end() && referer_it == request->header.end()) {
return true;
}
// A browser-like request arrived with an Origin/Referer that doesn't match an allowed origin.
// Require a CSRF token.
// Extract token from X-CSRF-Token header
const auto header_it = request->header.find("X-CSRF-Token");
if (header_it == request->header.end()) {
// Also check query parameters as fallback
auto query_params = request->parse_query_string();
const auto query_it = query_params.find("csrf_token");
if (query_it == query_params.end()) {
bad_request(response, request, "Missing CSRF token");
return false;
}
return validate_stored_csrf_token(response, request, client_id, query_it->second);
}
// Validate token from header
return validate_stored_csrf_token(response, request, client_id, header_it->second);
}
/**
* @brief Validates the application index and sends an error response if invalid.
* @param response The HTTP response object.
* @param request The HTTP request object.
* @param index The application index/id.
*/
bool check_app_index(resp_https_t response, req_https_t request, int index) {
bool check_app_index(const resp_https_t &response, const req_https_t &request, int index) {
std::string file = file_handler::read_file(config::stream.file_apps.c_str());
nlohmann::json file_tree = nlohmann::json::parse(file);
if (const auto &apps = file_tree["apps"]; index < 0 || index >= static_cast<int>(apps.size())) {
@@ -279,190 +424,41 @@ namespace confighttp {
} else {
error = std::format("'index' {} out of range, max index is {}", index, max_index);
}
bad_request(std::move(response), std::move(request), error);
bad_request(response, request, error);
return false;
}
return true;
}
/**
* @brief Get the index page.
* @brief Get an HTML 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"
* @param html_file The HTML file to serve (relative to WEB_DIR).
* @param require_auth Whether to require authentication (default: true).
* @param redirect_if_username If true, redirect to "/" when the username is set (for welcome page).
*/
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 featured apps page.
* @param response The HTTP response object.
* @param request The HTTP request object.
*/
void getFeaturedPage(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 "featured.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()) {
void getPage(const resp_https_t &response, const req_https_t &request, const char *html_file, const bool require_auth, const bool redirect_if_username) {
// Special handling for welcome page: redirect if the username is already set
if (redirect_if_username && !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)) {
if (require_auth && !authenticate(response, request)) {
return;
}
print_req(request);
std::string content = file_handler::read_file(WEB_DIR "troubleshooting.html");
const std::string content = file_handler::read_file((std::string(WEB_DIR) + html_file).c_str());
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
// prevent click jacking
headers.emplace("X-Frame-Options", "DENY");
headers.emplace("Content-Security-Policy", "frame-ancestors 'none';");
response->write(content, headers);
}
@@ -473,7 +469,7 @@ namespace confighttp {
* @todo combine function with getSunshineLogoImage and possibly getNodeModules
* @todo use mime_types map
*/
void getFaviconImage(resp_https_t response, req_https_t request) {
void getFaviconImage(const resp_https_t &response, const req_https_t &request) {
print_req(request);
std::ifstream in(WEB_DIR "images/sunshine.ico", std::ios::binary);
@@ -491,7 +487,7 @@ namespace confighttp {
* @todo combine function with getFaviconImage and possibly getNodeModules
* @todo use mime_types map
*/
void getSunshineLogoImage(resp_https_t response, req_https_t request) {
void getSunshineLogoImage(const resp_https_t &response, const req_https_t &request) {
print_req(request);
std::ifstream in(WEB_DIR "images/logo-sunshine-45.png", std::ios::binary);
@@ -514,11 +510,11 @@ namespace confighttp {
}
/**
* @brief Get an asset from the node_modules directory.
* @brief Get an asset.
* @param response The HTTP response object.
* @param request The HTTP request object.
*/
void getNodeModules(resp_https_t response, req_https_t request) {
void getAsset(const resp_https_t &response, const req_https_t &request) {
print_req(request);
fs::path webDirPath(WEB_DIR);
fs::path nodeModulesPath(webDirPath / "assets");
@@ -526,7 +522,7 @@ namespace confighttp {
// .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
// Don't do anything if the 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);
@@ -556,6 +552,28 @@ namespace confighttp {
response->write(SimpleWeb::StatusCode::success_ok, in, headers);
}
/**
* @brief Get a CSRF token for the authenticated user.
* @param response The HTTP response object.
* @param request The HTTP request object.
*
* @api_examples{/api/csrf-token| GET| null}
*/
void getCSRFToken(const resp_https_t &response, const req_https_t &request) {
if (!authenticate(response, request)) {
return;
}
print_req(request);
std::string client_id = get_client_id(request);
std::string token = generate_csrf_token(client_id);
nlohmann::json output_tree;
output_tree["csrf_token"] = token;
send_response(response, output_tree);
}
/**
* @brief Get the list of available applications.
* @param response The HTTP response object.
@@ -563,7 +581,7 @@ namespace confighttp {
*
* @api_examples{/api/apps| GET| null}
*/
void getApps(resp_https_t response, req_https_t request) {
void getApps(const resp_https_t &response, const req_https_t &request) {
if (!authenticate(response, request)) {
return;
}
@@ -576,7 +594,7 @@ namespace confighttp {
// 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 = {
const std::vector<std::string> boolean_keys = {
"exclude-global-prep-cmd",
"elevated",
"auto-detach",
@@ -617,7 +635,7 @@ namespace confighttp {
}
/**
* @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.
* @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:
@@ -648,7 +666,7 @@ namespace confighttp {
*
* @api_examples{/api/apps| POST| {"name":"Hello, World!","index":-1}}
*/
void saveApp(resp_https_t response, req_https_t request) {
void saveApp(const resp_https_t &response, const req_https_t &request) {
if (!check_content_type(response, request, "application/json")) {
return;
}
@@ -656,6 +674,11 @@ namespace confighttp {
return;
}
std::string client_id = get_client_id(request);
if (!validate_csrf_token(response, request, client_id)) {
return;
}
print_req(request);
std::stringstream ss;
@@ -677,7 +700,7 @@ namespace confighttp {
}
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
int index = input_tree["index"].get<int>(); // this will intentionally cause an exception if the provided value is the wrong type
input_tree.erase("index");
@@ -718,11 +741,13 @@ namespace confighttp {
*
* @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")) {
void closeApp(const resp_https_t &response, const req_https_t &request) {
if (!authenticate(response, request)) {
return;
}
if (!authenticate(response, request)) {
std::string client_id = get_client_id(request);
if (!validate_csrf_token(response, request, client_id)) {
return;
}
@@ -742,13 +767,16 @@ namespace confighttp {
*
* @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.
void deleteApp(const resp_https_t &response, const req_https_t &request) {
if (!authenticate(response, request)) {
return;
}
std::string client_id = get_client_id(request);
if (!validate_csrf_token(response, request, client_id)) {
return;
}
print_req(request);
try {
@@ -790,7 +818,7 @@ namespace confighttp {
*
* @api_examples{/api/clients/list| GET| null}
*/
void getClients(resp_https_t response, req_https_t request) {
void getClients(const resp_https_t &response, const req_https_t &request) {
if (!authenticate(response, request)) {
return;
}
@@ -809,7 +837,7 @@ namespace confighttp {
* @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:
* The body for the POST request should be JSON serialized in the following format:
* @code{.json}
* {
* "uuid": "<uuid>"
@@ -818,7 +846,7 @@ namespace confighttp {
*
* @api_examples{/api/unpair| POST| {"uuid":"1234"}}
*/
void unpair(resp_https_t response, req_https_t request) {
void unpair(const resp_https_t &response, const req_https_t &request) {
if (!check_content_type(response, request, "application/json")) {
return;
}
@@ -826,6 +854,11 @@ namespace confighttp {
return;
}
std::string client_id = get_client_id(request);
if (!validate_csrf_token(response, request, client_id)) {
return;
}
print_req(request);
std::stringstream ss;
@@ -851,11 +884,13 @@ namespace confighttp {
*
* @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")) {
void unpairAll(const resp_https_t &response, const req_https_t &request) {
if (!authenticate(response, request)) {
return;
}
if (!authenticate(response, request)) {
std::string client_id = get_client_id(request);
if (!validate_csrf_token(response, request, client_id)) {
return;
}
@@ -876,7 +911,7 @@ namespace confighttp {
*
* @api_examples{/api/config| GET| null}
*/
void getConfig(resp_https_t response, req_https_t request) {
void getConfig(const resp_https_t &response, const req_https_t &request) {
if (!authenticate(response, request)) {
return;
}
@@ -904,7 +939,7 @@ namespace confighttp {
*
* @api_examples{/api/configLocale| GET| null}
*/
void getLocale(resp_https_t response, req_https_t request) {
void getLocale(const resp_https_t &response, const req_https_t &request) {
// we need to return the locale whether authenticated or not
print_req(request);
@@ -919,7 +954,7 @@ namespace confighttp {
* @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:
* The body for the POST request should be JSON serialized in the following format:
* @code{.json}
* {
* "key": "value"
@@ -930,7 +965,7 @@ namespace confighttp {
*
* @api_examples{/api/config| POST| {"key":"value"}}
*/
void saveConfig(resp_https_t response, req_https_t request) {
void saveConfig(const resp_https_t &response, const req_https_t &request) {
if (!check_content_type(response, request, "application/json")) {
return;
}
@@ -938,6 +973,11 @@ namespace confighttp {
return;
}
std::string client_id = get_client_id(request);
if (!validate_csrf_token(response, request, client_id)) {
return;
}
print_req(request);
std::stringstream ss;
@@ -952,8 +992,8 @@ namespace confighttp {
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
// 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());
@@ -974,7 +1014,7 @@ namespace confighttp {
*
* @api_examples{/api/covers/9999 | GET| null}
*/
void getCover(resp_https_t response, req_https_t request) {
void getCover(const resp_https_t &response, const req_https_t &request) {
if (!authenticate(response, request)) {
return;
}
@@ -1044,7 +1084,7 @@ namespace confighttp {
*
* @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) {
void uploadCover(const resp_https_t &response, const req_https_t &request) {
if (!check_content_type(response, request, "application/json")) {
return;
}
@@ -1100,7 +1140,7 @@ namespace confighttp {
*
* @api_examples{/api/logs| GET| null}
*/
void getLogs(resp_https_t response, req_https_t request) {
void getLogs(const resp_https_t &response, const req_https_t &request) {
if (!authenticate(response, request)) {
return;
}
@@ -1132,7 +1172,7 @@ namespace confighttp {
*
* @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) {
void savePassword(const resp_https_t &response, const req_https_t &request) {
if (!check_content_type(response, request, "application/json")) {
return;
}
@@ -1140,6 +1180,11 @@ namespace confighttp {
return;
}
std::string client_id = get_client_id(request);
if (!validate_csrf_token(response, request, client_id)) {
return;
}
print_req(request);
std::vector<std::string> errors = {};
@@ -1205,7 +1250,7 @@ namespace confighttp {
*
* @api_examples{/api/pin| POST| {"pin":"1234","name":"My PC"}}
*/
void savePin(resp_https_t response, req_https_t request) {
void savePin(const resp_https_t &response, const req_https_t &request) {
if (!check_content_type(response, request, "application/json")) {
return;
}
@@ -1213,6 +1258,11 @@ namespace confighttp {
return;
}
std::string client_id = get_client_id(request);
if (!validate_csrf_token(response, request, client_id)) {
return;
}
print_req(request);
std::stringstream ss;
@@ -1244,11 +1294,13 @@ namespace confighttp {
*
* @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")) {
void resetDisplayDevicePersistence(const resp_https_t &response, const req_https_t &request) {
if (!authenticate(response, request)) {
return;
}
if (!authenticate(response, request)) {
std::string client_id = get_client_id(request);
if (!validate_csrf_token(response, request, client_id)) {
return;
}
@@ -1266,11 +1318,13 @@ namespace confighttp {
*
* @api_examples{/api/restart| POST| null}
*/
void restart(resp_https_t response, req_https_t request) {
if (!check_content_type(response, request, "application/json")) {
void restart(const resp_https_t &response, const req_https_t &request) {
if (!authenticate(response, request)) {
return;
}
if (!authenticate(response, request)) {
std::string client_id = get_client_id(request);
if (!validate_csrf_token(response, request, client_id)) {
return;
}
@@ -1287,7 +1341,7 @@ namespace confighttp {
*
* @api_examples{/api/vigembus/status| GET| null}
*/
void getViGEmBusStatus(resp_https_t response, req_https_t request) {
void getViGEmBusStatus(const resp_https_t &response, const req_https_t &request) {
if (!authenticate(response, request)) {
return;
}
@@ -1345,11 +1399,13 @@ namespace confighttp {
*
* @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")) {
void installViGEmBus(const resp_https_t &response, const req_https_t &request) {
if (!authenticate(response, request)) {
return;
}
if (!authenticate(response, request)) {
std::string client_id = get_client_id(request);
if (!validate_csrf_token(response, request, client_id)) {
return;
}
@@ -1408,58 +1464,73 @@ namespace confighttp {
void start() {
platf::set_thread_name("confighttp");
auto shutdown_event = mail::man->event<bool>(mail::shutdown);
const 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);
const auto port_https = net::map_port(PORT_HTTPS);
const 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) {
// Helper to create page handler lambdas without repeating the signature
auto page_handler = [](const char *file, bool require_auth = true, bool redirect_if_username = false) {
return [file, require_auth, redirect_if_username](const resp_https_t &response, const req_https_t &request) {
getPage(response, request, file, require_auth, redirect_if_username);
};
};
// Default resource handlers
const https_handler_t bad_request_handler = [](const resp_https_t &response, const 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"] = [](resp_https_t response, req_https_t request) {
const https_handler_t not_found_handler = [](const resp_https_t &response, const req_https_t &request) {
not_found(response, request);
};
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["^/featured/?$"]["GET"] = getFeaturedPage;
server.resource["^/password/?$"]["GET"] = getPasswordPage;
server.resource["^/welcome/?$"]["GET"] = getWelcomePage;
server.resource["^/troubleshooting/?$"]["GET"] = getTroubleshootingPage;
server.resource["^/api/pin$"]["POST"] = savePin;
// error by default
server.default_resource["DELETE"] = bad_request_handler;
server.default_resource["PATCH"] = bad_request_handler;
server.default_resource["POST"] = bad_request_handler;
server.default_resource["PUT"] = bad_request_handler;
server.default_resource["GET"] = not_found_handler;
// web pages
server.resource["^/$"]["GET"] = page_handler("index.html");
server.resource["^/apps/?$"]["GET"] = page_handler("apps.html");
server.resource["^/clients/?$"]["GET"] = page_handler("clients.html");
server.resource["^/config/?$"]["GET"] = page_handler("config.html");
server.resource["^/featured/?$"]["GET"] = page_handler("featured.html");
server.resource["^/password/?$"]["GET"] = page_handler("password.html");
server.resource["^/pin/?$"]["GET"] = page_handler("pin.html");
server.resource["^/troubleshooting/?$"]["GET"] = page_handler("troubleshooting.html");
server.resource["^/welcome/?$"]["GET"] = page_handler("welcome.html", false, true);
// rest api
server.resource["^/api/apps$"]["GET"] = getApps;
server.resource["^/api/logs$"]["GET"] = getLogs;
server.resource["^/api/apps$"]["POST"] = saveApp;
server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp;
server.resource["^/api/apps/close$"]["POST"] = closeApp;
server.resource["^/api/clients/list$"]["GET"] = getClients;
server.resource["^/api/clients/unpair$"]["POST"] = unpair;
server.resource["^/api/clients/unpair-all$"]["POST"] = unpairAll;
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/covers/([0-9]+)$"]["GET"] = getCover;
server.resource["^/api/covers/upload$"]["POST"] = uploadCover;
server.resource["^/api/csrf-token$"]["GET"] = getCSRFToken;
server.resource["^/api/password$"]["POST"] = savePassword;
server.resource["^/api/pin$"]["POST"] = savePin;
server.resource["^/api/logs$"]["GET"] = getLogs;
server.resource["^/api/reset-display-device-persistence$"]["POST"] = resetDisplayDevicePersistence;
server.resource["^/api/restart$"]["POST"] = restart;
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["^/api/covers/([0-9]+)$"]["GET"] = getCover;
// static/dynamic resources
server.resource["^/images/sunshine.ico$"]["GET"] = getFaviconImage;
server.resource["^/images/logo-sunshine-45.png$"]["GET"] = getSunshineLogoImage;
server.resource["^/assets\\/.+$"]["GET"] = getNodeModules;
server.resource["^/assets\\/.+$"]["GET"] = getAsset;
server.config.reuse_address = true;
server.config.address = net::get_bind_address(address_family);
server.config.port = port_https;
@@ -1467,7 +1538,7 @@ namespace confighttp {
auto accept_and_run = [&](auto *server) {
try {
platf::set_thread_name("confighttp::tcp");
server->start([](unsigned short port) {
server->start([](const unsigned short port) {
BOOST_LOG(info) << "Configuration UI available at [https://localhost:"sv << port << "]";
});
} catch (boost::system::system_error &err) {