From 62083ca0fc6fb45934c5776f218ca05d8d26822f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20C=C3=A9sar=20Rocha?= Date: Fri, 16 Sep 2022 18:56:55 -0700 Subject: [PATCH] [0.68] Implement HTTP redirection (#10600) * Implement Origin Policy filter (#9771) * Move all awaitable code into PerformSendRequest * Await coroutine methods * Remove blank lines * Simplify co_return UntrackResponse * Use dynamic body for server internal request * Defne SetOnOptions * Move url before headers in Response type * Add dummy preflight filter * Update x64 exports * Use strand for server IO context * Use HTTP message aliases * Use HTTP message aliases * Do not keep session references Allows dropping server scope in tests. * Comment out server scopes * Use DynamicRequest|Response alias * Format * Use thread vector * Drop outdated boost package validation in ReactCommon * Define experimental write strand * Drop server scope in default test * Also pass server context to sessions * Disable resource in default test * Use Beast example * Remove unused sample code * Create HttpServer as listener wrapper * Pass callbacks down to session * Strong-name response as DynamicResponse * Use DynamicRequest in handle_request * Define HandleRequest as member function * Lambda-based Respond() * #if-out original sample code * Keep count on get: ReaGetSucceeds * Implement lambda_ using std::function * Run context in io thread * Join threads in Stop method * Port send lambda to MS::R::T::HttpServer * Update other basic tests * Ensure Get+Options sequencing * Clean up comments * Use Callbacks() method * Add concurrency argument * Reduce macro usage * Fix default OPTIONS handler * Ensure number of headers * Define ResponseType * Use ResponseWrapper for polymorphism * Define remaining wrapped types (File, String) * Clean up server code * (WIP) add test PreflightSucceeds * catch hresult_error * Use ProcessRequest result in PreformSendReq * Rename test header value to Requested * Propagate orror in ProcessRequest * Rename OPReqFilter to PrototypeReqFilter * Port request filter to WinRT IHttpFilter subtype * Define allowed/forbidden methods and headers * Define MSRN::Networking::OriginPolicyHttpFilter * Move networking types into Shared\Networking folder * Refactor: Move network types to Microsoft::React::Networking * Clean up commented inline * Make OPFilter::SendAsync non const * Remove PrototypeHttpFilter * Temporarily have desk.ITs depend on CppWinRT * Define test OriginPolicyHttpFilterTest::UrlsHaveSameOrigin * Add more same origin tests * Start implementing ValidateRequest * Finish ValidateRequest() * Rename SingleOrigin to SameOrigin * Implement SendPreflightAsync * Fix OP assignment and GetOrigin rendering * Parse Access-Control-Allow-Headers * Done extracting access control values * Use request as argument of ValidatePreflightResponse * clang format * Pass RequestArgs to request properties * Pass RequestArgs to ValidateAllowOrigin * Remove prototype non-WinRT filter * Implement CorsUnsafeNotForbiddenRequestHeaderNames * Test WinRT RequestHeader case sensitivity * Fix ValidateAllowOrigin 4.10.5 * Add HttpOriginPolicyIntegrationTest * Use boost:iequals to compare method names * Add server support for CONNECT and TRACE methods * Make HttpServer port uint16_t * Prelfight only when OP is CORS. - Add test SimpleCorsSameOriginSucceededs - Add test NoCorsCrossOriginPatchSucceededs - Add test NoCorsCrossOriginFetchRequestSucceeds - Add test HTTP Server support for PATCH * Use runtime option Http.StrictScheme * Drop namespace from OriginPolicy * Remove Origin from request heders * Clean includes and usings in WinRTHttpResource.cpp * Update preflight cache issue references (#9770) * Pass origin in IHttpResource constructor * clang format * Prevent nullptr access when iterating preflight request headers * Include request content headers in preflight headers list * Send preflight to original request URL. - Change test origin URL to non-existing http://example.rnw - Have OriginPolicyHttpFilter::SendRequestAsync catch hresult_error to avoid losing info in (...) clause. * Export APIs Set/GetRuntimeOptionString - Switch to class-level static origin Uri. - Define static (global) origin Uri via runtime option "Http.GlobalOrigin". * clang format * Implement TestOriginPolicy to parameterize OP tests * Clean up non-parameterized tests * Use constant for dummy cross origin * Simplify test param constructors * Start implementing ValidateResponse * Add more tests - FullCorsCrossOriginMissingCorsHeadersFails - FullCorsCrossOriginMismatchedCorsHeaderFails * clang format * Change files * Update namespaces in MSRN solution * Move RequestArgs and ResponseType into new header * Implement ExtractAccessControlValues - Validate test result against ServerParams::Response::result * Report specific origin mismatch errors in ValidatePreflightResponse * Declare FullCorsCrossOriginCheckFailsOnPreflightRedirectFails * Add ABI-safe runtime options free functions. - Microsoft_React_SetRuntimeOptionBool - Microsoft_React_SetRuntimeOptionInt - Microsoft_React_SetRuntimeOptionString - Microsoft_React_GetRuntimeOptionBool - Microsoft_React_GetRuntimeOptionInt - Microsoft_React_GetRuntimeOptionString * Drop namespaced GetRuntimeOptionString * Use case-insensitive comparison * Drop newline from error message * Return unmanaged copy in GetRuntimeOptionString * Update FullCorsCrossOriginCheckFailsOnPreflightRedirectFails args * Disallow preflight redirect. See https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed * Replace winrt::get_self with .as * Drop LF from error messages * Use self-managed variable port in tests * Start writing FullCorsCorsCheckFailsOnResponseRedirectFails * Scope disabling autoredirect to preflight request only * Update TODOs with open issues * Compute originAllowed * Add test FullCorsSameOriginToSameOriginRedirectSucceeds * Test FullCorsSameOriginToCrossOriginRedirectSucceeds * Test FullCorsCrossOriginToOriginalOriginRedirectFails * Declare FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds * Implement OriginPolicyHttpFilter::ConstWcharComparer * Use _wcsicmp instead of boost::iequals in ConstWcharComparer * Correct SimpleCORS value search * Disable FullCorsCrossOriginWithCredentialsSucceeds for now * Rename type alias ResponseType to ResponseOperation * clang format * Handle originPolicy a request property instead of a member variable * Avoid iterating response headers while removing items * Gracefully fail when adding bad request headers - Write remaining integration tests * Use boost::iequals in PerformSendRequest * clang format * Use s_port for redirServer args * Rename TestOriginPolicy to TestOriginPolicyWithRedirect * Temporarily disabled tests - FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds - pending redirect - FullCorsRequestWithHostHeaderFails - Host request header is probematic * Use std::queue for nonSimpleNames * Finish ValidateResponse * Clean up comments * Add Test ExcludeHttpOnlyCookies * Add tests KeepNonHttpOnlyCookies, RemoveAllCookies * Address TODO comments * Always allow simple-CORS methods in preflight * Avoid auto for loop indexe sused against container .size() * Update Desktop.Test.DLL exports for ARM64 * Skip test FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds * Disable FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails * Ignore SimpleCorsForbiddenMethodFails * Ignore SimpleCorsCrossOriginFetchFails * RequestWithProxyAuthorizationHeaderFails * Ignore SimpleCorsSameOriginSucceededs * Ignore NoCorsCrossOriginFetchRequestSucceeds * Revert "Ignore NoCorsCrossOriginFetchRequestSucceeds" This reverts commit b5445fb5af85b428c623ac336b17f6e16bba7f1b. * Revert "Ignore SimpleCorsSameOriginSucceededs" This reverts commit ab75c3737301461894d30008256c92e0a5ba5adc. * Revert "RequestWithProxyAuthorizationHeaderFails" This reverts commit 70148b17de7daa4ed62c3ec115bdbd7c869e7dc5. * Revert "Ignore SimpleCorsCrossOriginFetchFails" This reverts commit 982e4508b9597fc82bde28130ebeaf15b06fcec2. * Revert "Ignore SimpleCorsForbiddenMethodFails" This reverts commit 869bda9782dad9b9b7d8e05d4c9cb689ec438617. * Revert "Disable FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails" This reverts commit e9e178a07ad1fd6fced627f23681c69e3866eed6. * Revert "Skip test FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds" This reverts commit 6688e7dce57e509b8865f181fd00355be59a2e4b. * Skip OP integration tests * Empty commit * Use scoped_lock for runtime options * Testing signature * Have C++ Rt Option functions call ABI-safe ones * Ensure different ports for each netwk test class * Remove remaining hard-coded ports from WS tests * Use ABI-safe callback for GetRtOptString * Only insert boolean rt options when true * Use static variable for port in HttpResourceIntegrationTest * Add HttpResourceIntegrationTest::SimpleRedirectSucceeds * Move C++ Rt Optio APIs to new header CppRuntimeOptions.h * Implement internal Set/GetRuntimeOptionString * clang format * Rename Microsoft_React_* functions to MicrosoftReact* * Update nuspec * Allow >10MB HTTP downloads (#9957) * Allow fetching HTTP content by segments * Change files * Restore the 10MB download chunk size * Remove usage of Content-Length * Revert segment size to 10MB * Make segmentSize and length uint32_t * Reuse response content buffer * Remove change files * Implement Blob module (#9352) * Added BlobModule and IWSModuleContHandler headers * Implement Blob module * Avoid raw self pointer in BlobModule * Implement WebSocketModule msg processing * clang format * Don't return until websocketMessage event is sent * Define CreateBlobModule() * Add DEF exports * Add Blob JS tests * Add Blob JS tests * Change files * yarn lint * Add overrides * Register BlobModule in DesktopTestRunner * Keep ignoring WebSocketBlob test by default * Add BlobModule to default modules list * Allow 'blob' responseType in HTTP module * Ensure React Instance can be accessed when using older versions of react-native * Emit error message on createFromParts failure * Remove redundant extra modules in Desktop integration tests * Declare IWebSocketModuleProxy * Remove Blob and WS module factories from DLL boundary * Implement IWebSocketModuleProxy * clang format * Update packages.lock * Use winrt::array_view directly in ResolveMessage * Define InstanceImpl::m_transitionalModuleProperties * Include CreateModules.h in projects accessing MSRN.Cxx * Define WinRT class WebSocketModuleContentHandler - Have BlobModule constructor register the content handler in transitive property bag CxxNativeModule/WebSocketModuleContentHandler * Have WebSocketModule use IInspectable as props arg * Use property bag instead of global singletons for blob helpers * Store blob helpers in prop bag as weak_ptr * Replace remaining lock_guard in BlobModule * Define IUriHandler, IReqBodyHandler, IRespHandler. * IHttpResource::SendRequest - add folly::dynamic data arg * Add data arg to test SendRequest calls * First implementation for BlobModuleUriHandler * Remove WebSocketModuleContentHandler WinRT class * Implement IBlobPersistor, MemoryBlobPersistor * clang format * Update yarn.lock * Update RctRootVieTagGen location * Implement addNetworkingHandler * Fix createFromParts buffer persistence * Drop WebSocketModule s_sharedState in favor of property bag * Disable back WebSocketBlob test * Rename iProperties to inspectableProperties * Pass ReactContext properties to CreateHttpModule in InstanceWin * Remove WebSocketModule constructor from x86 DLL boundary * yarn lint * Update packages.lock * Make transitional property bag non-member * Use blobURIScheme wherever possible * Pass request content as folly::dynaic. - Pass request ID directly from JavaScript layer. * Use constexpr for folly indexes * Implement GetMimeTypeFromUri * Finish BlobModule handler implementations. * Remove unused includes * Ensure HttpModule::m_resource is set * clang format * clang format * Allow blob responseType * Use winrt::to_hstring instead of Utf8ToUtf16 * Pass inspectableProperties down to WinRTHttpResource * Implement IHttpModuleProxy via WinRTHttpResource * Consume URI handler - IHttpResource - Rename SetOnRequest to SetOnRequestSuccess - Declare SetOnBlobData to pass complex (non-string) response data * Consume IRequestBodyHandler * Consume IResponseHandler * Ensure properties exist in bag before using value * Update packages lock * Add missing call to Modules::SendEvent * Fix Shared filters * Rename SetOnBlobData to SetOnData (different args) * Correctly retrieve blob slices * Correctly retrieve blob slices * Clang format * Update project filters * Drop BlobModuleUriHandler * Continue handling requests when not blob-supported * Add BlobTest * Update packages.lock.json * Define FileReaderModule * Implement FileReaderModule * Complete BlobTest * Make IBlobPersistor::ResolveMessage throw std::invalid_argument * Fail on Content-Encoding parsing even if no error handler * Remove MIME mappings. Currently unused * MemoryBlobPersistor::ResolveMessage throw on out of bounds * lint * Enable BlobTest by default * Disable Blob test in CI (may hang) * Use logical OR to assert HTTP responseType (#10095) * update yarn.lock * Use logical OR to assert HTTP responseType * Change files * Remove change files * Implement HTTP client timeout (#10261) * Implement hard-coded timeout * Create timeout from JS args * Timeout only for values greater than 0 * Change files * Remove variable sendRequestAny * Remove unused captures * Remove change files * Use uint8_t const in IBlobPersistor.h (#10276) * Use uint8_t const in IBlobPersistor.h Some versions of clang will not compile when the array_view data for CryptographicBuffer::CreateFromByteArray is not `uint8_t const`. This change switches the callsites to use uint8_t const where needed. * Change files * Fix Blob test comparison Co-authored-by: Julio C. Rocha * Remove change files * Adds missing headers for HttpRequestHeaderCollection (#10277) * Adds missing headers for HttpRequestHeaderCollection These headers are needed in case alternative PCH are used to compile (e.g., with clang BUCK). * Change files * Remove change files * Skip user agent HTTP header validation (#10279) * Skip user agent HTTP header validation In #8392, we added logic to skip HTTP header validation for `User-Agent` in the NetworkingModule. Now that NetworkingModule is being refactored, we need this change in the new implementation. This change skips user agent validation in the new networking module. * Change files * Remove change files * Implement HTTP redirection (#10534) * Define WinRTHttpResult::CreateRequest * Use request produced by CreateRequest() TODO: Have PerformSendRequest receive raw method and uri instead of HttpRequestMessage. * Exit if request is not created successfully * Enabled FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds * Single retry * Add test RedirectPatchSucceeds * Rename tests to SimpleRedirectSucceeds * Use method and URI intead of full HTTPReqMsg in PerformSendReq * Move HttpResourceIntegrationTest into workspace * Add WinInet-based test * Get request complete variables * Define RequestContext struct * Get response content * Add synchronization logic * Refer CoreFX reference PR and version * Disable SimpleRedirectWinInetSucceeds * Define RedirectHttpFilter - Meant to be the default internal filter for WinRTHttpResource. * Use redirect filter for OP and default clients in factory * Implement RedirectHttpFilter::SendRequestAsync TODO: Deal with IDS_REQUEST_ALREADY_SENT by making CreateRequest coroutine available to both the resource and filter classes. * Expose resource as IWinRTHttpRequestFactory - Allows redir filter to access resource's request factory method. * Re-arrange resource instantiation in Make factory * Re-enable disabled Origin Policy tests * Make redir filter constructors noexcept * Attempt to implement IHttpBaseProtocolFilter * Make redir filter implement IHttpBaseProtocolFilter * Enable inheritance of IHttpBPFilter via unsetting WINRT_LEAN_AND_MEAN * Implement IHttpBPfilter based on inner filter * Add RedirHttpFilterUTs class * Fix comment * Consume mocks in MockBaseFilter * Implement mocks in ManualRedirectSucceeds * Implement manual redir test with coroutines * Complete [Manual|Automatic]RedirectSucceeds * Allow setting max redirect # in constructor - Add test TooManyRedirectsFails * Add test MaxAllowedRedirectsSucceeds * Minor requestArgs factoring * Define and consume IRedirectEventSource * Add IRedirectEventSource.idl to unit test project * Update Shared vcx filters * Partially implement OPFilter::OnRedirecting * Update Shared filters * Make OPFilter drop redirection for preflights * Allow empty string, non-null req content * Allow non-movable responses in test server (OPIntTests) * Always clear Http.OmitCredentials rt option * Update outdated comment * Removed commented code * Clean up stale/commented code * Throw E_INVALIDARG if redirect gets null request handle * Throw ERROR_HTTP_REDIRECT_FAILED on too many redirects * Remove/ignore incorrect tests * clang format * Change files * Update packages lock * Remove Redir filter constructor from DLL boundary * Drop unused libs/include dirs * Restore ut project IncludePath * Remove /*const*/ comments from HTTP mocks * Explicitly capture `constexpr` Implicit capture only available starting MSVC 14.3 * Declare redirect counts as size_t * Update packages.lock.json * Update packages lock * Replace IInspectable with WinRT IMap (request props) in CreateRequest * Make TaintedOrigin a direct request property. The `RequestArgs` struct should not hold Origin POlicy specific data. * clang format * Fix compilation of filter and resource in MSRN * Rename local variables * Fix relative include of WinRTTypes * Simplify redirect count tests * Propagate isTimeout to JS layer * Comment alternative HRESULTs for timeout * Address feedback for internal MIDL type * Update packages lock * Use std::stol to parse Content-Length * Use constexpr for default max redirects * Drop WinRT/Http/Filters header from PCH - This prevents including the header with WINRT_LEAN_AND_MEAN macro conflict. - Only DevSupportManager required it. Performance loss is negligible. * Add interface IRedirectEventSource2 * Remove IDL IRedirectEventSource * Rename IRedirectEventSource2 to IRedirectEventSource * Revert packages lock * Remove stale IDL reference * Throw on RedirectHttpFilter::AllowUI * Change files * Fix merge errors * clang format * Use global.FileReader * Remove duplicate JS override * Enable Blob module in UWP (#10187) * Update packages.lock.json * Update packages.lock.json * Add Shared project to ReactUWPTestApp solution * RNTesterApp.csproj formatting * Enable Blob module in UWP * Change files * Update packages.lock.json * Use context property bag for runtime options in MSRN * Remove unused options header * Revert ReactUWPTestApp.sln * Update packages.lock.json * Update packages.lock.json * Use namespace in monolith HTTP module property * clang format * Revert unwanted changes Co-authored-by: Eric Rozell --- ...-b2f23c96-082a-4440-8d14-1d5a0ce3d115.json | 7 + .../windows/RNTesterApp/RNTesterApp.csproj | 4 +- .../HttpOriginPolicyIntegrationTest.cpp | 47 +-- .../HttpResourceIntegrationTests.cpp | 232 +++++++++-- .../OriginPolicyHttpFilterTest.cpp | 6 + .../React.Windows.Desktop.UnitTests.vcxproj | 3 +- ....Windows.Desktop.UnitTests.vcxproj.filters | 3 + .../RedirectHttpFilterUnitTest.cpp | 219 +++++++++++ .../WinRTNetworkingMocks.cpp | 159 ++++++++ .../Desktop.UnitTests/WinRTNetworkingMocks.h | 191 +++++++-- .../packages.lock.json | 49 ++- .../packages.lock.json | 59 ++- .../Base/CoreNativeModules.cpp | 25 ++ vnext/Microsoft.ReactNative/Pch/pch.h | 1 - vnext/Shared/Modules/BlobModule.cpp | 8 +- vnext/Shared/Modules/BlobModule.h | 2 +- vnext/Shared/Modules/FileReaderModule.cpp | 4 +- vnext/Shared/Modules/HttpModule.cpp | 142 +++---- vnext/Shared/Modules/IBlobPersistor.h | 2 +- vnext/Shared/Modules/WebSocketModule.cpp | 130 +++---- vnext/Shared/Networking/IHttpResource.h | 3 +- .../Shared/Networking/IRedirectEventSource.h | 18 + .../Networking/IWinRTHttpRequestFactory.h | 22 ++ .../Networking/OriginPolicyHttpFilter.cpp | 62 ++- .../Networking/OriginPolicyHttpFilter.h | 19 +- .../Shared/Networking/RedirectHttpFilter.cpp | 282 ++++++++++++++ vnext/Shared/Networking/RedirectHttpFilter.h | 97 +++++ vnext/Shared/Networking/WinRTHttpResource.cpp | 365 +++++++++++------- vnext/Shared/Networking/WinRTHttpResource.h | 21 +- vnext/Shared/OInstance.cpp | 12 +- vnext/Shared/Shared.vcxitems | 4 + vnext/Shared/Shared.vcxitems.filters | 12 + vnext/Test/HttpServer.cpp | 26 +- vnext/Test/HttpServer.h | 6 + vnext/overrides.json | 2 +- 35 files changed, 1828 insertions(+), 416 deletions(-) create mode 100644 change/react-native-windows-b2f23c96-082a-4440-8d14-1d5a0ce3d115.json create mode 100644 vnext/Desktop.UnitTests/RedirectHttpFilterUnitTest.cpp create mode 100644 vnext/Shared/Networking/IRedirectEventSource.h create mode 100644 vnext/Shared/Networking/IWinRTHttpRequestFactory.h create mode 100644 vnext/Shared/Networking/RedirectHttpFilter.cpp create mode 100644 vnext/Shared/Networking/RedirectHttpFilter.h 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{sendRequestOp}; auto result = sendRequestOp.ErrorCode(); if (result < 0) { if (self->m_onError) { - self->m_onError(coReqArgs->RequestId, Utilities::HResultToString(std::move(result))); + self->m_onError(reqArgs->RequestId, Utilities::HResultToString(std::move(result)), false); } - co_return self->UntrackResponse(coReqArgs->RequestId); + co_return self->UntrackResponse(reqArgs->RequestId); } auto response = sendRequestOp.GetResults(); @@ -322,7 +401,7 @@ fire_and_forget WinRTHttpResource::PerformSendRequest(HttpRequestMessage &&reque } self->m_onResponse( - coReqArgs->RequestId, + reqArgs->RequestId, {static_cast(response.StatusCode()), std::move(url), std::move(responseHeaders)}); } } @@ -337,21 +416,21 @@ fire_and_forget WinRTHttpResource::PerformSendRequest(HttpRequestMessage &&reque // Let response handler take over, if set if (auto responseHandler = self->m_responseHandler.lock()) { - if (responseHandler->Supports(coReqArgs->ResponseType)) { + if (responseHandler->Supports(reqArgs->ResponseType)) { auto bytes = vector(reader.UnconsumedBufferLength()); reader.ReadBytes(bytes); auto blob = responseHandler->ToResponseData(std::move(bytes)); 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; } } - auto isText = coReqArgs->ResponseType == "text"; + auto isText = reqArgs->ResponseType == "text"; if (isText) { reader.UnicodeEncoding(UnicodeEncoding::Utf8); } @@ -374,33 +453,33 @@ fire_and_forget WinRTHttpResource::PerformSendRequest(HttpRequestMessage &&reque buffer = reader.ReadBuffer(length); auto data = CryptographicBuffer::EncodeToBase64String(buffer); - responseData += to_string(std::wstring_view(data)); + responseData += winrt::to_string(std::wstring_view(data)); } } while (length > 0); if (self->m_onData) { - self->m_onData(coReqArgs->RequestId, std::move(responseData)); + self->m_onData(reqArgs->RequestId, std::move(responseData)); } } else { if (self->m_onError) { - self->m_onError(coReqArgs->RequestId, response == nullptr ? "request failed" : "No response content"); + self->m_onError(reqArgs->RequestId, response == nullptr ? "request failed" : "No response content", false); } } } catch (std::exception const &e) { if (self->m_onError) { - self->m_onError(coReqArgs->RequestId, e.what()); + self->m_onError(reqArgs->RequestId, e.what(), false); } } catch (hresult_error const &e) { if (self->m_onError) { - self->m_onError(coReqArgs->RequestId, Utilities::HResultToString(e)); + self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), false); } } catch (...) { if (self->m_onError) { - self->m_onError(coReqArgs->RequestId, "Unhandled exception during request"); + self->m_onError(reqArgs->RequestId, "Unhandled exception during request", false); } } - self->UntrackResponse(coReqArgs->RequestId); + self->UntrackResponse(reqArgs->RequestId); } // PerformSendRequest #pragma region IHttpModuleProxy @@ -431,19 +510,25 @@ void WinRTHttpResource::AddResponseHandler(shared_ptr response using namespace winrt::Microsoft::ReactNative; using winrt::Windows::Web::Http::HttpClient; - shared_ptr result; + auto redirFilter = winrt::make(); + HttpClient client; if (static_cast(GetRuntimeOptionInt("Http.OriginPolicy")) == OriginPolicy::None) { - result = std::make_shared(); + client = HttpClient{redirFilter}; } else { auto globalOrigin = GetRuntimeOptionString("Http.GlobalOrigin"); OriginPolicyHttpFilter::SetStaticOrigin(std::move(globalOrigin)); - auto opFilter = winrt::make(); - auto client = HttpClient{opFilter}; + auto opFilter = winrt::make(redirFilter); + redirFilter.as()->SetRedirectSource(opFilter.as()); - result = std::make_shared(std::move(client)); + client = HttpClient{opFilter}; } + auto result = std::make_shared(std::move(client)); + + // Allow redirect filter to create requests based on the resource's state + redirFilter.as()->SetRequestFactory(weak_ptr{result}); + // Register resource as HTTP module proxy. if (inspectableProperties) { auto propId = ReactPropertyId>>{L"HttpModule.Proxy"}; diff --git a/vnext/Shared/Networking/WinRTHttpResource.h b/vnext/Shared/Networking/WinRTHttpResource.h index 38d030f8264..23cc68dd299 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.h +++ b/vnext/Shared/Networking/WinRTHttpResource.h @@ -6,6 +6,7 @@ #include "IHttpResource.h" #include +#include "IWinRTHttpRequestFactory.h" #include "WinRTTypes.h" // Windows API @@ -18,6 +19,7 @@ namespace Microsoft::React::Networking { class WinRTHttpResource : public IHttpResource, public IHttpModuleProxy, + public IWinRTHttpRequestFactory, public std::enable_shared_from_this { winrt::Windows::Web::Http::IHttpClient m_client; std::mutex m_mutex; @@ -27,7 +29,7 @@ class WinRTHttpResource : public IHttpResource, std::function m_onResponse; std::function m_onData; std::function m_onDataDynamic; - std::function m_onError; + std::function m_onError; // Used for IHttpModuleProxy std::weak_ptr m_uriHandler; @@ -39,7 +41,8 @@ class WinRTHttpResource : public IHttpResource, void UntrackResponse(int64_t requestId) noexcept; winrt::fire_and_forget PerformSendRequest( - winrt::Windows::Web::Http::HttpRequestMessage &&request, + winrt::Windows::Web::Http::HttpMethod &&method, + winrt::Windows::Foundation::Uri &&uri, winrt::Windows::Foundation::IInspectable const &args) noexcept; public: @@ -47,6 +50,16 @@ class WinRTHttpResource : public IHttpResource, WinRTHttpResource(winrt::Windows::Web::Http::IHttpClient &&client) noexcept; +#pragma region IWinRTHttpRequestFactory + + winrt::Windows::Foundation::IAsyncOperation CreateRequest( + winrt::Windows::Web::Http::HttpMethod &&method, + winrt::Windows::Foundation::Uri &&uri, + winrt::Windows::Foundation::Collections::IMap + props) noexcept override; + +#pragma endregion IWinRTHttpRequestFactory + #pragma region IHttpResource void SendRequest( @@ -67,8 +80,8 @@ class WinRTHttpResource : public IHttpResource, void SetOnResponse(std::function &&handler) noexcept override; void SetOnData(std::function &&handler) noexcept override; void SetOnData(std::function &&handler) noexcept override; - void SetOnError(std::function - &&handler) noexcept override; + void SetOnError( + std::function &&handler) noexcept override; #pragma endregion IHttpResource diff --git a/vnext/Shared/OInstance.cpp b/vnext/Shared/OInstance.cpp index 81471c9d444..fdfbae52029 100644 --- a/vnext/Shared/OInstance.cpp +++ b/vnext/Shared/OInstance.cpp @@ -557,6 +557,7 @@ std::vector> InstanceImpl::GetDefaultNativeModules std::vector> modules; auto transitionalProps{ReactPropertyBagHelper::CreatePropertyBag()}; +#if (defined(_MSC_VER) && !defined(WINRT)) modules.push_back(std::make_unique( m_innerInstance, Microsoft::React::GetHttpModuleName(), @@ -564,6 +565,7 @@ std::vector> InstanceImpl::GetDefaultNativeModules return Microsoft::React::CreateHttpModule(transitionalProps); }, nativeQueue)); +#endif modules.push_back(std::make_unique( m_innerInstance, @@ -631,8 +633,13 @@ std::vector> InstanceImpl::GetDefaultNativeModules []() { return std::make_unique(); }, nativeQueue)); - // #10036 - Blob module not supported in UWP. Need to define property bag lifetime and onwership. - if (Microsoft::React::GetRuntimeOptionBool("Blob.EnableModule")) { + // These modules are instantiated separately in MSRN (Universal Windows). + // When there are module name colisions, the last one registered is used. + // If this code is enabled, we will have unused module instances. + // Also, MSRN has a different property bag mechanism incompatible with this method's transitionalProps variable. +#if (defined(_MSC_VER) && !defined(WINRT)) + if (Microsoft::React::GetRuntimeOptionBool("Blob.EnableModule") && + !Microsoft::React::GetRuntimeOptionBool("Http.UseMonolithicModule")) { modules.push_back(std::make_unique( m_innerInstance, Microsoft::React::GetBlobModuleName(), @@ -645,6 +652,7 @@ std::vector> InstanceImpl::GetDefaultNativeModules [transitionalProps]() { return Microsoft::React::CreateFileReaderModule(transitionalProps); }, nativeQueue)); } +#endif return modules; } diff --git a/vnext/Shared/Shared.vcxitems b/vnext/Shared/Shared.vcxitems index cb02173c0c0..e6a7e68d3ad 100644 --- a/vnext/Shared/Shared.vcxitems +++ b/vnext/Shared/Shared.vcxitems @@ -57,6 +57,7 @@ + @@ -104,9 +105,12 @@ + + + diff --git a/vnext/Shared/Shared.vcxitems.filters b/vnext/Shared/Shared.vcxitems.filters index eb2d1a75375..8109f928a8c 100644 --- a/vnext/Shared/Shared.vcxitems.filters +++ b/vnext/Shared/Shared.vcxitems.filters @@ -154,6 +154,9 @@ Source Files\Modules + + Source Files\Networking + @@ -459,6 +462,15 @@ Header Files\Modules + + Header Files\Networking + + + Header Files\Networking + + + Header Files\Networking + diff --git a/vnext/Test/HttpServer.cpp b/vnext/Test/HttpServer.cpp index b88510ac613..d6ca802d3cb 100644 --- a/vnext/Test/HttpServer.cpp +++ b/vnext/Test/HttpServer.cpp @@ -41,14 +41,14 @@ boost::beast::multi_buffer CreateStringResponseBody(string&& content) #pragma region ResponseWrapper -ResponseWrapper::ResponseWrapper(DynamicResponse&& res) - : m_response{ make_shared(std::move(res)) } +ResponseWrapper::ResponseWrapper(DynamicResponse&& response) + : m_response{ make_shared(std::move(response)) } , m_type{ ResponseType::Dynamic } { } -ResponseWrapper::ResponseWrapper(EmptyResponse&& res) - : m_response{ make_shared(std::move(res)) } +ResponseWrapper::ResponseWrapper(EmptyResponse&& response) + : m_response{ make_shared(std::move(response)) } , m_type{ ResponseType::Empty } { } @@ -65,6 +65,24 @@ ResponseWrapper::ResponseWrapper(StringResponse&& response) { } +ResponseWrapper::ResponseWrapper(DynamicResponse const& response) + : m_response{ make_shared(response) } + , m_type{ ResponseType::Dynamic } +{ +} + +ResponseWrapper::ResponseWrapper(EmptyResponse const& response) + : m_response{ make_shared(response) } + , m_type{ ResponseType::Empty } +{ +} + +ResponseWrapper::ResponseWrapper(StringResponse const& response) + : m_response{ make_shared(response) } + , m_type{ ResponseType::String } +{ +} + shared_ptr ResponseWrapper::Response() { return m_response; diff --git a/vnext/Test/HttpServer.h b/vnext/Test/HttpServer.h index 52cceeba433..a3e25b2f9ad 100644 --- a/vnext/Test/HttpServer.h +++ b/vnext/Test/HttpServer.h @@ -39,6 +39,12 @@ class ResponseWrapper ResponseWrapper(StringResponse&& response); + ResponseWrapper(DynamicResponse const& response); + + ResponseWrapper(EmptyResponse const& response); + + ResponseWrapper(StringResponse const& response); + ResponseWrapper(ResponseWrapper&&) = default; std::shared_ptr Response(); diff --git a/vnext/overrides.json b/vnext/overrides.json index 187abe5b1cd..3419192013f 100644 --- a/vnext/overrides.json +++ b/vnext/overrides.json @@ -479,4 +479,4 @@ "file": "src/typings-index.ts" } ] -} \ No newline at end of file +}