diff --git a/change/react-native-windows-b2f23c96-082a-4440-8d14-1d5a0ce3d115.json b/change/react-native-windows-b2f23c96-082a-4440-8d14-1d5a0ce3d115.json
new file mode 100644
index 00000000000..8d385233d4c
--- /dev/null
+++ b/change/react-native-windows-b2f23c96-082a-4440-8d14-1d5a0ce3d115.json
@@ -0,0 +1,7 @@
+{
+ "type": "patch",
+ "comment": "Implement HTTP redirection (#10534)",
+ "packageName": "react-native-windows",
+ "email": "julio.rocha@microsoft.com",
+ "dependentChangeType": "patch"
+}
diff --git a/packages/e2e-test-app/windows/RNTesterApp/RNTesterApp.csproj b/packages/e2e-test-app/windows/RNTesterApp/RNTesterApp.csproj
index 8d7c83cd37f..ffb60d4bb8b 100644
--- a/packages/e2e-test-app/windows/RNTesterApp/RNTesterApp.csproj
+++ b/packages/e2e-test-app/windows/RNTesterApp/RNTesterApp.csproj
@@ -145,7 +145,7 @@
-
+
6.2.9
@@ -165,4 +165,4 @@
-
+
\ No newline at end of file
diff --git a/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp b/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp
index 640383f71e6..c9026cc9831 100644
--- a/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp
+++ b/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp
@@ -15,6 +15,7 @@ using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace http = boost::beast::http;
+using folly::dynamic;
using Microsoft::React::Networking::IHttpResource;
using Microsoft::React::Networking::OriginPolicy;
using std::make_shared;
@@ -82,7 +83,8 @@ TEST_CLASS(HttpOriginPolicyIntegrationTest)
auto reqHandler = [&serverArgs](const DynamicRequest& request) -> ResponseWrapper
{
- return { std::move(serverArgs.Response) };
+ // Don't use move constructor in case of multiple requests
+ return { serverArgs.Response };
};
switch (clientArgs.Method)
@@ -133,7 +135,7 @@ TEST_CLASS(HttpOriginPolicyIntegrationTest)
clientArgs.ResponseContent = std::move(content);
clientArgs.ContentPromise.set_value();
});
- resource->SetOnError([&clientArgs](int64_t, string&& message)
+ resource->SetOnError([&clientArgs](int64_t, string&& message, bool)
{
clientArgs.ErrorMessage = std::move(message);
clientArgs.ContentPromise.set_value();
@@ -144,10 +146,10 @@ TEST_CLASS(HttpOriginPolicyIntegrationTest)
string{server1Args.Url},
0, /*requestId*/
std::move(clientArgs.RequestHeaders),
- {}, /*data*/
+ dynamic::object("string", ""), /*data*/
"text",
false, /*useIncrementalUpdates*/
- 1000, /*timeout*/
+ 0, /*timeout*/
clientArgs.WithCredentials, /*withCredentials*/
[](int64_t){} /*reactCallback*/
);
@@ -187,7 +189,7 @@ TEST_CLASS(HttpOriginPolicyIntegrationTest)
clientArgs.ResponseContent = std::move(content);
clientArgs.ContentPromise.set_value();
});
- resource->SetOnError([&clientArgs](int64_t, string&& message)
+ resource->SetOnError([&clientArgs](int64_t, string&& message, bool)
{
clientArgs.ErrorMessage = std::move(message);
clientArgs.ContentPromise.set_value();
@@ -198,10 +200,10 @@ TEST_CLASS(HttpOriginPolicyIntegrationTest)
string{serverArgs.Url},
0, /*requestId*/
std::move(clientArgs.RequestHeaders),
- {}, /*data*/
+ dynamic::object("string", ""), /*data*/
"text",
false, /*useIncrementalUpdates*/
- 1000, /*timeout*/
+ 0, /*timeout*/
clientArgs.WithCredentials, /*withCredentials*/
[](int64_t) {} /*reactCallback*/
);
@@ -223,16 +225,16 @@ TEST_CLASS(HttpOriginPolicyIntegrationTest)
TEST_METHOD_CLEANUP(MethodCleanup)
{
+ // Clear any runtime options that may be used by tests in this class.
SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None));
SetRuntimeOptionString("Http.GlobalOrigin", {});
+ SetRuntimeOptionBool("Http.OmitCredentials", false);
// Bug in HttpServer does not correctly release TCP port between test methods.
// Using a different por per test for now.
s_port++;
}
- //TODO: NoCors_InvalidMethod_Failed?
-
BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsForbiddenMethodSucceeds)
// CONNECT, TRACE, and TRACK methods not supported by Windows.Web.Http
// https://docs.microsoft.com/en-us/uwp/api/windows.web.http.httpmethod?view=winrt-19041#properties
@@ -283,7 +285,7 @@ TEST_CLASS(HttpOriginPolicyIntegrationTest)
getContent = std::move(content);
getDataPromise.set_value();
});
- resource->SetOnError([&server, &error, &getDataPromise](int64_t, string&& message)
+ resource->SetOnError([&server, &error, &getDataPromise](int64_t, string&& message, bool)
{
error = std::move(message);
getDataPromise.set_value();
@@ -296,11 +298,10 @@ TEST_CLASS(HttpOriginPolicyIntegrationTest)
{
{"ValidHeader", "AnyValue"}
},
- {}, /*data*/
- //{} /*bodyData*/,
+ dynamic::object("string", ""), /*data*/
"text",
false /*useIncrementalUpdates*/,
- 1000 /*timeout*/,
+ 0 /*timeout*/,
false /*withCredentials*/,
[](int64_t) {} /*callback*/
);
@@ -324,6 +325,7 @@ TEST_CLASS(HttpOriginPolicyIntegrationTest)
SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl);
SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing));
+
TestOriginPolicy(serverArgs, clientArgs, s_shouldFail);
}// SimpleCorsForbiddenMethodFails
@@ -344,8 +346,6 @@ TEST_CLASS(HttpOriginPolicyIntegrationTest)
TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/);
}// NoCorsCrossOriginFetchRequestSucceeds
- //NoCors_CrossOriginFetchRequestWithTimeout_Succeeded //TODO: Implement timeout
-
BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsCrossOriginPatchSucceededs)
END_TEST_METHOD_ATTRIBUTE()
TEST_METHOD(NoCorsCrossOriginPatchSucceededs)
@@ -377,6 +377,7 @@ TEST_CLASS(HttpOriginPolicyIntegrationTest)
SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str());
SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing));
+
TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/);
}// SimpleCorsSameOriginSucceededs
@@ -390,6 +391,7 @@ TEST_CLASS(HttpOriginPolicyIntegrationTest)
SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl);
SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing));
+
TestOriginPolicy(serverArgs, clientArgs, s_shouldFail);
}// SimpleCorsCrossOriginFetchFails
@@ -404,6 +406,7 @@ TEST_CLASS(HttpOriginPolicyIntegrationTest)
SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str());
SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing));
+
TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/);
}// FullCorsSameOriginRequestSucceeds
@@ -669,9 +672,6 @@ TEST_CLASS(HttpOriginPolicyIntegrationTest)
} // FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds
BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds)
- // [0x80072f88] The HTTP redirect request must be confirmed by the user
- //TODO: Figure out manual redirection.
- TEST_IGNORE()
END_TEST_METHOD_ATTRIBUTE()
TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds)
{
@@ -763,8 +763,6 @@ TEST_CLASS(HttpOriginPolicyIntegrationTest)
// The current implementation omits withCredentials flag from request and always sets it to false
// Configure the responses for CORS request
BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginWithCredentialsSucceeds)
- //TODO: Fails if run after FullCorsCrossOriginWithCredentialsFails
- TEST_IGNORE()
END_TEST_METHOD_ATTRIBUTE()
TEST_METHOD(FullCorsCrossOriginWithCredentialsSucceeds)
{
@@ -823,15 +821,6 @@ TEST_CLASS(HttpOriginPolicyIntegrationTest)
TestOriginPolicy(serverArgs, clientArgs, s_shouldFail);
}// RequestWithProxyAuthorizationHeaderFails
-
- BEGIN_TEST_METHOD_ATTRIBUTE(ExceedingRedirectLimitFails)
- TEST_IGNORE()
- END_TEST_METHOD_ATTRIBUTE()
- TEST_METHOD(ExceedingRedirectLimitFails)
- {
- Assert::Fail(L"NOT IMPLEMENTED");
- }// ExceedingRedirectLimitFails
-
};
uint16_t HttpOriginPolicyIntegrationTest::s_port = 7777;
diff --git a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp
index 2d2e7dac901..88c3d921c88 100644
--- a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp
+++ b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp
@@ -34,6 +34,8 @@ using Test::EmptyResponse;
using Test::HttpServer;
using Test::ResponseWrapper;
+namespace Microsoft::React::Test {
+
TEST_CLASS (HttpResourceIntegrationTest) {
static uint16_t s_port;
@@ -64,7 +66,7 @@ TEST_CLASS (HttpResourceIntegrationTest) {
statusCode = static_cast(response.StatusCode);
});
resource->SetOnData([&resPromise](int64_t, string &&content) { resPromise.set_value(); });
- resource->SetOnError([&resPromise, &error, &server](int64_t, string &&message) {
+ resource->SetOnError([&resPromise, &error, &server](int64_t, string &&message, bool) {
error = std::move(message);
resPromise.set_value();
@@ -78,7 +80,7 @@ TEST_CLASS (HttpResourceIntegrationTest) {
{}, /*data*/
"text",
false,
- 1000 /*timeout*/,
+ 0 /*timeout*/,
false /*withCredentials*/,
[](int64_t) {});
@@ -118,7 +120,7 @@ TEST_CLASS (HttpResourceIntegrationTest) {
response = callbackResponse;
rcPromise.set_value();
});
- resource->SetOnError([&rcPromise, &error, &server](int64_t, string &&message) {
+ resource->SetOnError([&rcPromise, &error, &server](int64_t, string &&message, bool) {
error = std::move(message);
rcPromise.set_value();
@@ -139,7 +141,7 @@ TEST_CLASS (HttpResourceIntegrationTest) {
{}, /*data*/
"text",
false,
- 1000 /*timeout*/,
+ 0 /*timeout*/,
false /*withCredentials*/,
[](int64_t) {});
//clang-format on
@@ -167,12 +169,12 @@ TEST_CLASS (HttpResourceIntegrationTest) {
promise promise;
auto resource = IHttpResource::Make();
- resource->SetOnError([&error, &promise](int64_t, string &&message) {
+ resource->SetOnError([&error, &promise](int64_t, string &&message, bool) {
error = message;
promise.set_value();
});
- resource->SendRequest("GET", "http://nonexistinghost", 0, {}, {}, "text", false, 1000, false, [](int64_t) {});
+ resource->SendRequest("GET", "http://nonexistinghost", 0, {}, {}, "text", false, 0, false, [](int64_t) {});
promise.get_future().wait();
@@ -226,7 +228,7 @@ TEST_CLASS (HttpResourceIntegrationTest) {
getDataPromise.set_value();
});
resource->SetOnError(
- [&optionsPromise, &getResponsePromise, &getDataPromise, &error, &server](int64_t, string &&message) {
+ [&optionsPromise, &getResponsePromise, &getDataPromise, &error, &server](int64_t, string &&message, bool) {
error = std::move(message);
optionsPromise.set_value();
@@ -256,7 +258,7 @@ TEST_CLASS (HttpResourceIntegrationTest) {
{}, /*data*/
"text",
false,
- 1000 /*timeout*/,
+ 0 /*timeout*/,
false /*withCredentials*/,
[](int64_t) {});
//clang-format on
@@ -279,14 +281,14 @@ TEST_CLASS (HttpResourceIntegrationTest) {
Assert::AreEqual({"Response Body"}, content);
}
- TEST_METHOD(SimpleRedirectSucceeds) {
+ TEST_METHOD(SimpleRedirectGetSucceeds) {
auto port1 = s_port;
auto port2 = ++s_port;
string url = "http://localhost:" + std::to_string(port1);
- promise getResponsePromise;
- promise getContentPromise;
- IHttpResource::Response getResponse;
+ promise responsePromise;
+ promise contentPromise;
+ IHttpResource::Response responseResult;
string content;
string error;
@@ -311,23 +313,23 @@ TEST_CLASS (HttpResourceIntegrationTest) {
server2->Start();
auto resource = IHttpResource::Make();
- resource->SetOnResponse([&getResponse, &getResponsePromise](int64_t, IHttpResource::Response response) {
+ resource->SetOnResponse([&responseResult, &responsePromise](int64_t, IHttpResource::Response response) {
if (response.StatusCode == static_cast(http::status::ok)) {
- getResponse = response;
- getResponsePromise.set_value();
+ responseResult = response;
+ responsePromise.set_value();
}
});
- resource->SetOnData([&getContentPromise, &content](int64_t, string &&responseData) {
+ resource->SetOnData([&contentPromise, &content](int64_t, string &&responseData) {
content = std::move(responseData);
if (!content.empty())
- getContentPromise.set_value();
+ contentPromise.set_value();
});
- resource->SetOnError([&getResponsePromise, &getContentPromise, &error, &server1](int64_t, string &&message) {
+ resource->SetOnError([&responsePromise, &contentPromise, &error, &server1](int64_t, string &&message, bool) {
error = std::move(message);
- getResponsePromise.set_value();
- getContentPromise.set_value();
+ responsePromise.set_value();
+ contentPromise.set_value();
});
//clang-format off
@@ -339,21 +341,203 @@ TEST_CLASS (HttpResourceIntegrationTest) {
{}, /*data*/
"text",
false, /*useIncrementalUpdates*/
- 1000 /*timeout*/,
+ 0 /*timeout*/,
false /*withCredentials*/,
[](int64_t) {});
//clang-format on
- getResponsePromise.get_future().wait();
- getContentPromise.get_future().wait();
+ responsePromise.get_future().wait();
+ contentPromise.get_future().wait();
server2->Stop();
server1->Stop();
Assert::AreEqual({}, error, L"Error encountered");
- Assert::AreEqual(static_cast(200), getResponse.StatusCode);
+ Assert::AreEqual(static_cast(200), responseResult.StatusCode);
Assert::AreEqual({"Redirect Content"}, content);
}
+
+ TEST_METHOD(SimpleRedirectPatchSucceeds) {
+ auto port1 = s_port;
+ auto port2 = ++s_port;
+ string url = "http://localhost:" + std::to_string(port1);
+
+ promise responsePromise;
+ promise contentPromise;
+ IHttpResource::Response responseResult;
+ string content;
+ string error;
+
+ auto server1 = make_shared(port1);
+ server1->Callbacks().OnPatch = [port2](const DynamicRequest &request) -> ResponseWrapper {
+ DynamicResponse response;
+ response.result(http::status::moved_permanently);
+ response.set(http::field::location, {"http://localhost:" + std::to_string(port2)});
+
+ return {std::move(response)};
+ };
+ auto server2 = make_shared(port2);
+ server2->Callbacks().OnPatch = [](const DynamicRequest &request) -> ResponseWrapper {
+ DynamicResponse response;
+ response.result(http::status::ok);
+ response.body() = Test::CreateStringResponseBody("Redirect Content");
+
+ return {std::move(response)};
+ };
+
+ server1->Start();
+ server2->Start();
+
+ auto resource = IHttpResource::Make();
+ resource->SetOnResponse([&responseResult, &responsePromise](int64_t, IHttpResource::Response response) {
+ if (response.StatusCode == static_cast(http::status::ok)) {
+ responseResult = response;
+ responsePromise.set_value();
+ }
+ });
+ resource->SetOnData([&contentPromise, &content](int64_t, string &&responseData) {
+ content = std::move(responseData);
+
+ if (!content.empty())
+ contentPromise.set_value();
+ });
+ resource->SetOnError([&responsePromise, &contentPromise, &error, &server1](int64_t, string &&message, bool) {
+ error = std::move(message);
+
+ responsePromise.set_value();
+ contentPromise.set_value();
+ });
+
+ //clang-format off
+ resource->SendRequest(
+ "PATCH",
+ std::move(url),
+ 0, /*requestId*/
+ {}, /*headers*/
+ {}, /*data*/
+ "text",
+ false, /*useIncrementalUpdates*/
+ 0 /*timeout*/,
+ false /*withCredentials*/,
+ [](int64_t) {});
+ //clang-format on
+
+ responsePromise.get_future().wait();
+ contentPromise.get_future().wait();
+
+ server2->Stop();
+ server1->Stop();
+
+ Assert::AreEqual({}, error, L"Error encountered");
+ Assert::AreEqual(static_cast(200), responseResult.StatusCode);
+ Assert::AreEqual({"Redirect Content"}, content);
+ }
+
+ TEST_METHOD(TimeoutSucceeds) {
+ auto port = s_port;
+ string url = "http://localhost:" + std::to_string(port);
+
+ promise getPromise;
+ string error;
+ int statusCode = 0;
+ bool timeoutError = false;
+
+ auto server = std::make_shared(s_port);
+ server->Callbacks().OnGet = [](const DynamicRequest &) -> ResponseWrapper {
+ DynamicResponse response;
+ response.result(http::status::ok);
+
+ // Hold response to test client timeout
+ promise timer;
+ timer.get_future().wait_for(std::chrono::milliseconds(2000));
+
+ return {std::move(response)};
+ };
+ server->Start();
+
+ auto resource = IHttpResource::Make();
+ resource->SetOnResponse([&getPromise, &statusCode](int64_t, IHttpResource::Response response) {
+ statusCode = static_cast(response.StatusCode);
+ getPromise.set_value();
+ });
+ resource->SetOnError([&getPromise, &error, &timeoutError](int64_t, string &&errorMessage, bool isTimeout) {
+ error = std::move(errorMessage);
+ timeoutError = isTimeout;
+ getPromise.set_value();
+ });
+ resource->SendRequest(
+ "GET",
+ std::move(url),
+ 0, /*requestId*/
+ {}, /*headers*/
+ {}, /*data*/
+ "text", /*responseType*/
+ false, /*useIncrementalUpdates*/
+ 6000, /*timeout*/
+ false, /*withCredentials*/
+ [](int64_t) {} /*callback*/);
+
+ getPromise.get_future().wait();
+ server->Stop();
+
+ Assert::AreEqual({}, error);
+ Assert::IsFalse(timeoutError);
+ Assert::AreEqual(200, statusCode);
+ }
+
+ TEST_METHOD(TimeoutFails) {
+ auto port = s_port;
+ string url = "http://localhost:" + std::to_string(port);
+
+ promise getPromise;
+ string error;
+ int statusCode = 0;
+ bool timeoutError = false;
+
+ auto server = std::make_shared(s_port);
+ server->Callbacks().OnGet = [](const DynamicRequest &) -> ResponseWrapper {
+ DynamicResponse response;
+ response.result(http::status::ok);
+
+ // Hold response to test client timeout
+ promise timer;
+ timer.get_future().wait_for(std::chrono::milliseconds(4000));
+
+ return {std::move(response)};
+ };
+ server->Start();
+
+ auto resource = IHttpResource::Make();
+ resource->SetOnResponse([&getPromise, &statusCode](int64_t, IHttpResource::Response response) {
+ statusCode = static_cast(response.StatusCode);
+ getPromise.set_value();
+ });
+ resource->SetOnError([&getPromise, &error, &timeoutError](int64_t, string &&errorMessage, bool isTimeout) {
+ error = std::move(errorMessage);
+ timeoutError = isTimeout;
+ getPromise.set_value();
+ });
+ resource->SendRequest(
+ "GET",
+ std::move(url),
+ 0, /*requestId*/
+ {}, /*headers*/
+ {}, /*data*/
+ "text", /*responseType*/
+ false, /*useIncrementalUpdates*/
+ 2000, /*timeout*/
+ false, /*withCredentials*/
+ [](int64_t) {} /*callback*/);
+
+ getPromise.get_future().wait();
+ server->Stop();
+
+ Assert::IsTrue(timeoutError);
+ Assert::AreEqual({"[0x800705b4] This operation returned because the timeout period expired."}, error);
+ Assert::AreEqual(0, statusCode);
+ }
};
/*static*/ uint16_t HttpResourceIntegrationTest::s_port = 4444;
+
+} // namespace Microsoft::React::Test
diff --git a/vnext/Desktop.UnitTests/OriginPolicyHttpFilterTest.cpp b/vnext/Desktop.UnitTests/OriginPolicyHttpFilterTest.cpp
index 65fec288b1b..a31afb0ff05 100644
--- a/vnext/Desktop.UnitTests/OriginPolicyHttpFilterTest.cpp
+++ b/vnext/Desktop.UnitTests/OriginPolicyHttpFilterTest.cpp
@@ -53,6 +53,12 @@ TEST_CLASS (OriginPolicyHttpFilterTest) {
Assert::IsTrue (OriginPolicyHttpFilter::IsSameOrigin(Uri{L"http://[2001:db8:1f70::999:de8:7648:6e8]"}, Uri{L"http://[2001:db8:1f70::999:de8:7648:6e8]/test.html"}));
Assert::IsTrue (OriginPolicyHttpFilter::IsSameOrigin(Uri{L"https://\u65E5\u672C\u8A9E.com" }, Uri{L"https://\u65E5\u672C\u8A9E.com/FakeResponse.ashx"}));
Assert::IsTrue (OriginPolicyHttpFilter::IsSameOrigin(Uri{L"https://www.microsoft.com" }, Uri{L"https://www.microsoft.com:443"}));
+ Assert::IsFalse(OriginPolicyHttpFilter::IsSameOrigin(Uri{L"https://www.microsoft.com" }, Uri{nullptr}));
+ Assert::IsFalse(OriginPolicyHttpFilter::IsSameOrigin(Uri{nullptr }, Uri{L"https://www.microsoft.com"}));
+ Assert::IsFalse(OriginPolicyHttpFilter::IsSameOrigin(Uri{nullptr }, Uri{nullptr}));
+ Assert::IsFalse(OriginPolicyHttpFilter::IsSameOrigin(Uri{L"https://www.microsoft.com" }, nullptr));
+ Assert::IsFalse(OriginPolicyHttpFilter::IsSameOrigin(nullptr , Uri{L"https://www.microsoft.com"}));
+ Assert::IsFalse(OriginPolicyHttpFilter::IsSameOrigin(nullptr , nullptr));
// clang-format on
}
diff --git a/vnext/Desktop.UnitTests/React.Windows.Desktop.UnitTests.vcxproj b/vnext/Desktop.UnitTests/React.Windows.Desktop.UnitTests.vcxproj
index 87b2757f01e..0c48bd6ceeb 100644
--- a/vnext/Desktop.UnitTests/React.Windows.Desktop.UnitTests.vcxproj
+++ b/vnext/Desktop.UnitTests/React.Windows.Desktop.UnitTests.vcxproj
@@ -45,7 +45,7 @@
- $(ReactNativeWindowsDir)\Mso;$(ReactNativeWindowsDir)Common;$(ReactNativeWindowsDir)Desktop;$(ReactNativeWindowsDir)stubs;$(ReactNativeWindowsDir)Shared;$(ReactNativeWindowsDir)Shared;$(ReactNativeWindowsDir)include\Shared;$(MSBuildThisFileDirectory);$(IncludePath)
+ $(ReactNativeWindowsDir)Mso;$(ReactNativeWindowsDir)Common;$(ReactNativeWindowsDir)Desktop;$(ReactNativeWindowsDir)stubs;$(ReactNativeWindowsDir)Shared;$(ReactNativeWindowsDir)include\Shared;$(MSBuildThisFileDirectory);$(IncludePath)
@@ -94,6 +94,7 @@
+
diff --git a/vnext/Desktop.UnitTests/React.Windows.Desktop.UnitTests.vcxproj.filters b/vnext/Desktop.UnitTests/React.Windows.Desktop.UnitTests.vcxproj.filters
index 5824ed65aa1..05751912951 100644
--- a/vnext/Desktop.UnitTests/React.Windows.Desktop.UnitTests.vcxproj.filters
+++ b/vnext/Desktop.UnitTests/React.Windows.Desktop.UnitTests.vcxproj.filters
@@ -61,6 +61,9 @@
Unit Tests
+
+ Unit Tests
+
diff --git a/vnext/Desktop.UnitTests/RedirectHttpFilterUnitTest.cpp b/vnext/Desktop.UnitTests/RedirectHttpFilterUnitTest.cpp
new file mode 100644
index 00000000000..920a3ca918a
--- /dev/null
+++ b/vnext/Desktop.UnitTests/RedirectHttpFilterUnitTest.cpp
@@ -0,0 +1,219 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#include
+
+#include
+#include
+#include "WinRTNetworkingMocks.h"
+
+// Windows API
+#include
+#include
+
+using namespace Microsoft::VisualStudio::CppUnitTestFramework;
+using namespace winrt::Windows::Web::Http;
+
+using Microsoft::React::Networking::RedirectHttpFilter;
+using Microsoft::React::Networking::ResponseOperation;
+using winrt::Windows::Foundation::Uri;
+using winrt::Windows::Web::Http::Filters::IHttpBaseProtocolFilter;
+using winrt::Windows::Web::Http::Filters::IHttpFilter;
+
+namespace Microsoft::VisualStudio::CppUnitTestFramework {
+
+template <>
+std::wstring ToString(const HttpStatusCode &status) {
+ return ToString(static_cast(status));
+}
+
+} // namespace Microsoft::VisualStudio::CppUnitTestFramework
+
+namespace Microsoft::React::Test {
+
+TEST_CLASS (RedirectHttpFilterUnitTest) {
+ TEST_CLASS_INITIALIZE(Initialize) {
+ winrt::uninit_apartment(); // Why does this work?
+ }
+
+ TEST_METHOD(QueryInterfacesSucceeds) {
+ auto filter = winrt::make();
+
+ auto iFilter = filter.try_as();
+ Assert::IsFalse(iFilter == nullptr);
+ auto baseFilter = filter.try_as();
+ Assert::IsFalse(baseFilter == nullptr);
+ }
+
+ TEST_METHOD(AutomaticRedirectSucceeds) {
+ auto url1 = L"http://initialhost";
+ auto url2 = L"http://redirecthost";
+ auto mockFilter1 = winrt::make();
+ auto mockFilter2 = winrt::make();
+ mockFilter1.as()->Mocks.SendRequestAsync =
+ [&url2](HttpRequestMessage const &request) -> ResponseOperation {
+ HttpResponseMessage response{};
+
+ if (request.RequestUri().Host() == L"initialhost") {
+ response.StatusCode(HttpStatusCode::MovedPermanently);
+ response.Headers().Location(Uri{url2});
+ response.Content(HttpStringContent{L""});
+ } else {
+ response.StatusCode(HttpStatusCode::BadRequest);
+ response.Content(HttpStringContent{L""});
+ }
+
+ co_return response;
+ };
+ mockFilter2.as()->Mocks.SendRequestAsync =
+ [](HttpRequestMessage const &request) -> ResponseOperation {
+ HttpResponseMessage response;
+
+ if (request.RequestUri().Host() == L"redirecthost") {
+ response.StatusCode(HttpStatusCode::Ok);
+ response.Content(HttpStringContent{L"Response Content"});
+ } else {
+ response.StatusCode(HttpStatusCode::BadRequest);
+ response.Content(HttpStringContent{L""});
+ }
+
+ co_return response;
+ };
+
+ auto filter = winrt::make(std::move(mockFilter1), std::move(mockFilter2));
+ auto client = HttpClient{filter};
+ auto request = HttpRequestMessage{HttpMethod::Get(), Uri{url1}};
+ auto sendOp = client.SendRequestAsync(request);
+ sendOp.get();
+ auto response = sendOp.GetResults();
+
+ Assert::AreEqual(HttpStatusCode::Ok, response.StatusCode());
+
+ auto contentOp = response.Content().ReadAsStringAsync();
+ contentOp.get();
+ auto content = contentOp.GetResults();
+ Assert::AreEqual(L"Response Content", content.c_str());
+ }
+
+ TEST_METHOD(ManualRedirectSucceeds) {
+ auto url1 = L"http://initialhost";
+ auto url2 = L"http://redirecthost";
+ auto mockFilter1 = winrt::make();
+ auto mockFilter2 = winrt::make();
+ mockFilter1.as()->Mocks.SendRequestAsync =
+ [&url2](HttpRequestMessage const &request) -> ResponseOperation {
+ HttpResponseMessage response{};
+
+ if (request.RequestUri().Host() == L"initialhost") {
+ response.StatusCode(HttpStatusCode::MovedPermanently);
+ response.Headers().Location(Uri{url2});
+ response.Content(HttpStringContent{L""});
+ } else if (request.RequestUri().Host() == L"redirecthost") {
+ response.StatusCode(HttpStatusCode::Ok);
+ response.Content(HttpStringContent{L"Response Content"});
+ }
+
+ co_return response;
+ };
+
+ auto filter = winrt::make(std::move(mockFilter1), std::move(mockFilter2));
+ // Disable automatic redirect
+ filter.try_as().AllowAutoRedirect(false);
+
+ auto client = HttpClient{filter};
+ auto request = HttpRequestMessage{HttpMethod::Get(), Uri{url1}};
+ auto sendOp = client.SendRequestAsync(request);
+ sendOp.get();
+ auto response = sendOp.GetResults();
+
+ Assert::AreEqual(HttpStatusCode::MovedPermanently, response.StatusCode());
+
+ request = HttpRequestMessage{response.RequestMessage().Method(), response.Headers().Location()};
+ sendOp = client.SendRequestAsync(request);
+ sendOp.get();
+ response = sendOp.GetResults();
+
+ Assert::AreEqual(HttpStatusCode::Ok, response.StatusCode());
+
+ auto contentOp = response.Content().ReadAsStringAsync();
+ contentOp.get();
+ auto content = contentOp.GetResults();
+ Assert::AreEqual(L"Response Content", content.c_str());
+ }
+
+ void TestRedirectCount(size_t maxRedirects, size_t actualRedirects) {
+ auto url1 = L"http://initialhost";
+ auto url2 = L"http://redirecthost";
+ auto mockFilter1 = winrt::make();
+ auto mockFilter2 = winrt::make();
+
+ size_t redirectCount = 0;
+ mockFilter1.as()->Mocks.SendRequestAsync =
+ [&url2, &redirectCount](auto const &request) -> ResponseOperation {
+ HttpResponseMessage response;
+
+ response.Headers().Location(Uri{url2});
+ response.StatusCode(HttpStatusCode::MovedPermanently);
+ redirectCount++;
+
+ co_return response;
+ };
+ mockFilter2.as()->Mocks.SendRequestAsync =
+ [&url2, &redirectCount, actualRedirects](auto const &request) -> ResponseOperation {
+ HttpResponseMessage response;
+
+ if (redirectCount >= actualRedirects) {
+ response.StatusCode(HttpStatusCode::Ok);
+ response.Content(HttpStringContent{L"Response Content"});
+ } else {
+ response.Headers().Location(Uri{url2});
+ response.StatusCode(HttpStatusCode::MovedPermanently);
+ redirectCount++;
+ }
+
+ co_return response;
+ };
+
+ auto filter = winrt::make(maxRedirects, std::move(mockFilter1), std::move(mockFilter2));
+ auto client = HttpClient{filter};
+ auto request = HttpRequestMessage{HttpMethod::Get(), Uri{url1}};
+ ResponseOperation sendOp = nullptr;
+ long errorCode = 0;
+ winrt::hstring errorMessage{};
+ try {
+ sendOp = client.SendRequestAsync(request);
+ sendOp.get();
+ } catch (const winrt::hresult_error &e) {
+ errorCode = e.code();
+ errorMessage = e.message();
+ }
+
+ if (maxRedirects >= actualRedirects) {
+ // Should succeed
+ auto response = sendOp.GetResults();
+
+ Assert::AreEqual(0, (int)errorCode);
+ Assert::AreEqual(L"", errorMessage.c_str());
+ Assert::AreEqual(HttpStatusCode::Ok, response.StatusCode());
+
+ auto contentOp = response.Content().ReadAsStringAsync();
+ contentOp.get();
+ auto content = contentOp.GetResults();
+ Assert::AreEqual(L"Response Content", content.c_str());
+ } else {
+ // Should fail
+ Assert::AreEqual(HRESULT_FROM_WIN32(ERROR_HTTP_REDIRECT_FAILED), errorCode);
+ Assert::AreEqual(L"Too many redirects", errorMessage.c_str());
+ }
+ }
+
+ TEST_METHOD(MaxAllowedRedirectsSucceeds) {
+ TestRedirectCount(3, 3);
+ }
+
+ TEST_METHOD(TooManyRedirectsFails) {
+ TestRedirectCount(2, 3);
+ }
+};
+
+} // namespace Microsoft::React::Test
diff --git a/vnext/Desktop.UnitTests/WinRTNetworkingMocks.cpp b/vnext/Desktop.UnitTests/WinRTNetworkingMocks.cpp
index 98e23a18ef5..0621d8cd6b7 100644
--- a/vnext/Desktop.UnitTests/WinRTNetworkingMocks.cpp
+++ b/vnext/Desktop.UnitTests/WinRTNetworkingMocks.cpp
@@ -7,12 +7,16 @@
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Networking::Sockets;
+using namespace winrt::Windows::Security::Cryptography::Certificates;
using namespace winrt::Windows::Storage::Streams;
+using namespace winrt::Windows::Web::Http;
using std::exception;
using winrt::auto_revoke_t;
using winrt::event_token;
using winrt::param::hstring;
+using winrt::Windows::Foundation::Collections::IVector;
+using winrt::Windows::Security::Credentials::PasswordCredential;
namespace Microsoft::React::Test {
@@ -371,4 +375,159 @@ void MockMessageWebSocketControl::MessageType(SocketMessageType const &value) co
#endif // 0
+#pragma region MockHttpBaseFilter
+
+MockHttpBaseFilter::MockHttpBaseFilter() noexcept {}
+
+#pragma region IHttpFilter
+
+IAsyncOperationWithProgress MockHttpBaseFilter::SendRequestAsync(
+ HttpRequestMessage const &request) {
+ if (Mocks.SendRequestAsync)
+ return Mocks.SendRequestAsync(request);
+
+ throw exception("Not implemented");
+}
+
+#pragma endregion IHttpFilter
+
+#pragma region IHttpBaseProtocolFilter
+
+bool MockHttpBaseFilter::AllowAutoRedirect() const {
+ if (Mocks.GetAllowAutoRedirect)
+ return Mocks.GetAllowAutoRedirect();
+
+ throw exception("Not implemented");
+}
+
+void MockHttpBaseFilter::AllowAutoRedirect(bool value) const {
+ if (Mocks.SetAllowAutoRedirect)
+ return Mocks.SetAllowAutoRedirect(value);
+
+ throw exception("Not implemented");
+}
+
+bool MockHttpBaseFilter::AllowUI() const {
+ if (Mocks.GetAllowUI)
+ return Mocks.GetAllowUI();
+
+ throw exception("Not implemented");
+}
+
+void MockHttpBaseFilter::AllowUI(bool value) const {
+ if (Mocks.SetAllowUI)
+ return Mocks.SetAllowUI(value);
+
+ throw exception("Not implemented");
+}
+
+bool MockHttpBaseFilter::AutomaticDecompression() const {
+ if (Mocks.GetAutomaticDecompression)
+ return Mocks.GetAutomaticDecompression();
+
+ throw exception("Not implemented");
+}
+
+void MockHttpBaseFilter::AutomaticDecompression(bool value) const {
+ if (Mocks.SetAutomaticDecompression)
+ return Mocks.SetAutomaticDecompression(value);
+
+ throw exception("Not implemented");
+}
+
+Filters::HttpCacheControl MockHttpBaseFilter::CacheControl() const {
+ if (Mocks.GetCacheControl)
+ return Mocks.GetCacheControl();
+
+ throw exception("Not implemented");
+}
+
+HttpCookieManager MockHttpBaseFilter::CookieManager() const {
+ if (Mocks.GetCookieManager)
+ return Mocks.GetCookieManager();
+
+ throw exception("Not implemented");
+}
+
+Certificate MockHttpBaseFilter::ClientCertificate() const {
+ if (Mocks.GetClientCertificate)
+ return Mocks.GetClientCertificate();
+
+ throw exception("Not implemented");
+}
+
+void MockHttpBaseFilter::ClientCertificate(Certificate const &value) const {
+ if (Mocks.SetClientCertificate)
+ return Mocks.SetClientCertificate(value);
+
+ throw exception("Not implemented");
+}
+
+IVector MockHttpBaseFilter::IgnorableServerCertificateErrors() const {
+ if (Mocks.GetIgnorableServerCertificateErrors)
+ return Mocks.GetIgnorableServerCertificateErrors();
+
+ throw exception("Not implemented");
+}
+
+uint32_t MockHttpBaseFilter::MaxConnectionsPerServer() const {
+ if (Mocks.GetMaxConnectionsPerServer)
+ return Mocks.GetMaxConnectionsPerServer();
+
+ throw exception("Not implemented");
+}
+
+void MockHttpBaseFilter::MaxConnectionsPerServer(uint32_t value) const {
+ if (Mocks.SetMaxConnectionsPerServer)
+ return Mocks.SetMaxConnectionsPerServer(value);
+
+ throw exception("Not implemented");
+}
+
+PasswordCredential MockHttpBaseFilter::ProxyCredential() const {
+ if (Mocks.GetProxyCredential)
+ return Mocks.GetProxyCredential();
+
+ throw exception("Not implemented");
+}
+
+void MockHttpBaseFilter::ProxyCredential(PasswordCredential const &value) const {
+ if (Mocks.SetProxyCredential)
+ return Mocks.SetProxyCredential(value);
+
+ throw exception("Not implemented");
+}
+
+PasswordCredential MockHttpBaseFilter::ServerCredential() const {
+ if (Mocks.GetServerCredential)
+ return Mocks.GetServerCredential();
+
+ throw exception("Not implemented");
+}
+
+void MockHttpBaseFilter::ServerCredential(PasswordCredential const &value) const {
+ if (Mocks.SetServerCredential)
+ return Mocks.SetServerCredential(value);
+
+ throw exception("Not implemented");
+}
+
+bool MockHttpBaseFilter::UseProxy() const {
+ if (Mocks.GetUseProxy)
+ return Mocks.GetUseProxy();
+
+ throw exception("Not implemented");
+}
+
+void MockHttpBaseFilter::UseProxy(bool value) const {
+ if (Mocks.SetUseProxy)
+ return Mocks.SetUseProxy(value);
+
+ throw exception("Not implemented");
+}
+
+#pragma endregion IHttpBaseProtocolFilter
+
+#pragma endregion MockHttpBaseFilter
+
} // namespace Microsoft::React::Test
diff --git a/vnext/Desktop.UnitTests/WinRTNetworkingMocks.h b/vnext/Desktop.UnitTests/WinRTNetworkingMocks.h
index 9fe166dac9c..b16f1219182 100644
--- a/vnext/Desktop.UnitTests/WinRTNetworkingMocks.h
+++ b/vnext/Desktop.UnitTests/WinRTNetworkingMocks.h
@@ -3,7 +3,15 @@
#pragma once
+// Windows API
+#include
#include
+#include
+#include
+#include
+#include
+
+// Standard Library
#include
namespace Microsoft::React::Test {
@@ -20,45 +28,44 @@ struct MockMessageWebSocket : public winrt::implements<
struct Mocks {
// IWebSocket
- std::function
- ConnectAsync;
+ std::function ConnectAsync;
- std::function SetRequestHeader;
+ std::function SetRequestHeader;
- std::function OutputStream;
+ std::function OutputStream;
- std::function Close;
+ std::function Close;
std::function const &) /*const*/>
+ winrt::Windows::Networking::Sockets::WebSocketClosedEventArgs> const &)>
ClosedToken;
std::function const &) /*const*/>
+ winrt::Windows::Networking::Sockets::WebSocketClosedEventArgs> const &)>
ClosedRevoker;
- std::function ClosedVoid;
+ std::function ClosedVoid;
// IMessageWebSocket
- std::function Control;
+ std::function Control;
- std::function Information;
+ std::function Information;
std::function const &) /*const*/>
+ winrt::Windows::Networking::Sockets::MessageWebSocketMessageReceivedEventArgs> const &)>
MessageReceivedToken;
std::function const &) /*const*/>
+ winrt::Windows::Networking::Sockets::MessageWebSocketMessageReceivedEventArgs> const &)>
MessageReceivedRevoker;
std::function MessageReceivedVoid;
@@ -125,37 +132,35 @@ struct ThrowingMessageWebSocket : public MockMessageWebSocket {
struct MockDataWriter : public winrt::Windows::Storage::Streams::IDataWriter {
struct Mocks {
- std::function UnstoredBufferLength;
- std::function GetUnicodeEncoding;
- std::function SetUnicodeEncoding;
- std::function GetByteOrder;
- std::function SetByteOrder;
- std::function WriteByte;
- std::function value) /*const*/> WriteBytes;
- std::function WriteBuffer;
- std::function
+ std::function UnstoredBufferLength;
+ std::function GetUnicodeEncoding;
+ std::function SetUnicodeEncoding;
+ std::function GetByteOrder;
+ std::function SetByteOrder;
+ std::function WriteByte;
+ std::function value)> WriteBytes;
+ std::function WriteBuffer;
+ std::function<
+ void(winrt::Windows::Storage::Streams::IBuffer const &buffer, std::uint32_t start, std::uint32_t count)>
WriteBufferRange;
- std::function WriteBoolean;
- std::function WriteGuid;
- std::function WriteInt16;
- std::function WriteInt32;
- std::function WriteInt64;
- std::function WriteUInt16;
- std::function WriteUInt32;
- std::function WriteUInt64;
- std::function WriteSingle;
- std::function WriteDouble;
- std::function WriteDateTime;
- std::function WriteTimeSpan;
- std::function WriteString;
- std::function MeasureString;
- std::function StoreAsync;
- std::function() /*const*/> FlushAsync;
- std::function DetachBuffer;
- std::function DetachStream;
+ std::function WriteBoolean;
+ std::function WriteGuid;
+ std::function WriteInt16;
+ std::function WriteInt32;
+ std::function WriteInt64;
+ std::function WriteUInt16;
+ std::function WriteUInt32;
+ std::function WriteUInt64;
+ std::function WriteSingle;
+ std::function WriteDouble;
+ std::function WriteDateTime;
+ std::function WriteTimeSpan;
+ std::function WriteString;
+ std::function MeasureString;
+ std::function StoreAsync;
+ std::function()> FlushAsync;
+ std::function DetachBuffer;
+ std::function DetachStream;
};
Mocks Mocks;
@@ -221,4 +226,106 @@ struct MockMessageWebSocketControl : winrt::implements<
#pragma endregion
};
+struct MockHttpBaseFilter : public winrt::implements<
+ MockHttpBaseFilter,
+ winrt::Windows::Web::Http::Filters::IHttpFilter,
+ winrt::Windows::Web::Http::Filters::IHttpBaseProtocolFilter> {
+ struct Mocks {
+#pragma region IHttpFilter
+
+ std::function(winrt::Windows::Web::Http::HttpRequestMessage const &request)>
+ SendRequestAsync;
+
+#pragma endregion IHttpFilter
+
+#pragma region IHttpBaseProtocolFilter
+
+ std::function GetAllowAutoRedirect;
+ std::function SetAllowAutoRedirect{[](bool) {}};
+
+ std::function GetAllowUI;
+ std::function SetAllowUI{[](bool) {}};
+
+ std::function GetAutomaticDecompression;
+ std::function SetAutomaticDecompression;
+
+ std::function GetCacheControl;
+
+ std::function GetCookieManager;
+
+ std::function GetClientCertificate;
+ std::function
+ SetClientCertificate;
+
+ std::function()>
+ GetIgnorableServerCertificateErrors;
+
+ std::function GetMaxConnectionsPerServer;
+ std::function SetMaxConnectionsPerServer;
+
+ std::function GetProxyCredential;
+ std::function SetProxyCredential;
+
+ std::function GetServerCredential;
+ std::function SetServerCredential;
+
+ std::function GetUseProxy;
+ std::function SetUseProxy;
+
+#pragma endregion IHttpBaseProtocolFilter
+ };
+
+ Mocks Mocks;
+
+ MockHttpBaseFilter() noexcept;
+
+#pragma region IHttpFilter
+
+ winrt::Windows::Foundation::IAsyncOperationWithProgress<
+ winrt::Windows::Web::Http::HttpResponseMessage,
+ winrt::Windows::Web::Http::HttpProgress>
+ SendRequestAsync(winrt::Windows::Web::Http::HttpRequestMessage const &request);
+
+#pragma endregion IHttpFilter
+
+#pragma region IHttpBaseProtocolFilter
+
+ bool AllowAutoRedirect() const;
+ void AllowAutoRedirect(bool value) const;
+
+ bool AllowUI() const;
+ void AllowUI(bool value) const;
+
+ bool AutomaticDecompression() const;
+ void AutomaticDecompression(bool value) const;
+
+ winrt::Windows::Web::Http::Filters::HttpCacheControl CacheControl() const;
+
+ winrt::Windows::Web::Http::HttpCookieManager CookieManager() const;
+
+ winrt::Windows::Security::Cryptography::Certificates::Certificate ClientCertificate() const;
+ void ClientCertificate(winrt::Windows::Security::Cryptography::Certificates::Certificate const &value) const;
+
+ winrt::Windows::Foundation::Collections::IVector<
+ winrt::Windows::Security::Cryptography::Certificates::ChainValidationResult>
+ IgnorableServerCertificateErrors() const;
+
+ uint32_t MaxConnectionsPerServer() const;
+ void MaxConnectionsPerServer(uint32_t value) const;
+
+ winrt::Windows::Security::Credentials::PasswordCredential ProxyCredential() const;
+ void ProxyCredential(winrt::Windows::Security::Credentials::PasswordCredential const &value) const;
+
+ winrt::Windows::Security::Credentials::PasswordCredential ServerCredential() const;
+ void ServerCredential(winrt::Windows::Security::Credentials::PasswordCredential const &value) const;
+
+ bool UseProxy() const;
+ void UseProxy(bool value) const;
+
+#pragma endregion IHttpBaseProtocolFilter
+};
+
} // namespace Microsoft::React::Test
diff --git a/vnext/Microsoft.ReactNative.Managed.UnitTests/packages.lock.json b/vnext/Microsoft.ReactNative.Managed.UnitTests/packages.lock.json
index 265cfe2c208..dec34f0ba84 100644
--- a/vnext/Microsoft.ReactNative.Managed.UnitTests/packages.lock.json
+++ b/vnext/Microsoft.ReactNative.Managed.UnitTests/packages.lock.json
@@ -75,10 +75,25 @@
"resolved": "1.0.1",
"contentHash": "rkn+fKobF/cbWfnnfBOQHKVKIOpxMZBvlSHkqDWgBpwGDcLRduvs3D9OLGeV6GWGvVwNlVi2CBbTjuPmtHvyNw=="
},
+ "Microsoft.UI.Xaml": {
+ "type": "Transitive",
+ "resolved": "2.7.0",
+ "contentHash": "dB4im13tfmMgL/V3Ei+3kD2rUF+/lTxAmR4gjJ45l577eljHfdo/KUrxpq/3I1Vp6e5GCDG1evDaEGuDxypLMg=="
+ },
+ "Microsoft.Windows.CppWinRT": {
+ "type": "Transitive",
+ "resolved": "2.0.211028.7",
+ "contentHash": "JBGI0c3WLoU6aYJRy9Qo0MLDQfObEp+d4nrhR95iyzf7+HOgjRunHDp/6eGFREd7xq3OI1mll9ecJrMfzBvlyg=="
+ },
+ "Microsoft.Windows.SDK.BuildTools": {
+ "type": "Transitive",
+ "resolved": "10.0.22000.194",
+ "contentHash": "4L0P3zqut466SIqT3VBeLTNUQTxCBDOrTRymRuROCRJKazcK7ibLz9yAO1nKWRt50ttCj39oAa2Iuz9ZTDmLlg=="
+ },
"NETStandard.Library": {
"type": "Transitive",
"resolved": "2.0.3",
- "contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
+ "contentHash": "548M6mnBSJWxsIlkQHfbzoYxpiYFXZZSL00p4GHYv8PkiqFBnnT68mW5mGEsA/ch9fDO9GkPgkFQpWiXZN7mAQ==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0"
}
@@ -272,12 +287,42 @@
"microsoft.reactnative": {
"type": "Project"
},
+ "fmt": {
+ "type": "Project"
+ },
+ "folly": {
+ "type": "Project",
+ "dependencies": {
+ "boost": "1.76.0",
+ "fmt": "1.0.0"
+ }
+ },
+ "microsoft.reactnative": {
+ "type": "Project",
+ "dependencies": {
+ "Common": "1.0.0",
+ "Folly": "1.0.0",
+ "Microsoft.UI.Xaml": "2.7.0",
+ "Microsoft.Windows.CppWinRT": "2.0.211028.7",
+ "Microsoft.Windows.SDK.BuildTools": "10.0.22000.194",
+ "ReactCommon": "1.0.0",
+ "ReactNative.Hermes.Windows": "0.11.0-ms.6",
+ "boost": "1.76.0"
+ }
+ },
"microsoft.reactnative.managed": {
"type": "Project",
"dependencies": {
"Microsoft.NETCore.UniversalWindowsPlatform": "6.2.9",
"Microsoft.ReactNative": "1.0.0"
}
+ },
+ "reactcommon": {
+ "type": "Project",
+ "dependencies": {
+ "Folly": "1.0.0",
+ "boost": "1.76.0"
+ }
}
},
"UAP,Version=v10.0.16299/win10-arm": {
@@ -1761,4 +1806,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/vnext/Microsoft.ReactNative.Managed/packages.lock.json b/vnext/Microsoft.ReactNative.Managed/packages.lock.json
index 04e54db679e..decd73cbdce 100644
--- a/vnext/Microsoft.ReactNative.Managed/packages.lock.json
+++ b/vnext/Microsoft.ReactNative.Managed/packages.lock.json
@@ -24,6 +24,11 @@
"Microsoft.SourceLink.Common": "1.0.0"
}
},
+ "boost": {
+ "type": "Transitive",
+ "resolved": "1.76.0",
+ "contentHash": "p+w3YvNdXL8Cu9Fzrmexssu0tZbWxuf6ywsQqHjDlKFE5ojXHof1HIyMC3zDLfLnh80dIeFcEUAuR2Asg/XHRA=="
+ },
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
"resolved": "1.0.0",
@@ -60,14 +65,34 @@
"resolved": "1.0.0",
"contentHash": "G8DuQY8/DK5NN+3jm5wcMcd9QYD90UV7MiLmdljSJixi3U/vNaeBKmmXUqI4DJCOeWizIUEh4ALhSt58mR+5eg=="
},
+ "Microsoft.UI.Xaml": {
+ "type": "Transitive",
+ "resolved": "2.7.0",
+ "contentHash": "dB4im13tfmMgL/V3Ei+3kD2rUF+/lTxAmR4gjJ45l577eljHfdo/KUrxpq/3I1Vp6e5GCDG1evDaEGuDxypLMg=="
+ },
+ "Microsoft.Windows.CppWinRT": {
+ "type": "Transitive",
+ "resolved": "2.0.211028.7",
+ "contentHash": "JBGI0c3WLoU6aYJRy9Qo0MLDQfObEp+d4nrhR95iyzf7+HOgjRunHDp/6eGFREd7xq3OI1mll9ecJrMfzBvlyg=="
+ },
+ "Microsoft.Windows.SDK.BuildTools": {
+ "type": "Transitive",
+ "resolved": "10.0.22000.194",
+ "contentHash": "4L0P3zqut466SIqT3VBeLTNUQTxCBDOrTRymRuROCRJKazcK7ibLz9yAO1nKWRt50ttCj39oAa2Iuz9ZTDmLlg=="
+ },
"NETStandard.Library": {
"type": "Transitive",
"resolved": "2.0.3",
- "contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
+ "contentHash": "548M6mnBSJWxsIlkQHfbzoYxpiYFXZZSL00p4GHYv8PkiqFBnnT68mW5mGEsA/ch9fDO9GkPgkFQpWiXZN7mAQ==",
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0"
}
},
+ "ReactNative.Hermes.Windows": {
+ "type": "Transitive",
+ "resolved": "0.11.0-ms.6",
+ "contentHash": "WAVLsSZBV4p/3hNC3W67su7xu3f/ZMSKxu0ON7g2GaKRbkJmH0Qyif1IlzcJwtvR48kuOdfgPu7Bgtz3AY+gqg=="
+ },
"runtime.win10-arm.Microsoft.Net.Native.Compiler": {
"type": "Transitive",
"resolved": "2.2.7-rel-27913-00",
@@ -135,8 +160,38 @@
"resolved": "2.2.9",
"contentHash": "qF6RRZKaflI+LR1YODNyWYjq5YoX8IJ2wx5y8O+AW2xO+1t/Q6Mm+jQ38zJbWnmXbrcOqUYofn7Y3/KC6lTLBQ=="
},
- "microsoft.reactnative": {
+ "common": {
"type": "Project"
+ },
+ "fmt": {
+ "type": "Project"
+ },
+ "folly": {
+ "type": "Project",
+ "dependencies": {
+ "boost": "1.76.0",
+ "fmt": "1.0.0"
+ }
+ },
+ "microsoft.reactnative": {
+ "type": "Project",
+ "dependencies": {
+ "Common": "1.0.0",
+ "Folly": "1.0.0",
+ "Microsoft.UI.Xaml": "2.7.0",
+ "Microsoft.Windows.CppWinRT": "2.0.211028.7",
+ "Microsoft.Windows.SDK.BuildTools": "10.0.22000.194",
+ "ReactCommon": "1.0.0",
+ "ReactNative.Hermes.Windows": "0.11.0-ms.6",
+ "boost": "1.76.0"
+ }
+ },
+ "reactcommon": {
+ "type": "Project",
+ "dependencies": {
+ "Folly": "1.0.0",
+ "boost": "1.76.0"
+ }
}
},
"UAP,Version=v10.0.16299/win10-arm": {
diff --git a/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp b/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp
index a2a2c9d3b36..9522dc4b59a 100644
--- a/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp
+++ b/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp
@@ -19,8 +19,12 @@
namespace Microsoft::ReactNative {
+using winrt::Microsoft::ReactNative::ReactPropertyBag;
+
namespace {
+using winrt::Microsoft::ReactNative::ReactPropertyId;
+
bool HasPackageIdentity() noexcept {
static const bool hasPackageIdentity = []() noexcept {
auto packageStatics = winrt::get_activation_factory(
@@ -35,6 +39,13 @@ bool HasPackageIdentity() noexcept {
return hasPackageIdentity;
}
+ReactPropertyId HttpUseMonolithicModuleProperty() noexcept {
+ static ReactPropertyId propId{
+ L"ReactNative.Http"
+ L"UseMonolithicModule"};
+ return propId;
+}
+
} // namespace
std::vector GetCoreModules(
@@ -50,11 +61,25 @@ std::vector GetCoreModules(
[props = context->Properties()]() { return Microsoft::React::CreateHttpModule(props); },
jsMessageQueue);
+ if (!ReactPropertyBag(context->Properties()).Get(HttpUseMonolithicModuleProperty())) {
+ modules.emplace_back(
+ Microsoft::React::GetBlobModuleName(),
+ [props = context->Properties()]() { return Microsoft::React::CreateBlobModule(props); },
+ batchingUIMessageQueue);
+
+ modules.emplace_back(
+ Microsoft::React::GetFileReaderModuleName(),
+ [props = context->Properties()]() { return Microsoft::React::CreateFileReaderModule(props); },
+ batchingUIMessageQueue);
+ }
+
modules.emplace_back(
"Timing",
[batchingUIMessageQueue]() { return facebook::react::CreateTimingModule(batchingUIMessageQueue); },
batchingUIMessageQueue);
+ // Note: `context` is moved to remove the reference from the current scope.
+ // This should either be the last usage of `context`, or the std::move call should happen later in this method.
modules.emplace_back(
NativeAnimatedModule::name,
[context = std::move(context)]() mutable { return std::make_unique(std::move(context)); },
diff --git a/vnext/Microsoft.ReactNative/Pch/pch.h b/vnext/Microsoft.ReactNative/Pch/pch.h
index bc1937d2613..34b0b02b9be 100644
--- a/vnext/Microsoft.ReactNative/Pch/pch.h
+++ b/vnext/Microsoft.ReactNative/Pch/pch.h
@@ -43,7 +43,6 @@
#include
#include
#include
-#include
#include
#include "Base/CxxReactIncludes.h"
diff --git a/vnext/Shared/Modules/BlobModule.cpp b/vnext/Shared/Modules/BlobModule.cpp
index b03b2e44269..867e45c0193 100644
--- a/vnext/Shared/Modules/BlobModule.cpp
+++ b/vnext/Shared/Modules/BlobModule.cpp
@@ -139,7 +139,7 @@ vector BlobModule::getMethods() {
auto size = blob[sizeKey].getInt();
auto socketID = jsArgAsInt(args, 1);
- winrt::array_view data;
+ winrt::array_view data;
try {
data = persistor->ResolveMessage(std::move(blobId), offset, size);
} catch (const std::exception &e) {
@@ -169,7 +169,7 @@ vector BlobModule::getMethods() {
auto type = part[typeKey].asString();
if (blobKey == type) {
auto blob = part[dataKey];
- winrt::array_view bufferPart;
+ winrt::array_view bufferPart;
try {
bufferPart = persistor->ResolveMessage(
blob[blobIdKey].asString(), blob[offsetKey].asInt(), blob[sizeKey].asInt());
@@ -216,7 +216,7 @@ vector BlobModule::getMethods() {
#pragma region IBlobPersistor
-winrt::array_view MemoryBlobPersistor::ResolveMessage(string &&blobId, int64_t offset, int64_t size) {
+winrt::array_view MemoryBlobPersistor::ResolveMessage(string &&blobId, int64_t offset, int64_t size) {
if (size < 1)
return {};
@@ -233,7 +233,7 @@ winrt::array_view MemoryBlobPersistor::ResolveMessage(string &&blobId,
if (endBound > bytes.size() || offset >= static_cast(bytes.size()) || offset < 0)
throw std::out_of_range("Offset or size out of range");
- return winrt::array_view(bytes.data() + offset, bytes.data() + endBound);
+ return winrt::array_view(bytes.data() + offset, bytes.data() + endBound);
}
void MemoryBlobPersistor::RemoveMessage(string &&blobId) noexcept {
diff --git a/vnext/Shared/Modules/BlobModule.h b/vnext/Shared/Modules/BlobModule.h
index 06e9b0b4695..76c5d46e5e7 100644
--- a/vnext/Shared/Modules/BlobModule.h
+++ b/vnext/Shared/Modules/BlobModule.h
@@ -30,7 +30,7 @@ class MemoryBlobPersistor final : public IBlobPersistor {
public:
#pragma region IBlobPersistor
- winrt::array_view ResolveMessage(std::string &&blobId, int64_t offset, int64_t size) override;
+ winrt::array_view ResolveMessage(std::string &&blobId, int64_t offset, int64_t size) override;
void RemoveMessage(std::string &&blobId) noexcept override;
diff --git a/vnext/Shared/Modules/FileReaderModule.cpp b/vnext/Shared/Modules/FileReaderModule.cpp
index a23328c0e15..9f7919e31fc 100644
--- a/vnext/Shared/Modules/FileReaderModule.cpp
+++ b/vnext/Shared/Modules/FileReaderModule.cpp
@@ -71,7 +71,7 @@ std::vector FileReaderModule::getMethods() {
auto offset = blob["offset"].asInt();
auto size = blob["size"].asInt();
- winrt::array_view bytes;
+ winrt::array_view bytes;
try {
bytes = blobPersistor->ResolveMessage(std::move(blobId), offset, size);
} catch (const std::exception &e) {
@@ -116,7 +116,7 @@ std::vector FileReaderModule::getMethods() {
auto offset = blob["offset"].asInt();
auto size = blob["size"].asInt();
- winrt::array_view bytes;
+ winrt::array_view bytes;
try {
bytes = blobPersistor->ResolveMessage(std::move(blobId), offset, size);
} catch (const std::exception &e) {
diff --git a/vnext/Shared/Modules/HttpModule.cpp b/vnext/Shared/Modules/HttpModule.cpp
index 0c9f2947af2..d6d39abc785 100644
--- a/vnext/Shared/Modules/HttpModule.cpp
+++ b/vnext/Shared/Modules/HttpModule.cpp
@@ -72,9 +72,11 @@ static void SetUpHttpResource(
};
resource->SetOnData(std::move(onDataDynamic));
- resource->SetOnError([weakReactInstance](int64_t requestId, string &&message) {
+ resource->SetOnError([weakReactInstance](int64_t requestId, string &&message, bool isTimeout) {
dynamic args = dynamic::array(requestId, std::move(message));
- // TODO: isTimeout errorArgs.push_back(true);
+ if (isTimeout) {
+ args.push_back(true);
+ }
SendEvent(weakReactInstance, completedResponse, std::move(args));
});
@@ -106,90 +108,90 @@ std::map HttpModule::getConstants() {
}
// clang-format off
-std::vector HttpModule::getMethods() {
+ std::vector HttpModule::getMethods() {
- return
- {
+ return
{
- "sendRequest",
- [weakHolder = weak_ptr(m_holder)](dynamic args, Callback cxxCallback)
{
- auto holder = weakHolder.lock();
- if (!holder) {
- return;
- }
-
- auto resource = holder->Module->m_resource;
- if (!holder->Module->m_isResourceSetup)
+ "sendRequest",
+ [weakHolder = weak_ptr(m_holder)](dynamic args, Callback cxxCallback)
{
- SetUpHttpResource(resource, holder->Module->getInstance(), holder->Module->m_inspectableProperties);
- holder->Module->m_isResourceSetup = true;
- }
+ auto holder = weakHolder.lock();
+ if (!holder) {
+ return;
+ }
- auto params = facebook::xplat::jsArgAsObject(args, 0);
- IHttpResource::Headers headers;
- for (auto& header : params["headers"].items()) {
- headers.emplace(header.first.getString(), header.second.getString());
- }
+ auto resource = holder->Module->m_resource;
+ if (!holder->Module->m_isResourceSetup)
+ {
+ SetUpHttpResource(resource, holder->Module->getInstance(), holder->Module->m_inspectableProperties);
+ holder->Module->m_isResourceSetup = true;
+ }
- resource->SendRequest(
- params["method"].asString(),
- params["url"].asString(),
- params["requestId"].asInt(),
- std::move(headers),
- std::move(params["data"]),
- params["responseType"].asString(),
- params["incrementalUpdates"].asBool(),
- static_cast(params["timeout"].asDouble()),
- params["withCredentials"].asBool(),
- [cxxCallback = std::move(cxxCallback)](int64_t requestId) {
- cxxCallback({requestId});
+ auto params = facebook::xplat::jsArgAsObject(args, 0);
+ IHttpResource::Headers headers;
+ for (auto& header : params["headers"].items()) {
+ headers.emplace(header.first.getString(), header.second.getString());
}
- );
- }
- },
- {
- "abortRequest",
- [weakHolder = weak_ptr(m_holder)](dynamic args)
+
+ resource->SendRequest(
+ params["method"].asString(),
+ params["url"].asString(),
+ params["requestId"].asInt(),
+ std::move(headers),
+ std::move(params["data"]),
+ params["responseType"].asString(),
+ params["incrementalUpdates"].asBool(),
+ static_cast(params["timeout"].asDouble()),
+ params["withCredentials"].asBool(),
+ [cxxCallback = std::move(cxxCallback)](int64_t requestId) {
+ cxxCallback({requestId});
+ }
+ );
+ }
+ },
{
- auto holder = weakHolder.lock();
- if (!holder)
+ "abortRequest",
+ [weakHolder = weak_ptr(m_holder)](dynamic args)
{
- return;
- }
+ auto holder = weakHolder.lock();
+ if (!holder)
+ {
+ return;
+ }
- auto resource = holder->Module->m_resource;
- if (!holder->Module->m_isResourceSetup)
- {
- SetUpHttpResource(resource, holder->Module->getInstance(), holder->Module->m_inspectableProperties);
- holder->Module->m_isResourceSetup = true;
- }
+ auto resource = holder->Module->m_resource;
+ if (!holder->Module->m_isResourceSetup)
+ {
+ SetUpHttpResource(resource, holder->Module->getInstance(), holder->Module->m_inspectableProperties);
+ holder->Module->m_isResourceSetup = true;
+ }
- resource->AbortRequest(facebook::xplat::jsArgAsInt(args, 0));
- }
- },
- {
- "clearCookies",
- [weakHolder = weak_ptr(m_holder)](dynamic args)
+ resource->AbortRequest(facebook::xplat::jsArgAsInt(args, 0));
+ }
+ },
{
- auto holder = weakHolder.lock();
- if (!holder)
+ "clearCookies",
+ [weakHolder = weak_ptr(m_holder)](dynamic args)
{
- return;
- }
+ auto holder = weakHolder.lock();
+ if (!holder)
+ {
+ return;
+ }
- auto resource = holder->Module->m_resource;
- if (!holder->Module->m_isResourceSetup)
- {
- SetUpHttpResource(resource, holder->Module->getInstance(), holder->Module->m_inspectableProperties);
- holder->Module->m_isResourceSetup = true;
- }
+ auto resource = holder->Module->m_resource;
+ if (!holder->Module->m_isResourceSetup)
+ {
+ SetUpHttpResource(resource, holder->Module->getInstance(), holder->Module->m_inspectableProperties);
+ holder->Module->m_isResourceSetup = true;
+ }
- resource->ClearCookies();
+ resource->ClearCookies();
+ }
}
- }
- };
-}
+ };
+ }
// clang-format on
#pragma endregion CxxModule
diff --git a/vnext/Shared/Modules/IBlobPersistor.h b/vnext/Shared/Modules/IBlobPersistor.h
index b1035fc461e..e4aaf5017e0 100644
--- a/vnext/Shared/Modules/IBlobPersistor.h
+++ b/vnext/Shared/Modules/IBlobPersistor.h
@@ -18,7 +18,7 @@ struct IBlobPersistor {
/// When an entry for blobId cannot be found.
///
///
- virtual winrt::array_view ResolveMessage(std::string &&blobId, int64_t offset, int64_t size) = 0;
+ virtual winrt::array_view ResolveMessage(std::string &&blobId, int64_t offset, int64_t size) = 0;
virtual void RemoveMessage(std::string &&blobId) noexcept = 0;
diff --git a/vnext/Shared/Modules/WebSocketModule.cpp b/vnext/Shared/Modules/WebSocketModule.cpp
index 2ccb180e26e..8ec5e4cbca9 100644
--- a/vnext/Shared/Modules/WebSocketModule.cpp
+++ b/vnext/Shared/Modules/WebSocketModule.cpp
@@ -187,23 +187,23 @@ std::map WebSocketModule::getConstants() {
}
// clang-format off
-std::vector WebSocketModule::getMethods()
-{
- return
+ std::vector WebSocketModule::getMethods()
{
+ return
{
- "connect",
- [weakState = weak_ptr(m_sharedState)](dynamic args) // const string& url, dynamic protocols, dynamic options, int64_t id
{
- IWebSocketResource::Protocols protocols;
- dynamic protocolsDynamic = jsArgAsDynamic(args, 1);
- if (!protocolsDynamic.empty())
+ "connect",
+ [weakState = weak_ptr(m_sharedState)](dynamic args) // const string& url, dynamic protocols, dynamic options, int64_t id
{
- for (const auto& protocol : protocolsDynamic)
+ IWebSocketResource::Protocols protocols;
+ dynamic protocolsDynamic = jsArgAsDynamic(args, 1);
+ if (!protocolsDynamic.empty())
{
- protocols.push_back(protocol.getString());
+ for (const auto& protocol : protocolsDynamic)
+ {
+ protocols.push_back(protocol.getString());
+ }
}
- }
IWebSocketResource::Options options;
dynamic optionsDynamic = jsArgAsDynamic(args, 2);
@@ -216,79 +216,79 @@ std::vector WebSocketModule::getMeth
}
}
- weak_ptr weakWs = GetOrCreateWebSocket(jsArgAsInt(args, 3), jsArgAsString(args, 0), weakState);
- if (auto sharedWs = weakWs.lock())
- {
- sharedWs->Connect(jsArgAsString(args, 0), protocols, options);
- }
- }
- },
- {
- "close",
- [weakState = weak_ptr(m_sharedState)](dynamic args) // [int64_t code, string reason,] int64_t id
- {
- // See react-native\Libraries\WebSocket\WebSocket.js:_close
- if (args.size() == 3) // WebSocketModule.close(statusCode, closeReason, this._socketId);
- {
- weak_ptr weakWs = GetOrCreateWebSocket(jsArgAsInt(args, 2), {}, weakState);
+ weak_ptr weakWs = GetOrCreateWebSocket(jsArgAsInt(args, 3), jsArgAsString(args, 0), weakState);
if (auto sharedWs = weakWs.lock())
{
- sharedWs->Close(static_cast(jsArgAsInt(args, 0)), jsArgAsString(args, 1));
+ sharedWs->Connect(jsArgAsString(args, 0), protocols, options);
}
}
- else if (args.size() == 1) // WebSocketModule.close(this._socketId);
+ },
+ {
+ "close",
+ [weakState = weak_ptr(m_sharedState)](dynamic args) // [int64_t code, string reason,] int64_t id
{
- weak_ptr weakWs = GetOrCreateWebSocket(jsArgAsInt(args, 0), {}, weakState);
- if (auto sharedWs = weakWs.lock())
+ // See react-native\Libraries\WebSocket\WebSocket.js:_close
+ if (args.size() == 3) // WebSocketModule.close(statusCode, closeReason, this._socketId);
{
- sharedWs->Close(IWebSocketResource::CloseCode::Normal, {});
+ weak_ptr weakWs = GetOrCreateWebSocket(jsArgAsInt(args, 2), {}, weakState);
+ if (auto sharedWs = weakWs.lock())
+ {
+ sharedWs->Close(static_cast(jsArgAsInt(args, 0)), jsArgAsString(args, 1));
+ }
}
- }
- else
- {
- auto state = weakState.lock();
- if (state && state->Module) {
- auto errorObj = dynamic::object("id", -1)("message", "Incorrect number of parameters");
- SendEvent(state->Module->getInstance(), "websocketFailed", std::move(errorObj));
+ else if (args.size() == 1) // WebSocketModule.close(this._socketId);
+ {
+ weak_ptr weakWs = GetOrCreateWebSocket(jsArgAsInt(args, 0), {}, weakState);
+ if (auto sharedWs = weakWs.lock())
+ {
+ sharedWs->Close(IWebSocketResource::CloseCode::Normal, {});
+ }
+ }
+ else
+ {
+ auto state = weakState.lock();
+ if (state && state->Module) {
+ auto errorObj = dynamic::object("id", -1)("message", "Incorrect number of parameters");
+ SendEvent(state->Module->getInstance(), "websocketFailed", std::move(errorObj));
+ }
}
}
- }
- },
- {
- "send",
- [weakState = weak_ptr(m_sharedState)](dynamic args) // const string& message, int64_t id
+ },
{
- weak_ptr weakWs = GetOrCreateWebSocket(jsArgAsInt(args, 1), {}, weakState);
- if (auto sharedWs = weakWs.lock())
+ "send",
+ [weakState = weak_ptr(m_sharedState)](dynamic args) // const string& message, int64_t id
{
- sharedWs->Send(jsArgAsString(args, 0));
+ weak_ptr weakWs = GetOrCreateWebSocket(jsArgAsInt(args, 1), {}, weakState);
+ if (auto sharedWs = weakWs.lock())
+ {
+ sharedWs->Send(jsArgAsString(args, 0));
+ }
}
- }
- },
- {
- "sendBinary",
- [weakState = weak_ptr(m_sharedState)](dynamic args) // const string& base64String, int64_t id
+ },
{
- weak_ptr weakWs = GetOrCreateWebSocket(jsArgAsInt(args, 1), {}, weakState);
- if (auto sharedWs = weakWs.lock())
+ "sendBinary",
+ [weakState = weak_ptr(m_sharedState)](dynamic args) // const string& base64String, int64_t id
{
- sharedWs->SendBinary(jsArgAsString(args, 0));
+ weak_ptr weakWs = GetOrCreateWebSocket(jsArgAsInt(args, 1), {}, weakState);
+ if (auto sharedWs = weakWs.lock())
+ {
+ sharedWs->SendBinary(jsArgAsString(args, 0));
+ }
}
- }
- },
- {
- "ping",
- [weakState = weak_ptr(m_sharedState)](dynamic args) // int64_t id
+ },
{
- weak_ptr weakWs = GetOrCreateWebSocket(jsArgAsInt(args, 0), {}, weakState);
- if (auto sharedWs = weakWs.lock())
+ "ping",
+ [weakState = weak_ptr(m_sharedState)](dynamic args) // int64_t id
{
- sharedWs->Ping();
+ weak_ptr weakWs = GetOrCreateWebSocket(jsArgAsInt(args, 0), {}, weakState);
+ if (auto sharedWs = weakWs.lock())
+ {
+ sharedWs->Ping();
+ }
}
}
- }
- };
-} // getMethods
+ };
+ } // getMethods
// clang-format on
#pragma endregion WebSocketModule
diff --git a/vnext/Shared/Networking/IHttpResource.h b/vnext/Shared/Networking/IHttpResource.h
index 7a8434e9f6f..17d791d1868 100644
--- a/vnext/Shared/Networking/IHttpResource.h
+++ b/vnext/Shared/Networking/IHttpResource.h
@@ -71,6 +71,7 @@ struct IHttpResource {
///
///
/// Request timeout in miliseconds.
+ /// Note: A value of 0 means no timeout. The resource will await the response indefinitely.
///
///
/// Allow including credentials in request.
@@ -95,7 +96,7 @@ struct IHttpResource {
virtual void SetOnData(std::function &&handler) noexcept = 0;
virtual void SetOnData(std::function &&handler) noexcept = 0;
virtual void SetOnError(
- std::function &&handler) noexcept = 0;
+ std::function &&handler) noexcept = 0;
};
} // namespace Microsoft::React::Networking
diff --git a/vnext/Shared/Networking/IRedirectEventSource.h b/vnext/Shared/Networking/IRedirectEventSource.h
new file mode 100644
index 00000000000..90e18fcccbe
--- /dev/null
+++ b/vnext/Shared/Networking/IRedirectEventSource.h
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#pragma once
+
+#include
+#include
+#include
+
+namespace Microsoft::React::Networking {
+
+struct IRedirectEventSource : winrt::implements {
+ virtual bool OnRedirecting(
+ winrt::Windows::Web::Http::HttpRequestMessage const &request,
+ winrt::Windows::Web::Http::HttpResponseMessage const &response) noexcept = 0;
+};
+
+} // namespace Microsoft::React::Networking
diff --git a/vnext/Shared/Networking/IWinRTHttpRequestFactory.h b/vnext/Shared/Networking/IWinRTHttpRequestFactory.h
new file mode 100644
index 00000000000..f6c060e737d
--- /dev/null
+++ b/vnext/Shared/Networking/IWinRTHttpRequestFactory.h
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#pragma once
+
+#include
+#include
+#include
+
+namespace Microsoft::React::Networking {
+
+struct IWinRTHttpRequestFactory {
+ virtual ~IWinRTHttpRequestFactory() noexcept {}
+
+ virtual winrt::Windows::Foundation::IAsyncOperation CreateRequest(
+ winrt::Windows::Web::Http::HttpMethod &&method,
+ winrt::Windows::Foundation::Uri &&uri,
+ winrt::Windows::Foundation::Collections::IMap
+ props) noexcept = 0;
+};
+
+} // namespace Microsoft::React::Networking
diff --git a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp
index b6430a119a7..bb902db79cb 100644
--- a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp
+++ b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp
@@ -11,9 +11,6 @@
#include
#include
-// Windows API
-#include
-
// Standard Library
#include
#include
@@ -27,6 +24,7 @@ using winrt::to_hstring;
using winrt::Windows::Foundation::IInspectable;
using winrt::Windows::Foundation::IPropertyValue;
using winrt::Windows::Foundation::Uri;
+using winrt::Windows::Foundation::Collections::IMap;
using winrt::Windows::Web::Http::HttpMethod;
using winrt::Windows::Web::Http::HttpRequestMessage;
using winrt::Windows::Web::Http::HttpResponseMessage;
@@ -118,7 +116,7 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co
}
/*static*/ bool OriginPolicyHttpFilter::IsSameOrigin(Uri const &u1, Uri const &u2) noexcept {
- return u1.SchemeName() == u2.SchemeName() && u1.Host() == u2.Host() && u1.Port() == u2.Port();
+ return (u1 && u2) && u1.SchemeName() == u2.SchemeName() && u1.Host() == u2.Host() && u1.Port() == u2.Port();
}
/*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const &request) noexcept {
@@ -373,7 +371,7 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co
}
}
-OriginPolicyHttpFilter::OriginPolicyHttpFilter(IHttpFilter &&innerFilter) : m_innerFilter{std::move(innerFilter)} {}
+OriginPolicyHttpFilter::OriginPolicyHttpFilter(IHttpFilter const &innerFilter) : m_innerFilter{innerFilter} {}
OriginPolicyHttpFilter::OriginPolicyHttpFilter()
: OriginPolicyHttpFilter(winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter{}) {}
@@ -442,21 +440,24 @@ OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const &r
void OriginPolicyHttpFilter::ValidateAllowOrigin(
hstring const &allowedOrigin,
hstring const &allowCredentials,
- IInspectable const &iRequestArgs) const {
+ IMap props) const {
// 4.10.1-2 - null allow origin
if (L"null" == allowedOrigin)
throw hresult_error{
E_INVALIDARG,
L"Response header Access-Control-Allow-Origin has a value of [null] which differs from the supplied origin"};
- bool withCredentials = iRequestArgs.as()->WithCredentials;
+ bool withCredentials = props.Lookup(L"RequestArgs").as()->WithCredentials;
// 4.10.3 - valid wild card allow origin
if (!withCredentials && L"*" == allowedOrigin)
return;
// We assume the source (request) origin is not "*", "null", or empty string. Valid URI is expected
// 4.10.4 - Mismatched allow origin
- if (allowedOrigin.empty() || !IsSameOrigin(s_origin, Uri{allowedOrigin})) {
+ auto taintedOriginProp = props.TryLookup(L"TaintedOrigin");
+ auto taintedOrigin = taintedOriginProp && winrt::unbox_value(taintedOriginProp);
+ auto origin = taintedOrigin ? nullptr : s_origin;
+ if (allowedOrigin.empty() || !IsSameOrigin(origin, Uri{allowedOrigin})) {
hstring errorMessage;
if (allowedOrigin.empty())
errorMessage = L"No valid origin in response";
@@ -510,14 +511,14 @@ void OriginPolicyHttpFilter::ValidatePreflightResponse(
auto controlValues = ExtractAccessControlValues(response.Headers());
- auto iRequestArgs = request.Properties().Lookup(L"RequestArgs");
+ auto props = request.Properties();
// Check if the origin is allowed in conjuction with the withCredentials flag
// CORS preflight should always exclude credentials although the subsequent CORS request may include credentials.
- ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, iRequestArgs);
+ ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props);
// See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.5
// Check if the request method is allowed
- bool withCredentials = iRequestArgs.as()->WithCredentials;
+ bool withCredentials = props.Lookup(L"RequestArgs").as()->WithCredentials;
bool requestMethodAllowed = false;
for (const auto &method : controlValues.AllowedMethods) {
if (L"*" == method) {
@@ -578,8 +579,8 @@ void OriginPolicyHttpFilter::ValidateResponse(HttpResponseMessage const &respons
if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing ||
originPolicy == OriginPolicy::CrossOriginResourceSharing) {
auto controlValues = ExtractAccessControlValues(response.Headers());
- auto withCredentials =
- response.RequestMessage().Properties().Lookup(L"RequestArgs").try_as()->WithCredentials;
+ auto props = response.RequestMessage().Properties();
+ auto withCredentials = props.Lookup(L"RequestArgs").try_as()->WithCredentials;
if (GetRuntimeOptionBool("Http.StrictOriginCheckSimpleCors") &&
originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) {
@@ -594,8 +595,7 @@ void OriginPolicyHttpFilter::ValidateResponse(HttpResponseMessage const &respons
throw hresult_error{E_INVALIDARG, L"The server does not support CORS or the origin is not allowed"};
}
} else {
- auto iRequestArgs = response.RequestMessage().Properties().Lookup(L"RequestArgs");
- ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, iRequestArgs);
+ ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props);
}
if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) {
@@ -678,6 +678,38 @@ ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage
co_return {co_await m_innerFilter.SendRequestAsync(preflightRequest)};
}
+#pragma region IRedirectEventSource
+
+bool OriginPolicyHttpFilter::OnRedirecting(
+ HttpRequestMessage const &request,
+ HttpResponseMessage const &response) noexcept {
+ // Consider the following scenario.
+ // User signs in to http://a.com and visits a page that makes CORS request to http://b.com with origin=http://a.com.
+ // Http://b.com reponds with a redirect to http://a.com. The browser follows the redirect to http://a.com with
+ // origin=http://a.com. Since the origin matches the URL, the request is authorized at http://a.com, but it actually
+ // allows http://b.com to bypass the CORS check at http://a.com since the redirected URL is from http://b.com.
+ if (!IsSameOrigin(response.Headers().Location(), request.RequestUri()) &&
+ !IsSameOrigin(s_origin, request.RequestUri())) {
+ // By masking the origin field in the request header, we make it impossible for the server to set a single value for
+ // the access-control-allow-origin header. It means, the only way to support redirect is that server allows access
+ // from all sites through wildcard.
+ request.Headers().Insert(L"Origin", L"null");
+
+ auto props = request.Properties();
+ // Look for 'RequestArgs' key to ensure we are redirecting the main request.
+ if (auto iReqArgs = props.TryLookup(L"RequestArgs")) {
+ props.Insert(L"TaintedOrigin", winrt::box_value(true));
+ } else {
+ // Abort redirection if the request is either preflight or extraneous.
+ return false;
+ }
+ }
+
+ return true;
+}
+
+#pragma endregion IRedirectEventSource
+
#pragma region IHttpFilter
ResponseOperation OriginPolicyHttpFilter::SendRequestAsync(HttpRequestMessage const &request) {
diff --git a/vnext/Shared/Networking/OriginPolicyHttpFilter.h b/vnext/Shared/Networking/OriginPolicyHttpFilter.h
index e0e6e68b6b8..49426d62eab 100644
--- a/vnext/Shared/Networking/OriginPolicyHttpFilter.h
+++ b/vnext/Shared/Networking/OriginPolicyHttpFilter.h
@@ -3,11 +3,14 @@
#pragma once
+#include "IRedirectEventSource.h"
#include "OriginPolicy.h"
// Windows API
+#include
#include
#include
+#include
#include
// Standard Library
@@ -16,7 +19,8 @@
namespace Microsoft::React::Networking {
class OriginPolicyHttpFilter
- : public winrt::implements {
+ : public winrt::
+ implements {
public:
struct ConstWcharComparer {
bool operator()(const wchar_t *, const wchar_t *) const;
@@ -75,7 +79,7 @@ class OriginPolicyHttpFilter
winrt::Windows::Web::Http::HttpResponseMessage const &response,
bool removeAll);
- OriginPolicyHttpFilter(winrt::Windows::Web::Http::Filters::IHttpFilter &&innerFilter);
+ OriginPolicyHttpFilter(winrt::Windows::Web::Http::Filters::IHttpFilter const &innerFilter);
OriginPolicyHttpFilter();
@@ -92,13 +96,22 @@ class OriginPolicyHttpFilter
void ValidateAllowOrigin(
winrt::hstring const &allowedOrigin,
winrt::hstring const &allowCredentials,
- winrt::Windows::Foundation::IInspectable const &iArgs) const;
+ winrt::Windows::Foundation::Collections::IMap props)
+ const;
winrt::Windows::Foundation::IAsyncOperationWithProgress<
winrt::Windows::Web::Http::HttpResponseMessage,
winrt::Windows::Web::Http::HttpProgress>
SendPreflightAsync(winrt::Windows::Web::Http::HttpRequestMessage const &request) const;
+#pragma region IRedirectEventSource
+
+ bool OnRedirecting(
+ winrt::Windows::Web::Http::HttpRequestMessage const &request,
+ winrt::Windows::Web::Http::HttpResponseMessage const &response) noexcept override;
+
+#pragma endregion IRedirectEventSource
+
#pragma region IHttpFilter
winrt::Windows::Foundation::IAsyncOperationWithProgress<
diff --git a/vnext/Shared/Networking/RedirectHttpFilter.cpp b/vnext/Shared/Networking/RedirectHttpFilter.cpp
new file mode 100644
index 00000000000..0ed67360b29
--- /dev/null
+++ b/vnext/Shared/Networking/RedirectHttpFilter.cpp
@@ -0,0 +1,282 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#undef WINRT_LEAN_AND_MEAN
+
+#include "RedirectHttpFilter.h"
+
+#include "WinRTTypes.h"
+
+// Windows API
+#include
+#include
+#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP)
+#include
+#else
+#define INTERNET_ERROR_BASE 12000
+#define ERROR_HTTP_REDIRECT_FAILED (INTERNET_ERROR_BASE + 156)
+#endif
+
+namespace {
+constexpr size_t DefaultMaxRedirects = 20;
+} // namespace
+
+using winrt::Windows::Foundation::Uri;
+using winrt::Windows::Foundation::Collections::IVector;
+using winrt::Windows::Security::Credentials::PasswordCredential;
+using winrt::Windows::Security::Cryptography::Certificates::Certificate;
+using winrt::Windows::Security::Cryptography::Certificates::ChainValidationResult;
+using winrt::Windows::Web::Http::HttpMethod;
+using winrt::Windows::Web::Http::HttpRequestMessage;
+using winrt::Windows::Web::Http::HttpResponseMessage;
+using winrt::Windows::Web::Http::HttpStatusCode;
+using winrt::Windows::Web::Http::Filters::IHttpBaseProtocolFilter;
+using winrt::Windows::Web::Http::Filters::IHttpFilter;
+
+namespace Microsoft::React::Networking {
+
+#pragma region RedirectHttpFilter
+
+RedirectHttpFilter::RedirectHttpFilter(
+ size_t maxRedirects,
+ IHttpFilter &&innerFilter,
+ IHttpFilter &&innerFilterWithNoCredentials) noexcept
+ : m_maximumRedirects{maxRedirects},
+ m_innerFilter{std::move(innerFilter)},
+ m_innerFilterWithNoCredentials{std::move(innerFilterWithNoCredentials)} {
+ // Prevent automatic redirections.
+ if (auto baseFilter = m_innerFilter.try_as()) {
+ baseFilter.AllowAutoRedirect(false);
+ baseFilter.AllowUI(false);
+ }
+ if (auto baseFilter = m_innerFilterWithNoCredentials.try_as()) {
+ baseFilter.AllowAutoRedirect(false);
+ baseFilter.AllowUI(false);
+ }
+}
+
+RedirectHttpFilter::RedirectHttpFilter(IHttpFilter &&innerFilter, IHttpFilter &&innerFilterWithNoCredentials) noexcept
+ : RedirectHttpFilter(DefaultMaxRedirects, std::move(innerFilter), std::move(innerFilterWithNoCredentials)) {}
+
+RedirectHttpFilter::RedirectHttpFilter() noexcept
+ : RedirectHttpFilter(
+ winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter{},
+ winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter{}) {}
+
+void RedirectHttpFilter::SetRequestFactory(std::weak_ptr factory) noexcept {
+ m_requestFactory = factory;
+}
+
+void RedirectHttpFilter::SetRedirectSource(
+ winrt::com_ptr const &eventSrc) noexcept {
+ m_redirEventSrc = eventSrc;
+}
+
+#pragma region IHttpBaseProtocolFilter
+
+bool RedirectHttpFilter::AllowAutoRedirect() const {
+ return m_allowAutoRedirect;
+}
+
+void RedirectHttpFilter::AllowAutoRedirect(bool value) {
+ m_allowAutoRedirect = value;
+}
+
+bool RedirectHttpFilter::AllowUI() const {
+ return false;
+}
+void RedirectHttpFilter::AllowUI(bool /*value*/) const {
+ throw winrt::hresult_error{HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)};
+}
+
+bool RedirectHttpFilter::AutomaticDecompression() const {
+ if (auto baseFilter = m_innerFilter.try_as()) {
+ return baseFilter.AutomaticDecompression();
+ }
+
+ return false;
+}
+void RedirectHttpFilter::AutomaticDecompression(bool value) const {
+ if (auto baseFilter = m_innerFilter.try_as()) {
+ baseFilter.AutomaticDecompression(value);
+ }
+}
+
+winrt::Windows::Web::Http::Filters::HttpCacheControl RedirectHttpFilter::CacheControl() const {
+ if (auto baseFilter = m_innerFilter.try_as()) {
+ return baseFilter.CacheControl();
+ }
+
+ return nullptr;
+}
+
+winrt::Windows::Web::Http::HttpCookieManager RedirectHttpFilter::CookieManager() const {
+ if (auto baseFilter = m_innerFilter.try_as()) {
+ return baseFilter.CookieManager();
+ }
+
+ return nullptr;
+}
+
+Certificate RedirectHttpFilter::ClientCertificate() const {
+ if (auto baseFilter = m_innerFilter.try_as()) {
+ return baseFilter.ClientCertificate();
+ }
+
+ return nullptr;
+}
+void RedirectHttpFilter::ClientCertificate(Certificate const &value) const {
+ if (auto baseFilter = m_innerFilter.try_as()) {
+ baseFilter.ClientCertificate(value);
+ }
+}
+
+IVector RedirectHttpFilter::IgnorableServerCertificateErrors() const {
+ if (auto baseFilter = m_innerFilter.try_as()) {
+ return baseFilter.IgnorableServerCertificateErrors();
+ }
+
+ return nullptr;
+}
+
+uint32_t RedirectHttpFilter::MaxConnectionsPerServer() const {
+ if (auto baseFilter = m_innerFilter.try_as()) {
+ return baseFilter.MaxConnectionsPerServer();
+ }
+
+ return 0;
+}
+void RedirectHttpFilter::MaxConnectionsPerServer(uint32_t value) const {
+ if (auto baseFilter = m_innerFilter.try_as()) {
+ baseFilter.MaxConnectionsPerServer(value);
+ }
+}
+
+PasswordCredential RedirectHttpFilter::ProxyCredential() const {
+ if (auto baseFilter = m_innerFilter.try_as()) {
+ return baseFilter.ProxyCredential();
+ }
+
+ return nullptr;
+}
+void RedirectHttpFilter::ProxyCredential(PasswordCredential const &value) const {
+ if (auto baseFilter = m_innerFilter.try_as()) {
+ baseFilter.ProxyCredential(value);
+ }
+}
+
+PasswordCredential RedirectHttpFilter::ServerCredential() const {
+ if (auto baseFilter = m_innerFilter.try_as()) {
+ return baseFilter.ServerCredential();
+ }
+
+ return nullptr;
+}
+void RedirectHttpFilter::ServerCredential(PasswordCredential const &value) const {
+ if (auto baseFilter = m_innerFilter.try_as()) {
+ baseFilter.ServerCredential(value);
+ }
+}
+
+bool RedirectHttpFilter::UseProxy() const {
+ if (auto baseFilter = m_innerFilter.try_as()) {
+ return baseFilter.UseProxy();
+ }
+
+ return false;
+}
+void RedirectHttpFilter::UseProxy(bool value) const {
+ if (auto baseFilter = m_innerFilter.try_as()) {
+ baseFilter.UseProxy(value);
+ }
+}
+
+#pragma endregion IHttpBaseProtocolFilter
+
+#pragma region IHttpFilter
+
+///
+/// See https://github.com/dotnet/corefx/pull/22702
+ResponseOperation RedirectHttpFilter::SendRequestAsync(HttpRequestMessage const &request) {
+ size_t redirectCount = 0;
+ HttpMethod method{nullptr};
+ HttpResponseMessage response{nullptr};
+
+ auto coRequest = request;
+ auto coAllowAutoRedirect = m_allowAutoRedirect;
+ auto coMaxRedirects = m_maximumRedirects;
+ auto coRequestFactory = m_requestFactory;
+ auto coEventSrc = m_redirEventSrc;
+
+ method = coRequest.Method();
+
+ do {
+ // 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);
+
+ // Stop redirecting when a non-redirect status is responded.
+ if (response.StatusCode() != HttpStatusCode::MultipleChoices &&
+ response.StatusCode() != HttpStatusCode::MovedPermanently &&
+ response.StatusCode() != HttpStatusCode::Found && // Redirect
+ response.StatusCode() != HttpStatusCode::SeeOther && // RedirectMethod
+ response.StatusCode() != HttpStatusCode::TemporaryRedirect && // RedirectKeepVerb
+ response.StatusCode() != HttpStatusCode::PermanentRedirect) {
+ break;
+ }
+
+ redirectCount++;
+ if (redirectCount > coMaxRedirects) {
+ throw winrt::hresult_error{HRESULT_FROM_WIN32(ERROR_HTTP_REDIRECT_FAILED), L"Too many redirects"};
+ }
+
+ // Call event source's OnRedirecting before modifying request parameters.
+ if (coEventSrc && !coEventSrc->OnRedirecting(coRequest, response)) {
+ break;
+ }
+
+ if (auto requestFactory = coRequestFactory.lock()) {
+ coRequest =
+ co_await requestFactory->CreateRequest(HttpMethod{method}, coRequest.RequestUri(), coRequest.Properties());
+
+ if (!coRequest) {
+ throw winrt::hresult_error{E_INVALIDARG, L"Invalid request handle"};
+ }
+ }
+
+ auto redirectUri = Uri{response.Headers().Location().AbsoluteUri()};
+ if (!redirectUri) {
+ break;
+ }
+
+ if (redirectUri.SchemeName() != L"http" && redirectUri.SchemeName() != L"https") {
+ break;
+ }
+
+ // Do not "downgrade" from HTTPS to HTTP
+ if (coRequest.RequestUri().SchemeName() == L"https" && redirectUri.SchemeName() == L"http") {
+ break;
+ }
+
+ /// See https://github.com/dotnet/corefx/blob/v3.1.28/src/System.Net.Http/src/uap/System/Net/HttpClientHandler.cs
+ // Follow HTTP RFC 7231 rules. In general, 3xx responses
+ // except for 307 and 308 will keep verb except POST becomes GET.
+ // 307 and 308 responses have all verbs stay the same.
+ // https://tools.ietf.org/html/rfc7231#section-6.4
+ if (response.StatusCode() != HttpStatusCode::TemporaryRedirect &&
+ response.StatusCode() != HttpStatusCode::PermanentRedirect && method != HttpMethod::Post()) {
+ method = HttpMethod::Get();
+ }
+
+ coRequest.RequestUri(redirectUri);
+ } while (coAllowAutoRedirect);
+
+ response.RequestMessage(coRequest);
+
+ co_return response;
+}
+
+#pragma endregion IHttpFilter
+
+#pragma endregion RedirectHttpFilter
+
+} // namespace Microsoft::React::Networking
diff --git a/vnext/Shared/Networking/RedirectHttpFilter.h b/vnext/Shared/Networking/RedirectHttpFilter.h
new file mode 100644
index 00000000000..949c90a8c37
--- /dev/null
+++ b/vnext/Shared/Networking/RedirectHttpFilter.h
@@ -0,0 +1,97 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#pragma once
+
+#include
+#include "IRedirectEventSource.h"
+#include "IWinRTHttpRequestFactory.h"
+
+// Windows API
+#include
+#include
+#include
+#include
+#include
+
+namespace Microsoft::React::Networking {
+
+class RedirectHttpFilter : public winrt::implements<
+ RedirectHttpFilter,
+ winrt::Windows::Web::Http::Filters::IHttpFilter,
+ winrt::Windows::Web::Http::Filters::IHttpBaseProtocolFilter> {
+ // See
+ // https://github.com/dotnet/corefx/pull/22702/files#diff-53f0c1940c6bec8054a95caac33680306aa6ab13ac48c9a8c9df013d3bc29d15R30
+ // We need two different WinRT filters because we need to remove credentials during redirection requests
+ // and WinRT doesn't allow changing the filter properties after the first request.
+ winrt::Windows::Web::Http::Filters::IHttpFilter m_innerFilter;
+ winrt::Windows::Web::Http::Filters::IHttpFilter m_innerFilterWithNoCredentials;
+
+ std::weak_ptr m_requestFactory;
+ winrt::com_ptr m_redirEventSrc;
+ bool m_allowAutoRedirect{true};
+ size_t m_maximumRedirects;
+
+ public:
+ RedirectHttpFilter(
+ size_t maxRedirects,
+ winrt::Windows::Web::Http::Filters::IHttpFilter &&innerFilter,
+ winrt::Windows::Web::Http::Filters::IHttpFilter &&innerFilterWithNoCredentials) noexcept;
+
+ RedirectHttpFilter(
+ winrt::Windows::Web::Http::Filters::IHttpFilter &&innerFilter,
+ winrt::Windows::Web::Http::Filters::IHttpFilter &&innerFilterWithNoCredentials) noexcept;
+
+ RedirectHttpFilter() noexcept;
+
+ void SetRequestFactory(std::weak_ptr factory) noexcept;
+
+ void SetRedirectSource(winrt::com_ptr const &eventSrc) noexcept;
+
+#pragma region IHttpFilter
+
+ winrt::Windows::Foundation::IAsyncOperationWithProgress<
+ winrt::Windows::Web::Http::HttpResponseMessage,
+ winrt::Windows::Web::Http::HttpProgress>
+ SendRequestAsync(winrt::Windows::Web::Http::HttpRequestMessage const &request);
+
+#pragma endregion IHttpFilter
+
+#pragma region IHttpBaseProtocolFilter
+
+ bool AllowAutoRedirect() const;
+ void AllowAutoRedirect(bool value);
+
+ bool AllowUI() const;
+ void AllowUI(bool value) const;
+
+ bool AutomaticDecompression() const;
+ void AutomaticDecompression(bool value) const;
+
+ winrt::Windows::Web::Http::Filters::HttpCacheControl CacheControl() const;
+
+ winrt::Windows::Web::Http::HttpCookieManager CookieManager() const;
+
+ winrt::Windows::Security::Cryptography::Certificates::Certificate ClientCertificate() const;
+ void ClientCertificate(winrt::Windows::Security::Cryptography::Certificates::Certificate const &value) const;
+
+ winrt::Windows::Foundation::Collections::IVector<
+ winrt::Windows::Security::Cryptography::Certificates::ChainValidationResult>
+ IgnorableServerCertificateErrors() const;
+
+ uint32_t MaxConnectionsPerServer() const;
+ void MaxConnectionsPerServer(uint32_t value) const;
+
+ winrt::Windows::Security::Credentials::PasswordCredential ProxyCredential() const;
+ void ProxyCredential(winrt::Windows::Security::Credentials::PasswordCredential const &value) const;
+
+ winrt::Windows::Security::Credentials::PasswordCredential ServerCredential() const;
+ void ServerCredential(winrt::Windows::Security::Credentials::PasswordCredential const &value) const;
+
+ bool UseProxy() const;
+ void UseProxy(bool value) const;
+
+#pragma endregion IHttpBaseProtocolFilter
+};
+
+} // namespace Microsoft::React::Networking
diff --git a/vnext/Shared/Networking/WinRTHttpResource.cpp b/vnext/Shared/Networking/WinRTHttpResource.cpp
index a496186d769..56e9899531f 100644
--- a/vnext/Shared/Networking/WinRTHttpResource.cpp
+++ b/vnext/Shared/Networking/WinRTHttpResource.cpp
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+#undef WINRT_LEAN_AND_MEAN
+
#include "WinRTHttpResource.h"
#include
@@ -8,12 +10,15 @@
#include
#include
#include
+#include "IRedirectEventSource.h"
#include "OriginPolicyHttpFilter.h"
+#include "RedirectHttpFilter.h"
// Boost Libraries
#include
// Windows API
+#include
#include
#include
#include
@@ -30,7 +35,7 @@ using std::weak_ptr;
using winrt::fire_and_forget;
using winrt::hresult_error;
using winrt::to_hstring;
-using winrt::to_string;
+using winrt::Windows::Foundation::IAsyncOperation;
using winrt::Windows::Foundation::IInspectable;
using winrt::Windows::Foundation::Uri;
using winrt::Windows::Security::Cryptography::CryptographicBuffer;
@@ -54,6 +59,138 @@ WinRTHttpResource::WinRTHttpResource(IHttpClient &&client) noexcept : m_client{s
WinRTHttpResource::WinRTHttpResource() noexcept : WinRTHttpResource(winrt::Windows::Web::Http::HttpClient{}) {}
+#pragma region IWinRTHttpRequestFactory
+
+IAsyncOperation WinRTHttpResource::CreateRequest(
+ HttpMethod &&method,
+ Uri &&uri,
+ winrt::Windows::Foundation::Collections::IMap props) noexcept /*override*/ {
+ auto request = HttpRequestMessage{std::move(method), std::move(uri)};
+ for (auto prop : props) {
+ request.Properties().Insert(prop.Key(), prop.Value());
+ }
+
+ auto iReqArgs = request.Properties().Lookup(L"RequestArgs");
+ auto reqArgs = iReqArgs.as();
+ auto self = shared_from_this();
+
+ HttpMediaTypeHeaderValue contentType{nullptr};
+ string contentEncoding;
+ string contentLength;
+
+ // Headers are generally case-insensitive
+ // https://www.ietf.org/rfc/rfc2616.txt section 4.2
+ for (auto &header : reqArgs->Headers) {
+ if (boost::iequals(header.first.c_str(), "Content-Type")) {
+ bool success = HttpMediaTypeHeaderValue::TryParse(to_hstring(header.second), contentType);
+ if (!success) {
+ if (self->m_onError) {
+ self->m_onError(reqArgs->RequestId, "Failed to parse Content-Type", false);
+ }
+ co_return nullptr;
+ }
+ } else if (boost::iequals(header.first.c_str(), "Content-Encoding")) {
+ contentEncoding = header.second;
+ } else if (boost::iequals(header.first.c_str(), "Content-Length")) {
+ contentLength = header.second;
+ } else if (boost::iequals(header.first.c_str(), "Authorization")) {
+ bool success = request.Headers().TryAppendWithoutValidation(to_hstring(header.first), to_hstring(header.second));
+ if (!success) {
+ if (self->m_onError) {
+ self->m_onError(reqArgs->RequestId, "Failed to append Authorization", false);
+ }
+ co_return nullptr;
+ }
+ } else {
+ try {
+ request.Headers().Append(to_hstring(header.first), to_hstring(header.second));
+ } catch (hresult_error const &e) {
+ if (self->m_onError) {
+ self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), false);
+ }
+ co_return nullptr;
+ }
+ }
+ }
+
+ // Initialize content
+ IHttpContent content{nullptr};
+ auto &data = reqArgs->Data;
+ if (!data.isNull()) {
+ auto bodyHandler = self->m_requestBodyHandler.lock();
+ if (bodyHandler && bodyHandler->Supports(data)) {
+ auto contentTypeString = contentType ? winrt::to_string(contentType.ToString()) : "";
+ dynamic blob;
+ try {
+ blob = bodyHandler->ToRequestBody(data, contentTypeString);
+ } catch (const std::invalid_argument &e) {
+ if (self->m_onError) {
+ self->m_onError(reqArgs->RequestId, e.what(), false);
+ }
+ co_return nullptr;
+ }
+ auto bytes = blob["bytes"];
+ auto byteVector = vector(bytes.size());
+ for (auto &byte : bytes) {
+ byteVector.push_back(static_cast(byte.asInt()));
+ }
+ auto view = winrt::array_view{byteVector};
+ auto buffer = CryptographicBuffer::CreateFromByteArray(view);
+ content = HttpBufferContent{std::move(buffer)};
+ } else if (!data["string"].isNull()) {
+ content = HttpStringContent{to_hstring(data["string"].asString())};
+ } else if (!data["base64"].empty()) {
+ auto buffer = CryptographicBuffer::DecodeFromBase64String(to_hstring(data["base64"].asString()));
+ content = HttpBufferContent{std::move(buffer)};
+ } else if (!data["uri"].empty()) {
+ auto file = co_await StorageFile::GetFileFromApplicationUriAsync(Uri{to_hstring(data["uri"].asString())});
+ auto stream = co_await file.OpenReadAsync();
+ content = HttpStreamContent{std::move(stream)};
+ } else if (!data["form"].empty()) {
+ // #9535 - HTTP form data support
+ // winrt::Windows::Web::Http::HttpMultipartFormDataContent()
+ }
+ }
+
+ // Attach content headers
+ if (content != nullptr) {
+ if (contentType) {
+ content.Headers().ContentType(contentType);
+ }
+ if (!contentEncoding.empty()) {
+ if (!content.Headers().ContentEncoding().TryParseAdd(to_hstring(contentEncoding))) {
+ if (self->m_onError)
+ self->m_onError(reqArgs->RequestId, "Failed to parse Content-Encoding", false);
+
+ co_return nullptr;
+ }
+ }
+
+ if (!contentLength.empty()) {
+ try {
+ const auto contentLengthHeader = std::stol(contentLength);
+ content.Headers().ContentLength(contentLengthHeader);
+ } catch (const std::invalid_argument &e) {
+ if (self->m_onError)
+ self->m_onError(reqArgs->RequestId, e.what() + string{" ["} + contentLength + "]", false);
+
+ co_return nullptr;
+ } catch (const std::out_of_range &e) {
+ if (self->m_onError)
+ self->m_onError(reqArgs->RequestId, e.what() + string{" ["} + contentLength + "]", false);
+
+ co_return nullptr;
+ }
+ }
+
+ request.Content(content);
+ }
+
+ co_return request;
+}
+
+#pragma endregion IWinRTHttpRequestFactory
+
#pragma region IHttpResource
void WinRTHttpResource::SendRequest(
@@ -77,29 +214,28 @@ void WinRTHttpResource::SendRequest(
try {
HttpMethod httpMethod{to_hstring(std::move(method))};
Uri uri{to_hstring(std::move(url))};
- HttpRequestMessage request{httpMethod, uri};
-
- auto args = winrt::make();
- auto concreteArgs = args.as();
- concreteArgs->RequestId = requestId;
- concreteArgs->Headers = std::move(headers);
- concreteArgs->Data = std::move(data);
- concreteArgs->IncrementalUpdates = useIncrementalUpdates;
- concreteArgs->WithCredentials = withCredentials;
- concreteArgs->ResponseType = std::move(responseType);
- concreteArgs->Timeout = timeout;
-
- PerformSendRequest(std::move(request), args);
+
+ auto iReqArgs = winrt::make();
+ auto reqArgs = iReqArgs.as();
+ reqArgs->RequestId = requestId;
+ reqArgs->Headers = std::move(headers);
+ reqArgs->Data = std::move(data);
+ reqArgs->IncrementalUpdates = useIncrementalUpdates;
+ reqArgs->WithCredentials = withCredentials;
+ reqArgs->ResponseType = std::move(responseType);
+ reqArgs->Timeout = timeout;
+
+ PerformSendRequest(std::move(httpMethod), std::move(uri), iReqArgs);
} catch (std::exception const &e) {
if (m_onError) {
- m_onError(requestId, e.what());
+ m_onError(requestId, e.what(), false);
}
} catch (hresult_error const &e) {
if (m_onError) {
- m_onError(requestId, Utilities::HResultToString(e));
+ m_onError(requestId, Utilities::HResultToString(e), false);
}
} catch (...) {
- m_onError(requestId, "Unidentified error sending HTTP request");
+ m_onError(requestId, "Unidentified error sending HTTP request", false);
}
}
@@ -118,7 +254,7 @@ void WinRTHttpResource::AbortRequest(int64_t requestId) noexcept /*override*/ {
try {
request.Cancel();
} catch (hresult_error const &e) {
- m_onError(requestId, Utilities::HResultToString(e));
+ m_onError(requestId, Utilities::HResultToString(e), false);
}
}
@@ -147,7 +283,8 @@ void WinRTHttpResource::SetOnData(function &&handler) noexcept
+void WinRTHttpResource::SetOnError(
+ function &&handler) noexcept
/*override*/ {
m_onError = std::move(handler);
}
@@ -164,146 +301,88 @@ void WinRTHttpResource::UntrackResponse(int64_t requestId) noexcept {
m_responses.erase(requestId);
}
-fire_and_forget WinRTHttpResource::PerformSendRequest(HttpRequestMessage &&request, IInspectable const &args) noexcept {
+fire_and_forget
+WinRTHttpResource::PerformSendRequest(HttpMethod &&method, Uri &&rtUri, IInspectable const &args) noexcept {
// Keep references after coroutine suspension.
auto self = shared_from_this();
- auto coRequest = std::move(request);
auto coArgs = args;
- auto coReqArgs = coArgs.as();
+ auto reqArgs = coArgs.as();
+ auto coMethod = std::move(method);
+ auto coUri = std::move(rtUri);
// Ensure background thread
co_await winrt::resume_background();
+ auto props = winrt::multi_threaded_map();
+ props.Insert(L"RequestArgs", coArgs);
+
+ auto coRequest = co_await CreateRequest(std::move(coMethod), std::move(coUri), props);
+ if (!coRequest) {
+ co_return;
+ }
+
// If URI handler is available, it takes over request processing.
if (auto uriHandler = self->m_uriHandler.lock()) {
auto uri = winrt::to_string(coRequest.RequestUri().ToString());
try {
- if (uriHandler->Supports(uri, coReqArgs->ResponseType)) {
+ if (uriHandler->Supports(uri, reqArgs->ResponseType)) {
auto blob = uriHandler->Fetch(uri);
if (self->m_onDataDynamic && self->m_onRequestSuccess) {
- self->m_onDataDynamic(coReqArgs->RequestId, std::move(blob));
- self->m_onRequestSuccess(coReqArgs->RequestId);
+ self->m_onDataDynamic(reqArgs->RequestId, std::move(blob));
+ self->m_onRequestSuccess(reqArgs->RequestId);
}
co_return;
}
} catch (const hresult_error &e) {
if (self->m_onError)
- co_return self->m_onError(coReqArgs->RequestId, Utilities::HResultToString(e));
+ co_return self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), false);
} catch (const std::exception &e) {
if (self->m_onError)
- co_return self->m_onError(coReqArgs->RequestId, e.what());
+ co_return self->m_onError(reqArgs->RequestId, e.what(), false);
}
}
- HttpMediaTypeHeaderValue contentType{nullptr};
- string contentEncoding;
- string contentLength;
+ try {
+ auto sendRequestOp = self->m_client.SendRequestAsync(coRequest);
+ self->TrackResponse(reqArgs->RequestId, sendRequestOp);
- // Headers are generally case-insensitive
- // https://www.ietf.org/rfc/rfc2616.txt section 4.2
- for (auto &header : coReqArgs->Headers) {
- if (boost::iequals(header.first.c_str(), "Content-Type")) {
- bool success = HttpMediaTypeHeaderValue::TryParse(to_hstring(header.second), contentType);
- if (!success && m_onError) {
- co_return m_onError(coReqArgs->RequestId, "Failed to parse Content-Type");
- }
- } else if (boost::iequals(header.first.c_str(), "Content-Encoding")) {
- contentEncoding = header.second;
- } else if (boost::iequals(header.first.c_str(), "Content-Length")) {
- contentLength = header.second;
- } else if (boost::iequals(header.first.c_str(), "Authorization")) {
- bool success =
- coRequest.Headers().TryAppendWithoutValidation(to_hstring(header.first), to_hstring(header.second));
- if (!success && m_onError) {
- co_return m_onError(coReqArgs->RequestId, "Failed to append Authorization");
- }
- } else {
- try {
- coRequest.Headers().Append(to_hstring(header.first), to_hstring(header.second));
- } catch (hresult_error const &e) {
- if (self->m_onError) {
- co_return self->m_onError(coReqArgs->RequestId, Utilities::HResultToString(e));
- }
- }
- }
- }
+ if (reqArgs->Timeout > 0) {
+ // See https://devblogs.microsoft.com/oldnewthing/20220415-00/?p=106486
+ auto timedOut = std::make_shared(false);
+ auto sendRequestTimeout = [](auto timedOut, auto milliseconds) -> ResponseOperation {
+ // Convert milliseconds to "ticks" (10^-7 seconds)
+ co_await winrt::resume_after(winrt::Windows::Foundation::TimeSpan{milliseconds * 10000});
+ *timedOut = true;
+ co_return nullptr;
+ }(timedOut, reqArgs->Timeout);
- IHttpContent content{nullptr};
- auto &data = coReqArgs->Data;
- if (!data.isNull()) {
- auto bodyHandler = self->m_requestBodyHandler.lock();
- if (bodyHandler && bodyHandler->Supports(data)) {
- auto contentTypeString = contentType ? winrt::to_string(contentType.ToString()) : "";
- dynamic blob;
- try {
- blob = bodyHandler->ToRequestBody(data, contentTypeString);
- } catch (const std::invalid_argument &e) {
+ co_await lessthrow_await_adapter{winrt::when_any(sendRequestOp, sendRequestTimeout)};
+
+ // Cancel either still unfinished coroutine.
+ sendRequestTimeout.Cancel();
+ sendRequestOp.Cancel();
+
+ if (*timedOut) {
if (self->m_onError) {
- self->m_onError(coReqArgs->RequestId, e.what());
+ // TODO: Try to replace with either:
+ // WININET_E_TIMEOUT
+ // ERROR_INTERNET_TIMEOUT
+ // INET_E_CONNECTION_TIMEOUT
+ self->m_onError(reqArgs->RequestId, Utilities::HResultToString(HRESULT_FROM_WIN32(ERROR_TIMEOUT)), true);
}
- co_return;
+ co_return self->UntrackResponse(reqArgs->RequestId);
}
- auto bytes = blob["bytes"];
- auto byteVector = vector(bytes.size());
- for (auto &byte : bytes) {
- byteVector.push_back(static_cast(byte.asInt()));
- }
- auto view = winrt::array_view{byteVector};
- auto buffer = CryptographicBuffer::CreateFromByteArray(view);
- content = HttpBufferContent{std::move(buffer)};
- } else if (!data["string"].empty()) {
- content = HttpStringContent{to_hstring(data["string"].asString())};
- } else if (!data["base64"].empty()) {
- auto buffer = CryptographicBuffer::DecodeFromBase64String(to_hstring(data["base64"].asString()));
- content = HttpBufferContent{std::move(buffer)};
- } else if (!data["uri"].empty()) {
- auto file = co_await StorageFile::GetFileFromApplicationUriAsync(Uri{to_hstring(data["uri"].asString())});
- auto stream = co_await file.OpenReadAsync();
- content = HttpStreamContent{std::move(stream)};
- } else if (!data["form"].empty()) {
- // #9535 - HTTP form data support
- // winrt::Windows::Web::Http::HttpMultipartFormDataContent()
} else {
- // Assume empty request body.
- // content = HttpStringContent{L""};
+ co_await lessthrow_await_adapter{sendRequestOp};
}
- }
- if (content != nullptr) {
- // Attach content headers
- if (contentType) {
- content.Headers().ContentType(contentType);
- }
- if (!contentEncoding.empty()) {
- if (!content.Headers().ContentEncoding().TryParseAdd(to_hstring(contentEncoding))) {
- if (self->m_onError)
- self->m_onError(coReqArgs->RequestId, "Failed to parse Content-Encoding");
-
- co_return;
- }
- }
-
- if (!contentLength.empty()) {
- const auto contentLengthHeader = _atoi64(contentLength.c_str());
- content.Headers().ContentLength(contentLengthHeader);
- }
-
- coRequest.Content(content);
- }
-
- try {
- coRequest.Properties().Insert(L"RequestArgs", coArgs);
- auto sendRequestOp = self->m_client.SendRequestAsync(coRequest);
- self->TrackResponse(coReqArgs->RequestId, sendRequestOp);
-
- co_await lessthrow_await_adapter