Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ basic authentication with the admin username and password.
## POST /api/config
@copydoc confighttp::saveConfig()

## GET /api/covers/{index}
@copydoc confighttp::getCover()

## POST /api/covers/upload
@copydoc confighttp::uploadCover()

Expand Down
113 changes: 97 additions & 16 deletions src/confighttp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,14 @@ namespace confighttp {
* @brief Send a 404 Not Found 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 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, const std::string &error_message = "Not Found") {
constexpr SimpleWeb::StatusCode code = SimpleWeb::StatusCode::client_error_not_found;

nlohmann::json tree;
tree["status_code"] = code;
tree["error"] = "Not Found";
tree["error"] = error_message;

SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "application/json");
Expand Down Expand Up @@ -262,6 +263,28 @@ namespace confighttp {
return true;
}

/**
* @brief Validates the application index and sends 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) {
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())) {
std::string error;
if (const int max_index = static_cast<int>(apps.size()) - 1; max_index < 0) {
error = "No applications found";
} else {
error = std::format("'index' {} out of range, max index is {}", index, max_index);
}
bad_request(std::move(response), std::move(request), error);
return false;
}
return true;
}

/**
* @brief Get the index page.
* @param response The HTTP response object.
Expand Down Expand Up @@ -711,25 +734,19 @@ namespace confighttp {
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);
if (!check_app_index(response, request, index)) {
return;
}

for (size_t i = 0; i < apps_node.size(); ++i) {
std::string file = file_handler::read_file(config::stream.file_apps.c_str());
nlohmann::json file_tree = nlohmann::json::parse(file);
auto &apps = file_tree["apps"];

for (size_t i = 0; i < apps.size(); ++i) {
if (i != index) {
new_apps.push_back(apps_node[i]);
new_apps.push_back(apps[i]);
}
}
file_tree["apps"] = new_apps;
Expand Down Expand Up @@ -928,6 +945,67 @@ namespace confighttp {
}
}

/**
* @brief Get an application's image.
* @param response The HTTP response object.
* @param request The HTTP request object.
*
* @note{The index in the url path is the application index.}
*
* @api_examples{/api/covers/9999 | GET| null}
*/
void getCover(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);

try {
const int index = std::stoi(request->path_match[1]);
if (!check_app_index(response, request, index)) {
return;
}

std::string file = file_handler::read_file(config::stream.file_apps.c_str());
nlohmann::json file_tree = nlohmann::json::parse(file);
auto &apps = file_tree["apps"];

auto &app = apps[index];

// Get the image path from the app configuration
std::string app_image_path;
if (app.contains("image-path") && !app["image-path"].is_null()) {
app_image_path = app["image-path"];
}

// Use validate_app_image_path to resolve and validate the path
// This handles extension validation, PNG signature validation, and path resolution
std::string validated_path = proc::validate_app_image_path(app_image_path);

// Open and stream the validated file
std::ifstream in(validated_path, std::ios::binary);
if (!in) {
BOOST_LOG(warning) << "Unable to read cover image file: " << validated_path;
bad_request(response, request, "Unable to read cover image file");
return;
}

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);
} catch (std::exception &e) {
BOOST_LOG(warning) << "GetCover: "sv << e.what();
bad_request(response, request, e.what());
}
}

