Generated app id with hashed input data (#715)

This commit is contained in:
Brad Richardson 2023-01-07 09:42:40 -05:00 committed by GitHub
parent effa98f76a
commit 9b6d0b7a06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 132 additions and 83 deletions

View File

@ -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

View File

@ -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<std::string> 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<std::int32_t>::max());
while(ids.count(std::to_string(id)) > 0) {
id = util::generate_int32(1, std::numeric_limits<std::int32_t>::max());
}
inputTree.put("id", id);
if(inputTree.get_child("prep-cmd").empty()) {
inputTree.erase("prep-cmd");
}

View File

@ -9,11 +9,16 @@
#include <vector>
#include <boost/algorithm/string.hpp>
#include <boost/crc.hpp>
#include <boost/filesystem.hpp>
#include <boost/program_options/parsers.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include <openssl/evp.h>
#include <openssl/sha.h>
#include "crypto.h"
#include "main.h"
#include "platform/common.h"
#include "utility.h"
@ -23,6 +28,8 @@
#include <share.h>
#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<ctx_t> &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<std::string> 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<std::string, std::string> 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<std::string> 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<proc::proc_t> parse(const std::string &file_name) {
pt::ptree tree;
@ -370,8 +450,9 @@ std::optional<proc::proc_t> parse(const std::string &file_name) {
this_env[name] = parse_env_val(this_env, val.get_value<std::string>());
}
int app_index = 1; // Start at 1, 0 indicates no app running
std::set<std::string> ids;
std::vector<proc::ctx_t> apps;
int i = 0;
for(auto &[_, app_node] : apps_node) {
proc::ctx_t ctx;
@ -382,7 +463,6 @@ std::optional<proc::proc_t> parse(const std::string &file_name) {
auto cmd = app_node.get_optional<std::string>("cmd"s);
auto image_path = app_node.get_optional<std::string>("image-path"s);
auto working_dir = app_node.get_optional<std::string>("working-dir"s);
auto id = app_node.get_optional<std::string>("id"s);
std::vector<proc::cmd_t> prep_cmds;
if(prep_nodes_opt) {
@ -428,14 +508,16 @@ std::optional<proc::proc_t> 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);

View File

@ -97,10 +97,15 @@ private:
file_t _pipe;
std::vector<cmd_t>::const_iterator _undo_it;
std::vector<cmd_t>::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<std::string, std::string> 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<proc::proc_t> parse(const std::string &file_name);

View File

@ -1,23 +0,0 @@
#ifndef SUNSHINE_RAND_H
#define SUNSHINE_RAND_H
#include <random>
namespace util {
static int32_t generate_int32(std::default_random_engine &engine, int32_t min, int32_t max) {
std::uniform_int_distribution<std::int32_t> 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