From e5674ca2c8a74c29e23417eab68558092af20a0c Mon Sep 17 00:00:00 2001 From: zortos293 <65777760+zortos293@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:37:07 +0000 Subject: [PATCH] Request WebRTC rigs with the CloudMatch browser identity Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- app/src/gfn/cloud_session.cpp | 30 +++--- app/src/gfn/cloud_session_internal.hpp | 5 + app/src/gfn/cloud_session_protocol.cpp | 6 +- scripts/test-streaming-host.sh | 2 + tests/cloud_session_identity_test.cpp | 140 +++++++++++++++++++++++++ 5 files changed, 165 insertions(+), 18 deletions(-) create mode 100644 tests/cloud_session_identity_test.cpp diff --git a/app/src/gfn/cloud_session.cpp b/app/src/gfn/cloud_session.cpp index a87f0d5..75091d9 100644 --- a/app/src/gfn/cloud_session.cpp +++ b/app/src/gfn/cloud_session.cpp @@ -73,9 +73,9 @@ SessionInfo GfnClient::StartSession(AuthSession& session, const std::string& lau "Content-Type: application/json", "nv-client-id: " + client_id_, "nv-browser-type: CHROME", - "nv-client-streamer: NVIDIA-CLASSIC", - "nv-client-type: NATIVE", - "nv-client-version: 2.0.80.173", + "nv-client-streamer: WEBRTC", + "nv-client-type: BROWSER", + "nv-client-version: " + std::string(kCloudMatchClientVersion), "nv-device-make: UNKNOWN", "nv-device-model: UNKNOWN", "nv-device-os: WINDOWS", @@ -129,14 +129,14 @@ SessionInfo GfnClient::StartSession(AuthSession& session, const std::string& lau AppendSessionTraceLog("START headers:\n" + HeadersForTrace(headers)); AppendSessionTraceLog("START request body:\n" + JsonForTrace(body)); - HttpResponse response = http_client_.Post(url, kUserAgent, headers, body); + HttpResponse response = http_client_.Post(url, kCloudMatchUserAgent, headers, body); if (response.status_code == 401) { session = ForceRefreshSavedSession(session); jwt_token = ResolveSessionJwt(session); headers[0] = "Authorization: GFNJWT " + jwt_token; AppendSessionTraceLog("START authorization rejected; refreshed token and retrying once"); - response = http_client_.Post(url, kUserAgent, headers, body); + response = http_client_.Post(url, kCloudMatchUserAgent, headers, body); } AppendSessionTraceLog("START response HTTP " + std::to_string(response.status_code)); AppendSessionTraceLog("START response body:\n" + JsonForTrace(response.body)); @@ -230,9 +230,9 @@ SessionInfo GfnClient::PollSession(AuthSession& session, const std::string& sess "Authorization: GFNJWT " + jwt_token, "nv-client-id: " + client_id_, "nv-browser-type: CHROME", - "nv-client-streamer: NVIDIA-CLASSIC", - "nv-client-type: NATIVE", - "nv-client-version: 2.0.80.173", + "nv-client-streamer: WEBRTC", + "nv-client-type: BROWSER", + "nv-client-version: " + std::string(kCloudMatchClientVersion), "nv-device-make: UNKNOWN", "nv-device-model: UNKNOWN", "nv-device-os: WINDOWS", @@ -243,14 +243,14 @@ SessionInfo GfnClient::PollSession(AuthSession& session, const std::string& sess AppendSessionTraceLog("POLL url=" + url); AppendSessionTraceLog("POLL headers:\n" + HeadersForTrace(headers)); - HttpResponse response = http_client_.Get(url, kUserAgent, headers); + HttpResponse response = http_client_.Get(url, kCloudMatchUserAgent, headers); if (response.status_code == 401) { session = ForceRefreshSavedSession(session); jwt_token = ResolveSessionJwt(session); headers[0] = "Authorization: GFNJWT " + jwt_token; AppendSessionTraceLog("POLL authorization rejected; refreshed token and retrying once"); - response = http_client_.Get(url, kUserAgent, headers); + response = http_client_.Get(url, kCloudMatchUserAgent, headers); } AppendSessionTraceLog("POLL response HTTP " + std::to_string(response.status_code)); AppendSessionTraceLog("POLL response body:\n" + JsonForTrace(response.body)); @@ -342,9 +342,9 @@ void GfnClient::StopSession(AuthSession& session, const std::string& session_id) "Authorization: GFNJWT " + jwt_token, "nv-client-id: " + client_id_, "nv-browser-type: CHROME", - "nv-client-streamer: NVIDIA-CLASSIC", - "nv-client-type: NATIVE", - "nv-client-version: 2.0.80.173", + "nv-client-streamer: WEBRTC", + "nv-client-type: BROWSER", + "nv-client-version: " + std::string(kCloudMatchClientVersion), "nv-device-make: UNKNOWN", "nv-device-model: UNKNOWN", "nv-device-os: WINDOWS", @@ -356,7 +356,7 @@ void GfnClient::StopSession(AuthSession& session, const std::string& session_id) AppendSessionTraceLog("STOP url=" + url); AppendSessionTraceLog("STOP headers:\n" + HeadersForTrace(headers)); HttpResponse response = http_client_.Request( - "DELETE", url, kUserAgent, headers); + "DELETE", url, kCloudMatchUserAgent, headers); if (response.status_code == 401) { session = ForceRefreshSavedSession(session); @@ -364,7 +364,7 @@ void GfnClient::StopSession(AuthSession& session, const std::string& session_id) headers[0] = "Authorization: GFNJWT " + jwt_token; AppendSessionTraceLog("STOP authorization rejected; refreshed token and retrying once"); response = http_client_.Request( - "DELETE", url, kUserAgent, headers); + "DELETE", url, kCloudMatchUserAgent, headers); } AppendSessionTraceLog("STOP response HTTP " + std::to_string(response.status_code)); AppendSessionTraceLog("STOP response body:\n" + JsonForTrace(response.body)); diff --git a/app/src/gfn/cloud_session_internal.hpp b/app/src/gfn/cloud_session_internal.hpp index f0db79b..d50387c 100644 --- a/app/src/gfn/cloud_session_internal.hpp +++ b/app/src/gfn/cloud_session_internal.hpp @@ -10,6 +10,11 @@ namespace opennow::gfn::cloud_session { +inline constexpr const char* kCloudMatchClientVersion = "2.0.89.154"; +inline constexpr const char* kCloudMatchUserAgent = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36"; + void ApplySessionNetworkInfo(json_t* session, SessionInfo& info); void CollectIceServersFromObject(json_t* object, SessionInfo& info); diff --git a/app/src/gfn/cloud_session_protocol.cpp b/app/src/gfn/cloud_session_protocol.cpp index 72efa28..b91bd96 100644 --- a/app/src/gfn/cloud_session_protocol.cpp +++ b/app/src/gfn/cloud_session_protocol.cpp @@ -327,7 +327,7 @@ std::string BuildSessionBody( json_object_set_new(req, "clientIdentification", json_string("GFN-PC")); json_object_set_new(req, "deviceHashId", json_string(device_id.c_str())); json_object_set_new(req, "clientVersion", json_string("30.0")); - json_object_set_new(req, "clientPlatformName", json_string("windows")); + json_object_set_new(req, "clientPlatformName", json_string("browser")); json_object_set_new(req, "availableSupportedControllers", json_array()); json_object_set_new(req, "sdkVersion", json_string("1.0")); @@ -434,13 +434,13 @@ std::string CreateNetworkTestSession( json_object_set_new(profile, "heightInPixels", json_integer(stream_settings.height)); json_object_set_new(profile, "framesPerSecond", json_integer(stream_settings.fps)); json_object_set_new( - request, "clientPlatformName", json_string("windows")); + request, "clientPlatformName", json_string("browser")); json_object_set_new(request, "netTestProfile", profile); json_object_set_new(root.get(), "netTestRequestData", request); const HttpResponse response = http_client.Post( streaming_base_url + "v2/nettestsession", - GfnClient::kUserAgent, + kCloudMatchUserAgent, headers, DumpJson(root.get())); if (response.status_code < 200 || response.status_code >= 300) diff --git a/scripts/test-streaming-host.sh b/scripts/test-streaming-host.sh index 97ba23e..e42bb06 100644 --- a/scripts/test-streaming-host.sh +++ b/scripts/test-streaming-host.sh @@ -96,6 +96,8 @@ bash tests/run_gl_renderer_reconfiguration_test.sh printf 'PASS gl_renderer_reconfiguration\n' run_cpp nvst_sdp app/src/webrtc/nvst_sdp.cpp run_cpp cloud_session_protocol "${sections[@]}" app/src/gfn/cloud_session_protocol.cpp app/src/gfn/shared.cpp -ljansson +run_cpp cloud_session_identity "${sections[@]}" app/src/gfn/cloud_session.cpp \ + app/src/gfn/cloud_session_protocol.cpp app/src/gfn/cloud_session_state.cpp app/src/gfn/shared.cpp -ljansson run_cpp stream_bitrate_pipeline "${sections[@]}" app/src/stream_settings.cpp app/src/localization.cpp \ app/src/gfn/cloud_session_protocol.cpp app/src/gfn/shared.cpp app/src/webrtc/nvst_sdp.cpp -ljansson run_c rtp_h264_assembly extern/libpeer/src/rtp.c diff --git a/tests/cloud_session_identity_test.cpp b/tests/cloud_session_identity_test.cpp new file mode 100644 index 0000000..ba9f07a --- /dev/null +++ b/tests/cloud_session_identity_test.cpp @@ -0,0 +1,140 @@ +#include "gfn/cloud_session_internal.hpp" + +#include +#include +#include +#include +#include + +namespace +{ +bool reject_next_request = false; +int request_count = 0; +int refresh_count = 0; +opennow::StreamSettings settings; +} + +namespace opennow +{ +bool StreamDiagnosticsEnabled() { return false; } + +StreamSettings LoadStreamSettings() { return settings; } + +AuthSession GfnClient::EnsureFreshSavedSession(const AuthSession& session) const +{ + return session; +} + +AuthSession GfnClient::ForceRefreshSavedSession(const AuthSession& session) const +{ + ++refresh_count; + auto refreshed = session; + refreshed.tokens.id_token = "synthetic-refreshed-" + std::to_string(refresh_count); + return refreshed; +} + +std::vector GfnClient::FetchStreamRegions(AuthSession&) const +{ + return {}; +} + +std::vector GfnClient::MeasureStreamRegionLatencies( + std::vector regions) const +{ + return regions; +} + +HttpResponse HttpClient::Request( + const std::string& method, const std::string& url, + const std::string& user_agent, const std::vector& headers, + const std::string& body, const std::string&, HttpTransferControl) const +{ + ++request_count; + assert(std::find(headers.begin(), headers.end(), "nv-client-type: BROWSER") != headers.end()); + assert(std::find(headers.begin(), headers.end(), "nv-client-streamer: WEBRTC") != headers.end()); + assert(std::find(headers.begin(), headers.end(), "nv-client-version: 2.0.89.154") != headers.end()); + assert(user_agent.find("Chrome/") != std::string::npos); + assert(user_agent.find("NVIDIACEFClient") == std::string::npos); + assert(user_agent.find("GFN-PC/") == std::string::npos); + const std::string token = refresh_count == 0 + ? "synthetic-initial" + : "synthetic-refreshed-" + std::to_string(refresh_count); + assert(std::find(headers.begin(), headers.end(), "Authorization: GFNJWT " + token) != headers.end()); + + const bool network_test = url.ends_with("v2/nettestsession"); + if (method == "POST") + { + const auto root = gfn::detail::LoadJson(body); + json_t* request = json_object_get( + root.get(), network_test ? "netTestRequestData" : "sessionRequestData"); + assert(gfn::detail::GetString(request, "clientPlatformName") == "browser"); + if (!network_test) + { + assert(gfn::detail::GetInteger(request, "appLaunchMode") == 2); + assert(gfn::detail::GetInteger(request, "remoteControllersBitmap") == 1); + assert(gfn::detail::GetString(request, "networkTestSessionId") == "synthetic-nettest"); + } + } + if (network_test) + return {200, R"({"requestStatus":{"statusCode":1},"netTestSession":{"sessionId":"synthetic-nettest"}})"}; + if (reject_next_request) + { + reject_next_request = false; + return {401, "{}"}; + } + return {200, R"({"requestStatus":{"statusCode":1},"session":{"sessionId":"synthetic-session","status":1}})"}; +} + +HttpResponse HttpClient::Get( + const std::string& url, const std::string& user_agent, + const std::vector& headers, const std::string& proxy, + HttpTransferControl control) const +{ + return Request("GET", url, user_agent, headers, {}, proxy, control); +} + +HttpResponse HttpClient::Post( + const std::string& url, const std::string& user_agent, + const std::vector& headers, const std::string& body, + const std::string& proxy, HttpTransferControl control) const +{ + return Request("POST", url, user_agent, headers, body, proxy, control); +} +} + +int main() +{ + const auto original_directory = std::filesystem::current_path(); + std::string pattern = (std::filesystem::temp_directory_path() / "opennow-cloudmatch-XXXXXX").string(); + const char* directory = mkdtemp(pattern.data()); + assert(directory); + std::filesystem::current_path(directory); + std::filesystem::create_directories(opennow::gfn::detail::GetAppHome()); + settings.region = "https://cloudmatch.example/"; + + for (const bool retry : {false, true}) + { + request_count = 0; + refresh_count = 0; + opennow::AuthSession session; + session.user.user_id = "synthetic-user"; + session.tokens.id_token = "synthetic-initial"; + session.provider.streaming_service_url = settings.region; + opennow::GfnClient client; + + reject_next_request = retry; + const auto started = client.StartSession(session, "123", settings); + assert(started.session_id == "synthetic-session"); + reject_next_request = retry; + const auto polled = client.PollSession(session, started.session_id); + assert(polled.session_id == started.session_id); + reject_next_request = retry; + client.StopSession(session, started.session_id); + assert(request_count == (retry ? 7 : 4)); + assert(refresh_count == (retry ? 3 : 0)); + assert(opennow::gfn::cloud_session::LoadActiveCloudSessionId(session).empty()); + } + + std::filesystem::current_path(original_directory); + std::filesystem::remove_all(directory); +}