diff --git a/CMakeLists.txt b/CMakeLists.txt index b192e008..ef731b51 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -375,7 +375,6 @@ set(SUNSHINE_TARGET_FILES src/network.cpp src/network.h src/move_by_copy.h - src/rand.h src/task_pool.h src/thread_pool.h src/thread_safe.h diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 79997435..1276c597 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -30,7 +30,6 @@ #include "network.h" #include "nvhttp.h" #include "platform/common.h" -#include "rand.h" #include "rtsp.h" #include "utility.h" #include "uuid.h" @@ -303,11 +302,6 @@ void saveApp(resp_https_t response, req_https_t request) { response->write(data.str()); }); - std::set ids; - for(auto const &app : proc::proc.get_apps()) { - ids.insert(app.id); - } - pt::ptree inputTree, fileTree; BOOST_LOG(fatal) << config::stream.file_apps; @@ -316,14 +310,6 @@ void saveApp(resp_https_t response, req_https_t request) { pt::read_json(ss, inputTree); pt::read_json(config::stream.file_apps, fileTree); - // Moonlight checks the id of an item to determine if an item was changed - // Needs to be a 32-bit positive integer due to client limitations, "0" indicates no app - auto id = util::generate_int32(1, std::numeric_limits::max()); - while(ids.count(std::to_string(id)) > 0) { - id = util::generate_int32(1, std::numeric_limits::max()); - } - inputTree.put("id", id); - if(inputTree.get_child("prep-cmd").empty()) { inputTree.erase("prep-cmd"); } diff --git a/src/process.cpp b/src/process.cpp index 457333da..21e633ae 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -9,11 +9,16 @@ #include #include +#include #include #include #include #include +#include +#include + +#include "crypto.h" #include "main.h" #include "platform/common.h" #include "utility.h" @@ -23,6 +28,8 @@ #include #endif +#define DEFAULT_APP_IMAGE_PATH SUNSHINE_ASSETS_DIR "/box.png" + namespace proc { using namespace std::literals; namespace bp = boost::process; @@ -233,47 +240,12 @@ std::vector &proc_t::get_apps() { // Returns default image if image configuration is not set. // Returns http content-type header compatible image type. std::string proc_t::get_app_image(int app_id) { - auto default_image = SUNSHINE_ASSETS_DIR "/box.png"; - auto iter = std::find_if(_apps.begin(), _apps.end(), [&app_id](const auto app) { return app.id == std::to_string(app_id); }); auto app_image_path = iter == _apps.end() ? std::string() : iter->image_path; - if(app_image_path.empty()) { - BOOST_LOG(warning) << "Couldn't find app image for ID ["sv << app_id << ']'; - return default_image; - } - - // get the image extension and convert it to lowercase - 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" - if(image_extension != ".png") { - return default_image; - } - - // 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)) { - return full_image_path.string(); - } - else 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)) { - // return default box image if image does not exist - return default_image; - } - - // image is a png, and not in assets directory - // return only "content-type" http header compatible image type - return app_image_path; + return validate_app_image_path(app_image_path); } proc_t::~proc_t() { @@ -355,6 +327,114 @@ std::string parse_env_val(bp::native_environment &env, const std::string_view &v return ss.str(); } +std::string validate_app_image_path(std::string app_image_path) { + if(app_image_path.empty()) { + return DEFAULT_APP_IMAGE_PATH; + } + + // get the image extension and convert it to lowercase + 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" + 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)) { + return full_image_path.string(); + } + else 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)) { + // 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; + } + + // image is a png, and not in assets directory + // return only "content-type" http header compatible image type + return app_image_path; +} + +std::optional calculate_sha256(const std::string &filename) { + crypto::md_ctx_t ctx { EVP_MD_CTX_create() }; + if(!ctx) { + return std::nullopt; + } + + if(!EVP_DigestInit_ex(ctx.get(), EVP_sha256(), nullptr)) { + return std::nullopt; + } + + // Read file and update calculated SHA + char buf[1024 * 16]; + std::ifstream file(filename, std::ifstream::binary); + while(file.good()) { + file.read(buf, sizeof(buf)); + if(!EVP_DigestUpdate(ctx.get(), buf, file.gcount())) { + return std::nullopt; + } + } + file.close(); + + unsigned char result[SHA256_DIGEST_LENGTH]; + if(!EVP_DigestFinal_ex(ctx.get(), result, nullptr)) { + return std::nullopt; + } + + // Transform byte-array to string + std::stringstream ss; + ss << std::hex << std::setfill('0'); + for(const auto &byte : result) { + ss << std::setw(2) << (int)byte; + } + return ss.str(); +} + +uint32_t calculate_crc32(const std::string &input) { + boost::crc_32_type result; + result.process_bytes(input.data(), input.length()); + return result.checksum(); +} + +std::tuple calculate_app_id(const std::string &app_name, std::string app_image_path, int index) { + // Generate id by hashing name with image data if present + std::vector to_hash; + to_hash.push_back(app_name); + auto file_path = validate_app_image_path(app_image_path); + if(file_path != DEFAULT_APP_IMAGE_PATH) { + auto file_hash = calculate_sha256(file_path); + if(file_hash) { + to_hash.push_back(file_hash.value()); + } + else { + // Fallback to just hashing image path + to_hash.push_back(file_path); + } + } + + // Create combined strings for hash + std::stringstream ss; + for_each(to_hash.begin(), to_hash.end(), [&ss](const std::string &s) { ss << s; }); + auto input_no_index = ss.str(); + ss << index; + auto input_with_index = ss.str(); + + // CRC32 then truncate to signed 32-bit range due to client limitations + auto id_no_index = std::to_string(abs((int32_t)calculate_crc32(input_no_index))); + auto id_with_index = std::to_string(abs((int32_t)calculate_crc32(input_with_index))); + + return std::make_tuple(id_no_index, id_with_index); +} + std::optional parse(const std::string &file_name) { pt::ptree tree; @@ -370,8 +450,9 @@ std::optional parse(const std::string &file_name) { this_env[name] = parse_env_val(this_env, val.get_value()); } - int app_index = 1; // Start at 1, 0 indicates no app running + std::set ids; std::vector apps; + int i = 0; for(auto &[_, app_node] : apps_node) { proc::ctx_t ctx; @@ -382,7 +463,6 @@ std::optional parse(const std::string &file_name) { auto cmd = app_node.get_optional("cmd"s); auto image_path = app_node.get_optional("image-path"s); auto working_dir = app_node.get_optional("working-dir"s); - auto id = app_node.get_optional("id"s); std::vector prep_cmds; if(prep_nodes_opt) { @@ -428,14 +508,16 @@ std::optional parse(const std::string &file_name) { ctx.image_path = parse_env_val(this_env, *image_path); } - if(id) { - ctx.id = parse_env_val(this_env, *id); + auto possible_ids = calculate_app_id(name, ctx.image_path, i++); + if(ids.count(std::get<0>(possible_ids)) == 0) { + // Avoid using index to generate id if possible + ctx.id = std::get<0>(possible_ids); } else { - ctx.id = std::to_string(app_index); + // Fallback to include index on collision + ctx.id = std::get<1>(possible_ids); } - // Always increment index to avoid order shuffling in moonlight - app_index++; + ids.insert(ctx.id); ctx.name = std::move(name); ctx.prep_cmds = std::move(prep_cmds); diff --git a/src/process.h b/src/process.h index fa6ecc01..e4bd0170 100644 --- a/src/process.h +++ b/src/process.h @@ -97,10 +97,15 @@ private: file_t _pipe; std::vector::const_iterator _undo_it; std::vector::const_iterator _undo_begin; - - int app_index_from_id(int app_id); }; +/** + * Calculate a stable id based on name and image data + * @return tuple of id calculated without index (for use if no collision) and one with +*/ +std::tuple calculate_app_id(const std::string &app_name, std::string app_image_path, int index); + +std::string validate_app_image_path(std::string app_image_path); void refresh(const std::string &file_name); std::optional parse(const std::string &file_name); diff --git a/src/rand.h b/src/rand.h deleted file mode 100644 index 62273bae..00000000 --- a/src/rand.h +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef SUNSHINE_RAND_H -#define SUNSHINE_RAND_H - -#include - -namespace util { - -static int32_t generate_int32(std::default_random_engine &engine, int32_t min, int32_t max) { - std::uniform_int_distribution dist(min, max); - - return dist(engine); -} - -static int32_t generate_int32(int32_t min, int32_t max) { - std::random_device r; - - std::default_random_engine engine { r() }; - - return util::generate_int32(engine, min, max); -} - -} // namespace util -#endif // SUNSHINE_RAND_H