From 53b9be1001320b9f1f386d917e968528cec2d901 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 15 Jan 2025 08:25:10 -0500 Subject: [PATCH 01/10] build(cmake): check for MinHook during configure (#3533) Co-authored-by: Lukas Senionis <22381748+FrogTheFrog@users.noreply.github.com> --- cmake/compile_definitions/windows.cmake | 28 ++++++++++++++----------- cmake/dependencies/common.cmake | 2 +- cmake/dependencies/windows.cmake | 10 ++++++++- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake index 7643d1d9..3ee287e0 100644 --- a/cmake/compile_definitions/windows.cmake +++ b/cmake/compile_definitions/windows.cmake @@ -64,23 +64,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 66053dc0..27da728b 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..8d7e32b3 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 minhook REQUIRED) +find_path(MINHOOK_INCLUDE_DIR MinHook.h PATH_SUFFIXES include REQUIRED) + +add_library(minhook::minhook UNKNOWN IMPORTED) +set_property(TARGET minhook::minhook PROPERTY IMPORTED_LOCATION ${MINHOOK_LIBRARY}) +target_include_directories(minhook::minhook INTERFACE ${MINHOOK_INCLUDE_DIR}) From f4d937c0a617048517f27ea0b07609d4e7ac1cd4 Mon Sep 17 00:00:00 2001 From: LizardByte-bot <108553330+LizardByte-bot@users.noreply.github.com> Date: Wed, 15 Jan 2025 09:26:20 -0500 Subject: [PATCH 02/10] chore: update global workflows (#3536) --- .github/workflows/release-notifier.yml | 192 +++++++++++++------------ 1 file changed, 104 insertions(+), 88 deletions(-) diff --git a/.github/workflows/release-notifier.yml b/.github/workflows/release-notifier.yml index aeb33ed2..16b471d7 100644 --- a/.github/workflows/release-notifier.yml +++ b/.github/workflows/release-notifier.yml @@ -3,7 +3,7 @@ # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in # the above-mentioned repo. -# Send release notification to various platforms. +# Create a blog post for a new release and open a PR to the blog repo name: Release Notifications @@ -13,99 +13,115 @@ on: - released # this triggers when a release is published, but does not include pre-releases or drafts jobs: - simplified_changelog: + update-blog: if: >- - startsWith(github.repository, 'LizardByte/') && - !github.event.release.prerelease && - !github.event.release.draft - outputs: - SIMPLIFIED_BODY: ${{ steps.output.outputs.SIMPLIFIED_BODY }} + github.repository_owner == 'LizardByte' runs-on: ubuntu-latest steps: - - name: remove contributors section + - name: Check topics env: - RELEASE_BODY: ${{ github.event.release.body }} - id: output + TOPIC: replicator-release-notifications + id: check-label + uses: actions/github-script@v7 + with: + script: | + const topic = process.env.TOPIC; + console.log(`Checking if repo has topic: ${topic}`); + + const repoTopics = await github.rest.repos.getAllTopics({ + owner: context.repo.owner, + repo: context.repo.repo + }); + console.log(`Repo topics: ${repoTopics.data.names}`); + + const hasTopic = repoTopics.data.names.includes(topic); + console.log(`Has topic: ${hasTopic}`); + + core.setOutput('hasTopic', hasTopic); + + - name: Check if latest GitHub release + id: check-release + if: >- + steps.check-label.outputs.hasTopic == 'true' + uses: actions/github-script@v7 + with: + script: | + const latestRelease = await github.rest.repos.getLatestRelease({ + owner: context.repo.owner, + repo: context.repo.repo + }); + + core.setOutput('isLatestRelease', latestRelease.data.tag_name === context.payload.release.tag_name); + + - name: Checkout blog + if: >- + steps.check-label.outputs.hasTopic == 'true' && + steps.check-release.outputs.isLatestRelease == 'true' + uses: actions/checkout@v4 + with: + repository: "LizardByte/LizardByte.github.io" + + - name: Create blog post + if: >- + steps.check-label.outputs.hasTopic == 'true' && + steps.check-release.outputs.isLatestRelease == 'true' run: | - echo "${RELEASE_BODY}" > ./release_body.md - modified_body=$(sed '/^---/,$d' ./release_body.md) - echo "modified_body: ${modified_body}" + # setup variables + tag_name="${{ github.event.release.tag_name }}" + semver="${tag_name#v}" + repo_lower="$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]')" + file_name="_posts/releases/${repo_lower}/${semver//./-}.md" + mkdir -p "$(dirname "${file_name}")" - # use a heredoc to ensure the output is multiline - echo "SIMPLIFIED_BODY<> $GITHUB_OUTPUT - echo "${modified_body}" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + # create jekyll blog post + echo "---" > "${file_name}" + echo "layout: release" >> "${file_name}" + echo "title: ${{ github.event.repository.name }} ${tag_name} Released" >> "${file_name}" + echo "gh-repo: ${{ github.repository }}" >> "${file_name}" + echo "gh-badge: [follow, fork, star]" >> "${file_name}" + echo "tags: [release, ${repo_lower}]" >> "${file_name}" + echo "comments: true" >> "${file_name}" + echo "author: LizardByte-bot" >> "${file_name}" + echo "---" >> "${file_name}" + echo "" >> "${file_name}" - discord: - if: >- - startsWith(github.repository, 'LizardByte/') && - !github.event.release.prerelease && - !github.event.release.draft - needs: simplified_changelog - runs-on: ubuntu-latest - steps: - - name: discord - uses: sarisia/actions-status-discord@v1 + release_body=$(cat <> "${file_name}" + + - name: Create/Update Pull Request + id: create-pr + if: >- + steps.check-label.outputs.hasTopic == 'true' && + steps.check-release.outputs.isLatestRelease == 'true' + uses: peter-evans/create-pull-request@v7 with: - avatar_url: ${{ secrets.ORG_LOGO_URL }} - color: 0x00ff00 - description: ${{ needs.simplified_changelog.outputs.SIMPLIFIED_BODY }} - nodetail: true - nofail: false - title: ${{ github.event.repository.name }} ${{ github.ref_name }} Released - url: ${{ github.event.release.html_url }} - username: ${{ secrets.DISCORD_USERNAME }} - webhook: ${{ secrets.DISCORD_RELEASE_WEBHOOK }} + token: ${{ secrets.GH_BOT_TOKEN }} + commit-message: | + chore: Add blog post for ${{ github.event.repository.name }} release ${{ github.event.release.tag_name }} + branch: bot/add-${{ github.event.repository.name }}-${{ github.event.release.tag_name }} + delete-branch: true + title: | + chore: Add blog post for ${{ github.event.repository.name }} release ${{ github.event.release.tag_name }} + body: ${{ github.event.release.body }} + labels: + blog - facebook_page: - if: >- - startsWith(github.repository, 'LizardByte/') && - !github.event.release.prerelease && - !github.event.release.draft - runs-on: ubuntu-latest - steps: - - name: facebook-post-action - uses: LizardByte/facebook-post-action@v2024.1207.15428 - with: - page_id: ${{ secrets.FACEBOOK_PAGE_ID }} - access_token: ${{ secrets.FACEBOOK_ACCESS_TOKEN }} - message: | - ${{ github.event.repository.name }} ${{ github.ref_name }} Released - url: ${{ github.event.release.html_url }} - - reddit: - if: >- - startsWith(github.repository, 'LizardByte/') && - !github.event.release.prerelease && - !github.event.release.draft - needs: simplified_changelog - runs-on: ubuntu-latest - steps: - - name: reddit - uses: bluwy/release-for-reddit-action@v2 - with: - username: ${{ secrets.REDDIT_USERNAME }} - password: ${{ secrets.REDDIT_PASSWORD }} - app-id: ${{ secrets.REDDIT_CLIENT_ID }} - app-secret: ${{ secrets.REDDIT_CLIENT_SECRET }} - subreddit: ${{ secrets.REDDIT_SUBREDDIT }} - title: ${{ github.event.repository.name }} ${{ github.ref_name }} Released - url: ${{ github.event.release.html_url }} - flair-id: ${{ secrets.REDDIT_FLAIR_ID }} # https://www.reddit.com/r/>/api/link_flair.json - comment: ${{ needs.simplified_changelog.outputs.SIMPLIFIED_BODY }} - - x: - if: >- - startsWith(github.repository, 'LizardByte/') && - !github.event.release.prerelease && - !github.event.release.draft - runs-on: ubuntu-latest - steps: - - name: x - uses: nearform-actions/github-action-notify-twitter@v1 - with: - message: ${{ github.event.release.html_url }} - twitter-app-key: ${{ secrets.X_APP_KEY }} - twitter-app-secret: ${{ secrets.X_APP_SECRET }} - twitter-access-token: ${{ secrets.X_ACCESS_TOKEN }} - twitter-access-token-secret: ${{ secrets.X_ACCESS_TOKEN_SECRET }} + - name: Automerge PR + env: + GH_TOKEN: ${{ secrets.GH_BOT_TOKEN }} + if: >- + steps.check-label.outputs.hasTopic == 'true' && + steps.check-release.outputs.isLatestRelease == 'true' + run: | + gh \ + pr \ + merge \ + --auto \ + --delete-branch \ + --repo "LizardByte/LizardByte.github.io" \ + --squash \ + "${{ steps.create-pr.outputs.pull-request-number }}" From d4052dc72c9531f10f3915245c8ee9334e6f7b2d Mon Sep 17 00:00:00 2001 From: LizardByte-bot <108553330+LizardByte-bot@users.noreply.github.com> Date: Wed, 15 Jan 2025 10:43:12 -0500 Subject: [PATCH 03/10] chore(l10n): update translations (#3535) --- .../assets/web/public/assets/locale/fr.json | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) 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 7b00cec9..1466c0d6 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": "Sunshine 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", @@ -204,7 +204,7 @@ "file_state": "Fichier des données", "file_state_desc": "Le fichier où l'état actuel de Sunshine est stocké", "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", @@ -431,14 +431,14 @@ "restart_sunshine_desc": "Si Sunshine ne fonctionne pas correctement, vous pouvez essayer de le redémarrer. Cela mettra fin à toutes les sessions en cours.", "restart_sunshine_success": "Sunshine 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", From 7f0351d125a506adf23b6b85ad5cd3d8663a1257 Mon Sep 17 00:00:00 2001 From: Lukas Senionis Date: Wed, 15 Jan 2025 21:00:29 +0200 Subject: [PATCH 04/10] fix(cmake/windows): static link MinHook (#3537) --- cmake/dependencies/windows.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/dependencies/windows.cmake b/cmake/dependencies/windows.cmake index 8d7e32b3..11a40ecf 100644 --- a/cmake/dependencies/windows.cmake +++ b/cmake/dependencies/windows.cmake @@ -4,9 +4,9 @@ find_package(nlohmann_json CONFIG 3.11 REQUIRED) # Make sure MinHook is installed -find_library(MINHOOK_LIBRARY minhook REQUIRED) +find_library(MINHOOK_LIBRARY libMinHook.a REQUIRED) find_path(MINHOOK_INCLUDE_DIR MinHook.h PATH_SUFFIXES include REQUIRED) -add_library(minhook::minhook UNKNOWN IMPORTED) +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}) From fb557df270e58d353817d723a059e733f4b92846 Mon Sep 17 00:00:00 2001 From: Lukas Senionis Date: Fri, 17 Jan 2025 03:00:37 +0200 Subject: [PATCH 05/10] feat(display): retry reverting configuration only if device was added or removed (#3539) --- src/display_device.cpp | 39 +++++++++++++++++++++++++++++++++--- third-party/libdisplaydevice | 2 +- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/display_device.cpp b/src/display_device.cpp index e337e9a8..fc273fcb 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/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 From 1c2d7ec830d8dde13653aa53be995e76f95d5be9 Mon Sep 17 00:00:00 2001 From: Lukas Senionis Date: Fri, 17 Jan 2025 15:24:49 +0200 Subject: [PATCH 06/10] fix(checkbox): inverse global prep values for apps (#3547) --- src_assets/common/assets/web/Checkbox.vue | 8 +++++++- src_assets/common/assets/web/apps.html | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) 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 a0237aab..de59d30e 100644 --- a/src_assets/common/assets/web/apps.html +++ b/src_assets/common/assets/web/apps.html @@ -122,6 +122,7 @@ desc="apps.global_prep_desc" v-model="editForm['exclude-global-prep-cmd']" default="true" + inverse-values >
From bc22cca59bb64ef78eb626b7bbede8583157f5f0 Mon Sep 17 00:00:00 2001 From: Lukas Senionis Date: Fri, 17 Jan 2025 18:45:50 +0200 Subject: [PATCH 07/10] feat(tray): add button to reset display device settings on Windows (#3546) --- src/system_tray.cpp | 12 ++++++++++++ src/system_tray.h | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/src/system_tray.cpp b/src/system_tray.cpp index 95edb67d..1709d8c9 100644 --- a/src/system_tray.cpp +++ b/src/system_tray.cpp @@ -37,6 +37,7 @@ // local includes #include "confighttp.h" + #include "display_device.h" #include "logging.h" #include "platform/common.h" #include "process.h" @@ -70,6 +71,13 @@ namespace system_tray { platf::open_url("https://www.paypal.com/paypalme/ReenigneArcher"); } + 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; @@ -110,6 +118,10 @@ namespace system_tray { { .text = "PayPal", .cb = tray_donate_paypal_cb }, { .text = nullptr } } }, { .text = "-" }, + // 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 d027fb45..8d716c65 100644 --- a/src/system_tray.h +++ b/src/system_tray.h @@ -36,6 +36,13 @@ namespace system_tray { void tray_donate_paypal_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. From 9d3a3826c744654a285cf93f86665d71efa11a43 Mon Sep 17 00:00:00 2001 From: LizardByte-bot <108553330+LizardByte-bot@users.noreply.github.com> Date: Fri, 17 Jan 2025 20:01:50 -0500 Subject: [PATCH 08/10] chore: update global workflows (#3551) --- .github/workflows/release-notifier.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-notifier.yml b/.github/workflows/release-notifier.yml index 16b471d7..bc7556ce 100644 --- a/.github/workflows/release-notifier.yml +++ b/.github/workflows/release-notifier.yml @@ -70,7 +70,17 @@ jobs: tag_name="${{ github.event.release.tag_name }}" semver="${tag_name#v}" repo_lower="$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]')" - file_name="_posts/releases/${repo_lower}/${semver//./-}.md" + + # extract year, month, and day + year="${semver%%.*}" + month_day="${semver#*.}" + month_day="${month_day%%.*}" + + # ensure month_day is 4 digits + month_day=$(printf "%04d" "$month_day") + + # create the filename + file_name="_posts/releases/${year}-${month_day:0:2}-${month_day:2:2}-v${semver}.md" mkdir -p "$(dirname "${file_name}")" # create jekyll blog post From 80fa04c33028f41e8d20854bbb84ca94b7dfd2ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jan 2025 23:07:15 -0500 Subject: [PATCH 09/10] build(deps): bump packaging/linux/flatpak/deps/shared-modules from `f1ad050` to `f5d368a` (#3549) build(deps): bump packaging/linux/flatpak/deps/shared-modules Bumps [packaging/linux/flatpak/deps/shared-modules](https://github.com/flathub/shared-modules) from `f1ad050` to `f5d368a`. - [Commits](https://github.com/flathub/shared-modules/compare/f1ad0508ead33d03b0c51d9318d3c3b63341056e...f5d368a31d6ef046eb2955c74ec6f54f32ed5c4e) --- updated-dependencies: - dependency-name: packaging/linux/flatpak/deps/shared-modules dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- packaging/linux/flatpak/deps/shared-modules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/linux/flatpak/deps/shared-modules b/packaging/linux/flatpak/deps/shared-modules index f1ad0508..f5d368a3 160000 --- a/packaging/linux/flatpak/deps/shared-modules +++ b/packaging/linux/flatpak/deps/shared-modules @@ -1 +1 @@ -Subproject commit f1ad0508ead33d03b0c51d9318d3c3b63341056e +Subproject commit f5d368a31d6ef046eb2955c74ec6f54f32ed5c4e From 89f097ae65277d42b5d40163d09d92e412e6d7dd Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Sat, 18 Jan 2025 04:17:13 +0000 Subject: [PATCH 10/10] Merge commit from fork Co-authored-by: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Co-authored-by: Cameron Gutman <2695644+cgutman@users.noreply.github.com> --- src/nvhttp.cpp | 188 +++++++++-------- src/nvhttp.h | 119 +++++++++++ tests/CMakeLists.txt | 4 + tests/fixtures/unit/pairing_test_key.pem | 28 +++ tests/fixtures/unit/pairing_test_public.cert | 18 ++ tests/unit/test_http_pairing.cpp | 210 +++++++++++++++++++ 6 files changed, 485 insertions(+), 82 deletions(-) create mode 100644 tests/fixtures/unit/pairing_test_key.pem create mode 100644 tests/fixtures/unit/pairing_test_public.cert create mode 100644 tests/unit/test_http_pairing.cpp diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index e510d083..83505310 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -11,7 +11,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" @@ -45,18 +43,6 @@ namespace nvhttp { crypto::cert_chain_t cert_chain; - 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): @@ -146,28 +132,6 @@ namespace nvhttp { std::vector named_devices; }; - 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; @@ -367,12 +331,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; } @@ -389,30 +370,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; @@ -446,21 +414,58 @@ namespace nvhttp { } void - clientpairingsecret(std::shared_ptr> &add_cert, 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, std::shared_ptr> &add_cert, 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; @@ -473,20 +478,20 @@ 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); add_cert->raise(crypto::x509(client.cert)); // The client is now successfully paired and will be authorized to connect - auto it = map_id_sess.find(client.uniqueID); add_authorized_client(client.name, std::move(client.cert)); - map_id_sess.erase(it); } else { - map_id_sess.erase(client.uniqueID); tree.put("root.paired", 0); } + remove_session(sess); tree.put("root..status_code", 200); } @@ -568,7 +573,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)) { @@ -603,16 +607,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(add_cert, sess_it->second, tree, args); + auto pairingsecret = util::from_hex_vec(it->second, true); + clientpairingsecret(sess_it->second, add_cert, tree, pairingsecret); } else { tree.put("root..status_code", 404); @@ -1030,6 +1047,12 @@ namespace nvhttp { response->close_connection_after_response = true; } + 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); @@ -1044,8 +1067,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); auto add_cert = std::make_shared>(30); diff --git a/src/nvhttp.h b/src/nvhttp.h index 1f8726c3..e3af8a26 100644 --- a/src/nvhttp.h +++ b/src/nvhttp.h @@ -9,9 +9,11 @@ #include // lib includes +#include #include // local includes +#include "crypto.h" #include "thread_safe.h" /** @@ -50,6 +52,123 @@ namespace nvhttp { void start(); + /** + * @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/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); +}