diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake index 6424f7fd..afc7341a 100644 --- a/cmake/compile_definitions/windows.cmake +++ b/cmake/compile_definitions/windows.cmake @@ -70,23 +70,27 @@ set(OPENSSL_LIBRARIES libcrypto.a) list(PREPEND PLATFORM_LIBRARIES + ${CURL_STATIC_LIBRARIES} + avrt + d3d11 + D3DCompiler + dwmapi + dxgi + iphlpapi + ksuser + libssp.a libstdc++.a libwinpthread.a - libssp.a + minhook::minhook + nlohmann_json::nlohmann_json ntdll - ksuser - wsock32 - ws2_32 - d3d11 dxgi D3DCompiler setupapi - dwmapi - userenv - synchronization.lib - avrt - iphlpapi shlwapi - PkgConfig::NLOHMANN_JSON - ${CURL_STATIC_LIBRARIES}) + synchronization.lib + userenv + ws2_32 + wsock32 +) if(SUNSHINE_ENABLE_TRAY) list(APPEND PLATFORM_TARGET_FILES diff --git a/cmake/dependencies/common.cmake b/cmake/dependencies/common.cmake index 8d8eb88f..b6ca8447 100644 --- a/cmake/dependencies/common.cmake +++ b/cmake/dependencies/common.cmake @@ -28,7 +28,7 @@ include_directories(SYSTEM ${MINIUPNP_INCLUDE_DIRS}) # ffmpeg pre-compiled binaries if(NOT DEFINED FFMPEG_PREPARED_BINARIES) if(WIN32) - set(FFMPEG_PLATFORM_LIBRARIES mfplat ole32 strmiids mfuuid vpl MinHook) + set(FFMPEG_PLATFORM_LIBRARIES mfplat ole32 strmiids mfuuid vpl) elseif(UNIX AND NOT APPLE) set(FFMPEG_PLATFORM_LIBRARIES numa va va-drm va-x11 X11) endif() diff --git a/cmake/dependencies/windows.cmake b/cmake/dependencies/windows.cmake index 0563e567..11a40ecf 100644 --- a/cmake/dependencies/windows.cmake +++ b/cmake/dependencies/windows.cmake @@ -1,4 +1,12 @@ # windows specific dependencies # nlohmann_json -pkg_check_modules(NLOHMANN_JSON nlohmann_json REQUIRED IMPORTED_TARGET) +find_package(nlohmann_json CONFIG 3.11 REQUIRED) + +# Make sure MinHook is installed +find_library(MINHOOK_LIBRARY libMinHook.a REQUIRED) +find_path(MINHOOK_INCLUDE_DIR MinHook.h PATH_SUFFIXES include REQUIRED) + +add_library(minhook::minhook STATIC IMPORTED) +set_property(TARGET minhook::minhook PROPERTY IMPORTED_LOCATION ${MINHOOK_LIBRARY}) +target_include_directories(minhook::minhook INTERFACE ${MINHOOK_INCLUDE_DIR}) diff --git a/src/display_device.cpp b/src/display_device.cpp index f5d434dc..21c52647 100644 --- a/src/display_device.cpp +++ b/src/display_device.cpp @@ -674,11 +674,44 @@ namespace display_device { scheduler_option.m_execution = SchedulerOptions::Execution::ScheduledOnly; } - DD_DATA.sm_instance->schedule([try_once = (option == revert_option_e::try_once)](auto &settings_iface, auto &stop_token) { - // Here we want to keep retrying indefinitely until we succeed. - if (settings_iface.revertSettings() || try_once) { + DD_DATA.sm_instance->schedule([try_once = (option == revert_option_e::try_once), tried_out_devices = std::set {}](auto &settings_iface, auto &stop_token) mutable { + if (try_once) { + std::ignore = settings_iface.revertSettings(); stop_token.requestStop(); + return; } + + auto available_devices { [&settings_iface]() { + const auto devices { settings_iface.enumAvailableDevices() }; + std::set parsed_devices; + + std::transform( + std::begin(devices), std::end(devices), + std::inserter(parsed_devices, std::end(parsed_devices)), + [](const auto &device) { return device.m_device_id + " - " + device.m_friendly_name; }); + + return parsed_devices; + }() }; + if (available_devices == tried_out_devices) { + BOOST_LOG(debug) << "Skipping reverting configuration, because no newly added/removed devices were detected since last check. Currently available devices:\n" + << toJson(available_devices); + return; + } + + using enum SettingsManagerInterface::RevertResult; + if (const auto result { settings_iface.revertSettings() }; result == Ok) { + stop_token.requestStop(); + return; + } + else if (result == ApiTemporarilyUnavailable) { + // Do nothing and retry next time + return; + } + + // If we have failed to revert settings then we will try to do it next time only if a device was added/removed + BOOST_LOG(warning) << "Failed to revert display device configuration (will retry once devices are added or removed). Enabling all of the available devices:\n" + << toJson(available_devices); + tried_out_devices.swap(available_devices); }, scheduler_option); } diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 4731ef0d..864116cd 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -12,7 +12,6 @@ // lib includes #include -#include #include #include #include @@ -21,7 +20,6 @@ // local includes #include "config.h" -#include "crypto.h" #include "display_device.h" #include "file_handler.h" #include "globals.h" @@ -63,18 +61,6 @@ namespace nvhttp { static std::string otp_device_name; static std::chrono::time_point otp_creation_time; - class SunshineHTTPS: public SimpleWeb::HTTPS { - public: - SunshineHTTPS(boost::asio::io_context &io_context, boost::asio::ssl::context &ctx): - SimpleWeb::HTTPS(io_context, ctx) {} - - virtual ~SunshineHTTPS() { - // Gracefully shutdown the TLS connection - SimpleWeb::error_code ec; - shutdown(ec); - } - }; - class SunshineHTTPSServer: public SimpleWeb::ServerBase { public: SunshineHTTPSServer(const std::string &certification_file, const std::string &private_key_file): @@ -154,28 +140,6 @@ namespace nvhttp { std::string pkey; } conf_intern; - struct pair_session_t { - struct { - std::string uniqueID; - std::string cert; - std::string name; - } client; - - std::unique_ptr cipher_key; - std::vector clienthash; - - std::string serversecret; - std::string serverchallenge; - - struct { - util::Either< - std::shared_ptr::Response>, - std::shared_ptr::Response>> - response; - std::string salt; - } async_insert_pin; - }; - // uniqueID, session std::unordered_map map_id_sess; client_t client_root; @@ -334,7 +298,6 @@ namespace nvhttp { client_t &client = client_root; client.named_devices.push_back(named_cert_p); - #if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::update_tray_paired(named_cert_p->name); #endif @@ -406,12 +369,29 @@ namespace nvhttp { return launch_session; } + void + remove_session(const pair_session_t &sess) { + map_id_sess.erase(sess.client.uniqueID); + } + + void + fail_pair(pair_session_t &sess, pt::ptree &tree, const std::string status_msg) { + tree.put("root.paired", 0); + tree.put("root..status_code", 400); + tree.put("root..status_message", status_msg); + remove_session(sess); // Security measure, delete the session when something went wrong and force a re-pair + } + void getservercert(pair_session_t &sess, pt::ptree &tree, const std::string &pin) { + if (sess.last_phase != PAIR_PHASE::NONE) { + fail_pair(sess, tree, "Out of order call to getservercert"); + return; + } + sess.last_phase = PAIR_PHASE::GETSERVERCERT; + if (sess.async_insert_pin.salt.size() < 32) { - tree.put("root.paired", 0); - tree.put("root..status_code", 400); - tree.put("root..status_message", "Salt too short"); + fail_pair(sess, tree, "Salt too short"); return; } @@ -428,30 +408,17 @@ namespace nvhttp { } void - serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const args_t &args) { - auto encrypted_response = util::from_hex_vec(get_arg(args, "serverchallengeresp"), true); - - std::vector decrypted; - crypto::cipher::ecb_t cipher(*sess.cipher_key, false); - - cipher.decrypt(encrypted_response, decrypted); - - sess.clienthash = std::move(decrypted); - - auto serversecret = sess.serversecret; - auto sign = crypto::sign256(crypto::pkey(conf_intern.pkey), serversecret); - - serversecret.insert(std::end(serversecret), std::begin(sign), std::end(sign)); - - tree.put("root.pairingsecret", util::hex_vec(serversecret, true)); - tree.put("root.paired", 1); - tree.put("root..status_code", 200); - } - - void - clientchallenge(pair_session_t &sess, pt::ptree &tree, const args_t &args) { - auto challenge = util::from_hex_vec(get_arg(args, "clientchallenge"), true); + clientchallenge(pair_session_t &sess, pt::ptree &tree, const std::string &challenge) { + if (sess.last_phase != PAIR_PHASE::GETSERVERCERT) { + fail_pair(sess, tree, "Out of order call to clientchallenge"); + return; + } + sess.last_phase = PAIR_PHASE::CLIENTCHALLENGE; + if (!sess.cipher_key) { + fail_pair(sess, tree, "Cipher key not set"); + return; + } crypto::cipher::ecb_t cipher(*sess.cipher_key, false); std::vector decrypted; @@ -485,21 +452,58 @@ namespace nvhttp { } void - clientpairingsecret(pair_session_t &sess, pt::ptree &tree, const args_t &args) { - auto &client = sess.client; + serverchallengeresp(pair_session_t &sess, pt::ptree &tree, const std::string &encrypted_response) { + if (sess.last_phase != PAIR_PHASE::CLIENTCHALLENGE) { + fail_pair(sess, tree, "Out of order call to serverchallengeresp"); + return; + } + sess.last_phase = PAIR_PHASE::SERVERCHALLENGERESP; - auto pairingsecret = util::from_hex_vec(get_arg(args, "clientpairingsecret"), true); - if (pairingsecret.size() <= 16) { - tree.put("root.paired", 0); - tree.put("root..status_code", 400); - tree.put("root..status_message", "Clientpairingsecret too short"); + if (!sess.cipher_key || sess.serversecret.empty()) { + fail_pair(sess, tree, "Cipher key or serversecret not set"); return; } - std::string_view secret { pairingsecret.data(), 16 }; - std::string_view sign { pairingsecret.data() + secret.size(), pairingsecret.size() - secret.size() }; + std::vector decrypted; + crypto::cipher::ecb_t cipher(*sess.cipher_key, false); + + cipher.decrypt(encrypted_response, decrypted); + + sess.clienthash = std::move(decrypted); + + auto serversecret = sess.serversecret; + auto sign = crypto::sign256(crypto::pkey(conf_intern.pkey), serversecret); + + serversecret.insert(std::end(serversecret), std::begin(sign), std::end(sign)); + + tree.put("root.pairingsecret", util::hex_vec(serversecret, true)); + tree.put("root.paired", 1); + tree.put("root..status_code", 200); + } + + void + clientpairingsecret(pair_session_t &sess, pt::ptree &tree, const std::string &client_pairing_secret) { + if (sess.last_phase != PAIR_PHASE::SERVERCHALLENGERESP) { + fail_pair(sess, tree, "Out of order call to clientpairingsecret"); + return; + } + sess.last_phase = PAIR_PHASE::CLIENTPAIRINGSECRET; + + auto &client = sess.client; + + if (client_pairing_secret.size() <= 16) { + fail_pair(sess, tree, "Client pairing secret too short"); + return; + } + + std::string_view secret { client_pairing_secret.data(), 16 }; + std::string_view sign { client_pairing_secret.data() + secret.size(), client_pairing_secret.size() - secret.size() }; auto x509 = crypto::x509(client.cert); + if (!x509) { + fail_pair(sess, tree, "Invalid client certificate"); + return; + } auto x509_sign = crypto::signature(x509); std::string data; @@ -512,7 +516,9 @@ namespace nvhttp { auto hash = crypto::hash(data); // if hash not correct, probably MITM - if (!std::memcmp(hash.data(), sess.clienthash.data(), hash.size()) && crypto::verify256(crypto::x509(client.cert), secret, sign)) { + bool same_hash = hash.size() == sess.clienthash.size() && std::equal(hash.begin(), hash.end(), sess.clienthash.begin()); + auto verify = crypto::verify256(crypto::x509(client.cert), secret, sign); + if (same_hash && verify) { tree.put("root.paired", 1); auto named_cert_p = std::make_shared(); @@ -536,10 +542,10 @@ namespace nvhttp { add_authorized_client(named_cert_p); } else { - map_id_sess.erase(client.uniqueID); tree.put("root.paired", 0); } + remove_session(sess); tree.put("root..status_code", 200); } @@ -628,7 +634,6 @@ namespace nvhttp { } auto uniqID { get_arg(args, "uniqueid") }; - auto sess_it = map_id_sess.find(uniqID); args_t::const_iterator it; if (it = args.find("phrase"); it != std::end(args)) { @@ -702,16 +707,29 @@ namespace nvhttp { else if (it->second == "pairchallenge"sv) { tree.put("root.paired", 1); tree.put("root..status_code", 200); + return; } } - else if (it = args.find("clientchallenge"); it != std::end(args)) { - clientchallenge(sess_it->second, tree, args); + + auto sess_it = map_id_sess.find(uniqID); + if (sess_it == std::end(map_id_sess)) { + tree.put("root..status_code", 400); + tree.put("root..status_message", "Invalid uniqueid"); + + return; + } + + if (it = args.find("clientchallenge"); it != std::end(args)) { + auto challenge = util::from_hex_vec(it->second, true); + clientchallenge(sess_it->second, tree, challenge); } else if (it = args.find("serverchallengeresp"); it != std::end(args)) { - serverchallengeresp(sess_it->second, tree, args); + auto encrypted_response = util::from_hex_vec(it->second, true); + serverchallengeresp(sess_it->second, tree, encrypted_response); } else if (it = args.find("clientpairingsecret"); it != std::end(args)) { - clientpairingsecret(sess_it->second, tree, args); + auto pairingsecret = util::from_hex_vec(it->second, true); + clientpairingsecret(sess_it->second, tree, pairingsecret); } else { tree.put("root..status_code", 404); @@ -1352,6 +1370,12 @@ namespace nvhttp { return; } + void + setup(const std::string &pkey, const std::string &cert) { + conf_intern.pkey = pkey; + conf_intern.servercert = cert; + } + void start() { auto shutdown_event = mail::man->event(mail::shutdown); @@ -1366,8 +1390,9 @@ namespace nvhttp { load_state(); } - conf_intern.pkey = file_handler::read_file(config::nvhttp.pkey.c_str()); - conf_intern.servercert = file_handler::read_file(config::nvhttp.cert.c_str()); + auto pkey = file_handler::read_file(config::nvhttp.pkey.c_str()); + auto cert = file_handler::read_file(config::nvhttp.cert.c_str()); + setup(pkey, cert); // resume doesn't always get the parameter "localAudioPlayMode" // launch will store it in host_audio diff --git a/src/nvhttp.h b/src/nvhttp.h index 45b8b1a6..b81c2a5e 100644 --- a/src/nvhttp.h +++ b/src/nvhttp.h @@ -66,6 +66,123 @@ namespace nvhttp { std::shared_ptr make_launch_session(bool host_audio, int appid, const args_t &args, const crypto::named_cert_t* named_cert_p); + /** + * @brief Setup the nvhttp server. + * @param pkey + * @param cert + */ + void + setup(const std::string &pkey, const std::string &cert); + + class SunshineHTTPS: public SimpleWeb::HTTPS { + public: + SunshineHTTPS(boost::asio::io_context &io_context, boost::asio::ssl::context &ctx): + SimpleWeb::HTTPS(io_context, ctx) {} + + virtual ~SunshineHTTPS() { + // Gracefully shutdown the TLS connection + SimpleWeb::error_code ec; + shutdown(ec); + } + }; + + enum class PAIR_PHASE { + NONE, ///< Sunshine is not in a pairing phase + GETSERVERCERT, ///< Sunshine is in the get server certificate phase + CLIENTCHALLENGE, ///< Sunshine is in the client challenge phase + SERVERCHALLENGERESP, ///< Sunshine is in the server challenge response phase + CLIENTPAIRINGSECRET ///< Sunshine is in the client pairing secret phase + }; + + struct pair_session_t { + struct { + std::string uniqueID = {}; + std::string cert = {}; + std::string name = {}; + } client; + + std::unique_ptr cipher_key = {}; + std::vector clienthash = {}; + + std::string serversecret = {}; + std::string serverchallenge = {}; + + struct { + util::Either< + std::shared_ptr::Response>, + std::shared_ptr::Response>> + response; + std::string salt = {}; + } async_insert_pin; + + /** + * @brief used as a security measure to prevent out of order calls + */ + PAIR_PHASE last_phase = PAIR_PHASE::NONE; + }; + + /** + * @brief removes the temporary pairing session + * @param sess + */ + void + remove_session(const pair_session_t &sess); + + /** + * @brief Pair, phase 1 + * + * Moonlight will send a salt and client certificate, we'll also need the user provided pin. + * + * PIN and SALT will be used to derive a shared AES key that needs to be stored + * in order to be used to decrypt_symmetric in the next phases. + * + * At this stage we only have to send back our public certificate. + */ + void + getservercert(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &pin); + + /** + * @brief Pair, phase 2 + * + * Using the AES key that we generated in phase 1 we have to decrypt the client challenge, + * + * We generate a SHA256 hash with the following: + * - Decrypted challenge + * - Server certificate signature + * - Server secret: a randomly generated secret + * + * The hash + server_challenge will then be AES encrypted and sent as the `challengeresponse` in the returned XML + */ + void + clientchallenge(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &challenge); + + /** + * @brief Pair, phase 3 + * + * Moonlight will send back a `serverchallengeresp`: an AES encrypted client hash, + * we have to send back the `pairingsecret`: + * using our private key we have to sign the certificate_signature + server_secret (generated in phase 2) + */ + void + serverchallengeresp(pair_session_t &sess, boost::property_tree::ptree &tree, const std::string &encrypted_response); + + /** + * @brief Pair, phase 4 (final) + * + * We now have to use everything we exchanged before in order to verify and finally pair the clients + * + * We'll check the client_hash obtained at phase 3, it should contain the following: + * - The original server_challenge + * - The signature of the X509 client_cert + * - The unencrypted client_pairing_secret + * We'll check that SHA256(server_challenge + client_public_cert_signature + client_secret) == client_hash + * + * Then using the client certificate public key we should be able to verify that + * the client secret has been signed by Moonlight + */ + void + clientpairingsecret(pair_session_t &sess, std::shared_ptr> &add_cert, boost::property_tree::ptree &tree, const std::string &client_pairing_secret); + /** * @brief Compare the user supplied pin to the Moonlight pin. * @param pin The user supplied pin. diff --git a/src/system_tray.cpp b/src/system_tray.cpp index 49f1f42e..d90904b9 100644 --- a/src/system_tray.cpp +++ b/src/system_tray.cpp @@ -41,6 +41,7 @@ // local includes #include "config.h" #include "confighttp.h" + #include "display_device.h" #include "logging.h" #include "platform/common.h" #include "process.h" @@ -66,6 +67,13 @@ namespace system_tray { proc::proc.terminate(); } + void + tray_reset_display_device_config_cb(struct tray_menu *item) { + BOOST_LOG(info) << "Resetting display device config from system tray"sv; + + std::ignore = display_device::reset_persistence(); + } + void tray_restart_cb(struct tray_menu *item) { BOOST_LOG(info) << "Restarting from system tray"sv; @@ -109,6 +117,10 @@ namespace system_tray { // { .text = nullptr } } }, // { .text = "-" }, { .text = TRAY_MSG_NO_APP_RUNNING, .cb = tray_force_stop_cb }, + // Currently display device settings are only supported on Windows + #ifdef _WIN32 + { .text = "Reset Display Device Config", .cb = tray_reset_display_device_config_cb }, + #endif { .text = "Restart", .cb = tray_restart_cb }, { .text = "Quit", .cb = tray_quit_cb }, { .text = nullptr } }, diff --git a/src/system_tray.h b/src/system_tray.h index fae59f05..ec9ba4de 100644 --- a/src/system_tray.h +++ b/src/system_tray.h @@ -19,6 +19,13 @@ namespace system_tray { void tray_force_stop_cb(struct tray_menu *item); + /** + * @brief Callback for resetting display device configuration. + * @param item The tray menu item. + */ + void + tray_reset_display_device_config_cb(struct tray_menu *item); + /** * @brief Callback for restarting Sunshine from the system tray. * @param item The tray menu item. diff --git a/src_assets/common/assets/web/Checkbox.vue b/src_assets/common/assets/web/Checkbox.vue index b94446d3..03da60d0 100644 --- a/src_assets/common/assets/web/Checkbox.vue +++ b/src_assets/common/assets/web/Checkbox.vue @@ -22,6 +22,10 @@ const props = defineProps({ type: String, default: "missing-prefix" }, + inverseValues: { + type: Boolean, + default: false, + }, default: { type: undefined, default: null, @@ -79,7 +83,9 @@ const checkboxValues = (() => { return ["true", "false"]; })(); - return { truthy: mappedValues[0], falsy: mappedValues[1] }; + const truthyIndex = props.inverseValues ? 1 : 0; + const falsyIndex = props.inverseValues ? 0 : 1; + return { truthy: mappedValues[truthyIndex], falsy: mappedValues[falsyIndex] }; })(); const parsedDefaultPropValue = (() => { const boolValues = mapToBoolRepresentation(props.default); diff --git a/src_assets/common/assets/web/apps.html b/src_assets/common/assets/web/apps.html index 5b95544c..3db9177c 100644 --- a/src_assets/common/assets/web/apps.html +++ b/src_assets/common/assets/web/apps.html @@ -157,6 +157,7 @@ desc="apps.global_prep_desc" v-model="editForm['exclude-global-prep-cmd']" default="true" + inverse-values >
diff --git a/src_assets/common/assets/web/public/assets/locale/fr.json b/src_assets/common/assets/web/public/assets/locale/fr.json index 02cc2cb7..b0ef27e9 100644 --- a/src_assets/common/assets/web/public/assets/locale/fr.json +++ b/src_assets/common/assets/web/public/assets/locale/fr.json @@ -51,12 +51,12 @@ "env_app_name": "Nom de l'application", "env_client_audio_config": "La configuration audio demandée par le client (2.0/5.1/7.1)", "env_client_enable_sops": "Le client a activé l'option pour optimiser le jeu pour une diffusion optimale (true/false)", - "env_client_fps": "FPS demandé par le client (int)", - "env_client_gcmap": "Le masque de manette demandé, au format bitset/bitfield (int)", + "env_client_fps": "FPS demandé par le client (entier)", + "env_client_gcmap": "Le masque de manette demandé, au format bitset/bitfield (entier)", "env_client_hdr": "Le HDR est activé par le client (true/false)", - "env_client_height": "La hauteur demandée par le client (int)", + "env_client_height": "La hauteur demandée par le client (entier)", "env_client_host_audio": "Le client a activé l'audio côté audio (true/false)", - "env_client_width": "La largeur demandée par le client (int)", + "env_client_width": "La largeur demandée par le client (entier)", "env_displayplacer_example": "Exemple - displayplacer pour l'automatisation de la résolution :", "env_qres_example": "Exemple - QRes pour l'automatisation de la résolution :", "env_qres_path": "chemin de qres", @@ -112,8 +112,8 @@ "amd_rc_cqp": "cqp -- mode constant qp", "amd_rc_desc": "Ceci contrôle la méthode de contrôle du débit pour s'assurer que nous ne dépassons pas la cible du bitrate client. 'cqp' n'est pas adapté pour le ciblage de débit, et d'autres options en plus de 'vbr_latency' dépendent de HRD Enforcement pour aider à limiter les débordements de débit.", "amd_rc_group": "Réglages de contrôle du débit AMF", - "amd_rc_vbr_latency": "vbr_latency -- Débit variable limité de latence (par défaut)", - "amd_rc_vbr_peak": "vbr_peak -- Débit variable contraint par le pic", + "amd_rc_vbr_latency": "vbr_latency -- débit variable limité de latence (par défaut)", + "amd_rc_vbr_peak": "vbr_peak -- débit variable contraint par le pic", "amd_usage": "Utilisation de l'AMF", "amd_usage_desc": "Définit le profil d'encodage de base. Toutes les options présentées ci-dessous remplaceront un sous-ensemble du profil d'utilisation, mais il y a d'autres paramètres cachés qui ne peuvent pas être configurés ailleurs.", "amd_usage_lowlatency": "lowlatency - faible latence (rapide)", @@ -145,7 +145,7 @@ "channels": "Nombre maximum de clients connectés", "channels_desc_1": "Apollo peut permettre à une seule session de streaming d'être partagée simultanément avec plusieurs clients.", "channels_desc_2": "Certains encodeurs matériels peuvent avoir des limitations qui réduisent les performances avec plusieurs flux.", - "coder_cabac": "cabac -- Contexte de codage arithmétique binaire adaptatif - qualité supérieure", + "coder_cabac": "cabac -- contexte de codage arithmétique binaire adaptatif - qualité supérieure", "coder_cavlc": "cavlc -- codage de la durée adaptative du contexte - décodage plus rapide", "configuration": "Configuration", "controller": "Activer l'entrée manette", @@ -205,7 +205,7 @@ "file_state_desc": "Le fichier où l'état actuel de Apollo est stocké", "fps": "FPS annoncés", "gamepad": "Type de manette émulée", - "gamepad_auto": "Options de la sélection automatique", + "gamepad_auto": "Options de sélection automatique", "gamepad_desc": "Choisissez le type de manette à émuler sur l'hôte", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds4_manual": "Options de sélection DS4", @@ -434,14 +434,14 @@ "restart_apollo_desc": "Si Apollo ne fonctionne pas correctement, vous pouvez essayer de le redémarrer. Cela mettra fin à toutes les sessions en cours.", "restart_apollo_success": "Apollo redémarre", "troubleshooting": "Dépannage", - "unpair_all": "Désappairer tous les appareils", + "unpair_all": "Dissocier tous les périphériques", "unpair_all_error": "Erreur lors de la dissociation", "unpair_all_success": "Désappairage réussi.", - "unpair_desc": "Retirer vos appareils appariés. Les appareils individuellement non appariés avec une session active resteront connectés, mais ne peuvent pas démarrer ou reprendre une session.", + "unpair_desc": "Supprimez vos périphériques appairés. Les périphériques dissociés individuellement avec une session active resteront connectés, mais ne pourront pas démarrer ou reprendre une session.", "unpair_single_no_devices": "Il n'y a aucun appareil associé.", "unpair_single_success": "Cependant, le(s) appareil(s) peuvent toujours être dans une session active. Utilisez le bouton 'Forcer la fermeture' ci-dessus pour mettre fin à toute session ouverte.", "unpair_single_unknown": "Client inconnu", - "unpair_title": "Désappairer les appareils" + "unpair_title": "Dissocier les périphériques" }, "welcome": { "confirm_password": "Confirmation du mot de passe", diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 927c12b7..0381ea36 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -45,6 +45,10 @@ file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS set(SUNSHINE_SOURCES ${SUNSHINE_TARGET_FILES}) +# copy fixtures to build directory +file(COPY ${CMAKE_SOURCE_DIR}/tests/fixtures/unit + DESTINATION ${CMAKE_BINARY_DIR}/tests/fixtures) + # remove main.cpp from the list of sources list(REMOVE_ITEM SUNSHINE_SOURCES ${CMAKE_SOURCE_DIR}/src/main.cpp) diff --git a/tests/fixtures/unit/pairing_test_key.pem b/tests/fixtures/unit/pairing_test_key.pem new file mode 100644 index 00000000..1c6e2ffe --- /dev/null +++ b/tests/fixtures/unit/pairing_test_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDLePNlWN06FLlM +ujWzIX8UICO7SWfH5DXlafVjpxwi/WCkdO6FxixqRNGu71wMvJXFbDlNR8fqX2xo ++eq17J3uFKn+qdjmP3L38bkqxhoJ/nCrXkeGyCTQ+Daug63ZYSJeW2Mmf+LAR5/i +/fWYfXpSlbcf5XJQPEWvENpLqWu+NOU50dJXIEVYpUXRx2+x4ZbwkH7tVJm94L+C +OUyiJKQPyWgU2aFsyJGwHFfePfSUpfYHqbHZV/ILpY59VJairBwE99bx/mBvMI7a +hBmJTSDuDffJcPDhFF5kZa0UkQPrPvhXcQaSRti7v0VonEQj8pTSnGYr9ktWKk92 +wxDyn9S3AgMBAAECggEAbEhQ14WELg2rUz7hpxPTaiV0fo4hEcrMN+u8sKzVF3Xa +QYsNCNoe9urq3/r39LtDxU3D7PGfXYYszmz50Jk8ruAGW8WN7XKkv3i/fxjv8JOc +6EYDMKJAnYkKqLLhCQddX/Oof2udg5BacVWPpvhX6a1NSEc2H6cDupfwZEWkVhMi +bCC3JcNmjFa8N7ow1/5VQiYVTjpxfV7GY1GRe7vMvBucdQKH3tUG5PYXKXytXw/j +KDLaECiYVT89KbApkI0zhy7I5g3LRq0Rs5fmYLCjVebbuAL1W5CJHFJeFOgMKvnO +QSl7MfHkTnzTzUqwkwXjgNMGsTosV4UloL9gXVF6GQKBgQD5fI771WETkpaKjWBe +6XUVSS98IOAPbTGpb8CIhSjzCuztNAJ+0ey1zklQHonMFbdmcWTkTJoF3ECqAos9 +vxB4ROg+TdqGDcRrXa7Twtmhv66QvYxttkaK3CqoLX8CCTnjgXBCijo6sCpo6H1T ++y55bBDpxZjNFT5BV3+YPBfWQwKBgQDQyNt+saTqJqxGYV7zWQtOqKORRHAjaJpy +m5035pky5wORsaxQY8HxbsTIQp9jBSw3SQHLHN/NAXDl2k7VAw/axMc+lj9eW+3z +2Hv5LVgj37jnJYEpYwehvtR0B4jZnXLyLwShoBdRPkGlC5fs9+oWjQZoDwMLZfTg +eZVOJm6SfQKBgQDfxYcB/kuKIKsCLvhHaSJpKzF6JoqRi6FFlkScrsMh66TCxSmP +0n58O0Cqqhlyge/z5LVXyBVGOF2Pn6SAh4UgOr4MVAwyvNp2aprKuTQ2zhSnIjx4 +k0sGdZ+VJOmMS/YuRwUHya+cwDHp0s3Gq77tja5F38PD/s/OD8sUIqJGvQKBgBfI +6ghy4GC0ayfRa+m5GSqq14dzDntaLU4lIDIAGS/NVYDBhunZk3yXq99Mh6/WJQVf +Uc77yRsnsN7ekeB+as33YONmZm2vd1oyLV1jpwjfMcdTZHV8jKAGh1l4ikSQRUoF +xTdMb5uXxg6xVWtvisFq63HrU+N2iAESmMnAYxRZAoGAVEFJRRjPrSIUTCCKRiTE +br+cHqy6S5iYRxGl9riKySBKeU16fqUACIvUqmqlx4Secj3/Hn/VzYEzkxcSPwGi +qMgdS0R+tacca7NopUYaaluneKYdS++DNlT/m+KVHqLynQr54z1qBlThg9KGrpmM +LGZkXtQpx6sX7v3Kq56PkNk= +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/tests/fixtures/unit/pairing_test_public.cert b/tests/fixtures/unit/pairing_test_public.cert new file mode 100644 index 00000000..1350105d --- /dev/null +++ b/tests/fixtures/unit/pairing_test_public.cert @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC6zCCAdOgAwIBAgIBATANBgkqhkiG9w0BAQsFADA5MQswCQYDVQQGEwJJVDEW +MBQGA1UECgwNR2FtZXNPbldoYWxlczESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIy +MDQwOTA5MTYwNVoXDTQyMDQwNDA5MTYwNVowOTELMAkGA1UEBhMCSVQxFjAUBgNV +BAoMDUdhbWVzT25XaGFsZXMxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMt482VY3ToUuUy6NbMhfxQgI7tJZ8fkNeVp +9WOnHCL9YKR07oXGLGpE0a7vXAy8lcVsOU1Hx+pfbGj56rXsne4Uqf6p2OY/cvfx +uSrGGgn+cKteR4bIJND4Nq6DrdlhIl5bYyZ/4sBHn+L99Zh9elKVtx/lclA8Ra8Q +2kupa7405TnR0lcgRVilRdHHb7HhlvCQfu1Umb3gv4I5TKIkpA/JaBTZoWzIkbAc +V9499JSl9gepsdlX8guljn1UlqKsHAT31vH+YG8wjtqEGYlNIO4N98lw8OEUXmRl +rRSRA+s++FdxBpJG2Lu/RWicRCPylNKcZiv2S1YqT3bDEPKf1LcCAwEAATANBgkq +hkiG9w0BAQsFAAOCAQEAqPBqzvDjl89pZMll3Ge8RS7HeDuzgocrhOcT2jnk4ag7 +/TROZuISjDp6+SnL3gPEt7E2OcFAczTg3l/wbT5PFb6vM96saLm4EP0zmLfK1FnM +JDRahKutP9rx6RO5OHqsUB+b4jA4W0L9UnXUoLKbjig501AUix0p52FBxu+HJ90r +HlLs3Vo6nj4Z/PZXrzaz8dtQ/KJMpd/g/9xlo6BKAnRk5SI8KLhO4hW6zG0QA56j +X4wnh1bwdiidqpcgyuKossLOPxbS786WmsesaAWPnpoY6M8aija+ALwNNuWWmyMg +9SVDV76xJzM36Uq7Kg3QJYTlY04WmPIdJHkCtXWf9g== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/tests/unit/test_http_pairing.cpp b/tests/unit/test_http_pairing.cpp new file mode 100644 index 00000000..7b355e04 --- /dev/null +++ b/tests/unit/test_http_pairing.cpp @@ -0,0 +1,210 @@ +/** + * @file tests/unit/test_http_pairing.cpp + * @brief Test src/nvhttp.cpp HTTP pairing process + */ + +#include + +#include "../tests_common.h" +#include "src/file_handler.h" + +using namespace nvhttp; + +struct pairing_input { + std::shared_ptr session; + /** + * Normally server challenge is generated by the server, but for testing purposes + * we can override it with a custom value. This way the process is deterministic. + */ + std::string override_server_challenge; + std::string pin; + std::string client_challenge; + std::string server_challenge_resp; + std::string client_pairing_secret; +}; + +struct pairing_output { + bool phase_1_success; + bool phase_2_success; + bool phase_3_success; + bool phase_4_success; +}; + +const auto PRIVATE_KEY = file_handler::read_file("fixtures/unit/pairing_test_key.pem"); +const auto PUBLIC_CERT = file_handler::read_file("fixtures/unit/pairing_test_public.cert"); + +struct PairingTest: testing::TestWithParam> {}; + +TEST_P(PairingTest, Run) { + auto [input, expected] = GetParam(); + + boost::property_tree::ptree tree; + + setup(PRIVATE_KEY, PUBLIC_CERT); + + // phase 1 + getservercert(*input.session, tree, input.pin); + ASSERT_EQ(tree.get("root.paired") == 1, expected.phase_1_success); + if (!expected.phase_1_success) { + return; + } + + // phase 2 + clientchallenge(*input.session, tree, input.client_challenge); + ASSERT_EQ(tree.get("root.paired") == 1, expected.phase_2_success); + if (!expected.phase_2_success) { + return; + } + + // phase 3 + serverchallengeresp(*input.session, tree, input.server_challenge_resp); + ASSERT_EQ(tree.get("root.paired") == 1, expected.phase_3_success); + if (!expected.phase_3_success) { + return; + } + input.session->serverchallenge = input.override_server_challenge; + + // phase 4 + auto input_client_cert = input.session->client.cert; // Will be moved + auto add_cert = std::make_shared>(30); + clientpairingsecret(*input.session, add_cert, tree, input.client_pairing_secret); + ASSERT_EQ(tree.get("root.paired") == 1, expected.phase_4_success); + + // Check that we actually added the input client certificate to `add_cert` + if (expected.phase_4_success) { + ASSERT_EQ(add_cert->peek(), true); + auto cert = add_cert->pop(); + char added_subject_name[256]; + X509_NAME_oneline(X509_get_subject_name(cert.get()), added_subject_name, sizeof(added_subject_name)); + + auto input_cert = crypto::x509(input_client_cert); + char original_suject_name[256]; + X509_NAME_oneline(X509_get_subject_name(input_cert.get()), original_suject_name, sizeof(original_suject_name)); + + ASSERT_EQ(std::string(added_subject_name), std::string(original_suject_name)); + } +} + +INSTANTIATE_TEST_SUITE_P( + TestWorkingPairing, + PairingTest, + testing::Values( + std::make_tuple( + pairing_input { + .session = std::make_shared( + pair_session_t { + .client = { + .uniqueID = "1234", + .cert = PUBLIC_CERT, + .name = "test" }, + .async_insert_pin = { .salt = "ff5dc6eda99339a8a0793e216c4257c4" } }), + .override_server_challenge = util::from_hex_vec("AAAAAAAAAAAAAAAA", true), + .pin = "5338", + /* AES("CLIENT CHALLENGE") */ + .client_challenge = util::from_hex_vec("741CD3D6890C16DA39D53BCA0893AAF0", true), + /* SHA = SHA265(server_challenge + public cert signature + "SECRET ") = "6493DAE49C913E1AEAF37C1072F71D664B72B2C4DA1FFB4720BECE0D929E008A" + * AES( SHA ) */ + .server_challenge_resp = util::from_hex_vec("920BABAE9F7599AA1CA8EC87FB3454C91872A7D8D5127DDC176C2FDAE635CF7A", true), + /* secret + x509 signature */ + .client_pairing_secret = util::from_hex_vec("000102030405060708090A0B0C0D0EFF" // secret + "9BB74D8DE2FF006C3F47FC45EFDAA97D433783AFAB3ACD85CA7ED2330BB2A7BD18A5B044AF8CAC177116FAE8A6E8E44653A8944A0F8EA138B2E013756D847D2C4FC52F736E2E7E9B4154712B18F8307B2A161E010F0587744163E42ECA9EA548FC435756EDCF1FEB94037631ABB72B29DDAC0EA5E61F2DBFCC3B20AA021473CC85AC98D88052CA6618ED1701EFBF142C18D5E779A3155B84DF65057D4823EC194E6DF14006793E8D7A3DCCE20A911636C4E01ECA8B54B9DE9F256F15DE9A980EA024B30D77579140D45EC220C738164BDEEEBF7364AE94A5FF9B784B40F2E640CE8603017DEEAC7B2AD77B807C643B7B349C110FE15F94C7B3D37FF15FDFBE26", + true) }, + pairing_output { true, true, true, true }), + // Testing that when passing some empty values we aren't triggering any exception + std::make_tuple(pairing_input { + .session = std::make_shared(pair_session_t { .client = {}, .async_insert_pin = { .salt = "ff5dc6eda99339a8a0793e216c4257c4" } }), + .override_server_challenge = {}, + .pin = {}, + .client_challenge = {}, + .server_challenge_resp = {}, + .client_pairing_secret = util::from_hex_vec("000102030405060708090A0B0C0D0EFFxDEADBEEF", true), + }, + // Only phase 4 will fail, when we check what has been exchanged + pairing_output { true, true, true, false }), + // Testing that when passing some empty values we aren't triggering any exception + std::make_tuple(pairing_input { + .session = std::make_shared(pair_session_t { .client = { .cert = PUBLIC_CERT }, .async_insert_pin = { .salt = "ff5dc6eda99339a8a0793e216c4257c4" } }), + .override_server_challenge = {}, + .pin = {}, + .client_challenge = {}, + .server_challenge_resp = {}, + .client_pairing_secret = util::from_hex_vec("000102030405060708090A0B0C0D0EFFxDEADBEEF", true), + }, + // Only phase 4 will fail, when we check what has been exchanged + pairing_output { true, true, true, false }))); + +INSTANTIATE_TEST_SUITE_P( + TestFailingPairing, + PairingTest, + testing::Values( + /** + * Wrong PIN + */ + std::make_tuple( + pairing_input { + .session = std::make_shared( + pair_session_t { + .client = { + .uniqueID = "1234", + .cert = PUBLIC_CERT, + .name = "test" }, + .async_insert_pin = { .salt = "ff5dc6eda99339a8a0793e216c4257c4" } }), + .override_server_challenge = util::from_hex_vec("AAAAAAAAAAAAAAAA", true), + .pin = "0000", + .client_challenge = util::from_hex_vec("741CD3D6890C16DA39D53BCA0893AAF0", true), + .server_challenge_resp = util::from_hex_vec("920BABAE9F7599AA1CA8EC87FB3454C91872A7D8D5127DDC176C2FDAE635CF7A", true), + .client_pairing_secret = util::from_hex_vec("000102030405060708090A0B0C0D0EFF" // secret + "9BB74D8DE2FF006C3F47FC45EFDAA97D433783AFAB3ACD85CA7ED2330BB2A7BD18A5B044AF8CAC177116FAE8A6E8E44653A8944A0F8EA138B2E013756D847D2C4FC52F736E2E7E9B4154712B18F8307B2A161E010F0587744163E42ECA9EA548FC435756EDCF1FEB94037631ABB72B29DDAC0EA5E61F2DBFCC3B20AA021473CC85AC98D88052CA6618ED1701EFBF142C18D5E779A3155B84DF65057D4823EC194E6DF14006793E8D7A3DCCE20A911636C4E01ECA8B54B9DE9F256F15DE9A980EA024B30D77579140D45EC220C738164BDEEEBF7364AE94A5FF9B784B40F2E640CE8603017DEEAC7B2AD77B807C643B7B349C110FE15F94C7B3D37FF15FDFBE26", + true) }, + pairing_output { true, true, true, false }), + /** + * Wrong client challenge + */ + std::make_tuple(pairing_input { .session = std::make_shared(pair_session_t { .client = { .uniqueID = "1234", .cert = PUBLIC_CERT, .name = "test" }, .async_insert_pin = { .salt = "ff5dc6eda99339a8a0793e216c4257c4" } }), .override_server_challenge = util::from_hex_vec("AAAAAAAAAAAAAAAA", true), .pin = "5338", .client_challenge = util::from_hex_vec("741CD3D6890C16DA39D53BCA0893AAF0", true), .server_challenge_resp = util::from_hex_vec("WRONG", true), + .client_pairing_secret = util::from_hex_vec("000102030405060708090A0B0C0D0EFF" // secret + "9BB74D8DE2FF006C3F47FC45EFDAA97D433783AFAB3ACD85CA7ED2330BB2A7BD18A5B044AF8CAC177116FAE8A6E8E44653A8944A0F8EA138B2E013756D847D2C4FC52F736E2E7E9B4154712B18F8307B2A161E010F0587744163E42ECA9EA548FC435756EDCF1FEB94037631ABB72B29DDAC0EA5E61F2DBFCC3B20AA021473CC85AC98D88052CA6618ED1701EFBF142C18D5E779A3155B84DF65057D4823EC194E6DF14006793E8D7A3DCCE20A911636C4E01ECA8B54B9DE9F256F15DE9A980EA024B30D77579140D45EC220C738164BDEEEBF7364AE94A5FF9B784B40F2E640CE8603017DEEAC7B2AD77B807C643B7B349C110FE15F94C7B3D37FF15FDFBE26", + true) }, + pairing_output { true, true, true, false }), + /** + * Wrong signature + */ + std::make_tuple(pairing_input { .session = std::make_shared(pair_session_t { .client = { .uniqueID = "1234", .cert = PUBLIC_CERT, .name = "test" }, .async_insert_pin = { .salt = "ff5dc6eda99339a8a0793e216c4257c4" } }), .override_server_challenge = util::from_hex_vec("AAAAAAAAAAAAAAAA", true), .pin = "5338", .client_challenge = util::from_hex_vec("741CD3D6890C16DA39D53BCA0893AAF0", true), .server_challenge_resp = util::from_hex_vec("920BABAE9F7599AA1CA8EC87FB3454C91872A7D8D5127DDC176C2FDAE635CF7A", true), + .client_pairing_secret = util::from_hex_vec("000102030405060708090A0B0C0D0EFF" // secret + "NOSIGNATURE", // Wrong signature + true) }, + pairing_output { true, true, true, false }), + /** + * null values (phase 1) + */ + std::make_tuple(pairing_input { .session = std::make_shared() }, pairing_output { false }), + /** + * null values (phase 4, phase 2 and 3 have no reason to fail since we are running them in order) + */ + std::make_tuple(pairing_input { .session = std::make_shared(pair_session_t { .async_insert_pin = { .salt = "ff5dc6eda99339a8a0793e216c4257c4" } }) }, pairing_output { true, true, true, false }))); + +TEST(PairingTest, OutOfOrderCalls) { + boost::property_tree::ptree tree; + + setup(PRIVATE_KEY, PUBLIC_CERT); + + pair_session_t sess {}; + + clientchallenge(sess, tree, "test"); + ASSERT_FALSE(tree.get("root.paired") == 1); + + serverchallengeresp(sess, tree, "test"); + ASSERT_FALSE(tree.get("root.paired") == 1); + + auto add_cert = std::make_shared>(30); + clientpairingsecret(sess, add_cert, tree, "test"); + ASSERT_FALSE(tree.get("root.paired") == 1); + + // This should work, it's the first time we call it + sess.async_insert_pin.salt = "ff5dc6eda99339a8a0793e216c4257c4"; + getservercert(sess, tree, "test"); + ASSERT_TRUE(tree.get("root.paired") == 1); + + // Calling it again should fail + getservercert(sess, tree, "test"); + ASSERT_FALSE(tree.get("root.paired") == 1); +} diff --git a/third-party/libdisplaydevice b/third-party/libdisplaydevice index 2c431bce..63599b07 160000 --- a/third-party/libdisplaydevice +++ b/third-party/libdisplaydevice @@ -1 +1 @@ -Subproject commit 2c431bce2981ebb4b6c116c7e11372a045596b70 +Subproject commit 63599b07659a5d1dd554a24bd0c8e96b21e21112