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" +} 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 f39e9e8d8a0..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; 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); 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 +