From 8db2bfa652cfdb512d550ad1521d72b5f5e69a34 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Fri, 7 Oct 2022 02:06:21 -0700 Subject: [PATCH 1/4] Set User-Agent and Origin in OP filter --- vnext/Shared/Networking/OriginPolicyHttpFilter.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp index f39e9e8d8a0..22ec4047a71 100644 --- a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp +++ b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp @@ -761,6 +761,17 @@ ResponseOperation OriginPolicyHttpFilter::SendRequestAsync(HttpRequestMessage co ValidatePreflightResponse(coRequest, preflightResponse); } + // Set User-Agent after validating request and preflight. + auto userAgent = GetRuntimeOptionString("Http.UserAgent"); + if (userAgent.size() > 0) { + coRequest.Headers().Append(L"User-Agent", to_hstring(userAgent)); + } + + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + originPolicy == OriginPolicy::CrossOriginResourceSharing) { + coRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); + } + auto response = co_await m_innerFilter.SendRequestAsync(coRequest); ValidateResponse(response, originPolicy); From e6a288860c4ea5997dfa5050c7f5db954a0f50bb Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Fri, 7 Oct 2022 02:07:49 -0700 Subject: [PATCH 2/4] Fix Shared filters --- vnext/Shared/Shared.vcxitems.filters | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/vnext/Shared/Shared.vcxitems.filters b/vnext/Shared/Shared.vcxitems.filters index efe5b56f835..20828ca25fd 100644 --- a/vnext/Shared/Shared.vcxitems.filters +++ b/vnext/Shared/Shared.vcxitems.filters @@ -152,6 +152,9 @@ Source Files\Networking + + Source Files\Modules + @@ -457,6 +460,15 @@ Header Files\Networking + + Header Files\Modules + + + Header Files\Modules + + + Header Files\Modules + From bc9d6331b695956e9efab7007accd7d0123282b9 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Fri, 7 Oct 2022 02:08:35 -0700 Subject: [PATCH 3/4] Change files --- ...ative-windows-a912b218-3e03-4b76-8b54-d9cd603a29c0.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/react-native-windows-a912b218-3e03-4b76-8b54-d9cd603a29c0.json diff --git a/change/react-native-windows-a912b218-3e03-4b76-8b54-d9cd603a29c0.json b/change/react-native-windows-a912b218-3e03-4b76-8b54-d9cd603a29c0.json new file mode 100644 index 00000000000..db12e10e94e --- /dev/null +++ b/change/react-native-windows-a912b218-3e03-4b76-8b54-d9cd603a29c0.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Set User-Agent header in Origin Policy filter", + "packageName": "react-native-windows", + "email": "julio.rocha@microsoft.com", + "dependentChangeType": "patch" +} From cc9a1334201bc79f8f79161b38344d19a78c5c06 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Fri, 7 Oct 2022 15:57:23 -0700 Subject: [PATCH 4/4] Set the implicit User-Agent in RedirectHttpFilter --- .../HttpResourceIntegrationTests.cpp | 120 +++++++++++++++++- .../Networking/OriginPolicyHttpFilter.cpp | 13 -- .../Shared/Networking/RedirectHttpFilter.cpp | 9 ++ 3 files changed, 128 insertions(+), 14 deletions(-) diff --git a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp index 88c3d921c88..dbe74138144 100644 --- a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp +++ b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -40,7 +41,10 @@ TEST_CLASS (HttpResourceIntegrationTest) { static uint16_t s_port; TEST_METHOD_CLEANUP(MethodCleanup) { - // Bug in WebSocketServer does not correctly release TCP port between test methods. + // Clear any runtime options that may be used by tests in this class. + MicrosoftReactSetRuntimeOptionString("Http.UserAgent", nullptr); + + // Bug in test HTTP server does not correctly release TCP port between test methods. // Using a different por per test for now. s_port++; } @@ -164,6 +168,120 @@ TEST_CLASS (HttpResourceIntegrationTest) { } } + TEST_METHOD(RequestGetExplicitUserAgentSucceeds) { + string url = "https://api.github.com/repos/microsoft/react-native-xaml"; + + promise rcPromise; + string error; + IHttpResource::Response response; + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { + response = callbackResponse; + rcPromise.set_value(); + }); + resource->SetOnError([&rcPromise, &error](int64_t, string &&message, bool) { + error = std::move(message); + rcPromise.set_value(); + }); + + //clang-format off + resource->SendRequest( + "GET", + std::move(url), + 0, /*requestId*/ + {{"User-Agent", "React Native Windows"}}, + {}, /*data*/ + "text", /*responseType*/ + false, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {}); + //clang-format on + + rcPromise.get_future().wait(); + + Assert::AreEqual({}, error, L"Error encountered"); + Assert::AreEqual(static_cast(200), response.StatusCode); + } + + TEST_METHOD(RequestGetImplicitUserAgentSucceeds) { + string url = "https://api.github.com/repos/microsoft/react-native-windows"; + + promise rcPromise; + string error; + IHttpResource::Response response; + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { + response = callbackResponse; + rcPromise.set_value(); + }); + resource->SetOnError([&rcPromise, &error](int64_t, string &&message, bool) { + error = std::move(message); + rcPromise.set_value(); + }); + + MicrosoftReactSetRuntimeOptionString("Http.UserAgent", "React Native Windows"); + + //clang-format off + resource->SendRequest( + "GET", + std::move(url), + 0, /*requestId*/ + {}, /*headers*/ + {}, /*data*/ + "text", /*responseType*/ + false, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {}); + //clang-format on + + rcPromise.get_future().wait(); + + Assert::AreEqual({}, error, L"Error encountered"); + Assert::AreEqual(static_cast(200), response.StatusCode); + } + + TEST_METHOD(RequestGetMissingUserAgentFails) { + // string url = "http://localhost:" + std::to_string(s_port); + string url = "https://api.github.com/repos/microsoft/react-native-macos"; + + promise rcPromise; + string error; + IHttpResource::Response response; + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { + response = callbackResponse; + rcPromise.set_value(); + }); + resource->SetOnError([&rcPromise, &error](int64_t, string &&message, bool) { + error = std::move(message); + rcPromise.set_value(); + }); + + //clang-format off + resource->SendRequest( + "GET", + std::move(url), + 0, /*requestId*/ + {}, /*headers*/ + {}, /*data*/ + "text", /*responseType*/ + false, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {}); + //clang-format on + + rcPromise.get_future().wait(); + + Assert::AreEqual({}, error, L"Error encountered"); + Assert::AreEqual(static_cast(403), response.StatusCode); + } + TEST_METHOD(RequestGetFails) { string error; promise promise; diff --git a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp index 22ec4047a71..fad847fb851 100644 --- a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp +++ b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp @@ -640,8 +640,6 @@ void OriginPolicyHttpFilter::ValidateResponse(HttpResponseMessage const &respons } ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage const &request) const { - // TODO: Inject user agent? - auto coRequest = request; HttpRequestMessage preflightRequest; @@ -761,17 +759,6 @@ ResponseOperation OriginPolicyHttpFilter::SendRequestAsync(HttpRequestMessage co ValidatePreflightResponse(coRequest, preflightResponse); } - // Set User-Agent after validating request and preflight. - auto userAgent = GetRuntimeOptionString("Http.UserAgent"); - if (userAgent.size() > 0) { - coRequest.Headers().Append(L"User-Agent", to_hstring(userAgent)); - } - - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || - originPolicy == OriginPolicy::CrossOriginResourceSharing) { - coRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); - } - auto response = co_await m_innerFilter.SendRequestAsync(coRequest); ValidateResponse(response, originPolicy); diff --git a/vnext/Shared/Networking/RedirectHttpFilter.cpp b/vnext/Shared/Networking/RedirectHttpFilter.cpp index a5248e031f8..a1fd8eedcc5 100644 --- a/vnext/Shared/Networking/RedirectHttpFilter.cpp +++ b/vnext/Shared/Networking/RedirectHttpFilter.cpp @@ -5,6 +5,8 @@ #include "RedirectHttpFilter.h" +// React Native Windows +#include #include "WinRTTypes.h" // Windows API @@ -211,6 +213,13 @@ ResponseOperation RedirectHttpFilter::SendRequestAsync(HttpRequestMessage const method = coRequest.Method(); do { + // Set User-Agent + // See https://fetch.spec.whatwg.org/#http-network-or-cache-fetch + auto userAgent = GetRuntimeOptionString("Http.UserAgent"); + if (userAgent.size() > 0) { + coRequest.Headers().Append(L"User-Agent", winrt::to_hstring(userAgent)); + } + // Send subsequent requests through the filter that doesn't have the credentials included in the first request response = co_await (redirectCount > 0 ? m_innerFilterWithNoCredentials : m_innerFilter).SendRequestAsync(coRequest);