From 0f070b144f0307d6da4a2f20848def31144a7f62 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 11 Feb 2026 14:39:37 -0700 Subject: [PATCH] Fix client UUID resolution from SSL peer certificate --- src/nvhttp.cpp | 57 +++++++++++++++++++++++++++++++++++ src/stream.cpp | 6 ++-- third-party/Simple-Web-Server | 2 +- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index e624bcb3..dff43e07 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -279,6 +279,53 @@ namespace nvhttp { } } + /** + * @brief Resolve the real paired client UUID from the SSL peer certificate. + * @param request The HTTPS request whose peer cert identifies the client. + * @return The paired client's UUID, or empty string if not found. + * + * Moonlight sends a placeholder uniqueid ("0123456789ABCDEF") in the launch + * query string. The real identity is the SSL client certificate presented + * during the TLS handshake, which we match against our paired device list. + */ + std::string resolve_client_uuid(req_https_t request) { + auto conn = request->connection_shared(); + if (!conn) { + BOOST_LOG(warning) << "resolve_client_uuid: connection expired"sv; + return {}; + } + + auto ssl = conn->socket->native_handle(); + if (!ssl) { + BOOST_LOG(warning) << "resolve_client_uuid: no SSL handle"sv; + return {}; + } + + crypto::x509_t peer_cert { +#if OPENSSL_VERSION_MAJOR >= 3 + SSL_get1_peer_certificate(ssl) +#else + SSL_get_peer_certificate(ssl) +#endif + }; + if (!peer_cert) { + BOOST_LOG(warning) << "resolve_client_uuid: no peer certificate"sv; + return {}; + } + + auto peer_pem = crypto::pem(peer_cert); + + for (const auto &device : client_root.named_devices) { + if (device.cert == peer_pem) { + BOOST_LOG(info) << "resolve_client_uuid: matched paired client '"sv << device.name << "' uuid="sv << device.uuid; + return device.uuid; + } + } + + BOOST_LOG(warning) << "resolve_client_uuid: peer cert did not match any paired device"sv; + return {}; + } + std::shared_ptr make_launch_session(bool host_audio, const args_t &args) { auto launch_session = std::make_shared(); @@ -868,6 +915,11 @@ namespace nvhttp { host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); auto launch_session = make_launch_session(host_audio, args); + auto resolved_uuid = resolve_client_uuid(request); + if (!resolved_uuid.empty()) { + launch_session->unique_id = std::move(resolved_uuid); + } + if (rtsp_stream::session_count() == 0) { // The display should be restored in case something fails as there are no other sessions. revert_display_configuration = true; @@ -976,6 +1028,11 @@ namespace nvhttp { } const auto launch_session = make_launch_session(host_audio, args); + auto resolved_uuid = resolve_client_uuid(request); + if (!resolved_uuid.empty()) { + launch_session->unique_id = std::move(resolved_uuid); + } + if (no_active_sessions) { // We want to prepare display only if there are no active sessions at // the moment. This should be done before probing encoders as it could diff --git a/src/stream.cpp b/src/stream.cpp index 4c6c7e9b..e9a3bf9d 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -1972,9 +1972,9 @@ namespace stream { {"keyboard_active", kb_ago <= KB_ACTIVE_WINDOW_MS}, {"mouse_active", mouse_ago <= MOUSE_ACTIVE_WINDOW_MS}, {"gamepad_active", gp_ago <= GAMEPAD_ACTIVE_WINDOW_MS}, - {"last_keyboard_ms_ago", last_kb > 0 ? kb_ago : -1}, - {"last_mouse_ms_ago", last_mouse > 0 ? mouse_ago : -1}, - {"last_gamepad_ms_ago", last_gp > 0 ? gp_ago : -1}, + {"last_keyboard_ms_ago", last_kb > 0 ? (int64_t) kb_ago : (int64_t) -1}, + {"last_mouse_ms_ago", last_mouse > 0 ? (int64_t) mouse_ago : (int64_t) -1}, + {"last_gamepad_ms_ago", last_gp > 0 ? (int64_t) gp_ago : (int64_t) -1}, }; result.push_back(std::move(entry)); diff --git a/third-party/Simple-Web-Server b/third-party/Simple-Web-Server index 546895a9..99c1f621 160000 --- a/third-party/Simple-Web-Server +++ b/third-party/Simple-Web-Server @@ -1 +1 @@ -Subproject commit 546895a93a29062bb178367b46c7afb72da9881e +Subproject commit 99c1f621ebd8d119c5d2dc3a88ecf255058acec0