/**
* @brief Upload a cover image.
* @param response The HTTP response object.
Expand Down Expand Up @@ -1324,7 +1402,9 @@ namespace confighttp {
server.default_resource["PUT"] = [](resp_https_t response, req_https_t request) {
bad_request(response, request);
};
server.default_resource["GET"] = not_found;
server.default_resource["GET"] = [](resp_https_t response, req_https_t request) {
not_found(response, request);
};
server.resource["^/$"]["GET"] = getIndexPage;
server.resource["^/pin/?$"]["GET"] = getPinPage;
server.resource["^/apps/?$"]["GET"] = getAppsPage;
Expand All @@ -1351,6 +1431,7 @@ namespace confighttp {
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;
server.resource["^/images/sunshine.ico$"]["GET"] = getFaviconImage;
server.resource["^/images/logo-sunshine-45.png$"]["GET"] = getSunshineLogoImage;
server.resource["^/assets\\/.+$"]["GET"] = getNodeModules;
Expand Down
59 changes: 51 additions & 8 deletions src/process.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@
#include <share.h>
#endif

#define DEFAULT_APP_IMAGE_PATH SUNSHINE_ASSETS_DIR "/box.png"

namespace proc {
using namespace std::literals;
namespace pt = boost::property_tree;
Expand Down Expand Up @@ -466,6 +464,40 @@ namespace proc {
return ss.str();
}

/**
* @brief Validates a path whether it is a valid PNG.
* @param path The path to the PNG file.
* @return true if the file has a valid PNG signature, false otherwise.
*/
bool check_valid_png(const std::filesystem::path &path) {
// PNG signature as defined in PNG specification
// http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html
static constexpr std::array<unsigned char, 8> PNG_SIGNATURE = {
0x89,
0x50,
0x4E,
0x47,
0x0D,
0x0A,
0x1A,
0x0A
};

std::ifstream file(path, std::ios::binary);
if (!file) {
return false;
}

std::array<unsigned char, 8> header;
file.read(reinterpret_cast<char *>(header.data()), 8);

if (file.gcount() != 8) {
return false;
}

return header == PNG_SIGNATURE;
}

std::string validate_app_image_path(std::string app_image_path) {
if (app_image_path.empty()) {
return DEFAULT_APP_IMAGE_PATH;
Expand All @@ -475,28 +507,39 @@ namespace proc {
auto image_extension = std::filesystem::path(app_image_path).extension().string();
boost::to_lower(image_extension);

// return the default box image if extension is not "png"
// return the default box image if the extension is not "png"
if (image_extension != ".png") {
return DEFAULT_APP_IMAGE_PATH;
}

// check if image is in assets directory
auto full_image_path = std::filesystem::path(SUNSHINE_ASSETS_DIR) / app_image_path;
if (std::filesystem::exists(full_image_path)) {
if (auto full_image_path = std::filesystem::path(SUNSHINE_ASSETS_DIR) / app_image_path; std::filesystem::exists(full_image_path)) {
// Validate PNG signature
if (!check_valid_png(full_image_path)) {
BOOST_LOG(warning) << "Invalid PNG file at path ["sv << full_image_path << ']';
return DEFAULT_APP_IMAGE_PATH;
}
return full_image_path.string();
} else if (app_image_path == "./assets/steam.png") {
}

if (app_image_path == "./assets/steam.png") {
// handle old default steam image definition
return SUNSHINE_ASSETS_DIR "/steam.png";
}

// check if specified image exists
std::error_code code;
if (!std::filesystem::exists(app_image_path, code)) {
if (std::error_code code; !std::filesystem::exists(app_image_path, code)) {
// return default box image if image does not exist
BOOST_LOG(warning) << "Couldn't find app image at path ["sv << app_image_path << ']';
return DEFAULT_APP_IMAGE_PATH;
}

// Validate PNG signature
if (!check_valid_png(app_image_path)) {
BOOST_LOG(warning) << "Invalid PNG file at path ["sv << app_image_path << ']';
return DEFAULT_APP_IMAGE_PATH;
}

// image is a png, and not in assets directory
// return only "content-type" http header compatible image type
return app_image_path;
Expand Down
3 changes: 3 additions & 0 deletions src/process.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
#include "rtsp.h"
#include "utility.h"

#define DEFAULT_APP_IMAGE_PATH SUNSHINE_ASSETS_DIR "/box.png"

namespace proc {
using file_t = util::safe_ptr_v2<FILE, int, fclose>;

Expand Down Expand Up @@ -120,6 +122,7 @@ namespace proc {
*/
std::tuple<std::string, std::string> calculate_app_id(const std::string &app_name, std::string app_image_path, int index);

bool check_valid_png(const std::filesystem::path &path);
std::string validate_app_image_path(std::string app_image_path);
void refresh(const std::string &file_name);
std::optional<proc::proc_t> parse(const std::string &file_name);
Expand Down
Loading
Loading