From 9dffac7f6cdb24eed9ab2fa10f02833cc42092f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20C=C3=A9sar=20Rocha?= Date: Fri, 20 May 2022 15:26:15 -0700 Subject: [PATCH 01/45] 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 --- ...-68257dfa-e3b3-4038-b170-6bc686cb3b08.json | 7 + vnext/Shared/Networking/WinRTHttpResource.cpp | 779 +++++++++--------- 2 files changed, 409 insertions(+), 377 deletions(-) create mode 100644 change/react-native-windows-68257dfa-e3b3-4038-b170-6bc686cb3b08.json diff --git a/change/react-native-windows-68257dfa-e3b3-4038-b170-6bc686cb3b08.json b/change/react-native-windows-68257dfa-e3b3-4038-b170-6bc686cb3b08.json new file mode 100644 index 00000000000..cda201f8577 --- /dev/null +++ b/change/react-native-windows-68257dfa-e3b3-4038-b170-6bc686cb3b08.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Allow fetching HTTP content by segments", + "packageName": "react-native-windows", + "email": "julio.rocha@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Shared/Networking/WinRTHttpResource.cpp b/vnext/Shared/Networking/WinRTHttpResource.cpp index 56e9899531f..be94fe6ed90 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.cpp +++ b/vnext/Shared/Networking/WinRTHttpResource.cpp @@ -55,449 +55,473 @@ namespace Microsoft::React::Networking { #pragma region WinRTHttpResource -WinRTHttpResource::WinRTHttpResource(IHttpClient &&client) noexcept : m_client{std::move(client)} {} + WinRTHttpResource::WinRTHttpResource(IHttpClient&& client) noexcept: m_client{ std::move(client) } {} -WinRTHttpResource::WinRTHttpResource() noexcept : WinRTHttpResource(winrt::Windows::Web::Http::HttpClient{}) {} + WinRTHttpResource::WinRTHttpResource() noexcept: WinRTHttpResource(winrt::Windows::Web::Http::HttpClient{}) {} #pragma region IWinRTHttpRequestFactory -IAsyncOperation WinRTHttpResource::CreateRequest( - HttpMethod &&method, - Uri &&uri, + 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; + auto request = HttpRequestMessage{ std::move(method), std::move(uri) }; + for (auto prop : props) { + request.Properties().Insert(prop.Key(), prop.Value()); + } - // 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); + 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; } - 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); + 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; } - 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); + 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; } - 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); + // 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())); } - co_return nullptr; + auto view = winrt::array_view{ byteVector }; + auto buffer = CryptographicBuffer::CreateFromByteArray(view); + content = HttpBufferContent{ std::move(buffer) }; } - auto bytes = blob["bytes"]; - auto byteVector = vector(bytes.size()); - for (auto &byte : bytes) { - byteVector.push_back(static_cast(byte.asInt())); + 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() } - 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); + // 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; + 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); + 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; + } + catch (const std::out_of_range& e) { + if (self->m_onError) + self->m_onError(reqArgs->RequestId, e.what() + string{ " [" } + contentLength + "]", false); - co_return nullptr; + co_return nullptr; + } } + + request.Content(content); } - request.Content(content); + co_return request; } - co_return request; -} - #pragma endregion IWinRTHttpRequestFactory #pragma region IHttpResource -void WinRTHttpResource::SendRequest( - string &&method, - string &&url, + void WinRTHttpResource::SendRequest( + string&& method, + string&& url, int64_t requestId, - Headers &&headers, - dynamic &&data, - string &&responseType, + Headers&& headers, + dynamic&& data, + string&& responseType, bool useIncrementalUpdates, int64_t timeout, bool withCredentials, - std::function &&callback) noexcept /*override*/ { - // Enforce supported args - assert(responseType == "text" || responseType == "base64" || responseType == "blob"); + std::function&& callback) noexcept /*override*/ { + // Enforce supported args + assert(responseType == "text" || responseType == "base64" || responseType == "blob"); - if (callback) { - callback(requestId); + if (callback) { + callback(requestId); + } + + try { + HttpMethod httpMethod{ to_hstring(std::move(method)) }; + Uri uri{ to_hstring(std::move(url)) }; + + 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(), false); + } + } + catch (hresult_error const& e) { + if (m_onError) { + m_onError(requestId, Utilities::HResultToString(e), false); + } + } + catch (...) { + m_onError(requestId, "Unidentified error sending HTTP request", false); + } } - try { - HttpMethod httpMethod{to_hstring(std::move(method))}; - Uri uri{to_hstring(std::move(url))}; + void WinRTHttpResource::AbortRequest(int64_t requestId) noexcept /*override*/ { + ResponseOperation request{ nullptr }; - 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(), false); + { + scoped_lock lock{ m_mutex }; + auto iter = m_responses.find(requestId); + if (iter == std::end(m_responses)) { + return; + } + request = iter->second; } - } catch (hresult_error const &e) { - if (m_onError) { + + try { + request.Cancel(); + } + catch (hresult_error const& e) { m_onError(requestId, Utilities::HResultToString(e), false); } - } catch (...) { - m_onError(requestId, "Unidentified error sending HTTP request", false); } -} -void WinRTHttpResource::AbortRequest(int64_t requestId) noexcept /*override*/ { - ResponseOperation request{nullptr}; + void WinRTHttpResource::ClearCookies() noexcept /*override*/ { + assert(false); + // NOT IMPLEMENTED + } + + void WinRTHttpResource::SetOnRequestSuccess(function&& handler) noexcept /*override*/ { + m_onRequestSuccess = std::move(handler); + } + + void WinRTHttpResource::SetOnResponse(function&& handler) noexcept + /*override*/ { + m_onResponse = std::move(handler); + } + + void WinRTHttpResource::SetOnData(function&& handler) noexcept + /*override*/ { + m_onData = std::move(handler); + } + void WinRTHttpResource::SetOnData(function&& handler) noexcept + /*override*/ { - scoped_lock lock{m_mutex}; - auto iter = m_responses.find(requestId); - if (iter == std::end(m_responses)) { - return; - } - request = iter->second; + m_onDataDynamic = std::move(handler); } - try { - request.Cancel(); - } catch (hresult_error const &e) { - m_onError(requestId, Utilities::HResultToString(e), false); + void WinRTHttpResource::SetOnError( + function&& handler) noexcept + /*override*/ { + m_onError = std::move(handler); } -} - -void WinRTHttpResource::ClearCookies() noexcept /*override*/ { - assert(false); - // NOT IMPLEMENTED -} - -void WinRTHttpResource::SetOnRequestSuccess(function &&handler) noexcept /*override*/ { - m_onRequestSuccess = std::move(handler); -} - -void WinRTHttpResource::SetOnResponse(function &&handler) noexcept -/*override*/ { - m_onResponse = std::move(handler); -} - -void WinRTHttpResource::SetOnData(function &&handler) noexcept -/*override*/ { - m_onData = std::move(handler); -} - -void WinRTHttpResource::SetOnData(function &&handler) noexcept -/*override*/ -{ - m_onDataDynamic = std::move(handler); -} - -void WinRTHttpResource::SetOnError( - function &&handler) noexcept -/*override*/ { - m_onError = std::move(handler); -} #pragma endregion IHttpResource -void WinRTHttpResource::TrackResponse(int64_t requestId, ResponseOperation response) noexcept { - scoped_lock lock{m_mutex}; - m_responses[requestId] = response; -} - -void WinRTHttpResource::UntrackResponse(int64_t requestId) noexcept { - scoped_lock lock{m_mutex}; - m_responses.erase(requestId); -} - -fire_and_forget -WinRTHttpResource::PerformSendRequest(HttpMethod &&method, Uri &&rtUri, IInspectable const &args) noexcept { - // Keep references after coroutine suspension. - auto self = shared_from_this(); - auto coArgs = args; - 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; + void WinRTHttpResource::TrackResponse(int64_t requestId, ResponseOperation response) noexcept { + scoped_lock lock{ m_mutex }; + m_responses[requestId] = response; } - // 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, reqArgs->ResponseType)) { - auto blob = uriHandler->Fetch(uri); - if (self->m_onDataDynamic && self->m_onRequestSuccess) { - 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(reqArgs->RequestId, Utilities::HResultToString(e), false); - } catch (const std::exception &e) { - if (self->m_onError) - co_return self->m_onError(reqArgs->RequestId, e.what(), false); - } + void WinRTHttpResource::UntrackResponse(int64_t requestId) noexcept { + scoped_lock lock{ m_mutex }; + m_responses.erase(requestId); } - try { - auto sendRequestOp = self->m_client.SendRequestAsync(coRequest); - self->TrackResponse(reqArgs->RequestId, sendRequestOp); + fire_and_forget + WinRTHttpResource::PerformSendRequest(HttpMethod&& method, Uri&& rtUri, IInspectable const& args) noexcept { + // Keep references after coroutine suspension. + auto self = shared_from_this(); + auto coArgs = args; + auto reqArgs = coArgs.as(); + auto coMethod = std::move(method); + auto coUri = std::move(rtUri); + + // Ensure background thread + co_await winrt::resume_background(); - 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); + auto props = winrt::multi_threaded_map(); + props.Insert(L"RequestArgs", coArgs); - co_await lessthrow_await_adapter{winrt::when_any(sendRequestOp, sendRequestTimeout)}; + auto coRequest = co_await CreateRequest(std::move(coMethod), std::move(coUri), props); + if (!coRequest) { + co_return; + } - // Cancel either still unfinished coroutine. - sendRequestTimeout.Cancel(); - sendRequestOp.Cancel(); + // 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, reqArgs->ResponseType)) { + auto blob = uriHandler->Fetch(uri); + if (self->m_onDataDynamic && self->m_onRequestSuccess) { + self->m_onDataDynamic(reqArgs->RequestId, std::move(blob)); + self->m_onRequestSuccess(reqArgs->RequestId); + } - if (*timedOut) { - if (self->m_onError) { - // 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); } - } else { - co_await lessthrow_await_adapter{sendRequestOp}; - } - - auto result = sendRequestOp.ErrorCode(); - if (result < 0) { - if (self->m_onError) { - self->m_onError(reqArgs->RequestId, Utilities::HResultToString(std::move(result)), false); + catch (const hresult_error& e) { + if (self->m_onError) + 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(reqArgs->RequestId, e.what(), false); } - co_return self->UntrackResponse(reqArgs->RequestId); } - auto response = sendRequestOp.GetResults(); - if (response) { - if (self->m_onResponse) { - auto url = to_string(response.RequestMessage().RequestUri().AbsoluteUri()); - - // Gather headers for both the response content and the response itself - // See Invoke-WebRequest PowerShell cmdlet or Chromium response handling - Headers responseHeaders; - for (auto header : response.Headers()) { - responseHeaders.emplace(to_string(header.Key()), to_string(header.Value())); + try { + auto sendRequestOp = self->m_client.SendRequestAsync(coRequest); + self->TrackResponse(reqArgs->RequestId, sendRequestOp); + + 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); + + 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) { + // 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 self->UntrackResponse(reqArgs->RequestId); } - for (auto header : response.Content().Headers()) { - responseHeaders.emplace(to_string(header.Key()), to_string(header.Value())); + } + else { + co_await lessthrow_await_adapter{sendRequestOp}; + } + + auto result = sendRequestOp.ErrorCode(); + if (result < 0) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, Utilities::HResultToString(std::move(result)), false); } + co_return self->UntrackResponse(reqArgs->RequestId); + } + + auto response = sendRequestOp.GetResults(); + if (response) { + if (self->m_onResponse) { + auto url = to_string(response.RequestMessage().RequestUri().AbsoluteUri()); - self->m_onResponse( + // Gather headers for both the response content and the response itself + // See Invoke-WebRequest PowerShell cmdlet or Chromium response handling + Headers responseHeaders; + for (auto header : response.Headers()) { + responseHeaders.emplace(to_string(header.Key()), to_string(header.Value())); + } + for (auto header : response.Content().Headers()) { + responseHeaders.emplace(to_string(header.Key()), to_string(header.Value())); + } + + self->m_onResponse( reqArgs->RequestId, - {static_cast(response.StatusCode()), std::move(url), std::move(responseHeaders)}); + { static_cast(response.StatusCode()), std::move(url), std::move(responseHeaders) }); + } } - } - // #9534 - Support HTTP incremental updates - if (response && response.Content()) { - auto inputStream = co_await response.Content().ReadAsInputStreamAsync(); - auto reader = DataReader{inputStream}; + // #9534 - Support HTTP incremental updates + if (response && response.Content()) { + auto inputStream = co_await response.Content().ReadAsInputStreamAsync(); + auto reader = DataReader{ inputStream }; - // #9510 - 10mb limit on fetch - co_await reader.LoadAsync(10 * 1024 * 1024); + // #9510 - 10mb limit on fetch + co_await reader.LoadAsync(10 * 1024 * 1024); - // Let response handler take over, if set - if (auto responseHandler = self->m_responseHandler.lock()) { - if (responseHandler->Supports(reqArgs->ResponseType)) { - auto bytes = vector(reader.UnconsumedBufferLength()); - reader.ReadBytes(bytes); - auto blob = responseHandler->ToResponseData(std::move(bytes)); + // Let response handler take over, if set + if (auto responseHandler = self->m_responseHandler.lock()) { + 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(reqArgs->RequestId, std::move(blob)); - self->m_onRequestSuccess(reqArgs->RequestId); + if (self->m_onDataDynamic && self->m_onRequestSuccess) { + self->m_onDataDynamic(reqArgs->RequestId, std::move(blob)); + self->m_onRequestSuccess(reqArgs->RequestId); + } + + co_return; } + } - co_return; + auto isText = reqArgs->ResponseType == "text"; + if (isText) { + reader.UnicodeEncoding(UnicodeEncoding::Utf8); } - } - auto isText = reqArgs->ResponseType == "text"; - if (isText) { - reader.UnicodeEncoding(UnicodeEncoding::Utf8); - } + // #9510 - We currently accumulate all incoming request data in 10MB chunks. + uint32_t segmentSize = 10 * 1024 * 1024; + string responseData; + winrt::Windows::Storage::Streams::IBuffer buffer; + uint32_t length; + do { + co_await reader.LoadAsync(segmentSize); + length = reader.UnconsumedBufferLength(); - // #9510 - We currently accumulate all incoming request data in 10MB chunks. - uint32_t segmentSize = 10 * 1024 * 1024; - string responseData; - winrt::Windows::Storage::Streams::IBuffer buffer; - uint32_t length; - do { - co_await reader.LoadAsync(segmentSize); - length = reader.UnconsumedBufferLength(); + if (coReqArgs->IsText) { + auto data = std::vector(length); + reader.ReadBytes(data); - if (isText) { - auto data = std::vector(length); - reader.ReadBytes(data); + responseData += string(Common::Utilities::CheckedReinterpretCast(data.data()), data.size()); + } + else { + buffer = reader.ReadBuffer(length); + auto data = CryptographicBuffer::EncodeToBase64String(buffer); - responseData += string(Common::Utilities::CheckedReinterpretCast(data.data()), data.size()); - } else { - buffer = reader.ReadBuffer(length); - auto data = CryptographicBuffer::EncodeToBase64String(buffer); + responseData += to_string(std::wstring_view(data)); + } + } while (length > 0); - responseData += winrt::to_string(std::wstring_view(data)); + if (self->m_onData) { + self->m_onData(coReqArgs->RequestId, std::move(responseData)); } - } while (length > 0); - - if (self->m_onData) { - self->m_onData(reqArgs->RequestId, std::move(responseData)); } - } else { - if (self->m_onError) { - self->m_onError(reqArgs->RequestId, response == nullptr ? "request failed" : "No response content", false); + else { + if (self->m_onError) { + 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(reqArgs->RequestId, e.what(), false); + catch (std::exception const& e) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, e.what(), false); + } } - } catch (hresult_error const &e) { - if (self->m_onError) { - self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), false); + catch (hresult_error const& e) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), false); + } } - } catch (...) { - if (self->m_onError) { - self->m_onError(reqArgs->RequestId, "Unhandled exception during request", false); + catch (...) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, "Unhandled exception during request", false); + } } - } - self->UntrackResponse(reqArgs->RequestId); -} // PerformSendRequest + self->UntrackResponse(reqArgs->RequestId); + } // PerformSendRequest #pragma region IHttpModuleProxy -void WinRTHttpResource::AddUriHandler(shared_ptr /*uriHandler*/) noexcept /*override*/ -{ - // TODO: Implement custom URI handling. -} + void WinRTHttpResource::AddUriHandler(shared_ptr /*uriHandler*/) noexcept /*override*/ + { + // TODO: Implement custom URI handling. + } -void WinRTHttpResource::AddRequestBodyHandler(shared_ptr requestBodyHandler) noexcept /*override*/ -{ - m_requestBodyHandler = weak_ptr(requestBodyHandler); -} + void WinRTHttpResource::AddRequestBodyHandler(shared_ptr requestBodyHandler) noexcept /*override*/ + { + m_requestBodyHandler = weak_ptr(requestBodyHandler); + } -void WinRTHttpResource::AddResponseHandler(shared_ptr responseHandler) noexcept /*override*/ -{ - m_responseHandler = weak_ptr(responseHandler); -} + void WinRTHttpResource::AddResponseHandler(shared_ptr responseHandler) noexcept /*override*/ + { + m_responseHandler = weak_ptr(responseHandler); + } #pragma endregion IHttpModuleProxy @@ -505,45 +529,46 @@ void WinRTHttpResource::AddResponseHandler(shared_ptr response #pragma region IHttpResource -/*static*/ shared_ptr IHttpResource::Make( - winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept { - using namespace winrt::Microsoft::ReactNative; - using winrt::Windows::Web::Http::HttpClient; + /*static*/ shared_ptr IHttpResource::Make( + winrt::Windows::Foundation::IInspectable const& inspectableProperties) noexcept { + using namespace winrt::Microsoft::ReactNative; + using winrt::Windows::Web::Http::HttpClient; - auto redirFilter = winrt::make(); - HttpClient client; + auto redirFilter = winrt::make(); + HttpClient client; - if (static_cast(GetRuntimeOptionInt("Http.OriginPolicy")) == OriginPolicy::None) { - client = HttpClient{redirFilter}; - } else { - auto globalOrigin = GetRuntimeOptionString("Http.GlobalOrigin"); - OriginPolicyHttpFilter::SetStaticOrigin(std::move(globalOrigin)); - auto opFilter = winrt::make(redirFilter); - redirFilter.as()->SetRedirectSource(opFilter.as()); + if (static_cast(GetRuntimeOptionInt("Http.OriginPolicy")) == OriginPolicy::None) { + client = HttpClient{ redirFilter }; + } + else { + auto globalOrigin = GetRuntimeOptionString("Http.GlobalOrigin"); + OriginPolicyHttpFilter::SetStaticOrigin(std::move(globalOrigin)); + auto opFilter = winrt::make(redirFilter); + redirFilter.as()->SetRedirectSource(opFilter.as()); - client = HttpClient{opFilter}; - } + client = HttpClient{ opFilter }; + } - auto result = std::make_shared(std::move(client)); + 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}); + // 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"}; - auto propBag = ReactPropertyBag{inspectableProperties.try_as()}; - auto moduleProxy = weak_ptr{result}; - propBag.Set(propId, std::move(moduleProxy)); - } + // Register resource as HTTP module proxy. + if (inspectableProperties) { + auto propId = ReactPropertyId>>{ L"HttpModule.Proxy" }; + auto propBag = ReactPropertyBag{ inspectableProperties.try_as() }; + auto moduleProxy = weak_ptr{ result }; + propBag.Set(propId, std::move(moduleProxy)); + } - return result; -} + return result; + } -/*static*/ shared_ptr IHttpResource::Make() noexcept { - auto inspectableProperties = IInspectable{nullptr}; - return Make(inspectableProperties); -} + /*static*/ shared_ptr IHttpResource::Make() noexcept { + auto inspectableProperties = IInspectable{ nullptr }; + return Make(inspectableProperties); + } #pragma endregion IHttpResource From 87f358ce7a222cd74caa66a9539cbdfbdb2e6b65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20C=C3=A9sar=20Rocha?= Date: Fri, 20 May 2022 19:48:48 -0700 Subject: [PATCH 02/45] 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) --- ...-2c5e8e12-8a2e-46b9-8b4f-3f9bdec5faca.json | 7 + .../HttpOriginPolicyIntegrationTest.cpp | 1289 +++++++++-------- .../HttpResourceIntegrationTests.cpp | 827 +++++------ .../Base/CoreNativeModules.cpp | 91 +- vnext/Shared/Modules/BlobModule.cpp | 494 +++---- vnext/Shared/Modules/BlobModule.h | 164 +-- vnext/Shared/Modules/FileReaderModule.cpp | 205 +-- vnext/Shared/Modules/HttpModule.cpp | 236 +-- vnext/Shared/Modules/IBlobPersistor.h | 22 +- vnext/Shared/Networking/IHttpResource.h | 137 +- vnext/Shared/Networking/WinRTHttpResource.cpp | 776 +++++----- vnext/Shared/Networking/WinRTHttpResource.h | 96 +- vnext/Shared/OInstance.cpp | 1032 ++++++------- vnext/Shared/OInstance.h | 167 ++- vnext/Shared/Shared.vcxitems.filters | 24 - 15 files changed, 2810 insertions(+), 2757 deletions(-) create mode 100644 change/react-native-windows-2c5e8e12-8a2e-46b9-8b4f-3f9bdec5faca.json diff --git a/change/react-native-windows-2c5e8e12-8a2e-46b9-8b4f-3f9bdec5faca.json b/change/react-native-windows-2c5e8e12-8a2e-46b9-8b4f-3f9bdec5faca.json new file mode 100644 index 00000000000..9c6be33fb71 --- /dev/null +++ b/change/react-native-windows-2c5e8e12-8a2e-46b9-8b4f-3f9bdec5faca.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Implement Blob module", + "packageName": "react-native-windows", + "email": "julio.rocha@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp b/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp index c9026cc9831..e749f07f83d 100644 --- a/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp +++ b/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp @@ -23,72 +23,72 @@ using std::promise; using std::string; namespace { -constexpr char s_serverHost[]{"http://localhost"}; -constexpr char s_crossOriginUrl[]{"http://example.rnw"}; + constexpr char s_serverHost[]{ "http://localhost" }; + constexpr char s_crossOriginUrl[]{ "http://example.rnw" }; } // namespace // clang-format off namespace Microsoft::React::Test { -TEST_CLASS(HttpOriginPolicyIntegrationTest) -{ - static constexpr bool s_shouldSucceed{true}; - static constexpr bool s_shouldFail{false}; - - static uint16_t s_port; - - struct ServerParams + TEST_CLASS(HttpOriginPolicyIntegrationTest) { - uint16_t Port; - string Url; - EmptyResponse Preflight; - StringResponse Response; + static constexpr bool s_shouldSucceed{ true }; + static constexpr bool s_shouldFail{ false }; - ServerParams( - uint16_t port) noexcept - : Port{port} - , Url{s_serverHost + string{":"} + std::to_string(port)} - { - Preflight.set(http::field::access_control_allow_methods, "GET, POST, DELETE, PATCH"); + static uint16_t s_port; - Response.result(http::status::unknown); - Response.body() = "RESPONSE_CONTENT"; - } - }; - - struct ClientParams - { - promise ContentPromise; - string ErrorMessage; - IHttpResource::Response Response; - string ResponseContent; - http::verb Method; - IHttpResource::Headers RequestHeaders; - bool WithCredentials{false}; - - ClientParams(http::verb method, IHttpResource::Headers&& headers) - : Method{ method } - , RequestHeaders{ std::move(headers) } + struct ServerParams { - } - }; + uint16_t Port; + string Url; + EmptyResponse Preflight; + StringResponse Response; + + ServerParams( + uint16_t port) noexcept + : Port{ port } + , Url{ s_serverHost + string{":"} + std::to_string(port) } + { + Preflight.set(http::field::access_control_allow_methods, "GET, POST, DELETE, PATCH"); - std::shared_ptr CreateServer(ServerParams& serverArgs, ClientParams& clientArgs) noexcept - { - auto server = make_shared(serverArgs.Port); - server->Callbacks().OnOptions = [&serverArgs](const DynamicRequest& request) -> ResponseWrapper - { - return { std::move(serverArgs.Preflight) }; + Response.result(http::status::unknown); + Response.body() = "RESPONSE_CONTENT"; + } }; - auto reqHandler = [&serverArgs](const DynamicRequest& request) -> ResponseWrapper + struct ClientParams { - // Don't use move constructor in case of multiple requests - return { serverArgs.Response }; + promise ContentPromise; + string ErrorMessage; + IHttpResource::Response Response; + string ResponseContent; + http::verb Method; + IHttpResource::Headers RequestHeaders; + bool WithCredentials{ false }; + + ClientParams(http::verb method, IHttpResource::Headers&& headers) + : Method{ method } + , RequestHeaders{ std::move(headers) } + { + } }; - switch (clientArgs.Method) + std::shared_ptr CreateServer(ServerParams& serverArgs, ClientParams& clientArgs) noexcept { + auto server = make_shared(serverArgs.Port); + server->Callbacks().OnOptions = [&serverArgs](const DynamicRequest& request) -> ResponseWrapper + { + return { std::move(serverArgs.Preflight) }; + }; + + auto reqHandler = [&serverArgs](const DynamicRequest& request) -> ResponseWrapper + { + // Don't use move constructor in case of multiple requests + return { serverArgs.Response }; + }; + + switch (clientArgs.Method) + { case http::verb::get: server->Callbacks().OnGet = reqHandler; break; @@ -111,719 +111,720 @@ TEST_CLASS(HttpOriginPolicyIntegrationTest) case http::verb::options: default: - Assert::Fail(L"Unsupported request method"); - } - - return server; - } + Assert::Fail(L"Unsupported request method"); + } - void TestOriginPolicyWithRedirect(ServerParams& server1Args, ServerParams& server2Args, ClientParams& clientArgs, bool shouldSucceed) - { - auto server1 = CreateServer(server1Args, clientArgs); - auto server2 = CreateServer(server2Args, clientArgs); - - server1->Start(); - server2->Start(); + return server; + } - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&clientArgs](int64_t, IHttpResource::Response&& response) + void TestOriginPolicyWithRedirect(ServerParams& server1Args, ServerParams& server2Args, ClientParams& clientArgs, bool shouldSucceed) { - clientArgs.Response = std::move(response); - }); - resource->SetOnData([&clientArgs](int64_t, string&& content) - { - clientArgs.ResponseContent = std::move(content); - clientArgs.ContentPromise.set_value(); - }); - resource->SetOnError([&clientArgs](int64_t, string&& message, bool) - { - clientArgs.ErrorMessage = std::move(message); + auto server1 = CreateServer(server1Args, clientArgs); + auto server2 = CreateServer(server2Args, clientArgs); + + server1->Start(); + server2->Start(); + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&clientArgs](int64_t, IHttpResource::Response&& response) + { + clientArgs.Response = std::move(response); + }); + resource->SetOnData([&clientArgs](int64_t, string&& content) + { + clientArgs.ResponseContent = std::move(content); clientArgs.ContentPromise.set_value(); - }); - - resource->SendRequest( - string{http::to_string(clientArgs.Method).data()}, - string{server1Args.Url}, - 0, /*requestId*/ - std::move(clientArgs.RequestHeaders), - dynamic::object("string", ""), /*data*/ - "text", - false, /*useIncrementalUpdates*/ - 0, /*timeout*/ - clientArgs.WithCredentials, /*withCredentials*/ - [](int64_t){} /*reactCallback*/ - ); - - clientArgs.ContentPromise.get_future().wait(); - - server2->Stop(); - server1->Stop(); - - if (shouldSucceed) - { - Assert::AreEqual({}, clientArgs.ErrorMessage); - //TODO: chose server? - // We assume 2-server tests will always redirect so the final response will come from server 2. - Assert::AreEqual(server2Args.Response.result_int(), static_cast(clientArgs.Response.StatusCode)); - Assert::AreEqual({"RESPONSE_CONTENT"}, clientArgs.ResponseContent); - } - else - { - Assert::AreNotEqual({}, clientArgs.ErrorMessage); + }); + resource->SetOnError([&clientArgs](int64_t, string&& message, bool) + { + clientArgs.ErrorMessage = std::move(message); + clientArgs.ContentPromise.set_value(); + }); + + resource->SendRequest( + string{ http::to_string(clientArgs.Method).data() }, + string{ server1Args.Url }, + 0, /*requestId*/ + std::move(clientArgs.RequestHeaders), + {}, /*data*/ + "text", + false, /*useIncrementalUpdates*/ + 0, /*timeout*/ + clientArgs.WithCredentials, /*withCredentials*/ + [](int64_t) {} /*reactCallback*/ + ); + + clientArgs.ContentPromise.get_future().wait(); + + server2->Stop(); + server1->Stop(); + + if (shouldSucceed) + { + Assert::AreEqual({}, clientArgs.ErrorMessage); + //TODO: chose server? + // We assume 2-server tests will always redirect so the final response will come from server 2. + Assert::AreEqual(server2Args.Response.result_int(), static_cast(clientArgs.Response.StatusCode)); + Assert::AreEqual({ "RESPONSE_CONTENT" }, clientArgs.ResponseContent); + } + else + { + Assert::AreNotEqual({}, clientArgs.ErrorMessage); + } } - } - - void TestOriginPolicy(ServerParams& serverArgs, ClientParams& clientArgs, bool shouldSucceed) - { - auto server = CreateServer(serverArgs, clientArgs); - - server->Start(); - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&clientArgs](int64_t, IHttpResource::Response&& res) + void TestOriginPolicy(ServerParams& serverArgs, ClientParams& clientArgs, bool shouldSucceed) { - clientArgs.Response = std::move(res); - }); - resource->SetOnData([&clientArgs](int64_t, string&& content) - { - clientArgs.ResponseContent = std::move(content); + auto server = CreateServer(serverArgs, clientArgs); + + server->Start(); + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&clientArgs](int64_t, IHttpResource::Response&& res) + { + clientArgs.Response = std::move(res); + }); + resource->SetOnData([&clientArgs](int64_t, string&& content) + { + clientArgs.ResponseContent = std::move(content); clientArgs.ContentPromise.set_value(); - }); - resource->SetOnError([&clientArgs](int64_t, string&& message, bool) - { - clientArgs.ErrorMessage = std::move(message); + }); + resource->SetOnError([&clientArgs](int64_t, string&& message, bool) + { + clientArgs.ErrorMessage = std::move(message); clientArgs.ContentPromise.set_value(); - }); - - resource->SendRequest( - string{http::to_string(clientArgs.Method).data()}, - string{serverArgs.Url}, - 0, /*requestId*/ - std::move(clientArgs.RequestHeaders), - dynamic::object("string", ""), /*data*/ - "text", - false, /*useIncrementalUpdates*/ - 0, /*timeout*/ - clientArgs.WithCredentials, /*withCredentials*/ - [](int64_t) {} /*reactCallback*/ - ); - - clientArgs.ContentPromise.get_future().wait(); - server->Stop(); - - if (shouldSucceed) - { - Assert::AreEqual({}, clientArgs.ErrorMessage); - Assert::AreEqual(serverArgs.Response.result_int(), static_cast(clientArgs.Response.StatusCode)); - Assert::AreEqual({"RESPONSE_CONTENT"}, clientArgs.ResponseContent); + }); + + resource->SendRequest( + string{ http::to_string(clientArgs.Method).data() }, + string{ serverArgs.Url }, + 0, /*requestId*/ + std::move(clientArgs.RequestHeaders), + {}, /*data*/ + "text", + false, /*useIncrementalUpdates*/ + 0, /*timeout*/ + clientArgs.WithCredentials, /*withCredentials*/ + [](int64_t) {} /*reactCallback*/ + ); + + clientArgs.ContentPromise.get_future().wait(); + server->Stop(); + + if (shouldSucceed) + { + Assert::AreEqual({}, clientArgs.ErrorMessage); + Assert::AreEqual(serverArgs.Response.result_int(), static_cast(clientArgs.Response.StatusCode)); + Assert::AreEqual({ "RESPONSE_CONTENT" }, clientArgs.ResponseContent); + } + else + { + Assert::AreNotEqual({}, clientArgs.ErrorMessage); + } } - else + + TEST_METHOD_CLEANUP(MethodCleanup) { - Assert::AreNotEqual({}, clientArgs.ErrorMessage); + // 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++; } - } - 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++; - } - - 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 - TEST_IGNORE() - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(NoCorsForbiddenMethodSucceeds) - { - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); - - constexpr uint16_t port{ 5556 }; - constexpr char url[]{ "http://localhost:5556" }; - - string error; - string getContent; - IHttpResource::Response getResponse; - promise getDataPromise; - - auto server = make_shared(port); - server->Callbacks().OnOptions = [&url](const DynamicRequest& request) -> ResponseWrapper + 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 + TEST_IGNORE() + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(NoCorsForbiddenMethodSucceeds) { - EmptyResponse response; - response.result(http::status::accepted); - - response.set(http::field::access_control_allow_credentials, "false"); - response.set(http::field::access_control_allow_headers, "ValidHeader"); - response.set(http::field::access_control_allow_methods, "GET, POST, DELETE, PATCH"); - response.set(http::field::access_control_allow_origin, url); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); - return { std::move(response) }; - }; - server->Callbacks().OnTrace = [](const DynamicRequest& request) -> ResponseWrapper - { - StringResponse response; - response.result(http::status::ok); - response.body() = "GET_CONTENT"; + constexpr uint16_t port{ 5556 }; + constexpr char url[]{ "http://localhost:5556" }; - return { std::move(response) }; - }; - server->Start(); + string error; + string getContent; + IHttpResource::Response getResponse; + promise getDataPromise; - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&getResponse](int64_t, IHttpResource::Response&& res) - { - getResponse = std::move(res); - }); - resource->SetOnData([&getDataPromise, &getContent](int64_t, string&& content) + auto server = make_shared(port); + server->Callbacks().OnOptions = [&url](const DynamicRequest& request) -> ResponseWrapper { - getContent = std::move(content); - getDataPromise.set_value(); - }); - resource->SetOnError([&server, &error, &getDataPromise](int64_t, string&& message, bool) - { - error = std::move(message); - getDataPromise.set_value(); - }); - - resource->SendRequest( - "TRACE", - url, - 0, /*requestId*/ + EmptyResponse response; + response.result(http::status::accepted); + + response.set(http::field::access_control_allow_credentials, "false"); + response.set(http::field::access_control_allow_headers, "ValidHeader"); + response.set(http::field::access_control_allow_methods, "GET, POST, DELETE, PATCH"); + response.set(http::field::access_control_allow_origin, url); + + return { std::move(response) }; + }; + server->Callbacks().OnTrace = [](const DynamicRequest& request) -> ResponseWrapper { - {"ValidHeader", "AnyValue"} - }, - dynamic::object("string", ""), /*data*/ - "text", - false /*useIncrementalUpdates*/, - 0 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {} /*callback*/ - ); - - getDataPromise.get_future().wait(); - server->Stop(); - - Assert::AreEqual({}, error); - Assert::AreEqual(200, static_cast(getResponse.StatusCode)); - Assert::AreEqual({ "GET_CONTENT" }, getContent); - }// NoCorsForbiddenMethodSucceeds - - BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsForbiddenMethodFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(SimpleCorsForbiddenMethodFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); + StringResponse response; + response.result(http::status::ok); + response.body() = "GET_CONTENT"; + + return { std::move(response) }; + }; + server->Start(); + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&getResponse](int64_t, IHttpResource::Response&& res) + { + getResponse = std::move(res); + }); + resource->SetOnData([&getDataPromise, &getContent](int64_t, string&& content) + { + getContent = std::move(content); + getDataPromise.set_value(); + }); + resource->SetOnError([&server, &error, &getDataPromise](int64_t, string&& message, bool) + { + error = std::move(message); + getDataPromise.set_value(); + }); + + resource->SendRequest( + "TRACE", + url, + 0, /*requestId*/ + { + {"ValidHeader", "AnyValue"} + }, + {}, /*data*/ + //{} /*bodyData*/, + "text", + false /*useIncrementalUpdates*/, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {} /*callback*/ + ); + + getDataPromise.get_future().wait(); + server->Stop(); + + Assert::AreEqual({}, error); + Assert::AreEqual(200, static_cast(getResponse.StatusCode)); + Assert::AreEqual({ "GET_CONTENT" }, getContent); + }// NoCorsForbiddenMethodSucceeds + + BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsForbiddenMethodFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(SimpleCorsForbiddenMethodFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); - ClientParams clientArgs(http::verb::connect, {{"Content-Type", "text/plain"}}); + ClientParams clientArgs(http::verb::connect, { {"Content-Type", "text/plain"} }); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// SimpleCorsForbiddenMethodFails + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// SimpleCorsForbiddenMethodFails - //NoCors_ForbiddenMethodConnect_Failed + //NoCors_ForbiddenMethodConnect_Failed - BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsCrossOriginFetchRequestSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(NoCorsCrossOriginFetchRequestSucceeds) - { - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); + BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsCrossOriginFetchRequestSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(NoCorsCrossOriginFetchRequestSucceeds) + { + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::ok); + ServerParams serverArgs(s_port); + serverArgs.Response.result(http::status::ok); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); + ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// NoCorsCrossOriginFetchRequestSucceeds + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// NoCorsCrossOriginFetchRequestSucceeds - BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsCrossOriginPatchSucceededs) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(NoCorsCrossOriginPatchSucceededs) - { - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); + BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsCrossOriginPatchSucceededs) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(NoCorsCrossOriginPatchSucceededs) + { + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::ok); + ServerParams serverArgs(s_port); + serverArgs.Response.result(http::status::ok); - ClientParams clientArgs(http::verb::patch, {{ "Content-Type", "text/plain" }}); + ClientParams clientArgs(http::verb::patch, { { "Content-Type", "text/plain" } }); - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// NoCorsCrossOriginPatchSucceededs + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// NoCorsCrossOriginPatchSucceededs - // Simple-Cors — Prevents the method from being anything other than HEAD, GET or POST, - // and the headers from being anything other than simple headers (CORS safe listed headers). - // If any ServiceWorkers intercept these requests, they may not add or override any headers except for those that are simple headers. - // In addition, JavaScript may not access any properties of the resulting Response. - // This ensures that ServiceWorkers do not affect the semantics of the Web and prevents security and privacy issues arising from leaking data across domains. - BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsSameOriginSucceededs) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(SimpleCorsSameOriginSucceededs) - { - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::ok); + // Simple-Cors — Prevents the method from being anything other than HEAD, GET or POST, + // and the headers from being anything other than simple headers (CORS safe listed headers). + // If any ServiceWorkers intercept these requests, they may not add or override any headers except for those that are simple headers. + // In addition, JavaScript may not access any properties of the resulting Response. + // This ensures that ServiceWorkers do not affect the semantics of the Web and prevents security and privacy issues arising from leaking data across domains. + BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsSameOriginSucceededs) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(SimpleCorsSameOriginSucceededs) + { + ServerParams serverArgs(s_port); + serverArgs.Response.result(http::status::ok); - ClientParams clientArgs(http::verb::patch, {{ "Content-Type", "text/plain" }}); + ClientParams clientArgs(http::verb::patch, { { "Content-Type", "text/plain" } }); - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// SimpleCorsSameOriginSucceededs + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// SimpleCorsSameOriginSucceededs - BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsCrossOriginFetchFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(SimpleCorsCrossOriginFetchFails) - { - ServerParams serverArgs(s_port); + BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsCrossOriginFetchFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(SimpleCorsCrossOriginFetchFails) + { + ServerParams serverArgs(s_port); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/html" }}); // text/html is a non-simple value + ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/html" } }); // text/html is a non-simple value - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// SimpleCorsCrossOriginFetchFails + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// SimpleCorsCrossOriginFetchFails - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginRequestSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsSameOriginRequestSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::ok); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginRequestSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsSameOriginRequestSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Response.result(http::status::ok); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); // text/plain is a non-simple header + ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); // text/plain is a non-simple header - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// FullCorsSameOriginRequestSucceeds + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// FullCorsSameOriginRequestSucceeds - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginAllowOriginWildcardSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginAllowOriginWildcardSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, "*"); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Response.result(http::status::accepted); - serverArgs.Response.set(http::field::access_control_allow_origin, "*"); - serverArgs.Response.set(http::field::access_control_allow_credentials, "true"); - - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); // text/plain is a non-simple header - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// FullCorsCrossOriginAllowOriginWildcardSucceeds - - // With CORS, Cross-Origin Resource Sharing, the server can decide what origins are permitted to read information from the client. - // Additionally, for non-simple requests, client should preflight the request through the HTTP Options request, and only send the - // actual request after the server has responded that the desired headers are supported. - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMatchingOriginSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginMatchingOriginSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Response.result(http::status::accepted); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Response.set(http::field::access_control_allow_credentials, "true"); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginAllowOriginWildcardSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginAllowOriginWildcardSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, "*"); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Response.result(http::status::accepted); + serverArgs.Response.set(http::field::access_control_allow_origin, "*"); + serverArgs.Response.set(http::field::access_control_allow_credentials, "true"); + + ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); // text/plain is a non-simple header + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// FullCorsCrossOriginAllowOriginWildcardSucceeds + + // With CORS, Cross-Origin Resource Sharing, the server can decide what origins are permitted to read information from the client. + // Additionally, for non-simple requests, client should preflight the request through the HTTP Options request, and only send the + // actual request after the server has responded that the desired headers are supported. + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMatchingOriginSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginMatchingOriginSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Response.result(http::status::accepted); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Response.set(http::field::access_control_allow_credentials, "true"); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); // text/plain is a non-simple header + ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); // text/plain is a non-simple header - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// FullCorsCrossOriginMatchingOriginSucceeds + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// FullCorsCrossOriginMatchingOriginSucceeds - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginWithCredentialsFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginWithCredentialsFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Preflight.set(http::field::access_control_allow_credentials, "true"); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginWithCredentialsFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginWithCredentialsFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Preflight.set(http::field::access_control_allow_credentials, "true"); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); // application/text is a non-simple header - clientArgs.WithCredentials = true; + ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); // application/text is a non-simple header + clientArgs.WithCredentials = true; - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - SetRuntimeOptionBool("Http.OmitCredentials", true); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + SetRuntimeOptionBool("Http.OmitCredentials", true); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// FullCorsCrossOriginWithCredentialsFails + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// FullCorsCrossOriginWithCredentialsFails - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMissingCorsHeadersFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginMissingCorsHeadersFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.erase(http::field::access_control_allow_methods); - serverArgs.Preflight.result(http::status::not_implemented); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMissingCorsHeadersFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginMissingCorsHeadersFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.erase(http::field::access_control_allow_methods); + serverArgs.Preflight.result(http::status::not_implemented); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); // application/text is a non-simple header + ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); // application/text is a non-simple header - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// FullCorsCrossOriginMissingCorsHeadersFails + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// FullCorsCrossOriginMissingCorsHeadersFails - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMismatchedCorsHeaderFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginMismatchedCorsHeaderFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Response.result(http::status::accepted); - serverArgs.Response.set(http::field::access_control_allow_origin, "http://other.example.rnw"); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMismatchedCorsHeaderFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginMismatchedCorsHeaderFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Response.result(http::status::accepted); + serverArgs.Response.set(http::field::access_control_allow_origin, "http://other.example.rnw"); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); // application/text is a non-simple header + ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); // application/text is a non-simple header - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// FullCorsCrossOriginMismatchedCorsHeaderFails + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// FullCorsCrossOriginMismatchedCorsHeaderFails - // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginCheckFailsOnPreflightRedirectFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginCheckFailsOnPreflightRedirectFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Preflight.set(http::field::location, "http://any-host.extension"); - serverArgs.Preflight.result(http::status::moved_permanently); + // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginCheckFailsOnPreflightRedirectFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginCheckFailsOnPreflightRedirectFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Preflight.set(http::field::location, "http://any-host.extension"); + serverArgs.Preflight.result(http::status::moved_permanently); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); + ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// FullCorsCrossOriginCheckFailsOnPreflightRedirectFails + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// FullCorsCrossOriginCheckFailsOnPreflightRedirectFails - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCorsCheckFailsOnResponseRedirectFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCorsCheckFailsOnResponseRedirectFails) - { - ServerParams serverArgs(s_port); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCorsCheckFailsOnResponseRedirectFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCorsCheckFailsOnResponseRedirectFails) + { + ServerParams serverArgs(s_port); - // server1 allowed origin header includes http://example.com - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + // server1 allowed origin header includes http://example.com + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - // This is a CORS request to server1, but server1 redirects the request to server2 - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - // Redir server's URL - serverArgs.Response.set(http::field::location, "http://localhost:6666"); - serverArgs.Response.set(http::field::server, "BaseServer"); + // This is a CORS request to server1, but server1 redirects the request to server2 + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + // Redir server's URL + serverArgs.Response.set(http::field::location, "http://localhost:6666"); + serverArgs.Response.set(http::field::server, "BaseServer"); - // Server2 does not set Access-Control-Allow-Origin for GET requests - ServerParams redirServerArgs(++s_port); - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::server, "RedirectServer"); + // Server2 does not set Access-Control-Allow-Origin for GET requests + ServerParams redirServerArgs(++s_port); + redirServerArgs.Response.result(http::status::accepted); + redirServerArgs.Response.set(http::field::server, "RedirectServer"); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); + ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); - }// FullCorsCorsCheckFailsOnResponseRedirectFails + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); + }// FullCorsCorsCheckFailsOnResponseRedirectFails - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginToSameOriginRedirectSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsSameOriginToSameOriginRedirectSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::location, serverArgs.Url); - serverArgs.Response.result(http::status::accepted); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginToSameOriginRedirectSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsSameOriginToSameOriginRedirectSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::location, serverArgs.Url); + serverArgs.Response.result(http::status::accepted); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); + ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); - } // FullCorsSameOriginToSameOriginRedirectSucceeds + TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); + } // FullCorsSameOriginToSameOriginRedirectSucceeds - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginToCrossOriginRedirectSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsSameOriginToCrossOriginRedirectSucceeds) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginToCrossOriginRedirectSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsSameOriginToCrossOriginRedirectSucceeds) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); - serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); + serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, redirServerArgs.Url); - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::access_control_allow_origin, serverArgs.Url); + redirServerArgs.Response.result(http::status::accepted); + redirServerArgs.Response.set(http::field::access_control_allow_origin, serverArgs.Url); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); + ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); - } // FullCorsSameOriginToCrossOriginRedirectSucceeds + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); + } // FullCorsSameOriginToCrossOriginRedirectSucceeds - //TODO: Seems to redirect to exact same resource. Implement second resource in same server. - // Redirects a cross origin request to cross origin request on the same server - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToCrossOriginRedirectSucceeds) - TEST_IGNORE() - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToCrossOriginRedirectSucceeds) - { - ServerParams serverArgs(s_port); - //ServerParams redirServerArgs(++s_port); + //TODO: Seems to redirect to exact same resource. Implement second resource in same server. + // Redirects a cross origin request to cross origin request on the same server + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToCrossOriginRedirectSucceeds) + TEST_IGNORE() + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToCrossOriginRedirectSucceeds) + { + ServerParams serverArgs(s_port); + //ServerParams redirServerArgs(++s_port); - serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, serverArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, serverArgs.Url); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - //redirServerArgs.Response.result(http::status::accepted); - //redirServerArgs.Response.set(http::field::access_control_allow_origin, serverArgs.Url); + //redirServerArgs.Response.result(http::status::accepted); + //redirServerArgs.Response.set(http::field::access_control_allow_origin, serverArgs.Url); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); + ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, /*redirServerArgs, */clientArgs, s_shouldSucceed); - } // FullCorsCrossOriginToCrossOriginRedirectSucceeds + TestOriginPolicy(serverArgs, /*redirServerArgs, */clientArgs, s_shouldSucceed); + } // FullCorsCrossOriginToCrossOriginRedirectSucceeds - // The initial request gets redirected back to the original origin, - // but it will lack the Access-Control-Allow-Origin header. - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToOriginalOriginRedirectFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToOriginalOriginRedirectFails) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + // The initial request gets redirected back to the original origin, + // but it will lack the Access-Control-Allow-Origin header. + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToOriginalOriginRedirectFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToOriginalOriginRedirectFails) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, redirServerArgs.Url); + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, redirServerArgs.Url); + serverArgs.Response.set(http::field::access_control_allow_origin, redirServerArgs.Url); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); + ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); - SetRuntimeOptionString("Http.GlobalOrigin", redirServerArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + SetRuntimeOptionString("Http.GlobalOrigin", redirServerArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); - } // FullCorsCrossOriginToOriginalOriginRedirectFails + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); + } // FullCorsCrossOriginToOriginalOriginRedirectFails - // Redirects cross origin request to server1 to cross origin request to server2 - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + // Redirects cross origin request to server1 to cross origin request to server2 + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, redirServerArgs.Url); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::access_control_allow_origin, "*"); + redirServerArgs.Response.result(http::status::accepted); + redirServerArgs.Response.set(http::field::access_control_allow_origin, "*"); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); + ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); - } // FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); + } // FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - // server1 redirects the GET request to server2 - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + // server1 redirects the GET request to server2 + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, redirServerArgs.Url); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - // Since redirect tainted the origin, the server has to allow all origins for CORS to succeed - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::access_control_allow_origin, "*"); + // Since redirect tainted the origin, the server has to allow all origins for CORS to succeed + redirServerArgs.Response.result(http::status::accepted); + redirServerArgs.Response.set(http::field::access_control_allow_origin, "*"); - // PATCH is not a simple method, so preflight is required for server1 - ClientParams clientArgs(http::verb::patch, {{ "Content-Type", "text/plain" }}); + // PATCH is not a simple method, so preflight is required for server1 + ClientParams clientArgs(http::verb::patch, { { "Content-Type", "text/plain" } }); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); - } // FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); + } // FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); - - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - // server1 redirects the GET request to server2 - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - // Since redirect tainted the origin, the server does not know what origin to allow through a single value. - // Even if server successfully guessed the single value, it will still fail on the client side. - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - // PATCH is not a simple method, so preflight is required for server1 - ClientParams clientArgs(http::verb::patch, {{ "Content-Type", "text/plain" }}); - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); - } // FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails - - BEGIN_TEST_METHOD_ATTRIBUTE(FullCors304ForSimpleGetFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCors304ForSimpleGetFails) - { - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::not_modified); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); + + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + // server1 redirects the GET request to server2 + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, redirServerArgs.Url); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + + // Since redirect tainted the origin, the server does not know what origin to allow through a single value. + // Even if server successfully guessed the single value, it will still fail on the client side. + redirServerArgs.Response.result(http::status::accepted); + redirServerArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + + // PATCH is not a simple method, so preflight is required for server1 + ClientParams clientArgs(http::verb::patch, { { "Content-Type", "text/plain" } }); + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); + } // FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails + + BEGIN_TEST_METHOD_ATTRIBUTE(FullCors304ForSimpleGetFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCors304ForSimpleGetFails) + { + ServerParams serverArgs(s_port); + serverArgs.Response.result(http::status::not_modified); - // PATCH is not a simple method, so preflight is required for server1 - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); + // PATCH is not a simple method, so preflight is required for server1 + ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - } // FullCors304ForSimpleGetFails + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + } // FullCors304ForSimpleGetFails - TEST_METHOD(FullCorsPreflightSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "ArbitraryHeader"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "ArbitraryHeader"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Response.result(http::status::ok); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - ClientParams clientArgs(http::verb::get, { {"Content-Type", "text/plain"}, {"ArbitraryHeader", "AnyValue"} }); - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); - }// FullCorsPreflightSucceeds - - // 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) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginWithCredentialsSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Preflight.set(http::field::access_control_allow_credentials, "true"); - serverArgs.Response.result(http::status::accepted); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); // application/text is a non-simple header - clientArgs.WithCredentials = true; - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); - }// FullCorsCrossOriginWithCredentialsSucceeds - - // "Host" is one of the forbidden headers for fetch - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsRequestWithHostHeaderFails) - // "Host" is not an accepted request header in WinRT. - TEST_IGNORE() - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsRequestWithHostHeaderFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Response.result(http::status::accepted); + TEST_METHOD(FullCorsPreflightSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "ArbitraryHeader"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "ArbitraryHeader"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Response.result(http::status::ok); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + + ClientParams clientArgs(http::verb::get, { {"Content-Type", "text/plain"}, {"ArbitraryHeader", "AnyValue"} }); + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); + }// FullCorsPreflightSucceeds + + // 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) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginWithCredentialsSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Preflight.set(http::field::access_control_allow_credentials, "true"); + serverArgs.Response.result(http::status::accepted); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + + ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); // application/text is a non-simple header + clientArgs.WithCredentials = true; + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); + }// FullCorsCrossOriginWithCredentialsSucceeds + + // "Host" is one of the forbidden headers for fetch + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsRequestWithHostHeaderFails) + // "Host" is not an accepted request header in WinRT. + TEST_IGNORE() + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsRequestWithHostHeaderFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Response.result(http::status::accepted); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }, { "Host", "http://sub.example.rnw" }}); + ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" }, { "Host", "http://sub.example.rnw" } }); - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - Assert::Fail(L"FIX!!! Passes for the worng reason. Error: 0x80070057 : 'Invalid HTTP headers.'"); - }// FullCorsRequestWithHostHeaderFails + Assert::Fail(L"FIX!!! Passes for the worng reason. Error: 0x80070057 : 'Invalid HTTP headers.'"); + }// FullCorsRequestWithHostHeaderFails - BEGIN_TEST_METHOD_ATTRIBUTE(RequestWithProxyAuthorizationHeaderFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(RequestWithProxyAuthorizationHeaderFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Response.result(http::status::accepted); + BEGIN_TEST_METHOD_ATTRIBUTE(RequestWithProxyAuthorizationHeaderFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(RequestWithProxyAuthorizationHeaderFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Response.result(http::status::accepted); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }, { "Proxy-Authorization", "Basic Zm9vOmJhcg==" }}); + ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" }, { "Proxy-Authorization", "Basic Zm9vOmJhcg==" } }); - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// RequestWithProxyAuthorizationHeaderFails -}; + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// RequestWithProxyAuthorizationHeaderFails + }; -uint16_t HttpOriginPolicyIntegrationTest::s_port = 7777; + uint16_t HttpOriginPolicyIntegrationTest::s_port = 7777; }//namespace Microsoft::React::Test // clang-format on diff --git a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp index dbe74138144..5ad55e8003a 100644 --- a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp +++ b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp @@ -37,46 +37,46 @@ using Test::ResponseWrapper; namespace Microsoft::React::Test { -TEST_CLASS (HttpResourceIntegrationTest) { - static uint16_t s_port; - - TEST_METHOD_CLEANUP(MethodCleanup) { - // Clear any runtime options that may be used by tests in this class. - MicrosoftReactSetRuntimeOptionString("Http.UserAgent", nullptr); - - // Bug in test HTTP server does not correctly release TCP port between test methods. - // Using a different por per test for now. - s_port++; - } - TEST_METHOD(RequestGetSucceeds) { - string url = "http://localhost:" + std::to_string(s_port); - - promise resPromise; - string error; - int statusCode = 0; - - auto server = make_shared(s_port); - server->Callbacks().OnGet = [&resPromise](const DynamicRequest &request) -> ResponseWrapper { - DynamicResponse response; - response.result(http::status::ok); - response.body() = Test::CreateStringResponseBody("some response content"); - - return {std::move(response)}; - }; - server->Start(); - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&resPromise, &statusCode](int64_t, IHttpResource::Response response) { - statusCode = static_cast(response.StatusCode); - }); - resource->SetOnData([&resPromise](int64_t, string &&content) { resPromise.set_value(); }); - resource->SetOnError([&resPromise, &error, &server](int64_t, string &&message, bool) { - error = std::move(message); + TEST_CLASS(HttpResourceIntegrationTest) { + static uint16_t s_port; + + TEST_METHOD_CLEANUP(MethodCleanup) { + // Clear any runtime options that may be used by tests in this class. + MicrosoftReactSetRuntimeOptionString("Http.UserAgent", nullptr); + + // Bug in test HTTP server does not correctly release TCP port between test methods. + // Using a different por per test for now. + s_port++; + } + TEST_METHOD(RequestGetSucceeds) { + string url = "http://localhost:" + std::to_string(s_port); + + promise resPromise; + string error; + int statusCode = 0; + + auto server = make_shared(s_port); + server->Callbacks().OnGet = [&resPromise](const DynamicRequest& request) -> ResponseWrapper { + DynamicResponse response; + response.result(http::status::ok); + response.body() = Test::CreateStringResponseBody("some response content"); + + return { std::move(response) }; + }; + server->Start(); + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&resPromise, &statusCode](int64_t, IHttpResource::Response response) { + statusCode = static_cast(response.StatusCode); + }); + resource->SetOnData([&resPromise](int64_t, string&& content) { resPromise.set_value(); }); + resource->SetOnError([&resPromise, &error, &server](int64_t, string&& message, bool) { + error = std::move(message); resPromise.set_value(); server->Stop(); - }); - resource->SendRequest( + }); + resource->SendRequest( "GET", std::move(url), 0, /*requestId*/ @@ -88,51 +88,51 @@ TEST_CLASS (HttpResourceIntegrationTest) { false /*withCredentials*/, [](int64_t) {}); - // Synchronize response. - resPromise.get_future().wait(); - server->Stop(); + // Synchronize response. + resPromise.get_future().wait(); + server->Stop(); - Assert::AreEqual({}, error); - Assert::AreEqual(200, statusCode); - } + Assert::AreEqual({}, error); + Assert::AreEqual(200, statusCode); + } - TEST_METHOD(RequestGetHeadersSucceeds) { - string url = "http://localhost:" + std::to_string(s_port); + TEST_METHOD(RequestGetHeadersSucceeds) { + string url = "http://localhost:" + std::to_string(s_port); - promise rcPromise; - string error; - IHttpResource::Response response; + promise rcPromise; + string error; + IHttpResource::Response response; - auto server = make_shared(s_port); - server->Callbacks().OnGet = [](const DynamicRequest &request) -> ResponseWrapper { - DynamicResponse response; - response.result(http::status::ok); + auto server = make_shared(s_port); + server->Callbacks().OnGet = [](const DynamicRequest& request) -> ResponseWrapper { + DynamicResponse response; + response.result(http::status::ok); - // Response header - response.set(http::field::server, "Microsoft::React::Test::HttpServer"); - // Response content header - response.set(http::field::content_length, "0"); - // Response arbitrary header - response.set("ResponseHeaderName1", "ResponseHeaderValue1"); + // Response header + response.set(http::field::server, "Microsoft::React::Test::HttpServer"); + // Response content header + response.set(http::field::content_length, "0"); + // Response arbitrary header + response.set("ResponseHeaderName1", "ResponseHeaderValue1"); - return {std::move(response)}; - }; - server->Start(); + return { std::move(response) }; + }; + server->Start(); - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { - response = callbackResponse; + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { + response = callbackResponse; rcPromise.set_value(); - }); - resource->SetOnError([&rcPromise, &error, &server](int64_t, string &&message, bool) { - error = std::move(message); + }); + resource->SetOnError([&rcPromise, &error, &server](int64_t, string&& message, bool) { + error = std::move(message); rcPromise.set_value(); server->Abort(); - }); + }); - //clang-format off - resource->SendRequest( + //clang-format off + resource->SendRequest( "GET", std::move(url), 0, /*requestId*/ @@ -148,84 +148,87 @@ TEST_CLASS (HttpResourceIntegrationTest) { 0 /*timeout*/, false /*withCredentials*/, [](int64_t) {}); - //clang-format on - - rcPromise.get_future().wait(); - server->Stop(); - - Assert::AreEqual({}, error, L"Error encountered"); - for (auto header : response.Headers) { - if (header.first == "Server") { - Assert::AreEqual({"Microsoft::React::Test::HttpServer"}, header.second, L"Wrong header"); - } else if (header.first == "Content-Length") { - Assert::AreEqual({"0"}, header.second, L"Wrong header"); - } else if (header.first == "ResponseHeaderName1") { - Assert::AreEqual({"ResponseHeaderValue1"}, header.second, L"Wrong header"); - } else { - string message = "Unexpected header: [" + header.first + "]=[" + header.second + "]"; - Assert::Fail(Microsoft::Common::Unicode::Utf8ToUtf16(message).c_str()); + //clang-format on + + rcPromise.get_future().wait(); + server->Stop(); + + Assert::AreEqual({}, error, L"Error encountered"); + for (auto header : response.Headers) { + if (header.first == "Server") { + Assert::AreEqual({ "Microsoft::React::Test::HttpServer" }, header.second, L"Wrong header"); + } + else if (header.first == "Content-Length") { + Assert::AreEqual({ "0" }, header.second, L"Wrong header"); + } + else if (header.first == "ResponseHeaderName1") { + Assert::AreEqual({ "ResponseHeaderValue1" }, header.second, L"Wrong header"); + } + else { + string message = "Unexpected header: [" + header.first + "]=[" + header.second + "]"; + Assert::Fail(Microsoft::Common::Unicode::Utf8ToUtf16(message).c_str()); + } } } - } - TEST_METHOD(RequestGetExplicitUserAgentSucceeds) { - string url = "https://api.github.com/repos/microsoft/react-native-xaml"; + TEST_METHOD(RequestGetExplicitUserAgentSucceeds) { + string url = "https://api.github.com/repos/microsoft/react-native-xaml"; - promise rcPromise; - string error; - IHttpResource::Response response; + promise rcPromise; + string error; + IHttpResource::Response response; - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { - response = callbackResponse; + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { + response = callbackResponse; rcPromise.set_value(); - }); - resource->SetOnError([&rcPromise, &error](int64_t, string &&message, bool) { - error = std::move(message); + }); + resource->SetOnError([&rcPromise, &error](int64_t, string&& message, bool) { + error = std::move(message); rcPromise.set_value(); - }); + }); - //clang-format off - resource->SendRequest( + //clang-format off + resource->SendRequest( "GET", std::move(url), 0, /*requestId*/ - {{"User-Agent", "React Native Windows"}}, + { {"User-Agent", "React Native Windows"} }, {}, /*data*/ "text", /*responseType*/ false, 0 /*timeout*/, false /*withCredentials*/, [](int64_t) {}); - //clang-format on + //clang-format on - rcPromise.get_future().wait(); + rcPromise.get_future().wait(); - Assert::AreEqual({}, error, L"Error encountered"); - Assert::AreEqual(static_cast(200), response.StatusCode); - } + Assert::AreEqual({}, error, L"Error encountered"); + Assert::AreEqual(static_cast(200), response.StatusCode); + } - TEST_METHOD(RequestGetImplicitUserAgentSucceeds) { - string url = "https://api.github.com/repos/microsoft/react-native-windows"; + TEST_METHOD(RequestGetImplicitUserAgentSucceeds) { + string url = "https://api.github.com/repos/microsoft/react-native-windows"; - promise rcPromise; - string error; - IHttpResource::Response response; + promise rcPromise; + string error; + IHttpResource::Response response; - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { - response = callbackResponse; + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { + response = callbackResponse; rcPromise.set_value(); - }); - resource->SetOnError([&rcPromise, &error](int64_t, string &&message, bool) { - error = std::move(message); + }); + resource->SetOnError([&rcPromise, &error](int64_t, string&& message, bool) { + error = std::move(message); rcPromise.set_value(); - }); + }); - MicrosoftReactSetRuntimeOptionString("Http.UserAgent", "React Native Windows"); + MicrosoftReactSetRuntimeOptionString("Http.UserAgent", "React Native Windows"); - //clang-format off - resource->SendRequest( + //clang-format off + resource->SendRequest( "GET", std::move(url), 0, /*requestId*/ @@ -236,34 +239,34 @@ TEST_CLASS (HttpResourceIntegrationTest) { 0 /*timeout*/, false /*withCredentials*/, [](int64_t) {}); - //clang-format on + //clang-format on - rcPromise.get_future().wait(); + rcPromise.get_future().wait(); - Assert::AreEqual({}, error, L"Error encountered"); - Assert::AreEqual(static_cast(200), response.StatusCode); - } + Assert::AreEqual({}, error, L"Error encountered"); + Assert::AreEqual(static_cast(200), response.StatusCode); + } - TEST_METHOD(RequestGetMissingUserAgentFails) { - // string url = "http://localhost:" + std::to_string(s_port); - string url = "https://api.github.com/repos/microsoft/react-native-macos"; + TEST_METHOD(RequestGetMissingUserAgentFails) { + // string url = "http://localhost:" + std::to_string(s_port); + string url = "https://api.github.com/repos/microsoft/react-native-macos"; - promise rcPromise; - string error; - IHttpResource::Response response; + promise rcPromise; + string error; + IHttpResource::Response response; - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { - response = callbackResponse; + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { + response = callbackResponse; rcPromise.set_value(); - }); - resource->SetOnError([&rcPromise, &error](int64_t, string &&message, bool) { - error = std::move(message); + }); + resource->SetOnError([&rcPromise, &error](int64_t, string&& message, bool) { + error = std::move(message); rcPromise.set_value(); - }); + }); - //clang-format off - resource->SendRequest( + //clang-format off + resource->SendRequest( "GET", std::move(url), 0, /*requestId*/ @@ -274,92 +277,93 @@ TEST_CLASS (HttpResourceIntegrationTest) { 0 /*timeout*/, false /*withCredentials*/, [](int64_t) {}); - //clang-format on + //clang-format on - rcPromise.get_future().wait(); + rcPromise.get_future().wait(); - Assert::AreEqual({}, error, L"Error encountered"); - Assert::AreEqual(static_cast(403), response.StatusCode); - } + Assert::AreEqual({}, error, L"Error encountered"); + Assert::AreEqual(static_cast(403), response.StatusCode); + } - TEST_METHOD(RequestGetFails) { - string error; - promise promise; + TEST_METHOD(RequestGetFails) { + string error; + promise promise; - auto resource = IHttpResource::Make(); - resource->SetOnError([&error, &promise](int64_t, string &&message, bool) { - error = message; + auto resource = IHttpResource::Make(); + resource->SetOnError([&error, &promise](int64_t, string&& message, bool) { + error = message; promise.set_value(); - }); - - resource->SendRequest("GET", "http://nonexistinghost", 0, {}, {}, "text", false, 0, false, [](int64_t) {}); - - promise.get_future().wait(); - - Logger::WriteMessage(error.c_str()); - Assert::AreNotEqual(string{}, error); - } - - TEST_METHOD(RequestOptionsSucceeds) { - string url = "http://localhost:" + std::to_string(s_port); - - promise getResponsePromise; - promise getDataPromise; - promise optionsPromise; - string error; - IHttpResource::Response getResponse; - IHttpResource::Response optionsResponse; - string content; - - auto server = make_shared(s_port); - server->Callbacks().OnOptions = [](const DynamicRequest &request) -> ResponseWrapper { - EmptyResponse response; - response.result(http::status::partial_content); - response.set("PreflightName", "PreflightValue"); - - return {std::move(response)}; - }; - server->Callbacks().OnGet = [](const DynamicRequest &request) -> ResponseWrapper { - DynamicResponse response; - response.result(http::status::ok); - response.body() = Test::CreateStringResponseBody("Response Body"); - - return {std::move(response)}; - }; - server->Start(); - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&getResponse, &getResponsePromise, &optionsResponse, &optionsPromise]( - int64_t, IHttpResource::Response callbackResponse) { - if (callbackResponse.StatusCode == static_cast(http::status::ok)) { - getResponse = callbackResponse; - getResponsePromise.set_value(); - } else if (callbackResponse.StatusCode == static_cast(http::status::partial_content)) { - optionsResponse = callbackResponse; - optionsPromise.set_value(); - } - }); - resource->SetOnData([&getDataPromise, &content](int64_t, string &&responseData) { - content = std::move(responseData); + }); + + resource->SendRequest("GET", "http://nonexistinghost", 0, {}, {}, "text", false, 1000, false, [](int64_t) {}); + + promise.get_future().wait(); + + Logger::WriteMessage(error.c_str()); + Assert::AreNotEqual(string{}, error); + } + + TEST_METHOD(RequestOptionsSucceeds) { + string url = "http://localhost:" + std::to_string(s_port); + + promise getResponsePromise; + promise getDataPromise; + promise optionsPromise; + string error; + IHttpResource::Response getResponse; + IHttpResource::Response optionsResponse; + string content; + + auto server = make_shared(s_port); + server->Callbacks().OnOptions = [](const DynamicRequest& request) -> ResponseWrapper { + EmptyResponse response; + response.result(http::status::partial_content); + response.set("PreflightName", "PreflightValue"); + + return { std::move(response) }; + }; + server->Callbacks().OnGet = [](const DynamicRequest& request) -> ResponseWrapper { + DynamicResponse response; + response.result(http::status::ok); + response.body() = Test::CreateStringResponseBody("Response Body"); + + return { std::move(response) }; + }; + server->Start(); + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&getResponse, &getResponsePromise, &optionsResponse, &optionsPromise]( + int64_t, IHttpResource::Response callbackResponse) { + if (callbackResponse.StatusCode == static_cast(http::status::ok)) { + getResponse = callbackResponse; + getResponsePromise.set_value(); + } + else if (callbackResponse.StatusCode == static_cast(http::status::partial_content)) { + optionsResponse = callbackResponse; + optionsPromise.set_value(); + } + }); + resource->SetOnData([&getDataPromise, &content](int64_t, string&& responseData) { + content = std::move(responseData); if (!content.empty()) getDataPromise.set_value(); - }); - resource->SetOnError( - [&optionsPromise, &getResponsePromise, &getDataPromise, &error, &server](int64_t, string &&message, bool) { + }); + resource->SetOnError( + [&optionsPromise, &getResponsePromise, &getDataPromise, &error, &server](int64_t, string&& message, bool) { error = std::move(message); - optionsPromise.set_value(); - getResponsePromise.set_value(); - getDataPromise.set_value(); + optionsPromise.set_value(); + getResponsePromise.set_value(); + getDataPromise.set_value(); - server->Stop(); + server->Stop(); }); - //clang-format off - resource->SendRequest( + //clang-format off + resource->SendRequest( "OPTIONS", - string{url}, + string{ url }, 0, /*requestId*/ {}, /*headers*/ {}, /*data*/ @@ -368,7 +372,7 @@ TEST_CLASS (HttpResourceIntegrationTest) { 1000 /*timeout*/, false /*withCredentials*/, [](int64_t) {}); - resource->SendRequest( + resource->SendRequest( "GET", std::move(url), 0, /*requestId*/ @@ -379,79 +383,80 @@ TEST_CLASS (HttpResourceIntegrationTest) { 0 /*timeout*/, false /*withCredentials*/, [](int64_t) {}); - //clang-format on - - optionsPromise.get_future().wait(); - getResponsePromise.get_future().wait(); - getDataPromise.get_future().wait(); - server->Stop(); - - Assert::AreEqual({}, error, L"Error encountered"); - Assert::AreEqual(static_cast(1), optionsResponse.Headers.size()); - for (auto header : optionsResponse.Headers) { - if (header.first == "PreflightName") { - Assert::AreEqual({"PreflightValue"}, header.second, L"Wrong header"); - } else { - string message = "Unexpected header: [" + header.first + "]=[" + header.second + "]"; - Assert::Fail(Microsoft::Common::Unicode::Utf8ToUtf16(message).c_str()); + //clang-format on + + optionsPromise.get_future().wait(); + getResponsePromise.get_future().wait(); + getDataPromise.get_future().wait(); + server->Stop(); + + Assert::AreEqual({}, error, L"Error encountered"); + Assert::AreEqual(static_cast(1), optionsResponse.Headers.size()); + for (auto header : optionsResponse.Headers) { + if (header.first == "PreflightName") { + Assert::AreEqual({ "PreflightValue" }, header.second, L"Wrong header"); + } + else { + string message = "Unexpected header: [" + header.first + "]=[" + header.second + "]"; + Assert::Fail(Microsoft::Common::Unicode::Utf8ToUtf16(message).c_str()); + } } + Assert::AreEqual({ "Response Body" }, content); } - Assert::AreEqual({"Response Body"}, content); - } - - TEST_METHOD(SimpleRedirectGetSucceeds) { - 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().OnGet = [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().OnGet = [](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); + + TEST_METHOD(SimpleRedirectGetSucceeds) { + 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().OnGet = [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().OnGet = [](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); + }); + 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( + //clang-format off + resource->SendRequest( "GET", std::move(url), 0, /*requestId*/ @@ -462,72 +467,72 @@ TEST_CLASS (HttpResourceIntegrationTest) { 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(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); + //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(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); + }); + 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( + //clang-format off + resource->SendRequest( "PATCH", std::move(url), 0, /*requestId*/ @@ -538,52 +543,52 @@ TEST_CLASS (HttpResourceIntegrationTest) { 0 /*timeout*/, false /*withCredentials*/, [](int64_t) {}); - //clang-format on + //clang-format on - responsePromise.get_future().wait(); - contentPromise.get_future().wait(); + responsePromise.get_future().wait(); + contentPromise.get_future().wait(); - server2->Stop(); - server1->Stop(); + server2->Stop(); + server1->Stop(); - Assert::AreEqual({}, error, L"Error encountered"); - Assert::AreEqual(static_cast(200), responseResult.StatusCode); - Assert::AreEqual({"Redirect Content"}, content); - } + 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); + 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; + 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); + 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)); + // Hold response to test client timeout + promise timer; + timer.get_future().wait_for(std::chrono::milliseconds(2000)); - return {std::move(response)}; - }; - server->Start(); + return { std::move(response) }; + }; + server->Start(); - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&getPromise, &statusCode](int64_t, IHttpResource::Response response) { - statusCode = static_cast(response.StatusCode); + 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); + }); + resource->SetOnError([&getPromise, &error, &timeoutError](int64_t, string&& errorMessage, bool isTimeout) { + error = std::move(errorMessage); timeoutError = isTimeout; getPromise.set_value(); - }); - resource->SendRequest( + }); + resource->SendRequest( "GET", std::move(url), 0, /*requestId*/ @@ -595,47 +600,47 @@ TEST_CLASS (HttpResourceIntegrationTest) { false, /*withCredentials*/ [](int64_t) {} /*callback*/); - getPromise.get_future().wait(); - server->Stop(); + getPromise.get_future().wait(); + server->Stop(); - Assert::AreEqual({}, error); - Assert::IsFalse(timeoutError); - Assert::AreEqual(200, statusCode); - } + 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); + 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; + 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); + 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)); + // Hold response to test client timeout + promise timer; + timer.get_future().wait_for(std::chrono::milliseconds(4000)); - return {std::move(response)}; - }; - server->Start(); + return { std::move(response) }; + }; + server->Start(); - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&getPromise, &statusCode](int64_t, IHttpResource::Response response) { - statusCode = static_cast(response.StatusCode); + 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); + }); + resource->SetOnError([&getPromise, &error, &timeoutError](int64_t, string&& errorMessage, bool isTimeout) { + error = std::move(errorMessage); timeoutError = isTimeout; getPromise.set_value(); - }); - resource->SendRequest( + }); + resource->SendRequest( "GET", std::move(url), 0, /*requestId*/ @@ -647,15 +652,15 @@ TEST_CLASS (HttpResourceIntegrationTest) { false, /*withCredentials*/ [](int64_t) {} /*callback*/); - getPromise.get_future().wait(); - server->Stop(); + 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); - } -}; + 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; + /*static*/ uint16_t HttpResourceIntegrationTest::s_port = 4444; } // namespace Microsoft::React::Test diff --git a/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp b/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp index 9522dc4b59a..cc5d1cd8b19 100644 --- a/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp +++ b/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp @@ -19,93 +19,82 @@ namespace Microsoft::ReactNative { -using winrt::Microsoft::ReactNative::ReactPropertyBag; + using winrt::Microsoft::ReactNative::ReactPropertyBag; -namespace { + namespace { -using winrt::Microsoft::ReactNative::ReactPropertyId; + using winrt::Microsoft::ReactNative::ReactPropertyId; -bool HasPackageIdentity() noexcept { - static const bool hasPackageIdentity = []() noexcept { - auto packageStatics = winrt::get_activation_factory( - winrt::name_of()); - auto abiPackageStatics = static_cast *>( - winrt::get_abi(packageStatics)); - winrt::com_ptr> dummy; - return abiPackageStatics->get_Current(winrt::put_abi(dummy)) != - winrt::impl::hresult_from_win32(APPMODEL_ERROR_NO_PACKAGE); - }(); + bool HasPackageIdentity() noexcept { + static const bool hasPackageIdentity = []() noexcept { + auto packageStatics = winrt::get_activation_factory( + winrt::name_of()); + auto abiPackageStatics = static_cast *>( + winrt::get_abi(packageStatics)); + winrt::com_ptr> dummy; + return abiPackageStatics->get_Current(winrt::put_abi(dummy)) != + winrt::impl::hresult_from_win32(APPMODEL_ERROR_NO_PACKAGE); + }(); - return hasPackageIdentity; -} + return hasPackageIdentity; + } -ReactPropertyId HttpUseMonolithicModuleProperty() noexcept { - static ReactPropertyId propId{ - L"ReactNative.Http" - L"UseMonolithicModule"}; - return propId; -} + ReactPropertyId HttpUseMonolithicModuleProperty() noexcept { + static ReactPropertyId propId{ + L"ReactNative.Http" + L"UseMonolithicModule" }; + return propId; + } -} // namespace + } // namespace -std::vector GetCoreModules( - const std::shared_ptr &batchingUIMessageQueue, + std::vector GetCoreModules( + const std::shared_ptr& batchingUIMessageQueue, const std::shared_ptr - &jsMessageQueue, // JS engine thread (what we use for external modules) - Mso::CntPtr &&appearanceListener, - Mso::CntPtr &&context) noexcept { - std::vector modules; + & jsMessageQueue, // JS engine thread (what we use for external modules) + Mso::CntPtr&& appearanceListener, + Mso::CntPtr&& context) noexcept { + std::vector modules; - modules.emplace_back( + modules.emplace_back( "Networking", [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( + // 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)); }, batchingUIMessageQueue); - modules.emplace_back( + modules.emplace_back( AppearanceModule::Name, [appearanceListener = std::move(appearanceListener)]() mutable { return std::make_unique(std::move(appearanceListener)); }, jsMessageQueue); - // AsyncStorageModule doesn't work without package identity (it indirectly depends on - // Windows.Storage.StorageFile), so check for package identity before adding it. - modules.emplace_back( + // AsyncStorageModule doesn't work without package identity (it indirectly depends on + // Windows.Storage.StorageFile), so check for package identity before adding it. + modules.emplace_back( "AsyncLocalStorage", []() -> std::unique_ptr { if (HasPackageIdentity()) { return std::make_unique(L"asyncStorage"); - } else { + } + else { return std::make_unique(); } }, jsMessageQueue); - return modules; -} + return modules; + } } // namespace Microsoft::ReactNative diff --git a/vnext/Shared/Modules/BlobModule.cpp b/vnext/Shared/Modules/BlobModule.cpp index 867e45c0193..1c4927f9187 100644 --- a/vnext/Shared/Modules/BlobModule.cpp +++ b/vnext/Shared/Modules/BlobModule.cpp @@ -41,172 +41,176 @@ using winrt::Windows::Security::Cryptography::CryptographicBuffer; namespace fs = std::filesystem; namespace { -constexpr char moduleName[] = "BlobModule"; -constexpr char blobKey[] = "blob"; -constexpr char blobIdKey[] = "blobId"; -constexpr char offsetKey[] = "offset"; -constexpr char sizeKey[] = "size"; -constexpr char typeKey[] = "type"; -constexpr char dataKey[] = "data"; + constexpr char moduleName[] = "BlobModule"; + constexpr char blobKey[] = "blob"; + constexpr char blobIdKey[] = "blobId"; + constexpr char offsetKey[] = "offset"; + constexpr char sizeKey[] = "size"; + constexpr char typeKey[] = "type"; + constexpr char dataKey[] = "data"; } // namespace namespace Microsoft::React { #pragma region BlobModule -BlobModule::BlobModule(winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept - : m_sharedState{std::make_shared()}, - m_blobPersistor{std::make_shared()}, - m_contentHandler{std::make_shared(m_blobPersistor)}, - m_requestBodyHandler{std::make_shared(m_blobPersistor)}, - m_responseHandler{std::make_shared(m_blobPersistor)}, - m_inspectableProperties{inspectableProperties} { - auto propBag = ReactPropertyBag{m_inspectableProperties.try_as()}; - - auto contentHandlerPropId = - ReactPropertyId>>{L"BlobModule.ContentHandler"}; - auto contentHandler = weak_ptr{m_contentHandler}; - propBag.Set(contentHandlerPropId, std::move(contentHandler)); - - auto blobPersistorPropId = ReactPropertyId>>{L"Blob.Persistor"}; - auto blobPersistor = weak_ptr{m_blobPersistor}; - propBag.Set(blobPersistorPropId, std::move(blobPersistor)); - - m_sharedState->Module = this; -} + BlobModule::BlobModule(winrt::Windows::Foundation::IInspectable const& inspectableProperties) noexcept + : m_sharedState{ std::make_shared() }, + m_blobPersistor{ std::make_shared() }, + m_contentHandler{ std::make_shared(m_blobPersistor) }, + m_requestBodyHandler{ std::make_shared(m_blobPersistor) }, + m_responseHandler{ std::make_shared(m_blobPersistor) }, + m_inspectableProperties{ inspectableProperties } { + auto propBag = ReactPropertyBag{ m_inspectableProperties.try_as() }; + + auto contentHandlerPropId = + ReactPropertyId>>{ L"BlobModule.ContentHandler" }; + auto contentHandler = weak_ptr{ m_contentHandler }; + propBag.Set(contentHandlerPropId, std::move(contentHandler)); + + auto blobPersistorPropId = ReactPropertyId>>{ L"Blob.Persistor" }; + auto blobPersistor = weak_ptr{ m_blobPersistor }; + propBag.Set(blobPersistorPropId, std::move(blobPersistor)); + + m_sharedState->Module = this; + } -BlobModule::~BlobModule() noexcept /*override*/ { - m_sharedState->Module = nullptr; -} + BlobModule::~BlobModule() noexcept /*override*/ { + m_sharedState->Module = nullptr; + } #pragma region CxxModule -string BlobModule::getName() { - return moduleName; -} + string BlobModule::getName() { + return moduleName; + } -std::map BlobModule::getConstants() { - return {{"BLOB_URI_SCHEME", blobKey}, {"BLOB_URI_HOST", {}}}; -} + std::map BlobModule::getConstants() { + return { {"BLOB_URI_SCHEME", blobKey}, {"BLOB_URI_HOST", {}} }; + } -vector BlobModule::getMethods() { - return { - {"addNetworkingHandler", - [propBag = ReactPropertyBag{m_inspectableProperties.try_as()}, - requestBodyHandler = m_requestBodyHandler, - responseHandler = m_responseHandler](dynamic args) { - auto propId = ReactPropertyId>>{L"HttpModule.Proxy"}; - - if (auto prop = propBag.Get(propId)) { - if (auto httpHandler = prop.Value().lock()) { - httpHandler->AddRequestBodyHandler(requestBodyHandler); - httpHandler->AddResponseHandler(responseHandler); + vector BlobModule::getMethods() { + return { + {"addNetworkingHandler", + [propBag = ReactPropertyBag{m_inspectableProperties.try_as()}, + requestBodyHandler = m_requestBodyHandler, + responseHandler = m_responseHandler](dynamic args) { + auto propId = ReactPropertyId>>{L"HttpModule.Proxy"}; + + if (auto prop = propBag.Get(propId)) { + if (auto httpHandler = prop.Value().lock()) { + httpHandler->AddRequestBodyHandler(requestBodyHandler); + httpHandler->AddResponseHandler(responseHandler); + } } - } - // TODO: else emit error? - }}, - - {"addWebSocketHandler", - [contentHandler = m_contentHandler](dynamic args) { - auto id = jsArgAsInt(args, 0); - - contentHandler->Register(id); - }}, - - {"removeWebSocketHandler", - [contentHandler = m_contentHandler](dynamic args) { - auto id = jsArgAsInt(args, 0); - - contentHandler->Unregister(id); - }}, - - {"sendOverSocket", - [weakState = weak_ptr(m_sharedState), - persistor = m_blobPersistor, - propBag = ReactPropertyBag{m_inspectableProperties.try_as()}](dynamic args) { - auto propId = ReactPropertyId>>{L"WebSocketModule.Proxy"}; - shared_ptr wsProxy; - if (auto prop = propBag.Get(propId)) { - wsProxy = prop.Value().lock(); - } - if (!wsProxy) { - return; - } - - auto blob = jsArgAsObject(args, 0); - auto blobId = blob[blobIdKey].getString(); - auto offset = blob[offsetKey].getInt(); - auto size = blob[sizeKey].getInt(); - auto socketID = jsArgAsInt(args, 1); - - winrt::array_view data; - try { - data = persistor->ResolveMessage(std::move(blobId), offset, size); - } catch (const std::exception &e) { - if (auto sharedState = weakState.lock()) { - Modules::SendEvent(sharedState->Module->getInstance(), "blobFailed", e.what()); + // TODO: else emit error? + }}, + + {"addWebSocketHandler", + [contentHandler = m_contentHandler](dynamic args) { + auto id = jsArgAsInt(args, 0); + + contentHandler->Register(id); + }}, + + {"removeWebSocketHandler", + [contentHandler = m_contentHandler](dynamic args) { + auto id = jsArgAsInt(args, 0); + + contentHandler->Unregister(id); + }}, + + {"sendOverSocket", + [weakState = weak_ptr(m_sharedState), + persistor = m_blobPersistor, + propBag = ReactPropertyBag{m_inspectableProperties.try_as()}](dynamic args) { + auto propId = ReactPropertyId>>{L"WebSocketModule.Proxy"}; + shared_ptr wsProxy; + if (auto prop = propBag.Get(propId)) { + wsProxy = prop.Value().lock(); } - return; - } - - auto buffer = CryptographicBuffer::CreateFromByteArray(data); - auto winrtString = CryptographicBuffer::EncodeToBase64String(std::move(buffer)); - auto base64String = Common::Unicode::Utf16ToUtf8(std::move(winrtString)); - - wsProxy->SendBinary(std::move(base64String), socketID); - }}, - - {"createFromParts", - // As of React Native 0.67, instance is set AFTER CxxModule::getMethods() is invoked. - // Use getInstance() directly once - // https://github.com/facebook/react-native/commit/1d45b20b6c6ba66df0485cdb9be36463d96cf182 becomes available. - [persistor = m_blobPersistor, weakState = weak_ptr(m_sharedState)](dynamic args) { - auto parts = jsArgAsArray(args, 0); // Array - auto blobId = jsArgAsString(args, 1); - vector buffer{}; - - for (const auto &part : parts) { - auto type = part[typeKey].asString(); - if (blobKey == type) { - auto blob = part[dataKey]; - winrt::array_view bufferPart; - try { - bufferPart = persistor->ResolveMessage( - blob[blobIdKey].asString(), blob[offsetKey].asInt(), blob[sizeKey].asInt()); - } catch (const std::exception &e) { - if (auto sharedState = weakState.lock()) { - Modules::SendEvent(sharedState->Module->getInstance(), "blobFailed", e.what()); - } - return; - } - - buffer.reserve(buffer.size() + bufferPart.size()); - buffer.insert(buffer.end(), bufferPart.begin(), bufferPart.end()); - } else if ("string" == type) { - auto data = part[dataKey].asString(); - - buffer.reserve(buffer.size() + data.size()); - buffer.insert(buffer.end(), data.begin(), data.end()); - } else { - if (auto state = weakState.lock()) { - auto message = "Invalid type for blob: " + type; - Modules::SendEvent(state->Module->getInstance(), "blobFailed", std::move(message)); - } + if (!wsProxy) { return; } - } - persistor->StoreMessage(std::move(buffer), std::move(blobId)); - }}, + auto blob = jsArgAsObject(args, 0); + auto blobId = blob[blobIdKey].getString(); + auto offset = blob[offsetKey].getInt(); + auto size = blob[sizeKey].getInt(); + auto socketID = jsArgAsInt(args, 1); - {"release", - [persistor = m_blobPersistor](dynamic args) // blobId: string - { - auto blobId = jsArgAsString(args, 0); + winrt::array_view data; + try { + data = persistor->ResolveMessage(std::move(blobId), offset, size); + } + catch (const std::exception& e) { + if (auto sharedState = weakState.lock()) { + Modules::SendEvent(sharedState->Module->getInstance(), "blobFailed", e.what()); + } + return; +} - persistor->RemoveMessage(std::move(blobId)); - }}}; +auto buffer = CryptographicBuffer::CreateFromByteArray(data); +auto winrtString = CryptographicBuffer::EncodeToBase64String(std::move(buffer)); +auto base64String = Common::Unicode::Utf16ToUtf8(std::move(winrtString)); + +wsProxy->SendBinary(std::move(base64String), socketID); +}}, + +{"createFromParts", +// As of React Native 0.67, instance is set AFTER CxxModule::getMethods() is invoked. +// Use getInstance() directly once +// https://github.com/facebook/react-native/commit/1d45b20b6c6ba66df0485cdb9be36463d96cf182 becomes available. +[persistor = m_blobPersistor, weakState = weak_ptr(m_sharedState)](dynamic args) { + auto parts = jsArgAsArray(args, 0); // Array + auto blobId = jsArgAsString(args, 1); + vector buffer{}; + + for (const auto& part : parts) { + auto type = part[typeKey].asString(); + if (blobKey == type) { + auto blob = part[dataKey]; + winrt::array_view bufferPart; + try { + bufferPart = persistor->ResolveMessage( + blob[blobIdKey].asString(), blob[offsetKey].asInt(), blob[sizeKey].asInt()); + } +catch (const std::exception& e) { +if (auto sharedState = weakState.lock()) { + Modules::SendEvent(sharedState->Module->getInstance(), "blobFailed", e.what()); } +return; +} + +buffer.reserve(buffer.size() + bufferPart.size()); +buffer.insert(buffer.end(), bufferPart.begin(), bufferPart.end()); +} +else if ("string" == type) { +auto data = part[dataKey].asString(); + +buffer.reserve(buffer.size() + data.size()); +buffer.insert(buffer.end(), data.begin(), data.end()); +} +else { +if (auto state = weakState.lock()) { + auto message = "Invalid type for blob: " + type; + Modules::SendEvent(state->Module->getInstance(), "blobFailed", std::move(message)); +} +return; +} +} + +persistor->StoreMessage(std::move(buffer), std::move(blobId)); +}}, + +{"release", + [persistor = m_blobPersistor](dynamic args) // blobId: string + { + auto blobId = jsArgAsString(args, 0); + + persistor->RemoveMessage(std::move(blobId)); + }} }; + } #pragma endregion CxxModule @@ -216,47 +220,47 @@ vector BlobModule::getMethods() { #pragma region IBlobPersistor -winrt::array_view MemoryBlobPersistor::ResolveMessage(string &&blobId, int64_t offset, int64_t size) { - if (size < 1) - return {}; + winrt::array_view MemoryBlobPersistor::ResolveMessage(string&& blobId, int64_t offset, int64_t size) { + if (size < 1) + return {}; - scoped_lock lock{m_mutex}; + scoped_lock lock{ m_mutex }; - auto dataItr = m_blobs.find(std::move(blobId)); - // Not found. - if (dataItr == m_blobs.cend()) - throw std::invalid_argument("Blob object not found"); + auto dataItr = m_blobs.find(std::move(blobId)); + // Not found. + if (dataItr == m_blobs.cend()) + throw std::invalid_argument("Blob object not found"); - auto &bytes = (*dataItr).second; - auto endBound = static_cast(offset + size); - // Out of bounds. - if (endBound > bytes.size() || offset >= static_cast(bytes.size()) || offset < 0) - throw std::out_of_range("Offset or size out of range"); + auto& bytes = (*dataItr).second; + auto endBound = static_cast(offset + size); + // Out of bounds. + 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 { - scoped_lock lock{m_mutex}; + void MemoryBlobPersistor::RemoveMessage(string&& blobId) noexcept { + scoped_lock lock{ m_mutex }; - m_blobs.erase(std::move(blobId)); -} + m_blobs.erase(std::move(blobId)); + } -void MemoryBlobPersistor::StoreMessage(vector &&message, string &&blobId) noexcept { - scoped_lock lock{m_mutex}; + void MemoryBlobPersistor::StoreMessage(vector&& message, string&& blobId) noexcept { + scoped_lock lock{ m_mutex }; - m_blobs.insert_or_assign(std::move(blobId), std::move(message)); -} + m_blobs.insert_or_assign(std::move(blobId), std::move(message)); + } -string MemoryBlobPersistor::StoreMessage(vector &&message) noexcept { - // substr(1, 36) strips curly braces from a GUID. - auto blobId = winrt::to_string(winrt::to_hstring(GuidHelper::CreateNewGuid())).substr(1, 36); + string MemoryBlobPersistor::StoreMessage(vector&& message) noexcept { + // substr(1, 36) strips curly braces from a GUID. + auto blobId = winrt::to_string(winrt::to_hstring(GuidHelper::CreateNewGuid())).substr(1, 36); - scoped_lock lock{m_mutex}; - m_blobs.insert_or_assign(blobId, std::move(message)); + scoped_lock lock{ m_mutex }; + m_blobs.insert_or_assign(blobId, std::move(message)); - return blobId; -} + return blobId; + } #pragma endregion IBlobPersistor @@ -264,74 +268,74 @@ string MemoryBlobPersistor::StoreMessage(vector &&message) noexcept { #pragma region BlobWebSocketModuleContentHandler -BlobWebSocketModuleContentHandler::BlobWebSocketModuleContentHandler(shared_ptr blobPersistor) noexcept - : m_blobPersistor{blobPersistor} {} + BlobWebSocketModuleContentHandler::BlobWebSocketModuleContentHandler(shared_ptr blobPersistor) noexcept + : m_blobPersistor{ blobPersistor } {} #pragma region IWebSocketModuleContentHandler -void BlobWebSocketModuleContentHandler::ProcessMessage(string &&message, dynamic ¶ms) /*override*/ { - params[dataKey] = std::move(message); -} + void BlobWebSocketModuleContentHandler::ProcessMessage(string&& message, dynamic& params) /*override*/ { + params[dataKey] = std::move(message); + } -void BlobWebSocketModuleContentHandler::ProcessMessage(vector &&message, dynamic ¶ms) /*override*/ { - auto blob = dynamic::object(); - blob(offsetKey, 0); - blob(sizeKey, message.size()); - blob(blobIdKey, m_blobPersistor->StoreMessage(std::move(message))); + void BlobWebSocketModuleContentHandler::ProcessMessage(vector&& message, dynamic& params) /*override*/ { + auto blob = dynamic::object(); + blob(offsetKey, 0); + blob(sizeKey, message.size()); + blob(blobIdKey, m_blobPersistor->StoreMessage(std::move(message))); - params[dataKey] = std::move(blob); - params[typeKey] = blobKey; -} + params[dataKey] = std::move(blob); + params[typeKey] = blobKey; + } #pragma endregion IWebSocketModuleContentHandler -void BlobWebSocketModuleContentHandler::Register(int64_t socketID) noexcept { - scoped_lock lock{m_mutex}; - m_socketIds.insert(socketID); -} + void BlobWebSocketModuleContentHandler::Register(int64_t socketID) noexcept { + scoped_lock lock{ m_mutex }; + m_socketIds.insert(socketID); + } -void BlobWebSocketModuleContentHandler::Unregister(int64_t socketID) noexcept { - scoped_lock lock{m_mutex}; + void BlobWebSocketModuleContentHandler::Unregister(int64_t socketID) noexcept { + scoped_lock lock{ m_mutex }; - auto itr = m_socketIds.find(socketID); - if (itr != m_socketIds.end()) - m_socketIds.erase(itr); -} + auto itr = m_socketIds.find(socketID); + if (itr != m_socketIds.end()) + m_socketIds.erase(itr); + } #pragma endregion BlobWebSocketModuleContentHandler #pragma region BlobModuleRequestBodyHandler -BlobModuleRequestBodyHandler::BlobModuleRequestBodyHandler(shared_ptr blobPersistor) noexcept - : m_blobPersistor{blobPersistor} {} + BlobModuleRequestBodyHandler::BlobModuleRequestBodyHandler(shared_ptr blobPersistor) noexcept + : m_blobPersistor{ blobPersistor } {} #pragma region IRequestBodyHandler -bool BlobModuleRequestBodyHandler::Supports(dynamic &data) /*override*/ { - auto itr = data.find(blobKey); + bool BlobModuleRequestBodyHandler::Supports(dynamic& data) /*override*/ { + auto itr = data.find(blobKey); - return itr != data.items().end() && !(*itr).second.empty(); -} - -dynamic BlobModuleRequestBodyHandler::ToRequestBody(dynamic &data, string &contentType) /*override*/ { - auto type = contentType; - if (!data[typeKey].isNull() && !data[typeKey].asString().empty()) { - type = data[typeKey].asString(); - } - if (type.empty()) { - type = "application/octet-stream"; + return itr != data.items().end() && !(*itr).second.empty(); } - auto blob = data[blobKey]; - auto blobId = blob[blobIdKey].asString(); - auto bytes = m_blobPersistor->ResolveMessage(std::move(blobId), blob[offsetKey].asInt(), blob[sizeKey].asInt()); - - auto result = dynamic::object(); - result(typeKey, type); - result(sizeKey, bytes.size()); - result("bytes", dynamic(bytes.cbegin(), bytes.cend())); - - return result; -} + dynamic BlobModuleRequestBodyHandler::ToRequestBody(dynamic& data, string& contentType) /*override*/ { + auto type = contentType; + if (!data[typeKey].isNull() && !data[typeKey].asString().empty()) { + type = data[typeKey].asString(); + } + if (type.empty()) { + type = "application/octet-stream"; + } + + auto blob = data[blobKey]; + auto blobId = blob[blobIdKey].asString(); + auto bytes = m_blobPersistor->ResolveMessage(std::move(blobId), blob[offsetKey].asInt(), blob[sizeKey].asInt()); + + auto result = dynamic::object(); + result(typeKey, type); + result(sizeKey, bytes.size()); + result("bytes", dynamic(bytes.cbegin(), bytes.cend())); + + return result; + } #pragma endregion IRequestBodyHandler @@ -339,38 +343,38 @@ dynamic BlobModuleRequestBodyHandler::ToRequestBody(dynamic &data, string &conte #pragma region BlobModuleResponseHandler -BlobModuleResponseHandler::BlobModuleResponseHandler(shared_ptr blobPersistor) noexcept - : m_blobPersistor{blobPersistor} {} + BlobModuleResponseHandler::BlobModuleResponseHandler(shared_ptr blobPersistor) noexcept + : m_blobPersistor{ blobPersistor } {} #pragma region IResponseHandler -bool BlobModuleResponseHandler::Supports(string &responseType) /*override*/ { - return blobKey == responseType; -} + bool BlobModuleResponseHandler::Supports(string& responseType) /*override*/ { + return blobKey == responseType; + } -dynamic BlobModuleResponseHandler::ToResponseData(vector &&content) /*override*/ { - auto blob = dynamic::object(); - blob(offsetKey, 0); - blob(sizeKey, content.size()); - blob(blobIdKey, m_blobPersistor->StoreMessage(std::move(content))); + dynamic BlobModuleResponseHandler::ToResponseData(vector&& content) /*override*/ { + auto blob = dynamic::object(); + blob(offsetKey, 0); + blob(sizeKey, content.size()); + blob(blobIdKey, m_blobPersistor->StoreMessage(std::move(content))); - return blob; -} + return blob; + } #pragma endregion IResponseHandler #pragma endregion BlobModuleResponseHandler -/*extern*/ const char *GetBlobModuleName() noexcept { - return moduleName; -} + /*extern*/ const char* GetBlobModuleName() noexcept { + return moduleName; + } -/*extern*/ std::unique_ptr CreateBlobModule( - IInspectable const &inspectableProperties) noexcept { - if (auto properties = inspectableProperties.try_as()) - return std::make_unique(properties); + /*extern*/ std::unique_ptr CreateBlobModule( + IInspectable const& inspectableProperties) noexcept { + if (auto properties = inspectableProperties.try_as()) + return std::make_unique(properties); - return nullptr; -} + return nullptr; + } } // namespace Microsoft::React diff --git a/vnext/Shared/Modules/BlobModule.h b/vnext/Shared/Modules/BlobModule.h index 76c5d46e5e7..d9a819b404e 100644 --- a/vnext/Shared/Modules/BlobModule.h +++ b/vnext/Shared/Modules/BlobModule.h @@ -23,131 +23,131 @@ namespace Microsoft::React { -class MemoryBlobPersistor final : public IBlobPersistor { - std::unordered_map> m_blobs; - std::mutex m_mutex; + class MemoryBlobPersistor final: public IBlobPersistor { + std::unordered_map> m_blobs; + std::mutex m_mutex; - public: + 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; + void RemoveMessage(std::string&& blobId) noexcept override; - void StoreMessage(std::vector &&message, std::string &&blobId) noexcept override; + void StoreMessage(std::vector&& message, std::string&& blobId) noexcept override; - std::string StoreMessage(std::vector &&message) noexcept override; + std::string StoreMessage(std::vector&& message) noexcept override; #pragma endregion IBlobPersistor -}; + }; -class BlobWebSocketModuleContentHandler final : public IWebSocketModuleContentHandler { - std::unordered_set m_socketIds; - std::mutex m_mutex; - std::shared_ptr m_blobPersistor; + class BlobWebSocketModuleContentHandler final: public IWebSocketModuleContentHandler { + std::unordered_set m_socketIds; + std::mutex m_mutex; + std::shared_ptr m_blobPersistor; - public: - BlobWebSocketModuleContentHandler(std::shared_ptr blobPersistor) noexcept; + public: + BlobWebSocketModuleContentHandler(std::shared_ptr blobPersistor) noexcept; #pragma region IWebSocketModuleContentHandler - void ProcessMessage(std::string &&message, folly::dynamic ¶ms) override; + void ProcessMessage(std::string&& message, folly::dynamic& params) override; - void ProcessMessage(std::vector &&message, folly::dynamic ¶ms) override; + void ProcessMessage(std::vector&& message, folly::dynamic& params) override; #pragma endregion IWebSocketModuleContentHandler - void Register(int64_t socketID) noexcept; + void Register(int64_t socketID) noexcept; - void Unregister(int64_t socketID) noexcept; -}; + void Unregister(int64_t socketID) noexcept; + }; -class BlobModuleRequestBodyHandler final : public IRequestBodyHandler { - std::shared_ptr m_blobPersistor; + class BlobModuleRequestBodyHandler final: public IRequestBodyHandler { + std::shared_ptr m_blobPersistor; - public: - BlobModuleRequestBodyHandler(std::shared_ptr blobPersistor) noexcept; + public: + BlobModuleRequestBodyHandler(std::shared_ptr blobPersistor) noexcept; #pragma region IRequestBodyHandler - bool Supports(folly::dynamic &data) override; + bool Supports(folly::dynamic& data) override; - folly::dynamic ToRequestBody(folly::dynamic &data, std::string &contentType) override; + folly::dynamic ToRequestBody(folly::dynamic& data, std::string& contentType) override; #pragma endregion IRequestBodyHandler -}; + }; -class BlobModuleResponseHandler final : public IResponseHandler { - std::shared_ptr m_blobPersistor; + class BlobModuleResponseHandler final: public IResponseHandler { + std::shared_ptr m_blobPersistor; - public: - BlobModuleResponseHandler(std::shared_ptr blobPersistor) noexcept; + public: + BlobModuleResponseHandler(std::shared_ptr blobPersistor) noexcept; #pragma region IResponseHandler - bool Supports(std::string &responseType) override; + bool Supports(std::string& responseType) override; - folly::dynamic ToResponseData(std::vector &&content) override; + folly::dynamic ToResponseData(std::vector&& content) override; #pragma endregion IResponseHandler -}; - -class BlobModule : public facebook::xplat::module::CxxModule { - std::shared_ptr m_blobPersistor; - std::shared_ptr m_contentHandler; - std::shared_ptr m_requestBodyHandler; - std::shared_ptr m_responseHandler; - - // Property bag high level reference. - winrt::Windows::Foundation::IInspectable m_inspectableProperties; - - public: - enum class MethodId { - AddNetworkingHandler = 0, - AddWebSocketHandler = 1, - RemoveWebSocketHandler = 2, - SendOverSocket = 3, - CreateFromParts = 4, - Release = 5, - SIZE = 6 }; - BlobModule(winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept; + class BlobModule: public facebook::xplat::module::CxxModule{ + std::shared_ptr m_blobPersistor; + std::shared_ptr m_contentHandler; + std::shared_ptr m_requestBodyHandler; + std::shared_ptr m_responseHandler; - ~BlobModule() noexcept override; + // Property bag high level reference. + winrt::Windows::Foundation::IInspectable m_inspectableProperties; - struct SharedState { - /// - /// Keeps a raw reference to the module object to lazily retrieve the React Instance as needed. - /// - CxxModule *Module{nullptr}; - }; + public: + enum class MethodId { + AddNetworkingHandler = 0, + AddWebSocketHandler = 1, + RemoveWebSocketHandler = 2, + SendOverSocket = 3, + CreateFromParts = 4, + Release = 5, + SIZE = 6 + }; + + BlobModule(winrt::Windows::Foundation::IInspectable const& inspectableProperties) noexcept; + + ~BlobModule() noexcept override; -#pragma region CxxModule + struct SharedState { + /// + /// Keeps a raw reference to the module object to lazily retrieve the React Instance as needed. + /// + CxxModule* Module{nullptr}; + }; - /// - /// - /// - std::string getName() override; + #pragma region CxxModule - /// - /// - /// - std::map getConstants() override; + /// + /// + /// + std::string getName() override; + + /// + /// + /// + std::map getConstants() override; - /// - /// - /// - /// See See react-native/Libraries/WebSocket/WebSocket.js - std::vector getMethods() override; + /// + /// + /// + /// See See react-native/Libraries/WebSocket/WebSocket.js + std::vector getMethods() override; -#pragma endregion CxxModule + #pragma endregion CxxModule - private: - /// - /// Keeps members that can be accessed threads other than this module's owner accessible. - /// - std::shared_ptr m_sharedState; -}; + private: + /// + /// Keeps members that can be accessed threads other than this module's owner accessible. + /// + std::shared_ptr m_sharedState; + }; } // namespace Microsoft::React diff --git a/vnext/Shared/Modules/FileReaderModule.cpp b/vnext/Shared/Modules/FileReaderModule.cpp index 9f7919e31fc..14c81f00758 100644 --- a/vnext/Shared/Modules/FileReaderModule.cpp +++ b/vnext/Shared/Modules/FileReaderModule.cpp @@ -28,129 +28,132 @@ using winrt::Microsoft::ReactNative::ReactPropertyId; using winrt::Windows::Foundation::IInspectable; namespace { -constexpr char moduleName[] = "FileReaderModule"; + constexpr char moduleName[] = "FileReaderModule"; } // namespace namespace Microsoft::React { #pragma region FileReaderModule -FileReaderModule::FileReaderModule(weak_ptr weakBlobPersistor) noexcept - : m_weakBlobPersistor{weakBlobPersistor} {} + FileReaderModule::FileReaderModule(weak_ptr weakBlobPersistor) noexcept + : m_weakBlobPersistor{ weakBlobPersistor } {} -FileReaderModule::~FileReaderModule() noexcept /*override*/ -{} + FileReaderModule::~FileReaderModule() noexcept /*override*/ + {} #pragma region CxxModule -string FileReaderModule::getName() { - return moduleName; -} + string FileReaderModule::getName() { + return moduleName; + } + + std::map FileReaderModule::getConstants() { + return {}; + } -std::map FileReaderModule::getConstants() { - return {}; + std::vector FileReaderModule::getMethods() { + return { + {/// + /// + /// Array of arguments passed from the JavaScript layer. + /// [0] - dynamic blob object { blobId, offset, size[, type] } + /// + /// + "readAsDataURL", + [blobPersistor = m_weakBlobPersistor.lock()](dynamic args, Callback resolve, Callback reject) { + if (!blobPersistor) { + return reject({"Could not find Blob persistor"}); + } + + auto blob = jsArgAsObject(args, 0); + + auto blobId = blob["blobId"].asString(); + auto offset = blob["offset"].asInt(); + auto size = blob["size"].asInt(); + + winrt::array_view bytes; + try { + bytes = blobPersistor->ResolveMessage(std::move(blobId), offset, size); + } + catch (const std::exception& e) { + return reject({e.what()}); } -std::vector FileReaderModule::getMethods() { - return { - {/// - /// - /// Array of arguments passed from the JavaScript layer. - /// [0] - dynamic blob object { blobId, offset, size[, type] } - /// - /// - "readAsDataURL", - [blobPersistor = m_weakBlobPersistor.lock()](dynamic args, Callback resolve, Callback reject) { - if (!blobPersistor) { - return reject({"Could not find Blob persistor"}); - } - - auto blob = jsArgAsObject(args, 0); - - auto blobId = blob["blobId"].asString(); - auto offset = blob["offset"].asInt(); - auto size = blob["size"].asInt(); - - winrt::array_view bytes; - try { - bytes = blobPersistor->ResolveMessage(std::move(blobId), offset, size); - } catch (const std::exception &e) { - return reject({e.what()}); - } - - auto result = string{"data:"}; - auto typeItr = blob.find("type"); - if (typeItr == blob.items().end()) { - result += "application/octet-stream"; - } else { - result += (*typeItr).second.asString(); - } - result += ";base64,"; - - // https://www.boost.org/doc/libs/1_76_0/libs/serialization/doc/dataflow.html - using namespace boost::archive::iterators; - typedef base64_from_binary> encode_base64; - std::ostringstream oss; - std::copy(encode_base64(bytes.cbegin()), encode_base64(bytes.cend()), ostream_iterator(oss)); - result += oss.str(); - - resolve({std::move(result)}); - }}, - {/// - /// - /// Array of arguments passed from the JavaScript layer. - /// [0] - dynamic blob object { blobId, offset, size } - /// [1] - string encoding - /// - /// - "readAsText", - [blobPersistor = m_weakBlobPersistor.lock()](dynamic args, Callback resolve, Callback reject) { - if (!blobPersistor) { - return reject({"Could not find Blob persistor"}); - } - - auto blob = jsArgAsObject(args, 0); - auto encoding = jsArgAsString(args, 1); // Default: "UTF-8" - - auto blobId = blob["blobId"].asString(); - auto offset = blob["offset"].asInt(); - auto size = blob["size"].asInt(); - - winrt::array_view bytes; - try { - bytes = blobPersistor->ResolveMessage(std::move(blobId), offset, size); - } catch (const std::exception &e) { - return reject({e.what()}); - } - - // #9982 - Handle non-UTF8 encodings - // See https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/charset/Charset.html - auto result = string{bytes.cbegin(), bytes.cend()}; - - resolve({std::move(result)}); - }}}; +auto result = string{"data:"}; +auto typeItr = blob.find("type"); +if (typeItr == blob.items().end()) { + result += "application/octet-stream"; } +else { +result += (*typeItr).second.asString(); +} +result += ";base64,"; + +// https://www.boost.org/doc/libs/1_76_0/libs/serialization/doc/dataflow.html +using namespace boost::archive::iterators; +typedef base64_from_binary> encode_base64; +std::ostringstream oss; +std::copy(encode_base64(bytes.cbegin()), encode_base64(bytes.cend()), ostream_iterator(oss)); +result += oss.str(); + +resolve({std::move(result)}); +}}, +{/// + /// + /// Array of arguments passed from the JavaScript layer. + /// [0] - dynamic blob object { blobId, offset, size } + /// [1] - string encoding + /// + /// + "readAsText", + [blobPersistor = m_weakBlobPersistor.lock()](dynamic args, Callback resolve, Callback reject) { + if (!blobPersistor) { + return reject({"Could not find Blob persistor"}); + } + + auto blob = jsArgAsObject(args, 0); + auto encoding = jsArgAsString(args, 1); // Default: "UTF-8" + + auto blobId = blob["blobId"].asString(); + auto offset = blob["offset"].asInt(); + auto size = blob["size"].asInt(); + + winrt::array_view bytes; + try { + bytes = blobPersistor->ResolveMessage(std::move(blobId), offset, size); + } +catch (const std::exception& e) { +return reject({e.what()}); +} + +// #9982 - Handle non-UTF8 encodings +// See https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/charset/Charset.html +auto result = string{bytes.cbegin(), bytes.cend()}; + +resolve({std::move(result)}); +}} }; + } #pragma endregion CxxModule #pragma endregion FileReaderModule -/*extern*/ const char *GetFileReaderModuleName() noexcept { - return moduleName; -} + /*extern*/ const char* GetFileReaderModuleName() noexcept { + return moduleName; + } -/*extern*/ std::unique_ptr CreateFileReaderModule( - IInspectable const &inspectableProperties) noexcept { - auto propId = ReactPropertyId>>{L"Blob.Persistor"}; - auto propBag = ReactPropertyBag{inspectableProperties.try_as()}; + /*extern*/ std::unique_ptr CreateFileReaderModule( + IInspectable const& inspectableProperties) noexcept { + auto propId = ReactPropertyId>>{ L"Blob.Persistor" }; + auto propBag = ReactPropertyBag{ inspectableProperties.try_as() }; - if (auto prop = propBag.Get(propId)) { - auto weakBlobPersistor = prop.Value(); + if (auto prop = propBag.Get(propId)) { + auto weakBlobPersistor = prop.Value(); - return std::make_unique(weakBlobPersistor); - } + return std::make_unique(weakBlobPersistor); + } - return nullptr; -} + return nullptr; + } } // namespace Microsoft::React diff --git a/vnext/Shared/Modules/HttpModule.cpp b/vnext/Shared/Modules/HttpModule.cpp index 20f4ea0fd04..bbc3b559f36 100644 --- a/vnext/Shared/Modules/HttpModule.cpp +++ b/vnext/Shared/Modules/HttpModule.cpp @@ -25,30 +25,30 @@ using winrt::Windows::Foundation::IInspectable; namespace { -using Microsoft::React::Modules::SendEvent; -using Microsoft::React::Networking::IHttpResource; + using Microsoft::React::Modules::SendEvent; + using Microsoft::React::Networking::IHttpResource; -constexpr char moduleName[] = "Networking"; + constexpr char moduleName[] = "Networking"; -// React event names -constexpr char completedResponse[] = "didCompleteNetworkResponse"; -constexpr char receivedResponse[] = "didReceiveNetworkResponse"; -constexpr char receivedData[] = "didReceiveNetworkData"; -constexpr char receivedDataProgress[] = "didReceiveNetworkDataProgress"; + // React event names + constexpr char completedResponse[] = "didCompleteNetworkResponse"; + constexpr char receivedResponse[] = "didReceiveNetworkResponse"; + constexpr char receivedData[] = "didReceiveNetworkData"; + constexpr char receivedDataProgress[] = "didReceiveNetworkDataProgress"; -static void SetUpHttpResource( + static void SetUpHttpResource( shared_ptr resource, weak_ptr weakReactInstance, - IInspectable &inspectableProperties) { - resource->SetOnRequestSuccess([weakReactInstance](int64_t requestId) { - auto args = dynamic::array(requestId); + IInspectable& inspectableProperties) { + resource->SetOnRequestSuccess([weakReactInstance](int64_t requestId) { + auto args = dynamic::array(requestId); SendEvent(weakReactInstance, completedResponse, std::move(args)); - }); + }); - resource->SetOnResponse([weakReactInstance](int64_t requestId, IHttpResource::Response &&response) { - dynamic headers = dynamic::object(); - for (auto &header : response.Headers) { + resource->SetOnResponse([weakReactInstance](int64_t requestId, IHttpResource::Response&& response) { + dynamic headers = dynamic::object(); + for (auto& header : response.Headers) { headers[header.first] = header.second; } @@ -56,148 +56,148 @@ static void SetUpHttpResource( dynamic args = dynamic::array(requestId, response.StatusCode, headers, response.Url); SendEvent(weakReactInstance, receivedResponse, std::move(args)); - }); + }); - resource->SetOnData([weakReactInstance](int64_t requestId, string &&responseData) { - SendEvent(weakReactInstance, receivedData, dynamic::array(requestId, std::move(responseData))); + resource->SetOnData([weakReactInstance](int64_t requestId, string&& responseData) { + SendEvent(weakReactInstance, receivedData, dynamic::array(requestId, std::move(responseData))); // TODO: Move into separate method IF not executed right after onData() SendEvent(weakReactInstance, completedResponse, dynamic::array(requestId)); - }); + }); - // Explicitly declaring function type to avoid type inference ambiguity. - std::function onDataDynamic = [weakReactInstance]( - int64_t requestId, dynamic &&responseData) { - SendEvent(weakReactInstance, receivedData, dynamic::array(requestId, std::move(responseData))); - }; - resource->SetOnData(std::move(onDataDynamic)); + // Explicitly declaring function type to avoid type inference ambiguity. + std::function onDataDynamic = [weakReactInstance]( + int64_t requestId, dynamic&& responseData) { + SendEvent(weakReactInstance, receivedData, dynamic::array(requestId, std::move(responseData))); + }; + resource->SetOnData(std::move(onDataDynamic)); - resource->SetOnError([weakReactInstance](int64_t requestId, string &&message, bool isTimeout) { - dynamic args = dynamic::array(requestId, std::move(message)); + resource->SetOnError([weakReactInstance](int64_t requestId, string&& message) { + dynamic args = dynamic::array(requestId, std::move(message)); if (isTimeout) { args.push_back(true); } SendEvent(weakReactInstance, completedResponse, std::move(args)); - }); -} + }); + } } // namespace namespace Microsoft::React { -HttpModule::HttpModule(IInspectable const &inspectableProperties) noexcept - : m_holder{std::make_shared()}, - m_inspectableProperties{inspectableProperties}, - m_resource{IHttpResource::Make(inspectableProperties)} { - m_holder->Module = this; -} + HttpModule::HttpModule(IInspectable const& inspectableProperties) noexcept + : m_holder{ std::make_shared() }, + m_inspectableProperties{ inspectableProperties }, + m_resource{ IHttpResource::Make(inspectableProperties) } { + m_holder->Module = this; + } -HttpModule::~HttpModule() noexcept /*override*/ { - m_holder->Module = nullptr; -} + HttpModule::~HttpModule() noexcept /*override*/ { + m_holder->Module = nullptr; + } #pragma region CxxModule -string HttpModule::getName() /*override*/ { - return moduleName; -} + string HttpModule::getName() /*override*/ { + return moduleName; + } -std::map HttpModule::getConstants() { - return {}; -} + std::map HttpModule::getConstants() { + return {}; + } -// clang-format off -std::vector HttpModule::getMethods() { + // clang-format off + 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 + }; + } + // clang-format on #pragma endregion CxxModule -/*extern*/ const char *GetHttpModuleName() noexcept { - return moduleName; -} + /*extern*/ const char* GetHttpModuleName() noexcept { + return moduleName; + } } // namespace Microsoft::React diff --git a/vnext/Shared/Modules/IBlobPersistor.h b/vnext/Shared/Modules/IBlobPersistor.h index e4aaf5017e0..638f09e8bea 100644 --- a/vnext/Shared/Modules/IBlobPersistor.h +++ b/vnext/Shared/Modules/IBlobPersistor.h @@ -12,19 +12,19 @@ namespace Microsoft::React { -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; + 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 void RemoveMessage(std::string &&blobId) noexcept = 0; + virtual void RemoveMessage(std::string&& blobId) noexcept = 0; - virtual void StoreMessage(std::vector &&message, std::string &&blobId) noexcept = 0; + virtual void StoreMessage(std::vector&& message, std::string&& blobId) noexcept = 0; - virtual std::string StoreMessage(std::vector &&message) noexcept = 0; -}; + virtual std::string StoreMessage(std::vector&& message) noexcept = 0; + }; } // namespace Microsoft::React diff --git a/vnext/Shared/Networking/IHttpResource.h b/vnext/Shared/Networking/IHttpResource.h index 17d791d1868..b5d949922b5 100644 --- a/vnext/Shared/Networking/IHttpResource.h +++ b/vnext/Shared/Networking/IHttpResource.h @@ -17,86 +17,85 @@ namespace Microsoft::React::Networking { -struct IHttpResource { - typedef std::unordered_map Headers; + struct IHttpResource { + typedef std::unordered_map Headers; - // TODO: Implement Form data - struct BodyData { - enum class Type : size_t { Empty, String, Base64, Uri, Form } Type = Type::Empty; - std::string Data; - }; + // TODO: Implement Form data + struct BodyData { + enum class Type: size_t { Empty, String, Base64, Uri, Form } Type = Type::Empty; + std::string Data; + }; - struct Response { - int64_t StatusCode; - std::string Url; - Headers Headers; - }; + struct Response { + int64_t StatusCode; + std::string Url; + Headers Headers; + }; - static std::shared_ptr Make() noexcept; + static std::shared_ptr Make() noexcept; - static std::shared_ptr Make( - winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept; + static std::shared_ptr Make( + winrt::Windows::Foundation::IInspectable const& inspectableProperties) noexcept; - virtual ~IHttpResource() noexcept {} + virtual ~IHttpResource() noexcept {} - /// - /// Initiates an HTTP request. - /// - /// - /// HTTP verb to send in de request. - /// GET | POST | PUT | DELETE | OPTIONS - /// - /// - /// Server/service remote endpoint to send the request to. - /// - /// - /// Request unique identifier. - /// - /// - /// HTTP request header map. - /// - /// - /// Dynamic map containing request payload. - /// The payload may be an empty request body or one of the following: - /// "string" - UTF-8 string payload - /// "base64" - Base64-encoded data string - /// "uri" - URI data reference - /// "form" - Form-encoded data - /// - /// - /// text | binary | blob - /// - /// - /// Response body to be retrieved in several iterations. - /// - /// - /// Request timeout in miliseconds. - /// Note: A value of 0 means no timeout. The resource will await the response indefinitely. - /// - /// - /// Allow including credentials in request. - /// - virtual void SendRequest( - std::string &&method, - std::string &&url, + /// + /// Initiates an HTTP request. + /// + /// + /// HTTP verb to send in de request. + /// GET | POST | PUT | DELETE | OPTIONS + /// + /// + /// Server/service remote endpoint to send the request to. + /// + /// + /// Request unique identifier. + /// + /// + /// HTTP request header map. + /// + /// + /// Dynamic map containing request payload. + /// The payload may be an empty request body or one of the following: + /// "string" - UTF-8 string payload + /// "base64" - Base64-encoded data string + /// "uri" - URI data reference + /// "form" - Form-encoded data + /// + /// + /// text | binary | blob + /// + /// + /// Response body to be retrieved in several iterations. + /// + /// + /// Request timeout in miliseconds. + /// + /// + /// Allow including credentials in request. + /// + virtual void SendRequest( + std::string&& method, + std::string&& url, int64_t requestId, - Headers &&headers, - folly::dynamic &&data, - std::string &&responseType, + Headers&& headers, + folly::dynamic&& data, + std::string&& responseType, bool useIncrementalUpdates, int64_t timeout, bool withCredentials, - std::function &&callback) noexcept = 0; - virtual void AbortRequest(int64_t requestId) noexcept = 0; + std::function&& callback) noexcept = 0; + virtual void AbortRequest(int64_t requestId) noexcept = 0; - virtual void ClearCookies() noexcept = 0; + virtual void ClearCookies() noexcept = 0; - virtual void SetOnRequestSuccess(std::function &&handler) noexcept = 0; - virtual void SetOnResponse(std::function &&handler) noexcept = 0; - virtual void SetOnData(std::function &&handler) noexcept = 0; - virtual void SetOnData(std::function &&handler) noexcept = 0; - virtual void SetOnError( - std::function &&handler) noexcept = 0; -}; + virtual void SetOnRequestSuccess(std::function&& handler) noexcept = 0; + virtual void SetOnResponse(std::function&& handler) noexcept = 0; + virtual void SetOnData(std::function&& handler) noexcept = 0; + virtual void SetOnData(std::function&& handler) noexcept = 0; + virtual void SetOnError( + std::function&& handler) noexcept = 0; + }; } // namespace Microsoft::React::Networking diff --git a/vnext/Shared/Networking/WinRTHttpResource.cpp b/vnext/Shared/Networking/WinRTHttpResource.cpp index be94fe6ed90..8776ddb56ed 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.cpp +++ b/vnext/Shared/Networking/WinRTHttpResource.cpp @@ -59,150 +59,6 @@ namespace Microsoft::React::Networking { 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( @@ -217,11 +73,7 @@ namespace Microsoft::React::Networking { bool withCredentials, std::function&& callback) noexcept /*override*/ { // Enforce supported args - assert(responseType == "text" || responseType == "base64" || responseType == "blob"); - - if (callback) { - callback(requestId); - } + assert(responseType == "text" || responseType == "base64" | responseType == "blob"); try { HttpMethod httpMethod{ to_hstring(std::move(method)) }; @@ -257,14 +109,15 @@ namespace Microsoft::React::Networking { void WinRTHttpResource::AbortRequest(int64_t requestId) noexcept /*override*/ { ResponseOperation request{ nullptr }; - { - scoped_lock lock{ m_mutex }; - auto iter = m_responses.find(requestId); - if (iter == std::end(m_responses)) { - return; - } - request = iter->second; - } + 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; try { request.Cancel(); @@ -304,272 +157,489 @@ namespace Microsoft::React::Networking { /*override*/ { m_onError = std::move(handler); } +} + +void WinRTHttpResource::ClearCookies() noexcept /*override*/ { + assert(false); + // NOT IMPLEMENTED +} + +void WinRTHttpResource::SetOnRequestSuccess(function&& handler) noexcept /*override*/ { + m_onRequestSuccess = std::move(handler); +} + +void WinRTHttpResource::SetOnResponse(function&& handler) noexcept +/*override*/ { + m_onResponse = std::move(handler); +} + +void WinRTHttpResource::SetOnData(function&& handler) noexcept +/*override*/ { + m_onData = std::move(handler); +} + +void WinRTHttpResource::SetOnData(function&& handler) noexcept +/*override*/ +{ + m_onDataDynamic = std::move(handler); +} + +void WinRTHttpResource::SetOnError(function&& handler) noexcept +/*override*/ { + m_onError = std::move(handler); +} #pragma endregion IHttpResource - void WinRTHttpResource::TrackResponse(int64_t requestId, ResponseOperation response) noexcept { - scoped_lock lock{ m_mutex }; - m_responses[requestId] = response; - } +void WinRTHttpResource::TrackResponse(int64_t requestId, ResponseOperation response) noexcept { + scoped_lock lock{ m_mutex }; + m_responses[requestId] = response; +} + +void WinRTHttpResource::UntrackResponse(int64_t requestId) noexcept { + scoped_lock lock{ m_mutex }; + m_responses.erase(requestId); +} + +fire_and_forget +WinRTHttpResource::PerformSendRequest(HttpMethod&& method, Uri&& rtUri, IInspectable const& args) noexcept { + // Keep references after coroutine suspension. + auto self = shared_from_this(); + auto coArgs = args; + auto reqArgs = coArgs.as(); + auto coMethod = std::move(method); + auto coUri = std::move(rtUri); + + // Ensure background thread + co_await winrt::resume_background(); + + // 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)) { + 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); + } - void WinRTHttpResource::UntrackResponse(int64_t requestId) noexcept { - scoped_lock lock{ m_mutex }; - m_responses.erase(requestId); + co_return; + } + } + catch (const hresult_error& e) { + if (self->m_onError) + co_return self->m_onError(coReqArgs->RequestId, Utilities::HResultToString(e)); + } + catch (const std::exception& e) { + if (self->m_onError) + co_return self->m_onError(coReqArgs->RequestId, e.what()); + } } - fire_and_forget - WinRTHttpResource::PerformSendRequest(HttpMethod&& method, Uri&& rtUri, IInspectable const& args) noexcept { - // Keep references after coroutine suspension. - auto self = shared_from_this(); - auto coArgs = args; - auto reqArgs = coArgs.as(); - auto coMethod = std::move(method); - auto coUri = std::move(rtUri); + HttpMediaTypeHeaderValue contentType{ nullptr }; + string contentEncoding; + string contentLength; - // Ensure background thread - co_await winrt::resume_background(); + auto coRequest = co_await CreateRequest(std::move(coMethod), std::move(coUri), props); + if (!coRequest) { + co_return; + } - auto props = winrt::multi_threaded_map(); - props.Insert(L"RequestArgs", coArgs); + // 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, reqArgs->ResponseType)) { + auto blob = uriHandler->Fetch(uri); + if (self->m_onDataDynamic && self->m_onRequestSuccess) { + self->m_onDataDynamic(reqArgs->RequestId, std::move(blob)); + self->m_onRequestSuccess(reqArgs->RequestId); + } - auto coRequest = co_await CreateRequest(std::move(coMethod), std::move(coUri), props); - if (!coRequest) { - co_return; + co_return; + } + } + catch (const hresult_error& e) { + if (self->m_onError) + 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(reqArgs->RequestId, e.what(), false); } + } - // 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, reqArgs->ResponseType)) { - auto blob = uriHandler->Fetch(uri); - if (self->m_onDataDynamic && self->m_onRequestSuccess) { - self->m_onDataDynamic(reqArgs->RequestId, std::move(blob)); - self->m_onRequestSuccess(reqArgs->RequestId); - } + try { + auto sendRequestOp = self->m_client.SendRequestAsync(coRequest); + self->TrackResponse(reqArgs->RequestId, sendRequestOp); - co_return; - } - } - catch (const hresult_error& e) { - if (self->m_onError) - 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(reqArgs->RequestId, e.what(), false); - } - } + 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); - try { - auto sendRequestOp = self->m_client.SendRequestAsync(coRequest); - self->TrackResponse(reqArgs->RequestId, sendRequestOp); - - 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); - - 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) { - // 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 self->UntrackResponse(reqArgs->RequestId); - } - } - else { - co_await lessthrow_await_adapter{sendRequestOp}; - } + co_await lessthrow_await_adapter{winrt::when_any(sendRequestOp, sendRequestTimeout)}; + + // Cancel either still unfinished coroutine. + sendRequestTimeout.Cancel(); + sendRequestOp.Cancel(); - auto result = sendRequestOp.ErrorCode(); - if (result < 0) { + if (*timedOut) { if (self->m_onError) { - self->m_onError(reqArgs->RequestId, Utilities::HResultToString(std::move(result)), false); + // 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 self->UntrackResponse(reqArgs->RequestId); } + } + else { + co_await lessthrow_await_adapter{sendRequestOp}; + } - auto response = sendRequestOp.GetResults(); - if (response) { - if (self->m_onResponse) { - auto url = to_string(response.RequestMessage().RequestUri().AbsoluteUri()); + auto result = sendRequestOp.ErrorCode(); + if (result < 0) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, Utilities::HResultToString(std::move(result)), false); + } + co_return self->UntrackResponse(reqArgs->RequestId); + } - // Gather headers for both the response content and the response itself - // See Invoke-WebRequest PowerShell cmdlet or Chromium response handling - Headers responseHeaders; - for (auto header : response.Headers()) { - responseHeaders.emplace(to_string(header.Key()), to_string(header.Value())); - } - for (auto header : response.Content().Headers()) { - responseHeaders.emplace(to_string(header.Key()), to_string(header.Value())); - } + auto response = sendRequestOp.GetResults(); + if (response) { + if (self->m_onResponse) { + auto url = to_string(response.RequestMessage().RequestUri().AbsoluteUri()); - self->m_onResponse( - reqArgs->RequestId, - { static_cast(response.StatusCode()), std::move(url), std::move(responseHeaders) }); + // Gather headers for both the response content and the response itself + // See Invoke-WebRequest PowerShell cmdlet or Chromium response handling + Headers responseHeaders; + for (auto header : response.Headers()) { + responseHeaders.emplace(to_string(header.Key()), to_string(header.Value())); + } + for (auto header : response.Content().Headers()) { + responseHeaders.emplace(to_string(header.Key()), to_string(header.Value())); } - } - // #9534 - Support HTTP incremental updates - if (response && response.Content()) { - auto inputStream = co_await response.Content().ReadAsInputStreamAsync(); - auto reader = DataReader{ inputStream }; + self->m_onResponse( + reqArgs->RequestId, + { static_cast(response.StatusCode()), std::move(url), std::move(responseHeaders) }); + } + } - // #9510 - 10mb limit on fetch - co_await reader.LoadAsync(10 * 1024 * 1024); + // #9534 - Support HTTP incremental updates + if (response && response.Content()) { + auto inputStream = co_await response.Content().ReadAsInputStreamAsync(); + auto reader = DataReader{ inputStream }; - // Let response handler take over, if set - if (auto responseHandler = self->m_responseHandler.lock()) { - if (responseHandler->Supports(reqArgs->ResponseType)) { - auto bytes = vector(reader.UnconsumedBufferLength()); - reader.ReadBytes(bytes); - auto blob = responseHandler->ToResponseData(std::move(bytes)); + // #9510 - 10mb limit on fetch + co_await reader.LoadAsync(10 * 1024 * 1024); - if (self->m_onDataDynamic && self->m_onRequestSuccess) { - self->m_onDataDynamic(reqArgs->RequestId, std::move(blob)); - self->m_onRequestSuccess(reqArgs->RequestId); - } + // Let response handler take over, if set + if (auto responseHandler = self->m_responseHandler.lock()) { + if (responseHandler->Supports(reqArgs->ResponseType)) { + auto bytes = vector(reader.UnconsumedBufferLength()); + reader.ReadBytes(bytes); + auto blob = responseHandler->ToResponseData(std::move(bytes)); - co_return; + if (self->m_onDataDynamic && self->m_onRequestSuccess) { + self->m_onDataDynamic(reqArgs->RequestId, std::move(blob)); + self->m_onRequestSuccess(reqArgs->RequestId); } - } - auto isText = reqArgs->ResponseType == "text"; - if (isText) { - reader.UnicodeEncoding(UnicodeEncoding::Utf8); + co_return; } + } - // #9510 - We currently accumulate all incoming request data in 10MB chunks. - uint32_t segmentSize = 10 * 1024 * 1024; - string responseData; - winrt::Windows::Storage::Streams::IBuffer buffer; - uint32_t length; - do { - co_await reader.LoadAsync(segmentSize); - length = reader.UnconsumedBufferLength(); + auto isText = reqArgs->ResponseType == "text"; + if (isText) { + reader.UnicodeEncoding(UnicodeEncoding::Utf8); + } - if (coReqArgs->IsText) { - auto data = std::vector(length); - reader.ReadBytes(data); + // #9510 - We currently accumulate all incoming request data in 10MB chunks. + uint32_t segmentSize = 10 * 1024 * 1024; + string responseData; + winrt::Windows::Storage::Streams::IBuffer buffer; + uint32_t length; + do { + co_await reader.LoadAsync(segmentSize); + length = reader.UnconsumedBufferLength(); - responseData += string(Common::Utilities::CheckedReinterpretCast(data.data()), data.size()); - } - else { - buffer = reader.ReadBuffer(length); - auto data = CryptographicBuffer::EncodeToBase64String(buffer); + if (coReqArgs->IsText) { + auto data = std::vector(length); + reader.ReadBytes(data); - responseData += to_string(std::wstring_view(data)); - } - } while (length > 0); - - if (self->m_onData) { - self->m_onData(coReqArgs->RequestId, std::move(responseData)); + responseData += string(Common::Utilities::CheckedReinterpretCast(data.data()), data.size()); } - } - else { - if (self->m_onError) { - self->m_onError(reqArgs->RequestId, response == nullptr ? "request failed" : "No response content", false); + else { + buffer = reader.ReadBuffer(length); + auto data = CryptographicBuffer::EncodeToBase64String(buffer); + + responseData += to_string(std::wstring_view(data)); } + } while (length > 0); + + if (self->m_onData) { + self->m_onData(coReqArgs->RequestId, std::move(responseData)); } } - catch (std::exception const& e) { + else { if (self->m_onError) { - self->m_onError(reqArgs->RequestId, e.what(), false); + self->m_onError(reqArgs->RequestId, response == nullptr ? "request failed" : "No response content", false); } } - catch (hresult_error const& e) { - if (self->m_onError) { - self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), false); - } + } +} + +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 (...) { + catch (const std::invalid_argument& e) { if (self->m_onError) { - self->m_onError(reqArgs->RequestId, "Unhandled exception during request", false); + self->m_onError(coReqArgs->RequestId, e.what()); } + co_return; } + 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""}; + } +} - self->UntrackResponse(reqArgs->RequestId); - } // PerformSendRequest - -#pragma region IHttpModuleProxy +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"); - void WinRTHttpResource::AddUriHandler(shared_ptr /*uriHandler*/) noexcept /*override*/ - { - // TODO: Implement custom URI handling. + co_return; + } } - void WinRTHttpResource::AddRequestBodyHandler(shared_ptr requestBodyHandler) noexcept /*override*/ - { - m_requestBodyHandler = weak_ptr(requestBodyHandler); + if (!contentLength.empty()) { + const auto contentLengthHeader = _atoi64(contentLength.c_str()); + content.Headers().ContentLength(contentLengthHeader); } - void WinRTHttpResource::AddResponseHandler(shared_ptr responseHandler) noexcept /*override*/ - { - m_responseHandler = weak_ptr(responseHandler); + 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))); + } + co_return self->UntrackResponse(coReqArgs->RequestId); } -#pragma endregion IHttpModuleProxy + auto response = sendRequestOp.GetResults(); + if (response) { + if (self->m_onResponse) { + auto url = to_string(response.RequestMessage().RequestUri().AbsoluteUri()); -#pragma endregion WinRTHttpResource + // Gather headers for both the response content and the response itself + // See Invoke-WebRequest PowerShell cmdlet or Chromium response handling + Headers responseHeaders; + for (auto header : response.Headers()) { + responseHeaders.emplace(to_string(header.Key()), to_string(header.Value())); + } + for (auto header : response.Content().Headers()) { + responseHeaders.emplace(to_string(header.Key()), to_string(header.Value())); + } -#pragma region IHttpResource + self->m_onResponse( + coReqArgs->RequestId, + { static_cast(response.StatusCode()), std::move(url), std::move(responseHeaders) }); + } + } + + // #9534 - Support HTTP incremental updates + if (response && response.Content()) { + auto inputStream = co_await response.Content().ReadAsInputStreamAsync(); + auto reader = DataReader{ inputStream }; - /*static*/ shared_ptr IHttpResource::Make( - winrt::Windows::Foundation::IInspectable const& inspectableProperties) noexcept { - using namespace winrt::Microsoft::ReactNative; - using winrt::Windows::Web::Http::HttpClient; + // #9510 - 10mb limit on fetch + co_await reader.LoadAsync(10 * 1024 * 1024); - auto redirFilter = winrt::make(); - HttpClient client; + // Let response handler take over, if set + if (auto responseHandler = self->m_responseHandler.lock()) { + if (responseHandler->Supports(coReqArgs->ResponseType)) { + auto bytes = vector(reader.UnconsumedBufferLength()); + reader.ReadBytes(bytes); + auto blob = responseHandler->ToResponseData(std::move(bytes)); - if (static_cast(GetRuntimeOptionInt("Http.OriginPolicy")) == OriginPolicy::None) { - client = HttpClient{ redirFilter }; + if (self->m_onDataDynamic && self->m_onRequestSuccess) { + self->m_onDataDynamic(coReqArgs->RequestId, std::move(blob)); + self->m_onRequestSuccess(coReqArgs->RequestId); + } + + co_return; + } } - else { - auto globalOrigin = GetRuntimeOptionString("Http.GlobalOrigin"); - OriginPolicyHttpFilter::SetStaticOrigin(std::move(globalOrigin)); - auto opFilter = winrt::make(redirFilter); - redirFilter.as()->SetRedirectSource(opFilter.as()); - client = HttpClient{ opFilter }; + auto isText = coReqArgs->ResponseType == "text"; + if (isText) { + reader.UnicodeEncoding(UnicodeEncoding::Utf8); } - auto result = std::make_shared(std::move(client)); + // #9510 - We currently accumulate all incoming request data in 10MB chunks. + uint32_t segmentSize = 10 * 1024 * 1024; + string responseData; + winrt::Windows::Storage::Streams::IBuffer buffer; + uint32_t length; + do { + co_await reader.LoadAsync(segmentSize); + length = reader.UnconsumedBufferLength(); + + if (isText) { + auto data = std::vector(length); + reader.ReadBytes(data); + + responseData += string(Common::Utilities::CheckedReinterpretCast(data.data()), data.size()); + } + else { + buffer = reader.ReadBuffer(length); + auto data = CryptographicBuffer::EncodeToBase64String(buffer); - // Allow redirect filter to create requests based on the resource's state - redirFilter.as()->SetRequestFactory(weak_ptr{result}); + responseData += to_string(std::wstring_view(data)); + } + } while (length > 0); - // Register resource as HTTP module proxy. - if (inspectableProperties) { - auto propId = ReactPropertyId>>{ L"HttpModule.Proxy" }; - auto propBag = ReactPropertyBag{ inspectableProperties.try_as() }; - auto moduleProxy = weak_ptr{ result }; - propBag.Set(propId, std::move(moduleProxy)); + if (self->m_onData) { + self->m_onData(coReqArgs->RequestId, std::move(responseData)); } + } + else { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), false); + } + } + catch (...) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, "Unhandled exception during request", false); + } + } - return result; + self->UntrackResponse(reqArgs->RequestId); +} // PerformSendRequest + +#pragma region IHttpModuleProxy + +void WinRTHttpResource::AddUriHandler(shared_ptr /*uriHandler*/) noexcept /*override*/ +{ + // TODO: Implement custom URI handling. +} + +self->UntrackResponse(coReqArgs->RequestId); +} // PerformSendRequest + +#pragma region IHttpModuleProxy + +void WinRTHttpResource::AddUriHandler(shared_ptr /*uriHandler*/) noexcept /*override*/ +{ + // TODO: Implement custom URI handling. +} + +void WinRTHttpResource::AddRequestBodyHandler(shared_ptr requestBodyHandler) noexcept /*override*/ +{ + m_requestBodyHandler = weak_ptr(requestBodyHandler); +} + +void WinRTHttpResource::AddResponseHandler(shared_ptr responseHandler) noexcept /*override*/ +{ + m_responseHandler = weak_ptr(responseHandler); +} + +#pragma endregion IHttpModuleProxy + +#pragma endregion WinRTHttpResource + +#pragma region IHttpResource + +/*static*/ shared_ptr IHttpResource::Make( + winrt::Windows::Foundation::IInspectable const& inspectableProperties) noexcept { + using namespace winrt::Microsoft::ReactNative; + using winrt::Windows::Web::Http::HttpClient; + + shared_ptr result; + + if (static_cast(GetRuntimeOptionInt("Http.OriginPolicy")) == OriginPolicy::None) { + result = std::make_shared(); + } + else { + auto globalOrigin = GetRuntimeOptionString("Http.GlobalOrigin"); + OriginPolicyHttpFilter::SetStaticOrigin(std::move(globalOrigin)); + auto opFilter = winrt::make(); + auto client = HttpClient{ opFilter }; + + result = std::make_shared(std::move(client)); } - /*static*/ shared_ptr IHttpResource::Make() noexcept { - auto inspectableProperties = IInspectable{ nullptr }; - return Make(inspectableProperties); + // Register resource as HTTP module proxy. + if (inspectableProperties) { + auto propId = ReactPropertyId>>{ L"HttpModule.Proxy" }; + auto propBag = ReactPropertyBag{ inspectableProperties.try_as() }; + auto moduleProxy = weak_ptr{ result }; + propBag.Set(propId, std::move(moduleProxy)); } + return result; +} + +/*static*/ shared_ptr IHttpResource::Make() noexcept { + auto inspectableProperties = IInspectable{ nullptr }; + return Make(inspectableProperties); +} + #pragma endregion IHttpResource } // namespace Microsoft::React::Networking diff --git a/vnext/Shared/Networking/WinRTHttpResource.h b/vnext/Shared/Networking/WinRTHttpResource.h index 23cc68dd299..59c025d26d5 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.h +++ b/vnext/Shared/Networking/WinRTHttpResource.h @@ -6,7 +6,6 @@ #include "IHttpResource.h" #include -#include "IWinRTHttpRequestFactory.h" #include "WinRTTypes.h" // Windows API @@ -17,83 +16,82 @@ 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; - std::unordered_map m_responses; + class WinRTHttpResource: public IHttpResource, + public IHttpModuleProxy, + public std::enable_shared_from_this { + winrt::Windows::Web::Http::IHttpClient m_client; + std::mutex m_mutex; + std::unordered_map m_responses; - std::function m_onRequestSuccess; - std::function m_onResponse; - std::function m_onData; - std::function m_onDataDynamic; - std::function m_onError; + std::function m_onRequestSuccess; + std::function m_onResponse; + std::function m_onData; + std::function m_onDataDynamic; + std::function m_onError; - // Used for IHttpModuleProxy - std::weak_ptr m_uriHandler; - std::weak_ptr m_requestBodyHandler; - std::weak_ptr m_responseHandler; + // Used for IHttpModuleProxy + std::weak_ptr m_uriHandler; + std::weak_ptr m_requestBodyHandler; + std::weak_ptr m_responseHandler; - void TrackResponse(int64_t requestId, ResponseOperation response) noexcept; + void TrackResponse(int64_t requestId, ResponseOperation response) noexcept; - void UntrackResponse(int64_t requestId) noexcept; + void UntrackResponse(int64_t requestId) noexcept; - winrt::fire_and_forget PerformSendRequest( - winrt::Windows::Web::Http::HttpMethod &&method, - winrt::Windows::Foundation::Uri &&uri, - winrt::Windows::Foundation::IInspectable const &args) noexcept; + winrt::fire_and_forget PerformSendRequest( + winrt::Windows::Web::Http::HttpMethod&& method, + winrt::Windows::Foundation::Uri&& uri, + winrt::Windows::Foundation::IInspectable const& args) noexcept; - public: - WinRTHttpResource() noexcept; + public: + WinRTHttpResource() noexcept; - WinRTHttpResource(winrt::Windows::Web::Http::IHttpClient &&client) noexcept; + 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::IAsyncOperation CreateRequest( + winrt::Windows::Web::Http::HttpMethod&& method, + winrt::Windows::Foundation::Uri&& uri, winrt::Windows::Foundation::Collections::IMap - props) noexcept override; + props) noexcept override; #pragma endregion IWinRTHttpRequestFactory #pragma region IHttpResource - void SendRequest( - std::string &&method, - std::string &&url, + void SendRequest( + std::string&& method, + std::string&& url, int64_t requestId, - Headers &&headers, - folly::dynamic &&data, - std::string &&responseType, + Headers&& headers, + folly::dynamic&& data, + std::string&& responseType, bool useIncrementalUpdates, int64_t timeout, bool withCredentials, - std::function &&callback) noexcept override; - void AbortRequest(int64_t requestId) noexcept override; - void ClearCookies() noexcept override; + std::function&& callback) noexcept override; + void AbortRequest(int64_t requestId) noexcept override; + void ClearCookies() noexcept override; - void SetOnRequestSuccess(std::function &&handler) noexcept override; - 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 SetOnRequestSuccess(std::function&& handler) noexcept override; + 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; #pragma endregion IHttpResource #pragma region IHttpModuleProxy - void AddUriHandler(std::shared_ptr uriHandler) noexcept override; + void AddUriHandler(std::shared_ptr uriHandler) noexcept override; - void AddRequestBodyHandler(std::shared_ptr requestBodyHandler) noexcept override; + void AddRequestBodyHandler(std::shared_ptr requestBodyHandler) noexcept override; - void AddResponseHandler(std::shared_ptr responseHandler) noexcept override; + void AddResponseHandler(std::shared_ptr responseHandler) noexcept override; #pragma endregion IHttpModuleProxy -}; + }; } // namespace Microsoft::React::Networking diff --git a/vnext/Shared/OInstance.cpp b/vnext/Shared/OInstance.cpp index 59fa2e23a8f..ea2874903bf 100644 --- a/vnext/Shared/OInstance.cpp +++ b/vnext/Shared/OInstance.cpp @@ -71,185 +71,187 @@ using winrt::Microsoft::ReactNative::ReactPropertyBagHelper; namespace Microsoft::React { -/*extern*/ std::unique_ptr CreateHttpModule( - winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept { - if (GetRuntimeOptionBool("Http.UseMonolithicModule")) { - return std::make_unique(); - } else { - return std::make_unique(inspectableProperties); + /*extern*/ std::unique_ptr CreateHttpModule( + winrt::Windows::Foundation::IInspectable const& inspectableProperties) noexcept { + if (GetRuntimeOptionBool("Http.UseMonolithicModule")) { + return std::make_unique(); + } + else { + return std::make_unique(inspectableProperties); + } } -} } // namespace Microsoft::React namespace facebook { -namespace react { - -namespace { - -class OJSIExecutorFactory : public JSExecutorFactory { - public: - std::unique_ptr createJSExecutor( - std::shared_ptr delegate, - std::shared_ptr jsQueue) override { - Logger logger; - if (loggingHook_) { - // TODO :: Ensure the logLevels are mapped properly. - logger = [loggingHook = std::move(loggingHook_)](const std::string &message, unsigned int logLevel) { - loggingHook(static_cast(logLevel), message.c_str()); - }; - } else { - logger = [loggingHook = std::move(loggingHook_)](const std::string & /*message*/, unsigned int /*logLevel*/) {}; - } - bindNativeLogger(*runtimeHolder_->getRuntime(), logger); + namespace react { + + namespace { + + class OJSIExecutorFactory: public JSExecutorFactory { + public: + std::unique_ptr createJSExecutor( + std::shared_ptr delegate, + std::shared_ptr jsQueue) override { + Logger logger; + if (loggingHook_) { + // TODO :: Ensure the logLevels are mapped properly. + logger = [loggingHook = std::move(loggingHook_)](const std::string& message, unsigned int logLevel) { + loggingHook(static_cast(logLevel), message.c_str()); + }; + } + else { + logger = [loggingHook = std::move(loggingHook_)](const std::string& /*message*/, unsigned int /*logLevel*/) {}; + } + bindNativeLogger(*runtimeHolder_->getRuntime(), logger); - auto turboModuleManager = std::make_shared(turboModuleRegistry_, jsCallInvoker_); + auto turboModuleManager = std::make_shared(turboModuleRegistry_, jsCallInvoker_); - // TODO: The binding here should also add the proxys that convert cxxmodules into turbomodules - // [vmoroz] Note, that we must not use the RN TurboCxxModule.h code because it uses global LongLivedObjectCollection - // instance that prevents us from using multiple RN instance in the same process. - auto binding = [turboModuleManager](const std::string &name) -> std::shared_ptr { - return turboModuleManager->getModule(name); - }; + // TODO: The binding here should also add the proxys that convert cxxmodules into turbomodules + // [vmoroz] Note, that we must not use the RN TurboCxxModule.h code because it uses global LongLivedObjectCollection + // instance that prevents us from using multiple RN instance in the same process. + auto binding = [turboModuleManager](const std::string& name) -> std::shared_ptr { + return turboModuleManager->getModule(name); + }; - TurboModuleBinding::install(*runtimeHolder_->getRuntime(), std::function(binding), longLivedObjectCollection_); + TurboModuleBinding::install(*runtimeHolder_->getRuntime(), std::function(binding), longLivedObjectCollection_); - // init TurboModule - for (const auto &moduleName : turboModuleManager->getEagerInitModuleNames()) { - turboModuleManager->getModule(moduleName); - } + // init TurboModule + for (const auto& moduleName : turboModuleManager->getEagerInitModuleNames()) { + turboModuleManager->getModule(moduleName); + } - return std::make_unique( - runtimeHolder_->getRuntime(), - std::move(delegate), - JSIExecutor::defaultTimeoutInvoker, - [isProfiling = isProfilingEnabled_]([[maybe_unused]] jsi::Runtime &runtime) { + return std::make_unique( + runtimeHolder_->getRuntime(), + std::move(delegate), + JSIExecutor::defaultTimeoutInvoker, + [isProfiling = isProfilingEnabled_]([[maybe_unused]] jsi::Runtime& runtime) { #ifdef ENABLE_JS_SYSTRACE_TO_ETW - facebook::react::tracing::initializeJSHooks(runtime, isProfiling); + facebook::react::tracing::initializeJSHooks(runtime, isProfiling); #endif - }); - } + }); + } + + OJSIExecutorFactory( + std::shared_ptr runtimeHolder, + NativeLoggingHook loggingHook, + std::shared_ptr turboModuleRegistry, + std::shared_ptr longLivedObjectCollection, + bool isProfilingEnabled, + std::shared_ptr jsCallInvoker) noexcept + : runtimeHolder_{ std::move(runtimeHolder) }, + loggingHook_{ std::move(loggingHook) }, + turboModuleRegistry_{ std::move(turboModuleRegistry) }, + longLivedObjectCollection_{ std::move(longLivedObjectCollection) }, + jsCallInvoker_{ std::move(jsCallInvoker) }, + isProfilingEnabled_{ isProfilingEnabled } {} + + private: + std::shared_ptr runtimeHolder_; + std::shared_ptr turboModuleRegistry_; + std::shared_ptr longLivedObjectCollection_; + std::shared_ptr jsCallInvoker_; + NativeLoggingHook loggingHook_; + bool isProfilingEnabled_; + }; + + } // namespace + + void logMarker(const facebook::react::ReactMarker::ReactMarkerId /*id*/, const char* /*tag*/) {} + + /*static*/ std::shared_ptr InstanceImpl::MakeNoBundle( + std::shared_ptr&& instance, + std::string&& jsBundleBasePath, + std::vector< + std::tuple>> + && cxxModules, + std::shared_ptr turboModuleRegistry, + std::shared_ptr longLivedObjectCollection, + std::unique_ptr&& callback, + std::shared_ptr jsQueue, + std::shared_ptr nativeQueue, + std::shared_ptr devSettings, + std::shared_ptr devManager) noexcept { + auto inner = std::shared_ptr(new InstanceImpl( + std::move(instance), + std::move(jsBundleBasePath), + std::move(cxxModules), + std::move(turboModuleRegistry), + std::move(longLivedObjectCollection), + std::move(callback), + std::move(jsQueue), + std::move(nativeQueue), + std::move(devSettings), + std::move(devManager))); + + inner->RegisterForReloadIfNecessary(); + + return inner; + } + + /*static*/ std::shared_ptr InstanceImpl::MakeAndLoadBundle( + std::shared_ptr&& instance, + std::string&& jsBundleBasePath, + std::string&& jsBundleRelativePath, + std::vector< + std::tuple>> + && cxxModules, + std::shared_ptr turboModuleRegistry, + std::unique_ptr&& callback, + std::shared_ptr jsQueue, + std::shared_ptr nativeQueue, + std::shared_ptr devSettings, + std::shared_ptr devManager) noexcept { + auto inner = std::shared_ptr(new InstanceImpl( + std::move(instance), + std::move(jsBundleBasePath), + std::move(cxxModules), + std::move(turboModuleRegistry), + nullptr, + std::move(callback), + std::move(jsQueue), + std::move(nativeQueue), + std::move(devSettings), + std::move(devManager))); + + inner->loadBundle(std::move(jsBundleRelativePath)); + inner->RegisterForReloadIfNecessary(); + + return inner; + } - OJSIExecutorFactory( - std::shared_ptr runtimeHolder, - NativeLoggingHook loggingHook, + void InstanceImpl::SetInError() noexcept { + m_isInError = true; + } + + namespace { + bool shouldStartHermesInspector(DevSettings& devSettings) { + bool isHermes = + ((devSettings.jsiEngineOverride == JSIEngineOverride::Hermes) || + (devSettings.jsiEngineOverride == JSIEngineOverride::Default && devSettings.jsiRuntimeHolder && + devSettings.jsiRuntimeHolder->getRuntimeType() == facebook::react::JSIEngineOverride::Hermes)); + + if (isHermes && devSettings.useDirectDebugger && !devSettings.useWebDebugger) + return true; + else + return false; + } + } // namespace + + InstanceImpl::InstanceImpl( + std::shared_ptr&& instance, + std::string&& jsBundleBasePath, + std::vector< + std::tuple>> + && cxxModules, std::shared_ptr turboModuleRegistry, - std::shared_ptr longLivedObjectCollection, - bool isProfilingEnabled, - std::shared_ptr jsCallInvoker) noexcept - : runtimeHolder_{std::move(runtimeHolder)}, - loggingHook_{std::move(loggingHook)}, - turboModuleRegistry_{std::move(turboModuleRegistry)}, - longLivedObjectCollection_{std::move(longLivedObjectCollection)}, - jsCallInvoker_{std::move(jsCallInvoker)}, - isProfilingEnabled_{isProfilingEnabled} {} - - private: - std::shared_ptr runtimeHolder_; - std::shared_ptr turboModuleRegistry_; - std::shared_ptr longLivedObjectCollection_; - std::shared_ptr jsCallInvoker_; - NativeLoggingHook loggingHook_; - bool isProfilingEnabled_; -}; - -} // namespace - -void logMarker(const facebook::react::ReactMarker::ReactMarkerId /*id*/, const char * /*tag*/) {} - -/*static*/ std::shared_ptr InstanceImpl::MakeNoBundle( - std::shared_ptr &&instance, - std::string &&jsBundleBasePath, - std::vector< - std::tuple>> - &&cxxModules, - std::shared_ptr turboModuleRegistry, - std::shared_ptr longLivedObjectCollection, - std::unique_ptr &&callback, - std::shared_ptr jsQueue, - std::shared_ptr nativeQueue, - std::shared_ptr devSettings, - std::shared_ptr devManager) noexcept { - auto inner = std::shared_ptr(new InstanceImpl( - std::move(instance), - std::move(jsBundleBasePath), - std::move(cxxModules), - std::move(turboModuleRegistry), - std::move(longLivedObjectCollection), - std::move(callback), - std::move(jsQueue), - std::move(nativeQueue), - std::move(devSettings), - std::move(devManager))); - - inner->RegisterForReloadIfNecessary(); - - return inner; -} - -/*static*/ std::shared_ptr InstanceImpl::MakeAndLoadBundle( - std::shared_ptr &&instance, - std::string &&jsBundleBasePath, - std::string &&jsBundleRelativePath, - std::vector< - std::tuple>> - &&cxxModules, - std::shared_ptr turboModuleRegistry, - std::unique_ptr &&callback, - std::shared_ptr jsQueue, - std::shared_ptr nativeQueue, - std::shared_ptr devSettings, - std::shared_ptr devManager) noexcept { - auto inner = std::shared_ptr(new InstanceImpl( - std::move(instance), - std::move(jsBundleBasePath), - std::move(cxxModules), - std::move(turboModuleRegistry), - nullptr, - std::move(callback), - std::move(jsQueue), - std::move(nativeQueue), - std::move(devSettings), - std::move(devManager))); - - inner->loadBundle(std::move(jsBundleRelativePath)); - inner->RegisterForReloadIfNecessary(); - - return inner; -} - -void InstanceImpl::SetInError() noexcept { - m_isInError = true; -} - -namespace { -bool shouldStartHermesInspector(DevSettings &devSettings) { - bool isHermes = - ((devSettings.jsiEngineOverride == JSIEngineOverride::Hermes) || - (devSettings.jsiEngineOverride == JSIEngineOverride::Default && devSettings.jsiRuntimeHolder && - devSettings.jsiRuntimeHolder->getRuntimeType() == facebook::react::JSIEngineOverride::Hermes)); - - if (isHermes && devSettings.useDirectDebugger && !devSettings.useWebDebugger) - return true; - else - return false; -} -} // namespace - -InstanceImpl::InstanceImpl( - std::shared_ptr &&instance, - std::string &&jsBundleBasePath, - std::vector< - std::tuple>> - &&cxxModules, - std::shared_ptr turboModuleRegistry, - std::shared_ptr longLivedObjectCollection, - std::unique_ptr &&callback, - std::shared_ptr jsQueue, - std::shared_ptr nativeQueue, - std::shared_ptr devSettings, - std::shared_ptr devManager) - : m_turboModuleRegistry(std::move(turboModuleRegistry)), + std::shared_ptr longLivedObjectCollection, + std::unique_ptr&& callback, + std::shared_ptr jsQueue, + std::shared_ptr nativeQueue, + std::shared_ptr devSettings, + std::shared_ptr devManager) + : m_turboModuleRegistry(std::move(turboModuleRegistry)), m_longLivedObjectCollection(std::move(longLivedObjectCollection)), m_jsThread(std::move(jsQueue)), m_nativeQueue(nativeQueue), @@ -257,430 +259,430 @@ InstanceImpl::InstanceImpl( m_devSettings(std::move(devSettings)), m_devManager(std::move(devManager)), m_innerInstance(std::move(instance)) { - // Temp set the logmarker here - facebook::react::ReactMarker::logTaggedMarker = logMarker; + // Temp set the logmarker here + facebook::react::ReactMarker::logTaggedMarker = logMarker; #ifdef ENABLE_ETW_TRACING - // TODO :: Find a better place to initialize ETW once per process. - facebook::react::tracing::initializeETW(); + // TODO :: Find a better place to initialize ETW once per process. + facebook::react::tracing::initializeETW(); #endif - if (shouldStartHermesInspector(*m_devSettings)) { - m_devManager->EnsureHermesInspector(m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort); - } - - // Default (common) NativeModules - auto modules = GetDefaultNativeModules(nativeQueue); + if (shouldStartHermesInspector(*m_devSettings)) { + m_devManager->EnsureHermesInspector(m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort); + } - // Add app provided modules. - for (auto &cxxModule : cxxModules) { - modules.push_back(std::make_unique( - m_innerInstance, move(std::get<0>(cxxModule)), move(std::get<1>(cxxModule)), move(std::get<2>(cxxModule)))); - } - m_moduleRegistry = std::make_shared(std::move(modules)); - - // Choose JSExecutor - std::shared_ptr jsef; - if (m_devSettings->useWebDebugger) { - try { - auto jseFunc = m_devManager->LoadJavaScriptInProxyMode(*m_devSettings, [weakthis = weak_from_this()]() { - if (auto strongThis = weakthis.lock()) { - strongThis->SetInError(); - } - }); + // Default (common) NativeModules + auto modules = GetDefaultNativeModules(nativeQueue); - if ((jseFunc == nullptr) || m_isInError) { - m_devSettings->errorCallback("Failed to create JavaScript Executor."); - return; + // Add app provided modules. + for (auto& cxxModule : cxxModules) { + modules.push_back(std::make_unique( + m_innerInstance, move(std::get<0>(cxxModule)), move(std::get<1>(cxxModule)), move(std::get<2>(cxxModule)))); } + m_moduleRegistry = std::make_shared(std::move(modules)); + + // Choose JSExecutor + std::shared_ptr jsef; + if (m_devSettings->useWebDebugger) { + try { + auto jseFunc = m_devManager->LoadJavaScriptInProxyMode(*m_devSettings, [weakthis = weak_from_this()]() { + if (auto strongThis = weakthis.lock()) { + strongThis->SetInError(); + } + }); + + if ((jseFunc == nullptr) || m_isInError) { + m_devSettings->errorCallback("Failed to create JavaScript Executor."); + return; + } - jsef = std::make_shared(std::move(jseFunc)); - } catch (std::exception &e) { - m_devSettings->errorCallback(e.what()); - return; - } - } else { - if (m_devSettings->useFastRefresh || m_devSettings->liveReloadCallback) { - Microsoft::ReactNative::PackagerConnection::CreateOrReusePackagerConnection(*m_devSettings); - } + jsef = std::make_shared(std::move(jseFunc)); + } + catch (std::exception& e) { + m_devSettings->errorCallback(e.what()); + return; + } + } + else { + if (m_devSettings->useFastRefresh || m_devSettings->liveReloadCallback) { + Microsoft::ReactNative::PackagerConnection::CreateOrReusePackagerConnection(*m_devSettings); + } - // If the consumer gives us a JSI runtime, then use it. - if (m_devSettings->jsiRuntimeHolder) { - assert(m_devSettings->jsiEngineOverride == JSIEngineOverride::Default); - jsef = std::make_shared( - m_devSettings->jsiRuntimeHolder, - m_devSettings->loggingCallback, - m_turboModuleRegistry, - m_longLivedObjectCollection, - !m_devSettings->useFastRefresh, - m_innerInstance->getJSCallInvoker()); - } else { - assert(m_devSettings->jsiEngineOverride != JSIEngineOverride::Default); - switch (m_devSettings->jsiEngineOverride) { - case JSIEngineOverride::Hermes: - m_devSettings->jsiRuntimeHolder = std::make_shared(m_devSettings, m_jsThread); - break; - case JSIEngineOverride::V8: { + // If the consumer gives us a JSI runtime, then use it. + if (m_devSettings->jsiRuntimeHolder) { + assert(m_devSettings->jsiEngineOverride == JSIEngineOverride::Default); + jsef = std::make_shared( + m_devSettings->jsiRuntimeHolder, + m_devSettings->loggingCallback, + m_turboModuleRegistry, + m_longLivedObjectCollection, + !m_devSettings->useFastRefresh, + m_innerInstance->getJSCallInvoker()); + } + else { + assert(m_devSettings->jsiEngineOverride != JSIEngineOverride::Default); + switch (m_devSettings->jsiEngineOverride) { + case JSIEngineOverride::Hermes: + m_devSettings->jsiRuntimeHolder = std::make_shared(m_devSettings, m_jsThread); + break; + case JSIEngineOverride::V8: { #if defined(USE_V8) - std::unique_ptr scriptStore = nullptr; - std::unique_ptr preparedScriptStore = nullptr; + std::unique_ptr scriptStore = nullptr; + std::unique_ptr preparedScriptStore = nullptr; - char tempPath[MAX_PATH]; - if (GetTempPathA(MAX_PATH, tempPath)) { - preparedScriptStore = std::make_unique(tempPath); - } + char tempPath[MAX_PATH]; + if (GetTempPathA(MAX_PATH, tempPath)) { + preparedScriptStore = std::make_unique(tempPath); + } - m_devSettings->jsiRuntimeHolder = std::make_shared( + m_devSettings->jsiRuntimeHolder = std::make_shared( m_devSettings, m_jsThread, std::move(scriptStore), std::move(preparedScriptStore)); - break; + break; #else - assert(false); // V8 is not available in this build, fallthrough - [[fallthrough]]; + assert(false); // V8 is not available in this build, fallthrough + [[fallthrough]]; #endif - } - case JSIEngineOverride::V8NodeApi: { + } + case JSIEngineOverride::V8NodeApi: { #if defined(USE_V8) - std::unique_ptr preparedScriptStore; + std::unique_ptr preparedScriptStore; - wchar_t tempPath[MAX_PATH]; - if (GetTempPathW(static_cast(std::size(tempPath)), tempPath)) { - preparedScriptStore = + wchar_t tempPath[MAX_PATH]; + if (GetTempPathW(static_cast(std::size(tempPath)), tempPath)) { + preparedScriptStore = std::make_unique(winrt::to_string(tempPath)); - } + } - if (!preparedScriptStore) { - if (m_devSettings->errorCallback) - m_devSettings->errorCallback("Could not initialize prepared script store"); + if (!preparedScriptStore) { + if (m_devSettings->errorCallback) + m_devSettings->errorCallback("Could not initialize prepared script store"); - break; - } + break; + } - m_devSettings->jsiRuntimeHolder = make_shared( + m_devSettings->jsiRuntimeHolder = make_shared( m_devSettings, m_jsThread, nullptr /*scriptStore*/, std::move(preparedScriptStore)); - break; + break; #else - if (m_devSettings->errorCallback) - m_devSettings->errorCallback("JSI/V8/NAPI engine is not available in this build"); - assert(false); - [[fallthrough]]; + if (m_devSettings->errorCallback) + m_devSettings->errorCallback("JSI/V8/NAPI engine is not available in this build"); + assert(false); + [[fallthrough]]; #endif - } - case JSIEngineOverride::Chakra: - case JSIEngineOverride::ChakraCore: - default: // TODO: Add other engines once supported - m_devSettings->jsiRuntimeHolder = + } + case JSIEngineOverride::Chakra: + case JSIEngineOverride::ChakraCore: + default: // TODO: Add other engines once supported + m_devSettings->jsiRuntimeHolder = std::make_shared(m_devSettings, m_jsThread, nullptr, nullptr); - break; + break; + } + jsef = std::make_shared( + m_devSettings->jsiRuntimeHolder, + m_devSettings->loggingCallback, + m_turboModuleRegistry, + m_longLivedObjectCollection, + !m_devSettings->useFastRefresh, + m_innerInstance->getJSCallInvoker()); + } + } + + m_innerInstance->initializeBridge(std::move(callback), jsef, m_jsThread, m_moduleRegistry); + + // All JSI runtimes do support host objects and hence the native modules + // proxy. + const bool isNativeModulesProxyAvailable = ((m_devSettings->jsiRuntimeHolder != nullptr) || + (m_devSettings->jsiEngineOverride != JSIEngineOverride::Default)) && + !m_devSettings->useWebDebugger; + if (!isNativeModulesProxyAvailable) { + folly::dynamic configArray = folly::dynamic::array; + for (auto const& moduleName : m_moduleRegistry->moduleNames()) { + auto moduleConfig = m_moduleRegistry->getConfig(moduleName); + configArray.push_back(moduleConfig ? std::move(moduleConfig->config) : nullptr); + } + + folly::dynamic configs = folly::dynamic::object("remoteModuleConfig", std::move(configArray)); + m_innerInstance->setGlobalVariable( + "__fbBatchedBridgeConfig", std::make_unique(folly::toJson(configs))); } - jsef = std::make_shared( - m_devSettings->jsiRuntimeHolder, - m_devSettings->loggingCallback, - m_turboModuleRegistry, - m_longLivedObjectCollection, - !m_devSettings->useFastRefresh, - m_innerInstance->getJSCallInvoker()); } - } - m_innerInstance->initializeBridge(std::move(callback), jsef, m_jsThread, m_moduleRegistry); - - // All JSI runtimes do support host objects and hence the native modules - // proxy. - const bool isNativeModulesProxyAvailable = ((m_devSettings->jsiRuntimeHolder != nullptr) || - (m_devSettings->jsiEngineOverride != JSIEngineOverride::Default)) && - !m_devSettings->useWebDebugger; - if (!isNativeModulesProxyAvailable) { - folly::dynamic configArray = folly::dynamic::array; - for (auto const &moduleName : m_moduleRegistry->moduleNames()) { - auto moduleConfig = m_moduleRegistry->getConfig(moduleName); - configArray.push_back(moduleConfig ? std::move(moduleConfig->config) : nullptr); + void InstanceImpl::loadBundle(std::string&& jsBundleRelativePath) { + loadBundleInternal(std::move(jsBundleRelativePath), /*synchronously:*/ false); } - folly::dynamic configs = folly::dynamic::object("remoteModuleConfig", std::move(configArray)); - m_innerInstance->setGlobalVariable( - "__fbBatchedBridgeConfig", std::make_unique(folly::toJson(configs))); - } -} - -void InstanceImpl::loadBundle(std::string &&jsBundleRelativePath) { - loadBundleInternal(std::move(jsBundleRelativePath), /*synchronously:*/ false); -} - -void InstanceImpl::loadBundleSync(std::string &&jsBundleRelativePath) { - loadBundleInternal(std::move(jsBundleRelativePath), /*synchronously:*/ true); -} - -// Note: Based on -// https://github.com/facebook/react-native/blob/24d91268b64c7abbd4b26547ffcc663dc90ec5e7/ReactCommon/cxxreact/Instance.cpp#L112 -bool isHBCBundle(const std::string &bundle) { - static uint32_t constexpr HBCBundleMagicNumber = 0xffe7c3c3; - - // Note:: Directly access the pointer to avoid copy/length-check. It matters as this string contains the bundle which - // can be potentially huge. - // https://herbsutter.com/2008/04/07/cringe-not-vectors-are-guaranteed-to-be-contiguous/#comment-483 - auto header = reinterpret_cast(&bundle[0]); - if (HBCBundleMagicNumber == header->magic32.value) { - return true; - } else { - return false; - } -} + void InstanceImpl::loadBundleSync(std::string&& jsBundleRelativePath) { + loadBundleInternal(std::move(jsBundleRelativePath), /*synchronously:*/ true); + } -void InstanceImpl::loadBundleInternal(std::string &&jsBundleRelativePath, bool synchronously) { - try { - if (m_devSettings->useWebDebugger || m_devSettings->liveReloadCallback != nullptr || - m_devSettings->useFastRefresh) { - // First attempt to get download the Js locally, to catch any bundling - // errors before attempting to load the actual script. + // Note: Based on + // https://github.com/facebook/react-native/blob/24d91268b64c7abbd4b26547ffcc663dc90ec5e7/ReactCommon/cxxreact/Instance.cpp#L112 + bool isHBCBundle(const std::string& bundle) { + static uint32_t constexpr HBCBundleMagicNumber = 0xffe7c3c3; + + // Note:: Directly access the pointer to avoid copy/length-check. It matters as this string contains the bundle which + // can be potentially huge. + // https://herbsutter.com/2008/04/07/cringe-not-vectors-are-guaranteed-to-be-contiguous/#comment-483 + auto header = reinterpret_cast(&bundle[0]); + if (HBCBundleMagicNumber == header->magic32.value) { + return true; + } + else { + return false; + } + } - uint32_t hermesBytecodeVersion = 0; + void InstanceImpl::loadBundleInternal(std::string&& jsBundleRelativePath, bool synchronously) { + try { + if (m_devSettings->useWebDebugger || m_devSettings->liveReloadCallback != nullptr || + m_devSettings->useFastRefresh) { + // First attempt to get download the Js locally, to catch any bundling + // errors before attempting to load the actual script. + + uint32_t hermesBytecodeVersion = 0; #if defined(USE_HERMES) && defined(ENABLE_DEVSERVER_HBCBUNDLES) - hermesBytecodeVersion = ::hermes::hbc::BYTECODE_VERSION; + hermesBytecodeVersion = ::hermes::hbc::BYTECODE_VERSION; #endif - auto [jsBundleString, success] = Microsoft::ReactNative::GetJavaScriptFromServer( - m_devSettings->sourceBundleHost, - m_devSettings->sourceBundlePort, - m_devSettings->debugBundlePath.empty() ? jsBundleRelativePath : m_devSettings->debugBundlePath, - m_devSettings->platformName, - true /* dev */, - m_devSettings->useFastRefresh, - m_devSettings->inlineSourceMap, - hermesBytecodeVersion); + auto [jsBundleString, success] = Microsoft::ReactNative::GetJavaScriptFromServer( + m_devSettings->sourceBundleHost, + m_devSettings->sourceBundlePort, + m_devSettings->debugBundlePath.empty() ? jsBundleRelativePath : m_devSettings->debugBundlePath, + m_devSettings->platformName, + true /* dev */, + m_devSettings->useFastRefresh, + m_devSettings->inlineSourceMap, + hermesBytecodeVersion); - if (!success) { - m_devManager->UpdateBundleStatus(false, -1); - m_devSettings->errorCallback(jsBundleString); - return; - } + if (!success) { + m_devManager->UpdateBundleStatus(false, -1); + m_devSettings->errorCallback(jsBundleString); + return; + } - int64_t currentTimeInMilliSeconds = - std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) - .count(); - m_devManager->UpdateBundleStatus(true, currentTimeInMilliSeconds); + int64_t currentTimeInMilliSeconds = + std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) + .count(); + m_devManager->UpdateBundleStatus(true, currentTimeInMilliSeconds); - auto bundleUrl = DevServerHelper::get_BundleUrl( - m_devSettings->sourceBundleHost, - m_devSettings->sourceBundlePort, - m_devSettings->debugBundlePath.empty() ? jsBundleRelativePath : m_devSettings->debugBundlePath, - m_devSettings->platformName, - /*dev*/ true, - /*hot*/ false, - m_devSettings->inlineSourceMap, - hermesBytecodeVersion); - - // This code is based on the HBC Bundle integration on Android - // Ref: - // https://github.com/facebook/react-native/blob/24d91268b64c7abbd4b26547ffcc663dc90ec5e7/ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.cpp#L231 - if (isHBCBundle(jsBundleString)) { - auto script = std::make_unique(jsBundleString, false); - const char *buffer = script->c_str(); - uint32_t bufferLength = (uint32_t)script->size(); - - // Please refer the code here for details on the file format: - // https://github.com/facebook/metro/blob/b1bacf52070be62872d6bd3420f37a4405ed34e6/packages/metro/src/lib/bundleToBytecode.js#L29 - // Essentially, there is an 8 byte long file header with 4 bytes of a magic number followed by 4 bytes to encode - // the number of modules.The module buffers follows, each one starts with 4 byte header which encodes module - // length.A properly formatted HBCB should have at least 8 bytes.. - uint32_t offset = 8; + auto bundleUrl = DevServerHelper::get_BundleUrl( + m_devSettings->sourceBundleHost, + m_devSettings->sourceBundlePort, + m_devSettings->debugBundlePath.empty() ? jsBundleRelativePath : m_devSettings->debugBundlePath, + m_devSettings->platformName, + /*dev*/ true, + /*hot*/ false, + m_devSettings->inlineSourceMap, + hermesBytecodeVersion); + + // This code is based on the HBC Bundle integration on Android + // Ref: + // https://github.com/facebook/react-native/blob/24d91268b64c7abbd4b26547ffcc663dc90ec5e7/ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.cpp#L231 + if (isHBCBundle(jsBundleString)) { + auto script = std::make_unique(jsBundleString, false); + const char* buffer = script->c_str(); + uint32_t bufferLength = (uint32_t)script->size(); + + // Please refer the code here for details on the file format: + // https://github.com/facebook/metro/blob/b1bacf52070be62872d6bd3420f37a4405ed34e6/packages/metro/src/lib/bundleToBytecode.js#L29 + // Essentially, there is an 8 byte long file header with 4 bytes of a magic number followed by 4 bytes to encode + // the number of modules.The module buffers follows, each one starts with 4 byte header which encodes module + // length.A properly formatted HBCB should have at least 8 bytes.. + uint32_t offset = 8; #define __SAFEADD__(s1, s2, t) \ if (!msl::utilities::SafeAdd(s1, s2, t)) \ break; - while (offset < bufferLength) { - uint32_t segment; - __SAFEADD__(offset, 4, segment) - uint32_t moduleLength = (bufferLength < segment) ? 0 : *(((uint32_t *)buffer) + offset / 4); - - // Early break if the module length is computed as 0.. as the segment start may be overflowing the buffer. - if (moduleLength == 0) - break; - - uint32_t segmentEnd; - __SAFEADD__(moduleLength, segment, segmentEnd) - // Early break if the segment overflows beyond the buffer. This is unlikely for a properly formatted - // HBCB though. - if (segmentEnd > bufferLength) - break; - - m_innerInstance->loadScriptFromString( - std::make_unique(std::string(buffer + segment, buffer + segmentEnd)), + while (offset < bufferLength) { + uint32_t segment; + __SAFEADD__(offset, 4, segment) + uint32_t moduleLength = (bufferLength < segment) ? 0 : *(((uint32_t*)buffer) + offset / 4); + + // Early break if the module length is computed as 0.. as the segment start may be overflowing the buffer. + if (moduleLength == 0) + break; + + uint32_t segmentEnd; + __SAFEADD__(moduleLength, segment, segmentEnd) + // Early break if the segment overflows beyond the buffer. This is unlikely for a properly formatted + // HBCB though. + if (segmentEnd > bufferLength) + break; + + m_innerInstance->loadScriptFromString( + std::make_unique(std::string(buffer + segment, buffer + segmentEnd)), + bundleUrl, + false); + + // Aligned at 4 byte boundary. + offset += ((moduleLength + 3) & ~3) + 4; + } +#undef __SAFEADD__ + } + else { + // Remote debug executor loads script from a Uri, rather than taking the actual bundle string + m_innerInstance->loadScriptFromString( + std::make_unique(m_devSettings->useWebDebugger ? bundleUrl : jsBundleString), bundleUrl, - false); - - // Aligned at 4 byte boundary. - offset += ((moduleLength + 3) & ~3) + 4; + synchronously); + } } -#undef __SAFEADD__ - } else { - // Remote debug executor loads script from a Uri, rather than taking the actual bundle string - m_innerInstance->loadScriptFromString( - std::make_unique(m_devSettings->useWebDebugger ? bundleUrl : jsBundleString), - bundleUrl, - synchronously); - } - } else { + else { #if (defined(_MSC_VER) && !defined(WINRT)) - std::string bundlePath = (fs::path(m_devSettings->bundleRootPath) / jsBundleRelativePath).string(); - auto bundleString = FileMappingBigString::fromPath(bundlePath); + std::string bundlePath = (fs::path(m_devSettings->bundleRootPath) / jsBundleRelativePath).string(); + auto bundleString = FileMappingBigString::fromPath(bundlePath); #else - std::string bundlePath; - if (m_devSettings->bundleRootPath._Starts_with("resource://")) { - auto uri = winrt::Windows::Foundation::Uri( - winrt::to_hstring(m_devSettings->bundleRootPath), winrt::to_hstring(jsBundleRelativePath)); - bundlePath = winrt::to_string(uri.ToString()); - } else { - bundlePath = (fs::path(m_devSettings->bundleRootPath) / (jsBundleRelativePath + ".bundle")).string(); - } + std::string bundlePath; + if (m_devSettings->bundleRootPath._Starts_with("resource://")) { + auto uri = winrt::Windows::Foundation::Uri( + winrt::to_hstring(m_devSettings->bundleRootPath), winrt::to_hstring(jsBundleRelativePath)); + bundlePath = winrt::to_string(uri.ToString()); + } + else { + bundlePath = (fs::path(m_devSettings->bundleRootPath) / (jsBundleRelativePath + ".bundle")).string(); + } - auto bundleString = std::make_unique<::Microsoft::ReactNative::StorageFileBigString>(bundlePath); + auto bundleString = std::make_unique<::Microsoft::ReactNative::StorageFileBigString>(bundlePath); #endif - m_innerInstance->loadScriptFromString(std::move(bundleString), std::move(jsBundleRelativePath), synchronously); - } - } catch (const std::exception &e) { - m_devSettings->errorCallback(e.what()); - } catch (const winrt::hresult_error &hrerr) { - auto error = fmt::format("[0x{:0>8x}] {}", static_cast(hrerr.code()), winrt::to_string(hrerr.message())); + m_innerInstance->loadScriptFromString(std::move(bundleString), std::move(jsBundleRelativePath), synchronously); + } + } + catch (const std::exception& e) { + m_devSettings->errorCallback(e.what()); + } + catch (const winrt::hresult_error& hrerr) { + auto error = fmt::format("[0x{:0>8x}] {}", static_cast(hrerr.code()), winrt::to_string(hrerr.message())); - m_devSettings->errorCallback(std::move(error)); - } -} + m_devSettings->errorCallback(std::move(error)); + } + } -InstanceImpl::~InstanceImpl() { - if (shouldStartHermesInspector(*m_devSettings) && m_devSettings->jsiRuntimeHolder) { - m_devSettings->jsiRuntimeHolder->teardown(); - } - m_nativeQueue->quitSynchronous(); -} + InstanceImpl::~InstanceImpl() { + if (shouldStartHermesInspector(*m_devSettings) && m_devSettings->jsiRuntimeHolder) { + m_devSettings->jsiRuntimeHolder->teardown(); + } + m_nativeQueue->quitSynchronous(); + } -std::vector> InstanceImpl::GetDefaultNativeModules( - std::shared_ptr nativeQueue) { - std::vector> modules; - auto transitionalProps{ReactPropertyBagHelper::CreatePropertyBag()}; + std::vector> InstanceImpl::GetDefaultNativeModules( + std::shared_ptr nativeQueue) { + std::vector> modules; + auto transitionalProps{ ReactPropertyBagHelper::CreatePropertyBag() }; #if (defined(_MSC_VER) && !defined(WINRT)) - modules.push_back(std::make_unique( - m_innerInstance, - Microsoft::React::GetHttpModuleName(), - [nativeQueue, transitionalProps]() -> std::unique_ptr { - return Microsoft::React::CreateHttpModule(transitionalProps); - }, - nativeQueue)); + modules.push_back(std::make_unique( + m_innerInstance, + Microsoft::React::GetHttpModuleName(), + [nativeQueue, transitionalProps]() -> std::unique_ptr { + return Microsoft::React::CreateHttpModule(transitionalProps); + }, + nativeQueue)); #endif - modules.push_back(std::make_unique( - m_innerInstance, - Microsoft::React::GetWebSocketModuleName(), - [nativeQueue, transitionalProps]() -> std::unique_ptr { - return Microsoft::React::CreateWebSocketModule(transitionalProps); - }, - nativeQueue)); - - // TODO: This is not included for UWP because we have a different module which - // is added later. However, this one is designed - // so that we can base a UWP version on it. We need to do that but is not high - // priority. + modules.push_back(std::make_unique( + m_innerInstance, + Microsoft::React::GetWebSocketModuleName(), + [nativeQueue, transitionalProps]() -> std::unique_ptr { + return Microsoft::React::CreateWebSocketModule(transitionalProps); + }, + nativeQueue)); + + // TODO: This is not included for UWP because we have a different module which + // is added later. However, this one is designed + // so that we can base a UWP version on it. We need to do that but is not high + // priority. #if (defined(_MSC_VER) && !defined(WINRT)) - modules.push_back(std::make_unique( - m_innerInstance, - "Timing", - [nativeQueue]() -> std::unique_ptr { return react::CreateTimingModule(nativeQueue); }, - nativeQueue)); + modules.push_back(std::make_unique( + m_innerInstance, + "Timing", + [nativeQueue]() -> std::unique_ptr { return react::CreateTimingModule(nativeQueue); }, + nativeQueue)); #endif - uint32_t hermesBytecodeVersion = 0; + uint32_t hermesBytecodeVersion = 0; #if defined(USE_HERMES) && defined(ENABLE_DEVSERVER_HBCBUNDLES) - hermesBytecodeVersion = ::hermes::hbc::BYTECODE_VERSION; + hermesBytecodeVersion = ::hermes::hbc::BYTECODE_VERSION; #endif - // TODO - Encapsulate this in a helpers, and make sure callers add it to their - // list - std::string bundleUrl = (m_devSettings->useWebDebugger || m_devSettings->liveReloadCallback) - ? DevServerHelper::get_BundleUrl( - m_devSettings->sourceBundleHost, - m_devSettings->sourceBundlePort, - m_devSettings->debugBundlePath, - m_devSettings->platformName, - true /*dev*/, - m_devSettings->useFastRefresh, - m_devSettings->inlineSourceMap, - hermesBytecodeVersion) - : std::string(); - modules.push_back(std::make_unique( - m_innerInstance, - facebook::react::SourceCodeModule::Name, - [bundleUrl]() -> std::unique_ptr { - return std::make_unique(bundleUrl); - }, - nativeQueue)); - - modules.push_back(std::make_unique( - m_innerInstance, - "ExceptionsManager", - [redboxHandler = m_devSettings->redboxHandler]() mutable { - return std::make_unique(redboxHandler); - }, - nativeQueue)); - - modules.push_back(std::make_unique( - m_innerInstance, - PlatformConstantsModule::Name, - []() { return std::make_unique(); }, - nativeQueue)); - - modules.push_back(std::make_unique( - m_innerInstance, - StatusBarManagerModule::Name, - []() { return std::make_unique(); }, - nativeQueue)); - - // 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( + // TODO - Encapsulate this in a helpers, and make sure callers add it to their + // list + std::string bundleUrl = (m_devSettings->useWebDebugger || m_devSettings->liveReloadCallback) + ? DevServerHelper::get_BundleUrl( + m_devSettings->sourceBundleHost, + m_devSettings->sourceBundlePort, + m_devSettings->debugBundlePath, + m_devSettings->platformName, + true /*dev*/, + m_devSettings->useFastRefresh, + m_devSettings->inlineSourceMap, + hermesBytecodeVersion) + : std::string(); + modules.push_back(std::make_unique( + m_innerInstance, + facebook::react::SourceCodeModule::Name, + [bundleUrl]() -> std::unique_ptr { + return std::make_unique(bundleUrl); + }, + nativeQueue)); + + modules.push_back(std::make_unique( + m_innerInstance, + "ExceptionsManager", + [redboxHandler = m_devSettings->redboxHandler]() mutable { + return std::make_unique(redboxHandler); + }, + nativeQueue)); + + modules.push_back(std::make_unique( + m_innerInstance, + PlatformConstantsModule::Name, + []() { return std::make_unique(); }, + nativeQueue)); + + modules.push_back(std::make_unique( + m_innerInstance, + StatusBarManagerModule::Name, + []() { return std::make_unique(); }, + nativeQueue)); + + modules.push_back(std::make_unique( m_innerInstance, Microsoft::React::GetBlobModuleName(), [transitionalProps]() { return Microsoft::React::CreateBlobModule(transitionalProps); }, nativeQueue)); - modules.push_back(std::make_unique( + modules.push_back(std::make_unique( m_innerInstance, Microsoft::React::GetFileReaderModuleName(), [transitionalProps]() { return Microsoft::React::CreateFileReaderModule(transitionalProps); }, nativeQueue)); - } -#endif - return modules; -} + return modules; + } -void InstanceImpl::RegisterForReloadIfNecessary() noexcept { - // setup polling for live reload - if (!m_isInError && !m_devSettings->useFastRefresh && m_devSettings->liveReloadCallback != nullptr) { - m_devManager->StartPollingLiveReload( - m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort, m_devSettings->liveReloadCallback); - } -} + void InstanceImpl::RegisterForReloadIfNecessary() noexcept { + // setup polling for live reload + if (!m_isInError && !m_devSettings->useFastRefresh && m_devSettings->liveReloadCallback != nullptr) { + m_devManager->StartPollingLiveReload( + m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort, m_devSettings->liveReloadCallback); + } + } -void InstanceImpl::DispatchEvent(int64_t viewTag, std::string eventName, folly::dynamic &&eventData) { - if (m_isInError) { - return; - } + void InstanceImpl::DispatchEvent(int64_t viewTag, std::string eventName, folly::dynamic&& eventData) { + if (m_isInError) { + return; + } - folly::dynamic params = folly::dynamic::array(viewTag, eventName, std::move(eventData)); - m_innerInstance->callJSFunction("RCTEventEmitter", "receiveEvent", std::move(params)); -} + folly::dynamic params = folly::dynamic::array(viewTag, eventName, std::move(eventData)); + m_innerInstance->callJSFunction("RCTEventEmitter", "receiveEvent", std::move(params)); + } -void InstanceImpl::invokeCallback(const int64_t callbackId, folly::dynamic &¶ms) { - if (m_isInError) { - return; - } + void InstanceImpl::invokeCallback(const int64_t callbackId, folly::dynamic&& params) { + if (m_isInError) { + return; + } - m_innerInstance->callJSCallback(callbackId, std::move(params)); -} + m_innerInstance->callJSCallback(callbackId, std::move(params)); + } -} // namespace react + } // namespace react } // namespace facebook diff --git a/vnext/Shared/OInstance.h b/vnext/Shared/OInstance.h index d09e2d25353..c14f915dd40 100644 --- a/vnext/Shared/OInstance.h +++ b/vnext/Shared/OInstance.h @@ -10,7 +10,6 @@ #include "InstanceManager.h" // React Native -#include #include // Standard Libriary @@ -19,87 +18,87 @@ #include namespace facebook { -namespace react { - -struct IDevSupportManager; -struct IReactRootView; - -class InstanceImpl final : public InstanceWrapper, private ::std::enable_shared_from_this { - public: - static std::shared_ptr MakeNoBundle( - std::shared_ptr &&instance, - std::string &&jsBundleBasePath, - std::vector< - std::tuple>> - &&cxxModules, - std::shared_ptr turboModuleRegistry, - std::shared_ptr longLivedObjectCollection, - std::unique_ptr &&callback, - std::shared_ptr jsQueue, - std::shared_ptr nativeQueue, - std::shared_ptr devSettings, - std::shared_ptr devManager) noexcept; - - static std::shared_ptr MakeAndLoadBundle( - std::shared_ptr &&instance, - std::string &&jsBundleBasePath, - std::string &&jsBundleRelativePath, - std::vector< - std::tuple>> - &&cxxModules, - std::shared_ptr turboModuleRegistry, - std::unique_ptr &&callback, - std::shared_ptr jsQueue, - std::shared_ptr nativeQueue, - std::shared_ptr devSettings, - std::shared_ptr devManager) noexcept; - - // Instance methods - void loadBundle(std::string &&jsBundleRelativePath) override; - void loadBundleSync(std::string &&jsBundleRelativePath) override; - virtual const std::shared_ptr &GetInstance() const noexcept override { - return m_innerInstance; - } - - void DispatchEvent(int64_t viewTag, std::string eventName, folly::dynamic &&eventData) override; - void invokeCallback(const int64_t callbackId, folly::dynamic &¶ms) override; - - ~InstanceImpl(); - - private: - InstanceImpl( - std::shared_ptr &&instance, - std::string &&jsBundleFile, - std::vector< - std::tuple>> - &&cxxModules, - std::shared_ptr turboModuleRegistry, - std::shared_ptr longLivedObjectCollection, - std::unique_ptr &&callback, - std::shared_ptr jsQueue, - std::shared_ptr nativeQueue, - std::shared_ptr devSettings, - std::shared_ptr devManager); - - std::vector> GetDefaultNativeModules(std::shared_ptr nativeQueue); - void RegisterForReloadIfNecessary() noexcept; - void loadBundleInternal(std::string &&jsBundleRelativePath, bool synchronously); - void SetInError() noexcept; - - private: - std::shared_ptr m_innerInstance; - - std::string m_jsBundleBasePath; - std::shared_ptr m_moduleRegistry; - std::shared_ptr m_turboModuleRegistry; - std::shared_ptr m_longLivedObjectCollection; - std::shared_ptr m_jsThread; - std::shared_ptr m_nativeQueue; - - std::shared_ptr m_devManager; - std::shared_ptr m_devSettings; - bool m_isInError{false}; -}; - -} // namespace react + namespace react { + + struct IDevSupportManager; + struct IReactRootView; + + class InstanceImpl final: public InstanceWrapper, private ::std::enable_shared_from_this { + public: + static std::shared_ptr MakeNoBundle( + std::shared_ptr&& instance, + std::string&& jsBundleBasePath, + std::vector< + std::tuple>> + && cxxModules, + std::shared_ptr turboModuleRegistry, + std::shared_ptr longLivedObjectCollection, + std::unique_ptr&& callback, + std::shared_ptr jsQueue, + std::shared_ptr nativeQueue, + std::shared_ptr devSettings, + std::shared_ptr devManager) noexcept; + + static std::shared_ptr MakeAndLoadBundle( + std::shared_ptr&& instance, + std::string&& jsBundleBasePath, + std::string&& jsBundleRelativePath, + std::vector< + std::tuple>> + && cxxModules, + std::shared_ptr turboModuleRegistry, + std::unique_ptr&& callback, + std::shared_ptr jsQueue, + std::shared_ptr nativeQueue, + std::shared_ptr devSettings, + std::shared_ptr devManager) noexcept; + + // Instance methods + void loadBundle(std::string&& jsBundleRelativePath) override; + void loadBundleSync(std::string&& jsBundleRelativePath) override; + virtual const std::shared_ptr& GetInstance() const noexcept override { + return m_innerInstance; + } + + void DispatchEvent(int64_t viewTag, std::string eventName, folly::dynamic&& eventData) override; + void invokeCallback(const int64_t callbackId, folly::dynamic&& params) override; + + ~InstanceImpl(); + + private: + InstanceImpl( + std::shared_ptr&& instance, + std::string&& jsBundleFile, + std::vector< + std::tuple>> + && cxxModules, + std::shared_ptr turboModuleRegistry, + std::shared_ptr longLivedObjectCollection, + std::unique_ptr&& callback, + std::shared_ptr jsQueue, + std::shared_ptr nativeQueue, + std::shared_ptr devSettings, + std::shared_ptr devManager); + + std::vector> GetDefaultNativeModules(std::shared_ptr nativeQueue); + void RegisterForReloadIfNecessary() noexcept; + void loadBundleInternal(std::string&& jsBundleRelativePath, bool synchronously); + void SetInError() noexcept; + + private: + std::shared_ptr m_innerInstance; + + std::string m_jsBundleBasePath; + std::shared_ptr m_moduleRegistry; + std::shared_ptr m_turboModuleRegistry; + std::shared_ptr m_longLivedObjectCollection; + std::shared_ptr m_jsThread; + std::shared_ptr m_nativeQueue; + + std::shared_ptr m_devManager; + std::shared_ptr m_devSettings; + bool m_isInError{ false }; + }; + + } // namespace react } // namespace facebook diff --git a/vnext/Shared/Shared.vcxitems.filters b/vnext/Shared/Shared.vcxitems.filters index d96476057d9..3976b01efbf 100644 --- a/vnext/Shared/Shared.vcxitems.filters +++ b/vnext/Shared/Shared.vcxitems.filters @@ -152,12 +152,6 @@ Source Files\Modules - - Source Files\Networking - - - Source Files\Modules - @@ -463,24 +457,6 @@ Header Files\Modules - - Header Files\Networking - - - Header Files\Networking - - - Header Files\Networking - - - Header Files\Modules - - - Header Files\Modules - - - Header Files\Modules - From 0c98789b2d93dfbf33233831bdc56a35d486f890 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Tue, 6 Dec 2022 23:15:55 -0800 Subject: [PATCH 03/45] Remove change file --- ...ative-windows-2c5e8e12-8a2e-46b9-8b4f-3f9bdec5faca.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 change/react-native-windows-2c5e8e12-8a2e-46b9-8b4f-3f9bdec5faca.json diff --git a/change/react-native-windows-2c5e8e12-8a2e-46b9-8b4f-3f9bdec5faca.json b/change/react-native-windows-2c5e8e12-8a2e-46b9-8b4f-3f9bdec5faca.json deleted file mode 100644 index 9c6be33fb71..00000000000 --- a/change/react-native-windows-2c5e8e12-8a2e-46b9-8b4f-3f9bdec5faca.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "prerelease", - "comment": "Implement Blob module", - "packageName": "react-native-windows", - "email": "julio.rocha@microsoft.com", - "dependentChangeType": "patch" -} From b43f2d754bcc9969a5f8771ba3fbda5209ffc87b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20C=C3=A9sar=20Rocha?= Date: Thu, 9 Jun 2022 20:07:37 -0700 Subject: [PATCH 04/45] Use logical OR to assert HTTP responseType (#10095) * update yarn.lock * Use logical OR to assert HTTP responseType * Change files --- ...ative-windows-1653495d-e9db-497e-a6e2-bb5fbcc05317.json | 7 +++++++ vnext/Shared/Networking/WinRTHttpResource.cpp | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 change/react-native-windows-1653495d-e9db-497e-a6e2-bb5fbcc05317.json diff --git a/change/react-native-windows-1653495d-e9db-497e-a6e2-bb5fbcc05317.json b/change/react-native-windows-1653495d-e9db-497e-a6e2-bb5fbcc05317.json new file mode 100644 index 00000000000..e825cb14dcb --- /dev/null +++ b/change/react-native-windows-1653495d-e9db-497e-a6e2-bb5fbcc05317.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Use logical OR to assert HTTP responseType", + "packageName": "react-native-windows", + "email": "julio.rocha@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Shared/Networking/WinRTHttpResource.cpp b/vnext/Shared/Networking/WinRTHttpResource.cpp index 8776ddb56ed..da5be8bb48b 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.cpp +++ b/vnext/Shared/Networking/WinRTHttpResource.cpp @@ -73,7 +73,7 @@ namespace Microsoft::React::Networking { bool withCredentials, std::function&& callback) noexcept /*override*/ { // Enforce supported args - assert(responseType == "text" || responseType == "base64" | responseType == "blob"); + assert(responseType == "text" || responseType == "base64" || responseType == "blob"); try { HttpMethod httpMethod{ to_hstring(std::move(method)) }; From 912d248601ca2da23b7a0d95a997e8a92d494b34 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Tue, 6 Dec 2022 23:17:16 -0800 Subject: [PATCH 05/45] Remove change file --- ...ative-windows-1653495d-e9db-497e-a6e2-bb5fbcc05317.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 change/react-native-windows-1653495d-e9db-497e-a6e2-bb5fbcc05317.json diff --git a/change/react-native-windows-1653495d-e9db-497e-a6e2-bb5fbcc05317.json b/change/react-native-windows-1653495d-e9db-497e-a6e2-bb5fbcc05317.json deleted file mode 100644 index e825cb14dcb..00000000000 --- a/change/react-native-windows-1653495d-e9db-497e-a6e2-bb5fbcc05317.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "prerelease", - "comment": "Use logical OR to assert HTTP responseType", - "packageName": "react-native-windows", - "email": "julio.rocha@microsoft.com", - "dependentChangeType": "patch" -} From 14fb864eb15e8f1d30d9e1abe65c0fcc8c43fc4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20C=C3=A9sar=20Rocha?= Date: Mon, 18 Jul 2022 17:13:40 -0700 Subject: [PATCH 06/45] 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 --- ...-42dec614-af90-4b29-b8c7-f266415ea240.json | 7 + .../HttpOriginPolicyIntegrationTest.cpp | 950 ++++++++++-------- .../HttpResourceIntegrationTests.cpp | 109 +- vnext/Shared/Networking/IHttpResource.h | 1 + vnext/Shared/Networking/WinRTHttpResource.cpp | 27 + 5 files changed, 668 insertions(+), 426 deletions(-) create mode 100644 change/react-native-windows-42dec614-af90-4b29-b8c7-f266415ea240.json diff --git a/change/react-native-windows-42dec614-af90-4b29-b8c7-f266415ea240.json b/change/react-native-windows-42dec614-af90-4b29-b8c7-f266415ea240.json new file mode 100644 index 00000000000..5937254e3f5 --- /dev/null +++ b/change/react-native-windows-42dec614-af90-4b29-b8c7-f266415ea240.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Implement HTTP client timeout", + "packageName": "react-native-windows", + "email": "julio.rocha@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp b/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp index e749f07f83d..369b57e6596 100644 --- a/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp +++ b/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp @@ -134,10 +134,54 @@ namespace Microsoft::React::Test { { clientArgs.ResponseContent = std::move(content); clientArgs.ContentPromise.set_value(); - }); - resource->SetOnError([&clientArgs](int64_t, string&& message, bool) - { - clientArgs.ErrorMessage = std::move(message); + }); + + resource->SendRequest( + string{http::to_string(clientArgs.Method).data()}, + string{server1Args.Url}, + 0, /*requestId*/ + std::move(clientArgs.RequestHeaders), + {}, /*data*/ + "text", + false, /*useIncrementalUpdates*/ + 0, /*timeout*/ + clientArgs.WithCredentials, /*withCredentials*/ + [](int64_t){} /*reactCallback*/ + ); + + clientArgs.ContentPromise.get_future().wait(); + + server2->Stop(); + server1->Stop(); + + if (shouldSucceed) + { + Assert::AreEqual({}, clientArgs.ErrorMessage); + //TODO: chose server? + // We assume 2-server tests will always redirect so the final response will come from server 2. + Assert::AreEqual(server2Args.Response.result_int(), static_cast(clientArgs.Response.StatusCode)); + Assert::AreEqual({"RESPONSE_CONTENT"}, clientArgs.ResponseContent); + } + else + { + Assert::AreNotEqual({}, clientArgs.ErrorMessage); + } + } + + void TestOriginPolicy(ServerParams& serverArgs, ClientParams& clientArgs, bool shouldSucceed) + { + auto server = CreateServer(serverArgs, clientArgs); + + server->Start(); + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&clientArgs](int64_t, IHttpResource::Response&& res) + { + clientArgs.Response = std::move(res); + }); + resource->SetOnData([&clientArgs](int64_t, string&& content) + { + clientArgs.ResponseContent = std::move(content); clientArgs.ContentPromise.set_value(); }); @@ -195,18 +239,18 @@ namespace Microsoft::React::Test { clientArgs.ContentPromise.set_value(); }); - resource->SendRequest( - string{ http::to_string(clientArgs.Method).data() }, - string{ serverArgs.Url }, - 0, /*requestId*/ - std::move(clientArgs.RequestHeaders), - {}, /*data*/ - "text", - false, /*useIncrementalUpdates*/ - 0, /*timeout*/ - clientArgs.WithCredentials, /*withCredentials*/ - [](int64_t) {} /*reactCallback*/ - ); + resource->SendRequest( + string{http::to_string(clientArgs.Method).data()}, + string{serverArgs.Url}, + 0, /*requestId*/ + std::move(clientArgs.RequestHeaders), + {}, /*data*/ + "text", + false, /*useIncrementalUpdates*/ + 0, /*timeout*/ + clientArgs.WithCredentials, /*withCredentials*/ + [](int64_t) {} /*reactCallback*/ + ); clientArgs.ContentPromise.get_future().wait(); server->Stop(); @@ -319,512 +363,576 @@ namespace Microsoft::React::Test { END_TEST_METHOD_ATTRIBUTE() TEST_METHOD(SimpleCorsForbiddenMethodFails) { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); - - ClientParams clientArgs(http::verb::connect, { {"Content-Type", "text/plain"} }); - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); + StringResponse response; + response.result(http::status::ok); + response.body() = "GET_CONTENT"; - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// SimpleCorsForbiddenMethodFails - - //NoCors_ForbiddenMethodConnect_Failed - - BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsCrossOriginFetchRequestSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(NoCorsCrossOriginFetchRequestSucceeds) - { - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); + return { std::move(response) }; + }; + server->Start(); - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::ok); + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&getResponse](int64_t, IHttpResource::Response&& res) + { + getResponse = std::move(res); + }); + resource->SetOnData([&getDataPromise, &getContent](int64_t, string&& content) + { + getContent = std::move(content); + getDataPromise.set_value(); + }); + resource->SetOnError([&server, &error, &getDataPromise](int64_t, string&& message) + { + error = std::move(message); + getDataPromise.set_value(); + }); + + resource->SendRequest( + "TRACE", + url, + 0, /*requestId*/ + { + {"ValidHeader", "AnyValue"} + }, + {}, /*data*/ + //{} /*bodyData*/, + "text", + false /*useIncrementalUpdates*/, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {} /*callback*/ + ); + + getDataPromise.get_future().wait(); + server->Stop(); + + Assert::AreEqual({}, error); + Assert::AreEqual(200, static_cast(getResponse.StatusCode)); + Assert::AreEqual({ "GET_CONTENT" }, getContent); + }// NoCorsForbiddenMethodSucceeds + + BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsForbiddenMethodFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(SimpleCorsForbiddenMethodFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); - ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); + ClientParams clientArgs(http::verb::connect, {{"Content-Type", "text/plain"}}); - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// NoCorsCrossOriginFetchRequestSucceeds + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// SimpleCorsForbiddenMethodFails - BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsCrossOriginPatchSucceededs) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(NoCorsCrossOriginPatchSucceededs) - { - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); + //NoCors_ForbiddenMethodConnect_Failed - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::ok); + BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsCrossOriginFetchRequestSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(NoCorsCrossOriginFetchRequestSucceeds) + { + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); - ClientParams clientArgs(http::verb::patch, { { "Content-Type", "text/plain" } }); + ServerParams serverArgs(s_port); + serverArgs.Response.result(http::status::ok); - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// NoCorsCrossOriginPatchSucceededs + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); - // Simple-Cors — Prevents the method from being anything other than HEAD, GET or POST, - // and the headers from being anything other than simple headers (CORS safe listed headers). - // If any ServiceWorkers intercept these requests, they may not add or override any headers except for those that are simple headers. - // In addition, JavaScript may not access any properties of the resulting Response. - // This ensures that ServiceWorkers do not affect the semantics of the Web and prevents security and privacy issues arising from leaking data across domains. - BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsSameOriginSucceededs) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(SimpleCorsSameOriginSucceededs) - { - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::ok); + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// NoCorsCrossOriginFetchRequestSucceeds - ClientParams clientArgs(http::verb::patch, { { "Content-Type", "text/plain" } }); + //NoCors_CrossOriginFetchRequestWithTimeout_Succeeded //TODO: Implement timeout - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// SimpleCorsSameOriginSucceededs + BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsCrossOriginPatchSucceededs) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(NoCorsCrossOriginPatchSucceededs) + { + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); - BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsCrossOriginFetchFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(SimpleCorsCrossOriginFetchFails) - { - ServerParams serverArgs(s_port); + ServerParams serverArgs(s_port); + serverArgs.Response.result(http::status::ok); - ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/html" } }); // text/html is a non-simple value + ClientParams clientArgs(http::verb::patch, {{ "Content-Type", "text/plain" }}); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// NoCorsCrossOriginPatchSucceededs - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// SimpleCorsCrossOriginFetchFails + // Simple-Cors — Prevents the method from being anything other than HEAD, GET or POST, + // and the headers from being anything other than simple headers (CORS safe listed headers). + // If any ServiceWorkers intercept these requests, they may not add or override any headers except for those that are simple headers. + // In addition, JavaScript may not access any properties of the resulting Response. + // This ensures that ServiceWorkers do not affect the semantics of the Web and prevents security and privacy issues arising from leaking data across domains. + BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsSameOriginSucceededs) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(SimpleCorsSameOriginSucceededs) + { + ServerParams serverArgs(s_port); + serverArgs.Response.result(http::status::ok); - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginRequestSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsSameOriginRequestSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::ok); + ClientParams clientArgs(http::verb::patch, {{ "Content-Type", "text/plain" }}); - ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); // text/plain is a non-simple header + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// SimpleCorsSameOriginSucceededs - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsCrossOriginFetchFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(SimpleCorsCrossOriginFetchFails) + { + ServerParams serverArgs(s_port); - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// FullCorsSameOriginRequestSucceeds + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/html" }}); // text/html is a non-simple value - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginAllowOriginWildcardSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginAllowOriginWildcardSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, "*"); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Response.result(http::status::accepted); - serverArgs.Response.set(http::field::access_control_allow_origin, "*"); - serverArgs.Response.set(http::field::access_control_allow_credentials, "true"); - - ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); // text/plain is a non-simple header - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// FullCorsCrossOriginAllowOriginWildcardSucceeds - - // With CORS, Cross-Origin Resource Sharing, the server can decide what origins are permitted to read information from the client. - // Additionally, for non-simple requests, client should preflight the request through the HTTP Options request, and only send the - // actual request after the server has responded that the desired headers are supported. - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMatchingOriginSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginMatchingOriginSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Response.result(http::status::accepted); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Response.set(http::field::access_control_allow_credentials, "true"); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// SimpleCorsCrossOriginFetchFails - ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); // text/plain is a non-simple header + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginRequestSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsSameOriginRequestSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Response.result(http::status::ok); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); // text/plain is a non-simple header - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// FullCorsCrossOriginMatchingOriginSucceeds + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// FullCorsSameOriginRequestSucceeds - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginWithCredentialsFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginWithCredentialsFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Preflight.set(http::field::access_control_allow_credentials, "true"); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginAllowOriginWildcardSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginAllowOriginWildcardSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, "*"); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Response.result(http::status::accepted); + serverArgs.Response.set(http::field::access_control_allow_origin, "*"); + serverArgs.Response.set(http::field::access_control_allow_credentials, "true"); + + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); // text/plain is a non-simple header + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// FullCorsCrossOriginAllowOriginWildcardSucceeds + + // With CORS, Cross-Origin Resource Sharing, the server can decide what origins are permitted to read information from the client. + // Additionally, for non-simple requests, client should preflight the request through the HTTP Options request, and only send the + // actual request after the server has responded that the desired headers are supported. + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMatchingOriginSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginMatchingOriginSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Response.result(http::status::accepted); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Response.set(http::field::access_control_allow_credentials, "true"); - ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); // application/text is a non-simple header - clientArgs.WithCredentials = true; + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); // text/plain is a non-simple header - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - SetRuntimeOptionBool("Http.OmitCredentials", true); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// FullCorsCrossOriginWithCredentialsFails + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// FullCorsCrossOriginMatchingOriginSucceeds + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginWithCredentialsFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginWithCredentialsFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Preflight.set(http::field::access_control_allow_credentials, "true"); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMissingCorsHeadersFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginMissingCorsHeadersFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.erase(http::field::access_control_allow_methods); - serverArgs.Preflight.result(http::status::not_implemented); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); // application/text is a non-simple header + clientArgs.WithCredentials = true; - ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); // application/text is a non-simple header + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + SetRuntimeOptionBool("Http.OmitCredentials", true); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// FullCorsCrossOriginWithCredentialsFails - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// FullCorsCrossOriginMissingCorsHeadersFails - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMismatchedCorsHeaderFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginMismatchedCorsHeaderFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Response.result(http::status::accepted); - serverArgs.Response.set(http::field::access_control_allow_origin, "http://other.example.rnw"); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMissingCorsHeadersFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginMissingCorsHeadersFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.erase(http::field::access_control_allow_methods); + serverArgs.Preflight.result(http::status::not_implemented); - ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); // application/text is a non-simple header + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); // application/text is a non-simple header - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// FullCorsCrossOriginMismatchedCorsHeaderFails + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// FullCorsCrossOriginMissingCorsHeadersFails - // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginCheckFailsOnPreflightRedirectFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginCheckFailsOnPreflightRedirectFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Preflight.set(http::field::location, "http://any-host.extension"); - serverArgs.Preflight.result(http::status::moved_permanently); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMismatchedCorsHeaderFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginMismatchedCorsHeaderFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Response.result(http::status::accepted); + serverArgs.Response.set(http::field::access_control_allow_origin, "http://other.example.rnw"); - ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); // application/text is a non-simple header - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// FullCorsCrossOriginCheckFailsOnPreflightRedirectFails + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// FullCorsCrossOriginMismatchedCorsHeaderFails - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCorsCheckFailsOnResponseRedirectFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCorsCheckFailsOnResponseRedirectFails) - { - ServerParams serverArgs(s_port); + // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginCheckFailsOnPreflightRedirectFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginCheckFailsOnPreflightRedirectFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Preflight.set(http::field::location, "http://any-host.extension"); + serverArgs.Preflight.result(http::status::moved_permanently); - // server1 allowed origin header includes http://example.com - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); - // This is a CORS request to server1, but server1 redirects the request to server2 - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - // Redir server's URL - serverArgs.Response.set(http::field::location, "http://localhost:6666"); - serverArgs.Response.set(http::field::server, "BaseServer"); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - // Server2 does not set Access-Control-Allow-Origin for GET requests - ServerParams redirServerArgs(++s_port); - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::server, "RedirectServer"); + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// FullCorsCrossOriginCheckFailsOnPreflightRedirectFails - ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCorsCheckFailsOnResponseRedirectFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCorsCheckFailsOnResponseRedirectFails) + { + ServerParams serverArgs(s_port); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + // server1 allowed origin header includes http://example.com + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); - }// FullCorsCorsCheckFailsOnResponseRedirectFails + // This is a CORS request to server1, but server1 redirects the request to server2 + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + // Redir server's URL + serverArgs.Response.set(http::field::location, "http://localhost:6666"); + serverArgs.Response.set(http::field::server, "BaseServer"); + // Server2 does not set Access-Control-Allow-Origin for GET requests + ServerParams redirServerArgs(++s_port); + redirServerArgs.Response.result(http::status::accepted); + redirServerArgs.Response.set(http::field::server, "RedirectServer"); - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginToSameOriginRedirectSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsSameOriginToSameOriginRedirectSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::location, serverArgs.Url); - serverArgs.Response.result(http::status::accepted); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); - ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); + }// FullCorsCorsCheckFailsOnResponseRedirectFails - TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); - } // FullCorsSameOriginToSameOriginRedirectSucceeds - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginToCrossOriginRedirectSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsSameOriginToCrossOriginRedirectSucceeds) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginToSameOriginRedirectSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsSameOriginToSameOriginRedirectSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::location, serverArgs.Url); + serverArgs.Response.result(http::status::accepted); - serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::access_control_allow_origin, serverArgs.Url); + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); + TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); + } // FullCorsSameOriginToSameOriginRedirectSucceeds - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginToCrossOriginRedirectSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsSameOriginToCrossOriginRedirectSucceeds) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); - } // FullCorsSameOriginToCrossOriginRedirectSucceeds + serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, redirServerArgs.Url); - //TODO: Seems to redirect to exact same resource. Implement second resource in same server. - // Redirects a cross origin request to cross origin request on the same server - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToCrossOriginRedirectSucceeds) - TEST_IGNORE() - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToCrossOriginRedirectSucceeds) - { - ServerParams serverArgs(s_port); - //ServerParams redirServerArgs(++s_port); + redirServerArgs.Response.result(http::status::accepted); + redirServerArgs.Response.set(http::field::access_control_allow_origin, serverArgs.Url); - serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, serverArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); - //redirServerArgs.Response.result(http::status::accepted); - //redirServerArgs.Response.set(http::field::access_control_allow_origin, serverArgs.Url); + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); + } // FullCorsSameOriginToCrossOriginRedirectSucceeds - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + //TODO: Seems to redirect to exact same resource. Implement second resource in same server. + // Redirects a cross origin request to cross origin request on the same server + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToCrossOriginRedirectSucceeds) + TEST_IGNORE() + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToCrossOriginRedirectSucceeds) + { + ServerParams serverArgs(s_port); + //ServerParams redirServerArgs(++s_port); - TestOriginPolicy(serverArgs, /*redirServerArgs, */clientArgs, s_shouldSucceed); - } // FullCorsCrossOriginToCrossOriginRedirectSucceeds + serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, serverArgs.Url); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - // The initial request gets redirected back to the original origin, - // but it will lack the Access-Control-Allow-Origin header. - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToOriginalOriginRedirectFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToOriginalOriginRedirectFails) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + //redirServerArgs.Response.result(http::status::accepted); + //redirServerArgs.Response.set(http::field::access_control_allow_origin, serverArgs.Url); - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, redirServerArgs.Url); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); - ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - SetRuntimeOptionString("Http.GlobalOrigin", redirServerArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + TestOriginPolicy(serverArgs, /*redirServerArgs, */clientArgs, s_shouldSucceed); + } // FullCorsCrossOriginToCrossOriginRedirectSucceeds - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); - } // FullCorsCrossOriginToOriginalOriginRedirectFails + // The initial request gets redirected back to the original origin, + // but it will lack the Access-Control-Allow-Origin header. + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToOriginalOriginRedirectFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToOriginalOriginRedirectFails) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); - // Redirects cross origin request to server1 to cross origin request to server2 - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, redirServerArgs.Url); + serverArgs.Response.set(http::field::access_control_allow_origin, redirServerArgs.Url); - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::access_control_allow_origin, "*"); + SetRuntimeOptionString("Http.GlobalOrigin", redirServerArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); + } // FullCorsCrossOriginToOriginalOriginRedirectFails - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + // Redirects cross origin request to server1 to cross origin request to server2 + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); - } // FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, redirServerArgs.Url); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + redirServerArgs.Response.result(http::status::accepted); + redirServerArgs.Response.set(http::field::access_control_allow_origin, "*"); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - // server1 redirects the GET request to server2 - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); - // Since redirect tainted the origin, the server has to allow all origins for CORS to succeed - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::access_control_allow_origin, "*"); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - // PATCH is not a simple method, so preflight is required for server1 - ClientParams clientArgs(http::verb::patch, { { "Content-Type", "text/plain" } }); + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); + } // FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + 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) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); - } // FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + // server1 redirects the GET request to server2 + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, redirServerArgs.Url); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + // Since redirect tainted the origin, the server has to allow all origins for CORS to succeed + redirServerArgs.Response.result(http::status::accepted); + redirServerArgs.Response.set(http::field::access_control_allow_origin, "*"); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - // server1 redirects the GET request to server2 - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + // PATCH is not a simple method, so preflight is required for server1 + ClientParams clientArgs(http::verb::patch, {{ "Content-Type", "text/plain" }}); - // Since redirect tainted the origin, the server does not know what origin to allow through a single value. - // Even if server successfully guessed the single value, it will still fail on the client side. - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - // PATCH is not a simple method, so preflight is required for server1 - ClientParams clientArgs(http::verb::patch, { { "Content-Type", "text/plain" } }); + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); + } // FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); + + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + // server1 redirects the GET request to server2 + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, redirServerArgs.Url); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + + // Since redirect tainted the origin, the server does not know what origin to allow through a single value. + // Even if server successfully guessed the single value, it will still fail on the client side. + redirServerArgs.Response.result(http::status::accepted); + redirServerArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + + // PATCH is not a simple method, so preflight is required for server1 + ClientParams clientArgs(http::verb::patch, {{ "Content-Type", "text/plain" }}); + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); + } // FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails + + BEGIN_TEST_METHOD_ATTRIBUTE(FullCors304ForSimpleGetFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCors304ForSimpleGetFails) + { + ServerParams serverArgs(s_port); + serverArgs.Response.result(http::status::not_modified); - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); - } // FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails + // PATCH is not a simple method, so preflight is required for server1 + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); - BEGIN_TEST_METHOD_ATTRIBUTE(FullCors304ForSimpleGetFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCors304ForSimpleGetFails) - { - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::not_modified); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - // PATCH is not a simple method, so preflight is required for server1 - ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + } // FullCors304ForSimpleGetFails - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + TEST_METHOD(FullCorsPreflightSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "ArbitraryHeader"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "ArbitraryHeader"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Response.result(http::status::ok); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + + ClientParams clientArgs(http::verb::get, { {"Content-Type", "text/plain"}, {"ArbitraryHeader", "AnyValue"} }); + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); + }// FullCorsPreflightSucceeds + + // 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) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Preflight.set(http::field::access_control_allow_credentials, "true"); + serverArgs.Response.result(http::status::accepted); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); // application/text is a non-simple header + clientArgs.WithCredentials = true; + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); + }// FullCorsCrossOriginWithCredentialsSucceeds + + // "Host" is one of the forbidden headers for fetch + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsRequestWithHostHeaderFails) + // "Host" is not an accepted request header in WinRT. + TEST_IGNORE() + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsRequestWithHostHeaderFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Response.result(http::status::accepted); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - } // FullCors304ForSimpleGetFails + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }, { "Host", "http://sub.example.rnw" }}); - TEST_METHOD(FullCorsPreflightSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "ArbitraryHeader"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "ArbitraryHeader"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Response.result(http::status::ok); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - ClientParams clientArgs(http::verb::get, { {"Content-Type", "text/plain"}, {"ArbitraryHeader", "AnyValue"} }); - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); - }// FullCorsPreflightSucceeds - - // 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) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginWithCredentialsSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Preflight.set(http::field::access_control_allow_credentials, "true"); - serverArgs.Response.result(http::status::accepted); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); // application/text is a non-simple header - clientArgs.WithCredentials = true; - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); - }// FullCorsCrossOriginWithCredentialsSucceeds - - // "Host" is one of the forbidden headers for fetch - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsRequestWithHostHeaderFails) - // "Host" is not an accepted request header in WinRT. - TEST_IGNORE() - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsRequestWithHostHeaderFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Response.result(http::status::accepted); + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" }, { "Host", "http://sub.example.rnw" } }); + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + Assert::Fail(L"FIX!!! Passes for the worng reason. Error: 0x80070057 : 'Invalid HTTP headers.'"); + }// FullCorsRequestWithHostHeaderFails - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + BEGIN_TEST_METHOD_ATTRIBUTE(RequestWithProxyAuthorizationHeaderFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(RequestWithProxyAuthorizationHeaderFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Response.result(http::status::accepted); - Assert::Fail(L"FIX!!! Passes for the worng reason. Error: 0x80070057 : 'Invalid HTTP headers.'"); - }// FullCorsRequestWithHostHeaderFails + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }, { "Proxy-Authorization", "Basic Zm9vOmJhcg==" }}); - BEGIN_TEST_METHOD_ATTRIBUTE(RequestWithProxyAuthorizationHeaderFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(RequestWithProxyAuthorizationHeaderFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Response.result(http::status::accepted); + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" }, { "Proxy-Authorization", "Basic Zm9vOmJhcg==" } }); + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// RequestWithProxyAuthorizationHeaderFails - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + BEGIN_TEST_METHOD_ATTRIBUTE(ExceedingRedirectLimitFails) + TEST_IGNORE() + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(ExceedingRedirectLimitFails) + { + Assert::Fail(L"NOT IMPLEMENTED"); + }// ExceedingRedirectLimitFails - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// RequestWithProxyAuthorizationHeaderFails - }; +}; - uint16_t HttpOriginPolicyIntegrationTest::s_port = 7777; +uint16_t HttpOriginPolicyIntegrationTest::s_port = 7777; }//namespace Microsoft::React::Test // clang-format on diff --git a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp index 5ad55e8003a..772681d214b 100644 --- a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp +++ b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp @@ -295,7 +295,7 @@ namespace Microsoft::React::Test { 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(); @@ -475,10 +475,109 @@ namespace Microsoft::React::Test { server2->Stop(); server1->Stop(); - Assert::AreEqual({}, error, L"Error encountered"); - Assert::AreEqual(static_cast(200), responseResult.StatusCode); - Assert::AreEqual({ "Redirect Content" }, content); - } + Assert::AreEqual({}, error, L"Error encountered"); + Assert::AreEqual(static_cast(200), getResponse.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; + + 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](int64_t, string &&errorMessage) { + error = std::move(errorMessage); + 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::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; + + 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](int64_t, string &&errorMessage) { + error = std::move(errorMessage); + 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::AreEqual({"[0x800705b4] This operation returned because the timeout period expired."}, error); + Assert::AreEqual(0, statusCode); + } +}; TEST_METHOD(SimpleRedirectPatchSucceeds) { auto port1 = s_port; diff --git a/vnext/Shared/Networking/IHttpResource.h b/vnext/Shared/Networking/IHttpResource.h index b5d949922b5..a64999ad76d 100644 --- a/vnext/Shared/Networking/IHttpResource.h +++ b/vnext/Shared/Networking/IHttpResource.h @@ -71,6 +71,7 @@ namespace Microsoft::React::Networking { /// /// /// Request timeout in miliseconds. + /// Note: A value of 0 means no timeout. The resource will await the response indefinitely. /// /// /// Allow including credentials in request. diff --git a/vnext/Shared/Networking/WinRTHttpResource.cpp b/vnext/Shared/Networking/WinRTHttpResource.cpp index da5be8bb48b..e6cc9e075d9 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.cpp +++ b/vnext/Shared/Networking/WinRTHttpResource.cpp @@ -305,6 +305,33 @@ WinRTHttpResource::PerformSendRequest(HttpMethod&& method, Uri&& rtUri, IInspect co_await lessthrow_await_adapter{sendRequestOp}; } + if (coReqArgs->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, coReqArgs->Timeout); + + 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, Utilities::HResultToString(HRESULT_FROM_WIN32(ERROR_TIMEOUT))); + } + co_return self->UntrackResponse(coReqArgs->RequestId); + } + } + else { + co_await lessthrow_await_adapter{sendRequestOp}; + } + auto result = sendRequestOp.ErrorCode(); if (result < 0) { if (self->m_onError) { From 3749f1388bb3eeca94a0d5967ea80b831e40f5d2 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Tue, 6 Dec 2022 23:18:57 -0800 Subject: [PATCH 07/45] Remove change file --- ...ative-windows-42dec614-af90-4b29-b8c7-f266415ea240.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 change/react-native-windows-42dec614-af90-4b29-b8c7-f266415ea240.json diff --git a/change/react-native-windows-42dec614-af90-4b29-b8c7-f266415ea240.json b/change/react-native-windows-42dec614-af90-4b29-b8c7-f266415ea240.json deleted file mode 100644 index 5937254e3f5..00000000000 --- a/change/react-native-windows-42dec614-af90-4b29-b8c7-f266415ea240.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "prerelease", - "comment": "Implement HTTP client timeout", - "packageName": "react-native-windows", - "email": "julio.rocha@microsoft.com", - "dependentChangeType": "patch" -} From 7e8bff3a286a4ff3a75a766ab4adad204b94e8ab Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Fri, 22 Jul 2022 20:06:55 -0400 Subject: [PATCH 08/45] 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 --- ...-195fdcc2-1b14-49aa-a0eb-06763be2eed8.json | 7 + vnext/Shared/Modules/BlobModule.cpp | 95 ++++++- vnext/Shared/Modules/BlobModule.h | 2 +- vnext/Shared/Modules/FileReaderModule.cpp | 68 ++++- vnext/Shared/Modules/IBlobPersistor.h | 2 +- vnext/Shared/Networking/WinRTHttpResource.cpp | 42 ++++ vnext/src/IntegrationTests/BlobTest.js | 233 +++++++++--------- 7 files changed, 318 insertions(+), 131 deletions(-) create mode 100644 change/react-native-windows-195fdcc2-1b14-49aa-a0eb-06763be2eed8.json diff --git a/change/react-native-windows-195fdcc2-1b14-49aa-a0eb-06763be2eed8.json b/change/react-native-windows-195fdcc2-1b14-49aa-a0eb-06763be2eed8.json new file mode 100644 index 00000000000..0d5014c93c4 --- /dev/null +++ b/change/react-native-windows-195fdcc2-1b14-49aa-a0eb-06763be2eed8.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Use uint8_t const in IBlobPersistor.h", + "packageName": "react-native-windows", + "email": "erozell@outlook.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Shared/Modules/BlobModule.cpp b/vnext/Shared/Modules/BlobModule.cpp index 1c4927f9187..9bb2c4f9f45 100644 --- a/vnext/Shared/Modules/BlobModule.cpp +++ b/vnext/Shared/Modules/BlobModule.cpp @@ -97,10 +97,87 @@ namespace Microsoft::React { responseHandler = m_responseHandler](dynamic args) { auto propId = ReactPropertyId>>{L"HttpModule.Proxy"}; - if (auto prop = propBag.Get(propId)) { - if (auto httpHandler = prop.Value().lock()) { - httpHandler->AddRequestBodyHandler(requestBodyHandler); - httpHandler->AddResponseHandler(responseHandler); + if (auto prop = propBag.Get(propId)) { + if (auto httpHandler = prop.Value().lock()) { + httpHandler->AddRequestBodyHandler(requestBodyHandler); + httpHandler->AddResponseHandler(responseHandler); + } + } + // TODO: else emit error? + }}, + + {"addWebSocketHandler", + [contentHandler = m_contentHandler](dynamic args) { + auto id = jsArgAsInt(args, 0); + + contentHandler->Register(id); + }}, + + {"removeWebSocketHandler", + [contentHandler = m_contentHandler](dynamic args) { + auto id = jsArgAsInt(args, 0); + + contentHandler->Unregister(id); + }}, + + {"sendOverSocket", + [weakState = weak_ptr(m_sharedState), + persistor = m_blobPersistor, + propBag = ReactPropertyBag{m_inspectableProperties.try_as()}](dynamic args) { + auto propId = ReactPropertyId>>{L"WebSocketModule.Proxy"}; + shared_ptr wsProxy; + if (auto prop = propBag.Get(propId)) { + wsProxy = prop.Value().lock(); + } + if (!wsProxy) { + return; + } + + auto blob = jsArgAsObject(args, 0); + auto blobId = blob[blobIdKey].getString(); + auto offset = blob[offsetKey].getInt(); + auto size = blob[sizeKey].getInt(); + auto socketID = jsArgAsInt(args, 1); + + winrt::array_view data; + try { + data = persistor->ResolveMessage(std::move(blobId), offset, size); + } catch (const std::exception &e) { + if (auto sharedState = weakState.lock()) { + Modules::SendEvent(sharedState->Module->getInstance(), "blobFailed", e.what()); + } + return; + } + + auto buffer = CryptographicBuffer::CreateFromByteArray(data); + auto winrtString = CryptographicBuffer::EncodeToBase64String(std::move(buffer)); + auto base64String = Common::Unicode::Utf16ToUtf8(std::move(winrtString)); + + wsProxy->SendBinary(std::move(base64String), socketID); + }}, + + {"createFromParts", + // As of React Native 0.67, instance is set AFTER CxxModule::getMethods() is invoked. + // Use getInstance() directly once + // https://github.com/facebook/react-native/commit/1d45b20b6c6ba66df0485cdb9be36463d96cf182 becomes available. + [persistor = m_blobPersistor, weakState = weak_ptr(m_sharedState)](dynamic args) { + auto parts = jsArgAsArray(args, 0); // Array + auto blobId = jsArgAsString(args, 1); + vector buffer{}; + + for (const auto &part : parts) { + auto type = part[typeKey].asString(); + if (blobKey == type) { + auto blob = part[dataKey]; + winrt::array_view bufferPart; + try { + bufferPart = persistor->ResolveMessage( + blob[blobIdKey].asString(), blob[offsetKey].asInt(), blob[sizeKey].asInt()); + } catch (const std::exception &e) { + if (auto sharedState = weakState.lock()) { + Modules::SendEvent(sharedState->Module->getInstance(), "blobFailed", e.what()); + } + return; } } // TODO: else emit error? @@ -220,9 +297,9 @@ persistor->StoreMessage(std::move(buffer), std::move(blobId)); #pragma region IBlobPersistor - winrt::array_view MemoryBlobPersistor::ResolveMessage(string&& blobId, int64_t offset, int64_t size) { - if (size < 1) - return {}; +winrt::array_view MemoryBlobPersistor::ResolveMessage(string &&blobId, int64_t offset, int64_t size) { + if (size < 1) + return {}; scoped_lock lock{ m_mutex }; @@ -237,8 +314,8 @@ persistor->StoreMessage(std::move(buffer), std::move(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 { scoped_lock lock{ m_mutex }; diff --git a/vnext/Shared/Modules/BlobModule.h b/vnext/Shared/Modules/BlobModule.h index d9a819b404e..f27ab2d8595 100644 --- a/vnext/Shared/Modules/BlobModule.h +++ b/vnext/Shared/Modules/BlobModule.h @@ -30,7 +30,7 @@ namespace Microsoft::React { 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 14c81f00758..66c2ac79243 100644 --- a/vnext/Shared/Modules/FileReaderModule.cpp +++ b/vnext/Shared/Modules/FileReaderModule.cpp @@ -130,9 +130,71 @@ return reject({e.what()}); // See https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/charset/Charset.html auto result = string{bytes.cbegin(), bytes.cend()}; -resolve({std::move(result)}); -}} }; - } + auto blob = jsArgAsObject(args, 0); + + auto blobId = blob["blobId"].asString(); + auto offset = blob["offset"].asInt(); + auto size = blob["size"].asInt(); + + winrt::array_view bytes; + try { + bytes = blobPersistor->ResolveMessage(std::move(blobId), offset, size); + } catch (const std::exception &e) { + return reject({e.what()}); + } + + auto result = string{"data:"}; + auto typeItr = blob.find("type"); + if (typeItr == blob.items().end()) { + result += "application/octet-stream"; + } else { + result += (*typeItr).second.asString(); + } + result += ";base64,"; + + // https://www.boost.org/doc/libs/1_76_0/libs/serialization/doc/dataflow.html + using namespace boost::archive::iterators; + typedef base64_from_binary> encode_base64; + std::ostringstream oss; + std::copy(encode_base64(bytes.cbegin()), encode_base64(bytes.cend()), ostream_iterator(oss)); + result += oss.str(); + + resolve({std::move(result)}); + }}, + {/// + /// + /// Array of arguments passed from the JavaScript layer. + /// [0] - dynamic blob object { blobId, offset, size } + /// [1] - string encoding + /// + /// + "readAsText", + [blobPersistor = m_weakBlobPersistor.lock()](dynamic args, Callback resolve, Callback reject) { + if (!blobPersistor) { + return reject({"Could not find Blob persistor"}); + } + + auto blob = jsArgAsObject(args, 0); + auto encoding = jsArgAsString(args, 1); // Default: "UTF-8" + + auto blobId = blob["blobId"].asString(); + auto offset = blob["offset"].asInt(); + auto size = blob["size"].asInt(); + + winrt::array_view bytes; + try { + bytes = blobPersistor->ResolveMessage(std::move(blobId), offset, size); + } catch (const std::exception &e) { + return reject({e.what()}); + } + + // #9982 - Handle non-UTF8 encodings + // See https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/charset/Charset.html + auto result = string{bytes.cbegin(), bytes.cend()}; + + resolve({std::move(result)}); + }}}; +} #pragma endregion CxxModule diff --git a/vnext/Shared/Modules/IBlobPersistor.h b/vnext/Shared/Modules/IBlobPersistor.h index 638f09e8bea..d6518a6c763 100644 --- a/vnext/Shared/Modules/IBlobPersistor.h +++ b/vnext/Shared/Modules/IBlobPersistor.h @@ -18,7 +18,7 @@ namespace Microsoft::React { /// 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/Networking/WinRTHttpResource.cpp b/vnext/Shared/Networking/WinRTHttpResource.cpp index e6cc9e075d9..24d1c2ff429 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.cpp +++ b/vnext/Shared/Networking/WinRTHttpResource.cpp @@ -256,6 +256,48 @@ WinRTHttpResource::PerformSendRequest(HttpMethod&& method, Uri&& rtUri, IInspect self->m_onDataDynamic(reqArgs->RequestId, std::move(blob)); self->m_onRequestSuccess(reqArgs->RequestId); } + co_return; + } + 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""}; + } + } + + 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; } diff --git a/vnext/src/IntegrationTests/BlobTest.js b/vnext/src/IntegrationTests/BlobTest.js index a9a87a365d5..4070b8f8176 100644 --- a/vnext/src/IntegrationTests/BlobTest.js +++ b/vnext/src/IntegrationTests/BlobTest.js @@ -16,7 +16,7 @@ const {TestModule} = ReactNative.NativeModules; type State = { statusCode: number, xhr: XMLHttpRequest, - expected: String, + expected: string, }; class BlobTest extends React.Component<{...}, State> { @@ -24,123 +24,122 @@ class BlobTest extends React.Component<{...}, State> { statusCode: 0, xhr: new XMLHttpRequest(), // https://www.facebook.com/favicon.ico - expected: new String( + expected: 'data:application/octet-stream;base64,' + - 'AAABAAIAEBAAAAEAIABoBAAAJgAAACAgAAABACAAqBAAAI4EAAAoAAAAEAAAACAA' + - 'AAABACAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOFl' + - 'BiviZgKP4WYB1f//////////4WUA1eJmAI/hawYrAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAA/4ArBuNpA5TkawP942kC/+NpAv///////////+NpAv/jaQL/5GoD/eNp' + - 'A5T/gCsGAAAAAAAAAAAAAAAA/4ArBuVvBL3lbgT/5W4E/+VuBP/lbgT/////////' + - '///lbgT/5W4E/+VuBP/lbgT/5W8Evf+AKwYAAAAAAAAAAOlzBZTncwX/53MF/+dz' + - 'Bf/ncwX/53MF////////////53MF/+dzBf/ncwb/53MF/+dzBv/pcweUAAAAAO19' + - 'DCvpeAf96HcH/+l4B//odwf/6XgH/+l4B////////////+h3B//odwf/6XgH/+h3' + - 'B//peAf/6nkH/e19DCvrfQmP630J/+t9Cf/rfAn/630J/+t8Cf/rfAn/////////' + - '///rfQn/630J/+p8CP/rfQn/6nwI/+p8CP/rfQuP7YEL1e2BCv/tgQr/7IEK/+2B' + - 'Cv/////////////////////////////////uiRj/7IEK/+2CCv/tggr/7YIM1e6G' + - 'DfPvhgz/74YM/++HDP/vhgz/////////////////////////////////8Zw4/++G' + - 'DP/uhgz/7oYM/+6GDPPwiw7z8IsN//CLDf/wiw3/8IsN//CLDf/wiw3/////////' + - '///wig3/8IoN//CLDf/wig3/8IsN//CLDf/xjA/z85EQ1fOQD//zkA//85AP//OQ' + - 'D//zkA//85AP////////////8o8P//KPD//ykA//8o8P//KQD//ykA//85EQ1fSU' + - 'EI/1lRH/9ZUR//SUEP/1lRH/9JQQ//SUEP/+9uz///////jDev/0mRz/9ZUR//SV' + - 'Ef/1lRH/9ZUR//SUEI/5mhgr95kS/faZEv/2mRL/9pkS//aZEv/2mRL//Nqo////' + - '//////////////rLhv/3mhL/9pkS//eZEv35mhgrAAAAAPifFZT4nhT/+Z8U//ie' + - 'FP/5nxT/+Z8U//ikI//83a3//vjw//78+f/7yX3/+J4T//ieFP/4nxWUAAAAAAAA' + - 'AAD/qisG+6MWvfqjFf/6oxX/+qMV//qjFf/6oxX/+qMV//qjFf/6oxX/+qIV//qj' + - 'Ff/7oxa9/6orBgAAAAAAAAAAAAAAAP+qKwb9qRiU/agW/fyoF//8qBf//agX//yo' + - 'F//9qBf//agX//2oF/39qRiU/6orBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+y' + - 'Hiv/rRmP/6wZ1f+tGPP/rBjz/64Z1f+vGY//sh4rAAAAAAAAAAAAAAAAAAAAAPAP' + - 'AADAAwAAgAEAAIABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAB' + - 'AACAAQAAwAMAAPAPAAAoAAAAIAAAAEAAAAABACAAAAAAAAAQAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+A' + - 'AAbiZQRH4GMAlf//////////////////////////4GQAv+BjAJXiZQBH/4AABgAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAOpqCxjiZgKW4WYB8eJmAf/hZQH/////////' + - '///////////////////hZgH/4mYB/+FmAf/iZwHx4mYClupqCxgAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP9t' + - 'JAfkagSC42kC9ONoAv/jaAL/4mgC/+NoAv///////////////////////////+Jo' + - 'Af/iaAL/4mgB/+JoAv/iaAL/42kC9ORqBIL/bSQHAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADqagsY5GoEx+NqA//jagP/42oD/+Nq' + - 'A//kawP/42oD////////////////////////////42oD/+RrA//jagP/5GsD/+Rr' + - 'A//kawP/5GsD/+RsBMfqdQsYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAA6HEGLeVuBOPlbQT/5GwD/+VtBP/kbAP/5GwD/+VtBP/kbAP/////////' + - '///////////////////kbAP/5G0D/+RsA//kbQP/5G0D/+RtA//kbQP/5G0D/+Ru' + - 'A+PocQYtAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOp1CxjmcAbj5W8E/+Vv' + - 'BP/lbwT/5W8E/+VvBP/lbwT/5W8E/+VvBP///////////////////////////+Vv' + - 'BP/mcAX/5W8E/+ZwBf/mcAX/5W8E/+ZwBf/lbwT/5W8E/+ZwBuPqdQsYAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAD/kiQH53IFx+ZyBf/mcQX/5nEF/+ZxBf/mcQX/5nEF/+Zx' + - 'Bf/ncgX/5nEF////////////////////////////53IG/+ZxBf/ncgb/5nEF/+Zx' + - 'Bf/mcgX/5nEF/+ZyBf/mcgX/5nEF/+dyBcf/kiQHAAAAAAAAAAAAAAAAAAAAAOd2' + - 'CILodAb/53QG/+h0Bv/odAb/6HQG/+h0Bv/odAb/6HQG/+d0Bv/odAb/////////' + - '///////////////////ndAX/53QG/+d0Bf/ndAb/53QG/+h1Bv/ndAb/6HUG/+h1' + - 'Bv/ndAX/6HUG/+d2BoIAAAAAAAAAAAAAAADqgAsY6HYG9Oh2B//odgb/6HYH/+h2' + - 'B//odgb/6HYH/+h2Bv/odgb/6HYH/+h2Bv///////////////////////////+l3' + - 'B//odgb/6XcH/+h2Bv/odgb/6HYH/+h2Bv/odgf/6HYH/+h2Bv/odgf/6HYG9OqA' + - 'CxgAAAAAAAAAAOt6CZbpeQj/6nkI/+l5CP/qeQj/6nkI/+l5B//qeQj/6XkH/+l5' + - 'B//peQf/6XkH////////////////////////////6XkI/+l5B//peQj/6XkH/+l5' + - 'B//peQj/6XkH/+l5CP/peQj/6XkH/+l5CP/peQf/63oJlgAAAAD/gCsG7H0K8et8' + - 'Cf/qewj/63wJ/+p7CP/qewj/6nsI/+p7CP/qewj/6nsI/+t8Cf/qewj/////////' + - '///////////////////qewj/6nwJ/+p7CP/qfAn/6nwJ/+p7CP/qfAn/6nsI/+p7' + - 'CP/rfAn/6nsI/+t8Cf/sfQrx/4ArBu2BC0frfQn/630J/+t+Cf/rfQn/634J/+t+' + - 'Cf/rfgn/634J////////////////////////////////////////////////////' + - '///////////////////zs27/634J/+x+Cf/rfgn/634J/+t+Cf/rfgn/634J/+t+' + - 'Cf/tgQtH7IAKleyACv/sgAr/7IAK/+yACv/sgAr/7IAK/+yACv/sgAr/////////' + - '//////////////////////////////////////////////////////////////XC' + - 'iv/sgAr/7IAK/+yACv/sgAr/7IAJ/+yACv/sgAn/7IAJ/+yACpXugwu/7YML/+2D' + - 'C//tggr/7YML/+2CCv/tggr/7YIK/+2CCv//////////////////////////////' + - '////////////////////////////////////////+NKn/+2DC//tggr/7YML/+2D' + - 'C//tgwv/7YML/+2DC//tgwv/7oMLv++GDNnuhQv/7oUL/+6FC//uhQv/7oUL/+6F' + - 'C//vhQz/7oUL////////////////////////////////////////////////////' + - '///////////////////64cT/7oUL/+6FC//uhQv/7oUL/+6EC//uhQv/7oQL/+6E' + - 'C//vhgzZ74gO8++IDP/viAz/74cM/++IDP/vhwz/74cM/++HDP/vhwz/////////' + - '//////////////////////////////////////////////////////////////3w' + - '4f/viA3/74cM/++IDf/viA3/74cM/++IDf/vhwz/74cM/++HDfPwiw7z8IoN//CK' + - 'Df/wig3/8IoN//CKDf/wig3/8IkN//CKDf/wiQ3/8IkN//CKDf/wiQ3/////////' + - '///////////////////wiQ3/8IoN//CJDf/wig3/8IoN//CJDf/wig3/8IkN//CJ' + - 'Df/wiQ3/8IkN//CJDf/wiQ3/8IsO8/KNDtnxjA7/8YwO//GMDf/xjA7/8YwN//GM' + - 'Df/xjA3/8YwN//GMDf/xjA3/8YwO//GMDf////////////////////////////GM' + - 'Dv/xjA7/8YwO//GMDv/xjA7/8YwO//GMDv/xjA7/8YwO//GMDv/xjA7/8YwO//GM' + - 'Dv/yjQ7Z8o8Pv/KPD//yjw//8o8P//KPD//yjw//8o8P//KPD//yjw//8o8P//KP' + - 'D//yjg7/8o8P////////////////////////////8Y4O//KODv/xjg7/8o4O//KO' + - 'Dv/yjg7/8o4O//KODv/yjg7/8o8P//KODv/yjw//8o8P//OQEL/zkQ+V85EP//OR' + - 'D//zkQ//85EP//ORD//zkQ//85EP//ORD//zkQ//85EP//OREP/zkQ///vr0////' + - '///////////////////0myb/85EQ//ORD//zkRD/85EQ//ORD//zkRD/85EP//OR' + - 'D//zkQ//85EP//ORD//zkQ//85EPlfSXEkf0kxD/9JMQ//SUEP/0kxD/9JQQ//SU' + - 'EP/zkxD/9JQQ//OTEP/zkxD/9JQQ//OTEP/86tD///////////////////////rV' + - 'ov/1nSb/85MQ//STEP/0kxD/9JMQ//STEP/0kxD/9JMQ//SUEP/0kxD/9JQQ//SU' + - 'EP/0kxJH/6orBvWWEvH1lhH/9ZYR//WWEf/1lhH/9ZYR//WWEf/1lhH/9ZYR//WW' + - 'Ef/1lhH/9ZYR//vZq///////////////////////////////////////////////' + - '///1lhH/9ZYR//WWEf/1lhH/9ZYR//WWEf/1lhH/9ZYS8f+qKwYAAAAA95kTlvaY' + - 'Ev/2mBH/9pgS//aYEf/2mBH/9ZgR//aYEf/1mBH/9ZgR//aYEv/1mBH/+LFN////' + - '//////////////////////////////////////////////aYEv/1mBH/9pgS//aY' + - 'Ev/1mBH/9pgS//WYEf/3mRGWAAAAAAAAAAD/nxUY+JwU9PebE//3mxP/95sT//eb' + - 'E//3mxP/95sT//ebE//3mxP/95oS//ebE//3mhL//OK7////////////////////' + - '////////////////////////95sT//ebE//3mxP/95sT//aaEv/3mxP/95sT9P+f' + - 'FRgAAAAAAAAAAAAAAAD5nxSC+J0T//idE//4nRP/+J0T//ecE//4nRP/95wT//ec' + - 'E//4nRP/95wT//idE//4pSf//efF////////////////////////////////////' + - '///4nRP/950T//idE//4nRP/+J0T//idE//5nxSCAAAAAAAAAAAAAAAAAAAAAP+2' + - 'JAf6oBXH+aAU//mgFP/5oBT/+J8U//mgFP/4nxT/+J8U//mfFP/4nxT/+Z8U//mf' + - 'FP/5oRf/+86H//7w2v/+/Pj//v36//758f/+8+P//evQ//mgFP/5nxT/+aAU//mg' + - 'FP/4nxT/+qAVx/+2JAcAAAAAAAAAAAAAAAAAAAAAAAAAAP+qFRj7oxXj+aEU//mh' + - 'FP/6ohX/+aEU//qiFf/6ohX/+qIV//qiFf/6ohX/+qIV//mhFP/6ohX/+aEU//mh' + - 'FP/6ohX/+aEU//qiFf/6ohX/+aEU//qiFf/5oRT/+aEU//ujFeP/qhUYAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+mFi78pBXj+6QV//ukFv/7pBX/+6QW//uk' + - 'Fv/6pBX/+6QW//qkFf/6pBX/+6QW//qkFf/7pBb/+6QW//ulFv/7pBb/+6UW//ul' + - 'Fv/7pBX/+6UW//ukFf/8pBXj/6QXLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAP+qIBj8qBfH/KcW//ynF//8pxb//KcW//ynFv/8pxb//KcW//yn' + - 'Fv/7phb//KcW//umFv/7phb/+6YW//umFv/7phb/+6YW//ynFv/7phb//KgXx/+q' + - 'IBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+2' + - 'JAf9qxiC/akY9PypF//8qRf//KgX//ypF//8qBf//KgX//2pF//8qBf//akX//2p' + - 'F//9qRf//akX//2pF//9qRf//qkY9P2rGIL/tiQHAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/tSAY/60alv+s' + - 'GPH+rBj//qwY//6sGP/+rBj//asY//6sGP/9qxj//asY//2rF//9qxj//qsX8f2s' + - 'GJb/tSAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/9UrBv+wGUf/rxqV/68Zv/+v' + - 'Gtn/rhnz/64Z8/+vGtn/rxm//68alf+wGUf/1SsGAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/AA///AAD//AAAP/gAAB/wAAAP4AAAB8AA' + - 'AAPAAAADgAAAAYAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAACAAAABgAAAAcAAAAPAAAAD4AAAB/AAAA/4AAAf/AAAP/8A' + - 'AP//wAP/', - ), + 'AAABAAIAEBAAAAEAIABoBAAAJgAAACAgAAABACAAqBAAAI4EAAAoAAAAEAAAACAA' + + 'AAABACAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOFl' + + 'BiviZgKP4WYB1f//////////4WUA1eJmAI/hawYrAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAA/4ArBuNpA5TkawP942kC/+NpAv///////////+NpAv/jaQL/5GoD/eNp' + + 'A5T/gCsGAAAAAAAAAAAAAAAA/4ArBuVvBL3lbgT/5W4E/+VuBP/lbgT/////////' + + '///lbgT/5W4E/+VuBP/lbgT/5W8Evf+AKwYAAAAAAAAAAOlzBZTncwX/53MF/+dz' + + 'Bf/ncwX/53MF////////////53MF/+dzBf/ncwb/53MF/+dzBv/pcweUAAAAAO19' + + 'DCvpeAf96HcH/+l4B//odwf/6XgH/+l4B////////////+h3B//odwf/6XgH/+h3' + + 'B//peAf/6nkH/e19DCvrfQmP630J/+t9Cf/rfAn/630J/+t8Cf/rfAn/////////' + + '///rfQn/630J/+p8CP/rfQn/6nwI/+p8CP/rfQuP7YEL1e2BCv/tgQr/7IEK/+2B' + + 'Cv/////////////////////////////////uiRj/7IEK/+2CCv/tggr/7YIM1e6G' + + 'DfPvhgz/74YM/++HDP/vhgz/////////////////////////////////8Zw4/++G' + + 'DP/uhgz/7oYM/+6GDPPwiw7z8IsN//CLDf/wiw3/8IsN//CLDf/wiw3/////////' + + '///wig3/8IoN//CLDf/wig3/8IsN//CLDf/xjA/z85EQ1fOQD//zkA//85AP//OQ' + + 'D//zkA//85AP////////////8o8P//KPD//ykA//8o8P//KQD//ykA//85EQ1fSU' + + 'EI/1lRH/9ZUR//SUEP/1lRH/9JQQ//SUEP/+9uz///////jDev/0mRz/9ZUR//SV' + + 'Ef/1lRH/9ZUR//SUEI/5mhgr95kS/faZEv/2mRL/9pkS//aZEv/2mRL//Nqo////' + + '//////////////rLhv/3mhL/9pkS//eZEv35mhgrAAAAAPifFZT4nhT/+Z8U//ie' + + 'FP/5nxT/+Z8U//ikI//83a3//vjw//78+f/7yX3/+J4T//ieFP/4nxWUAAAAAAAA' + + 'AAD/qisG+6MWvfqjFf/6oxX/+qMV//qjFf/6oxX/+qMV//qjFf/6oxX/+qIV//qj' + + 'Ff/7oxa9/6orBgAAAAAAAAAAAAAAAP+qKwb9qRiU/agW/fyoF//8qBf//agX//yo' + + 'F//9qBf//agX//2oF/39qRiU/6orBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+y' + + 'Hiv/rRmP/6wZ1f+tGPP/rBjz/64Z1f+vGY//sh4rAAAAAAAAAAAAAAAAAAAAAPAP' + + 'AADAAwAAgAEAAIABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAB' + + 'AACAAQAAwAMAAPAPAAAoAAAAIAAAAEAAAAABACAAAAAAAAAQAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+A' + + 'AAbiZQRH4GMAlf//////////////////////////4GQAv+BjAJXiZQBH/4AABgAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAOpqCxjiZgKW4WYB8eJmAf/hZQH/////////' + + '///////////////////hZgH/4mYB/+FmAf/iZwHx4mYClupqCxgAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP9t' + + 'JAfkagSC42kC9ONoAv/jaAL/4mgC/+NoAv///////////////////////////+Jo' + + 'Af/iaAL/4mgB/+JoAv/iaAL/42kC9ORqBIL/bSQHAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADqagsY5GoEx+NqA//jagP/42oD/+Nq' + + 'A//kawP/42oD////////////////////////////42oD/+RrA//jagP/5GsD/+Rr' + + 'A//kawP/5GsD/+RsBMfqdQsYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAA6HEGLeVuBOPlbQT/5GwD/+VtBP/kbAP/5GwD/+VtBP/kbAP/////////' + + '///////////////////kbAP/5G0D/+RsA//kbQP/5G0D/+RtA//kbQP/5G0D/+Ru' + + 'A+PocQYtAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOp1CxjmcAbj5W8E/+Vv' + + 'BP/lbwT/5W8E/+VvBP/lbwT/5W8E/+VvBP///////////////////////////+Vv' + + 'BP/mcAX/5W8E/+ZwBf/mcAX/5W8E/+ZwBf/lbwT/5W8E/+ZwBuPqdQsYAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAD/kiQH53IFx+ZyBf/mcQX/5nEF/+ZxBf/mcQX/5nEF/+Zx' + + 'Bf/ncgX/5nEF////////////////////////////53IG/+ZxBf/ncgb/5nEF/+Zx' + + 'Bf/mcgX/5nEF/+ZyBf/mcgX/5nEF/+dyBcf/kiQHAAAAAAAAAAAAAAAAAAAAAOd2' + + 'CILodAb/53QG/+h0Bv/odAb/6HQG/+h0Bv/odAb/6HQG/+d0Bv/odAb/////////' + + '///////////////////ndAX/53QG/+d0Bf/ndAb/53QG/+h1Bv/ndAb/6HUG/+h1' + + 'Bv/ndAX/6HUG/+d2BoIAAAAAAAAAAAAAAADqgAsY6HYG9Oh2B//odgb/6HYH/+h2' + + 'B//odgb/6HYH/+h2Bv/odgb/6HYH/+h2Bv///////////////////////////+l3' + + 'B//odgb/6XcH/+h2Bv/odgb/6HYH/+h2Bv/odgf/6HYH/+h2Bv/odgf/6HYG9OqA' + + 'CxgAAAAAAAAAAOt6CZbpeQj/6nkI/+l5CP/qeQj/6nkI/+l5B//qeQj/6XkH/+l5' + + 'B//peQf/6XkH////////////////////////////6XkI/+l5B//peQj/6XkH/+l5' + + 'B//peQj/6XkH/+l5CP/peQj/6XkH/+l5CP/peQf/63oJlgAAAAD/gCsG7H0K8et8' + + 'Cf/qewj/63wJ/+p7CP/qewj/6nsI/+p7CP/qewj/6nsI/+t8Cf/qewj/////////' + + '///////////////////qewj/6nwJ/+p7CP/qfAn/6nwJ/+p7CP/qfAn/6nsI/+p7' + + 'CP/rfAn/6nsI/+t8Cf/sfQrx/4ArBu2BC0frfQn/630J/+t+Cf/rfQn/634J/+t+' + + 'Cf/rfgn/634J////////////////////////////////////////////////////' + + '///////////////////zs27/634J/+x+Cf/rfgn/634J/+t+Cf/rfgn/634J/+t+' + + 'Cf/tgQtH7IAKleyACv/sgAr/7IAK/+yACv/sgAr/7IAK/+yACv/sgAr/////////' + + '//////////////////////////////////////////////////////////////XC' + + 'iv/sgAr/7IAK/+yACv/sgAr/7IAJ/+yACv/sgAn/7IAJ/+yACpXugwu/7YML/+2D' + + 'C//tggr/7YML/+2CCv/tggr/7YIK/+2CCv//////////////////////////////' + + '////////////////////////////////////////+NKn/+2DC//tggr/7YML/+2D' + + 'C//tgwv/7YML/+2DC//tgwv/7oMLv++GDNnuhQv/7oUL/+6FC//uhQv/7oUL/+6F' + + 'C//vhQz/7oUL////////////////////////////////////////////////////' + + '///////////////////64cT/7oUL/+6FC//uhQv/7oUL/+6EC//uhQv/7oQL/+6E' + + 'C//vhgzZ74gO8++IDP/viAz/74cM/++IDP/vhwz/74cM/++HDP/vhwz/////////' + + '//////////////////////////////////////////////////////////////3w' + + '4f/viA3/74cM/++IDf/viA3/74cM/++IDf/vhwz/74cM/++HDfPwiw7z8IoN//CK' + + 'Df/wig3/8IoN//CKDf/wig3/8IkN//CKDf/wiQ3/8IkN//CKDf/wiQ3/////////' + + '///////////////////wiQ3/8IoN//CJDf/wig3/8IoN//CJDf/wig3/8IkN//CJ' + + 'Df/wiQ3/8IkN//CJDf/wiQ3/8IsO8/KNDtnxjA7/8YwO//GMDf/xjA7/8YwN//GM' + + 'Df/xjA3/8YwN//GMDf/xjA3/8YwO//GMDf////////////////////////////GM' + + 'Dv/xjA7/8YwO//GMDv/xjA7/8YwO//GMDv/xjA7/8YwO//GMDv/xjA7/8YwO//GM' + + 'Dv/yjQ7Z8o8Pv/KPD//yjw//8o8P//KPD//yjw//8o8P//KPD//yjw//8o8P//KP' + + 'D//yjg7/8o8P////////////////////////////8Y4O//KODv/xjg7/8o4O//KO' + + 'Dv/yjg7/8o4O//KODv/yjg7/8o8P//KODv/yjw//8o8P//OQEL/zkQ+V85EP//OR' + + 'D//zkQ//85EP//ORD//zkQ//85EP//ORD//zkQ//85EP//OREP/zkQ///vr0////' + + '///////////////////0myb/85EQ//ORD//zkRD/85EQ//ORD//zkRD/85EP//OR' + + 'D//zkQ//85EP//ORD//zkQ//85EPlfSXEkf0kxD/9JMQ//SUEP/0kxD/9JQQ//SU' + + 'EP/zkxD/9JQQ//OTEP/zkxD/9JQQ//OTEP/86tD///////////////////////rV' + + 'ov/1nSb/85MQ//STEP/0kxD/9JMQ//STEP/0kxD/9JMQ//SUEP/0kxD/9JQQ//SU' + + 'EP/0kxJH/6orBvWWEvH1lhH/9ZYR//WWEf/1lhH/9ZYR//WWEf/1lhH/9ZYR//WW' + + 'Ef/1lhH/9ZYR//vZq///////////////////////////////////////////////' + + '///1lhH/9ZYR//WWEf/1lhH/9ZYR//WWEf/1lhH/9ZYS8f+qKwYAAAAA95kTlvaY' + + 'Ev/2mBH/9pgS//aYEf/2mBH/9ZgR//aYEf/1mBH/9ZgR//aYEv/1mBH/+LFN////' + + '//////////////////////////////////////////////aYEv/1mBH/9pgS//aY' + + 'Ev/1mBH/9pgS//WYEf/3mRGWAAAAAAAAAAD/nxUY+JwU9PebE//3mxP/95sT//eb' + + 'E//3mxP/95sT//ebE//3mxP/95oS//ebE//3mhL//OK7////////////////////' + + '////////////////////////95sT//ebE//3mxP/95sT//aaEv/3mxP/95sT9P+f' + + 'FRgAAAAAAAAAAAAAAAD5nxSC+J0T//idE//4nRP/+J0T//ecE//4nRP/95wT//ec' + + 'E//4nRP/95wT//idE//4pSf//efF////////////////////////////////////' + + '///4nRP/950T//idE//4nRP/+J0T//idE//5nxSCAAAAAAAAAAAAAAAAAAAAAP+2' + + 'JAf6oBXH+aAU//mgFP/5oBT/+J8U//mgFP/4nxT/+J8U//mfFP/4nxT/+Z8U//mf' + + 'FP/5oRf/+86H//7w2v/+/Pj//v36//758f/+8+P//evQ//mgFP/5nxT/+aAU//mg' + + 'FP/4nxT/+qAVx/+2JAcAAAAAAAAAAAAAAAAAAAAAAAAAAP+qFRj7oxXj+aEU//mh' + + 'FP/6ohX/+aEU//qiFf/6ohX/+qIV//qiFf/6ohX/+qIV//mhFP/6ohX/+aEU//mh' + + 'FP/6ohX/+aEU//qiFf/6ohX/+aEU//qiFf/5oRT/+aEU//ujFeP/qhUYAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+mFi78pBXj+6QV//ukFv/7pBX/+6QW//uk' + + 'Fv/6pBX/+6QW//qkFf/6pBX/+6QW//qkFf/7pBb/+6QW//ulFv/7pBb/+6UW//ul' + + 'Fv/7pBX/+6UW//ukFf/8pBXj/6QXLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAP+qIBj8qBfH/KcW//ynF//8pxb//KcW//ynFv/8pxb//KcW//yn' + + 'Fv/7phb//KcW//umFv/7phb/+6YW//umFv/7phb/+6YW//ynFv/7phb//KgXx/+q' + + 'IBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+2' + + 'JAf9qxiC/akY9PypF//8qRf//KgX//ypF//8qBf//KgX//2pF//8qBf//akX//2p' + + 'F//9qRf//akX//2pF//9qRf//qkY9P2rGIL/tiQHAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/tSAY/60alv+s' + + 'GPH+rBj//qwY//6sGP/+rBj//asY//6sGP/9qxj//asY//2rF//9qxj//qsX8f2s' + + 'GJb/tSAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/9UrBv+wGUf/rxqV/68Zv/+v' + + 'Gtn/rhnz/64Z8/+vGtn/rxm//68alf+wGUf/1SsGAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/AA///AAD//AAAP/gAAB/wAAAP4AAAB8AA' + + 'AAPAAAADgAAAAYAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAACAAAABgAAAAcAAAAPAAAAD4AAAB/AAAA/4AAAf/AAAP/8A' + + 'AP//wAP/', }; _get = () => { From 77d5812d0c0cc559dd37811f37a6c4179d661a3a Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Tue, 6 Dec 2022 23:21:12 -0800 Subject: [PATCH 09/45] Remove change file --- ...ative-windows-195fdcc2-1b14-49aa-a0eb-06763be2eed8.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 change/react-native-windows-195fdcc2-1b14-49aa-a0eb-06763be2eed8.json diff --git a/change/react-native-windows-195fdcc2-1b14-49aa-a0eb-06763be2eed8.json b/change/react-native-windows-195fdcc2-1b14-49aa-a0eb-06763be2eed8.json deleted file mode 100644 index 0d5014c93c4..00000000000 --- a/change/react-native-windows-195fdcc2-1b14-49aa-a0eb-06763be2eed8.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "prerelease", - "comment": "Use uint8_t const in IBlobPersistor.h", - "packageName": "react-native-windows", - "email": "erozell@outlook.com", - "dependentChangeType": "patch" -} From a9ecf4e9e5655611548f5c806a4a539240e796c2 Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Mon, 15 Aug 2022 11:51:53 -0400 Subject: [PATCH 10/45] 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 --- ...-ed8de10f-3224-4e5d-8624-ff9f24b9cbb1.json | 7 + .../Networking/OriginPolicyHttpFilter.cpp | 989 +++++++++--------- 2 files changed, 510 insertions(+), 486 deletions(-) create mode 100644 change/react-native-windows-ed8de10f-3224-4e5d-8624-ff9f24b9cbb1.json diff --git a/change/react-native-windows-ed8de10f-3224-4e5d-8624-ff9f24b9cbb1.json b/change/react-native-windows-ed8de10f-3224-4e5d-8624-ff9f24b9cbb1.json new file mode 100644 index 00000000000..526d80aee48 --- /dev/null +++ b/change/react-native-windows-ed8de10f-3224-4e5d-8624-ff9f24b9cbb1.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Adds missing headers for HttpRequestHeaderCollection", + "packageName": "react-native-windows", + "email": "erozell@outlook.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp index 15d23ec1b5b..225408cd7f2 100644 --- a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp +++ b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp @@ -11,6 +11,10 @@ #include #include +// Windows API +#include +#include + // Standard Library #include #include @@ -39,20 +43,20 @@ namespace Microsoft::React::Networking { #pragma region ConstWcharComparer -bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, const wchar_t *b) const { - return _wcsicmp(a, b) < 0; -} + bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t* a, const wchar_t* b) const { + return _wcsicmp(a, b) < 0; + } #pragma endregion ConstWcharComparer -// https://fetch.spec.whatwg.org/#forbidden-method -/*static*/ set OriginPolicyHttpFilter::s_forbiddenMethods = - {L"CONNECT", L"TRACE", L"TRACK"}; + // https://fetch.spec.whatwg.org/#forbidden-method + /*static*/ set OriginPolicyHttpFilter::s_forbiddenMethods = + { L"CONNECT", L"TRACE", L"TRACK" }; -/*static*/ set - OriginPolicyHttpFilter::s_simpleCorsMethods = {L"GET", L"HEAD", L"POST"}; + /*static*/ set + OriginPolicyHttpFilter::s_simpleCorsMethods = { L"GET", L"HEAD", L"POST" }; -/*static*/ set + /*static*/ set OriginPolicyHttpFilter::s_simpleCorsRequestHeaderNames = { L"Accept", L"Accept-Language", @@ -62,21 +66,21 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co L"Downlink", L"Save-Data", L"Viewport-Width", - L"Width"}; + L"Width" }; -/*static*/ set + /*static*/ set OriginPolicyHttpFilter::s_simpleCorsResponseHeaderNames = - {L"Cache-Control", L"Content-Language", L"Content-Type", L"Expires", L"Last-Modified", L"Pragma"}; + { L"Cache-Control", L"Content-Language", L"Content-Type", L"Expires", L"Last-Modified", L"Pragma" }; -/*static*/ set + /*static*/ set OriginPolicyHttpFilter::s_simpleCorsContentTypeValues = { L"application/x-www-form-urlencoded", L"multipart/form-data", - L"text/plain"}; + L"text/plain" }; -// https://fetch.spec.whatwg.org/#forbidden-header-name -// Chromium still bans "User-Agent" due to https://crbug.com/571722 -/*static*/ set + // https://fetch.spec.whatwg.org/#forbidden-header-name + // Chromium still bans "User-Agent" due to https://crbug.com/571722 + /*static*/ set OriginPolicyHttpFilter::s_corsForbiddenRequestHeaderNames = { L"Accept-Charset", L"Accept-Encoding", @@ -97,93 +101,93 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co L"Trailer", L"Transfer-Encoding", L"Upgrade", - L"Via"}; + L"Via" }; -/*static*/ set + /*static*/ set OriginPolicyHttpFilter::s_cookieSettingResponseHeaders = { L"Set-Cookie", L"Set-Cookie2", // Deprecated by the spec, but probably still used -}; - -/*static*/ set - OriginPolicyHttpFilter::s_corsForbiddenRequestHeaderNamePrefixes = {L"Proxy-", L"Sec-"}; - -/*static*/ Uri OriginPolicyHttpFilter::s_origin{nullptr}; + }; -/*static*/ void OriginPolicyHttpFilter::SetStaticOrigin(std::string &&url) { - if (!url.empty()) - s_origin = Uri{to_hstring(url)}; -} + /*static*/ set + OriginPolicyHttpFilter::s_corsForbiddenRequestHeaderNamePrefixes = { L"Proxy-", L"Sec-" }; -/*static*/ bool OriginPolicyHttpFilter::IsSameOrigin(Uri const &u1, Uri const &u2) noexcept { - return (u1 && u2) && u1.SchemeName() == u2.SchemeName() && u1.Host() == u2.Host() && u1.Port() == u2.Port(); -} + /*static*/ Uri OriginPolicyHttpFilter::s_origin{ nullptr }; -/*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const &request) noexcept { - // Ensure header is in Simple CORS white list - for (const auto &header : request.Headers()) { - if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend()) - return false; + /*static*/ void OriginPolicyHttpFilter::SetStaticOrigin(std::string&& url) { + if (!url.empty()) + s_origin = Uri{ to_hstring(url) }; + } - // Ensure Content-Type value is in Simple CORS white list, if present - if (boost::iequals(header.Key(), L"Content-Type")) { - if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) != s_simpleCorsContentTypeValues.cend()) - return false; - } + /*static*/ bool OriginPolicyHttpFilter::IsSameOrigin(Uri const& u1, Uri const& u2) noexcept { + return (u1 && u2) && u1.SchemeName() == u2.SchemeName() && u1.Host() == u2.Host() && u1.Port() == u2.Port(); } - // WinRT separates request headers from request content headers - if (auto content = request.Content()) { - for (const auto &header : content.Headers()) { - // WinRT automatically appends non-whitelisted header Content-Length when Content-Type is set. Skip it. - if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend() && - !boost::iequals(header.Key(), "Content-Length")) + /*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const& request) noexcept { + // Ensure header is in Simple CORS white list + for (const auto& header : request.Headers()) { + if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend()) return false; // Ensure Content-Type value is in Simple CORS white list, if present if (boost::iequals(header.Key(), L"Content-Type")) { - if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) == s_simpleCorsContentTypeValues.cend()) + if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) != s_simpleCorsContentTypeValues.cend()) return false; } } - } - // Ensure method is in Simple CORS white list - return s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); -} + // WinRT separates request headers from request content headers + if (auto content = request.Content()) { + for (const auto& header : content.Headers()) { + // WinRT automatically appends non-whitelisted header Content-Length when Content-Type is set. Skip it. + if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend() && + !boost::iequals(header.Key(), "Content-Length")) + return false; -/*static*/ Uri OriginPolicyHttpFilter::GetOrigin(Uri const &uri) noexcept { - return Uri{uri.SchemeName() + L"://" + uri.Host() + L":" + to_hstring(uri.Port())}; -} + // Ensure Content-Type value is in Simple CORS white list, if present + if (boost::iequals(header.Key(), L"Content-Type")) { + if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) == s_simpleCorsContentTypeValues.cend()) + return false; + } + } + } -/*static*/ bool OriginPolicyHttpFilter::AreSafeRequestHeaders( - winrt::Windows::Web::Http::Headers::HttpRequestHeaderCollection const &headers) noexcept { - for (const auto &header : headers) { - if (s_corsForbiddenRequestHeaderNames.find(header.Key().c_str()) != s_corsForbiddenRequestHeaderNames.cend()) - return false; + // Ensure method is in Simple CORS white list + return s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); + } - for (const auto &prefix : s_corsForbiddenRequestHeaderNamePrefixes) { - if (boost::istarts_with(header.Key(), prefix)) + /*static*/ Uri OriginPolicyHttpFilter::GetOrigin(Uri const& uri) noexcept { + return Uri{ uri.SchemeName() + L"://" + uri.Host() + L":" + to_hstring(uri.Port()) }; + } + + /*static*/ bool OriginPolicyHttpFilter::AreSafeRequestHeaders( + winrt::Windows::Web::Http::Headers::HttpRequestHeaderCollection const& headers) noexcept { + for (const auto& header : headers) { + if (s_corsForbiddenRequestHeaderNames.find(header.Key().c_str()) != s_corsForbiddenRequestHeaderNames.cend()) return false; + + for (const auto& prefix : s_corsForbiddenRequestHeaderNamePrefixes) { + if (boost::istarts_with(header.Key(), prefix)) + return false; + } } + + return true; } - return true; -} - -// See https://fetch.spec.whatwg.org/#cors-safelisted-request-header -// "A CORS-safelisted header is a header whose name is either one of 'Accept', 'Accept-Language', and -// 'Content-Language', or whose name is 'Content-Type' and value is one of 'application/x-www-form-urlencoded', -// 'multipart/form-data', and 'text/plain' -/*static*/ bool OriginPolicyHttpFilter::IsCorsSafelistedRequestHeader( - hstring const &name, - hstring const &value) noexcept { - // 1. If value's length is greater than 128, then return false. - if (value.size() > 128) - return false; - - // 2. Byte-lowercase name and switch on the result: - static const wchar_t *const safeHeaderNames[] = { + // See https://fetch.spec.whatwg.org/#cors-safelisted-request-header + // "A CORS-safelisted header is a header whose name is either one of 'Accept', 'Accept-Language', and + // 'Content-Language', or whose name is 'Content-Type' and value is one of 'application/x-www-form-urlencoded', + // 'multipart/form-data', and 'text/plain' + /*static*/ bool OriginPolicyHttpFilter::IsCorsSafelistedRequestHeader( + hstring const& name, + hstring const& value) noexcept { + // 1. If value's length is greater than 128, then return false. + if (value.size() > 128) + return false; + + // 2. Byte-lowercase name and switch on the result: + static const wchar_t* const safeHeaderNames[] = { // The following four headers are from the CORS spec L"accept", L"accept-language", @@ -210,189 +214,195 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co L"sec-ch-ua-mobile", L"sec-ch-ua-full-version", L"sec-ch-ua-platform-version", - }; + }; - auto nameLower = boost::to_lower_copy(wstring{name.c_str()}); - if (std::find(std::cbegin(safeHeaderNames), std::cend(safeHeaderNames), nameLower) == std::end(safeHeaderNames)) - return false; + auto nameLower = boost::to_lower_copy(wstring{ name.c_str() }); + if (std::find(std::cbegin(safeHeaderNames), std::cend(safeHeaderNames), nameLower) == std::end(safeHeaderNames)) + return false; - double doubleHolder; - if (nameLower == L"device-memory" || nameLower == L"dpr") - return boost::conversion::try_lexical_convert(value.c_str(), doubleHolder); + double doubleHolder; + if (nameLower == L"device-memory" || nameLower == L"dpr") + return boost::conversion::try_lexical_convert(value.c_str(), doubleHolder); - int intHolder; - if (nameLower == L"width" || nameLower == L"viewport-width") - return boost::conversion::try_lexical_convert(value.c_str(), intHolder); + int intHolder; + if (nameLower == L"width" || nameLower == L"viewport-width") + return boost::conversion::try_lexical_convert(value.c_str(), intHolder); - auto valueLower = boost::to_lower_copy(wstring{value.c_str()}); - if (nameLower == L"save-data") - return valueLower == L"on"; + auto valueLower = boost::to_lower_copy(wstring{ value.c_str() }); + if (nameLower == L"save-data") + return valueLower == L"on"; - if (nameLower == L"accept") - return !std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte); + if (nameLower == L"accept") + return !std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte); - if (nameLower == L"accept-language" || nameLower == L"content-language") { - return std::all_of(valueLower.cbegin(), valueLower.cend(), [](wchar_t c) noexcept { - return (0x30 <= c && c <= 0x39) || // 0-9 - (0x41 <= c && c <= 0x5A) || // A-Z - (0x61 <= c && c <= 0x7A) || // a-z - c == 0x20 || c == 0x2A || c == 0x2C || c == 0x2D || c == 0x2E || c == 0x3B || c == 0x3D; // *,-.;= - }); - } + if (nameLower == L"accept-language" || nameLower == L"content-language") { + return std::all_of(valueLower.cbegin(), valueLower.cend(), [](wchar_t c) noexcept { + return (0x30 <= c && c <= 0x39) || // 0-9 + (0x41 <= c && c <= 0x5A) || // A-Z + (0x61 <= c && c <= 0x7A) || // a-z + c == 0x20 || c == 0x2A || c == 0x2C || c == 0x2D || c == 0x2E || c == 0x3B || c == 0x3D; // *,-.;= + }); + } - if (nameLower == L"content-type") { - if (std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte)) - return false; + if (nameLower == L"content-type") { + if (std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte)) + return false; - // https://mimesniff.spec.whatwg.org/#parse-a-mime-type - HttpMediaTypeHeaderValue mediaType{nullptr}; - if (HttpMediaTypeHeaderValue::TryParse(valueLower, mediaType)) - return mediaType.ToString() == L"application/x-www-form-urlencoded" || - mediaType.ToString() == L"multipart/form-data" || mediaType.ToString() == L"text/plain"; + // https://mimesniff.spec.whatwg.org/#parse-a-mime-type + HttpMediaTypeHeaderValue mediaType{ nullptr }; + if (HttpMediaTypeHeaderValue::TryParse(valueLower, mediaType)) + return mediaType.ToString() == L"application/x-www-form-urlencoded" || + mediaType.ToString() == L"multipart/form-data" || mediaType.ToString() == L"text/plain"; - return false; - } + return false; + } - return true; -} + return true; + } -// https://fetch.spec.whatwg.org/#cors-unsafe-request-header-byte -/*static*/ bool OriginPolicyHttpFilter::IsCorsUnsafeRequestHeaderByte(wchar_t c) noexcept { - return (c < 0x20 && c != 0x09) || c == 0x22 || c == 0x28 || c == 0x29 || c == 0x3a || c == 0x3c || c == 0x3e || + // https://fetch.spec.whatwg.org/#cors-unsafe-request-header-byte + /*static*/ bool OriginPolicyHttpFilter::IsCorsUnsafeRequestHeaderByte(wchar_t c) noexcept { + return (c < 0x20 && c != 0x09) || c == 0x22 || c == 0x28 || c == 0x29 || c == 0x3a || c == 0x3c || c == 0x3e || c == 0x3f || c == 0x40 || c == 0x5b || c == 0x5c || c == 0x5d || c == 0x7b || c == 0x7d || c == 0x7f; -} - -/*static*/ set OriginPolicyHttpFilter::CorsUnsafeNotForbiddenRequestHeaderNames( - HttpRequestHeaderCollection const &headers) noexcept { - constexpr size_t maxSafelistValueSize = 1024; - size_t safelistValueSize = 0; - std::vector potentiallyUnsafeNames; - set result; - for (const auto &header : headers) { - const auto headerName = header.Key().c_str(); - - // If header is not safe - if (boost::istarts_with(headerName, L"Proxy-") || boost::istarts_with(headerName, L"Sec-") || + } + + /*static*/ set OriginPolicyHttpFilter::CorsUnsafeNotForbiddenRequestHeaderNames( + HttpRequestHeaderCollection const& headers) noexcept { + constexpr size_t maxSafelistValueSize = 1024; + size_t safelistValueSize = 0; + std::vector potentiallyUnsafeNames; + set result; + for (const auto& header : headers) { + const auto headerName = header.Key().c_str(); + + // If header is not safe + if (boost::istarts_with(headerName, L"Proxy-") || boost::istarts_with(headerName, L"Sec-") || s_corsForbiddenRequestHeaderNames.find(headerName) != s_corsForbiddenRequestHeaderNames.cend()) - continue; + continue; - if (!IsCorsSafelistedRequestHeader(header.Key(), header.Value())) { - result.emplace(header.Key().c_str()); - } else { - potentiallyUnsafeNames.emplace_back(std::wstring_view{header.Key()}.data()); - safelistValueSize += header.Value().size(); + if (!IsCorsSafelistedRequestHeader(header.Key(), header.Value())) { + result.emplace(header.Key().c_str()); + } + else { + potentiallyUnsafeNames.emplace_back(std::wstring_view{ header.Key() }.data()); + safelistValueSize += header.Value().size(); + } } + + if (safelistValueSize > maxSafelistValueSize) + result.insert(potentiallyUnsafeNames.begin(), potentiallyUnsafeNames.end()); + + return result; } - if (safelistValueSize > maxSafelistValueSize) - result.insert(potentiallyUnsafeNames.begin(), potentiallyUnsafeNames.end()); - - return result; -} - -/*static*/ OriginPolicyHttpFilter::AccessControlValues OriginPolicyHttpFilter::ExtractAccessControlValues( - winrt::Windows::Foundation::Collections::IMap const &headers) { - using std::wregex; - using std::wsregex_token_iterator; - - // https://tools.ietf.org/html/rfc2616#section-4.2 - wregex rgx{L"\\s*,\\s*"}; - AccessControlValues result; - - auto ciStrCmp = [](const wstring &a, const wstring &b) { return _wcsicmp(a.c_str(), b.c_str()) < 0; }; - set allowedHeaders{ciStrCmp}; - set allowedMethods{ciStrCmp}; - set exposedHeaders{ciStrCmp}; - - for (const auto &header : headers) { - if (boost::iequals(header.Key(), L"Access-Control-Allow-Headers")) { - auto value = wstring{header.Value().c_str()}; - - // TODO: Avoid redundant comparison. - auto parsed = set{ - wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp}; - allowedHeaders.insert(parsed.cbegin(), parsed.cend()); - result.AllowedHeaders.insert(allowedHeaders.cbegin(), allowedHeaders.cend()); - } else if (boost::iequals(header.Key(), L"Access-Control-Allow-Methods")) { - auto value = wstring{header.Value().c_str()}; - - // TODO: Avoid redundant comparison. - auto parsed = set{ - wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp}; - allowedMethods.insert(parsed.cbegin(), parsed.cend()); - result.AllowedMethods.insert(allowedMethods.cbegin(), allowedMethods.cend()); - } else if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { - result.AllowedOrigin = header.Value(); - } else if (boost::iequals(header.Key(), L"Access-Control-Expose-Headers")) { - auto value = wstring{header.Value().c_str()}; - - // TODO: Avoid redundant comparison. - auto parsed = set{ - wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp}; - exposedHeaders.insert(parsed.cbegin(), parsed.cend()); - result.ExposedHeaders.insert(exposedHeaders.cbegin(), exposedHeaders.cend()); - } else if (boost::iequals(header.Key(), L"Access-Control-Allow-Credentials")) { - result.AllowedCredentials = header.Value(); - } else if (boost::iequals(header.Key(), L"Access-Control-Max-Age")) { - result.MaxAge = _wtoi(header.Value().c_str()); + /*static*/ OriginPolicyHttpFilter::AccessControlValues OriginPolicyHttpFilter::ExtractAccessControlValues( + winrt::Windows::Foundation::Collections::IMap const& headers) { + using std::wregex; + using std::wsregex_token_iterator; + + // https://tools.ietf.org/html/rfc2616#section-4.2 + wregex rgx{ L"\\s*,\\s*" }; + AccessControlValues result; + + auto ciStrCmp = [](const wstring& a, const wstring& b) { return _wcsicmp(a.c_str(), b.c_str()) < 0; }; + set allowedHeaders{ ciStrCmp }; + set allowedMethods{ ciStrCmp }; + set exposedHeaders{ ciStrCmp }; + + for (const auto& header : headers) { + if (boost::iequals(header.Key(), L"Access-Control-Allow-Headers")) { + auto value = wstring{ header.Value().c_str() }; + + // TODO: Avoid redundant comparison. + auto parsed = set{ + wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; + allowedHeaders.insert(parsed.cbegin(), parsed.cend()); + result.AllowedHeaders.insert(allowedHeaders.cbegin(), allowedHeaders.cend()); + } + else if (boost::iequals(header.Key(), L"Access-Control-Allow-Methods")) { + auto value = wstring{ header.Value().c_str() }; + + // TODO: Avoid redundant comparison. + auto parsed = set{ + wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; + allowedMethods.insert(parsed.cbegin(), parsed.cend()); + result.AllowedMethods.insert(allowedMethods.cbegin(), allowedMethods.cend()); + } + else if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { + result.AllowedOrigin = header.Value(); + } + else if (boost::iequals(header.Key(), L"Access-Control-Expose-Headers")) { + auto value = wstring{ header.Value().c_str() }; + + // TODO: Avoid redundant comparison. + auto parsed = set{ + wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; + exposedHeaders.insert(parsed.cbegin(), parsed.cend()); + result.ExposedHeaders.insert(exposedHeaders.cbegin(), exposedHeaders.cend()); + } + else if (boost::iequals(header.Key(), L"Access-Control-Allow-Credentials")) { + result.AllowedCredentials = header.Value(); + } + else if (boost::iequals(header.Key(), L"Access-Control-Max-Age")) { + result.MaxAge = _wtoi(header.Value().c_str()); + } } - } - return result; -} // ExtractAccessControlValues + return result; + } // ExtractAccessControlValues -/*static*/ void OriginPolicyHttpFilter::RemoveHttpOnlyCookiesFromResponseHeaders( - HttpResponseMessage const &response, + /*static*/ void OriginPolicyHttpFilter::RemoveHttpOnlyCookiesFromResponseHeaders( + HttpResponseMessage const& response, bool removeAll) { - // Example: "Set-Cookie", L"id=a3fWa; Expires=Wed, 21 Oct 2020 07:28:00 GMT; HttpOnly" - std::queue httpOnlyCookies; - for (const auto &header : response.Headers()) { - if (s_cookieSettingResponseHeaders.find(header.Key().c_str()) == s_cookieSettingResponseHeaders.cend()) - continue; - - if (removeAll) { - httpOnlyCookies.push(header.Key()); - continue; - } + // Example: "Set-Cookie", L"id=a3fWa; Expires=Wed, 21 Oct 2020 07:28:00 GMT; HttpOnly" + std::queue httpOnlyCookies; + for (const auto& header : response.Headers()) { + if (s_cookieSettingResponseHeaders.find(header.Key().c_str()) == s_cookieSettingResponseHeaders.cend()) + continue; + + if (removeAll) { + httpOnlyCookies.push(header.Key()); + continue; + } - // Anchors (^$) can't be part of bracket expressions ([]). - // Create 3 matching groups: 1. Beginning of string 2. Between delimiters 3. End of string - const std::wregex re(L"(^HttpOnly\\s*;)|(;\\s*HttpOnly\\s*;)|(;\\s*HttpOnly$)", std::regex_constants::icase); - if (!std::regex_search(header.Value().c_str(), re)) - continue; + // Anchors (^$) can't be part of bracket expressions ([]). + // Create 3 matching groups: 1. Beginning of string 2. Between delimiters 3. End of string + const std::wregex re(L"(^HttpOnly\\s*;)|(;\\s*HttpOnly\\s*;)|(;\\s*HttpOnly$)", std::regex_constants::icase); + if (!std::regex_search(header.Value().c_str(), re)) + continue; - // HttpOnly cookie detected. Removing. - httpOnlyCookies.push(header.Key()); - } // const auto &header : response.Headers() + // HttpOnly cookie detected. Removing. + httpOnlyCookies.push(header.Key()); + } // const auto &header : response.Headers() - while (!httpOnlyCookies.empty()) { - response.Headers().Remove(httpOnlyCookies.front()); - httpOnlyCookies.pop(); + while (!httpOnlyCookies.empty()) { + response.Headers().Remove(httpOnlyCookies.front()); + httpOnlyCookies.pop(); + } } -} -OriginPolicyHttpFilter::OriginPolicyHttpFilter(IHttpFilter const &innerFilter) : m_innerFilter{innerFilter} {} + OriginPolicyHttpFilter::OriginPolicyHttpFilter(IHttpFilter const& innerFilter): m_innerFilter{ innerFilter } {} -OriginPolicyHttpFilter::OriginPolicyHttpFilter() + OriginPolicyHttpFilter::OriginPolicyHttpFilter() : OriginPolicyHttpFilter(winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter{}) {} -OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const &request) { - auto effectiveOriginPolicy = + OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const& request) { + auto effectiveOriginPolicy = static_cast(request.Properties().Lookup(L"OriginPolicy").as().GetUInt64()); - switch (effectiveOriginPolicy) { + switch (effectiveOriginPolicy) { case OriginPolicy::None: return effectiveOriginPolicy; case OriginPolicy::SameOrigin: if (!IsSameOrigin(s_origin, request.RequestUri())) - throw hresult_error{E_INVALIDARG, L"SOP (same-origin policy) is enforced"}; + throw hresult_error{ E_INVALIDARG, L"SOP (same-origin policy) is enforced" }; break; case OriginPolicy::SimpleCrossOriginResourceSharing: // Check for disallowed mixed content if (GetRuntimeOptionBool("Http.BlockMixedContentSimpleCors") && - s_origin.SchemeName() != request.RequestUri().SchemeName()) - throw hresult_error{E_INVALIDARG, L"The origin and request URLs must have the same scheme"}; + s_origin.SchemeName() != request.RequestUri().SchemeName()) + throw hresult_error{ E_INVALIDARG, L"The origin and request URLs must have the same scheme" }; if (IsSameOrigin(s_origin, request.RequestUri())) // Same origin. Therefore, skip Cross-Origin handling. @@ -400,7 +410,7 @@ OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const &r else if (!IsSimpleCorsRequest(request)) throw hresult_error{ E_INVALIDARG, - L"The request does not meet the requirements for Same-Origin policy or Simple Cross-Origin resource sharing"}; + L"The request does not meet the requirements for Same-Origin policy or Simple Cross-Origin resource sharing" }; break; case OriginPolicy::CrossOriginResourceSharing: @@ -411,13 +421,13 @@ OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const &r // https://fetch.spec.whatwg.org/#forbidden-header-name if (s_origin.SchemeName() != request.RequestUri().SchemeName()) - throw hresult_error{E_INVALIDARG, L"The origin and request URLs must have the same scheme"}; + throw hresult_error{ E_INVALIDARG, L"The origin and request URLs must have the same scheme" }; if (!AreSafeRequestHeaders(request.Headers())) - throw hresult_error{E_INVALIDARG, L"Request header not allowed in cross-origin resource sharing"}; + throw hresult_error{ E_INVALIDARG, L"Request header not allowed in cross-origin resource sharing" }; if (s_forbiddenMethods.find(request.Method().ToString().c_str()) != s_forbiddenMethods.cend()) - throw hresult_error{E_INVALIDARG, L"Request method not allowed in cross-origin resource sharing"}; + throw hresult_error{ E_INVALIDARG, L"Request method not allowed in cross-origin resource sharing" }; if (IsSameOrigin(s_origin, request.RequestUri())) effectiveOriginPolicy = OriginPolicy::SameOrigin; @@ -430,349 +440,356 @@ OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const &r default: throw hresult_error{ - E_INVALIDARG, L"Invalid OriginPolicy type: " + to_hstring(static_cast(effectiveOriginPolicy))}; - } + E_INVALIDARG, L"Invalid OriginPolicy type: " + to_hstring(static_cast(effectiveOriginPolicy)) }; + } - return effectiveOriginPolicy; -} + return effectiveOriginPolicy; + } -// See https://fetch.spec.whatwg.org/#cors-check -void OriginPolicyHttpFilter::ValidateAllowOrigin( - hstring const &allowedOrigin, - hstring const &allowCredentials, + // See https://fetch.spec.whatwg.org/#cors-check + void OriginPolicyHttpFilter::ValidateAllowOrigin( + hstring const& allowedOrigin, + hstring const& allowCredentials, 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 = 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 - 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"; - - // See https://fetch.spec.whatwg.org/#http-access-control-allow-origin. - else if (boost::contains(allowedOrigin, L" ,")) - errorMessage = L"Response header Access-Control-Allow-Origin can not have multiple values"; - - // A wildcard Access-Control-Allow-Origin can not be used if credentials are to be sent, - // even with Access-Control-Allow-Credentials set to true - // See https://fetch.spec.whatwg.org/#cors-protocol-and-credentials - else if (L"*" == allowedOrigin) - errorMessage = - L"Response header Access-Control-Allow-Origin can not have a wildcard value when the request includes credentials"; - - else - errorMessage = L"The Access-Control-Allow-Origin header has a value of [" + allowedOrigin + - L"] which differs from the supplied origin"; - - throw hresult_error{E_INVALIDARG, errorMessage}; - } + // 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 = 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 + 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"; + + // See https://fetch.spec.whatwg.org/#http-access-control-allow-origin. + else if (boost::contains(allowedOrigin, L" ,")) + errorMessage = L"Response header Access-Control-Allow-Origin can not have multiple values"; + + // A wildcard Access-Control-Allow-Origin can not be used if credentials are to be sent, + // even with Access-Control-Allow-Credentials set to true + // See https://fetch.spec.whatwg.org/#cors-protocol-and-credentials + else if (L"*" == allowedOrigin) + errorMessage = + L"Response header Access-Control-Allow-Origin can not have a wildcard value when the request includes credentials"; + + else + errorMessage = L"The Access-Control-Allow-Origin header has a value of [" + allowedOrigin + + L"] which differs from the supplied origin"; + + throw hresult_error{ E_INVALIDARG, errorMessage }; + } + + // 4.10.5 + if (!withCredentials) + return; + + // 4.10.6-8 + // https://fetch.spec.whatwg.org/#http-access-control-allow-credentials + // This check should be case sensitive. + // See also https://fetch.spec.whatwg.org/#http-new-header-syntax + if (L"true" != allowCredentials) + throw hresult_error{ + E_INVALIDARG, + L"Access-Control-Allow-Credentials value must be \"true\" when the response includes credentials" }; + }; - // 4.10.5 - if (!withCredentials) - return; - - // 4.10.6-8 - // https://fetch.spec.whatwg.org/#http-access-control-allow-credentials - // This check should be case sensitive. - // See also https://fetch.spec.whatwg.org/#http-new-header-syntax - if (L"true" != allowCredentials) - throw hresult_error{ - E_INVALIDARG, - L"Access-Control-Allow-Credentials value must be \"true\" when the response includes credentials"}; -}; - -void OriginPolicyHttpFilter::ValidatePreflightResponse( - HttpRequestMessage const &request, - HttpResponseMessage const &response) const { - // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed - using winrt::Windows::Web::Http::HttpStatusCode; - switch (response.StatusCode()) { + void OriginPolicyHttpFilter::ValidatePreflightResponse( + HttpRequestMessage const& request, + HttpResponseMessage const& response) const { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed + using winrt::Windows::Web::Http::HttpStatusCode; + switch (response.StatusCode()) { case HttpStatusCode::MovedPermanently: case HttpStatusCode::TemporaryRedirect: case HttpStatusCode::PermanentRedirect: - throw hresult_error{INET_E_REDIRECTING, L"Redirect is not allowed in a preflight request"}; + throw hresult_error{ INET_E_REDIRECTING, L"Redirect is not allowed in a preflight request" }; default: break; - } - - auto controlValues = ExtractAccessControlValues(response.Headers()); + } - 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, props); + auto controlValues = ExtractAccessControlValues(response.Headers()); - // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.5 - // Check if the request method is allowed - bool withCredentials = props.Lookup(L"RequestArgs").as()->WithCredentials; - bool requestMethodAllowed = false; - for (const auto &method : controlValues.AllowedMethods) { - if (L"*" == method) { - if (!withCredentials) { + 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, props); + + // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.5 + // Check if the request method is allowed + bool withCredentials = props.Lookup(L"RequestArgs").as()->WithCredentials; + bool requestMethodAllowed = false; + for (const auto& method : controlValues.AllowedMethods) { + if (L"*" == method) { + if (!withCredentials) { + requestMethodAllowed = true; + break; + } + } + else if (boost::iequals(method, request.Method().ToString())) { requestMethodAllowed = true; break; } - } else if (boost::iequals(method, request.Method().ToString())) { - requestMethodAllowed = true; - break; } - } - // Preflight should always allow simple CORS methods - requestMethodAllowed |= s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); - - if (!requestMethodAllowed) - throw hresult_error{ - E_INVALIDARG, - L"Method [" + request.Method().ToString() + - L"] is not allowed by Access-Control-Allow-Methods in preflight response"}; - - // Check if request headers are allowed - // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.6-7 - // Check if the header should be allowed through wildcard, if the request does not have credentials. - bool requestHeadersAllowed = false; - if (!withCredentials && controlValues.AllowedHeaders.find(L"*") != controlValues.AllowedHeaders.cend()) { - // "Authorization" header cannot be allowed through wildcard alone. - // "Authorization" is the only member of https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name. - if (request.Headers().HasKey(L"Authorization") && - controlValues.AllowedHeaders.find(L"Authorization") == controlValues.AllowedHeaders.cend()) + // Preflight should always allow simple CORS methods + requestMethodAllowed |= s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); + + if (!requestMethodAllowed) throw hresult_error{ E_INVALIDARG, - L"Request header field [Authorization] is not allowed by Access-Control-Allow-Headers in preflight response"}; - - requestHeadersAllowed = true; - } - if (!requestHeadersAllowed) { - // Forbidden headers are excluded from the JavaScript layer. - // User agents may use these headers internally. - const set unsafeNotForbidenHeaderNames = CorsUnsafeNotForbiddenRequestHeaderNames(request.Headers()); - for (const auto name : unsafeNotForbidenHeaderNames) { - if (controlValues.AllowedHeaders.find(name) == controlValues.AllowedHeaders.cend()) + L"Method [" + request.Method().ToString() + + L"] is not allowed by Access-Control-Allow-Methods in preflight response" }; + + // Check if request headers are allowed + // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.6-7 + // Check if the header should be allowed through wildcard, if the request does not have credentials. + bool requestHeadersAllowed = false; + if (!withCredentials && controlValues.AllowedHeaders.find(L"*") != controlValues.AllowedHeaders.cend()) { + // "Authorization" header cannot be allowed through wildcard alone. + // "Authorization" is the only member of https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name. + if (request.Headers().HasKey(L"Authorization") && + controlValues.AllowedHeaders.find(L"Authorization") == controlValues.AllowedHeaders.cend()) throw hresult_error{ E_INVALIDARG, - L"Request header field [" + to_hstring(name) + - L"] is not allowed by Access-Control-Allow-Headers in preflight response"}; + L"Request header field [Authorization] is not allowed by Access-Control-Allow-Headers in preflight response" }; + + requestHeadersAllowed = true; + } + if (!requestHeadersAllowed) { + // Forbidden headers are excluded from the JavaScript layer. + // User agents may use these headers internally. + const set unsafeNotForbidenHeaderNames = CorsUnsafeNotForbiddenRequestHeaderNames(request.Headers()); + for (const auto name : unsafeNotForbidenHeaderNames) { + if (controlValues.AllowedHeaders.find(name) == controlValues.AllowedHeaders.cend()) + throw hresult_error{ + E_INVALIDARG, + L"Request header field [" + to_hstring(name) + + L"] is not allowed by Access-Control-Allow-Headers in preflight response" }; + } } - } - // #9770 - insert into preflight cache -} + // #9770 - insert into preflight cache + } -// See 10.7.4 of https://fetch.spec.whatwg.org/#http-network-or-cache-fetch -void OriginPolicyHttpFilter::ValidateResponse(HttpResponseMessage const &response, const OriginPolicy originPolicy) + // See 10.7.4 of https://fetch.spec.whatwg.org/#http-network-or-cache-fetch + void OriginPolicyHttpFilter::ValidateResponse(HttpResponseMessage const& response, const OriginPolicy originPolicy) const { - bool removeAllCookies = false; - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + bool removeAllCookies = false; + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || originPolicy == OriginPolicy::CrossOriginResourceSharing) { - auto controlValues = ExtractAccessControlValues(response.Headers()); - auto props = response.RequestMessage().Properties(); - auto withCredentials = props.Lookup(L"RequestArgs").try_as()->WithCredentials; + auto controlValues = ExtractAccessControlValues(response.Headers()); + auto props = response.RequestMessage().Properties(); + auto withCredentials = props.Lookup(L"RequestArgs").try_as()->WithCredentials; - if (GetRuntimeOptionBool("Http.StrictOriginCheckSimpleCors") && + if (GetRuntimeOptionBool("Http.StrictOriginCheckSimpleCors") && originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { - bool originAllowed = false; - for (const auto &header : response.Headers()) { - if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { - originAllowed |= L"*" == header.Value() || s_origin == Uri{header.Value()}; + bool originAllowed = false; + for (const auto& header : response.Headers()) { + if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { + originAllowed |= L"*" == header.Value() || s_origin == Uri{ header.Value() }; + } } - } - if (!originAllowed) { - throw hresult_error{E_INVALIDARG, L"The server does not support CORS or the origin is not allowed"}; + if (!originAllowed) { + throw hresult_error{ E_INVALIDARG, L"The server does not support CORS or the origin is not allowed" }; + } } - } else { - ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props); - } - - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { - // Filter out response headers that are not in the Simple CORS whitelist - std::queue nonSimpleNames; - for (const auto &header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend()) - nonSimpleNames.push(header.Key()); + else { + ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props); } - while (!nonSimpleNames.empty()) { - response.Headers().Remove(nonSimpleNames.front()); - nonSimpleNames.pop(); + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { + // Filter out response headers that are not in the Simple CORS whitelist + std::queue nonSimpleNames; + for (const auto& header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend()) + nonSimpleNames.push(header.Key()); + } + + while (!nonSimpleNames.empty()) { + response.Headers().Remove(nonSimpleNames.front()); + nonSimpleNames.pop(); + } } - } else { - // Filter out response headers that are not simple headers and not in expose list + else { + // Filter out response headers that are not simple headers and not in expose list - // Keep simple headers and those found in the expose header list. - if (withCredentials || controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { - std::queue nonSimpleNonExposedHeaders; + // Keep simple headers and those found in the expose header list. + if (withCredentials || controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { + std::queue nonSimpleNonExposedHeaders; - for (const auto &header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend() && + for (const auto& header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend() && controlValues.ExposedHeaders.find(header.Key().c_str()) == controlValues.ExposedHeaders.cend()) { - nonSimpleNonExposedHeaders.push(header.Key()); + nonSimpleNonExposedHeaders.push(header.Key()); + } } - } - while (!nonSimpleNonExposedHeaders.empty()) { - response.Headers().Remove(nonSimpleNonExposedHeaders.front()); - nonSimpleNonExposedHeaders.pop(); + while (!nonSimpleNonExposedHeaders.empty()) { + response.Headers().Remove(nonSimpleNonExposedHeaders.front()); + nonSimpleNonExposedHeaders.pop(); + } } } - } - - // When withCredentials is false, request cannot include cookies. Also, cookies will be ignored in responses. - removeAllCookies = !withCredentials && GetRuntimeOptionBool("Http.RemoveCookiesFromResponse"); - } // originPolicy == SimpleCrossOriginResourceSharing || CrossOriginResourceSharing - - // Don't expose HttpOnly cookies to JavaScript - RemoveHttpOnlyCookiesFromResponseHeaders(response, removeAllCookies); -} -ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage const &request) const { - auto coRequest = request; + // When withCredentials is false, request cannot include cookies. Also, cookies will be ignored in responses. + removeAllCookies = !withCredentials && GetRuntimeOptionBool("Http.RemoveCookiesFromResponse"); + } // originPolicy == SimpleCrossOriginResourceSharing || CrossOriginResourceSharing - HttpRequestMessage preflightRequest; + // Don't expose HttpOnly cookies to JavaScript + RemoveHttpOnlyCookiesFromResponseHeaders(response, removeAllCookies); + } - // Section 4.8.2 https://fetch.spec.whatwg.org/#cors-preflight-fetch - preflightRequest.Method(HttpMethod::Options()); - preflightRequest.RequestUri(coRequest.RequestUri()); - preflightRequest.Headers().Insert(L"Accept", L"*/*"); - preflightRequest.Headers().Insert(L"Access-Control-Request-Method", coRequest.Method().ToString()); + ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage const& request) const { + auto coRequest = request; - auto headerNames = wstring{}; - auto headerItr = coRequest.Headers().begin(); - if (headerItr != coRequest.Headers().end()) { - headerNames += (*headerItr).Key(); + HttpRequestMessage preflightRequest; - while (++headerItr != coRequest.Headers().end()) - headerNames += L", " + (*headerItr).Key(); - } + // Section 4.8.2 https://fetch.spec.whatwg.org/#cors-preflight-fetch + preflightRequest.Method(HttpMethod::Options()); + preflightRequest.RequestUri(coRequest.RequestUri()); + preflightRequest.Headers().Insert(L"Accept", L"*/*"); + preflightRequest.Headers().Insert(L"Access-Control-Request-Method", coRequest.Method().ToString()); - if (coRequest.Content()) { - headerItr = coRequest.Content().Headers().begin(); - if (headerItr != coRequest.Content().Headers().end()) { + auto headerNames = wstring{}; + auto headerItr = coRequest.Headers().begin(); + if (headerItr != coRequest.Headers().end()) { headerNames += (*headerItr).Key(); - while (++headerItr != coRequest.Content().Headers().end()) + while (++headerItr != coRequest.Headers().end()) headerNames += L", " + (*headerItr).Key(); } - } - preflightRequest.Headers().Insert(L"Access-Control-Request-Headers", headerNames); - preflightRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); - preflightRequest.Headers().Insert(L"Sec-Fetch-Mode", L"CORS"); + if (coRequest.Content()) { + headerItr = coRequest.Content().Headers().begin(); + if (headerItr != coRequest.Content().Headers().end()) { + headerNames += (*headerItr).Key(); - co_return {co_await m_innerFilter.SendRequestAsync(preflightRequest)}; -} + while (++headerItr != coRequest.Content().Headers().end()) + headerNames += L", " + (*headerItr).Key(); + } + } + + preflightRequest.Headers().Insert(L"Access-Control-Request-Headers", headerNames); + preflightRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); + preflightRequest.Headers().Insert(L"Sec-Fetch-Mode", L"CORS"); + + 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()) && + 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; + // 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; -} + return true; + } #pragma endregion IRedirectEventSource #pragma region IHttpFilter -ResponseOperation OriginPolicyHttpFilter::SendRequestAsync(HttpRequestMessage const &request) { - auto coRequest = request; + ResponseOperation OriginPolicyHttpFilter::SendRequestAsync(HttpRequestMessage const& request) { + auto coRequest = request; - // Set initial origin policy to global runtime option. - request.Properties().Insert(L"OriginPolicy", winrt::box_value(GetRuntimeOptionInt("Http.OriginPolicy"))); + // Set initial origin policy to global runtime option. + request.Properties().Insert(L"OriginPolicy", winrt::box_value(GetRuntimeOptionInt("Http.OriginPolicy"))); - // Allow only HTTP or HTTPS schemes - if (GetRuntimeOptionBool("Http.StrictScheme") && coRequest.RequestUri().SchemeName() != L"https" && + // Allow only HTTP or HTTPS schemes + if (GetRuntimeOptionBool("Http.StrictScheme") && coRequest.RequestUri().SchemeName() != L"https" && coRequest.RequestUri().SchemeName() != L"http") - throw hresult_error{E_INVALIDARG, L"Invalid URL scheme: [" + s_origin.SchemeName() + L"]"}; + throw hresult_error{ E_INVALIDARG, L"Invalid URL scheme: [" + s_origin.SchemeName() + L"]" }; - if (!GetRuntimeOptionBool("Http.OmitCredentials")) { - coRequest.Properties().Lookup(L"RequestArgs").as()->WithCredentials = false; - } + if (!GetRuntimeOptionBool("Http.OmitCredentials")) { + coRequest.Properties().Lookup(L"RequestArgs").as()->WithCredentials = false; + } - // Ensure absolute URL - coRequest.RequestUri(Uri{coRequest.RequestUri().AbsoluteCanonicalUri()}); + // Ensure absolute URL + coRequest.RequestUri(Uri{ coRequest.RequestUri().AbsoluteCanonicalUri() }); - auto originPolicy = ValidateRequest(coRequest); - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + auto originPolicy = ValidateRequest(coRequest); + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || originPolicy == OriginPolicy::CrossOriginResourceSharing) { - if (coRequest.RequestUri().UserName().size() > 0 || coRequest.RequestUri().Password().size() > 0) { - coRequest.RequestUri(Uri{coRequest.RequestUri().DisplayUri()}); + if (coRequest.RequestUri().UserName().size() > 0 || coRequest.RequestUri().Password().size() > 0) { + coRequest.RequestUri(Uri{ coRequest.RequestUri().DisplayUri() }); + } } - } - try { - // #9770 - Validate preflight cache - if (originPolicy == OriginPolicy::CrossOriginResourceSharing) { - // If inner filter can AllowRedirect, disable for preflight. - winrt::impl::com_ref baseFilter; - if (baseFilter = m_innerFilter.try_as()) { - baseFilter.AllowAutoRedirect(false); - } + try { + // #9770 - Validate preflight cache + if (originPolicy == OriginPolicy::CrossOriginResourceSharing) { + // If inner filter can AllowRedirect, disable for preflight. + winrt::impl::com_ref baseFilter; + if (baseFilter = m_innerFilter.try_as()) { + baseFilter.AllowAutoRedirect(false); + } - auto preflightResponse = co_await SendPreflightAsync(coRequest); + auto preflightResponse = co_await SendPreflightAsync(coRequest); - if (baseFilter) { - baseFilter.AllowAutoRedirect(true); - } + if (baseFilter) { + baseFilter.AllowAutoRedirect(true); + } - ValidatePreflightResponse(coRequest, preflightResponse); - } + ValidatePreflightResponse(coRequest, preflightResponse); + } - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || originPolicy == OriginPolicy::CrossOriginResourceSharing) { - coRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); - } + coRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); + } - auto response = co_await m_innerFilter.SendRequestAsync(coRequest); + auto response = co_await m_innerFilter.SendRequestAsync(coRequest); - ValidateResponse(response, originPolicy); + ValidateResponse(response, originPolicy); - co_return response; + co_return response; - } catch (hresult_error const &e) { - throw e; - } catch (const std::exception &e) { - throw hresult_error{E_FAIL, to_hstring(e.what())}; - } catch (...) { - throw hresult_error{E_FAIL, L"Unspecified error processing Origin Policy request"}; + } + catch (hresult_error const& e) { + throw e; + } + catch (const std::exception& e) { + throw hresult_error{ E_FAIL, to_hstring(e.what()) }; + } + catch (...) { + throw hresult_error{ E_FAIL, L"Unspecified error processing Origin Policy request" }; + } } -} #pragma endregion IHttpFilter From e4fb0caa435294aaea96e0b7baea8497c2b47267 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Tue, 6 Dec 2022 23:22:16 -0800 Subject: [PATCH 11/45] Remove change file --- ...ative-windows-ed8de10f-3224-4e5d-8624-ff9f24b9cbb1.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 change/react-native-windows-ed8de10f-3224-4e5d-8624-ff9f24b9cbb1.json diff --git a/change/react-native-windows-ed8de10f-3224-4e5d-8624-ff9f24b9cbb1.json b/change/react-native-windows-ed8de10f-3224-4e5d-8624-ff9f24b9cbb1.json deleted file mode 100644 index 526d80aee48..00000000000 --- a/change/react-native-windows-ed8de10f-3224-4e5d-8624-ff9f24b9cbb1.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "prerelease", - "comment": "Adds missing headers for HttpRequestHeaderCollection", - "packageName": "react-native-windows", - "email": "erozell@outlook.com", - "dependentChangeType": "patch" -} From 8b2e1f97c8256ad33f25a83353946798d9bf2172 Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Mon, 15 Aug 2022 11:52:16 -0400 Subject: [PATCH 12/45] 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 --- ...-b0a84e25-6abf-44f7-ab49-48c8982eb792.json | 7 ++++ vnext/Shared/Networking/WinRTHttpResource.cpp | 42 +++++++++++++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 change/react-native-windows-b0a84e25-6abf-44f7-ab49-48c8982eb792.json diff --git a/change/react-native-windows-b0a84e25-6abf-44f7-ab49-48c8982eb792.json b/change/react-native-windows-b0a84e25-6abf-44f7-ab49-48c8982eb792.json new file mode 100644 index 00000000000..cea86ab621c --- /dev/null +++ b/change/react-native-windows-b0a84e25-6abf-44f7-ab49-48c8982eb792.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Skip user agent HTTP header validation", + "packageName": "react-native-windows", + "email": "erozell@outlook.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Shared/Networking/WinRTHttpResource.cpp b/vnext/Shared/Networking/WinRTHttpResource.cpp index 24d1c2ff429..8ff66df822a 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.cpp +++ b/vnext/Shared/Networking/WinRTHttpResource.cpp @@ -241,9 +241,45 @@ WinRTHttpResource::PerformSendRequest(HttpMethod&& method, Uri&& rtUri, IInspect string contentEncoding; string contentLength; - auto coRequest = co_await CreateRequest(std::move(coMethod), std::move(coUri), props); - if (!coRequest) { - co_return; + // 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 if (boost::iequals(header.first.c_str(), "User-Agent")) { + 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 User-Agent"); + } + } + 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 URI handler is available, it takes over request processing. From 532bdea9dfaa92c73ae2f313d339b5ebf1c8f88a Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Tue, 6 Dec 2022 23:27:39 -0800 Subject: [PATCH 13/45] Remove change file --- ...ative-windows-b0a84e25-6abf-44f7-ab49-48c8982eb792.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 change/react-native-windows-b0a84e25-6abf-44f7-ab49-48c8982eb792.json diff --git a/change/react-native-windows-b0a84e25-6abf-44f7-ab49-48c8982eb792.json b/change/react-native-windows-b0a84e25-6abf-44f7-ab49-48c8982eb792.json deleted file mode 100644 index cea86ab621c..00000000000 --- a/change/react-native-windows-b0a84e25-6abf-44f7-ab49-48c8982eb792.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "prerelease", - "comment": "Skip user agent HTTP header validation", - "packageName": "react-native-windows", - "email": "erozell@outlook.com", - "dependentChangeType": "patch" -} From 60501c84702533c93ce11d1cd6bac9bb609bcda8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20C=C3=A9sar=20Rocha?= Date: Fri, 16 Sep 2022 13:10:43 -0700 Subject: [PATCH 14/45] 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 --- ...-d5cf0017-99f3-40eb-856e-25f6e85f6fa2.json | 7 + .../HttpOriginPolicyIntegrationTest.cpp | 1280 ++++++++--------- .../HttpResourceIntegrationTests.cpp | 479 ++---- vnext/Shared/Modules/HttpModule.cpp | 4 +- vnext/Shared/Networking/IHttpResource.h | 14 +- .../Networking/OriginPolicyHttpFilter.cpp | 1195 ++++++++------- .../Shared/Networking/RedirectHttpFilter.cpp | 9 - vnext/Shared/Networking/WinRTHttpResource.cpp | 528 +++---- vnext/Shared/Networking/WinRTHttpResource.h | 54 +- vnext/Shared/Shared.vcxitems.filters | 12 + 10 files changed, 1647 insertions(+), 1935 deletions(-) create mode 100644 change/react-native-windows-d5cf0017-99f3-40eb-856e-25f6e85f6fa2.json diff --git a/change/react-native-windows-d5cf0017-99f3-40eb-856e-25f6e85f6fa2.json b/change/react-native-windows-d5cf0017-99f3-40eb-856e-25f6e85f6fa2.json new file mode 100644 index 00000000000..0bcdf5a011b --- /dev/null +++ b/change/react-native-windows-d5cf0017-99f3-40eb-856e-25f6e85f6fa2.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Implement HTTP redirection", + "packageName": "react-native-windows", + "email": "julio.rocha@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp b/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp index 369b57e6596..4a2e08b8f93 100644 --- a/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp +++ b/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp @@ -58,19 +58,8 @@ namespace Microsoft::React::Test { struct ClientParams { - promise ContentPromise; - string ErrorMessage; - IHttpResource::Response Response; - string ResponseContent; - http::verb Method; - IHttpResource::Headers RequestHeaders; - bool WithCredentials{ false }; - - ClientParams(http::verb method, IHttpResource::Headers&& headers) - : Method{ method } - , RequestHeaders{ std::move(headers) } - { - } + // Don't use move constructor in case of multiple requests + return { serverArgs.Response }; }; std::shared_ptr CreateServer(ServerParams& serverArgs, ClientParams& clientArgs) noexcept @@ -119,34 +108,30 @@ namespace Microsoft::React::Test { void TestOriginPolicyWithRedirect(ServerParams& server1Args, ServerParams& server2Args, ClientParams& clientArgs, bool shouldSucceed) { - auto server1 = CreateServer(server1Args, clientArgs); - auto server2 = CreateServer(server2Args, clientArgs); - - server1->Start(); - server2->Start(); - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&clientArgs](int64_t, IHttpResource::Response&& response) - { - clientArgs.Response = std::move(response); - }); - resource->SetOnData([&clientArgs](int64_t, string&& content) - { - clientArgs.ResponseContent = std::move(content); - clientArgs.ContentPromise.set_value(); + clientArgs.Response = std::move(response); }); + resource->SetOnData([&clientArgs](int64_t, string&& content) + { + clientArgs.ResponseContent = std::move(content); + clientArgs.ContentPromise.set_value(); + }); + resource->SetOnError([&clientArgs](int64_t, string&& message, bool) + { + clientArgs.ErrorMessage = std::move(message); + clientArgs.ContentPromise.set_value(); + }); resource->SendRequest( - string{http::to_string(clientArgs.Method).data()}, - string{server1Args.Url}, + string{ http::to_string(clientArgs.Method).data() }, + string{ server1Args.Url }, 0, /*requestId*/ std::move(clientArgs.RequestHeaders), - {}, /*data*/ + dynamic::object("string", ""), /*data*/ "text", false, /*useIncrementalUpdates*/ 0, /*timeout*/ clientArgs.WithCredentials, /*withCredentials*/ - [](int64_t){} /*reactCallback*/ + [](int64_t) {} /*reactCallback*/ ); clientArgs.ContentPromise.get_future().wait(); @@ -160,7 +145,7 @@ namespace Microsoft::React::Test { //TODO: chose server? // We assume 2-server tests will always redirect so the final response will come from server 2. Assert::AreEqual(server2Args.Response.result_int(), static_cast(clientArgs.Response.StatusCode)); - Assert::AreEqual({"RESPONSE_CONTENT"}, clientArgs.ResponseContent); + Assert::AreEqual({ "RESPONSE_CONTENT" }, clientArgs.ResponseContent); } else { @@ -176,75 +161,42 @@ namespace Microsoft::React::Test { auto resource = IHttpResource::Make(); resource->SetOnResponse([&clientArgs](int64_t, IHttpResource::Response&& res) - { + { clientArgs.Response = std::move(res); - }); + }); resource->SetOnData([&clientArgs](int64_t, string&& content) - { - clientArgs.ResponseContent = std::move(content); - clientArgs.ContentPromise.set_value(); - }); - - resource->SendRequest( - string{ http::to_string(clientArgs.Method).data() }, - string{ server1Args.Url }, - 0, /*requestId*/ - std::move(clientArgs.RequestHeaders), - {}, /*data*/ - "text", - false, /*useIncrementalUpdates*/ - 0, /*timeout*/ - clientArgs.WithCredentials, /*withCredentials*/ - [](int64_t) {} /*reactCallback*/ - ); - - clientArgs.ContentPromise.get_future().wait(); - - server2->Stop(); - server1->Stop(); - - if (shouldSucceed) - { - Assert::AreEqual({}, clientArgs.ErrorMessage); - //TODO: chose server? - // We assume 2-server tests will always redirect so the final response will come from server 2. - Assert::AreEqual(server2Args.Response.result_int(), static_cast(clientArgs.Response.StatusCode)); - Assert::AreEqual({ "RESPONSE_CONTENT" }, clientArgs.ResponseContent); - } - else { - Assert::AreNotEqual({}, clientArgs.ErrorMessage); - } - } - - void TestOriginPolicy(ServerParams& serverArgs, ClientParams& clientArgs, bool shouldSucceed) - { - auto server = CreateServer(serverArgs, clientArgs); + clientArgs.ResponseContent = std::move(content); + clientArgs.ContentPromise.set_value(); + }); + resource->SetOnError([&clientArgs](int64_t, string&& message, bool) + { + auto server = CreateServer(serverArgs, clientArgs); - server->Start(); + server->Start(); - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&clientArgs](int64_t, IHttpResource::Response&& res) - { - clientArgs.Response = std::move(res); - }); - resource->SetOnData([&clientArgs](int64_t, string&& content) - { - clientArgs.ResponseContent = std::move(content); - clientArgs.ContentPromise.set_value(); - }); - resource->SetOnError([&clientArgs](int64_t, string&& message, bool) - { - clientArgs.ErrorMessage = std::move(message); - clientArgs.ContentPromise.set_value(); - }); + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&clientArgs](int64_t, IHttpResource::Response&& res) + { + clientArgs.Response = std::move(res); + }); + resource->SetOnData([&clientArgs](int64_t, string&& content) + { + clientArgs.ResponseContent = std::move(content); + clientArgs.ContentPromise.set_value(); + }); + resource->SetOnError([&clientArgs](int64_t, string&& message, bool) + { + clientArgs.ErrorMessage = std::move(message); + clientArgs.ContentPromise.set_value(); + }); resource->SendRequest( - string{http::to_string(clientArgs.Method).data()}, - string{serverArgs.Url}, + string{ http::to_string(clientArgs.Method).data() }, + string{ serverArgs.Url }, 0, /*requestId*/ std::move(clientArgs.RequestHeaders), - {}, /*data*/ + dynamic::object("string", ""), /*data*/ "text", false, /*useIncrementalUpdates*/ 0, /*timeout*/ @@ -252,687 +204,699 @@ namespace Microsoft::React::Test { [](int64_t) {} /*reactCallback*/ ); - clientArgs.ContentPromise.get_future().wait(); - server->Stop(); - - if (shouldSucceed) - { - Assert::AreEqual({}, clientArgs.ErrorMessage); - Assert::AreEqual(serverArgs.Response.result_int(), static_cast(clientArgs.Response.StatusCode)); - Assert::AreEqual({ "RESPONSE_CONTENT" }, clientArgs.ResponseContent); - } - else - { - Assert::AreNotEqual({}, clientArgs.ErrorMessage); - } - } + clientArgs.ContentPromise.get_future().wait(); + server->Stop(); - TEST_METHOD_CLEANUP(MethodCleanup) + if (shouldSucceed) { - // 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++; + Assert::AreEqual({}, clientArgs.ErrorMessage); + Assert::AreEqual(serverArgs.Response.result_int(), static_cast(clientArgs.Response.StatusCode)); + Assert::AreEqual({ "RESPONSE_CONTENT" }, clientArgs.ResponseContent); } - - 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 - TEST_IGNORE() - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(NoCorsForbiddenMethodSucceeds) + else { - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); - - constexpr uint16_t port{ 5556 }; - constexpr char url[]{ "http://localhost:5556" }; - - string error; - string getContent; - IHttpResource::Response getResponse; - promise getDataPromise; + Assert::AreNotEqual({}, clientArgs.ErrorMessage); + } + } - auto server = make_shared(port); - server->Callbacks().OnOptions = [&url](const DynamicRequest& request) -> ResponseWrapper + TEST_METHOD_CLEANUP(MethodCleanup) { - EmptyResponse response; - response.result(http::status::accepted); + // 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++; + } - response.set(http::field::access_control_allow_credentials, "false"); - response.set(http::field::access_control_allow_headers, "ValidHeader"); - response.set(http::field::access_control_allow_methods, "GET, POST, DELETE, PATCH"); - response.set(http::field::access_control_allow_origin, url); + 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++; + } - return { std::move(response) }; - }; - server->Callbacks().OnTrace = [](const DynamicRequest& request) -> ResponseWrapper + 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 + TEST_IGNORE() + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(NoCorsForbiddenMethodSucceeds) { - StringResponse response; - response.result(http::status::ok); - response.body() = "GET_CONTENT"; + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); - return { std::move(response) }; - }; - server->Start(); + constexpr uint16_t port{ 5556 }; + constexpr char url[]{ "http://localhost:5556" }; - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&getResponse](int64_t, IHttpResource::Response&& res) - { - getResponse = std::move(res); - }); - resource->SetOnData([&getDataPromise, &getContent](int64_t, string&& content) - { - getContent = std::move(content); - getDataPromise.set_value(); - }); - resource->SetOnError([&server, &error, &getDataPromise](int64_t, string&& message, bool) + string error; + string getContent; + IHttpResource::Response getResponse; + promise getDataPromise; + + auto server = make_shared(port); + server->Callbacks().OnOptions = [&url](const DynamicRequest& request) -> ResponseWrapper { - error = std::move(message); - getDataPromise.set_value(); - }); - - resource->SendRequest( - "TRACE", - url, - 0, /*requestId*/ + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); + + constexpr uint16_t port{ 5556 }; + constexpr char url[]{ "http://localhost:5556" }; + + string error; + string getContent; + IHttpResource::Response getResponse; + promise getDataPromise; + + auto server = make_shared(port); + server->Callbacks().OnOptions = [&url](const DynamicRequest& request) -> ResponseWrapper + { + EmptyResponse response; + response.result(http::status::accepted); + + response.set(http::field::access_control_allow_credentials, "false"); + response.set(http::field::access_control_allow_headers, "ValidHeader"); + response.set(http::field::access_control_allow_methods, "GET, POST, DELETE, PATCH"); + response.set(http::field::access_control_allow_origin, url); + + return { std::move(response) }; + }; + server->Callbacks().OnTrace = [](const DynamicRequest& request) -> ResponseWrapper + { + StringResponse response; + response.result(http::status::ok); + response.body() = "GET_CONTENT"; + + return { std::move(response) }; + }; + server->Start(); + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&getResponse](int64_t, IHttpResource::Response&& res) + { + getResponse = std::move(res); + }); + resource->SetOnData([&getDataPromise, &getContent](int64_t, string&& content) + { + getContent = std::move(content); + getDataPromise.set_value(); + }); + resource->SetOnError([&server, &error, &getDataPromise](int64_t, string&& message, bool) + { + error = std::move(message); + getDataPromise.set_value(); + }); + + resource->SendRequest( + "TRACE", + url, + 0, /*requestId*/ + { + {"ValidHeader", "AnyValue"} + }, + {}, /*data*/ + //{} /*bodyData*/, + "text", + false /*useIncrementalUpdates*/, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {} /*callback*/ + ); + + getDataPromise.get_future().wait(); + server->Stop(); + + Assert::AreEqual({}, error); + Assert::AreEqual(200, static_cast(getResponse.StatusCode)); + Assert::AreEqual({ "GET_CONTENT" }, getContent); + }// NoCorsForbiddenMethodSucceeds + + BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsForbiddenMethodFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(SimpleCorsForbiddenMethodFails) { - {"ValidHeader", "AnyValue"} - }, - {}, /*data*/ - //{} /*bodyData*/, - "text", - false /*useIncrementalUpdates*/, - 0 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {} /*callback*/ - ); - - getDataPromise.get_future().wait(); - server->Stop(); - - Assert::AreEqual({}, error); - Assert::AreEqual(200, static_cast(getResponse.StatusCode)); - Assert::AreEqual({ "GET_CONTENT" }, getContent); - }// NoCorsForbiddenMethodSucceeds - - BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsForbiddenMethodFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(SimpleCorsForbiddenMethodFails) - { - StringResponse response; - response.result(http::status::ok); - response.body() = "GET_CONTENT"; - - return { std::move(response) }; - }; - server->Start(); - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&getResponse](int64_t, IHttpResource::Response&& res) - { - getResponse = std::move(res); - }); - resource->SetOnData([&getDataPromise, &getContent](int64_t, string&& content) - { - getContent = std::move(content); + StringResponse response; + response.result(http::status::ok); + response.body() = "GET_CONTENT"; + + return { std::move(response) }; + }; + server->Start(); + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&getResponse](int64_t, IHttpResource::Response&& res) + { + getResponse = std::move(res); + }); + resource->SetOnData([&getDataPromise, &getContent](int64_t, string&& content) + { + getContent = std::move(content); getDataPromise.set_value(); - }); - resource->SetOnError([&server, &error, &getDataPromise](int64_t, string&& message) - { - error = std::move(message); + }); + resource->SetOnError([&server, &error, &getDataPromise](int64_t, string&& message, bool) + { + error = std::move(message); getDataPromise.set_value(); - }); - - resource->SendRequest( - "TRACE", - url, - 0, /*requestId*/ + }); + + resource->SendRequest( + "TRACE", + url, + 0, /*requestId*/ + { + {"ValidHeader", "AnyValue"} + }, + dynamic::object("string", ""), /*data*/ + "text", + false /*useIncrementalUpdates*/, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {} /*callback*/ + ); + + getDataPromise.get_future().wait(); + server->Stop(); + + Assert::AreEqual({}, error); + Assert::AreEqual(200, static_cast(getResponse.StatusCode)); + Assert::AreEqual({ "GET_CONTENT" }, getContent); + }// NoCorsForbiddenMethodSucceeds + + BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsForbiddenMethodFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(SimpleCorsForbiddenMethodFails) { - {"ValidHeader", "AnyValue"} - }, - {}, /*data*/ - //{} /*bodyData*/, - "text", - false /*useIncrementalUpdates*/, - 0 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {} /*callback*/ - ); + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); - getDataPromise.get_future().wait(); - server->Stop(); - - Assert::AreEqual({}, error); - Assert::AreEqual(200, static_cast(getResponse.StatusCode)); - Assert::AreEqual({ "GET_CONTENT" }, getContent); - }// NoCorsForbiddenMethodSucceeds - - BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsForbiddenMethodFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(SimpleCorsForbiddenMethodFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); + ClientParams clientArgs(http::verb::connect, { {"Content-Type", "text/plain"} }); - ClientParams clientArgs(http::verb::connect, {{"Content-Type", "text/plain"}}); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// SimpleCorsForbiddenMethodFails + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// SimpleCorsForbiddenMethodFails - //NoCors_ForbiddenMethodConnect_Failed + //NoCors_ForbiddenMethodConnect_Failed - BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsCrossOriginFetchRequestSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(NoCorsCrossOriginFetchRequestSucceeds) - { - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); + BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsCrossOriginFetchRequestSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(NoCorsCrossOriginFetchRequestSucceeds) + { + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::ok); + ServerParams serverArgs(s_port); + serverArgs.Response.result(http::status::ok); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); + ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// NoCorsCrossOriginFetchRequestSucceeds + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// NoCorsCrossOriginFetchRequestSucceeds - //NoCors_CrossOriginFetchRequestWithTimeout_Succeeded //TODO: Implement timeout + BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsCrossOriginPatchSucceededs) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(NoCorsCrossOriginPatchSucceededs) + { + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); - BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsCrossOriginPatchSucceededs) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(NoCorsCrossOriginPatchSucceededs) - { - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); + ServerParams serverArgs(s_port); + serverArgs.Response.result(http::status::ok); - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::ok); + ClientParams clientArgs(http::verb::patch, { { "Content-Type", "text/plain" } }); - ClientParams clientArgs(http::verb::patch, {{ "Content-Type", "text/plain" }}); + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// NoCorsCrossOriginPatchSucceededs - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// NoCorsCrossOriginPatchSucceededs + // Simple-Cors — Prevents the method from being anything other than HEAD, GET or POST, + // and the headers from being anything other than simple headers (CORS safe listed headers). + // If any ServiceWorkers intercept these requests, they may not add or override any headers except for those that are simple headers. + // In addition, JavaScript may not access any properties of the resulting Response. + // This ensures that ServiceWorkers do not affect the semantics of the Web and prevents security and privacy issues arising from leaking data across domains. + BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsSameOriginSucceededs) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(SimpleCorsSameOriginSucceededs) + { + ServerParams serverArgs(s_port); + serverArgs.Response.result(http::status::ok); - // Simple-Cors — Prevents the method from being anything other than HEAD, GET or POST, - // and the headers from being anything other than simple headers (CORS safe listed headers). - // If any ServiceWorkers intercept these requests, they may not add or override any headers except for those that are simple headers. - // In addition, JavaScript may not access any properties of the resulting Response. - // This ensures that ServiceWorkers do not affect the semantics of the Web and prevents security and privacy issues arising from leaking data across domains. - BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsSameOriginSucceededs) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(SimpleCorsSameOriginSucceededs) - { - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::ok); + ClientParams clientArgs(http::verb::patch, { { "Content-Type", "text/plain" } }); - ClientParams clientArgs(http::verb::patch, {{ "Content-Type", "text/plain" }}); + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// SimpleCorsSameOriginSucceededs + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// SimpleCorsSameOriginSucceededs - BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsCrossOriginFetchFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(SimpleCorsCrossOriginFetchFails) - { - ServerParams serverArgs(s_port); + BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsCrossOriginFetchFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(SimpleCorsCrossOriginFetchFails) + { + ServerParams serverArgs(s_port); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/html" }}); // text/html is a non-simple value + ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/html" } }); // text/html is a non-simple value - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// SimpleCorsCrossOriginFetchFails + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginRequestSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsSameOriginRequestSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::ok); + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// SimpleCorsCrossOriginFetchFails - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); // text/plain is a non-simple header + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginRequestSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsSameOriginRequestSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Response.result(http::status::ok); - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// FullCorsSameOriginRequestSucceeds + ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); // text/plain is a non-simple header - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginAllowOriginWildcardSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginAllowOriginWildcardSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, "*"); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Response.result(http::status::accepted); - serverArgs.Response.set(http::field::access_control_allow_origin, "*"); - serverArgs.Response.set(http::field::access_control_allow_credentials, "true"); - - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); // text/plain is a non-simple header - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// FullCorsCrossOriginAllowOriginWildcardSucceeds - - // With CORS, Cross-Origin Resource Sharing, the server can decide what origins are permitted to read information from the client. - // Additionally, for non-simple requests, client should preflight the request through the HTTP Options request, and only send the - // actual request after the server has responded that the desired headers are supported. - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMatchingOriginSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginMatchingOriginSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Response.result(http::status::accepted); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Response.set(http::field::access_control_allow_credentials, "true"); + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); // text/plain is a non-simple header + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// FullCorsSameOriginRequestSucceeds - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginAllowOriginWildcardSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginAllowOriginWildcardSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, "*"); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Response.result(http::status::accepted); + serverArgs.Response.set(http::field::access_control_allow_origin, "*"); + serverArgs.Response.set(http::field::access_control_allow_credentials, "true"); + + ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); // text/plain is a non-simple header + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// FullCorsCrossOriginAllowOriginWildcardSucceeds + + // With CORS, Cross-Origin Resource Sharing, the server can decide what origins are permitted to read information from the client. + // Additionally, for non-simple requests, client should preflight the request through the HTTP Options request, and only send the + // actual request after the server has responded that the desired headers are supported. + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMatchingOriginSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginMatchingOriginSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Response.result(http::status::accepted); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Response.set(http::field::access_control_allow_credentials, "true"); - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// FullCorsCrossOriginMatchingOriginSucceeds + ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); // text/plain is a non-simple header - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginWithCredentialsFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginWithCredentialsFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Preflight.set(http::field::access_control_allow_credentials, "true"); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); // application/text is a non-simple header - clientArgs.WithCredentials = true; + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// FullCorsCrossOriginMatchingOriginSucceeds - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - SetRuntimeOptionBool("Http.OmitCredentials", true); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginWithCredentialsFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginWithCredentialsFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Preflight.set(http::field::access_control_allow_credentials, "true"); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// FullCorsCrossOriginWithCredentialsFails + ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); // application/text is a non-simple header + clientArgs.WithCredentials = true; + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + SetRuntimeOptionBool("Http.OmitCredentials", true); - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMissingCorsHeadersFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginMissingCorsHeadersFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.erase(http::field::access_control_allow_methods); - serverArgs.Preflight.result(http::status::not_implemented); + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// FullCorsCrossOriginWithCredentialsFails - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); // application/text is a non-simple header - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMissingCorsHeadersFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginMissingCorsHeadersFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.erase(http::field::access_control_allow_methods); + serverArgs.Preflight.result(http::status::not_implemented); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// FullCorsCrossOriginMissingCorsHeadersFails + ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); // application/text is a non-simple header - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMismatchedCorsHeaderFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginMismatchedCorsHeaderFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Response.result(http::status::accepted); - serverArgs.Response.set(http::field::access_control_allow_origin, "http://other.example.rnw"); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); // application/text is a non-simple header + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// FullCorsCrossOriginMissingCorsHeadersFails - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMismatchedCorsHeaderFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginMismatchedCorsHeaderFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Response.result(http::status::accepted); + serverArgs.Response.set(http::field::access_control_allow_origin, "http://other.example.rnw"); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// FullCorsCrossOriginMismatchedCorsHeaderFails + ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); // application/text is a non-simple header - // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginCheckFailsOnPreflightRedirectFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginCheckFailsOnPreflightRedirectFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Preflight.set(http::field::location, "http://any-host.extension"); - serverArgs.Preflight.result(http::status::moved_permanently); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// FullCorsCrossOriginMismatchedCorsHeaderFails - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginCheckFailsOnPreflightRedirectFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginCheckFailsOnPreflightRedirectFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Preflight.set(http::field::location, "http://any-host.extension"); + serverArgs.Preflight.result(http::status::moved_permanently); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// FullCorsCrossOriginCheckFailsOnPreflightRedirectFails + ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCorsCheckFailsOnResponseRedirectFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCorsCheckFailsOnResponseRedirectFails) - { - ServerParams serverArgs(s_port); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - // server1 allowed origin header includes http://example.com - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// FullCorsCrossOriginCheckFailsOnPreflightRedirectFails - // This is a CORS request to server1, but server1 redirects the request to server2 - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - // Redir server's URL - serverArgs.Response.set(http::field::location, "http://localhost:6666"); - serverArgs.Response.set(http::field::server, "BaseServer"); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCorsCheckFailsOnResponseRedirectFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCorsCheckFailsOnResponseRedirectFails) + { + ServerParams serverArgs(s_port); - // Server2 does not set Access-Control-Allow-Origin for GET requests - ServerParams redirServerArgs(++s_port); - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::server, "RedirectServer"); + // server1 allowed origin header includes http://example.com + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); + // This is a CORS request to server1, but server1 redirects the request to server2 + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + // Redir server's URL + serverArgs.Response.set(http::field::location, "http://localhost:6666"); + serverArgs.Response.set(http::field::server, "BaseServer"); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + // Server2 does not set Access-Control-Allow-Origin for GET requests + ServerParams redirServerArgs(++s_port); + redirServerArgs.Response.result(http::status::accepted); + redirServerArgs.Response.set(http::field::server, "RedirectServer"); - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); - }// FullCorsCorsCheckFailsOnResponseRedirectFails + ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginToSameOriginRedirectSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsSameOriginToSameOriginRedirectSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::location, serverArgs.Url); - serverArgs.Response.result(http::status::accepted); + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); + }// FullCorsCorsCheckFailsOnResponseRedirectFails - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginToSameOriginRedirectSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsSameOriginToSameOriginRedirectSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::location, serverArgs.Url); + serverArgs.Response.result(http::status::accepted); - TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); - } // FullCorsSameOriginToSameOriginRedirectSucceeds + ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginToCrossOriginRedirectSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsSameOriginToCrossOriginRedirectSucceeds) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); + TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); + } // FullCorsSameOriginToSameOriginRedirectSucceeds - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::access_control_allow_origin, serverArgs.Url); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginToCrossOriginRedirectSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsSameOriginToCrossOriginRedirectSucceeds) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); + serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, redirServerArgs.Url); - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + redirServerArgs.Response.result(http::status::accepted); + redirServerArgs.Response.set(http::field::access_control_allow_origin, serverArgs.Url); - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); - } // FullCorsSameOriginToCrossOriginRedirectSucceeds + ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); - //TODO: Seems to redirect to exact same resource. Implement second resource in same server. - // Redirects a cross origin request to cross origin request on the same server - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToCrossOriginRedirectSucceeds) - TEST_IGNORE() - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToCrossOriginRedirectSucceeds) - { - ServerParams serverArgs(s_port); - //ServerParams redirServerArgs(++s_port); + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, serverArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); + } // FullCorsSameOriginToCrossOriginRedirectSucceeds - //redirServerArgs.Response.result(http::status::accepted); - //redirServerArgs.Response.set(http::field::access_control_allow_origin, serverArgs.Url); + //TODO: Seems to redirect to exact same resource. Implement second resource in same server. + // Redirects a cross origin request to cross origin request on the same server + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToCrossOriginRedirectSucceeds) + TEST_IGNORE() + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToCrossOriginRedirectSucceeds) + { + ServerParams serverArgs(s_port); + //ServerParams redirServerArgs(++s_port); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); + serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, serverArgs.Url); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + //redirServerArgs.Response.result(http::status::accepted); + //redirServerArgs.Response.set(http::field::access_control_allow_origin, serverArgs.Url); - TestOriginPolicy(serverArgs, /*redirServerArgs, */clientArgs, s_shouldSucceed); - } // FullCorsCrossOriginToCrossOriginRedirectSucceeds + ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); - // The initial request gets redirected back to the original origin, - // but it will lack the Access-Control-Allow-Origin header. - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToOriginalOriginRedirectFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToOriginalOriginRedirectFails) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, redirServerArgs.Url); + TestOriginPolicy(serverArgs, /*redirServerArgs, */clientArgs, s_shouldSucceed); + } // FullCorsCrossOriginToCrossOriginRedirectSucceeds - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); + // The initial request gets redirected back to the original origin, + // but it will lack the Access-Control-Allow-Origin header. + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToOriginalOriginRedirectFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToOriginalOriginRedirectFails) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); - SetRuntimeOptionString("Http.GlobalOrigin", redirServerArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, redirServerArgs.Url); + serverArgs.Response.set(http::field::access_control_allow_origin, redirServerArgs.Url); - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); - } // FullCorsCrossOriginToOriginalOriginRedirectFails + ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); - // Redirects cross origin request to server1 to cross origin request to server2 - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + SetRuntimeOptionString("Http.GlobalOrigin", redirServerArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); + } // FullCorsCrossOriginToOriginalOriginRedirectFails - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::access_control_allow_origin, "*"); + // Redirects cross origin request to server1 to cross origin request to server2 + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, redirServerArgs.Url); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + redirServerArgs.Response.result(http::status::accepted); + redirServerArgs.Response.set(http::field::access_control_allow_origin, "*"); - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); - } // FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds + ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); - 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) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - // server1 redirects the GET request to server2 - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); + } // FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds - // Since redirect tainted the origin, the server has to allow all origins for CORS to succeed - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::access_control_allow_origin, "*"); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); - // PATCH is not a simple method, so preflight is required for server1 - ClientParams clientArgs(http::verb::patch, {{ "Content-Type", "text/plain" }}); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + // server1 redirects the GET request to server2 + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, redirServerArgs.Url); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + // Since redirect tainted the origin, the server has to allow all origins for CORS to succeed + redirServerArgs.Response.result(http::status::accepted); + redirServerArgs.Response.set(http::field::access_control_allow_origin, "*"); - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); - } // FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds + // PATCH is not a simple method, so preflight is required for server1 + ClientParams clientArgs(http::verb::patch, { { "Content-Type", "text/plain" } }); - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); - - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - // server1 redirects the GET request to server2 - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - // Since redirect tainted the origin, the server does not know what origin to allow through a single value. - // Even if server successfully guessed the single value, it will still fail on the client side. - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - // PATCH is not a simple method, so preflight is required for server1 - ClientParams clientArgs(http::verb::patch, {{ "Content-Type", "text/plain" }}); - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); - } // FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails - - BEGIN_TEST_METHOD_ATTRIBUTE(FullCors304ForSimpleGetFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCors304ForSimpleGetFails) - { - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::not_modified); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - // PATCH is not a simple method, so preflight is required for server1 - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); + } // FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); + + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + // server1 redirects the GET request to server2 + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, redirServerArgs.Url); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + + // Since redirect tainted the origin, the server does not know what origin to allow through a single value. + // Even if server successfully guessed the single value, it will still fail on the client side. + redirServerArgs.Response.result(http::status::accepted); + redirServerArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + + // PATCH is not a simple method, so preflight is required for server1 + ClientParams clientArgs(http::verb::patch, { { "Content-Type", "text/plain" } }); + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); + } // FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails + + BEGIN_TEST_METHOD_ATTRIBUTE(FullCors304ForSimpleGetFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCors304ForSimpleGetFails) + { + ServerParams serverArgs(s_port); + serverArgs.Response.result(http::status::not_modified); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - } // FullCors304ForSimpleGetFails + // PATCH is not a simple method, so preflight is required for server1 + ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); - TEST_METHOD(FullCorsPreflightSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "ArbitraryHeader"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "ArbitraryHeader"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Response.result(http::status::ok); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - ClientParams clientArgs(http::verb::get, { {"Content-Type", "text/plain"}, {"ArbitraryHeader", "AnyValue"} }); - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); - }// FullCorsPreflightSucceeds - - // 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) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Preflight.set(http::field::access_control_allow_credentials, "true"); - serverArgs.Response.result(http::status::accepted); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); // application/text is a non-simple header - clientArgs.WithCredentials = true; - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); - }// FullCorsCrossOriginWithCredentialsSucceeds - - // "Host" is one of the forbidden headers for fetch - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsRequestWithHostHeaderFails) - // "Host" is not an accepted request header in WinRT. - TEST_IGNORE() - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsRequestWithHostHeaderFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Response.result(http::status::accepted); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }, { "Host", "http://sub.example.rnw" }}); + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + } // FullCors304ForSimpleGetFails - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + TEST_METHOD(FullCorsPreflightSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "ArbitraryHeader"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "ArbitraryHeader"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Response.result(http::status::ok); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + + ClientParams clientArgs(http::verb::get, { {"Content-Type", "text/plain"}, {"ArbitraryHeader", "AnyValue"} }); + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); + }// FullCorsPreflightSucceeds + + // 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) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginWithCredentialsSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Preflight.set(http::field::access_control_allow_credentials, "true"); + serverArgs.Response.result(http::status::accepted); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + + ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); // application/text is a non-simple header + clientArgs.WithCredentials = true; + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); + }// FullCorsCrossOriginWithCredentialsSucceeds + + // "Host" is one of the forbidden headers for fetch + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsRequestWithHostHeaderFails) + // "Host" is not an accepted request header in WinRT. + TEST_IGNORE() + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsRequestWithHostHeaderFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Response.result(http::status::accepted); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" }, { "Host", "http://sub.example.rnw" } }); - Assert::Fail(L"FIX!!! Passes for the worng reason. Error: 0x80070057 : 'Invalid HTTP headers.'"); - }// FullCorsRequestWithHostHeaderFails + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - BEGIN_TEST_METHOD_ATTRIBUTE(RequestWithProxyAuthorizationHeaderFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(RequestWithProxyAuthorizationHeaderFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Response.result(http::status::accepted); + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }, { "Proxy-Authorization", "Basic Zm9vOmJhcg==" }}); + Assert::Fail(L"FIX!!! Passes for the worng reason. Error: 0x80070057 : 'Invalid HTTP headers.'"); + }// FullCorsRequestWithHostHeaderFails - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + BEGIN_TEST_METHOD_ATTRIBUTE(RequestWithProxyAuthorizationHeaderFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(RequestWithProxyAuthorizationHeaderFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Response.result(http::status::accepted); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// RequestWithProxyAuthorizationHeaderFails + ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" }, { "Proxy-Authorization", "Basic Zm9vOmJhcg==" } }); - BEGIN_TEST_METHOD_ATTRIBUTE(ExceedingRedirectLimitFails) - TEST_IGNORE() - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(ExceedingRedirectLimitFails) - { - Assert::Fail(L"NOT IMPLEMENTED"); - }// ExceedingRedirectLimitFails + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); -}; + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// RequestWithProxyAuthorizationHeaderFails + }; -uint16_t HttpOriginPolicyIntegrationTest::s_port = 7777; + uint16_t HttpOriginPolicyIntegrationTest::s_port = 7777; }//namespace Microsoft::React::Test // clang-format on diff --git a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp index 772681d214b..90de185c76f 100644 --- a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp +++ b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp @@ -37,6 +37,9 @@ using Test::ResponseWrapper; namespace Microsoft::React::Test { +TEST_CLASS (HttpResourceIntegrationTest) { + static uint16_t s_port; + TEST_CLASS(HttpResourceIntegrationTest) { static uint16_t s_port; @@ -55,23 +58,13 @@ namespace Microsoft::React::Test { string error; int statusCode = 0; - auto server = make_shared(s_port); - server->Callbacks().OnGet = [&resPromise](const DynamicRequest& request) -> ResponseWrapper { - DynamicResponse response; - response.result(http::status::ok); - response.body() = Test::CreateStringResponseBody("some response content"); - - return { std::move(response) }; - }; - server->Start(); - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&resPromise, &statusCode](int64_t, IHttpResource::Response response) { - statusCode = static_cast(response.StatusCode); - }); - resource->SetOnData([&resPromise](int64_t, string&& content) { resPromise.set_value(); }); - resource->SetOnError([&resPromise, &error, &server](int64_t, string&& message, bool) { - error = std::move(message); + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&resPromise, &statusCode](int64_t, IHttpResource::Response response) { + statusCode = static_cast(response.StatusCode); + }); + resource->SetOnData([&resPromise](int64_t, string &&content) { resPromise.set_value(); }); + resource->SetOnError([&resPromise, &error, &server](int64_t, string &&message, bool) { + error = std::move(message); resPromise.set_value(); server->Stop(); @@ -123,9 +116,9 @@ namespace Microsoft::React::Test { resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { response = callbackResponse; rcPromise.set_value(); - }); - resource->SetOnError([&rcPromise, &error, &server](int64_t, string&& message, bool) { - error = std::move(message); + }); + resource->SetOnError([&rcPromise, &error, &server](int64_t, string &&message, bool) { + error = std::move(message); rcPromise.set_value(); server->Abort(); @@ -174,124 +167,9 @@ namespace Microsoft::React::Test { TEST_METHOD(RequestGetExplicitUserAgentSucceeds) { string url = "https://api.github.com/repos/microsoft/react-native-xaml"; - promise rcPromise; - string error; - IHttpResource::Response response; - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { - response = callbackResponse; - rcPromise.set_value(); - }); - resource->SetOnError([&rcPromise, &error](int64_t, string&& message, bool) { - error = std::move(message); - rcPromise.set_value(); - }); - - //clang-format off - resource->SendRequest( - "GET", - std::move(url), - 0, /*requestId*/ - { {"User-Agent", "React Native Windows"} }, - {}, /*data*/ - "text", /*responseType*/ - false, - 0 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {}); - //clang-format on - - rcPromise.get_future().wait(); - - Assert::AreEqual({}, error, L"Error encountered"); - Assert::AreEqual(static_cast(200), response.StatusCode); - } - - TEST_METHOD(RequestGetImplicitUserAgentSucceeds) { - string url = "https://api.github.com/repos/microsoft/react-native-windows"; - - promise rcPromise; - string error; - IHttpResource::Response response; - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { - response = callbackResponse; - rcPromise.set_value(); - }); - resource->SetOnError([&rcPromise, &error](int64_t, string&& message, bool) { - error = std::move(message); - rcPromise.set_value(); - }); - - MicrosoftReactSetRuntimeOptionString("Http.UserAgent", "React Native Windows"); - - //clang-format off - resource->SendRequest( - "GET", - std::move(url), - 0, /*requestId*/ - {}, /*headers*/ - {}, /*data*/ - "text", /*responseType*/ - false, - 0 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {}); - //clang-format on - - rcPromise.get_future().wait(); - - Assert::AreEqual({}, error, L"Error encountered"); - Assert::AreEqual(static_cast(200), response.StatusCode); - } - - TEST_METHOD(RequestGetMissingUserAgentFails) { - // string url = "http://localhost:" + std::to_string(s_port); - string url = "https://api.github.com/repos/microsoft/react-native-macos"; - - promise rcPromise; - string error; - IHttpResource::Response response; - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { - response = callbackResponse; - rcPromise.set_value(); - }); - resource->SetOnError([&rcPromise, &error](int64_t, string&& message, bool) { - error = std::move(message); - rcPromise.set_value(); - }); - - //clang-format off - resource->SendRequest( - "GET", - std::move(url), - 0, /*requestId*/ - {}, /*headers*/ - {}, /*data*/ - "text", /*responseType*/ - false, - 0 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {}); - //clang-format on - - rcPromise.get_future().wait(); - - Assert::AreEqual({}, error, L"Error encountered"); - Assert::AreEqual(static_cast(403), response.StatusCode); - } - - TEST_METHOD(RequestGetFails) { - string error; - promise promise; - - auto resource = IHttpResource::Make(); - resource->SetOnError([&error, &promise](int64_t, string&& message, bool) { - error = message; + auto resource = IHttpResource::Make(); + resource->SetOnError([&error, &promise](int64_t, string &&message, bool) { + error = message; promise.set_value(); }); @@ -348,9 +226,9 @@ namespace Microsoft::React::Test { if (!content.empty()) getDataPromise.set_value(); - }); - resource->SetOnError( - [&optionsPromise, &getResponsePromise, &getDataPromise, &error, &server](int64_t, string&& message, bool) { + }); + resource->SetOnError( + [&optionsPromise, &getResponsePromise, &getDataPromise, &error, &server](int64_t, string &&message, bool) { error = std::move(message); optionsPromise.set_value(); @@ -404,16 +282,16 @@ namespace Microsoft::React::Test { Assert::AreEqual({ "Response Body" }, content); } - TEST_METHOD(SimpleRedirectGetSucceeds) { - auto port1 = s_port; - auto port2 = ++s_port; - string url = "http://localhost:" + std::to_string(port1); + TEST_METHOD(SimpleRedirectGetSucceeds) { + 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; + promise responsePromise; + promise contentPromise; + IHttpResource::Response responseResult; + string content; + string error; auto server1 = make_shared(port1); server1->Callbacks().OnGet = [port2](const DynamicRequest& request) -> ResponseWrapper { @@ -435,25 +313,25 @@ namespace Microsoft::React::Test { 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); + 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); + }); + 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( @@ -469,14 +347,90 @@ namespace Microsoft::React::Test { [](int64_t) {}); //clang-format on - responsePromise.get_future().wait(); - contentPromise.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); } @@ -487,6 +441,7 @@ namespace Microsoft::React::Test { promise getPromise; string error; int statusCode = 0; + bool timeoutError = false; auto server = std::make_shared(s_port); server->Callbacks().OnGet = [](const DynamicRequest &) -> ResponseWrapper { @@ -506,8 +461,9 @@ namespace Microsoft::React::Test { statusCode = static_cast(response.StatusCode); getPromise.set_value(); }); - resource->SetOnError([&getPromise, &error](int64_t, string &&errorMessage) { + resource->SetOnError([&getPromise, &error, &timeoutError](int64_t, string &&errorMessage, bool isTimeout) { error = std::move(errorMessage); + timeoutError = isTimeout; getPromise.set_value(); }); resource->SendRequest( @@ -526,6 +482,7 @@ namespace Microsoft::React::Test { server->Stop(); Assert::AreEqual({}, error); + Assert::IsFalse(timeoutError); Assert::AreEqual(200, statusCode); } @@ -536,6 +493,7 @@ namespace Microsoft::React::Test { promise getPromise; string error; int statusCode = 0; + bool timeoutError = false; auto server = std::make_shared(s_port); server->Callbacks().OnGet = [](const DynamicRequest &) -> ResponseWrapper { @@ -555,8 +513,9 @@ namespace Microsoft::React::Test { statusCode = static_cast(response.StatusCode); getPromise.set_value(); }); - resource->SetOnError([&getPromise, &error](int64_t, string &&errorMessage) { + resource->SetOnError([&getPromise, &error, &timeoutError](int64_t, string &&errorMessage, bool isTimeout) { error = std::move(errorMessage); + timeoutError = isTimeout; getPromise.set_value(); }); resource->SendRequest( @@ -574,192 +533,12 @@ namespace Microsoft::React::Test { 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); } }; - 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; +/*static*/ uint16_t HttpResourceIntegrationTest::s_port = 4444; } // namespace Microsoft::React::Test diff --git a/vnext/Shared/Modules/HttpModule.cpp b/vnext/Shared/Modules/HttpModule.cpp index bbc3b559f36..21192dfabf4 100644 --- a/vnext/Shared/Modules/HttpModule.cpp +++ b/vnext/Shared/Modules/HttpModule.cpp @@ -72,8 +72,8 @@ namespace { }; resource->SetOnData(std::move(onDataDynamic)); - resource->SetOnError([weakReactInstance](int64_t requestId, string&& message) { - dynamic args = dynamic::array(requestId, std::move(message)); + resource->SetOnError([weakReactInstance](int64_t requestId, string &&message, bool isTimeout) { + dynamic args = dynamic::array(requestId, std::move(message)); if (isTimeout) { args.push_back(true); } diff --git a/vnext/Shared/Networking/IHttpResource.h b/vnext/Shared/Networking/IHttpResource.h index a64999ad76d..7f26d59c7ae 100644 --- a/vnext/Shared/Networking/IHttpResource.h +++ b/vnext/Shared/Networking/IHttpResource.h @@ -91,12 +91,12 @@ namespace Microsoft::React::Networking { virtual void ClearCookies() noexcept = 0; - virtual void SetOnRequestSuccess(std::function&& handler) noexcept = 0; - virtual void SetOnResponse(std::function&& handler) noexcept = 0; - virtual void SetOnData(std::function&& handler) noexcept = 0; - virtual void SetOnData(std::function&& handler) noexcept = 0; - virtual void SetOnError( - std::function&& handler) noexcept = 0; - }; + virtual void SetOnRequestSuccess(std::function &&handler) noexcept = 0; + virtual void SetOnResponse(std::function &&handler) noexcept = 0; + virtual void SetOnData(std::function &&handler) noexcept = 0; + virtual void SetOnData(std::function &&handler) noexcept = 0; + virtual void SetOnError( + std::function &&handler) noexcept = 0; +}; } // namespace Microsoft::React::Networking diff --git a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp index 225408cd7f2..e231876036a 100644 --- a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp +++ b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp @@ -11,10 +11,6 @@ #include #include -// Windows API -#include -#include - // Standard Library #include #include @@ -124,675 +120,762 @@ namespace Microsoft::React::Networking { } /*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const& request) noexcept { - // Ensure header is in Simple CORS white list + // Ensure header is in Simple CORS allowlist for (const auto& header : request.Headers()) { if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend()) return false; - // Ensure Content-Type value is in Simple CORS white list, if present + // Ensure Content-Type value is in Simple CORS allowlist, if present if (boost::iequals(header.Key(), L"Content-Type")) { if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) != s_simpleCorsContentTypeValues.cend()) return false; } } - // WinRT separates request headers from request content headers - if (auto content = request.Content()) { - for (const auto& header : content.Headers()) { - // WinRT automatically appends non-whitelisted header Content-Length when Content-Type is set. Skip it. - if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend() && - !boost::iequals(header.Key(), "Content-Length")) + /*static*/ bool OriginPolicyHttpFilter::IsSameOrigin(Uri const& u1, Uri const& u2) noexcept { + return (u1 && u2) && u1.SchemeName() == u2.SchemeName() && u1.Host() == u2.Host() && u1.Port() == u2.Port(); + } + + /*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const& request) noexcept { + // Ensure header is in Simple CORS white list + for (const auto& header : request.Headers()) { + if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend()) return false; // Ensure Content-Type value is in Simple CORS white list, if present if (boost::iequals(header.Key(), L"Content-Type")) { - if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) == s_simpleCorsContentTypeValues.cend()) + if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) != s_simpleCorsContentTypeValues.cend()) return false; } } - } - // Ensure method is in Simple CORS white list - return s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); - } + // WinRT separates request headers from request content headers + if (auto content = request.Content()) { + for (const auto& header : content.Headers()) { + // WinRT automatically appends non-whitelisted header Content-Length when Content-Type is set. Skip it. + if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend() && + !boost::iequals(header.Key(), "Content-Length")) + return false; - /*static*/ Uri OriginPolicyHttpFilter::GetOrigin(Uri const& uri) noexcept { - return Uri{ uri.SchemeName() + L"://" + uri.Host() + L":" + to_hstring(uri.Port()) }; - } + // Ensure Content-Type value is in Simple CORS white list, if present + if (boost::iequals(header.Key(), L"Content-Type")) { + if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) == s_simpleCorsContentTypeValues.cend()) + return false; + } + } + } - /*static*/ bool OriginPolicyHttpFilter::AreSafeRequestHeaders( - winrt::Windows::Web::Http::Headers::HttpRequestHeaderCollection const& headers) noexcept { - for (const auto& header : headers) { - if (s_corsForbiddenRequestHeaderNames.find(header.Key().c_str()) != s_corsForbiddenRequestHeaderNames.cend()) - return false; + // Ensure method is in Simple CORS white list + return s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); + } - for (const auto& prefix : s_corsForbiddenRequestHeaderNamePrefixes) { - if (boost::istarts_with(header.Key(), prefix)) - return false; - } + /*static*/ Uri OriginPolicyHttpFilter::GetOrigin(Uri const& uri) noexcept { + return Uri{ uri.SchemeName() + L"://" + uri.Host() + L":" + to_hstring(uri.Port()) }; } - return true; - } + /*static*/ bool OriginPolicyHttpFilter::AreSafeRequestHeaders( + winrt::Windows::Web::Http::Headers::HttpRequestHeaderCollection const& headers) noexcept { + for (const auto& header : headers) { + if (s_corsForbiddenRequestHeaderNames.find(header.Key().c_str()) != s_corsForbiddenRequestHeaderNames.cend()) + return false; - // See https://fetch.spec.whatwg.org/#cors-safelisted-request-header - // "A CORS-safelisted header is a header whose name is either one of 'Accept', 'Accept-Language', and - // 'Content-Language', or whose name is 'Content-Type' and value is one of 'application/x-www-form-urlencoded', - // 'multipart/form-data', and 'text/plain' - /*static*/ bool OriginPolicyHttpFilter::IsCorsSafelistedRequestHeader( - hstring const& name, - hstring const& value) noexcept { - // 1. If value's length is greater than 128, then return false. - if (value.size() > 128) - return false; - - // 2. Byte-lowercase name and switch on the result: - static const wchar_t* const safeHeaderNames[] = { - // The following four headers are from the CORS spec - L"accept", - L"accept-language", - L"content-language", - L"content-type", - - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Save-Data - L"save-data", - - // https://w3c.github.io/device-memory/#sec-device-memory-client-hint-header - L"device-memory", - L"dpr", - L"width", - L"viewport-width", - - // https://tools.ietf.org/html/draft-west-lang-client-hint - L"sec-ch-lang", - - // https://tools.ietf.org/html/draft-west-ua-client-hints - L"sec-ch-ua", - L"sec-ch-ua-platform", - L"sec-ch-ua-arch", - L"sec-ch-ua-model", - L"sec-ch-ua-mobile", - L"sec-ch-ua-full-version", - L"sec-ch-ua-platform-version", - }; - - auto nameLower = boost::to_lower_copy(wstring{ name.c_str() }); - if (std::find(std::cbegin(safeHeaderNames), std::cend(safeHeaderNames), nameLower) == std::end(safeHeaderNames)) - return false; - - double doubleHolder; - if (nameLower == L"device-memory" || nameLower == L"dpr") - return boost::conversion::try_lexical_convert(value.c_str(), doubleHolder); - - int intHolder; - if (nameLower == L"width" || nameLower == L"viewport-width") - return boost::conversion::try_lexical_convert(value.c_str(), intHolder); - - auto valueLower = boost::to_lower_copy(wstring{ value.c_str() }); - if (nameLower == L"save-data") - return valueLower == L"on"; - - if (nameLower == L"accept") - return !std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte); - - if (nameLower == L"accept-language" || nameLower == L"content-language") { - return std::all_of(valueLower.cbegin(), valueLower.cend(), [](wchar_t c) noexcept { - return (0x30 <= c && c <= 0x39) || // 0-9 - (0x41 <= c && c <= 0x5A) || // A-Z - (0x61 <= c && c <= 0x7A) || // a-z - c == 0x20 || c == 0x2A || c == 0x2C || c == 0x2D || c == 0x2E || c == 0x3B || c == 0x3D; // *,-.;= - }); + for (const auto& prefix : s_corsForbiddenRequestHeaderNamePrefixes) { + if (boost::istarts_with(header.Key(), prefix)) + return false; + } + } + + return true; } - if (nameLower == L"content-type") { - if (std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte)) + // See https://fetch.spec.whatwg.org/#cors-safelisted-request-header + // "A CORS-safelisted header is a header whose name is either one of 'Accept', 'Accept-Language', and + // 'Content-Language', or whose name is 'Content-Type' and value is one of 'application/x-www-form-urlencoded', + // 'multipart/form-data', and 'text/plain' + /*static*/ bool OriginPolicyHttpFilter::IsCorsSafelistedRequestHeader( + hstring const& name, + hstring const& value) noexcept { + // 1. If value's length is greater than 128, then return false. + if (value.size() > 128) return false; - // https://mimesniff.spec.whatwg.org/#parse-a-mime-type - HttpMediaTypeHeaderValue mediaType{ nullptr }; - if (HttpMediaTypeHeaderValue::TryParse(valueLower, mediaType)) - return mediaType.ToString() == L"application/x-www-form-urlencoded" || - mediaType.ToString() == L"multipart/form-data" || mediaType.ToString() == L"text/plain"; + // 2. Byte-lowercase name and switch on the result: + static const wchar_t* const safeHeaderNames[] = { + // The following four headers are from the CORS spec + L"accept", + L"accept-language", + L"content-language", + L"content-type", + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Save-Data + L"save-data", + + // https://w3c.github.io/device-memory/#sec-device-memory-client-hint-header + L"device-memory", + L"dpr", + L"width", + L"viewport-width", + + // https://tools.ietf.org/html/draft-west-lang-client-hint + L"sec-ch-lang", + + // https://tools.ietf.org/html/draft-west-ua-client-hints + L"sec-ch-ua", + L"sec-ch-ua-platform", + L"sec-ch-ua-arch", + L"sec-ch-ua-model", + L"sec-ch-ua-mobile", + L"sec-ch-ua-full-version", + L"sec-ch-ua-platform-version", + }; + + auto nameLower = boost::to_lower_copy(wstring{ name.c_str() }); + if (std::find(std::cbegin(safeHeaderNames), std::cend(safeHeaderNames), nameLower) == std::end(safeHeaderNames)) + return false; - return false; - } + double doubleHolder; + if (nameLower == L"device-memory" || nameLower == L"dpr") + return boost::conversion::try_lexical_convert(value.c_str(), doubleHolder); - return true; - } + int intHolder; + if (nameLower == L"width" || nameLower == L"viewport-width") + return boost::conversion::try_lexical_convert(value.c_str(), intHolder); - // https://fetch.spec.whatwg.org/#cors-unsafe-request-header-byte - /*static*/ bool OriginPolicyHttpFilter::IsCorsUnsafeRequestHeaderByte(wchar_t c) noexcept { - return (c < 0x20 && c != 0x09) || c == 0x22 || c == 0x28 || c == 0x29 || c == 0x3a || c == 0x3c || c == 0x3e || - c == 0x3f || c == 0x40 || c == 0x5b || c == 0x5c || c == 0x5d || c == 0x7b || c == 0x7d || c == 0x7f; - } + auto valueLower = boost::to_lower_copy(wstring{ value.c_str() }); + if (nameLower == L"save-data") + return valueLower == L"on"; - /*static*/ set OriginPolicyHttpFilter::CorsUnsafeNotForbiddenRequestHeaderNames( - HttpRequestHeaderCollection const& headers) noexcept { - constexpr size_t maxSafelistValueSize = 1024; - size_t safelistValueSize = 0; - std::vector potentiallyUnsafeNames; - set result; - for (const auto& header : headers) { - const auto headerName = header.Key().c_str(); - - // If header is not safe - if (boost::istarts_with(headerName, L"Proxy-") || boost::istarts_with(headerName, L"Sec-") || - s_corsForbiddenRequestHeaderNames.find(headerName) != s_corsForbiddenRequestHeaderNames.cend()) - continue; - - if (!IsCorsSafelistedRequestHeader(header.Key(), header.Value())) { - result.emplace(header.Key().c_str()); - } - else { - potentiallyUnsafeNames.emplace_back(std::wstring_view{ header.Key() }.data()); - safelistValueSize += header.Value().size(); + if (nameLower == L"accept") + return !std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte); + + if (nameLower == L"accept-language" || nameLower == L"content-language") { + return std::all_of(valueLower.cbegin(), valueLower.cend(), [](wchar_t c) noexcept { + return (0x30 <= c && c <= 0x39) || // 0-9 + (0x41 <= c && c <= 0x5A) || // A-Z + (0x61 <= c && c <= 0x7A) || // a-z + c == 0x20 || c == 0x2A || c == 0x2C || c == 0x2D || c == 0x2E || c == 0x3B || c == 0x3D; // *,-.;= + }); } - } - if (safelistValueSize > maxSafelistValueSize) - result.insert(potentiallyUnsafeNames.begin(), potentiallyUnsafeNames.end()); + if (nameLower == L"content-type") { + if (std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte)) + return false; - return result; - } + // https://mimesniff.spec.whatwg.org/#parse-a-mime-type + HttpMediaTypeHeaderValue mediaType{ nullptr }; + if (HttpMediaTypeHeaderValue::TryParse(valueLower, mediaType)) + return mediaType.ToString() == L"application/x-www-form-urlencoded" || + mediaType.ToString() == L"multipart/form-data" || mediaType.ToString() == L"text/plain"; - /*static*/ OriginPolicyHttpFilter::AccessControlValues OriginPolicyHttpFilter::ExtractAccessControlValues( - winrt::Windows::Foundation::Collections::IMap const& headers) { - using std::wregex; - using std::wsregex_token_iterator; - - // https://tools.ietf.org/html/rfc2616#section-4.2 - wregex rgx{ L"\\s*,\\s*" }; - AccessControlValues result; - - auto ciStrCmp = [](const wstring& a, const wstring& b) { return _wcsicmp(a.c_str(), b.c_str()) < 0; }; - set allowedHeaders{ ciStrCmp }; - set allowedMethods{ ciStrCmp }; - set exposedHeaders{ ciStrCmp }; - - for (const auto& header : headers) { - if (boost::iequals(header.Key(), L"Access-Control-Allow-Headers")) { - auto value = wstring{ header.Value().c_str() }; - - // TODO: Avoid redundant comparison. - auto parsed = set{ - wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; - allowedHeaders.insert(parsed.cbegin(), parsed.cend()); - result.AllowedHeaders.insert(allowedHeaders.cbegin(), allowedHeaders.cend()); - } - else if (boost::iequals(header.Key(), L"Access-Control-Allow-Methods")) { - auto value = wstring{ header.Value().c_str() }; - - // TODO: Avoid redundant comparison. - auto parsed = set{ - wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; - allowedMethods.insert(parsed.cbegin(), parsed.cend()); - result.AllowedMethods.insert(allowedMethods.cbegin(), allowedMethods.cend()); - } - else if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { - result.AllowedOrigin = header.Value(); - } - else if (boost::iequals(header.Key(), L"Access-Control-Expose-Headers")) { - auto value = wstring{ header.Value().c_str() }; - - // TODO: Avoid redundant comparison. - auto parsed = set{ - wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; - exposedHeaders.insert(parsed.cbegin(), parsed.cend()); - result.ExposedHeaders.insert(exposedHeaders.cbegin(), exposedHeaders.cend()); - } - else if (boost::iequals(header.Key(), L"Access-Control-Allow-Credentials")) { - result.AllowedCredentials = header.Value(); - } - else if (boost::iequals(header.Key(), L"Access-Control-Max-Age")) { - result.MaxAge = _wtoi(header.Value().c_str()); + return false; } - } - return result; - } // ExtractAccessControlValues + return true; + } - /*static*/ void OriginPolicyHttpFilter::RemoveHttpOnlyCookiesFromResponseHeaders( - HttpResponseMessage const& response, - bool removeAll) { - // Example: "Set-Cookie", L"id=a3fWa; Expires=Wed, 21 Oct 2020 07:28:00 GMT; HttpOnly" - std::queue httpOnlyCookies; - for (const auto& header : response.Headers()) { - if (s_cookieSettingResponseHeaders.find(header.Key().c_str()) == s_cookieSettingResponseHeaders.cend()) - continue; + // https://fetch.spec.whatwg.org/#cors-unsafe-request-header-byte + /*static*/ bool OriginPolicyHttpFilter::IsCorsUnsafeRequestHeaderByte(wchar_t c) noexcept { + return (c < 0x20 && c != 0x09) || c == 0x22 || c == 0x28 || c == 0x29 || c == 0x3a || c == 0x3c || c == 0x3e || + c == 0x3f || c == 0x40 || c == 0x5b || c == 0x5c || c == 0x5d || c == 0x7b || c == 0x7d || c == 0x7f; + } - if (removeAll) { - httpOnlyCookies.push(header.Key()); - continue; + /*static*/ set OriginPolicyHttpFilter::CorsUnsafeNotForbiddenRequestHeaderNames( + HttpRequestHeaderCollection const& headers) noexcept { + constexpr size_t maxSafelistValueSize = 1024; + size_t safelistValueSize = 0; + std::vector potentiallyUnsafeNames; + set result; + for (const auto& header : headers) { + const auto headerName = header.Key().c_str(); + + // If header is not safe + if (boost::istarts_with(headerName, L"Proxy-") || boost::istarts_with(headerName, L"Sec-") || + s_corsForbiddenRequestHeaderNames.find(headerName) != s_corsForbiddenRequestHeaderNames.cend()) + continue; + + if (!IsCorsSafelistedRequestHeader(header.Key(), header.Value())) { + result.emplace(header.Key().c_str()); + } + else { + potentiallyUnsafeNames.emplace_back(std::wstring_view{ header.Key() }.data()); + safelistValueSize += header.Value().size(); + } } - // Anchors (^$) can't be part of bracket expressions ([]). - // Create 3 matching groups: 1. Beginning of string 2. Between delimiters 3. End of string - const std::wregex re(L"(^HttpOnly\\s*;)|(;\\s*HttpOnly\\s*;)|(;\\s*HttpOnly$)", std::regex_constants::icase); - if (!std::regex_search(header.Value().c_str(), re)) - continue; - - // HttpOnly cookie detected. Removing. - httpOnlyCookies.push(header.Key()); - } // const auto &header : response.Headers() + if (safelistValueSize > maxSafelistValueSize) + result.insert(potentiallyUnsafeNames.begin(), potentiallyUnsafeNames.end()); - while (!httpOnlyCookies.empty()) { - response.Headers().Remove(httpOnlyCookies.front()); - httpOnlyCookies.pop(); + return result; } - } - OriginPolicyHttpFilter::OriginPolicyHttpFilter(IHttpFilter const& innerFilter): m_innerFilter{ innerFilter } {} + /*static*/ OriginPolicyHttpFilter::AccessControlValues OriginPolicyHttpFilter::ExtractAccessControlValues( + winrt::Windows::Foundation::Collections::IMap const& headers) { + using std::wregex; + using std::wsregex_token_iterator; + + // https://tools.ietf.org/html/rfc2616#section-4.2 + wregex rgx{ L"\\s*,\\s*" }; + AccessControlValues result; + + auto ciStrCmp = [](const wstring& a, const wstring& b) { return _wcsicmp(a.c_str(), b.c_str()) < 0; }; + set allowedHeaders{ ciStrCmp }; + set allowedMethods{ ciStrCmp }; + set exposedHeaders{ ciStrCmp }; + + for (const auto& header : headers) { + if (boost::iequals(header.Key(), L"Access-Control-Allow-Headers")) { + auto value = wstring{ header.Value().c_str() }; + + // TODO: Avoid redundant comparison. + auto parsed = set{ + wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; + allowedHeaders.insert(parsed.cbegin(), parsed.cend()); + result.AllowedHeaders.insert(allowedHeaders.cbegin(), allowedHeaders.cend()); + } + else if (boost::iequals(header.Key(), L"Access-Control-Allow-Methods")) { + auto value = wstring{ header.Value().c_str() }; + + // TODO: Avoid redundant comparison. + auto parsed = set{ + wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; + allowedMethods.insert(parsed.cbegin(), parsed.cend()); + result.AllowedMethods.insert(allowedMethods.cbegin(), allowedMethods.cend()); + } + else if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { + result.AllowedOrigin = header.Value(); + } + else if (boost::iequals(header.Key(), L"Access-Control-Expose-Headers")) { + auto value = wstring{ header.Value().c_str() }; + + // TODO: Avoid redundant comparison. + auto parsed = set{ + wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; + exposedHeaders.insert(parsed.cbegin(), parsed.cend()); + result.ExposedHeaders.insert(exposedHeaders.cbegin(), exposedHeaders.cend()); + } + else if (boost::iequals(header.Key(), L"Access-Control-Allow-Credentials")) { + result.AllowedCredentials = header.Value(); + } + else if (boost::iequals(header.Key(), L"Access-Control-Max-Age")) { + result.MaxAge = _wtoi(header.Value().c_str()); + } + } - OriginPolicyHttpFilter::OriginPolicyHttpFilter() - : OriginPolicyHttpFilter(winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter{}) {} + return result; + } // ExtractAccessControlValues + + /*static*/ void OriginPolicyHttpFilter::RemoveHttpOnlyCookiesFromResponseHeaders( + HttpResponseMessage const& response, + bool removeAll) { + // Example: "Set-Cookie", L"id=a3fWa; Expires=Wed, 21 Oct 2020 07:28:00 GMT; HttpOnly" + std::queue httpOnlyCookies; + for (const auto& header : response.Headers()) { + if (s_cookieSettingResponseHeaders.find(header.Key().c_str()) == s_cookieSettingResponseHeaders.cend()) + continue; + + if (removeAll) { + httpOnlyCookies.push(header.Key()); + continue; + } - OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const& request) { - auto effectiveOriginPolicy = - static_cast(request.Properties().Lookup(L"OriginPolicy").as().GetUInt64()); - switch (effectiveOriginPolicy) { - case OriginPolicy::None: - return effectiveOriginPolicy; + // Anchors (^$) can't be part of bracket expressions ([]). + // Create 3 matching groups: 1. Beginning of string 2. Between delimiters 3. End of string + const std::wregex re(L"(^HttpOnly\\s*;)|(;\\s*HttpOnly\\s*;)|(;\\s*HttpOnly$)", std::regex_constants::icase); + if (!std::regex_search(header.Value().c_str(), re)) + continue; - case OriginPolicy::SameOrigin: - if (!IsSameOrigin(s_origin, request.RequestUri())) - throw hresult_error{ E_INVALIDARG, L"SOP (same-origin policy) is enforced" }; - break; - - case OriginPolicy::SimpleCrossOriginResourceSharing: - // Check for disallowed mixed content - if (GetRuntimeOptionBool("Http.BlockMixedContentSimpleCors") && - s_origin.SchemeName() != request.RequestUri().SchemeName()) - throw hresult_error{ E_INVALIDARG, L"The origin and request URLs must have the same scheme" }; - - if (IsSameOrigin(s_origin, request.RequestUri())) - // Same origin. Therefore, skip Cross-Origin handling. - effectiveOriginPolicy = OriginPolicy::SameOrigin; - else if (!IsSimpleCorsRequest(request)) - throw hresult_error{ - E_INVALIDARG, - L"The request does not meet the requirements for Same-Origin policy or Simple Cross-Origin resource sharing" }; - break; + // HttpOnly cookie detected. Removing. + httpOnlyCookies.push(header.Key()); + } // const auto &header : response.Headers() - case OriginPolicy::CrossOriginResourceSharing: - // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests - // Refer to CorsURLLoaderFactory::IsValidRequest in chrome\src\services\network\cors\cors_url_loader_factory.cc. - // Forbidden headers should be blocked regardless of origins. - // Example: On the Edge browser, an XHR request with the "Host" header set gets rejected as unsafe. - // https://fetch.spec.whatwg.org/#forbidden-header-name + while (!httpOnlyCookies.empty()) { + response.Headers().Remove(httpOnlyCookies.front()); + httpOnlyCookies.pop(); + } + } - if (s_origin.SchemeName() != request.RequestUri().SchemeName()) - throw hresult_error{ E_INVALIDARG, L"The origin and request URLs must have the same scheme" }; + OriginPolicyHttpFilter::OriginPolicyHttpFilter(IHttpFilter const& innerFilter): m_innerFilter{ innerFilter } {} - if (!AreSafeRequestHeaders(request.Headers())) - throw hresult_error{ E_INVALIDARG, L"Request header not allowed in cross-origin resource sharing" }; + OriginPolicyHttpFilter::OriginPolicyHttpFilter() + : OriginPolicyHttpFilter(winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter{}) {} - if (s_forbiddenMethods.find(request.Method().ToString().c_str()) != s_forbiddenMethods.cend()) - throw hresult_error{ E_INVALIDARG, L"Request method not allowed in cross-origin resource sharing" }; + OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const& request) { + auto effectiveOriginPolicy = + static_cast(request.Properties().Lookup(L"OriginPolicy").as().GetUInt64()); + switch (effectiveOriginPolicy) { + case OriginPolicy::None: + return effectiveOriginPolicy; - if (IsSameOrigin(s_origin, request.RequestUri())) - effectiveOriginPolicy = OriginPolicy::SameOrigin; - else if (IsSimpleCorsRequest(request)) - effectiveOriginPolicy = OriginPolicy::SimpleCrossOriginResourceSharing; - else - effectiveOriginPolicy = OriginPolicy::CrossOriginResourceSharing; + case OriginPolicy::SameOrigin: + if (!IsSameOrigin(s_origin, request.RequestUri())) + throw hresult_error{ E_INVALIDARG, L"SOP (same-origin policy) is enforced" }; + break; - break; + case OriginPolicy::SimpleCrossOriginResourceSharing: + // Check for disallowed mixed content + if (GetRuntimeOptionBool("Http.BlockMixedContentSimpleCors") && + s_origin.SchemeName() != request.RequestUri().SchemeName()) + throw hresult_error{ E_INVALIDARG, L"The origin and request URLs must have the same scheme" }; - default: - throw hresult_error{ - E_INVALIDARG, L"Invalid OriginPolicy type: " + to_hstring(static_cast(effectiveOriginPolicy)) }; - } + if (IsSameOrigin(s_origin, request.RequestUri())) + // Same origin. Therefore, skip Cross-Origin handling. + effectiveOriginPolicy = OriginPolicy::SameOrigin; + else if (!IsSimpleCorsRequest(request)) + throw hresult_error{ + E_INVALIDARG, + L"The request does not meet the requirements for Same-Origin policy or Simple Cross-Origin resource sharing" }; + break; - return effectiveOriginPolicy; - } + case OriginPolicy::CrossOriginResourceSharing: + // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests + // Refer to CorsURLLoaderFactory::IsValidRequest in chrome\src\services\network\cors\cors_url_loader_factory.cc. + // Forbidden headers should be blocked regardless of origins. + // Example: On the Edge browser, an XHR request with the "Host" header set gets rejected as unsafe. + // https://fetch.spec.whatwg.org/#forbidden-header-name - // See https://fetch.spec.whatwg.org/#cors-check - void OriginPolicyHttpFilter::ValidateAllowOrigin( - hstring const& allowedOrigin, - hstring const& allowCredentials, - 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 = 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 - 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"; - - // See https://fetch.spec.whatwg.org/#http-access-control-allow-origin. - else if (boost::contains(allowedOrigin, L" ,")) - errorMessage = L"Response header Access-Control-Allow-Origin can not have multiple values"; - - // A wildcard Access-Control-Allow-Origin can not be used if credentials are to be sent, - // even with Access-Control-Allow-Credentials set to true - // See https://fetch.spec.whatwg.org/#cors-protocol-and-credentials - else if (L"*" == allowedOrigin) - errorMessage = - L"Response header Access-Control-Allow-Origin can not have a wildcard value when the request includes credentials"; - - else - errorMessage = L"The Access-Control-Allow-Origin header has a value of [" + allowedOrigin + - L"] which differs from the supplied origin"; - - throw hresult_error{ E_INVALIDARG, errorMessage }; - } + if (s_origin.SchemeName() != request.RequestUri().SchemeName()) + throw hresult_error{ E_INVALIDARG, L"The origin and request URLs must have the same scheme" }; - // 4.10.5 - if (!withCredentials) - return; - - // 4.10.6-8 - // https://fetch.spec.whatwg.org/#http-access-control-allow-credentials - // This check should be case sensitive. - // See also https://fetch.spec.whatwg.org/#http-new-header-syntax - if (L"true" != allowCredentials) - throw hresult_error{ - E_INVALIDARG, - L"Access-Control-Allow-Credentials value must be \"true\" when the response includes credentials" }; - }; + if (!AreSafeRequestHeaders(request.Headers())) + throw hresult_error{ E_INVALIDARG, L"Request header not allowed in cross-origin resource sharing" }; - void OriginPolicyHttpFilter::ValidatePreflightResponse( - HttpRequestMessage const& request, - HttpResponseMessage const& response) const { - // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed - using winrt::Windows::Web::Http::HttpStatusCode; - switch (response.StatusCode()) { - case HttpStatusCode::MovedPermanently: - case HttpStatusCode::TemporaryRedirect: - case HttpStatusCode::PermanentRedirect: - throw hresult_error{ INET_E_REDIRECTING, L"Redirect is not allowed in a preflight request" }; - - default: - break; - } + if (s_forbiddenMethods.find(request.Method().ToString().c_str()) != s_forbiddenMethods.cend()) + throw hresult_error{ E_INVALIDARG, L"Request method not allowed in cross-origin resource sharing" }; + + if (IsSameOrigin(s_origin, request.RequestUri())) + effectiveOriginPolicy = OriginPolicy::SameOrigin; + else if (IsSimpleCorsRequest(request)) + effectiveOriginPolicy = OriginPolicy::SimpleCrossOriginResourceSharing; + else + effectiveOriginPolicy = OriginPolicy::CrossOriginResourceSharing; - auto controlValues = ExtractAccessControlValues(response.Headers()); - - 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, props); - - // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.5 - // Check if the request method is allowed - bool withCredentials = props.Lookup(L"RequestArgs").as()->WithCredentials; - bool requestMethodAllowed = false; - for (const auto& method : controlValues.AllowedMethods) { - if (L"*" == method) { - if (!withCredentials) { - requestMethodAllowed = true; - break; - } - } - else if (boost::iequals(method, request.Method().ToString())) { - requestMethodAllowed = true; break; + + default: + throw hresult_error{ + E_INVALIDARG, L"Invalid OriginPolicy type: " + to_hstring(static_cast(effectiveOriginPolicy)) }; } + + return effectiveOriginPolicy; } - // Preflight should always allow simple CORS methods - requestMethodAllowed |= s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); - - if (!requestMethodAllowed) - throw hresult_error{ - E_INVALIDARG, - L"Method [" + request.Method().ToString() + - L"] is not allowed by Access-Control-Allow-Methods in preflight response" }; - - // Check if request headers are allowed - // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.6-7 - // Check if the header should be allowed through wildcard, if the request does not have credentials. - bool requestHeadersAllowed = false; - if (!withCredentials && controlValues.AllowedHeaders.find(L"*") != controlValues.AllowedHeaders.cend()) { - // "Authorization" header cannot be allowed through wildcard alone. - // "Authorization" is the only member of https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name. - if (request.Headers().HasKey(L"Authorization") && - controlValues.AllowedHeaders.find(L"Authorization") == controlValues.AllowedHeaders.cend()) + // See https://fetch.spec.whatwg.org/#cors-check + void OriginPolicyHttpFilter::ValidateAllowOrigin( + hstring const& allowedOrigin, + hstring const& allowCredentials, + IMap props) const { + // 4.10.1-2 - null allow origin + if (L"null" == allowedOrigin) throw hresult_error{ E_INVALIDARG, - L"Request header field [Authorization] is not allowed by Access-Control-Allow-Headers in preflight response" }; - - requestHeadersAllowed = true; - } - if (!requestHeadersAllowed) { - // Forbidden headers are excluded from the JavaScript layer. - // User agents may use these headers internally. - const set unsafeNotForbidenHeaderNames = CorsUnsafeNotForbiddenRequestHeaderNames(request.Headers()); - for (const auto name : unsafeNotForbidenHeaderNames) { - if (controlValues.AllowedHeaders.find(name) == controlValues.AllowedHeaders.cend()) + L"Response header Access-Control-Allow-Origin has a value of [null] which differs from the supplied origin" }; + + // See https://fetch.spec.whatwg.org/#cors-check + void OriginPolicyHttpFilter::ValidateAllowOrigin( + hstring const& allowedOrigin, + hstring const& allowCredentials, + IMap props) const { + // 4.10.1-2 - null allow origin + if (L"null" == allowedOrigin) throw hresult_error{ E_INVALIDARG, - L"Request header field [" + to_hstring(name) + - L"] is not allowed by Access-Control-Allow-Headers in preflight response" }; - } - } + L"Response header Access-Control-Allow-Origin has a value of [null] which differs from the supplied origin" }; + + 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 + 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"; + + // A wildcard Access-Control-Allow-Origin can not be used if credentials are to be sent, + // even with Access-Control-Allow-Credentials set to true + // See https://fetch.spec.whatwg.org/#cors-protocol-and-credentials + else if (L"*" == allowedOrigin) + errorMessage = + L"Response header Access-Control-Allow-Origin can not have a wildcard value when the request includes credentials"; + + else + errorMessage = L"The Access-Control-Allow-Origin header has a value of [" + allowedOrigin + + L"] which differs from the supplied origin"; + + throw hresult_error{ E_INVALIDARG, errorMessage }; + } - // #9770 - insert into preflight cache - } + // 4.10.5 + if (!withCredentials) + return; - // See 10.7.4 of https://fetch.spec.whatwg.org/#http-network-or-cache-fetch - void OriginPolicyHttpFilter::ValidateResponse(HttpResponseMessage const& response, const OriginPolicy originPolicy) - const { - bool removeAllCookies = false; - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || - originPolicy == OriginPolicy::CrossOriginResourceSharing) { - auto controlValues = ExtractAccessControlValues(response.Headers()); - auto props = response.RequestMessage().Properties(); - auto withCredentials = props.Lookup(L"RequestArgs").try_as()->WithCredentials; - - if (GetRuntimeOptionBool("Http.StrictOriginCheckSimpleCors") && - originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { - bool originAllowed = false; - for (const auto& header : response.Headers()) { - if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { - originAllowed |= L"*" == header.Value() || s_origin == Uri{ header.Value() }; - } + // 4.10.6-8 + // https://fetch.spec.whatwg.org/#http-access-control-allow-credentials + // This check should be case sensitive. + // See also https://fetch.spec.whatwg.org/#http-new-header-syntax + if (L"true" != allowCredentials) + throw hresult_error{ + E_INVALIDARG, + L"Access-Control-Allow-Credentials value must be \"true\" when the response includes credentials" }; + }; + + void OriginPolicyHttpFilter::ValidatePreflightResponse( + HttpRequestMessage const& request, + HttpResponseMessage const& response) const { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed + using winrt::Windows::Web::Http::HttpStatusCode; + switch (response.StatusCode()) { + case HttpStatusCode::MovedPermanently: + case HttpStatusCode::TemporaryRedirect: + case HttpStatusCode::PermanentRedirect: + throw hresult_error{ INET_E_REDIRECTING, L"Redirect is not allowed in a preflight request" }; + + default: + break; } - if (!originAllowed) { - throw hresult_error{ E_INVALIDARG, L"The server does not support CORS or the origin is not allowed" }; - } - } - else { + auto controlValues = ExtractAccessControlValues(response.Headers()); + + 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, props); - } - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { - // Filter out response headers that are not in the Simple CORS whitelist - std::queue nonSimpleNames; - for (const auto& header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend()) - nonSimpleNames.push(header.Key()); - } + // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.5 + // Check if the request method is allowed + bool withCredentials = props.Lookup(L"RequestArgs").as()->WithCredentials; + bool requestMethodAllowed = false; + for (const auto& method : controlValues.AllowedMethods) { + if (L"*" == method) { + if (!withCredentials) { + requestMethodAllowed = true; + break; + } + } - while (!nonSimpleNames.empty()) { - response.Headers().Remove(nonSimpleNames.front()); - nonSimpleNames.pop(); + // Preflight should always allow simple CORS methods + requestMethodAllowed |= s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); + + if (!requestMethodAllowed) + throw hresult_error{ + E_INVALIDARG, + L"Method [" + request.Method().ToString() + + L"] is not allowed by Access-Control-Allow-Methods in preflight response" }; + + // Check if request headers are allowed + // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.6-7 + // Check if the header should be allowed through wildcard, if the request does not have credentials. + bool requestHeadersAllowed = false; + if (!withCredentials && controlValues.AllowedHeaders.find(L"*") != controlValues.AllowedHeaders.cend()) { + // "Authorization" header cannot be allowed through wildcard alone. + // "Authorization" is the only member of https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name. + if (request.Headers().HasKey(L"Authorization") && + controlValues.AllowedHeaders.find(L"Authorization") == controlValues.AllowedHeaders.cend()) + throw hresult_error{ + E_INVALIDARG, + L"Request header field [Authorization] is not allowed by Access-Control-Allow-Headers in preflight response" }; + + requestHeadersAllowed = true; + } + if (!requestHeadersAllowed) { + // Forbidden headers are excluded from the JavaScript layer. + // User agents may use these headers internally. + const set unsafeNotForbidenHeaderNames = CorsUnsafeNotForbiddenRequestHeaderNames(request.Headers()); + for (const auto name : unsafeNotForbidenHeaderNames) { + if (controlValues.AllowedHeaders.find(name) == controlValues.AllowedHeaders.cend()) + throw hresult_error{ + E_INVALIDARG, + L"Request header field [" + to_hstring(name) + + L"] is not allowed by Access-Control-Allow-Headers in preflight response" }; + } + } + + // #9770 - insert into preflight cache } - } - else { - // Filter out response headers that are not simple headers and not in expose list - // Keep simple headers and those found in the expose header list. - if (withCredentials || controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { - std::queue nonSimpleNonExposedHeaders; + // See 10.7.4 of https://fetch.spec.whatwg.org/#http-network-or-cache-fetch + void OriginPolicyHttpFilter::ValidateResponse(HttpResponseMessage const& response, const OriginPolicy originPolicy) + const { + bool removeAllCookies = false; + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + originPolicy == OriginPolicy::CrossOriginResourceSharing) { + auto controlValues = ExtractAccessControlValues(response.Headers()); + auto props = response.RequestMessage().Properties(); + auto withCredentials = props.Lookup(L"RequestArgs").try_as()->WithCredentials; + + if (GetRuntimeOptionBool("Http.StrictOriginCheckSimpleCors") && + originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { + bool originAllowed = false; + for (const auto& header : response.Headers()) { + if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { + originAllowed |= L"*" == header.Value() || s_origin == Uri{ header.Value() }; + } + } + + if (!originAllowed) { + throw hresult_error{ E_INVALIDARG, L"The server does not support CORS or the origin is not allowed" }; + } + } + else { + ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props); + } - for (const auto& header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend() && - controlValues.ExposedHeaders.find(header.Key().c_str()) == controlValues.ExposedHeaders.cend()) { - nonSimpleNonExposedHeaders.push(header.Key()); + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { + // Filter out response headers that are not in the Simple CORS allowlist + std::queue nonSimpleNames; + for (const auto& header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend()) + nonSimpleNames.push(header.Key()); + } + + while (!nonSimpleNames.empty()) { + response.Headers().Remove(nonSimpleNames.front()); + nonSimpleNames.pop(); + } } + else { + // Filter out response headers that are not simple headers and not in expose list + + // Keep simple headers and those found in the expose header list. + if (withCredentials || controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { + std::queue nonSimpleNonExposedHeaders; + + for (const auto& header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend() && + controlValues.ExposedHeaders.find(header.Key().c_str()) == controlValues.ExposedHeaders.cend()) { + nonSimpleNonExposedHeaders.push(header.Key()); + } + } + + if (!originAllowed) { + throw hresult_error{ E_INVALIDARG, L"The server does not support CORS or the origin is not allowed" }; + } + } + else { + ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props); + } + + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { + // Filter out response headers that are not in the Simple CORS whitelist + std::queue nonSimpleNames; + for (const auto& header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend()) + nonSimpleNames.push(header.Key()); + } + + while (!nonSimpleNames.empty()) { + response.Headers().Remove(nonSimpleNames.front()); + nonSimpleNames.pop(); + } + } + else { + // Filter out response headers that are not simple headers and not in expose list + + // Keep simple headers and those found in the expose header list. + if (withCredentials || controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { + std::queue nonSimpleNonExposedHeaders; + + for (const auto& header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend() && + controlValues.ExposedHeaders.find(header.Key().c_str()) == controlValues.ExposedHeaders.cend()) { + nonSimpleNonExposedHeaders.push(header.Key()); + } + } + + while (!nonSimpleNonExposedHeaders.empty()) { + response.Headers().Remove(nonSimpleNonExposedHeaders.front()); + nonSimpleNonExposedHeaders.pop(); + } + } + } + + // When withCredentials is false, request cannot include cookies. Also, cookies will be ignored in responses. + removeAllCookies = !withCredentials && GetRuntimeOptionBool("Http.RemoveCookiesFromResponse"); + } // originPolicy == SimpleCrossOriginResourceSharing || CrossOriginResourceSharing + + // Don't expose HttpOnly cookies to JavaScript + RemoveHttpOnlyCookiesFromResponseHeaders(response, removeAllCookies); } - while (!nonSimpleNonExposedHeaders.empty()) { - response.Headers().Remove(nonSimpleNonExposedHeaders.front()); - nonSimpleNonExposedHeaders.pop(); - } - } - } + ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage const& request) const { + auto coRequest = request; - // When withCredentials is false, request cannot include cookies. Also, cookies will be ignored in responses. - removeAllCookies = !withCredentials && GetRuntimeOptionBool("Http.RemoveCookiesFromResponse"); - } // originPolicy == SimpleCrossOriginResourceSharing || CrossOriginResourceSharing + HttpRequestMessage preflightRequest; - // Don't expose HttpOnly cookies to JavaScript - RemoveHttpOnlyCookiesFromResponseHeaders(response, removeAllCookies); - } + // Section 4.8.2 https://fetch.spec.whatwg.org/#cors-preflight-fetch + preflightRequest.Method(HttpMethod::Options()); + preflightRequest.RequestUri(coRequest.RequestUri()); + preflightRequest.Headers().Insert(L"Accept", L"*/*"); + preflightRequest.Headers().Insert(L"Access-Control-Request-Method", coRequest.Method().ToString()); - ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage const& request) const { - auto coRequest = request; + auto headerNames = wstring{}; + auto headerItr = coRequest.Headers().begin(); + if (headerItr != coRequest.Headers().end()) { + headerNames += (*headerItr).Key(); - HttpRequestMessage preflightRequest; + while (++headerItr != coRequest.Headers().end()) + headerNames += L", " + (*headerItr).Key(); + } - // Section 4.8.2 https://fetch.spec.whatwg.org/#cors-preflight-fetch - preflightRequest.Method(HttpMethod::Options()); - preflightRequest.RequestUri(coRequest.RequestUri()); - preflightRequest.Headers().Insert(L"Accept", L"*/*"); - preflightRequest.Headers().Insert(L"Access-Control-Request-Method", coRequest.Method().ToString()); + if (coRequest.Content()) { + headerItr = coRequest.Content().Headers().begin(); + if (headerItr != coRequest.Content().Headers().end()) { + headerNames += (*headerItr).Key(); - auto headerNames = wstring{}; - auto headerItr = coRequest.Headers().begin(); - if (headerItr != coRequest.Headers().end()) { - headerNames += (*headerItr).Key(); + while (++headerItr != coRequest.Content().Headers().end()) + headerNames += L", " + (*headerItr).Key(); + } + } - while (++headerItr != coRequest.Headers().end()) - headerNames += L", " + (*headerItr).Key(); - } + preflightRequest.Headers().Insert(L"Access-Control-Request-Headers", headerNames); + preflightRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); + preflightRequest.Headers().Insert(L"Sec-Fetch-Mode", L"CORS"); - if (coRequest.Content()) { - headerItr = coRequest.Content().Headers().begin(); - if (headerItr != coRequest.Content().Headers().end()) { - headerNames += (*headerItr).Key(); + co_return{ co_await m_innerFilter.SendRequestAsync(preflightRequest) }; + } - while (++headerItr != coRequest.Content().Headers().end()) - headerNames += L", " + (*headerItr).Key(); - } - } +#pragma region IRedirectEventSource - preflightRequest.Headers().Insert(L"Access-Control-Request-Headers", headerNames); - preflightRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); - preflightRequest.Headers().Insert(L"Sec-Fetch-Mode", L"CORS"); + 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; + } + } - co_return{ co_await m_innerFilter.SendRequestAsync(preflightRequest) }; - } + return true; + } + +#pragma endregion IRedirectEventSource #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; - } - } + 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; - } + return true; + } #pragma endregion IRedirectEventSource #pragma region IHttpFilter - ResponseOperation OriginPolicyHttpFilter::SendRequestAsync(HttpRequestMessage const& request) { - auto coRequest = request; + ResponseOperation OriginPolicyHttpFilter::SendRequestAsync(HttpRequestMessage const& request) { + auto coRequest = request; - // Set initial origin policy to global runtime option. - request.Properties().Insert(L"OriginPolicy", winrt::box_value(GetRuntimeOptionInt("Http.OriginPolicy"))); + // Set initial origin policy to global runtime option. + request.Properties().Insert(L"OriginPolicy", winrt::box_value(GetRuntimeOptionInt("Http.OriginPolicy"))); - // Allow only HTTP or HTTPS schemes - if (GetRuntimeOptionBool("Http.StrictScheme") && coRequest.RequestUri().SchemeName() != L"https" && - coRequest.RequestUri().SchemeName() != L"http") - throw hresult_error{ E_INVALIDARG, L"Invalid URL scheme: [" + s_origin.SchemeName() + L"]" }; + // Allow only HTTP or HTTPS schemes + if (GetRuntimeOptionBool("Http.StrictScheme") && coRequest.RequestUri().SchemeName() != L"https" && + coRequest.RequestUri().SchemeName() != L"http") + throw hresult_error{ E_INVALIDARG, L"Invalid URL scheme: [" + s_origin.SchemeName() + L"]" }; - if (!GetRuntimeOptionBool("Http.OmitCredentials")) { - coRequest.Properties().Lookup(L"RequestArgs").as()->WithCredentials = false; - } + if (!GetRuntimeOptionBool("Http.OmitCredentials")) { + coRequest.Properties().Lookup(L"RequestArgs").as()->WithCredentials = false; + } - // Ensure absolute URL - coRequest.RequestUri(Uri{ coRequest.RequestUri().AbsoluteCanonicalUri() }); + // Ensure absolute URL + coRequest.RequestUri(Uri{ coRequest.RequestUri().AbsoluteCanonicalUri() }); - auto originPolicy = ValidateRequest(coRequest); - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || - originPolicy == OriginPolicy::CrossOriginResourceSharing) { - if (coRequest.RequestUri().UserName().size() > 0 || coRequest.RequestUri().Password().size() > 0) { - coRequest.RequestUri(Uri{ coRequest.RequestUri().DisplayUri() }); - } - } + auto originPolicy = ValidateRequest(coRequest); + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + originPolicy == OriginPolicy::CrossOriginResourceSharing) { + if (coRequest.RequestUri().UserName().size() > 0 || coRequest.RequestUri().Password().size() > 0) { + coRequest.RequestUri(Uri{ coRequest.RequestUri().DisplayUri() }); + } + } - try { - // #9770 - Validate preflight cache - if (originPolicy == OriginPolicy::CrossOriginResourceSharing) { - // If inner filter can AllowRedirect, disable for preflight. - winrt::impl::com_ref baseFilter; - if (baseFilter = m_innerFilter.try_as()) { - baseFilter.AllowAutoRedirect(false); - } + try { + // #9770 - Validate preflight cache + if (originPolicy == OriginPolicy::CrossOriginResourceSharing) { + // If inner filter can AllowRedirect, disable for preflight. + winrt::impl::com_ref baseFilter; + if (baseFilter = m_innerFilter.try_as()) { + baseFilter.AllowAutoRedirect(false); + } - auto preflightResponse = co_await SendPreflightAsync(coRequest); + auto preflightResponse = co_await SendPreflightAsync(coRequest); - if (baseFilter) { - baseFilter.AllowAutoRedirect(true); - } + if (baseFilter) { + baseFilter.AllowAutoRedirect(true); + } - ValidatePreflightResponse(coRequest, preflightResponse); - } + ValidatePreflightResponse(coRequest, preflightResponse); + } - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || - originPolicy == OriginPolicy::CrossOriginResourceSharing) { - coRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); - } + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + originPolicy == OriginPolicy::CrossOriginResourceSharing) { + coRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); + } - auto response = co_await m_innerFilter.SendRequestAsync(coRequest); + auto response = co_await m_innerFilter.SendRequestAsync(coRequest); - ValidateResponse(response, originPolicy); + ValidateResponse(response, originPolicy); - co_return response; + co_return response; - } - catch (hresult_error const& e) { - throw e; - } - catch (const std::exception& e) { - throw hresult_error{ E_FAIL, to_hstring(e.what()) }; - } - catch (...) { - throw hresult_error{ E_FAIL, L"Unspecified error processing Origin Policy request" }; - } - } + } + catch (hresult_error const& e) { + throw e; + } + catch (const std::exception& e) { + throw hresult_error{ E_FAIL, to_hstring(e.what()) }; + } + catch (...) { + throw hresult_error{ E_FAIL, L"Unspecified error processing Origin Policy request" }; + } + } #pragma endregion IHttpFilter #pragma endregion OriginPolicyHttpFilter -} // namespace Microsoft::React::Networking + } // namespace Microsoft::React::Networking diff --git a/vnext/Shared/Networking/RedirectHttpFilter.cpp b/vnext/Shared/Networking/RedirectHttpFilter.cpp index a1fd8eedcc5..a5248e031f8 100644 --- a/vnext/Shared/Networking/RedirectHttpFilter.cpp +++ b/vnext/Shared/Networking/RedirectHttpFilter.cpp @@ -5,8 +5,6 @@ #include "RedirectHttpFilter.h" -// React Native Windows -#include #include "WinRTTypes.h" // Windows API @@ -213,13 +211,6 @@ ResponseOperation RedirectHttpFilter::SendRequestAsync(HttpRequestMessage const method = coRequest.Method(); do { - // Set User-Agent - // See https://fetch.spec.whatwg.org/#http-network-or-cache-fetch - auto userAgent = GetRuntimeOptionString("Http.UserAgent"); - if (userAgent.size() > 0) { - coRequest.Headers().Append(L"User-Agent", winrt::to_hstring(userAgent)); - } - // Send subsequent requests through the filter that doesn't have the credentials included in the first request response = co_await (redirectCount > 0 ? m_innerFilterWithNoCredentials : m_innerFilter).SendRequestAsync(coRequest); diff --git a/vnext/Shared/Networking/WinRTHttpResource.cpp b/vnext/Shared/Networking/WinRTHttpResource.cpp index 8ff66df822a..8edc9607ebb 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.cpp +++ b/vnext/Shared/Networking/WinRTHttpResource.cpp @@ -59,6 +59,150 @@ namespace Microsoft::React::Networking { 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( @@ -106,26 +250,35 @@ namespace Microsoft::React::Networking { } } - void WinRTHttpResource::AbortRequest(int64_t requestId) noexcept /*override*/ { - ResponseOperation request{ nullptr }; + try { + HttpMethod httpMethod{ to_hstring(std::move(method)) }; + Uri uri{ to_hstring(std::move(url)) }; - 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; + 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; - try { - request.Cancel(); + PerformSendRequest(std::move(httpMethod), std::move(uri), iReqArgs); + } + catch (std::exception const& e) { + if (m_onError) { + m_onError(requestId, e.what(), false); } - catch (hresult_error const& e) { + } + catch (hresult_error const& e) { + if (m_onError) { m_onError(requestId, Utilities::HResultToString(e), false); } } + catch (...) { + m_onError(requestId, "Unidentified error sending HTTP request", false); + } void WinRTHttpResource::ClearCookies() noexcept /*override*/ { assert(false); @@ -152,10 +305,11 @@ namespace Microsoft::React::Networking { m_onDataDynamic = std::move(handler); } - void WinRTHttpResource::SetOnError( - function&& handler) noexcept - /*override*/ { - m_onError = std::move(handler); + try { + request.Cancel(); + } + catch (hresult_error const& e) { + m_onError(requestId, Utilities::HResultToString(e), false); } } @@ -184,8 +338,9 @@ void WinRTHttpResource::SetOnData(function&& handler) noexcept -/*override*/ { +void WinRTHttpResource::SetOnError( + function&& handler) noexcept + /*override*/ { m_onError = std::move(handler); } @@ -213,73 +368,12 @@ WinRTHttpResource::PerformSendRequest(HttpMethod&& method, Uri&& rtUri, IInspect // Ensure background thread co_await winrt::resume_background(); - // 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)) { - 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); - } - - co_return; - } - } - catch (const hresult_error& e) { - if (self->m_onError) - co_return self->m_onError(coReqArgs->RequestId, Utilities::HResultToString(e)); - } - catch (const std::exception& e) { - if (self->m_onError) - co_return self->m_onError(coReqArgs->RequestId, e.what()); - } - } + auto props = winrt::multi_threaded_map(); + props.Insert(L"RequestArgs", coArgs); - 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 : 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 if (boost::iequals(header.first.c_str(), "User-Agent")) { - 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 User-Agent"); - } - } - 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)); - } - } - } + 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. @@ -292,48 +386,6 @@ WinRTHttpResource::PerformSendRequest(HttpMethod&& method, Uri&& rtUri, IInspect self->m_onDataDynamic(reqArgs->RequestId, std::move(blob)); self->m_onRequestSuccess(reqArgs->RequestId); } - co_return; - } - 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""}; - } - } - - 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; } @@ -383,33 +435,6 @@ WinRTHttpResource::PerformSendRequest(HttpMethod&& method, Uri&& rtUri, IInspect co_await lessthrow_await_adapter{sendRequestOp}; } - if (coReqArgs->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, coReqArgs->Timeout); - - 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, Utilities::HResultToString(HRESULT_FROM_WIN32(ERROR_TIMEOUT))); - } - co_return self->UntrackResponse(coReqArgs->RequestId); - } - } - else { - co_await lessthrow_await_adapter{sendRequestOp}; - } - auto result = sendRequestOp.ErrorCode(); if (result < 0) { if (self->m_onError) { @@ -487,12 +512,12 @@ WinRTHttpResource::PerformSendRequest(HttpMethod&& method, Uri&& rtUri, IInspect 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 { @@ -501,169 +526,12 @@ WinRTHttpResource::PerformSendRequest(HttpMethod&& method, Uri&& rtUri, IInspect } } } -} - -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) { - if (self->m_onError) { - self->m_onError(coReqArgs->RequestId, e.what()); - } - co_return; - } - 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""}; - } -} - -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) { + catch (std::exception const& e) { if (self->m_onError) { - self->m_onError(coReqArgs->RequestId, Utilities::HResultToString(std::move(result))); - } - co_return self->UntrackResponse(coReqArgs->RequestId); - } - - auto response = sendRequestOp.GetResults(); - if (response) { - if (self->m_onResponse) { - auto url = to_string(response.RequestMessage().RequestUri().AbsoluteUri()); - - // Gather headers for both the response content and the response itself - // See Invoke-WebRequest PowerShell cmdlet or Chromium response handling - Headers responseHeaders; - for (auto header : response.Headers()) { - responseHeaders.emplace(to_string(header.Key()), to_string(header.Value())); - } - for (auto header : response.Content().Headers()) { - responseHeaders.emplace(to_string(header.Key()), to_string(header.Value())); - } - - self->m_onResponse( - coReqArgs->RequestId, - { static_cast(response.StatusCode()), std::move(url), std::move(responseHeaders) }); - } - } - - // #9534 - Support HTTP incremental updates - if (response && response.Content()) { - auto inputStream = co_await response.Content().ReadAsInputStreamAsync(); - auto reader = DataReader{ inputStream }; - - // #9510 - 10mb limit on fetch - co_await reader.LoadAsync(10 * 1024 * 1024); - - // Let response handler take over, if set - if (auto responseHandler = self->m_responseHandler.lock()) { - if (responseHandler->Supports(coReqArgs->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); - } - - co_return; - } - } - - auto isText = coReqArgs->ResponseType == "text"; - if (isText) { - reader.UnicodeEncoding(UnicodeEncoding::Utf8); - } - - // #9510 - We currently accumulate all incoming request data in 10MB chunks. - uint32_t segmentSize = 10 * 1024 * 1024; - string responseData; - winrt::Windows::Storage::Streams::IBuffer buffer; - uint32_t length; - do { - co_await reader.LoadAsync(segmentSize); - length = reader.UnconsumedBufferLength(); - - if (isText) { - auto data = std::vector(length); - reader.ReadBytes(data); - - responseData += string(Common::Utilities::CheckedReinterpretCast(data.data()), data.size()); - } - else { - buffer = reader.ReadBuffer(length); - auto data = CryptographicBuffer::EncodeToBase64String(buffer); - - responseData += to_string(std::wstring_view(data)); - } - } while (length > 0); - - if (self->m_onData) { - self->m_onData(coReqArgs->RequestId, std::move(responseData)); + self->m_onError(reqArgs->RequestId, e.what(), false); } } - else { + catch (hresult_error const& e) { if (self->m_onError) { self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), false); } @@ -684,16 +552,6 @@ void WinRTHttpResource::AddUriHandler(shared_ptr /*uriHandler*/) no // TODO: Implement custom URI handling. } -self->UntrackResponse(coReqArgs->RequestId); -} // PerformSendRequest - -#pragma region IHttpModuleProxy - -void WinRTHttpResource::AddUriHandler(shared_ptr /*uriHandler*/) noexcept /*override*/ -{ - // TODO: Implement custom URI handling. -} - void WinRTHttpResource::AddRequestBodyHandler(shared_ptr requestBodyHandler) noexcept /*override*/ { m_requestBodyHandler = weak_ptr(requestBodyHandler); @@ -715,20 +573,26 @@ 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 59c025d26d5..43591dd772c 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 @@ -16,18 +17,19 @@ namespace Microsoft::React::Networking { - class WinRTHttpResource: public IHttpResource, - public IHttpModuleProxy, - public std::enable_shared_from_this { - winrt::Windows::Web::Http::IHttpClient m_client; - std::mutex m_mutex; - std::unordered_map m_responses; +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; + std::unordered_map m_responses; - std::function m_onRequestSuccess; - std::function m_onResponse; - std::function m_onData; - std::function m_onDataDynamic; - std::function m_onError; + std::function m_onRequestSuccess; + std::function m_onResponse; + std::function m_onData; + std::function m_onDataDynamic; + std::function m_onError; // Used for IHttpModuleProxy std::weak_ptr m_uriHandler; @@ -38,10 +40,10 @@ namespace Microsoft::React::Networking { void UntrackResponse(int64_t requestId) noexcept; - winrt::fire_and_forget PerformSendRequest( - winrt::Windows::Web::Http::HttpMethod&& method, - winrt::Windows::Foundation::Uri&& uri, - winrt::Windows::Foundation::IInspectable const& args) noexcept; + winrt::fire_and_forget PerformSendRequest( + winrt::Windows::Web::Http::HttpMethod &&method, + winrt::Windows::Foundation::Uri &&uri, + winrt::Windows::Foundation::IInspectable const &args) noexcept; public: WinRTHttpResource() noexcept; @@ -58,6 +60,16 @@ namespace Microsoft::React::Networking { #pragma endregion IWinRTHttpRequestFactory +#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( @@ -74,12 +86,12 @@ namespace Microsoft::React::Networking { void AbortRequest(int64_t requestId) noexcept override; void ClearCookies() noexcept override; - void SetOnRequestSuccess(std::function&& handler) noexcept override; - 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 SetOnRequestSuccess(std::function &&handler) noexcept override; + 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; #pragma endregion IHttpResource diff --git a/vnext/Shared/Shared.vcxitems.filters b/vnext/Shared/Shared.vcxitems.filters index 3976b01efbf..bc53343114c 100644 --- a/vnext/Shared/Shared.vcxitems.filters +++ b/vnext/Shared/Shared.vcxitems.filters @@ -152,6 +152,9 @@ Source Files\Modules + + Source Files\Networking + @@ -457,6 +460,15 @@ Header Files\Modules + + Header Files\Networking + + + Header Files\Networking + + + Header Files\Networking + From fd68cb8b803cc3e6a8dcf8e3596eacfeff0bc4f6 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Tue, 6 Dec 2022 23:29:49 -0800 Subject: [PATCH 15/45] Remove change file --- ...ative-windows-d5cf0017-99f3-40eb-856e-25f6e85f6fa2.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 change/react-native-windows-d5cf0017-99f3-40eb-856e-25f6e85f6fa2.json diff --git a/change/react-native-windows-d5cf0017-99f3-40eb-856e-25f6e85f6fa2.json b/change/react-native-windows-d5cf0017-99f3-40eb-856e-25f6e85f6fa2.json deleted file mode 100644 index 0bcdf5a011b..00000000000 --- a/change/react-native-windows-d5cf0017-99f3-40eb-856e-25f6e85f6fa2.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "prerelease", - "comment": "Implement HTTP redirection", - "packageName": "react-native-windows", - "email": "julio.rocha@microsoft.com", - "dependentChangeType": "patch" -} From 0ab5cb2ee2c73f5463a83aeb407bfa2c707dfd2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20C=C3=A9sar=20Rocha?= Date: Sun, 6 Nov 2022 21:09:28 -0800 Subject: [PATCH 16/45] Implement Form Data requests (#10797) * Implement Form Data requests * Change files * Update packages.lock * Update packages.lock * Update packages.lock * Enable XHRExample * Update packages.lock * Use actual multipart content in XHRExample * Use less-throw adapter for CreateRequest * Validate AttachMultipartHeaders * Update packages.lock * Omit headers when using the W3 Schools example * format * Use name,value aliases in header parsing * Avoid overwriting Content-Type for multipart/form-data * Throw when failing to append form data Authorization * Use non-const folly::dynamic object reference * Add HTTP test examples * Move HTTP examples into @react-native-windows * Moved HTTP examples to src/js/ * lint * Add overrides * Revert "Simplify root node lookups (#10689)" This reverts commit 39e908c1ae54dd4434611e528e8f5c65fd83ad1d. * Change files --- ...-002cdf98-9f28-4a37-ac6c-33789f8b36b1.json | 7 + .../tester/overrides.json | 16 +- .../src/js/examples/HTTP/HTTPExample.js | 32 ++++ .../HTTP/HTTPExampleMultiPartFormData.js | 164 ++++++++++++++++++ .../src/js/utils/RNTesterList.windows.js | 8 + .../windows/RNTesterApp/packages.lock.json | 2 +- .../InteropTestModuleCS/packages.lock.json | 24 ++- vnext/Shared/Networking/WinRTHttpResource.cpp | 135 ++++++++++---- 8 files changed, 355 insertions(+), 33 deletions(-) create mode 100644 change/react-native-windows-002cdf98-9f28-4a37-ac6c-33789f8b36b1.json create mode 100644 packages/@react-native-windows/tester/src/js/examples/HTTP/HTTPExample.js create mode 100644 packages/@react-native-windows/tester/src/js/examples/HTTP/HTTPExampleMultiPartFormData.js diff --git a/change/react-native-windows-002cdf98-9f28-4a37-ac6c-33789f8b36b1.json b/change/react-native-windows-002cdf98-9f28-4a37-ac6c-33789f8b36b1.json new file mode 100644 index 00000000000..701c1cca7f6 --- /dev/null +++ b/change/react-native-windows-002cdf98-9f28-4a37-ac6c-33789f8b36b1.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Implement Form Data requests", + "packageName": "react-native-windows", + "email": "julio.rocha@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/@react-native-windows/tester/overrides.json b/packages/@react-native-windows/tester/overrides.json index 65a0a651fd4..953622a6d8c 100644 --- a/packages/@react-native-windows/tester/overrides.json +++ b/packages/@react-native-windows/tester/overrides.json @@ -14,6 +14,20 @@ "baseHash": "a1d1c6638e815f6dd53504983813fda92f3b5478", "issue": 6341 }, + { + "type": "derived", + "file": "src/js/examples-win/Button/ButtonExample.windows.js", + "baseFile": "packages/rn-tester/js/examples/Button/ButtonExample.js", + "baseHash": "36c55afe75ac9c6efdec22e5ceb1f114a84ebd45" + }, + { + "type": "platform", + "file": "src/js/examples/HTTP/HTTPExample.js" + }, + { + "type": "platform", + "file": "src/js/examples/HTTP/HTTPExampleMultiPartFormData.js" + }, { "type": "patch", "file": "src/js/examples/Pressable/PressableExample.windows.js", @@ -48,4 +62,4 @@ "baseHash": "d12b4947135ada2dcb1c68835f53d7f0beff8a4e" } ] -} \ No newline at end of file +} diff --git a/packages/@react-native-windows/tester/src/js/examples/HTTP/HTTPExample.js b/packages/@react-native-windows/tester/src/js/examples/HTTP/HTTPExample.js new file mode 100644 index 00000000000..95311de2d10 --- /dev/null +++ b/packages/@react-native-windows/tester/src/js/examples/HTTP/HTTPExample.js @@ -0,0 +1,32 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * @flow + * @format + */ +'use strict'; + +const React = require('react'); +const HTTPExampleMultiPartFormData = require('./HTTPExampleMultiPartFormData'); +const XHRExampleFetch = require('../XHR/XHRExampleFetch'); + +exports.framework = 'React'; +exports.title = 'HTTP'; +exports.category = 'Basic'; +exports.description = + ('Example that demonstrates direct and indirect HTTP native module usage.': string); +exports.examples = [ + { + title: 'multipart/form-data POST', + render(): React.Node { + return ; + }, + }, + { + title: 'Fetch Test', + render(): React.Node { + return ; + }, + }, +]; diff --git a/packages/@react-native-windows/tester/src/js/examples/HTTP/HTTPExampleMultiPartFormData.js b/packages/@react-native-windows/tester/src/js/examples/HTTP/HTTPExampleMultiPartFormData.js new file mode 100644 index 00000000000..9f80487bf32 --- /dev/null +++ b/packages/@react-native-windows/tester/src/js/examples/HTTP/HTTPExampleMultiPartFormData.js @@ -0,0 +1,164 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + * + * @flow + * @format + */ +'use strict'; + +const React = require('react'); + +const {StyleSheet, Text, TextInput, View} = require('react-native'); + +/** + * See https://www.w3schools.com/php/php_form_validation.asp + */ +class HTTPExampleMultiPartFormData extends React.Component { + responseURL: ?string; + responseHeaders: ?Object; + + constructor(props: any) { + super(props); + this.state = { + responseText: null, + }; + this.responseURL = null; + this.responseHeaders = null; + } + + submit(uri: string) { + const formData = new FormData(); + + formData.append('name', { + string: 'Name', + type: 'application/text', + }); + formData.append('email', { + string: 'me@mail.com', + type: 'application/text', + }); + formData.append('website', { + string: 'http://aweb.com', + type: 'application/text', + }); + formData.append('comment', { + string: 'Hello', + type: 'application/text', + }); + formData.append('gender', { + string: 'Other', + type: 'application/text', + }); + + fetch(uri, { + method: 'POST', + headers: { + 'Content-Type': 'multipart/form-data', + }, + body: formData, + }) + .then(response => { + this.responseURL = response.url; + this.responseHeaders = response.headers; + + return response.text(); + }) + .then(body => { + this.setState({responseText: body}); + }); + } + + _renderHeaders(): null | Array { + if (!this.responseHeaders) { + return null; + } + + const responseHeaders = []; + const keys = Object.keys(this.responseHeaders.map); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + // $FlowFixMe[incompatible-use] + const value = this.responseHeaders.get(key); + responseHeaders.push( + + {key}: {value} + , + ); + } + return responseHeaders; + } + + render(): React.Node { + const responseURL = this.responseURL ? ( + + Server response URL: + {this.responseURL} + + ) : null; + + const responseHeaders = this.responseHeaders ? ( + + Server response headers: + {this._renderHeaders()} + + ) : null; + + const response = this.state.responseText ? ( + + Server response: + + + ) : null; + + return ( + + Edit URL to submit: + { + this.submit(event.nativeEvent.text); + }} + style={styles.textInput} + /> + {responseURL} + {responseHeaders} + {response} + + ); + } +} + +const styles = StyleSheet.create({ + textInput: { + flex: 1, + borderRadius: 3, + borderColor: 'grey', + borderWidth: 1, + height: 30, + paddingLeft: 8, + }, + label: { + flex: 1, + color: '#aaa', + fontWeight: '500', + height: 20, + }, + textOutput: { + flex: 1, + fontSize: 17, + borderRadius: 3, + borderColor: 'grey', + borderWidth: 1, + height: 200, + paddingLeft: 8, + }, +}); + +module.exports = HTTPExampleMultiPartFormData; diff --git a/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js b/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js index a450e5df622..f9b6d45246c 100644 --- a/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js +++ b/packages/@react-native-windows/tester/src/js/utils/RNTesterList.windows.js @@ -12,6 +12,14 @@ import type {RNTesterModuleInfo} from '../types/RNTesterTypes'; import ReactNativeFeatureFlags from 'react-native/Libraries/ReactNative/ReactNativeFeatureFlags'; const Components: Array = [ + { + key: 'HTTPExample', + module: require('../examples/HTTP/HTTPExample'), + }, + { + key: 'XHRExample', + module: require('../examples/XHR/XHRExample'), + }, { key: 'ActivityIndicatorExample', category: 'UI', diff --git a/packages/e2e-test-app/windows/RNTesterApp/packages.lock.json b/packages/e2e-test-app/windows/RNTesterApp/packages.lock.json index fe9dd4e8b1a..e459dba5ac7 100644 --- a/packages/e2e-test-app/windows/RNTesterApp/packages.lock.json +++ b/packages/e2e-test-app/windows/RNTesterApp/packages.lock.json @@ -300,4 +300,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/integration-test-app/windows/InteropTestModuleCS/packages.lock.json b/packages/integration-test-app/windows/InteropTestModuleCS/packages.lock.json index 2b78fbef5aa..dc39619502a 100644 --- a/packages/integration-test-app/windows/InteropTestModuleCS/packages.lock.json +++ b/packages/integration-test-app/windows/InteropTestModuleCS/packages.lock.json @@ -14,6 +14,11 @@ "NETStandard.Library": "2.0.3" } }, + "boost": { + "type": "Transitive", + "resolved": "1.76.0", + "contentHash": "p+w3YvNdXL8Cu9Fzrmexssu0tZbWxuf6ywsQqHjDlKFE5ojXHof1HIyMC3zDLfLnh80dIeFcEUAuR2Asg/XHRA==" + }, "Microsoft.Net.Native.Compiler": { "type": "Transitive", "resolved": "2.2.7-rel-27913-00", @@ -38,7 +43,17 @@ "Microsoft.NETCore.Platforms": { "type": "Transitive", "resolved": "2.1.0", - "contentHash": "GmkKfoyerqmsHMn7OZj0AKpcBabD+GaafqphvX2Mw406IwiJRy1pKcKqdCfKJfYmkRyJ6+e+RaUylgdJoDa1jQ==" + "contentHash": "ok+RPAtESz/9MUXeIEz6Lv5XAGQsaNmEYXMsgVALj4D7kqC8gveKWXWXbufLySR2fWrwZf8smyN5RmHu0e4BHA==" + }, + "Microsoft.UI.Xaml": { + "type": "Transitive", + "resolved": "2.7.0", + "contentHash": "dB4im13tfmMgL/V3Ei+3kD2rUF+/lTxAmR4gjJ45l577eljHfdo/KUrxpq/3I1Vp6e5GCDG1evDaEGuDxypLMg==" + }, + "Microsoft.Windows.SDK.BuildTools": { + "type": "Transitive", + "resolved": "10.0.22000.194", + "contentHash": "4L0P3zqut466SIqT3VBeLTNUQTxCBDOrTRymRuROCRJKazcK7ibLz9yAO1nKWRt50ttCj39oAa2Iuz9ZTDmLlg==" }, "Microsoft.UI.Xaml": { "type": "Transitive", @@ -53,6 +68,11 @@ "Microsoft.NETCore.Platforms": "1.1.0" } }, + "ReactNative.Hermes.Windows": { + "type": "Transitive", + "resolved": "0.0.0-2209.12005-9f3ce7e5", + "contentHash": "/RxDNnNA6w42Kz7cEep5wc6j8cmE4jepbjm5ElWXYWPHg4xu5D0AIj6RQ+WW2DxUHaDhbm6pjtS7MJSWagdi+g==" + }, "runtime.win10-arm.Microsoft.Net.Native.Compiler": { "type": "Transitive", "resolved": "2.2.7-rel-27913-00", @@ -296,4 +316,4 @@ } } } -} \ No newline at end of file +} diff --git a/vnext/Shared/Networking/WinRTHttpResource.cpp b/vnext/Shared/Networking/WinRTHttpResource.cpp index 8edc9607ebb..093f21088ee 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.cpp +++ b/vnext/Shared/Networking/WinRTHttpResource.cpp @@ -53,6 +53,34 @@ using winrt::Windows::Web::Http::Headers::HttpMediaTypeHeaderValue; namespace Microsoft::React::Networking { +// May throw winrt::hresult_error +void AttachMultipartHeaders(IHttpContent content, const dynamic &headers) { + HttpMediaTypeHeaderValue contentType{nullptr}; + + // Headers are generally case-insensitive + // https://www.ietf.org/rfc/rfc2616.txt section 4.2 + // TODO: Consolidate with PerformRequest's header parsing. + for (auto &header : headers.items()) { + auto &name = header.first.getString(); + auto &value = header.second.getString(); + + if (boost::iequals(name.c_str(), "Content-Type")) { + contentType = HttpMediaTypeHeaderValue::Parse(to_hstring(value)); + } else if (boost::iequals(name.c_str(), "Authorization")) { + bool success = content.Headers().TryAppendWithoutValidation(to_hstring(name), to_hstring(value)); + if (!success) { + throw hresult_error{E_INVALIDARG, L"Failed to append Authorization"}; + } + } else { + content.Headers().Append(to_hstring(name), to_hstring(value)); + } + } + + if (contentType) { + content.Headers().ContentType(contentType); + } +} + #pragma region WinRTHttpResource WinRTHttpResource::WinRTHttpResource(IHttpClient&& client) noexcept: m_client{ std::move(client) } {} @@ -78,36 +106,36 @@ namespace Microsoft::React::Networking { 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; + // Headers are generally case-insensitive + // https://www.ietf.org/rfc/rfc2616.txt section 4.2 + for (auto &header : reqArgs->Headers) { + auto &name = header.first; + auto &value = header.second; + + if (boost::iequals(name.c_str(), "Content-Type")) { + bool success = HttpMediaTypeHeaderValue::TryParse(to_hstring(value), contentType); + if (!success) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, "Failed to parse Content-Type", false); } } - 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 if (boost::iequals(name.c_str(), "Content-Encoding")) { + contentEncoding = value; + } else if (boost::iequals(name.c_str(), "Content-Length")) { + contentLength = value; + } else if (boost::iequals(name.c_str(), "Authorization")) { + bool success = request.Headers().TryAppendWithoutValidation(to_hstring(name), to_hstring(value)); + if (!success) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, "Failed to append Authorization", false); } } - else { - try { - request.Headers().Append(to_hstring(header.first), to_hstring(header.second)); + } else { + try { + request.Headers().Append(to_hstring(name), to_hstring(value)); + } catch (hresult_error const &e) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), false); } catch (hresult_error const& e) { if (self->m_onError) { @@ -116,6 +144,48 @@ namespace Microsoft::React::Networking { 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["formData"].empty()) { + winrt::Windows::Web::Http::HttpMultipartFormDataContent multiPartContent; + auto formData = data["formData"]; + + // #6046 - Overwriting WinRT's HttpMultipartFormDataContent implicit Content-Type clears the generated boundary + contentType = nullptr; + + for (auto &formDataPart : formData) { + IHttpContent formContent{nullptr}; + if (!formDataPart["string"].isNull()) { + formContent = HttpStringContent{to_hstring(formDataPart["string"].asString())}; + } else if (!formDataPart["uri"].empty()) { + auto filePath = to_hstring(formDataPart["uri"].asString()); + auto file = co_await StorageFile::GetFileFromPathAsync(filePath); + auto stream = co_await file.OpenReadAsync(); + formContent = HttpStreamContent{stream}; + } + + if (formContent) { + AttachMultipartHeaders(formContent, formDataPart["headers"]); + multiPartContent.Add(formContent, to_hstring(formDataPart["fieldName"].asString())); + } + } // foreach form data part + + content = multiPartContent; } // Initialize content @@ -371,11 +441,18 @@ WinRTHttpResource::PerformSendRequest(HttpMethod&& method, Uri&& rtUri, IInspect 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; + auto coRequestOp = CreateRequest(std::move(coMethod), std::move(coUri), props); + co_await lessthrow_await_adapter>{coRequestOp}; + auto coRequestOpHR = coRequestOp.ErrorCode(); + if (coRequestOpHR < 0) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, Utilities::HResultToString(std::move(coRequestOpHR)), false); + } + co_return self->UntrackResponse(reqArgs->RequestId); } + auto coRequest = coRequestOp.GetResults(); + // 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()); From b2d6fcff6235c4c5fff26aacaf19291c897447f4 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Tue, 6 Dec 2022 23:33:30 -0800 Subject: [PATCH 17/45] Remove change file --- ...ative-windows-002cdf98-9f28-4a37-ac6c-33789f8b36b1.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 change/react-native-windows-002cdf98-9f28-4a37-ac6c-33789f8b36b1.json diff --git a/change/react-native-windows-002cdf98-9f28-4a37-ac6c-33789f8b36b1.json b/change/react-native-windows-002cdf98-9f28-4a37-ac6c-33789f8b36b1.json deleted file mode 100644 index 701c1cca7f6..00000000000 --- a/change/react-native-windows-002cdf98-9f28-4a37-ac6c-33789f8b36b1.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "prerelease", - "comment": "Implement Form Data requests", - "packageName": "react-native-windows", - "email": "julio.rocha@microsoft.com", - "dependentChangeType": "patch" -} From c68ba3f7560416146bdebc81f86ce193906bd757 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20C=C3=A9sar=20Rocha?= Date: Tue, 15 Nov 2022 20:44:55 -0800 Subject: [PATCH 18/45] Allow >10MB content for blob responses (#10885) * Allow >10MB content for blob responses * Change files * clang format * Measure reader loaded bytes * Revert AutolinkedNativeModules.g.* --- ...-cc7e46ad-44db-4623-937f-d2899c415dfa.json | 7 ++++ vnext/Shared/Networking/WinRTHttpResource.cpp | 33 +++++++++++-------- 2 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 change/react-native-windows-cc7e46ad-44db-4623-937f-d2899c415dfa.json diff --git a/change/react-native-windows-cc7e46ad-44db-4623-937f-d2899c415dfa.json b/change/react-native-windows-cc7e46ad-44db-4623-937f-d2899c415dfa.json new file mode 100644 index 00000000000..8fefd35209a --- /dev/null +++ b/change/react-native-windows-cc7e46ad-44db-4623-937f-d2899c415dfa.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Allow >10MB content for blob responses", + "packageName": "react-native-windows", + "email": "julio.rocha@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Shared/Networking/WinRTHttpResource.cpp b/vnext/Shared/Networking/WinRTHttpResource.cpp index 093f21088ee..fd1c68d67b1 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.cpp +++ b/vnext/Shared/Networking/WinRTHttpResource.cpp @@ -546,15 +546,23 @@ WinRTHttpResource::PerformSendRequest(HttpMethod&& method, Uri&& rtUri, IInspect auto inputStream = co_await response.Content().ReadAsInputStreamAsync(); auto reader = DataReader{ inputStream }; - // #9510 - 10mb limit on fetch - co_await reader.LoadAsync(10 * 1024 * 1024); + // #9510 - We currently accumulate all incoming request data in 10MB chunks + const uint32_t segmentSize = 10 * 1024 * 1024; // Let response handler take over, if set if (auto responseHandler = self->m_responseHandler.lock()) { if (responseHandler->Supports(reqArgs->ResponseType)) { - auto bytes = vector(reader.UnconsumedBufferLength()); - reader.ReadBytes(bytes); - auto blob = responseHandler->ToResponseData(std::move(bytes)); + // #9510 + vector responseData{}; + while (auto loaded = co_await reader.LoadAsync(segmentSize)) { + auto length = reader.UnconsumedBufferLength(); + auto data = vector(length); + reader.ReadBytes(data); + + responseData.insert(responseData.cend(), data.cbegin(), data.cend()); + } + + auto blob = responseHandler->ToResponseData(std::move(responseData)); if (self->m_onDataDynamic && self->m_onRequestSuccess) { self->m_onDataDynamic(reqArgs->RequestId, std::move(blob)); @@ -570,17 +578,14 @@ WinRTHttpResource::PerformSendRequest(HttpMethod&& method, Uri&& rtUri, IInspect reader.UnicodeEncoding(UnicodeEncoding::Utf8); } - // #9510 - We currently accumulate all incoming request data in 10MB chunks. - uint32_t segmentSize = 10 * 1024 * 1024; + // #9510 string responseData; winrt::Windows::Storage::Streams::IBuffer buffer; - uint32_t length; - do { - co_await reader.LoadAsync(segmentSize); - length = reader.UnconsumedBufferLength(); + while (auto loaded = co_await reader.LoadAsync(segmentSize)) { + auto length = reader.UnconsumedBufferLength(); - if (coReqArgs->IsText) { - auto data = std::vector(length); + if (isText) { + auto data = vector(length); reader.ReadBytes(data); responseData += string(Common::Utilities::CheckedReinterpretCast(data.data()), data.size()); @@ -591,7 +596,7 @@ WinRTHttpResource::PerformSendRequest(HttpMethod&& method, Uri&& rtUri, IInspect responseData += winrt::to_string(std::wstring_view(data)); } - } while (length > 0); + } if (self->m_onData) { self->m_onData(reqArgs->RequestId, std::move(responseData)); From 37d63cbd18e84221bf17c30bb8e9c4d3f2476753 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Tue, 6 Dec 2022 23:34:30 -0800 Subject: [PATCH 19/45] Remove change file --- ...ative-windows-cc7e46ad-44db-4623-937f-d2899c415dfa.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 change/react-native-windows-cc7e46ad-44db-4623-937f-d2899c415dfa.json diff --git a/change/react-native-windows-cc7e46ad-44db-4623-937f-d2899c415dfa.json b/change/react-native-windows-cc7e46ad-44db-4623-937f-d2899c415dfa.json deleted file mode 100644 index 8fefd35209a..00000000000 --- a/change/react-native-windows-cc7e46ad-44db-4623-937f-d2899c415dfa.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "prerelease", - "comment": "Allow >10MB content for blob responses", - "packageName": "react-native-windows", - "email": "julio.rocha@microsoft.com", - "dependentChangeType": "patch" -} From b375b216aab9e73fae4470ca247e3170d8791b54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20C=C3=A9sar=20Rocha?= Date: Tue, 6 Dec 2022 20:08:28 -0800 Subject: [PATCH 20/45] Implement HTTP incremental updates (#10933) * Declare remaining HTTP module event names * Set Progress handler * More debug data * Update AutolinkedNativeModules.g * Define IHttpResource::SetOnIncrementalData * Set up and invoke OnIncrementalData * Clean up Progress functor * Handle didReceiveNetworkDataProgress event - Define IHttpResource::SetOnDataProgress * Default segment size to 8MB * Define IHttpResource::SetOnResponseComplete - Separates request completion event from non-incremental data reception event. * Revert playground-win32 generated files * Change files * Address feedback * Added docs for IHttpResource methods --- ...-00b587ec-6a97-4334-b5eb-18d571ce94d8.json | 7 + vnext/Shared/Modules/HttpModule.cpp | 37 +- vnext/Shared/Networking/IHttpResource.h | 126 +++ vnext/Shared/Networking/WinRTHttpResource.cpp | 831 ++++++++++-------- vnext/Shared/Networking/WinRTHttpResource.h | 10 + 5 files changed, 623 insertions(+), 388 deletions(-) create mode 100644 change/react-native-windows-00b587ec-6a97-4334-b5eb-18d571ce94d8.json diff --git a/change/react-native-windows-00b587ec-6a97-4334-b5eb-18d571ce94d8.json b/change/react-native-windows-00b587ec-6a97-4334-b5eb-18d571ce94d8.json new file mode 100644 index 00000000000..b1668063e0e --- /dev/null +++ b/change/react-native-windows-00b587ec-6a97-4334-b5eb-18d571ce94d8.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Implement HTTP incremental updates", + "packageName": "react-native-windows", + "email": "julio.rocha@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Shared/Modules/HttpModule.cpp b/vnext/Shared/Modules/HttpModule.cpp index 21192dfabf4..cde9df4b54d 100644 --- a/vnext/Shared/Modules/HttpModule.cpp +++ b/vnext/Shared/Modules/HttpModule.cpp @@ -30,11 +30,13 @@ namespace { constexpr char moduleName[] = "Networking"; - // React event names - constexpr char completedResponse[] = "didCompleteNetworkResponse"; - constexpr char receivedResponse[] = "didReceiveNetworkResponse"; - constexpr char receivedData[] = "didReceiveNetworkData"; - constexpr char receivedDataProgress[] = "didReceiveNetworkDataProgress"; +// React event names +constexpr char completedResponse[] = "didCompleteNetworkResponse"; +constexpr char receivedResponse[] = "didReceiveNetworkResponse"; +constexpr char sentData[] = "didSendNetworkData"; +constexpr char receivedIncrementalData[] = "didReceiveNetworkIncrementalData"; +constexpr char receivedDataProgress[] = "didReceiveNetworkDataProgress"; +constexpr char receivedData[] = "didReceiveNetworkData"; static void SetUpHttpResource( shared_ptr resource, @@ -58,12 +60,9 @@ namespace { SendEvent(weakReactInstance, receivedResponse, std::move(args)); }); - resource->SetOnData([weakReactInstance](int64_t requestId, string&& responseData) { - SendEvent(weakReactInstance, receivedData, dynamic::array(requestId, std::move(responseData))); - - // TODO: Move into separate method IF not executed right after onData() - SendEvent(weakReactInstance, completedResponse, dynamic::array(requestId)); - }); + resource->SetOnData([weakReactInstance](int64_t requestId, string &&responseData) { + SendEvent(weakReactInstance, receivedData, dynamic::array(requestId, std::move(responseData))); + }); // Explicitly declaring function type to avoid type inference ambiguity. std::function onDataDynamic = [weakReactInstance]( @@ -72,6 +71,22 @@ namespace { }; resource->SetOnData(std::move(onDataDynamic)); + resource->SetOnIncrementalData( + [weakReactInstance](int64_t requestId, string &&responseData, int64_t progress, int64_t total) { + SendEvent( + weakReactInstance, + receivedIncrementalData, + dynamic::array(requestId, std::move(responseData), progress, total)); + }); + + resource->SetOnDataProgress([weakReactInstance](int64_t requestId, int64_t progress, int64_t total) { + SendEvent(weakReactInstance, receivedDataProgress, dynamic::array(requestId, progress, total)); + }); + + resource->SetOnResponseComplete([weakReactInstance](int64_t requestId) { + SendEvent(weakReactInstance, completedResponse, dynamic::array(requestId)); + }); + resource->SetOnError([weakReactInstance](int64_t requestId, string &&message, bool isTimeout) { dynamic args = dynamic::array(requestId, std::move(message)); if (isTimeout) { diff --git a/vnext/Shared/Networking/IHttpResource.h b/vnext/Shared/Networking/IHttpResource.h index 7f26d59c7ae..d79ffce6e29 100644 --- a/vnext/Shared/Networking/IHttpResource.h +++ b/vnext/Shared/Networking/IHttpResource.h @@ -91,10 +91,136 @@ namespace Microsoft::React::Networking { virtual void ClearCookies() noexcept = 0; + /// + /// Sets a function to be invoked when a request has been successfully responded. + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// virtual void SetOnRequestSuccess(std::function &&handler) noexcept = 0; + + /// + /// Sets a function to be invoked when a response arrives and its headers are received. + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// + /// Object containing basic response data + /// + /// virtual void SetOnResponse(std::function &&handler) noexcept = 0; + + /// + /// Sets a function to be invoked when response content data has been received. + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// + /// Response content payload (plain text or Base64-encoded) + /// + /// virtual void SetOnData(std::function &&handler) noexcept = 0; + + /// + /// Sets a function to be invoked when response content data has been received. + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// + /// Structured response content payload (i.e. Blob data) + /// + /// virtual void SetOnData(std::function &&handler) noexcept = 0; + + /// + /// Sets a function to be invoked when a response content increment has been received. + /// + /// + /// The handler set by this method will only be called if the request sets the incremental updates flag. + /// The handler is also mutually exclusive with those set by `SetOnData`, which are used for one pass, non-incremental + /// updates. + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// + /// Partial response content data increment (non-accumulative) + /// + /// + /// Number of bytes received so far + /// + /// + /// Number of total bytes to receive + /// + /// + virtual void SetOnIncrementalData( + std::function + &&handler) noexcept = 0; + + /// + /// Sets a function to be invoked when response content download progress is reported. + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// + /// Number of bytes received so far + /// + /// + /// Number of total bytes to receive + /// + /// + virtual void SetOnDataProgress( + std::function &&handler) noexcept = 0; + + /// + /// Sets a function to be invoked when a response has been fully handled (either succeeded or failed). + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// + virtual void SetOnResponseComplete(std::function &&handler) noexcept = 0; + + /// + /// Sets a function to be invoked when an error condition is found. + /// + /// + /// The handler's purpose is not to report any given HTTP error status (i.e. 403, 501). + /// It is meant to report application errors when executing HTTP requests. + /// + /// + /// + /// Parameters: + /// + /// Unique number identifying the HTTP request + /// + /// virtual void SetOnError( std::function &&handler) noexcept = 0; }; diff --git a/vnext/Shared/Networking/WinRTHttpResource.cpp b/vnext/Shared/Networking/WinRTHttpResource.cpp index fd1c68d67b1..1d6336f5e64 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.cpp +++ b/vnext/Shared/Networking/WinRTHttpResource.cpp @@ -51,35 +51,52 @@ using winrt::Windows::Web::Http::IHttpClient; using winrt::Windows::Web::Http::IHttpContent; using winrt::Windows::Web::Http::Headers::HttpMediaTypeHeaderValue; +namespace { + + constexpr uint32_t operator""_KiB(unsigned long long int x) { + return static_cast(1024 * x); + } + + constexpr uint32_t operator""_MiB(unsigned long long int x) { + return static_cast(1024_KiB * x); + } + + constexpr char responseTypeText[] = "text"; + constexpr char responseTypeBase64[] = "base64"; + constexpr char responseTypeBlob[] = "blob"; + +} // namespace namespace Microsoft::React::Networking { -// May throw winrt::hresult_error -void AttachMultipartHeaders(IHttpContent content, const dynamic &headers) { - HttpMediaTypeHeaderValue contentType{nullptr}; - - // Headers are generally case-insensitive - // https://www.ietf.org/rfc/rfc2616.txt section 4.2 - // TODO: Consolidate with PerformRequest's header parsing. - for (auto &header : headers.items()) { - auto &name = header.first.getString(); - auto &value = header.second.getString(); - - if (boost::iequals(name.c_str(), "Content-Type")) { - contentType = HttpMediaTypeHeaderValue::Parse(to_hstring(value)); - } else if (boost::iequals(name.c_str(), "Authorization")) { - bool success = content.Headers().TryAppendWithoutValidation(to_hstring(name), to_hstring(value)); - if (!success) { - throw hresult_error{E_INVALIDARG, L"Failed to append Authorization"}; - } - } else { - content.Headers().Append(to_hstring(name), to_hstring(value)); + // May throw winrt::hresult_error + void AttachMultipartHeaders(IHttpContent content, const dynamic& headers) { + HttpMediaTypeHeaderValue contentType{ nullptr }; + + // Headers are generally case-insensitive + // https://www.ietf.org/rfc/rfc2616.txt section 4.2 + // TODO: Consolidate with PerformRequest's header parsing. + for (auto& header : headers.items()) { + auto& name = header.first.getString(); + auto& value = header.second.getString(); + + if (boost::iequals(name.c_str(), "Content-Type")) { + contentType = HttpMediaTypeHeaderValue::Parse(to_hstring(value)); + } + else if (boost::iequals(name.c_str(), "Authorization")) { + bool success = content.Headers().TryAppendWithoutValidation(to_hstring(name), to_hstring(value)); + if (!success) { + throw hresult_error{ E_INVALIDARG, L"Failed to append Authorization" }; + } + } + else { + content.Headers().Append(to_hstring(name), to_hstring(value)); + } } - } - if (contentType) { - content.Headers().ContentType(contentType); + if (contentType) { + content.Headers().ContentType(contentType); + } } -} #pragma region WinRTHttpResource @@ -106,86 +123,96 @@ void AttachMultipartHeaders(IHttpContent content, const dynamic &headers) { string contentEncoding; string contentLength; - // Headers are generally case-insensitive - // https://www.ietf.org/rfc/rfc2616.txt section 4.2 - for (auto &header : reqArgs->Headers) { - auto &name = header.first; - auto &value = header.second; + // Headers are generally case-insensitive + // https://www.ietf.org/rfc/rfc2616.txt section 4.2 + for (auto& header : reqArgs->Headers) { + auto& name = header.first; + auto& value = header.second; - if (boost::iequals(name.c_str(), "Content-Type")) { - bool success = HttpMediaTypeHeaderValue::TryParse(to_hstring(value), contentType); - if (!success) { - if (self->m_onError) { - self->m_onError(reqArgs->RequestId, "Failed to parse Content-Type", false); + if (boost::iequals(name.c_str(), "Content-Type")) { + bool success = HttpMediaTypeHeaderValue::TryParse(to_hstring(value), contentType); + if (!success) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, "Failed to parse Content-Type", false); + } } } - } else if (boost::iequals(name.c_str(), "Content-Encoding")) { - contentEncoding = value; - } else if (boost::iequals(name.c_str(), "Content-Length")) { - contentLength = value; - } else if (boost::iequals(name.c_str(), "Authorization")) { - bool success = request.Headers().TryAppendWithoutValidation(to_hstring(name), to_hstring(value)); - if (!success) { - if (self->m_onError) { - self->m_onError(reqArgs->RequestId, "Failed to append Authorization", false); + else if (boost::iequals(name.c_str(), "Content-Encoding")) { + contentEncoding = value; + } + else if (boost::iequals(name.c_str(), "Content-Length")) { + contentLength = value; + } + else if (boost::iequals(name.c_str(), "Authorization")) { + bool success = request.Headers().TryAppendWithoutValidation(to_hstring(name), to_hstring(value)); + if (!success) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, "Failed to append Authorization", false); + } } } - } else { - try { - request.Headers().Append(to_hstring(name), to_hstring(value)); - } catch (hresult_error const &e) { - if (self->m_onError) { - self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), false); + else { + try { + request.Headers().Append(to_hstring(name), to_hstring(value)); } catch (hresult_error const& e) { if (self->m_onError) { self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), 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["formData"].empty()) { - winrt::Windows::Web::Http::HttpMultipartFormDataContent multiPartContent; - auto formData = data["formData"]; - - // #6046 - Overwriting WinRT's HttpMultipartFormDataContent implicit Content-Type clears the generated boundary - contentType = nullptr; - - for (auto &formDataPart : formData) { - IHttpContent formContent{nullptr}; - if (!formDataPart["string"].isNull()) { - formContent = HttpStringContent{to_hstring(formDataPart["string"].asString())}; - } else if (!formDataPart["uri"].empty()) { - auto filePath = to_hstring(formDataPart["uri"].asString()); - auto file = co_await StorageFile::GetFileFromPathAsync(filePath); - auto stream = co_await file.OpenReadAsync(); - formContent = HttpStreamContent{stream}; + catch (hresult_error const& e) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), false); + } + co_return nullptr; + } } - - if (formContent) { - AttachMultipartHeaders(formContent, formDataPart["headers"]); - multiPartContent.Add(formContent, to_hstring(formDataPart["fieldName"].asString())); + auto bytes = blob["bytes"]; + auto byteVector = vector(bytes.size()); + for (auto& byte : bytes) { + byteVector.push_back(static_cast(byte.asInt())); } - } // foreach form data part - - content = multiPartContent; + 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["formData"].empty()) { + winrt::Windows::Web::Http::HttpMultipartFormDataContent multiPartContent; + auto formData = data["formData"]; + + // #6046 - Overwriting WinRT's HttpMultipartFormDataContent implicit Content-Type clears the generated boundary + contentType = nullptr; + + for (auto& formDataPart : formData) { + IHttpContent formContent{ nullptr }; + if (!formDataPart["string"].isNull()) { + formContent = HttpStringContent{ to_hstring(formDataPart["string"].asString()) }; + } + else if (!formDataPart["uri"].empty()) { + auto filePath = to_hstring(formDataPart["uri"].asString()); + auto file = co_await StorageFile::GetFileFromPathAsync(filePath); + auto stream = co_await file.OpenReadAsync(); + formContent = HttpStreamContent{ stream }; + } + + if (formContent) { + AttachMultipartHeaders(formContent, formDataPart["headers"]); + multiPartContent.Add(formContent, to_hstring(formDataPart["fieldName"].asString())); + } + } // foreach form data part + + content = multiPartContent; } // Initialize content @@ -269,25 +296,56 @@ void AttachMultipartHeaders(IHttpContent content, const dynamic &headers) { } co_return request; - } + } #pragma endregion IWinRTHttpRequestFactory #pragma region IHttpResource - void WinRTHttpResource::SendRequest( - string&& method, - string&& url, - int64_t requestId, - Headers&& headers, - dynamic&& data, - string&& responseType, - bool useIncrementalUpdates, - int64_t timeout, - bool withCredentials, - std::function&& callback) noexcept /*override*/ { - // Enforce supported args - assert(responseType == "text" || responseType == "base64" || responseType == "blob"); + void WinRTHttpResource::SendRequest( + string && method, + string && url, + int64_t requestId, + Headers && headers, + dynamic && data, + string && responseType, + bool useIncrementalUpdates, + int64_t timeout, + bool withCredentials, + std::function && callback) noexcept /*override*/ { + // Enforce supported args + assert(responseType == responseTypeText || responseType == responseTypeBase64 || responseType == responseTypeBlob); + + try { + HttpMethod httpMethod{ to_hstring(std::move(method)) }; + Uri uri{ to_hstring(std::move(url)) }; + + 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(), false); + } + } + catch (hresult_error const& e) { + if (m_onError) { + m_onError(requestId, Utilities::HResultToString(e), false); + } + } + catch (...) { + m_onError(requestId, "Unidentified error sending HTTP request", false); + } + } try { HttpMethod httpMethod{ to_hstring(std::move(method)) }; @@ -318,37 +376,39 @@ void AttachMultipartHeaders(IHttpContent content, const dynamic &headers) { catch (...) { m_onError(requestId, "Unidentified error sending HTTP request", false); } - } - try { - HttpMethod httpMethod{ to_hstring(std::move(method)) }; - Uri uri{ to_hstring(std::move(url)) }; + void WinRTHttpResource::ClearCookies() noexcept /*override*/ { + assert(false); + // NOT IMPLEMENTED + } - 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(), false); + void WinRTHttpResource::SetOnRequestSuccess(function && handler) noexcept /*override*/ { + m_onRequestSuccess = std::move(handler); } - } - catch (hresult_error const& e) { - if (m_onError) { + + void WinRTHttpResource::SetOnResponse(function && handler) noexcept + /*override*/ { + m_onResponse = std::move(handler); + } + + void WinRTHttpResource::SetOnData(function && handler) noexcept + /*override*/ { + m_onData = std::move(handler); + } + + void WinRTHttpResource::SetOnData(function && handler) noexcept + /*override*/ + { + m_onDataDynamic = std::move(handler); + } + + try { + request.Cancel(); + } + catch (hresult_error const& e) { m_onError(requestId, Utilities::HResultToString(e), false); } } - catch (...) { - m_onError(requestId, "Unidentified error sending HTTP request", false); - } void WinRTHttpResource::ClearCookies() noexcept /*override*/ { assert(false); @@ -375,274 +435,291 @@ void AttachMultipartHeaders(IHttpContent content, const dynamic &headers) { m_onDataDynamic = std::move(handler); } - try { - request.Cancel(); + void WinRTHttpResource::SetOnIncrementalData( + function&& handler) noexcept + /*override*/ { + m_onIncrementalData = std::move(handler); } - catch (hresult_error const& e) { - m_onError(requestId, Utilities::HResultToString(e), false); + + void WinRTHttpResource::SetOnDataProgress( + function&& handler) noexcept + /*override*/ { + m_onDataProgress = std::move(handler); + } + + void WinRTHttpResource::SetOnResponseComplete(function&& handler) noexcept /*override*/ { + m_onComplete = std::move(handler); + } + + void WinRTHttpResource::SetOnError( + function&& handler) noexcept + /*override*/ { + m_onError = std::move(handler); } -} - -void WinRTHttpResource::ClearCookies() noexcept /*override*/ { - assert(false); - // NOT IMPLEMENTED -} - -void WinRTHttpResource::SetOnRequestSuccess(function&& handler) noexcept /*override*/ { - m_onRequestSuccess = std::move(handler); -} - -void WinRTHttpResource::SetOnResponse(function&& handler) noexcept -/*override*/ { - m_onResponse = std::move(handler); -} - -void WinRTHttpResource::SetOnData(function&& handler) noexcept -/*override*/ { - m_onData = std::move(handler); -} - -void WinRTHttpResource::SetOnData(function&& handler) noexcept -/*override*/ -{ - m_onDataDynamic = std::move(handler); -} - -void WinRTHttpResource::SetOnError( - function&& handler) noexcept - /*override*/ { - m_onError = std::move(handler); -} #pragma endregion IHttpResource -void WinRTHttpResource::TrackResponse(int64_t requestId, ResponseOperation response) noexcept { - scoped_lock lock{ m_mutex }; - m_responses[requestId] = response; -} - -void WinRTHttpResource::UntrackResponse(int64_t requestId) noexcept { - scoped_lock lock{ m_mutex }; - m_responses.erase(requestId); -} - -fire_and_forget -WinRTHttpResource::PerformSendRequest(HttpMethod&& method, Uri&& rtUri, IInspectable const& args) noexcept { - // Keep references after coroutine suspension. - auto self = shared_from_this(); - auto coArgs = args; - 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 coRequestOp = CreateRequest(std::move(coMethod), std::move(coUri), props); - co_await lessthrow_await_adapter>{coRequestOp}; - auto coRequestOpHR = coRequestOp.ErrorCode(); - if (coRequestOpHR < 0) { - if (self->m_onError) { - self->m_onError(reqArgs->RequestId, Utilities::HResultToString(std::move(coRequestOpHR)), false); - } - co_return self->UntrackResponse(reqArgs->RequestId); + void WinRTHttpResource::TrackResponse(int64_t requestId, ResponseOperation response) noexcept { + scoped_lock lock{ m_mutex }; + m_responses[requestId] = response; } - auto coRequest = coRequestOp.GetResults(); + void WinRTHttpResource::UntrackResponse(int64_t requestId) noexcept { + scoped_lock lock{ m_mutex }; + m_responses.erase(requestId); + } - // 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, reqArgs->ResponseType)) { - auto blob = uriHandler->Fetch(uri); - if (self->m_onDataDynamic && self->m_onRequestSuccess) { - self->m_onDataDynamic(reqArgs->RequestId, std::move(blob)); - self->m_onRequestSuccess(reqArgs->RequestId); - } + fire_and_forget + WinRTHttpResource::PerformSendRequest(HttpMethod&& method, Uri&& rtUri, IInspectable const& args) noexcept { + // Keep references after coroutine suspension. + auto self = shared_from_this(); + auto coArgs = args; + 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); - co_return; + auto coRequestOp = CreateRequest(std::move(coMethod), std::move(coUri), props); + co_await lessthrow_await_adapter>{coRequestOp}; + auto coRequestOpHR = coRequestOp.ErrorCode(); + if (coRequestOpHR < 0) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, Utilities::HResultToString(std::move(coRequestOpHR)), false); } + co_return self->UntrackResponse(reqArgs->RequestId); } - catch (const hresult_error& e) { - if (self->m_onError) - 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(reqArgs->RequestId, e.what(), false); + + auto coRequest = coRequestOp.GetResults(); + + // 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, reqArgs->ResponseType)) { + auto blob = uriHandler->Fetch(uri); + if (self->m_onDataDynamic && self->m_onRequestSuccess) { + self->m_onDataDynamic(reqArgs->RequestId, std::move(blob)); + self->m_onRequestSuccess(reqArgs->RequestId); + } + + if (self->m_onComplete) { + self->m_onComplete(reqArgs->RequestId); + } + + co_return; + } + } + catch (const hresult_error& e) { + if (self->m_onError) + 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(reqArgs->RequestId, e.what(), false); + } } - } - try { - auto sendRequestOp = self->m_client.SendRequestAsync(coRequest); - self->TrackResponse(reqArgs->RequestId, sendRequestOp); + try { + auto sendRequestOp = self->m_client.SendRequestAsync(coRequest); + + auto isText = reqArgs->ResponseType == responseTypeText; - 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); + self->TrackResponse(reqArgs->RequestId, sendRequestOp); - co_await lessthrow_await_adapter{winrt::when_any(sendRequestOp, sendRequestTimeout)}; + 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); - // Cancel either still unfinished coroutine. - sendRequestTimeout.Cancel(); - sendRequestOp.Cancel(); + co_await lessthrow_await_adapter{winrt::when_any(sendRequestOp, sendRequestTimeout)}; - if (*timedOut) { + // Cancel either still unfinished coroutine. + sendRequestTimeout.Cancel(); + sendRequestOp.Cancel(); + + if (*timedOut) { + if (self->m_onError) { + // 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 self->UntrackResponse(reqArgs->RequestId); + } + } + else { + co_await lessthrow_await_adapter{sendRequestOp}; + } + + auto result = sendRequestOp.ErrorCode(); + if (result < 0) { if (self->m_onError) { - // 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); + self->m_onError(reqArgs->RequestId, Utilities::HResultToString(std::move(result)), false); } co_return self->UntrackResponse(reqArgs->RequestId); } - } - else { - co_await lessthrow_await_adapter{sendRequestOp}; - } - auto result = sendRequestOp.ErrorCode(); - if (result < 0) { - if (self->m_onError) { - self->m_onError(reqArgs->RequestId, Utilities::HResultToString(std::move(result)), false); - } - co_return self->UntrackResponse(reqArgs->RequestId); - } + auto response = sendRequestOp.GetResults(); + if (response) { + if (self->m_onResponse) { + auto url = to_string(response.RequestMessage().RequestUri().AbsoluteUri()); - auto response = sendRequestOp.GetResults(); - if (response) { - if (self->m_onResponse) { - auto url = to_string(response.RequestMessage().RequestUri().AbsoluteUri()); + // Gather headers for both the response content and the response itself + // See Invoke-WebRequest PowerShell cmdlet or Chromium response handling + Headers responseHeaders; + for (auto header : response.Headers()) { + responseHeaders.emplace(to_string(header.Key()), to_string(header.Value())); + } + for (auto header : response.Content().Headers()) { + responseHeaders.emplace(to_string(header.Key()), to_string(header.Value())); + } - // Gather headers for both the response content and the response itself - // See Invoke-WebRequest PowerShell cmdlet or Chromium response handling - Headers responseHeaders; - for (auto header : response.Headers()) { - responseHeaders.emplace(to_string(header.Key()), to_string(header.Value())); + self->m_onResponse( + reqArgs->RequestId, + { static_cast(response.StatusCode()), std::move(url), std::move(responseHeaders) }); } - for (auto header : response.Content().Headers()) { - responseHeaders.emplace(to_string(header.Key()), to_string(header.Value())); + } + + // #9534 - Support HTTP incremental updates + if (response && response.Content()) { + auto inputStream = co_await response.Content().ReadAsInputStreamAsync(); + auto reader = DataReader{ inputStream }; + + // Accumulate all incoming request data in 8MB chunks + // Note, the minimum apparent valid chunk size is 128 KB + // Apple's implementation appears to grab 5-8 KB chunks + const uint32_t segmentSize = reqArgs->IncrementalUpdates ? 128_KiB : 8_MiB; + + // Let response handler take over, if set + if (auto responseHandler = self->m_responseHandler.lock()) { + if (responseHandler->Supports(reqArgs->ResponseType)) { + vector responseData{}; + while (auto loaded = co_await reader.LoadAsync(segmentSize)) { + auto length = reader.UnconsumedBufferLength(); + auto data = vector(length); + reader.ReadBytes(data); + + responseData.insert(responseData.cend(), data.cbegin(), data.cend()); + } + + auto blob = responseHandler->ToResponseData(std::move(responseData)); + + if (self->m_onDataDynamic && self->m_onRequestSuccess) { + self->m_onDataDynamic(reqArgs->RequestId, std::move(blob)); + self->m_onRequestSuccess(reqArgs->RequestId); + } + + if (self->m_onComplete) { + self->m_onComplete(reqArgs->RequestId); + } + co_return; + } } - self->m_onResponse( - reqArgs->RequestId, - { static_cast(response.StatusCode()), std::move(url), std::move(responseHeaders) }); - } - } + if (isText) { + reader.UnicodeEncoding(UnicodeEncoding::Utf8); + } - // #9534 - Support HTTP incremental updates - if (response && response.Content()) { - auto inputStream = co_await response.Content().ReadAsInputStreamAsync(); - auto reader = DataReader{ inputStream }; - - // #9510 - We currently accumulate all incoming request data in 10MB chunks - const uint32_t segmentSize = 10 * 1024 * 1024; - - // Let response handler take over, if set - if (auto responseHandler = self->m_responseHandler.lock()) { - if (responseHandler->Supports(reqArgs->ResponseType)) { - // #9510 - vector responseData{}; - while (auto loaded = co_await reader.LoadAsync(segmentSize)) { - auto length = reader.UnconsumedBufferLength(); + int64_t receivedBytes = 0; + string responseData; + winrt::Windows::Storage::Streams::IBuffer buffer; + while (auto loaded = co_await reader.LoadAsync(segmentSize)) { + auto length = reader.UnconsumedBufferLength(); + receivedBytes += length; + + if (isText) { auto data = vector(length); reader.ReadBytes(data); - responseData.insert(responseData.cend(), data.cbegin(), data.cend()); + auto incrementData = string(Common::Utilities::CheckedReinterpretCast(data.data()), data.size()); + // #9534 - Send incremental updates. + // See https://github.com/facebook/react-native/blob/v0.70.6/Libraries/Network/RCTNetworking.mm#L561 + if (reqArgs->IncrementalUpdates) { + responseData = std::move(incrementData); + + if (self->m_onIncrementalData) { + // For total, see #10849 + self->m_onIncrementalData(reqArgs->RequestId, std::move(responseData), receivedBytes, 0 /*total*/); + } + } + else { + responseData += std::move(incrementData); + } } + else { + buffer = reader.ReadBuffer(length); + auto data = CryptographicBuffer::EncodeToBase64String(buffer); - auto blob = responseHandler->ToResponseData(std::move(responseData)); + responseData += winrt::to_string(std::wstring_view(data)); - if (self->m_onDataDynamic && self->m_onRequestSuccess) { - self->m_onDataDynamic(reqArgs->RequestId, std::move(blob)); - self->m_onRequestSuccess(reqArgs->RequestId); + if (self->m_onDataProgress) { + // For total, see #10849 + self->m_onDataProgress(reqArgs->RequestId, receivedBytes, 0 /*total*/); + } } - - co_return; } - } - - auto isText = reqArgs->ResponseType == "text"; - if (isText) { - reader.UnicodeEncoding(UnicodeEncoding::Utf8); - } - - // #9510 - string responseData; - winrt::Windows::Storage::Streams::IBuffer buffer; - while (auto loaded = co_await reader.LoadAsync(segmentSize)) { - auto length = reader.UnconsumedBufferLength(); - - if (isText) { - auto data = vector(length); - reader.ReadBytes(data); - responseData += string(Common::Utilities::CheckedReinterpretCast(data.data()), data.size()); + // If dealing with text-incremental response data, use m_onIncrementalData instead + if (self->m_onData && !(reqArgs->IncrementalUpdates && isText)) { + self->m_onData(reqArgs->RequestId, std::move(responseData)); } - else { - buffer = reader.ReadBuffer(length); - auto data = CryptographicBuffer::EncodeToBase64String(buffer); - responseData += winrt::to_string(std::wstring_view(data)); + if (self->m_onComplete) { + self->m_onComplete(reqArgs->RequestId); } } - - if (self->m_onData) { - self->m_onData(reqArgs->RequestId, std::move(responseData)); + else { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, response == nullptr ? "request failed" : "No response content", false); + } } } - else { + catch (std::exception const& e) { if (self->m_onError) { - self->m_onError(reqArgs->RequestId, response == nullptr ? "request failed" : "No response content", false); + self->m_onError(reqArgs->RequestId, e.what(), false); } } - } - catch (std::exception const& e) { - if (self->m_onError) { - self->m_onError(reqArgs->RequestId, e.what(), false); - } - } - catch (hresult_error const& e) { - if (self->m_onError) { - self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), false); + catch (hresult_error const& e) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), false); + } } - } - catch (...) { - if (self->m_onError) { - self->m_onError(reqArgs->RequestId, "Unhandled exception during request", false); + catch (...) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, "Unhandled exception during request", false); + } } - } - self->UntrackResponse(reqArgs->RequestId); -} // PerformSendRequest + self->UntrackResponse(reqArgs->RequestId); + } // PerformSendRequest #pragma region IHttpModuleProxy -void WinRTHttpResource::AddUriHandler(shared_ptr /*uriHandler*/) noexcept /*override*/ -{ - // TODO: Implement custom URI handling. -} + void WinRTHttpResource::AddUriHandler(shared_ptr /*uriHandler*/) noexcept /*override*/ + { + // TODO: Implement custom URI handling. + } -void WinRTHttpResource::AddRequestBodyHandler(shared_ptr requestBodyHandler) noexcept /*override*/ -{ - m_requestBodyHandler = weak_ptr(requestBodyHandler); -} + void WinRTHttpResource::AddRequestBodyHandler(shared_ptr requestBodyHandler) noexcept /*override*/ + { + m_requestBodyHandler = weak_ptr(requestBodyHandler); + } -void WinRTHttpResource::AddResponseHandler(shared_ptr responseHandler) noexcept /*override*/ -{ - m_responseHandler = weak_ptr(responseHandler); -} + void WinRTHttpResource::AddResponseHandler(shared_ptr responseHandler) noexcept /*override*/ + { + m_responseHandler = weak_ptr(responseHandler); + } #pragma endregion IHttpModuleProxy @@ -650,46 +727,46 @@ void WinRTHttpResource::AddResponseHandler(shared_ptr response #pragma region IHttpResource -/*static*/ shared_ptr IHttpResource::Make( - winrt::Windows::Foundation::IInspectable const& inspectableProperties) noexcept { - using namespace winrt::Microsoft::ReactNative; - using winrt::Windows::Web::Http::HttpClient; + /*static*/ shared_ptr IHttpResource::Make( + winrt::Windows::Foundation::IInspectable const& inspectableProperties) noexcept { + using namespace winrt::Microsoft::ReactNative; + using winrt::Windows::Web::Http::HttpClient; - auto redirFilter = winrt::make(); - HttpClient client; + auto redirFilter = winrt::make(); + HttpClient client; - if (static_cast(GetRuntimeOptionInt("Http.OriginPolicy")) == OriginPolicy::None) { - client = HttpClient{ redirFilter }; - } - else { - auto globalOrigin = GetRuntimeOptionString("Http.GlobalOrigin"); - OriginPolicyHttpFilter::SetStaticOrigin(std::move(globalOrigin)); - auto opFilter = winrt::make(redirFilter); - redirFilter.as()->SetRedirectSource(opFilter.as()); + if (static_cast(GetRuntimeOptionInt("Http.OriginPolicy")) == OriginPolicy::None) { + client = HttpClient{ redirFilter }; + } + else { + auto globalOrigin = GetRuntimeOptionString("Http.GlobalOrigin"); + OriginPolicyHttpFilter::SetStaticOrigin(std::move(globalOrigin)); + auto opFilter = winrt::make(redirFilter); + redirFilter.as()->SetRedirectSource(opFilter.as()); - client = HttpClient{ opFilter }; - } + client = HttpClient{ opFilter }; + } - auto result = std::make_shared(std::move(client)); + 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}); + // 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" }; - auto propBag = ReactPropertyBag{ inspectableProperties.try_as() }; - auto moduleProxy = weak_ptr{ result }; - propBag.Set(propId, std::move(moduleProxy)); - } + // Register resource as HTTP module proxy. + if (inspectableProperties) { + auto propId = ReactPropertyId>>{ L"HttpModule.Proxy" }; + auto propBag = ReactPropertyBag{ inspectableProperties.try_as() }; + auto moduleProxy = weak_ptr{ result }; + propBag.Set(propId, std::move(moduleProxy)); + } - return result; -} + return result; + } -/*static*/ shared_ptr IHttpResource::Make() noexcept { - auto inspectableProperties = IInspectable{ nullptr }; - return Make(inspectableProperties); -} + /*static*/ shared_ptr IHttpResource::Make() noexcept { + auto inspectableProperties = IInspectable{ nullptr }; + return Make(inspectableProperties); + } #pragma endregion IHttpResource diff --git a/vnext/Shared/Networking/WinRTHttpResource.h b/vnext/Shared/Networking/WinRTHttpResource.h index 43591dd772c..76224ea6f63 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.h +++ b/vnext/Shared/Networking/WinRTHttpResource.h @@ -30,6 +30,10 @@ class WinRTHttpResource : public IHttpResource, std::function m_onData; std::function m_onDataDynamic; std::function m_onError; + std::function + m_onIncrementalData; + std::function m_onDataProgress; + std::function m_onComplete; // Used for IHttpModuleProxy std::weak_ptr m_uriHandler; @@ -90,6 +94,12 @@ 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 SetOnIncrementalData( + std::function + &&handler) noexcept override; + void SetOnDataProgress( + std::function &&handler) noexcept override; + void SetOnResponseComplete(std::function &&handler) noexcept override; void SetOnError( std::function &&handler) noexcept override; From b69eeeacf37036d767d63a4be42eb7e40085379b Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Tue, 6 Dec 2022 23:36:40 -0800 Subject: [PATCH 21/45] Remove change file --- ...ative-windows-00b587ec-6a97-4334-b5eb-18d571ce94d8.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 change/react-native-windows-00b587ec-6a97-4334-b5eb-18d571ce94d8.json diff --git a/change/react-native-windows-00b587ec-6a97-4334-b5eb-18d571ce94d8.json b/change/react-native-windows-00b587ec-6a97-4334-b5eb-18d571ce94d8.json deleted file mode 100644 index b1668063e0e..00000000000 --- a/change/react-native-windows-00b587ec-6a97-4334-b5eb-18d571ce94d8.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "prerelease", - "comment": "Implement HTTP incremental updates", - "packageName": "react-native-windows", - "email": "julio.rocha@microsoft.com", - "dependentChangeType": "patch" -} From 8c52692ba39222bf317633dd00d94206746f42ff Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Tue, 6 Dec 2022 23:52:58 -0800 Subject: [PATCH 22/45] clang format --- .../HttpOriginPolicyIntegrationTest.cpp | 4 +- .../HttpResourceIntegrationTests.cpp | 673 +++++----- .../Base/CoreNativeModules.cpp | 81 +- vnext/Shared/Modules/BlobModule.cpp | 438 ++++--- vnext/Shared/Modules/BlobModule.h | 162 +-- vnext/Shared/Modules/FileReaderModule.cpp | 197 ++- vnext/Shared/Modules/HttpModule.cpp | 80 +- vnext/Shared/Modules/IBlobPersistor.h | 22 +- vnext/Shared/Networking/IHttpResource.h | 136 +-- .../Networking/OriginPolicyHttpFilter.cpp | 1081 ++++++++--------- vnext/Shared/Networking/WinRTHttpResource.cpp | 944 +++++++------- vnext/Shared/Networking/WinRTHttpResource.h | 52 +- vnext/Shared/OInstance.cpp | 1045 ++++++++-------- vnext/Shared/OInstance.h | 166 +-- 14 files changed, 2505 insertions(+), 2576 deletions(-) diff --git a/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp b/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp index 4a2e08b8f93..e9ba7d19bd3 100644 --- a/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp +++ b/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp @@ -23,8 +23,8 @@ using std::promise; using std::string; namespace { - constexpr char s_serverHost[]{ "http://localhost" }; - constexpr char s_crossOriginUrl[]{ "http://example.rnw" }; +constexpr char s_serverHost[]{"http://localhost"}; +constexpr char s_crossOriginUrl[]{"http://example.rnw"}; } // namespace // clang-format off diff --git a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp index 90de185c76f..9e15e7971eb 100644 --- a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp +++ b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp @@ -40,7 +40,7 @@ namespace Microsoft::React::Test { TEST_CLASS (HttpResourceIntegrationTest) { static uint16_t s_port; - TEST_CLASS(HttpResourceIntegrationTest) { + TEST_CLASS (HttpResourceIntegrationTest) { static uint16_t s_port; TEST_METHOD_CLEANUP(MethodCleanup) { @@ -58,28 +58,28 @@ TEST_CLASS (HttpResourceIntegrationTest) { string error; int statusCode = 0; - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&resPromise, &statusCode](int64_t, IHttpResource::Response response) { - statusCode = static_cast(response.StatusCode); - }); - resource->SetOnData([&resPromise](int64_t, string &&content) { resPromise.set_value(); }); - resource->SetOnError([&resPromise, &error, &server](int64_t, string &&message, bool) { - error = std::move(message); - resPromise.set_value(); - - server->Stop(); - }); + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&resPromise, &statusCode](int64_t, IHttpResource::Response response) { + statusCode = static_cast(response.StatusCode); + }); + resource->SetOnData([&resPromise](int64_t, string &&content) { resPromise.set_value(); }); + resource->SetOnError([&resPromise, &error, &server](int64_t, string &&message, bool) { + error = std::move(message); + resPromise.set_value(); + + server->Stop(); + }); resource->SendRequest( - "GET", - std::move(url), - 0, /*requestId*/ - {}, /*header*/ - {}, /*data*/ - "text", - false, - 0 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {}); + "GET", + std::move(url), + 0, /*requestId*/ + {}, /*header*/ + {}, /*data*/ + "text", + false, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {}); // Synchronize response. resPromise.get_future().wait(); @@ -97,7 +97,7 @@ TEST_CLASS (HttpResourceIntegrationTest) { IHttpResource::Response response; auto server = make_shared(s_port); - server->Callbacks().OnGet = [](const DynamicRequest& request) -> ResponseWrapper { + server->Callbacks().OnGet = [](const DynamicRequest &request) -> ResponseWrapper { DynamicResponse response; response.result(http::status::ok); @@ -108,39 +108,39 @@ TEST_CLASS (HttpResourceIntegrationTest) { // Response arbitrary header response.set("ResponseHeaderName1", "ResponseHeaderValue1"); - return { std::move(response) }; + return {std::move(response)}; }; server->Start(); auto resource = IHttpResource::Make(); resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { response = callbackResponse; - rcPromise.set_value(); - }); - resource->SetOnError([&rcPromise, &error, &server](int64_t, string &&message, bool) { - error = std::move(message); - rcPromise.set_value(); + rcPromise.set_value(); + }); + resource->SetOnError([&rcPromise, &error, &server](int64_t, string &&message, bool) { + error = std::move(message); + rcPromise.set_value(); - server->Abort(); - }); + server->Abort(); + }); //clang-format off resource->SendRequest( - "GET", - std::move(url), - 0, /*requestId*/ - { - {"Content-Type", "application/json"}, - {"Content-Encoding", "ASCII"}, - {"name3", "value3"}, - {"name4", "value4"}, - }, - {}, /*data*/ - "text", - false, - 0 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {}); + "GET", + std::move(url), + 0, /*requestId*/ + { + {"Content-Type", "application/json"}, + {"Content-Encoding", "ASCII"}, + {"name3", "value3"}, + {"name4", "value4"}, + }, + {}, /*data*/ + "text", + false, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {}); //clang-format on rcPromise.get_future().wait(); @@ -149,15 +149,12 @@ TEST_CLASS (HttpResourceIntegrationTest) { Assert::AreEqual({}, error, L"Error encountered"); for (auto header : response.Headers) { if (header.first == "Server") { - Assert::AreEqual({ "Microsoft::React::Test::HttpServer" }, header.second, L"Wrong header"); - } - else if (header.first == "Content-Length") { - Assert::AreEqual({ "0" }, header.second, L"Wrong header"); - } - else if (header.first == "ResponseHeaderName1") { - Assert::AreEqual({ "ResponseHeaderValue1" }, header.second, L"Wrong header"); - } - else { + Assert::AreEqual({"Microsoft::React::Test::HttpServer"}, header.second, L"Wrong header"); + } else if (header.first == "Content-Length") { + Assert::AreEqual({"0"}, header.second, L"Wrong header"); + } else if (header.first == "ResponseHeaderName1") { + Assert::AreEqual({"ResponseHeaderValue1"}, header.second, L"Wrong header"); + } else { string message = "Unexpected header: [" + header.first + "]=[" + header.second + "]"; Assert::Fail(Microsoft::Common::Unicode::Utf8ToUtf16(message).c_str()); } @@ -167,13 +164,13 @@ TEST_CLASS (HttpResourceIntegrationTest) { TEST_METHOD(RequestGetExplicitUserAgentSucceeds) { string url = "https://api.github.com/repos/microsoft/react-native-xaml"; - auto resource = IHttpResource::Make(); - resource->SetOnError([&error, &promise](int64_t, string &&message, bool) { - error = message; - promise.set_value(); - }); + auto resource = IHttpResource::Make(); + resource->SetOnError([&error, &promise](int64_t, string &&message, bool) { + error = message; + promise.set_value(); + }); - resource->SendRequest("GET", "http://nonexistinghost", 0, {}, {}, "text", false, 0, false, [](int64_t) {}); + resource->SendRequest("GET", "http://nonexistinghost", 0, {}, {}, "text", false, 0, false, [](int64_t) {}); promise.get_future().wait(); @@ -193,74 +190,73 @@ TEST_CLASS (HttpResourceIntegrationTest) { string content; auto server = make_shared(s_port); - server->Callbacks().OnOptions = [](const DynamicRequest& request) -> ResponseWrapper { + server->Callbacks().OnOptions = [](const DynamicRequest &request) -> ResponseWrapper { EmptyResponse response; response.result(http::status::partial_content); response.set("PreflightName", "PreflightValue"); - return { std::move(response) }; + return {std::move(response)}; }; - server->Callbacks().OnGet = [](const DynamicRequest& request) -> ResponseWrapper { + server->Callbacks().OnGet = [](const DynamicRequest &request) -> ResponseWrapper { DynamicResponse response; response.result(http::status::ok); response.body() = Test::CreateStringResponseBody("Response Body"); - return { std::move(response) }; + return {std::move(response)}; }; server->Start(); auto resource = IHttpResource::Make(); resource->SetOnResponse([&getResponse, &getResponsePromise, &optionsResponse, &optionsPromise]( - int64_t, IHttpResource::Response callbackResponse) { - if (callbackResponse.StatusCode == static_cast(http::status::ok)) { - getResponse = callbackResponse; - getResponsePromise.set_value(); - } - else if (callbackResponse.StatusCode == static_cast(http::status::partial_content)) { - optionsResponse = callbackResponse; - optionsPromise.set_value(); - } - }); - resource->SetOnData([&getDataPromise, &content](int64_t, string&& responseData) { + int64_t, IHttpResource::Response callbackResponse) { + if (callbackResponse.StatusCode == static_cast(http::status::ok)) { + getResponse = callbackResponse; + getResponsePromise.set_value(); + } else if (callbackResponse.StatusCode == static_cast(http::status::partial_content)) { + optionsResponse = callbackResponse; + optionsPromise.set_value(); + } + }); + resource->SetOnData([&getDataPromise, &content](int64_t, string &&responseData) { content = std::move(responseData); - if (!content.empty()) - getDataPromise.set_value(); - }); - resource->SetOnError( - [&optionsPromise, &getResponsePromise, &getDataPromise, &error, &server](int64_t, string &&message, bool) { - error = std::move(message); + if (!content.empty()) + getDataPromise.set_value(); + }); + resource->SetOnError( + [&optionsPromise, &getResponsePromise, &getDataPromise, &error, &server](int64_t, string &&message, bool) { + error = std::move(message); - optionsPromise.set_value(); - getResponsePromise.set_value(); - getDataPromise.set_value(); + optionsPromise.set_value(); + getResponsePromise.set_value(); + getDataPromise.set_value(); - server->Stop(); - }); + server->Stop(); + }); //clang-format off resource->SendRequest( - "OPTIONS", - string{ url }, - 0, /*requestId*/ - {}, /*headers*/ - {}, /*data*/ - "text", - false, - 1000 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {}); + "OPTIONS", + string{url}, + 0, /*requestId*/ + {}, /*headers*/ + {}, /*data*/ + "text", + false, + 1000 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {}); resource->SendRequest( - "GET", - std::move(url), - 0, /*requestId*/ - {}, /*headers*/ - {}, /*data*/ - "text", - false, - 0 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {}); + "GET", + std::move(url), + 0, /*requestId*/ + {}, /*headers*/ + {}, /*data*/ + "text", + false, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {}); //clang-format on optionsPromise.get_future().wait(); @@ -272,273 +268,272 @@ TEST_CLASS (HttpResourceIntegrationTest) { Assert::AreEqual(static_cast(1), optionsResponse.Headers.size()); for (auto header : optionsResponse.Headers) { if (header.first == "PreflightName") { - Assert::AreEqual({ "PreflightValue" }, header.second, L"Wrong header"); - } - else { + Assert::AreEqual({"PreflightValue"}, header.second, L"Wrong header"); + } else { string message = "Unexpected header: [" + header.first + "]=[" + header.second + "]"; Assert::Fail(Microsoft::Common::Unicode::Utf8ToUtf16(message).c_str()); } } - Assert::AreEqual({ "Response Body" }, content); + Assert::AreEqual({"Response Body"}, content); } - TEST_METHOD(SimpleRedirectGetSucceeds) { - auto port1 = s_port; - auto port2 = ++s_port; - string url = "http://localhost:" + std::to_string(port1); + TEST_METHOD(SimpleRedirectGetSucceeds) { + 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; + promise responsePromise; + promise contentPromise; + IHttpResource::Response responseResult; + string content; + string error; auto server1 = make_shared(port1); - server1->Callbacks().OnGet = [port2](const DynamicRequest& request) -> ResponseWrapper { + server1->Callbacks().OnGet = [port2](const DynamicRequest &request) -> ResponseWrapper { DynamicResponse response; response.result(http::status::moved_permanently); - response.set(http::field::location, { "http://localhost:" + std::to_string(port2) }); + response.set(http::field::location, {"http://localhost:" + std::to_string(port2)}); - return { std::move(response) }; + return {std::move(response)}; }; auto server2 = make_shared(port2); - server2->Callbacks().OnGet = [](const DynamicRequest& request) -> ResponseWrapper { + server2->Callbacks().OnGet = [](const DynamicRequest &request) -> ResponseWrapper { DynamicResponse response; response.result(http::status::ok); response.body() = Test::CreateStringResponseBody("Redirect Content"); - return { std::move(response) }; + 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); + 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); + 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(); - }); + responsePromise.set_value(); + contentPromise.set_value(); + }); //clang-format off resource->SendRequest( - "GET", - std::move(url), - 0, /*requestId*/ - {}, /*headers*/ - {}, /*data*/ - "text", - false, /*useIncrementalUpdates*/ - 0 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {}); + "GET", + 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(); + 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(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); + Assert::AreEqual({}, error, L"Error encountered"); + 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()) + 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(); - }); - 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; + }); + + //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/Microsoft.ReactNative/Base/CoreNativeModules.cpp b/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp index cc5d1cd8b19..692db7bf8ec 100644 --- a/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp +++ b/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp @@ -19,82 +19,81 @@ namespace Microsoft::ReactNative { - using winrt::Microsoft::ReactNative::ReactPropertyBag; +using winrt::Microsoft::ReactNative::ReactPropertyBag; - namespace { +namespace { - using winrt::Microsoft::ReactNative::ReactPropertyId; +using winrt::Microsoft::ReactNative::ReactPropertyId; - bool HasPackageIdentity() noexcept { - static const bool hasPackageIdentity = []() noexcept { - auto packageStatics = winrt::get_activation_factory( - winrt::name_of()); - auto abiPackageStatics = static_cast *>( - winrt::get_abi(packageStatics)); - winrt::com_ptr> dummy; - return abiPackageStatics->get_Current(winrt::put_abi(dummy)) != - winrt::impl::hresult_from_win32(APPMODEL_ERROR_NO_PACKAGE); - }(); +bool HasPackageIdentity() noexcept { + static const bool hasPackageIdentity = []() noexcept { + auto packageStatics = winrt::get_activation_factory( + winrt::name_of()); + auto abiPackageStatics = static_cast *>( + winrt::get_abi(packageStatics)); + winrt::com_ptr> dummy; + return abiPackageStatics->get_Current(winrt::put_abi(dummy)) != + winrt::impl::hresult_from_win32(APPMODEL_ERROR_NO_PACKAGE); + }(); - return hasPackageIdentity; - } + return hasPackageIdentity; +} - ReactPropertyId HttpUseMonolithicModuleProperty() noexcept { - static ReactPropertyId propId{ - L"ReactNative.Http" - L"UseMonolithicModule" }; - return propId; - } +ReactPropertyId HttpUseMonolithicModuleProperty() noexcept { + static ReactPropertyId propId{ + L"ReactNative.Http" + L"UseMonolithicModule"}; + return propId; +} - } // namespace +} // namespace - std::vector GetCoreModules( - const std::shared_ptr& batchingUIMessageQueue, +std::vector GetCoreModules( + const std::shared_ptr &batchingUIMessageQueue, const std::shared_ptr - & jsMessageQueue, // JS engine thread (what we use for external modules) - Mso::CntPtr&& appearanceListener, - Mso::CntPtr&& context) noexcept { - std::vector modules; + &jsMessageQueue, // JS engine thread (what we use for external modules) + Mso::CntPtr &&appearanceListener, + Mso::CntPtr &&context) noexcept { + std::vector modules; - modules.emplace_back( + modules.emplace_back( "Networking", [props = context->Properties()]() { return Microsoft::React::CreateHttpModule(props); }, jsMessageQueue); - modules.emplace_back( + 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( + // 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)); }, batchingUIMessageQueue); - modules.emplace_back( + modules.emplace_back( AppearanceModule::Name, [appearanceListener = std::move(appearanceListener)]() mutable { return std::make_unique(std::move(appearanceListener)); }, jsMessageQueue); - // AsyncStorageModule doesn't work without package identity (it indirectly depends on - // Windows.Storage.StorageFile), so check for package identity before adding it. - modules.emplace_back( + // AsyncStorageModule doesn't work without package identity (it indirectly depends on + // Windows.Storage.StorageFile), so check for package identity before adding it. + modules.emplace_back( "AsyncLocalStorage", []() -> std::unique_ptr { if (HasPackageIdentity()) { return std::make_unique(L"asyncStorage"); - } - else { + } else { return std::make_unique(); } }, jsMessageQueue); - return modules; - } + return modules; +} } // namespace Microsoft::ReactNative diff --git a/vnext/Shared/Modules/BlobModule.cpp b/vnext/Shared/Modules/BlobModule.cpp index 9bb2c4f9f45..68803d03d4a 100644 --- a/vnext/Shared/Modules/BlobModule.cpp +++ b/vnext/Shared/Modules/BlobModule.cpp @@ -41,147 +41,70 @@ using winrt::Windows::Security::Cryptography::CryptographicBuffer; namespace fs = std::filesystem; namespace { - constexpr char moduleName[] = "BlobModule"; - constexpr char blobKey[] = "blob"; - constexpr char blobIdKey[] = "blobId"; - constexpr char offsetKey[] = "offset"; - constexpr char sizeKey[] = "size"; - constexpr char typeKey[] = "type"; - constexpr char dataKey[] = "data"; +constexpr char moduleName[] = "BlobModule"; +constexpr char blobKey[] = "blob"; +constexpr char blobIdKey[] = "blobId"; +constexpr char offsetKey[] = "offset"; +constexpr char sizeKey[] = "size"; +constexpr char typeKey[] = "type"; +constexpr char dataKey[] = "data"; } // namespace namespace Microsoft::React { #pragma region BlobModule - BlobModule::BlobModule(winrt::Windows::Foundation::IInspectable const& inspectableProperties) noexcept - : m_sharedState{ std::make_shared() }, - m_blobPersistor{ std::make_shared() }, - m_contentHandler{ std::make_shared(m_blobPersistor) }, - m_requestBodyHandler{ std::make_shared(m_blobPersistor) }, - m_responseHandler{ std::make_shared(m_blobPersistor) }, - m_inspectableProperties{ inspectableProperties } { - auto propBag = ReactPropertyBag{ m_inspectableProperties.try_as() }; - - auto contentHandlerPropId = - ReactPropertyId>>{ L"BlobModule.ContentHandler" }; - auto contentHandler = weak_ptr{ m_contentHandler }; - propBag.Set(contentHandlerPropId, std::move(contentHandler)); - - auto blobPersistorPropId = ReactPropertyId>>{ L"Blob.Persistor" }; - auto blobPersistor = weak_ptr{ m_blobPersistor }; - propBag.Set(blobPersistorPropId, std::move(blobPersistor)); - - m_sharedState->Module = this; - } +BlobModule::BlobModule(winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept + : m_sharedState{std::make_shared()}, + m_blobPersistor{std::make_shared()}, + m_contentHandler{std::make_shared(m_blobPersistor)}, + m_requestBodyHandler{std::make_shared(m_blobPersistor)}, + m_responseHandler{std::make_shared(m_blobPersistor)}, + m_inspectableProperties{inspectableProperties} { + auto propBag = ReactPropertyBag{m_inspectableProperties.try_as()}; + + auto contentHandlerPropId = + ReactPropertyId>>{L"BlobModule.ContentHandler"}; + auto contentHandler = weak_ptr{m_contentHandler}; + propBag.Set(contentHandlerPropId, std::move(contentHandler)); + + auto blobPersistorPropId = ReactPropertyId>>{L"Blob.Persistor"}; + auto blobPersistor = weak_ptr{m_blobPersistor}; + propBag.Set(blobPersistorPropId, std::move(blobPersistor)); + + m_sharedState->Module = this; +} - BlobModule::~BlobModule() noexcept /*override*/ { - m_sharedState->Module = nullptr; - } +BlobModule::~BlobModule() noexcept /*override*/ { + m_sharedState->Module = nullptr; +} #pragma region CxxModule - string BlobModule::getName() { - return moduleName; - } - - std::map BlobModule::getConstants() { - return { {"BLOB_URI_SCHEME", blobKey}, {"BLOB_URI_HOST", {}} }; - } +string BlobModule::getName() { + return moduleName; +} - vector BlobModule::getMethods() { - return { - {"addNetworkingHandler", - [propBag = ReactPropertyBag{m_inspectableProperties.try_as()}, - requestBodyHandler = m_requestBodyHandler, - responseHandler = m_responseHandler](dynamic args) { - auto propId = ReactPropertyId>>{L"HttpModule.Proxy"}; - - if (auto prop = propBag.Get(propId)) { - if (auto httpHandler = prop.Value().lock()) { - httpHandler->AddRequestBodyHandler(requestBodyHandler); - httpHandler->AddResponseHandler(responseHandler); - } - } - // TODO: else emit error? - }}, - - {"addWebSocketHandler", - [contentHandler = m_contentHandler](dynamic args) { - auto id = jsArgAsInt(args, 0); - - contentHandler->Register(id); - }}, - - {"removeWebSocketHandler", - [contentHandler = m_contentHandler](dynamic args) { - auto id = jsArgAsInt(args, 0); - - contentHandler->Unregister(id); - }}, - - {"sendOverSocket", - [weakState = weak_ptr(m_sharedState), - persistor = m_blobPersistor, - propBag = ReactPropertyBag{m_inspectableProperties.try_as()}](dynamic args) { - auto propId = ReactPropertyId>>{L"WebSocketModule.Proxy"}; - shared_ptr wsProxy; - if (auto prop = propBag.Get(propId)) { - wsProxy = prop.Value().lock(); - } - if (!wsProxy) { - return; - } +std::map BlobModule::getConstants() { + return {{"BLOB_URI_SCHEME", blobKey}, {"BLOB_URI_HOST", {}}}; +} - auto blob = jsArgAsObject(args, 0); - auto blobId = blob[blobIdKey].getString(); - auto offset = blob[offsetKey].getInt(); - auto size = blob[sizeKey].getInt(); - auto socketID = jsArgAsInt(args, 1); - - winrt::array_view data; - try { - data = persistor->ResolveMessage(std::move(blobId), offset, size); - } catch (const std::exception &e) { - if (auto sharedState = weakState.lock()) { - Modules::SendEvent(sharedState->Module->getInstance(), "blobFailed", e.what()); - } - return; +vector BlobModule::getMethods() { + return { + {"addNetworkingHandler", + [propBag = ReactPropertyBag{m_inspectableProperties.try_as()}, + requestBodyHandler = m_requestBodyHandler, + responseHandler = m_responseHandler](dynamic args) { + auto propId = ReactPropertyId>>{L"HttpModule.Proxy"}; + + if (auto prop = propBag.Get(propId)) { + if (auto httpHandler = prop.Value().lock()) { + httpHandler->AddRequestBodyHandler(requestBodyHandler); + httpHandler->AddResponseHandler(responseHandler); } - - auto buffer = CryptographicBuffer::CreateFromByteArray(data); - auto winrtString = CryptographicBuffer::EncodeToBase64String(std::move(buffer)); - auto base64String = Common::Unicode::Utf16ToUtf8(std::move(winrtString)); - - wsProxy->SendBinary(std::move(base64String), socketID); - }}, - - {"createFromParts", - // As of React Native 0.67, instance is set AFTER CxxModule::getMethods() is invoked. - // Use getInstance() directly once - // https://github.com/facebook/react-native/commit/1d45b20b6c6ba66df0485cdb9be36463d96cf182 becomes available. - [persistor = m_blobPersistor, weakState = weak_ptr(m_sharedState)](dynamic args) { - auto parts = jsArgAsArray(args, 0); // Array - auto blobId = jsArgAsString(args, 1); - vector buffer{}; - - for (const auto &part : parts) { - auto type = part[typeKey].asString(); - if (blobKey == type) { - auto blob = part[dataKey]; - winrt::array_view bufferPart; - try { - bufferPart = persistor->ResolveMessage( - blob[blobIdKey].asString(), blob[offsetKey].asInt(), blob[sizeKey].asInt()); - } catch (const std::exception &e) { - if (auto sharedState = weakState.lock()) { - Modules::SendEvent(sharedState->Module->getInstance(), "blobFailed", e.what()); - } - return; - } - } - // TODO: else emit error? - }}, + } + // TODO: else emit error? + }}, {"addWebSocketHandler", [contentHandler = m_contentHandler](dynamic args) { @@ -216,77 +139,151 @@ namespace Microsoft::React { 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) { + if (auto sharedState = weakState.lock()) { + Modules::SendEvent(sharedState->Module->getInstance(), "blobFailed", e.what()); + } + return; } - catch (const std::exception& e) { - if (auto sharedState = weakState.lock()) { - Modules::SendEvent(sharedState->Module->getInstance(), "blobFailed", e.what()); - } - return; -} - -auto buffer = CryptographicBuffer::CreateFromByteArray(data); -auto winrtString = CryptographicBuffer::EncodeToBase64String(std::move(buffer)); -auto base64String = Common::Unicode::Utf16ToUtf8(std::move(winrtString)); - -wsProxy->SendBinary(std::move(base64String), socketID); -}}, - -{"createFromParts", -// As of React Native 0.67, instance is set AFTER CxxModule::getMethods() is invoked. -// Use getInstance() directly once -// https://github.com/facebook/react-native/commit/1d45b20b6c6ba66df0485cdb9be36463d96cf182 becomes available. -[persistor = m_blobPersistor, weakState = weak_ptr(m_sharedState)](dynamic args) { - auto parts = jsArgAsArray(args, 0); // Array - auto blobId = jsArgAsString(args, 1); - vector buffer{}; - - for (const auto& part : parts) { - auto type = part[typeKey].asString(); - if (blobKey == type) { - auto blob = part[dataKey]; - winrt::array_view bufferPart; - try { - bufferPart = persistor->ResolveMessage( - blob[blobIdKey].asString(), blob[offsetKey].asInt(), blob[sizeKey].asInt()); - } -catch (const std::exception& e) { -if (auto sharedState = weakState.lock()) { - Modules::SendEvent(sharedState->Module->getInstance(), "blobFailed", e.what()); -} -return; -} - -buffer.reserve(buffer.size() + bufferPart.size()); -buffer.insert(buffer.end(), bufferPart.begin(), bufferPart.end()); -} -else if ("string" == type) { -auto data = part[dataKey].asString(); - -buffer.reserve(buffer.size() + data.size()); -buffer.insert(buffer.end(), data.begin(), data.end()); -} -else { -if (auto state = weakState.lock()) { - auto message = "Invalid type for blob: " + type; - Modules::SendEvent(state->Module->getInstance(), "blobFailed", std::move(message)); -} -return; -} -} -persistor->StoreMessage(std::move(buffer), std::move(blobId)); -}}, + auto buffer = CryptographicBuffer::CreateFromByteArray(data); + auto winrtString = CryptographicBuffer::EncodeToBase64String(std::move(buffer)); + auto base64String = Common::Unicode::Utf16ToUtf8(std::move(winrtString)); -{"release", - [persistor = m_blobPersistor](dynamic args) // blobId: string - { - auto blobId = jsArgAsString(args, 0); + wsProxy->SendBinary(std::move(base64String), socketID); + }}, - persistor->RemoveMessage(std::move(blobId)); - }} }; + {"createFromParts", + // As of React Native 0.67, instance is set AFTER CxxModule::getMethods() is invoked. + // Use getInstance() directly once + // https://github.com/facebook/react-native/commit/1d45b20b6c6ba66df0485cdb9be36463d96cf182 becomes available. + [persistor = m_blobPersistor, weakState = weak_ptr(m_sharedState)](dynamic args) { + auto parts = jsArgAsArray(args, 0); // Array + auto blobId = jsArgAsString(args, 1); + vector buffer{}; + + for (const auto &part : parts) { + auto type = part[typeKey].asString(); + if (blobKey == type) { + auto blob = part[dataKey]; + winrt::array_view bufferPart; + try { + bufferPart = persistor->ResolveMessage( + blob[blobIdKey].asString(), blob[offsetKey].asInt(), blob[sizeKey].asInt()); + } catch (const std::exception &e) { + if (auto sharedState = weakState.lock()) { + Modules::SendEvent(sharedState->Module->getInstance(), "blobFailed", e.what()); + } + return; + } + } + // TODO: else emit error? + } + }, + + {"addWebSocketHandler", + [contentHandler = m_contentHandler](dynamic args) { + auto id = jsArgAsInt(args, 0); + + contentHandler->Register(id); + }}, + + {"removeWebSocketHandler", + [contentHandler = m_contentHandler](dynamic args) { + auto id = jsArgAsInt(args, 0); + + contentHandler->Unregister(id); + }}, + + {"sendOverSocket", + [weakState = weak_ptr(m_sharedState), + persistor = m_blobPersistor, + propBag = ReactPropertyBag{m_inspectableProperties.try_as()}](dynamic args) { + auto propId = ReactPropertyId>>{L"WebSocketModule.Proxy"}; + shared_ptr wsProxy; + if (auto prop = propBag.Get(propId)) { + wsProxy = prop.Value().lock(); + } + if (!wsProxy) { + return; + } + + auto blob = jsArgAsObject(args, 0); + auto blobId = blob[blobIdKey].getString(); + auto offset = blob[offsetKey].getInt(); + auto size = blob[sizeKey].getInt(); + auto socketID = jsArgAsInt(args, 1); + + winrt::array_view data; + try { + data = persistor->ResolveMessage(std::move(blobId), offset, size); + } catch (const std::exception &e) { + if (auto sharedState = weakState.lock()) { + Modules::SendEvent(sharedState->Module->getInstance(), "blobFailed", e.what()); + } + return; + } + + auto buffer = CryptographicBuffer::CreateFromByteArray(data); + auto winrtString = CryptographicBuffer::EncodeToBase64String(std::move(buffer)); + auto base64String = Common::Unicode::Utf16ToUtf8(std::move(winrtString)); + + wsProxy->SendBinary(std::move(base64String), socketID); + }}, + + {"createFromParts", + // As of React Native 0.67, instance is set AFTER CxxModule::getMethods() is invoked. + // Use getInstance() directly once + // https://github.com/facebook/react-native/commit/1d45b20b6c6ba66df0485cdb9be36463d96cf182 becomes available. + [persistor = m_blobPersistor, weakState = weak_ptr(m_sharedState)](dynamic args) { + auto parts = jsArgAsArray(args, 0); // Array + auto blobId = jsArgAsString(args, 1); + vector buffer{}; + + for (const auto &part : parts) { + auto type = part[typeKey].asString(); + if (blobKey == type) { + auto blob = part[dataKey]; + winrt::array_view bufferPart; + try { + bufferPart = persistor->ResolveMessage( + blob[blobIdKey].asString(), blob[offsetKey].asInt(), blob[sizeKey].asInt()); + } catch (const std::exception &e) { + if (auto sharedState = weakState.lock()) { + Modules::SendEvent(sharedState->Module->getInstance(), "blobFailed", e.what()); + } + return; + } + + buffer.reserve(buffer.size() + bufferPart.size()); + buffer.insert(buffer.end(), bufferPart.begin(), bufferPart.end()); + } else if ("string" == type) { + auto data = part[dataKey].asString(); + + buffer.reserve(buffer.size() + data.size()); + buffer.insert(buffer.end(), data.begin(), data.end()); + } else { + if (auto state = weakState.lock()) { + auto message = "Invalid type for blob: " + type; + Modules::SendEvent(state->Module->getInstance(), "blobFailed", std::move(message)); + } + return; + } + } + + persistor->StoreMessage(std::move(buffer), std::move(blobId)); + }}, + + {"release", + [persistor = m_blobPersistor](dynamic args) // blobId: string + { + auto blobId = jsArgAsString(args, 0); + + persistor->RemoveMessage(std::move(blobId)); + }}}; } #pragma endregion CxxModule @@ -297,43 +294,43 @@ persistor->StoreMessage(std::move(buffer), std::move(blobId)); #pragma region IBlobPersistor -winrt::array_view MemoryBlobPersistor::ResolveMessage(string &&blobId, int64_t offset, int64_t size) { - if (size < 1) - return {}; + winrt::array_view MemoryBlobPersistor::ResolveMessage(string && blobId, int64_t offset, int64_t size) { + if (size < 1) + return {}; - scoped_lock lock{ m_mutex }; + scoped_lock lock{m_mutex}; auto dataItr = m_blobs.find(std::move(blobId)); // Not found. if (dataItr == m_blobs.cend()) throw std::invalid_argument("Blob object not found"); - auto& bytes = (*dataItr).second; + auto &bytes = (*dataItr).second; auto endBound = static_cast(offset + size); // Out of bounds. 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 { - scoped_lock lock{ m_mutex }; + void MemoryBlobPersistor::RemoveMessage(string && blobId) noexcept { + scoped_lock lock{m_mutex}; m_blobs.erase(std::move(blobId)); } - void MemoryBlobPersistor::StoreMessage(vector&& message, string&& blobId) noexcept { - scoped_lock lock{ m_mutex }; + void MemoryBlobPersistor::StoreMessage(vector && message, string && blobId) noexcept { + scoped_lock lock{m_mutex}; m_blobs.insert_or_assign(std::move(blobId), std::move(message)); } - string MemoryBlobPersistor::StoreMessage(vector&& message) noexcept { + string MemoryBlobPersistor::StoreMessage(vector && message) noexcept { // substr(1, 36) strips curly braces from a GUID. auto blobId = winrt::to_string(winrt::to_hstring(GuidHelper::CreateNewGuid())).substr(1, 36); - scoped_lock lock{ m_mutex }; + scoped_lock lock{m_mutex}; m_blobs.insert_or_assign(blobId, std::move(message)); return blobId; @@ -345,16 +342,17 @@ winrt::array_view MemoryBlobPersistor::ResolveMessage(string &&bl #pragma region BlobWebSocketModuleContentHandler - BlobWebSocketModuleContentHandler::BlobWebSocketModuleContentHandler(shared_ptr blobPersistor) noexcept - : m_blobPersistor{ blobPersistor } {} + BlobWebSocketModuleContentHandler::BlobWebSocketModuleContentHandler( + shared_ptr blobPersistor) noexcept + : m_blobPersistor{blobPersistor} {} #pragma region IWebSocketModuleContentHandler - void BlobWebSocketModuleContentHandler::ProcessMessage(string&& message, dynamic& params) /*override*/ { + void BlobWebSocketModuleContentHandler::ProcessMessage(string && message, dynamic & params) /*override*/ { params[dataKey] = std::move(message); } - void BlobWebSocketModuleContentHandler::ProcessMessage(vector&& message, dynamic& params) /*override*/ { + void BlobWebSocketModuleContentHandler::ProcessMessage(vector && message, dynamic & params) /*override*/ { auto blob = dynamic::object(); blob(offsetKey, 0); blob(sizeKey, message.size()); @@ -366,12 +364,12 @@ winrt::array_view MemoryBlobPersistor::ResolveMessage(string &&bl #pragma endregion IWebSocketModuleContentHandler void BlobWebSocketModuleContentHandler::Register(int64_t socketID) noexcept { - scoped_lock lock{ m_mutex }; + scoped_lock lock{m_mutex}; m_socketIds.insert(socketID); } void BlobWebSocketModuleContentHandler::Unregister(int64_t socketID) noexcept { - scoped_lock lock{ m_mutex }; + scoped_lock lock{m_mutex}; auto itr = m_socketIds.find(socketID); if (itr != m_socketIds.end()) @@ -383,17 +381,17 @@ winrt::array_view MemoryBlobPersistor::ResolveMessage(string &&bl #pragma region BlobModuleRequestBodyHandler BlobModuleRequestBodyHandler::BlobModuleRequestBodyHandler(shared_ptr blobPersistor) noexcept - : m_blobPersistor{ blobPersistor } {} + : m_blobPersistor{blobPersistor} {} #pragma region IRequestBodyHandler - bool BlobModuleRequestBodyHandler::Supports(dynamic& data) /*override*/ { + bool BlobModuleRequestBodyHandler::Supports(dynamic & data) /*override*/ { auto itr = data.find(blobKey); return itr != data.items().end() && !(*itr).second.empty(); } - dynamic BlobModuleRequestBodyHandler::ToRequestBody(dynamic& data, string& contentType) /*override*/ { + dynamic BlobModuleRequestBodyHandler::ToRequestBody(dynamic & data, string & contentType) /*override*/ { auto type = contentType; if (!data[typeKey].isNull() && !data[typeKey].asString().empty()) { type = data[typeKey].asString(); @@ -421,15 +419,15 @@ winrt::array_view MemoryBlobPersistor::ResolveMessage(string &&bl #pragma region BlobModuleResponseHandler BlobModuleResponseHandler::BlobModuleResponseHandler(shared_ptr blobPersistor) noexcept - : m_blobPersistor{ blobPersistor } {} + : m_blobPersistor{blobPersistor} {} #pragma region IResponseHandler - bool BlobModuleResponseHandler::Supports(string& responseType) /*override*/ { + bool BlobModuleResponseHandler::Supports(string & responseType) /*override*/ { return blobKey == responseType; } - dynamic BlobModuleResponseHandler::ToResponseData(vector&& content) /*override*/ { + dynamic BlobModuleResponseHandler::ToResponseData(vector && content) /*override*/ { auto blob = dynamic::object(); blob(offsetKey, 0); blob(sizeKey, content.size()); @@ -442,12 +440,12 @@ winrt::array_view MemoryBlobPersistor::ResolveMessage(string &&bl #pragma endregion BlobModuleResponseHandler - /*extern*/ const char* GetBlobModuleName() noexcept { + /*extern*/ const char *GetBlobModuleName() noexcept { return moduleName; } /*extern*/ std::unique_ptr CreateBlobModule( - IInspectable const& inspectableProperties) noexcept { + IInspectable const &inspectableProperties) noexcept { if (auto properties = inspectableProperties.try_as()) return std::make_unique(properties); diff --git a/vnext/Shared/Modules/BlobModule.h b/vnext/Shared/Modules/BlobModule.h index f27ab2d8595..76c5d46e5e7 100644 --- a/vnext/Shared/Modules/BlobModule.h +++ b/vnext/Shared/Modules/BlobModule.h @@ -23,131 +23,131 @@ namespace Microsoft::React { - class MemoryBlobPersistor final: public IBlobPersistor { - std::unordered_map> m_blobs; - std::mutex m_mutex; +class MemoryBlobPersistor final : public IBlobPersistor { + std::unordered_map> m_blobs; + std::mutex m_mutex; - public: + public: #pragma region IBlobPersistor winrt::array_view ResolveMessage(std::string &&blobId, int64_t offset, int64_t size) override; - void RemoveMessage(std::string&& blobId) noexcept override; + void RemoveMessage(std::string &&blobId) noexcept override; - void StoreMessage(std::vector&& message, std::string&& blobId) noexcept override; + void StoreMessage(std::vector &&message, std::string &&blobId) noexcept override; - std::string StoreMessage(std::vector&& message) noexcept override; + std::string StoreMessage(std::vector &&message) noexcept override; #pragma endregion IBlobPersistor - }; +}; - class BlobWebSocketModuleContentHandler final: public IWebSocketModuleContentHandler { - std::unordered_set m_socketIds; - std::mutex m_mutex; - std::shared_ptr m_blobPersistor; +class BlobWebSocketModuleContentHandler final : public IWebSocketModuleContentHandler { + std::unordered_set m_socketIds; + std::mutex m_mutex; + std::shared_ptr m_blobPersistor; - public: - BlobWebSocketModuleContentHandler(std::shared_ptr blobPersistor) noexcept; + public: + BlobWebSocketModuleContentHandler(std::shared_ptr blobPersistor) noexcept; #pragma region IWebSocketModuleContentHandler - void ProcessMessage(std::string&& message, folly::dynamic& params) override; + void ProcessMessage(std::string &&message, folly::dynamic ¶ms) override; - void ProcessMessage(std::vector&& message, folly::dynamic& params) override; + void ProcessMessage(std::vector &&message, folly::dynamic ¶ms) override; #pragma endregion IWebSocketModuleContentHandler - void Register(int64_t socketID) noexcept; + void Register(int64_t socketID) noexcept; - void Unregister(int64_t socketID) noexcept; - }; + void Unregister(int64_t socketID) noexcept; +}; - class BlobModuleRequestBodyHandler final: public IRequestBodyHandler { - std::shared_ptr m_blobPersistor; +class BlobModuleRequestBodyHandler final : public IRequestBodyHandler { + std::shared_ptr m_blobPersistor; - public: - BlobModuleRequestBodyHandler(std::shared_ptr blobPersistor) noexcept; + public: + BlobModuleRequestBodyHandler(std::shared_ptr blobPersistor) noexcept; #pragma region IRequestBodyHandler - bool Supports(folly::dynamic& data) override; + bool Supports(folly::dynamic &data) override; - folly::dynamic ToRequestBody(folly::dynamic& data, std::string& contentType) override; + folly::dynamic ToRequestBody(folly::dynamic &data, std::string &contentType) override; #pragma endregion IRequestBodyHandler - }; +}; - class BlobModuleResponseHandler final: public IResponseHandler { - std::shared_ptr m_blobPersistor; +class BlobModuleResponseHandler final : public IResponseHandler { + std::shared_ptr m_blobPersistor; - public: - BlobModuleResponseHandler(std::shared_ptr blobPersistor) noexcept; + public: + BlobModuleResponseHandler(std::shared_ptr blobPersistor) noexcept; #pragma region IResponseHandler - bool Supports(std::string& responseType) override; + bool Supports(std::string &responseType) override; - folly::dynamic ToResponseData(std::vector&& content) override; + folly::dynamic ToResponseData(std::vector &&content) override; #pragma endregion IResponseHandler +}; + +class BlobModule : public facebook::xplat::module::CxxModule { + std::shared_ptr m_blobPersistor; + std::shared_ptr m_contentHandler; + std::shared_ptr m_requestBodyHandler; + std::shared_ptr m_responseHandler; + + // Property bag high level reference. + winrt::Windows::Foundation::IInspectable m_inspectableProperties; + + public: + enum class MethodId { + AddNetworkingHandler = 0, + AddWebSocketHandler = 1, + RemoveWebSocketHandler = 2, + SendOverSocket = 3, + CreateFromParts = 4, + Release = 5, + SIZE = 6 }; - class BlobModule: public facebook::xplat::module::CxxModule{ - std::shared_ptr m_blobPersistor; - std::shared_ptr m_contentHandler; - std::shared_ptr m_requestBodyHandler; - std::shared_ptr m_responseHandler; - - // Property bag high level reference. - winrt::Windows::Foundation::IInspectable m_inspectableProperties; - - public: - enum class MethodId { - AddNetworkingHandler = 0, - AddWebSocketHandler = 1, - RemoveWebSocketHandler = 2, - SendOverSocket = 3, - CreateFromParts = 4, - Release = 5, - SIZE = 6 - }; - - BlobModule(winrt::Windows::Foundation::IInspectable const& inspectableProperties) noexcept; - - ~BlobModule() noexcept override; - - struct SharedState { - /// - /// Keeps a raw reference to the module object to lazily retrieve the React Instance as needed. - /// - CxxModule* Module{nullptr}; - }; + BlobModule(winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept; - #pragma region CxxModule + ~BlobModule() noexcept override; + struct SharedState { /// - /// + /// Keeps a raw reference to the module object to lazily retrieve the React Instance as needed. /// - std::string getName() override; + CxxModule *Module{nullptr}; + }; - /// - /// - /// - std::map getConstants() override; +#pragma region CxxModule - /// - /// - /// - /// See See react-native/Libraries/WebSocket/WebSocket.js - std::vector getMethods() override; + /// + /// + /// + std::string getName() override; - #pragma endregion CxxModule + /// + /// + /// + std::map getConstants() override; - private: - /// - /// Keeps members that can be accessed threads other than this module's owner accessible. - /// - std::shared_ptr m_sharedState; - }; + /// + /// + /// + /// See See react-native/Libraries/WebSocket/WebSocket.js + std::vector getMethods() override; + +#pragma endregion CxxModule + + private: + /// + /// Keeps members that can be accessed threads other than this module's owner accessible. + /// + std::shared_ptr m_sharedState; +}; } // namespace Microsoft::React diff --git a/vnext/Shared/Modules/FileReaderModule.cpp b/vnext/Shared/Modules/FileReaderModule.cpp index 66c2ac79243..4da7f20902e 100644 --- a/vnext/Shared/Modules/FileReaderModule.cpp +++ b/vnext/Shared/Modules/FileReaderModule.cpp @@ -28,107 +28,104 @@ using winrt::Microsoft::ReactNative::ReactPropertyId; using winrt::Windows::Foundation::IInspectable; namespace { - constexpr char moduleName[] = "FileReaderModule"; +constexpr char moduleName[] = "FileReaderModule"; } // namespace namespace Microsoft::React { #pragma region FileReaderModule - FileReaderModule::FileReaderModule(weak_ptr weakBlobPersistor) noexcept - : m_weakBlobPersistor{ weakBlobPersistor } {} +FileReaderModule::FileReaderModule(weak_ptr weakBlobPersistor) noexcept + : m_weakBlobPersistor{weakBlobPersistor} {} - FileReaderModule::~FileReaderModule() noexcept /*override*/ - {} +FileReaderModule::~FileReaderModule() noexcept /*override*/ +{} #pragma region CxxModule - string FileReaderModule::getName() { - return moduleName; - } - - std::map FileReaderModule::getConstants() { - return {}; - } - - std::vector FileReaderModule::getMethods() { - return { - {/// - /// - /// Array of arguments passed from the JavaScript layer. - /// [0] - dynamic blob object { blobId, offset, size[, type] } - /// - /// - "readAsDataURL", - [blobPersistor = m_weakBlobPersistor.lock()](dynamic args, Callback resolve, Callback reject) { - if (!blobPersistor) { - return reject({"Could not find Blob persistor"}); - } - - auto blob = jsArgAsObject(args, 0); - - auto blobId = blob["blobId"].asString(); - auto offset = blob["offset"].asInt(); - auto size = blob["size"].asInt(); - - winrt::array_view bytes; - try { - bytes = blobPersistor->ResolveMessage(std::move(blobId), offset, size); - } - catch (const std::exception& e) { - return reject({e.what()}); +string FileReaderModule::getName() { + return moduleName; } -auto result = string{"data:"}; -auto typeItr = blob.find("type"); -if (typeItr == blob.items().end()) { - result += "application/octet-stream"; -} -else { -result += (*typeItr).second.asString(); -} -result += ";base64,"; - -// https://www.boost.org/doc/libs/1_76_0/libs/serialization/doc/dataflow.html -using namespace boost::archive::iterators; -typedef base64_from_binary> encode_base64; -std::ostringstream oss; -std::copy(encode_base64(bytes.cbegin()), encode_base64(bytes.cend()), ostream_iterator(oss)); -result += oss.str(); - -resolve({std::move(result)}); -}}, -{/// - /// - /// Array of arguments passed from the JavaScript layer. - /// [0] - dynamic blob object { blobId, offset, size } - /// [1] - string encoding - /// - /// - "readAsText", - [blobPersistor = m_weakBlobPersistor.lock()](dynamic args, Callback resolve, Callback reject) { - if (!blobPersistor) { - return reject({"Could not find Blob persistor"}); - } - - auto blob = jsArgAsObject(args, 0); - auto encoding = jsArgAsString(args, 1); // Default: "UTF-8" - - auto blobId = blob["blobId"].asString(); - auto offset = blob["offset"].asInt(); - auto size = blob["size"].asInt(); - - winrt::array_view bytes; - try { - bytes = blobPersistor->ResolveMessage(std::move(blobId), offset, size); - } -catch (const std::exception& e) { -return reject({e.what()}); +std::map FileReaderModule::getConstants() { + return {}; } -// #9982 - Handle non-UTF8 encodings -// See https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/charset/Charset.html -auto result = string{bytes.cbegin(), bytes.cend()}; +std::vector FileReaderModule::getMethods() { + return { + {/// + /// + /// Array of arguments passed from the JavaScript layer. + /// [0] - dynamic blob object { blobId, offset, size[, type] } + /// + /// + "readAsDataURL", + [blobPersistor = m_weakBlobPersistor.lock()](dynamic args, Callback resolve, Callback reject) { + if (!blobPersistor) { + return reject({"Could not find Blob persistor"}); + } + + auto blob = jsArgAsObject(args, 0); + + auto blobId = blob["blobId"].asString(); + auto offset = blob["offset"].asInt(); + auto size = blob["size"].asInt(); + + winrt::array_view bytes; + try { + bytes = blobPersistor->ResolveMessage(std::move(blobId), offset, size); + } catch (const std::exception &e) { + return reject({e.what()}); + } + + auto result = string{"data:"}; + auto typeItr = blob.find("type"); + if (typeItr == blob.items().end()) { + result += "application/octet-stream"; + } else { + result += (*typeItr).second.asString(); + } + result += ";base64,"; + + // https://www.boost.org/doc/libs/1_76_0/libs/serialization/doc/dataflow.html + using namespace boost::archive::iterators; + typedef base64_from_binary> encode_base64; + std::ostringstream oss; + std::copy(encode_base64(bytes.cbegin()), encode_base64(bytes.cend()), ostream_iterator(oss)); + result += oss.str(); + + resolve({std::move(result)}); + }}, + {/// + /// + /// Array of arguments passed from the JavaScript layer. + /// [0] - dynamic blob object { blobId, offset, size } + /// [1] - string encoding + /// + /// + "readAsText", + [blobPersistor = m_weakBlobPersistor.lock()](dynamic args, Callback resolve, Callback reject) { + if (!blobPersistor) { + return reject({"Could not find Blob persistor"}); + } + + auto blob = jsArgAsObject(args, 0); + auto encoding = jsArgAsString(args, 1); // Default: "UTF-8" + + auto blobId = blob["blobId"].asString(); + auto offset = blob["offset"].asInt(); + auto size = blob["size"].asInt(); + + winrt::array_view bytes; + try { + bytes = blobPersistor->ResolveMessage(std::move(blobId), offset, size); + } catch (const std::exception &e) { + return reject({e.what()}); + } + + // #9982 - Handle non-UTF8 encodings + // See https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/charset/Charset.html + auto result = string{bytes.cbegin(), bytes.cend()}; auto blob = jsArgAsObject(args, 0); @@ -200,22 +197,22 @@ auto result = string{bytes.cbegin(), bytes.cend()}; #pragma endregion FileReaderModule - /*extern*/ const char* GetFileReaderModuleName() noexcept { - return moduleName; - } - - /*extern*/ std::unique_ptr CreateFileReaderModule( - IInspectable const& inspectableProperties) noexcept { - auto propId = ReactPropertyId>>{ L"Blob.Persistor" }; - auto propBag = ReactPropertyBag{ inspectableProperties.try_as() }; +/*extern*/ const char *GetFileReaderModuleName() noexcept { + return moduleName; +} - if (auto prop = propBag.Get(propId)) { - auto weakBlobPersistor = prop.Value(); +/*extern*/ std::unique_ptr CreateFileReaderModule( + IInspectable const &inspectableProperties) noexcept { + auto propId = ReactPropertyId>>{L"Blob.Persistor"}; + auto propBag = ReactPropertyBag{inspectableProperties.try_as()}; - return std::make_unique(weakBlobPersistor); - } + if (auto prop = propBag.Get(propId)) { + auto weakBlobPersistor = prop.Value(); - return nullptr; + return std::make_unique(weakBlobPersistor); } + return nullptr; +} + } // namespace Microsoft::React diff --git a/vnext/Shared/Modules/HttpModule.cpp b/vnext/Shared/Modules/HttpModule.cpp index cde9df4b54d..326f677d843 100644 --- a/vnext/Shared/Modules/HttpModule.cpp +++ b/vnext/Shared/Modules/HttpModule.cpp @@ -25,10 +25,10 @@ using winrt::Windows::Foundation::IInspectable; namespace { - using Microsoft::React::Modules::SendEvent; - using Microsoft::React::Networking::IHttpResource; +using Microsoft::React::Modules::SendEvent; +using Microsoft::React::Networking::IHttpResource; - constexpr char moduleName[] = "Networking"; +constexpr char moduleName[] = "Networking"; // React event names constexpr char completedResponse[] = "didCompleteNetworkResponse"; @@ -38,19 +38,19 @@ constexpr char receivedIncrementalData[] = "didReceiveNetworkIncrementalData"; constexpr char receivedDataProgress[] = "didReceiveNetworkDataProgress"; constexpr char receivedData[] = "didReceiveNetworkData"; - static void SetUpHttpResource( +static void SetUpHttpResource( shared_ptr resource, weak_ptr weakReactInstance, - IInspectable& inspectableProperties) { - resource->SetOnRequestSuccess([weakReactInstance](int64_t requestId) { - auto args = dynamic::array(requestId); + IInspectable &inspectableProperties) { + resource->SetOnRequestSuccess([weakReactInstance](int64_t requestId) { + auto args = dynamic::array(requestId); SendEvent(weakReactInstance, completedResponse, std::move(args)); - }); + }); - resource->SetOnResponse([weakReactInstance](int64_t requestId, IHttpResource::Response&& response) { - dynamic headers = dynamic::object(); - for (auto& header : response.Headers) { + resource->SetOnResponse([weakReactInstance](int64_t requestId, IHttpResource::Response &&response) { + dynamic headers = dynamic::object(); + for (auto &header : response.Headers) { headers[header.first] = header.second; } @@ -58,18 +58,18 @@ constexpr char receivedData[] = "didReceiveNetworkData"; dynamic args = dynamic::array(requestId, response.StatusCode, headers, response.Url); SendEvent(weakReactInstance, receivedResponse, std::move(args)); - }); + }); resource->SetOnData([weakReactInstance](int64_t requestId, string &&responseData) { SendEvent(weakReactInstance, receivedData, dynamic::array(requestId, std::move(responseData))); }); - // Explicitly declaring function type to avoid type inference ambiguity. - std::function onDataDynamic = [weakReactInstance]( - int64_t requestId, dynamic&& responseData) { - SendEvent(weakReactInstance, receivedData, dynamic::array(requestId, std::move(responseData))); - }; - resource->SetOnData(std::move(onDataDynamic)); + // Explicitly declaring function type to avoid type inference ambiguity. + std::function onDataDynamic = [weakReactInstance]( + int64_t requestId, dynamic &&responseData) { + SendEvent(weakReactInstance, receivedData, dynamic::array(requestId, std::move(responseData))); + }; + resource->SetOnData(std::move(onDataDynamic)); resource->SetOnIncrementalData( [weakReactInstance](int64_t requestId, string &&responseData, int64_t progress, int64_t total) { @@ -94,35 +94,35 @@ constexpr char receivedData[] = "didReceiveNetworkData"; } SendEvent(weakReactInstance, completedResponse, std::move(args)); - }); - } + }); +} } // namespace namespace Microsoft::React { - HttpModule::HttpModule(IInspectable const& inspectableProperties) noexcept - : m_holder{ std::make_shared() }, - m_inspectableProperties{ inspectableProperties }, - m_resource{ IHttpResource::Make(inspectableProperties) } { - m_holder->Module = this; - } +HttpModule::HttpModule(IInspectable const &inspectableProperties) noexcept + : m_holder{std::make_shared()}, + m_inspectableProperties{inspectableProperties}, + m_resource{IHttpResource::Make(inspectableProperties)} { + m_holder->Module = this; +} - HttpModule::~HttpModule() noexcept /*override*/ { - m_holder->Module = nullptr; - } +HttpModule::~HttpModule() noexcept /*override*/ { + m_holder->Module = nullptr; +} #pragma region CxxModule - string HttpModule::getName() /*override*/ { - return moduleName; - } +string HttpModule::getName() /*override*/ { + return moduleName; +} - std::map HttpModule::getConstants() { - return {}; - } +std::map HttpModule::getConstants() { + return {}; +} - // clang-format off +// clang-format off std::vector HttpModule::getMethods() { return @@ -207,12 +207,12 @@ namespace Microsoft::React { } }; } - // clang-format on +// clang-format on #pragma endregion CxxModule - /*extern*/ const char* GetHttpModuleName() noexcept { - return moduleName; - } +/*extern*/ const char *GetHttpModuleName() noexcept { + return moduleName; +} } // namespace Microsoft::React diff --git a/vnext/Shared/Modules/IBlobPersistor.h b/vnext/Shared/Modules/IBlobPersistor.h index d6518a6c763..e4aaf5017e0 100644 --- a/vnext/Shared/Modules/IBlobPersistor.h +++ b/vnext/Shared/Modules/IBlobPersistor.h @@ -12,19 +12,19 @@ namespace Microsoft::React { - 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; +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 void RemoveMessage(std::string&& blobId) noexcept = 0; + virtual void RemoveMessage(std::string &&blobId) noexcept = 0; - virtual void StoreMessage(std::vector&& message, std::string&& blobId) noexcept = 0; + virtual void StoreMessage(std::vector &&message, std::string &&blobId) noexcept = 0; - virtual std::string StoreMessage(std::vector&& message) noexcept = 0; - }; + virtual std::string StoreMessage(std::vector &&message) noexcept = 0; +}; } // namespace Microsoft::React diff --git a/vnext/Shared/Networking/IHttpResource.h b/vnext/Shared/Networking/IHttpResource.h index d79ffce6e29..c590e91aac7 100644 --- a/vnext/Shared/Networking/IHttpResource.h +++ b/vnext/Shared/Networking/IHttpResource.h @@ -17,79 +17,79 @@ namespace Microsoft::React::Networking { - struct IHttpResource { - typedef std::unordered_map Headers; - - // TODO: Implement Form data - struct BodyData { - enum class Type: size_t { Empty, String, Base64, Uri, Form } Type = Type::Empty; - std::string Data; - }; - - struct Response { - int64_t StatusCode; - std::string Url; - Headers Headers; - }; - - static std::shared_ptr Make() noexcept; - - static std::shared_ptr Make( - winrt::Windows::Foundation::IInspectable const& inspectableProperties) noexcept; - - virtual ~IHttpResource() noexcept {} - - /// - /// Initiates an HTTP request. - /// - /// - /// HTTP verb to send in de request. - /// GET | POST | PUT | DELETE | OPTIONS - /// - /// - /// Server/service remote endpoint to send the request to. - /// - /// - /// Request unique identifier. - /// - /// - /// HTTP request header map. - /// - /// - /// Dynamic map containing request payload. - /// The payload may be an empty request body or one of the following: - /// "string" - UTF-8 string payload - /// "base64" - Base64-encoded data string - /// "uri" - URI data reference - /// "form" - Form-encoded data - /// - /// - /// text | binary | blob - /// - /// - /// Response body to be retrieved in several iterations. - /// - /// - /// Request timeout in miliseconds. - /// Note: A value of 0 means no timeout. The resource will await the response indefinitely. - /// - /// - /// Allow including credentials in request. - /// - virtual void SendRequest( - std::string&& method, - std::string&& url, +struct IHttpResource { + typedef std::unordered_map Headers; + + // TODO: Implement Form data + struct BodyData { + enum class Type : size_t { Empty, String, Base64, Uri, Form } Type = Type::Empty; + std::string Data; + }; + + struct Response { + int64_t StatusCode; + std::string Url; + Headers Headers; + }; + + static std::shared_ptr Make() noexcept; + + static std::shared_ptr Make( + winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept; + + virtual ~IHttpResource() noexcept {} + + /// + /// Initiates an HTTP request. + /// + /// + /// HTTP verb to send in de request. + /// GET | POST | PUT | DELETE | OPTIONS + /// + /// + /// Server/service remote endpoint to send the request to. + /// + /// + /// Request unique identifier. + /// + /// + /// HTTP request header map. + /// + /// + /// Dynamic map containing request payload. + /// The payload may be an empty request body or one of the following: + /// "string" - UTF-8 string payload + /// "base64" - Base64-encoded data string + /// "uri" - URI data reference + /// "form" - Form-encoded data + /// + /// + /// text | binary | blob + /// + /// + /// Response body to be retrieved in several iterations. + /// + /// + /// Request timeout in miliseconds. + /// Note: A value of 0 means no timeout. The resource will await the response indefinitely. + /// + /// + /// Allow including credentials in request. + /// + virtual void SendRequest( + std::string &&method, + std::string &&url, int64_t requestId, - Headers&& headers, - folly::dynamic&& data, - std::string&& responseType, + Headers &&headers, + folly::dynamic &&data, + std::string &&responseType, bool useIncrementalUpdates, int64_t timeout, bool withCredentials, - std::function&& callback) noexcept = 0; - virtual void AbortRequest(int64_t requestId) noexcept = 0; + std::function &&callback) noexcept = 0; + virtual void AbortRequest(int64_t requestId) noexcept = 0; - virtual void ClearCookies() noexcept = 0; + virtual void ClearCookies() noexcept = 0; /// /// Sets a function to be invoked when a request has been successfully responded. diff --git a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp index e231876036a..36feb1e9ee6 100644 --- a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp +++ b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp @@ -39,20 +39,20 @@ namespace Microsoft::React::Networking { #pragma region ConstWcharComparer - bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t* a, const wchar_t* b) const { - return _wcsicmp(a, b) < 0; - } +bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, const wchar_t *b) const { + return _wcsicmp(a, b) < 0; +} #pragma endregion ConstWcharComparer - // https://fetch.spec.whatwg.org/#forbidden-method - /*static*/ set OriginPolicyHttpFilter::s_forbiddenMethods = - { L"CONNECT", L"TRACE", L"TRACK" }; +// https://fetch.spec.whatwg.org/#forbidden-method +/*static*/ set OriginPolicyHttpFilter::s_forbiddenMethods = + {L"CONNECT", L"TRACE", L"TRACK"}; - /*static*/ set - OriginPolicyHttpFilter::s_simpleCorsMethods = { L"GET", L"HEAD", L"POST" }; +/*static*/ set + OriginPolicyHttpFilter::s_simpleCorsMethods = {L"GET", L"HEAD", L"POST"}; - /*static*/ set +/*static*/ set OriginPolicyHttpFilter::s_simpleCorsRequestHeaderNames = { L"Accept", L"Accept-Language", @@ -62,21 +62,21 @@ namespace Microsoft::React::Networking { L"Downlink", L"Save-Data", L"Viewport-Width", - L"Width" }; + L"Width"}; - /*static*/ set +/*static*/ set OriginPolicyHttpFilter::s_simpleCorsResponseHeaderNames = - { L"Cache-Control", L"Content-Language", L"Content-Type", L"Expires", L"Last-Modified", L"Pragma" }; + {L"Cache-Control", L"Content-Language", L"Content-Type", L"Expires", L"Last-Modified", L"Pragma"}; - /*static*/ set +/*static*/ set OriginPolicyHttpFilter::s_simpleCorsContentTypeValues = { L"application/x-www-form-urlencoded", L"multipart/form-data", - L"text/plain" }; + L"text/plain"}; - // https://fetch.spec.whatwg.org/#forbidden-header-name - // Chromium still bans "User-Agent" due to https://crbug.com/571722 - /*static*/ set +// https://fetch.spec.whatwg.org/#forbidden-header-name +// Chromium still bans "User-Agent" due to https://crbug.com/571722 +/*static*/ set OriginPolicyHttpFilter::s_corsForbiddenRequestHeaderNames = { L"Accept-Charset", L"Accept-Encoding", @@ -97,110 +97,109 @@ namespace Microsoft::React::Networking { L"Trailer", L"Transfer-Encoding", L"Upgrade", - L"Via" }; + L"Via"}; - /*static*/ set +/*static*/ set OriginPolicyHttpFilter::s_cookieSettingResponseHeaders = { L"Set-Cookie", L"Set-Cookie2", // Deprecated by the spec, but probably still used - }; +}; + +/*static*/ set + OriginPolicyHttpFilter::s_corsForbiddenRequestHeaderNamePrefixes = {L"Proxy-", L"Sec-"}; + +/*static*/ Uri OriginPolicyHttpFilter::s_origin{nullptr}; - /*static*/ set - OriginPolicyHttpFilter::s_corsForbiddenRequestHeaderNamePrefixes = { L"Proxy-", L"Sec-" }; +/*static*/ void OriginPolicyHttpFilter::SetStaticOrigin(std::string &&url) { + if (!url.empty()) + s_origin = Uri{to_hstring(url)}; +} - /*static*/ Uri OriginPolicyHttpFilter::s_origin{ nullptr }; +/*static*/ bool OriginPolicyHttpFilter::IsSameOrigin(Uri const &u1, Uri const &u2) noexcept { + return (u1 && u2) && u1.SchemeName() == u2.SchemeName() && u1.Host() == u2.Host() && u1.Port() == u2.Port(); +} - /*static*/ void OriginPolicyHttpFilter::SetStaticOrigin(std::string&& url) { - if (!url.empty()) - s_origin = Uri{ to_hstring(url) }; +/*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const &request) noexcept { + // Ensure header is in Simple CORS allowlist + for (const auto &header : request.Headers()) { + if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend()) + return false; + + // Ensure Content-Type value is in Simple CORS allowlist, if present + if (boost::iequals(header.Key(), L"Content-Type")) { + if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) != s_simpleCorsContentTypeValues.cend()) + return false; + } } - /*static*/ bool OriginPolicyHttpFilter::IsSameOrigin(Uri const& u1, Uri const& u2) noexcept { + /*static*/ bool OriginPolicyHttpFilter::IsSameOrigin(Uri const &u1, Uri const &u2) noexcept { return (u1 && u2) && u1.SchemeName() == u2.SchemeName() && u1.Host() == u2.Host() && u1.Port() == u2.Port(); } - /*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const& request) noexcept { - // Ensure header is in Simple CORS allowlist - for (const auto& header : request.Headers()) { + /*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const &request) noexcept { + // Ensure header is in Simple CORS white list + for (const auto &header : request.Headers()) { if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend()) return false; - // Ensure Content-Type value is in Simple CORS allowlist, if present + // Ensure Content-Type value is in Simple CORS white list, if present if (boost::iequals(header.Key(), L"Content-Type")) { if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) != s_simpleCorsContentTypeValues.cend()) return false; } } - /*static*/ bool OriginPolicyHttpFilter::IsSameOrigin(Uri const& u1, Uri const& u2) noexcept { - return (u1 && u2) && u1.SchemeName() == u2.SchemeName() && u1.Host() == u2.Host() && u1.Port() == u2.Port(); - } - - /*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const& request) noexcept { - // Ensure header is in Simple CORS white list - for (const auto& header : request.Headers()) { - if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend()) + // WinRT separates request headers from request content headers + if (auto content = request.Content()) { + for (const auto &header : content.Headers()) { + // WinRT automatically appends non-whitelisted header Content-Length when Content-Type is set. Skip it. + if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend() && + !boost::iequals(header.Key(), "Content-Length")) return false; // Ensure Content-Type value is in Simple CORS white list, if present if (boost::iequals(header.Key(), L"Content-Type")) { - if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) != s_simpleCorsContentTypeValues.cend()) + if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) == s_simpleCorsContentTypeValues.cend()) return false; } } + } - // WinRT separates request headers from request content headers - if (auto content = request.Content()) { - for (const auto& header : content.Headers()) { - // WinRT automatically appends non-whitelisted header Content-Length when Content-Type is set. Skip it. - if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend() && - !boost::iequals(header.Key(), "Content-Length")) - return false; - - // Ensure Content-Type value is in Simple CORS white list, if present - if (boost::iequals(header.Key(), L"Content-Type")) { - if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) == s_simpleCorsContentTypeValues.cend()) - return false; - } - } - } + // Ensure method is in Simple CORS white list + return s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); + } - // Ensure method is in Simple CORS white list - return s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); - } + /*static*/ Uri OriginPolicyHttpFilter::GetOrigin(Uri const &uri) noexcept { + return Uri{uri.SchemeName() + L"://" + uri.Host() + L":" + to_hstring(uri.Port())}; + } - /*static*/ Uri OriginPolicyHttpFilter::GetOrigin(Uri const& uri) noexcept { - return Uri{ uri.SchemeName() + L"://" + uri.Host() + L":" + to_hstring(uri.Port()) }; - } + /*static*/ bool OriginPolicyHttpFilter::AreSafeRequestHeaders( + winrt::Windows::Web::Http::Headers::HttpRequestHeaderCollection const &headers) noexcept { + for (const auto &header : headers) { + if (s_corsForbiddenRequestHeaderNames.find(header.Key().c_str()) != s_corsForbiddenRequestHeaderNames.cend()) + return false; - /*static*/ bool OriginPolicyHttpFilter::AreSafeRequestHeaders( - winrt::Windows::Web::Http::Headers::HttpRequestHeaderCollection const& headers) noexcept { - for (const auto& header : headers) { - if (s_corsForbiddenRequestHeaderNames.find(header.Key().c_str()) != s_corsForbiddenRequestHeaderNames.cend()) + for (const auto &prefix : s_corsForbiddenRequestHeaderNamePrefixes) { + if (boost::istarts_with(header.Key(), prefix)) return false; - - for (const auto& prefix : s_corsForbiddenRequestHeaderNamePrefixes) { - if (boost::istarts_with(header.Key(), prefix)) - return false; - } } - - return true; } - // See https://fetch.spec.whatwg.org/#cors-safelisted-request-header - // "A CORS-safelisted header is a header whose name is either one of 'Accept', 'Accept-Language', and - // 'Content-Language', or whose name is 'Content-Type' and value is one of 'application/x-www-form-urlencoded', - // 'multipart/form-data', and 'text/plain' - /*static*/ bool OriginPolicyHttpFilter::IsCorsSafelistedRequestHeader( - hstring const& name, - hstring const& value) noexcept { - // 1. If value's length is greater than 128, then return false. - if (value.size() > 128) - return false; + return true; + } - // 2. Byte-lowercase name and switch on the result: - static const wchar_t* const safeHeaderNames[] = { + // See https://fetch.spec.whatwg.org/#cors-safelisted-request-header + // "A CORS-safelisted header is a header whose name is either one of 'Accept', 'Accept-Language', and + // 'Content-Language', or whose name is 'Content-Type' and value is one of 'application/x-www-form-urlencoded', + // 'multipart/form-data', and 'text/plain' + /*static*/ bool OriginPolicyHttpFilter::IsCorsSafelistedRequestHeader( + hstring const &name, hstring const &value) noexcept { + // 1. If value's length is greater than 128, then return false. + if (value.size() > 128) + return false; + + // 2. Byte-lowercase name and switch on the result: + static const wchar_t *const safeHeaderNames[] = { // The following four headers are from the CORS spec L"accept", L"accept-language", @@ -227,195 +226,188 @@ namespace Microsoft::React::Networking { L"sec-ch-ua-mobile", L"sec-ch-ua-full-version", L"sec-ch-ua-platform-version", - }; + }; + + auto nameLower = boost::to_lower_copy(wstring{name.c_str()}); + if (std::find(std::cbegin(safeHeaderNames), std::cend(safeHeaderNames), nameLower) == std::end(safeHeaderNames)) + return false; + + double doubleHolder; + if (nameLower == L"device-memory" || nameLower == L"dpr") + return boost::conversion::try_lexical_convert(value.c_str(), doubleHolder); + + int intHolder; + if (nameLower == L"width" || nameLower == L"viewport-width") + return boost::conversion::try_lexical_convert(value.c_str(), intHolder); + + auto valueLower = boost::to_lower_copy(wstring{value.c_str()}); + if (nameLower == L"save-data") + return valueLower == L"on"; + + if (nameLower == L"accept") + return !std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte); + + if (nameLower == L"accept-language" || nameLower == L"content-language") { + return std::all_of(valueLower.cbegin(), valueLower.cend(), [](wchar_t c) noexcept { + return (0x30 <= c && c <= 0x39) || // 0-9 + (0x41 <= c && c <= 0x5A) || // A-Z + (0x61 <= c && c <= 0x7A) || // a-z + c == 0x20 || c == 0x2A || c == 0x2C || c == 0x2D || c == 0x2E || c == 0x3B || c == 0x3D; // *,-.;= + }); + } - auto nameLower = boost::to_lower_copy(wstring{ name.c_str() }); - if (std::find(std::cbegin(safeHeaderNames), std::cend(safeHeaderNames), nameLower) == std::end(safeHeaderNames)) + if (nameLower == L"content-type") { + if (std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte)) return false; - double doubleHolder; - if (nameLower == L"device-memory" || nameLower == L"dpr") - return boost::conversion::try_lexical_convert(value.c_str(), doubleHolder); - - int intHolder; - if (nameLower == L"width" || nameLower == L"viewport-width") - return boost::conversion::try_lexical_convert(value.c_str(), intHolder); - - auto valueLower = boost::to_lower_copy(wstring{ value.c_str() }); - if (nameLower == L"save-data") - return valueLower == L"on"; + // https://mimesniff.spec.whatwg.org/#parse-a-mime-type + HttpMediaTypeHeaderValue mediaType{nullptr}; + if (HttpMediaTypeHeaderValue::TryParse(valueLower, mediaType)) + return mediaType.ToString() == L"application/x-www-form-urlencoded" || + mediaType.ToString() == L"multipart/form-data" || mediaType.ToString() == L"text/plain"; - if (nameLower == L"accept") - return !std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte); + return false; + } - if (nameLower == L"accept-language" || nameLower == L"content-language") { - return std::all_of(valueLower.cbegin(), valueLower.cend(), [](wchar_t c) noexcept { - return (0x30 <= c && c <= 0x39) || // 0-9 - (0x41 <= c && c <= 0x5A) || // A-Z - (0x61 <= c && c <= 0x7A) || // a-z - c == 0x20 || c == 0x2A || c == 0x2C || c == 0x2D || c == 0x2E || c == 0x3B || c == 0x3D; // *,-.;= - }); - } + return true; + } - if (nameLower == L"content-type") { - if (std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte)) - return false; + // https://fetch.spec.whatwg.org/#cors-unsafe-request-header-byte + /*static*/ bool OriginPolicyHttpFilter::IsCorsUnsafeRequestHeaderByte(wchar_t c) noexcept { + return (c < 0x20 && c != 0x09) || c == 0x22 || c == 0x28 || c == 0x29 || c == 0x3a || c == 0x3c || c == 0x3e || + c == 0x3f || c == 0x40 || c == 0x5b || c == 0x5c || c == 0x5d || c == 0x7b || c == 0x7d || c == 0x7f; + } - // https://mimesniff.spec.whatwg.org/#parse-a-mime-type - HttpMediaTypeHeaderValue mediaType{ nullptr }; - if (HttpMediaTypeHeaderValue::TryParse(valueLower, mediaType)) - return mediaType.ToString() == L"application/x-www-form-urlencoded" || - mediaType.ToString() == L"multipart/form-data" || mediaType.ToString() == L"text/plain"; + /*static*/ set OriginPolicyHttpFilter::CorsUnsafeNotForbiddenRequestHeaderNames( + HttpRequestHeaderCollection const &headers) noexcept { + constexpr size_t maxSafelistValueSize = 1024; + size_t safelistValueSize = 0; + std::vector potentiallyUnsafeNames; + set result; + for (const auto &header : headers) { + const auto headerName = header.Key().c_str(); + + // If header is not safe + if (boost::istarts_with(headerName, L"Proxy-") || boost::istarts_with(headerName, L"Sec-") || + s_corsForbiddenRequestHeaderNames.find(headerName) != s_corsForbiddenRequestHeaderNames.cend()) + continue; - return false; + if (!IsCorsSafelistedRequestHeader(header.Key(), header.Value())) { + result.emplace(header.Key().c_str()); + } else { + potentiallyUnsafeNames.emplace_back(std::wstring_view{header.Key()}.data()); + safelistValueSize += header.Value().size(); } - - return true; } - // https://fetch.spec.whatwg.org/#cors-unsafe-request-header-byte - /*static*/ bool OriginPolicyHttpFilter::IsCorsUnsafeRequestHeaderByte(wchar_t c) noexcept { - return (c < 0x20 && c != 0x09) || c == 0x22 || c == 0x28 || c == 0x29 || c == 0x3a || c == 0x3c || c == 0x3e || - c == 0x3f || c == 0x40 || c == 0x5b || c == 0x5c || c == 0x5d || c == 0x7b || c == 0x7d || c == 0x7f; - } + if (safelistValueSize > maxSafelistValueSize) + result.insert(potentiallyUnsafeNames.begin(), potentiallyUnsafeNames.end()); - /*static*/ set OriginPolicyHttpFilter::CorsUnsafeNotForbiddenRequestHeaderNames( - HttpRequestHeaderCollection const& headers) noexcept { - constexpr size_t maxSafelistValueSize = 1024; - size_t safelistValueSize = 0; - std::vector potentiallyUnsafeNames; - set result; - for (const auto& header : headers) { - const auto headerName = header.Key().c_str(); - - // If header is not safe - if (boost::istarts_with(headerName, L"Proxy-") || boost::istarts_with(headerName, L"Sec-") || - s_corsForbiddenRequestHeaderNames.find(headerName) != s_corsForbiddenRequestHeaderNames.cend()) - continue; + return result; + } - if (!IsCorsSafelistedRequestHeader(header.Key(), header.Value())) { - result.emplace(header.Key().c_str()); - } - else { - potentiallyUnsafeNames.emplace_back(std::wstring_view{ header.Key() }.data()); - safelistValueSize += header.Value().size(); - } + /*static*/ OriginPolicyHttpFilter::AccessControlValues OriginPolicyHttpFilter::ExtractAccessControlValues( + winrt::Windows::Foundation::Collections::IMap const &headers) { + using std::wregex; + using std::wsregex_token_iterator; + + // https://tools.ietf.org/html/rfc2616#section-4.2 + wregex rgx{L"\\s*,\\s*"}; + AccessControlValues result; + + auto ciStrCmp = [](const wstring &a, const wstring &b) { return _wcsicmp(a.c_str(), b.c_str()) < 0; }; + set allowedHeaders{ciStrCmp}; + set allowedMethods{ciStrCmp}; + set exposedHeaders{ciStrCmp}; + + for (const auto &header : headers) { + if (boost::iequals(header.Key(), L"Access-Control-Allow-Headers")) { + auto value = wstring{header.Value().c_str()}; + + // TODO: Avoid redundant comparison. + auto parsed = set{ + wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp}; + allowedHeaders.insert(parsed.cbegin(), parsed.cend()); + result.AllowedHeaders.insert(allowedHeaders.cbegin(), allowedHeaders.cend()); + } else if (boost::iequals(header.Key(), L"Access-Control-Allow-Methods")) { + auto value = wstring{header.Value().c_str()}; + + // TODO: Avoid redundant comparison. + auto parsed = set{ + wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp}; + allowedMethods.insert(parsed.cbegin(), parsed.cend()); + result.AllowedMethods.insert(allowedMethods.cbegin(), allowedMethods.cend()); + } else if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { + result.AllowedOrigin = header.Value(); + } else if (boost::iequals(header.Key(), L"Access-Control-Expose-Headers")) { + auto value = wstring{header.Value().c_str()}; + + // TODO: Avoid redundant comparison. + auto parsed = set{ + wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp}; + exposedHeaders.insert(parsed.cbegin(), parsed.cend()); + result.ExposedHeaders.insert(exposedHeaders.cbegin(), exposedHeaders.cend()); + } else if (boost::iequals(header.Key(), L"Access-Control-Allow-Credentials")) { + result.AllowedCredentials = header.Value(); + } else if (boost::iequals(header.Key(), L"Access-Control-Max-Age")) { + result.MaxAge = _wtoi(header.Value().c_str()); } + } - if (safelistValueSize > maxSafelistValueSize) - result.insert(potentiallyUnsafeNames.begin(), potentiallyUnsafeNames.end()); + return result; + } // ExtractAccessControlValues - return result; - } + /*static*/ void OriginPolicyHttpFilter::RemoveHttpOnlyCookiesFromResponseHeaders( + HttpResponseMessage const &response, bool removeAll) { + // Example: "Set-Cookie", L"id=a3fWa; Expires=Wed, 21 Oct 2020 07:28:00 GMT; HttpOnly" + std::queue httpOnlyCookies; + for (const auto &header : response.Headers()) { + if (s_cookieSettingResponseHeaders.find(header.Key().c_str()) == s_cookieSettingResponseHeaders.cend()) + continue; - /*static*/ OriginPolicyHttpFilter::AccessControlValues OriginPolicyHttpFilter::ExtractAccessControlValues( - winrt::Windows::Foundation::Collections::IMap const& headers) { - using std::wregex; - using std::wsregex_token_iterator; - - // https://tools.ietf.org/html/rfc2616#section-4.2 - wregex rgx{ L"\\s*,\\s*" }; - AccessControlValues result; - - auto ciStrCmp = [](const wstring& a, const wstring& b) { return _wcsicmp(a.c_str(), b.c_str()) < 0; }; - set allowedHeaders{ ciStrCmp }; - set allowedMethods{ ciStrCmp }; - set exposedHeaders{ ciStrCmp }; - - for (const auto& header : headers) { - if (boost::iequals(header.Key(), L"Access-Control-Allow-Headers")) { - auto value = wstring{ header.Value().c_str() }; - - // TODO: Avoid redundant comparison. - auto parsed = set{ - wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; - allowedHeaders.insert(parsed.cbegin(), parsed.cend()); - result.AllowedHeaders.insert(allowedHeaders.cbegin(), allowedHeaders.cend()); - } - else if (boost::iequals(header.Key(), L"Access-Control-Allow-Methods")) { - auto value = wstring{ header.Value().c_str() }; - - // TODO: Avoid redundant comparison. - auto parsed = set{ - wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; - allowedMethods.insert(parsed.cbegin(), parsed.cend()); - result.AllowedMethods.insert(allowedMethods.cbegin(), allowedMethods.cend()); - } - else if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { - result.AllowedOrigin = header.Value(); - } - else if (boost::iequals(header.Key(), L"Access-Control-Expose-Headers")) { - auto value = wstring{ header.Value().c_str() }; - - // TODO: Avoid redundant comparison. - auto parsed = set{ - wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; - exposedHeaders.insert(parsed.cbegin(), parsed.cend()); - result.ExposedHeaders.insert(exposedHeaders.cbegin(), exposedHeaders.cend()); - } - else if (boost::iequals(header.Key(), L"Access-Control-Allow-Credentials")) { - result.AllowedCredentials = header.Value(); - } - else if (boost::iequals(header.Key(), L"Access-Control-Max-Age")) { - result.MaxAge = _wtoi(header.Value().c_str()); - } + if (removeAll) { + httpOnlyCookies.push(header.Key()); + continue; } - return result; - } // ExtractAccessControlValues - - /*static*/ void OriginPolicyHttpFilter::RemoveHttpOnlyCookiesFromResponseHeaders( - HttpResponseMessage const& response, - bool removeAll) { - // Example: "Set-Cookie", L"id=a3fWa; Expires=Wed, 21 Oct 2020 07:28:00 GMT; HttpOnly" - std::queue httpOnlyCookies; - for (const auto& header : response.Headers()) { - if (s_cookieSettingResponseHeaders.find(header.Key().c_str()) == s_cookieSettingResponseHeaders.cend()) - continue; - - if (removeAll) { - httpOnlyCookies.push(header.Key()); - continue; - } - - // Anchors (^$) can't be part of bracket expressions ([]). - // Create 3 matching groups: 1. Beginning of string 2. Between delimiters 3. End of string - const std::wregex re(L"(^HttpOnly\\s*;)|(;\\s*HttpOnly\\s*;)|(;\\s*HttpOnly$)", std::regex_constants::icase); - if (!std::regex_search(header.Value().c_str(), re)) - continue; + // Anchors (^$) can't be part of bracket expressions ([]). + // Create 3 matching groups: 1. Beginning of string 2. Between delimiters 3. End of string + const std::wregex re(L"(^HttpOnly\\s*;)|(;\\s*HttpOnly\\s*;)|(;\\s*HttpOnly$)", std::regex_constants::icase); + if (!std::regex_search(header.Value().c_str(), re)) + continue; - // HttpOnly cookie detected. Removing. - httpOnlyCookies.push(header.Key()); - } // const auto &header : response.Headers() + // HttpOnly cookie detected. Removing. + httpOnlyCookies.push(header.Key()); + } // const auto &header : response.Headers() - while (!httpOnlyCookies.empty()) { - response.Headers().Remove(httpOnlyCookies.front()); - httpOnlyCookies.pop(); - } + while (!httpOnlyCookies.empty()) { + response.Headers().Remove(httpOnlyCookies.front()); + httpOnlyCookies.pop(); } + } - OriginPolicyHttpFilter::OriginPolicyHttpFilter(IHttpFilter const& innerFilter): m_innerFilter{ innerFilter } {} + OriginPolicyHttpFilter::OriginPolicyHttpFilter(IHttpFilter const &innerFilter) : m_innerFilter{innerFilter} {} - OriginPolicyHttpFilter::OriginPolicyHttpFilter() + OriginPolicyHttpFilter::OriginPolicyHttpFilter() : OriginPolicyHttpFilter(winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter{}) {} - OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const& request) { - auto effectiveOriginPolicy = + OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const &request) { + auto effectiveOriginPolicy = static_cast(request.Properties().Lookup(L"OriginPolicy").as().GetUInt64()); - switch (effectiveOriginPolicy) { + switch (effectiveOriginPolicy) { case OriginPolicy::None: return effectiveOriginPolicy; case OriginPolicy::SameOrigin: if (!IsSameOrigin(s_origin, request.RequestUri())) - throw hresult_error{ E_INVALIDARG, L"SOP (same-origin policy) is enforced" }; + throw hresult_error{E_INVALIDARG, L"SOP (same-origin policy) is enforced"}; break; case OriginPolicy::SimpleCrossOriginResourceSharing: // Check for disallowed mixed content if (GetRuntimeOptionBool("Http.BlockMixedContentSimpleCors") && - s_origin.SchemeName() != request.RequestUri().SchemeName()) - throw hresult_error{ E_INVALIDARG, L"The origin and request URLs must have the same scheme" }; + s_origin.SchemeName() != request.RequestUri().SchemeName()) + throw hresult_error{E_INVALIDARG, L"The origin and request URLs must have the same scheme"}; if (IsSameOrigin(s_origin, request.RequestUri())) // Same origin. Therefore, skip Cross-Origin handling. @@ -423,7 +415,7 @@ namespace Microsoft::React::Networking { else if (!IsSimpleCorsRequest(request)) throw hresult_error{ E_INVALIDARG, - L"The request does not meet the requirements for Same-Origin policy or Simple Cross-Origin resource sharing" }; + L"The request does not meet the requirements for Same-Origin policy or Simple Cross-Origin resource sharing"}; break; case OriginPolicy::CrossOriginResourceSharing: @@ -434,13 +426,13 @@ namespace Microsoft::React::Networking { // https://fetch.spec.whatwg.org/#forbidden-header-name if (s_origin.SchemeName() != request.RequestUri().SchemeName()) - throw hresult_error{ E_INVALIDARG, L"The origin and request URLs must have the same scheme" }; + throw hresult_error{E_INVALIDARG, L"The origin and request URLs must have the same scheme"}; if (!AreSafeRequestHeaders(request.Headers())) - throw hresult_error{ E_INVALIDARG, L"Request header not allowed in cross-origin resource sharing" }; + throw hresult_error{E_INVALIDARG, L"Request header not allowed in cross-origin resource sharing"}; if (s_forbiddenMethods.find(request.Method().ToString().c_str()) != s_forbiddenMethods.cend()) - throw hresult_error{ E_INVALIDARG, L"Request method not allowed in cross-origin resource sharing" }; + throw hresult_error{E_INVALIDARG, L"Request method not allowed in cross-origin resource sharing"}; if (IsSameOrigin(s_origin, request.RequestUri())) effectiveOriginPolicy = OriginPolicy::SameOrigin; @@ -453,183 +445,213 @@ namespace Microsoft::React::Networking { default: throw hresult_error{ - E_INVALIDARG, L"Invalid OriginPolicy type: " + to_hstring(static_cast(effectiveOriginPolicy)) }; - } - - return effectiveOriginPolicy; + E_INVALIDARG, L"Invalid OriginPolicy type: " + to_hstring(static_cast(effectiveOriginPolicy))}; } + return effectiveOriginPolicy; + } + + // See https://fetch.spec.whatwg.org/#cors-check + void OriginPolicyHttpFilter::ValidateAllowOrigin( + hstring const &allowedOrigin, hstring const &allowCredentials, 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"}; + // See https://fetch.spec.whatwg.org/#cors-check void OriginPolicyHttpFilter::ValidateAllowOrigin( - hstring const& allowedOrigin, - hstring const& allowCredentials, - IMap props) const { + hstring const &allowedOrigin, hstring const &allowCredentials, 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" }; - - // See https://fetch.spec.whatwg.org/#cors-check - void OriginPolicyHttpFilter::ValidateAllowOrigin( - hstring const& allowedOrigin, - hstring const& allowCredentials, - 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 = 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 - 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"; - - // A wildcard Access-Control-Allow-Origin can not be used if credentials are to be sent, - // even with Access-Control-Allow-Credentials set to true - // See https://fetch.spec.whatwg.org/#cors-protocol-and-credentials - else if (L"*" == allowedOrigin) - errorMessage = - L"Response header Access-Control-Allow-Origin can not have a wildcard value when the request includes credentials"; - - else - errorMessage = L"The Access-Control-Allow-Origin header has a value of [" + allowedOrigin + - L"] which differs from the supplied origin"; - - throw hresult_error{ E_INVALIDARG, errorMessage }; - } + L"Response header Access-Control-Allow-Origin has a value of [null] which differs from the supplied origin"}; + + 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 + 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"; + + // A wildcard Access-Control-Allow-Origin can not be used if credentials are to be sent, + // even with Access-Control-Allow-Credentials set to true + // See https://fetch.spec.whatwg.org/#cors-protocol-and-credentials + else if (L"*" == allowedOrigin) + errorMessage = + L"Response header Access-Control-Allow-Origin can not have a wildcard value when the request includes credentials"; - // 4.10.5 - if (!withCredentials) - return; + else + errorMessage = L"The Access-Control-Allow-Origin header has a value of [" + allowedOrigin + + L"] which differs from the supplied origin"; - // 4.10.6-8 - // https://fetch.spec.whatwg.org/#http-access-control-allow-credentials - // This check should be case sensitive. - // See also https://fetch.spec.whatwg.org/#http-new-header-syntax - if (L"true" != allowCredentials) - throw hresult_error{ - E_INVALIDARG, - L"Access-Control-Allow-Credentials value must be \"true\" when the response includes credentials" }; - }; - - void OriginPolicyHttpFilter::ValidatePreflightResponse( - HttpRequestMessage const& request, - HttpResponseMessage const& response) const { - // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed - using winrt::Windows::Web::Http::HttpStatusCode; - switch (response.StatusCode()) { + throw hresult_error{E_INVALIDARG, errorMessage}; + } + + // 4.10.5 + if (!withCredentials) + return; + + // 4.10.6-8 + // https://fetch.spec.whatwg.org/#http-access-control-allow-credentials + // This check should be case sensitive. + // See also https://fetch.spec.whatwg.org/#http-new-header-syntax + if (L"true" != allowCredentials) + throw hresult_error{ + E_INVALIDARG, + L"Access-Control-Allow-Credentials value must be \"true\" when the response includes credentials"}; + }; + + void OriginPolicyHttpFilter::ValidatePreflightResponse( + HttpRequestMessage const &request, HttpResponseMessage const &response) const { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed + using winrt::Windows::Web::Http::HttpStatusCode; + switch (response.StatusCode()) { case HttpStatusCode::MovedPermanently: case HttpStatusCode::TemporaryRedirect: case HttpStatusCode::PermanentRedirect: - throw hresult_error{ INET_E_REDIRECTING, L"Redirect is not allowed in a preflight request" }; + throw hresult_error{INET_E_REDIRECTING, L"Redirect is not allowed in a preflight request"}; default: break; - } + } - auto controlValues = ExtractAccessControlValues(response.Headers()); - - 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, props); - - // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.5 - // Check if the request method is allowed - bool withCredentials = props.Lookup(L"RequestArgs").as()->WithCredentials; - bool requestMethodAllowed = false; - for (const auto& method : controlValues.AllowedMethods) { - if (L"*" == method) { - if (!withCredentials) { - requestMethodAllowed = true; - break; - } + auto controlValues = ExtractAccessControlValues(response.Headers()); + + 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, props); + + // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.5 + // Check if the request method is allowed + bool withCredentials = props.Lookup(L"RequestArgs").as()->WithCredentials; + bool requestMethodAllowed = false; + for (const auto &method : controlValues.AllowedMethods) { + if (L"*" == method) { + if (!withCredentials) { + requestMethodAllowed = true; + break; } + } - // Preflight should always allow simple CORS methods - requestMethodAllowed |= s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); + // Preflight should always allow simple CORS methods + requestMethodAllowed |= + s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); - if (!requestMethodAllowed) + if (!requestMethodAllowed) + throw hresult_error{ + E_INVALIDARG, + L"Method [" + request.Method().ToString() + + L"] is not allowed by Access-Control-Allow-Methods in preflight response"}; + + // Check if request headers are allowed + // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.6-7 + // Check if the header should be allowed through wildcard, if the request does not have credentials. + bool requestHeadersAllowed = false; + if (!withCredentials && controlValues.AllowedHeaders.find(L"*") != controlValues.AllowedHeaders.cend()) { + // "Authorization" header cannot be allowed through wildcard alone. + // "Authorization" is the only member of https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name. + if (request.Headers().HasKey(L"Authorization") && + controlValues.AllowedHeaders.find(L"Authorization") == controlValues.AllowedHeaders.cend()) throw hresult_error{ E_INVALIDARG, - L"Method [" + request.Method().ToString() + - L"] is not allowed by Access-Control-Allow-Methods in preflight response" }; - - // Check if request headers are allowed - // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.6-7 - // Check if the header should be allowed through wildcard, if the request does not have credentials. - bool requestHeadersAllowed = false; - if (!withCredentials && controlValues.AllowedHeaders.find(L"*") != controlValues.AllowedHeaders.cend()) { - // "Authorization" header cannot be allowed through wildcard alone. - // "Authorization" is the only member of https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name. - if (request.Headers().HasKey(L"Authorization") && - controlValues.AllowedHeaders.find(L"Authorization") == controlValues.AllowedHeaders.cend()) + L"Request header field [Authorization] is not allowed by Access-Control-Allow-Headers in preflight response"}; + + requestHeadersAllowed = true; + } + if (!requestHeadersAllowed) { + // Forbidden headers are excluded from the JavaScript layer. + // User agents may use these headers internally. + const set unsafeNotForbidenHeaderNames = CorsUnsafeNotForbiddenRequestHeaderNames(request.Headers()); + for (const auto name : unsafeNotForbidenHeaderNames) { + if (controlValues.AllowedHeaders.find(name) == controlValues.AllowedHeaders.cend()) throw hresult_error{ E_INVALIDARG, - L"Request header field [Authorization] is not allowed by Access-Control-Allow-Headers in preflight response" }; - - requestHeadersAllowed = true; + L"Request header field [" + to_hstring(name) + + L"] is not allowed by Access-Control-Allow-Headers in preflight response"}; } - if (!requestHeadersAllowed) { - // Forbidden headers are excluded from the JavaScript layer. - // User agents may use these headers internally. - const set unsafeNotForbidenHeaderNames = CorsUnsafeNotForbiddenRequestHeaderNames(request.Headers()); - for (const auto name : unsafeNotForbidenHeaderNames) { - if (controlValues.AllowedHeaders.find(name) == controlValues.AllowedHeaders.cend()) - throw hresult_error{ - E_INVALIDARG, - L"Request header field [" + to_hstring(name) + - L"] is not allowed by Access-Control-Allow-Headers in preflight response" }; - } - } - - // #9770 - insert into preflight cache } - // See 10.7.4 of https://fetch.spec.whatwg.org/#http-network-or-cache-fetch - void OriginPolicyHttpFilter::ValidateResponse(HttpResponseMessage const& response, const OriginPolicy originPolicy) - const { - bool removeAllCookies = false; - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + // #9770 - insert into preflight cache + } + + // See 10.7.4 of https://fetch.spec.whatwg.org/#http-network-or-cache-fetch + void OriginPolicyHttpFilter::ValidateResponse( + HttpResponseMessage const &response, const OriginPolicy originPolicy) const { + bool removeAllCookies = false; + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || originPolicy == OriginPolicy::CrossOriginResourceSharing) { - auto controlValues = ExtractAccessControlValues(response.Headers()); - auto props = response.RequestMessage().Properties(); - auto withCredentials = props.Lookup(L"RequestArgs").try_as()->WithCredentials; + auto controlValues = ExtractAccessControlValues(response.Headers()); + auto props = response.RequestMessage().Properties(); + auto withCredentials = props.Lookup(L"RequestArgs").try_as()->WithCredentials; - if (GetRuntimeOptionBool("Http.StrictOriginCheckSimpleCors") && + if (GetRuntimeOptionBool("Http.StrictOriginCheckSimpleCors") && originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { - bool originAllowed = false; - for (const auto& header : response.Headers()) { - if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { - originAllowed |= L"*" == header.Value() || s_origin == Uri{ header.Value() }; + bool originAllowed = false; + for (const auto &header : response.Headers()) { + if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { + originAllowed |= L"*" == header.Value() || s_origin == Uri{header.Value()}; + } + } + + if (!originAllowed) { + throw hresult_error{E_INVALIDARG, L"The server does not support CORS or the origin is not allowed"}; + } + } else { + ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props); + } + + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { + // Filter out response headers that are not in the Simple CORS allowlist + std::queue nonSimpleNames; + for (const auto &header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend()) + nonSimpleNames.push(header.Key()); + } + + while (!nonSimpleNames.empty()) { + response.Headers().Remove(nonSimpleNames.front()); + nonSimpleNames.pop(); + } + } else { + // Filter out response headers that are not simple headers and not in expose list + + // Keep simple headers and those found in the expose header list. + if (withCredentials || controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { + std::queue nonSimpleNonExposedHeaders; + + for (const auto &header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == + s_simpleCorsResponseHeaderNames.cend() && + controlValues.ExposedHeaders.find(header.Key().c_str()) == controlValues.ExposedHeaders.cend()) { + nonSimpleNonExposedHeaders.push(header.Key()); } } if (!originAllowed) { - throw hresult_error{ E_INVALIDARG, L"The server does not support CORS or the origin is not allowed" }; + throw hresult_error{E_INVALIDARG, L"The server does not support CORS or the origin is not allowed"}; } - } - else { + } else { ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props); } if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { - // Filter out response headers that are not in the Simple CORS allowlist + // Filter out response headers that are not in the Simple CORS whitelist std::queue nonSimpleNames; - for (const auto& header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend()) + for (const auto &header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == + s_simpleCorsResponseHeaderNames.cend()) nonSimpleNames.push(header.Key()); } @@ -637,245 +659,206 @@ namespace Microsoft::React::Networking { response.Headers().Remove(nonSimpleNames.front()); nonSimpleNames.pop(); } - } - else { + } else { // Filter out response headers that are not simple headers and not in expose list // Keep simple headers and those found in the expose header list. if (withCredentials || controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { std::queue nonSimpleNonExposedHeaders; - for (const auto& header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend() && - controlValues.ExposedHeaders.find(header.Key().c_str()) == controlValues.ExposedHeaders.cend()) { + for (const auto &header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == + s_simpleCorsResponseHeaderNames.cend() && + controlValues.ExposedHeaders.find(header.Key().c_str()) == controlValues.ExposedHeaders.cend()) { nonSimpleNonExposedHeaders.push(header.Key()); } } - if (!originAllowed) { - throw hresult_error{ E_INVALIDARG, L"The server does not support CORS or the origin is not allowed" }; + while (!nonSimpleNonExposedHeaders.empty()) { + response.Headers().Remove(nonSimpleNonExposedHeaders.front()); + nonSimpleNonExposedHeaders.pop(); } } - else { - ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props); - } + } - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { - // Filter out response headers that are not in the Simple CORS whitelist - std::queue nonSimpleNames; - for (const auto& header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend()) - nonSimpleNames.push(header.Key()); - } + // When withCredentials is false, request cannot include cookies. Also, cookies will be ignored in + // responses. + removeAllCookies = !withCredentials && GetRuntimeOptionBool("Http.RemoveCookiesFromResponse"); + } // originPolicy == SimpleCrossOriginResourceSharing || CrossOriginResourceSharing - while (!nonSimpleNames.empty()) { - response.Headers().Remove(nonSimpleNames.front()); - nonSimpleNames.pop(); - } - } - else { - // Filter out response headers that are not simple headers and not in expose list + // Don't expose HttpOnly cookies to JavaScript + RemoveHttpOnlyCookiesFromResponseHeaders(response, removeAllCookies); + } - // Keep simple headers and those found in the expose header list. - if (withCredentials || controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { - std::queue nonSimpleNonExposedHeaders; + ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage const &request) const { + auto coRequest = request; - for (const auto& header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend() && - controlValues.ExposedHeaders.find(header.Key().c_str()) == controlValues.ExposedHeaders.cend()) { - nonSimpleNonExposedHeaders.push(header.Key()); - } - } + HttpRequestMessage preflightRequest; - while (!nonSimpleNonExposedHeaders.empty()) { - response.Headers().Remove(nonSimpleNonExposedHeaders.front()); - nonSimpleNonExposedHeaders.pop(); - } - } - } + // Section 4.8.2 https://fetch.spec.whatwg.org/#cors-preflight-fetch + preflightRequest.Method(HttpMethod::Options()); + preflightRequest.RequestUri(coRequest.RequestUri()); + preflightRequest.Headers().Insert(L"Accept", L"*/*"); + preflightRequest.Headers().Insert(L"Access-Control-Request-Method", coRequest.Method().ToString()); - // When withCredentials is false, request cannot include cookies. Also, cookies will be ignored in responses. - removeAllCookies = !withCredentials && GetRuntimeOptionBool("Http.RemoveCookiesFromResponse"); - } // originPolicy == SimpleCrossOriginResourceSharing || CrossOriginResourceSharing + auto headerNames = wstring{}; + auto headerItr = coRequest.Headers().begin(); + if (headerItr != coRequest.Headers().end()) { + headerNames += (*headerItr).Key(); - // Don't expose HttpOnly cookies to JavaScript - RemoveHttpOnlyCookiesFromResponseHeaders(response, removeAllCookies); + while (++headerItr != coRequest.Headers().end()) + headerNames += L", " + (*headerItr).Key(); } - ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage const& request) const { - auto coRequest = request; - - HttpRequestMessage preflightRequest; - - // Section 4.8.2 https://fetch.spec.whatwg.org/#cors-preflight-fetch - preflightRequest.Method(HttpMethod::Options()); - preflightRequest.RequestUri(coRequest.RequestUri()); - preflightRequest.Headers().Insert(L"Accept", L"*/*"); - preflightRequest.Headers().Insert(L"Access-Control-Request-Method", coRequest.Method().ToString()); - - auto headerNames = wstring{}; - auto headerItr = coRequest.Headers().begin(); - if (headerItr != coRequest.Headers().end()) { + if (coRequest.Content()) { + headerItr = coRequest.Content().Headers().begin(); + if (headerItr != coRequest.Content().Headers().end()) { headerNames += (*headerItr).Key(); - while (++headerItr != coRequest.Headers().end()) + while (++headerItr != coRequest.Content().Headers().end()) headerNames += L", " + (*headerItr).Key(); } + } - if (coRequest.Content()) { - headerItr = coRequest.Content().Headers().begin(); - if (headerItr != coRequest.Content().Headers().end()) { - headerNames += (*headerItr).Key(); - - while (++headerItr != coRequest.Content().Headers().end()) - headerNames += L", " + (*headerItr).Key(); - } - } - - preflightRequest.Headers().Insert(L"Access-Control-Request-Headers", headerNames); - preflightRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); - preflightRequest.Headers().Insert(L"Sec-Fetch-Mode", L"CORS"); + preflightRequest.Headers().Insert(L"Access-Control-Request-Headers", headerNames); + preflightRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); + preflightRequest.Headers().Insert(L"Sec-Fetch-Mode", L"CORS"); - co_return{ co_await m_innerFilter.SendRequestAsync(preflightRequest) }; - } + 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()) && + 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; - } + // 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; } + return true; + } + #pragma endregion IRedirectEventSource #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()) && + 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; - } + // 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; } + return true; + } + #pragma endregion IRedirectEventSource #pragma region IHttpFilter - ResponseOperation OriginPolicyHttpFilter::SendRequestAsync(HttpRequestMessage const& request) { - auto coRequest = request; + ResponseOperation OriginPolicyHttpFilter::SendRequestAsync(HttpRequestMessage const &request) { + auto coRequest = request; - // Set initial origin policy to global runtime option. - request.Properties().Insert(L"OriginPolicy", winrt::box_value(GetRuntimeOptionInt("Http.OriginPolicy"))); + // Set initial origin policy to global runtime option. + request.Properties().Insert(L"OriginPolicy", winrt::box_value(GetRuntimeOptionInt("Http.OriginPolicy"))); - // Allow only HTTP or HTTPS schemes - if (GetRuntimeOptionBool("Http.StrictScheme") && coRequest.RequestUri().SchemeName() != L"https" && + // Allow only HTTP or HTTPS schemes + if (GetRuntimeOptionBool("Http.StrictScheme") && coRequest.RequestUri().SchemeName() != L"https" && coRequest.RequestUri().SchemeName() != L"http") - throw hresult_error{ E_INVALIDARG, L"Invalid URL scheme: [" + s_origin.SchemeName() + L"]" }; + throw hresult_error{E_INVALIDARG, L"Invalid URL scheme: [" + s_origin.SchemeName() + L"]"}; - if (!GetRuntimeOptionBool("Http.OmitCredentials")) { - coRequest.Properties().Lookup(L"RequestArgs").as()->WithCredentials = false; - } + if (!GetRuntimeOptionBool("Http.OmitCredentials")) { + coRequest.Properties().Lookup(L"RequestArgs").as()->WithCredentials = false; + } - // Ensure absolute URL - coRequest.RequestUri(Uri{ coRequest.RequestUri().AbsoluteCanonicalUri() }); + // Ensure absolute URL + coRequest.RequestUri(Uri{coRequest.RequestUri().AbsoluteCanonicalUri()}); - auto originPolicy = ValidateRequest(coRequest); - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + auto originPolicy = ValidateRequest(coRequest); + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || originPolicy == OriginPolicy::CrossOriginResourceSharing) { - if (coRequest.RequestUri().UserName().size() > 0 || coRequest.RequestUri().Password().size() > 0) { - coRequest.RequestUri(Uri{ coRequest.RequestUri().DisplayUri() }); - } + if (coRequest.RequestUri().UserName().size() > 0 || coRequest.RequestUri().Password().size() > 0) { + coRequest.RequestUri(Uri{coRequest.RequestUri().DisplayUri()}); } + } - try { - // #9770 - Validate preflight cache - if (originPolicy == OriginPolicy::CrossOriginResourceSharing) { - // If inner filter can AllowRedirect, disable for preflight. - winrt::impl::com_ref baseFilter; - if (baseFilter = m_innerFilter.try_as()) { - baseFilter.AllowAutoRedirect(false); - } - - auto preflightResponse = co_await SendPreflightAsync(coRequest); + try { + // #9770 - Validate preflight cache + if (originPolicy == OriginPolicy::CrossOriginResourceSharing) { + // If inner filter can AllowRedirect, disable for preflight. + winrt::impl::com_ref baseFilter; + if (baseFilter = m_innerFilter.try_as()) { + baseFilter.AllowAutoRedirect(false); + } - if (baseFilter) { - baseFilter.AllowAutoRedirect(true); - } + auto preflightResponse = co_await SendPreflightAsync(coRequest); - ValidatePreflightResponse(coRequest, preflightResponse); + if (baseFilter) { + baseFilter.AllowAutoRedirect(true); } - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + ValidatePreflightResponse(coRequest, preflightResponse); + } + + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || originPolicy == OriginPolicy::CrossOriginResourceSharing) { - coRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); - } + coRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); + } - auto response = co_await m_innerFilter.SendRequestAsync(coRequest); + auto response = co_await m_innerFilter.SendRequestAsync(coRequest); - ValidateResponse(response, originPolicy); + ValidateResponse(response, originPolicy); - co_return response; + co_return response; - } - catch (hresult_error const& e) { - throw e; - } - catch (const std::exception& e) { - throw hresult_error{ E_FAIL, to_hstring(e.what()) }; - } - catch (...) { - throw hresult_error{ E_FAIL, L"Unspecified error processing Origin Policy request" }; - } + } catch (hresult_error const &e) { + throw e; + } catch (const std::exception &e) { + throw hresult_error{E_FAIL, to_hstring(e.what())}; + } catch (...) { + throw hresult_error{E_FAIL, L"Unspecified error processing Origin Policy request"}; } + } #pragma endregion IHttpFilter #pragma endregion OriginPolicyHttpFilter - } // namespace Microsoft::React::Networking + } // namespace Microsoft::React::Networking diff --git a/vnext/Shared/Networking/WinRTHttpResource.cpp b/vnext/Shared/Networking/WinRTHttpResource.cpp index 1d6336f5e64..f0445d868c5 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.cpp +++ b/vnext/Shared/Networking/WinRTHttpResource.cpp @@ -53,171 +53,163 @@ using winrt::Windows::Web::Http::Headers::HttpMediaTypeHeaderValue; namespace { - constexpr uint32_t operator""_KiB(unsigned long long int x) { - return static_cast(1024 * x); - } +constexpr uint32_t operator""_KiB(unsigned long long int x) { + return static_cast(1024 * x); +} - constexpr uint32_t operator""_MiB(unsigned long long int x) { - return static_cast(1024_KiB * x); - } +constexpr uint32_t operator""_MiB(unsigned long long int x) { + return static_cast(1024_KiB * x); +} - constexpr char responseTypeText[] = "text"; - constexpr char responseTypeBase64[] = "base64"; - constexpr char responseTypeBlob[] = "blob"; +constexpr char responseTypeText[] = "text"; +constexpr char responseTypeBase64[] = "base64"; +constexpr char responseTypeBlob[] = "blob"; } // namespace namespace Microsoft::React::Networking { - // May throw winrt::hresult_error - void AttachMultipartHeaders(IHttpContent content, const dynamic& headers) { - HttpMediaTypeHeaderValue contentType{ nullptr }; - - // Headers are generally case-insensitive - // https://www.ietf.org/rfc/rfc2616.txt section 4.2 - // TODO: Consolidate with PerformRequest's header parsing. - for (auto& header : headers.items()) { - auto& name = header.first.getString(); - auto& value = header.second.getString(); - - if (boost::iequals(name.c_str(), "Content-Type")) { - contentType = HttpMediaTypeHeaderValue::Parse(to_hstring(value)); - } - else if (boost::iequals(name.c_str(), "Authorization")) { - bool success = content.Headers().TryAppendWithoutValidation(to_hstring(name), to_hstring(value)); - if (!success) { - throw hresult_error{ E_INVALIDARG, L"Failed to append Authorization" }; - } - } - else { - content.Headers().Append(to_hstring(name), to_hstring(value)); - } +// May throw winrt::hresult_error +void AttachMultipartHeaders(IHttpContent content, const dynamic &headers) { + HttpMediaTypeHeaderValue contentType{nullptr}; + + // Headers are generally case-insensitive + // https://www.ietf.org/rfc/rfc2616.txt section 4.2 + // TODO: Consolidate with PerformRequest's header parsing. + for (auto &header : headers.items()) { + auto &name = header.first.getString(); + auto &value = header.second.getString(); + + if (boost::iequals(name.c_str(), "Content-Type")) { + contentType = HttpMediaTypeHeaderValue::Parse(to_hstring(value)); + } else if (boost::iequals(name.c_str(), "Authorization")) { + bool success = content.Headers().TryAppendWithoutValidation(to_hstring(name), to_hstring(value)); + if (!success) { + throw hresult_error{E_INVALIDARG, L"Failed to append Authorization"}; + } + } else { + content.Headers().Append(to_hstring(name), to_hstring(value)); } + } - if (contentType) { - content.Headers().ContentType(contentType); - } + if (contentType) { + content.Headers().ContentType(contentType); } +} #pragma region WinRTHttpResource - WinRTHttpResource::WinRTHttpResource(IHttpClient&& client) noexcept: m_client{ std::move(client) } {} +WinRTHttpResource::WinRTHttpResource(IHttpClient &&client) noexcept : m_client{std::move(client)} {} - WinRTHttpResource::WinRTHttpResource() noexcept: WinRTHttpResource(winrt::Windows::Web::Http::HttpClient{}) {} +WinRTHttpResource::WinRTHttpResource() noexcept : WinRTHttpResource(winrt::Windows::Web::Http::HttpClient{}) {} #pragma region IWinRTHttpRequestFactory - IAsyncOperation WinRTHttpResource::CreateRequest( - HttpMethod&& method, - Uri&& uri, +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 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(); + auto iReqArgs = request.Properties().Lookup(L"RequestArgs"); + auto reqArgs = iReqArgs.as(); + auto self = shared_from_this(); - HttpMediaTypeHeaderValue contentType{ nullptr }; - string contentEncoding; - string contentLength; + 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) { - auto& name = header.first; - auto& value = header.second; + // Headers are generally case-insensitive + // https://www.ietf.org/rfc/rfc2616.txt section 4.2 + for (auto &header : reqArgs->Headers) { + auto &name = header.first; + auto &value = header.second; - if (boost::iequals(name.c_str(), "Content-Type")) { - bool success = HttpMediaTypeHeaderValue::TryParse(to_hstring(value), contentType); - if (!success) { - if (self->m_onError) { - self->m_onError(reqArgs->RequestId, "Failed to parse Content-Type", false); - } + if (boost::iequals(name.c_str(), "Content-Type")) { + bool success = HttpMediaTypeHeaderValue::TryParse(to_hstring(value), contentType); + if (!success) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, "Failed to parse Content-Type", false); } } - else if (boost::iequals(name.c_str(), "Content-Encoding")) { - contentEncoding = value; - } - else if (boost::iequals(name.c_str(), "Content-Length")) { - contentLength = value; - } - else if (boost::iequals(name.c_str(), "Authorization")) { - bool success = request.Headers().TryAppendWithoutValidation(to_hstring(name), to_hstring(value)); - if (!success) { - if (self->m_onError) { - self->m_onError(reqArgs->RequestId, "Failed to append Authorization", false); - } + } else if (boost::iequals(name.c_str(), "Content-Encoding")) { + contentEncoding = value; + } else if (boost::iequals(name.c_str(), "Content-Length")) { + contentLength = value; + } else if (boost::iequals(name.c_str(), "Authorization")) { + bool success = request.Headers().TryAppendWithoutValidation(to_hstring(name), to_hstring(value)); + if (!success) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, "Failed to append Authorization", false); } } - else { - try { - request.Headers().Append(to_hstring(name), to_hstring(value)); + } else { + try { + request.Headers().Append(to_hstring(name), to_hstring(value)); + } catch (hresult_error const &e) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), false); } - catch (hresult_error const& e) { + catch (hresult_error const &e) { if (self->m_onError) { self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), false); } - catch (hresult_error const& e) { - if (self->m_onError) { - self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), false); - } - co_return nullptr; - } - } - auto bytes = blob["bytes"]; - auto byteVector = vector(bytes.size()); - for (auto& byte : bytes) { - byteVector.push_back(static_cast(byte.asInt())); + co_return nullptr; } - 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()) }; + 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["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["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["formData"].empty()) { - winrt::Windows::Web::Http::HttpMultipartFormDataContent multiPartContent; - auto formData = data["formData"]; - - // #6046 - Overwriting WinRT's HttpMultipartFormDataContent implicit Content-Type clears the generated boundary - contentType = nullptr; - - for (auto& formDataPart : formData) { - IHttpContent formContent{ nullptr }; - if (!formDataPart["string"].isNull()) { - formContent = HttpStringContent{ to_hstring(formDataPart["string"].asString()) }; - } - else if (!formDataPart["uri"].empty()) { - auto filePath = to_hstring(formDataPart["uri"].asString()); - auto file = co_await StorageFile::GetFileFromPathAsync(filePath); - auto stream = co_await file.OpenReadAsync(); - formContent = HttpStreamContent{ stream }; - } - - if (formContent) { - AttachMultipartHeaders(formContent, formDataPart["headers"]); - multiPartContent.Add(formContent, to_hstring(formDataPart["fieldName"].asString())); - } - } // foreach form data part - - content = multiPartContent; + else if (!data["formData"].empty()) { + winrt::Windows::Web::Http::HttpMultipartFormDataContent multiPartContent; + auto formData = data["formData"]; + + // #6046 - Overwriting WinRT's HttpMultipartFormDataContent implicit Content-Type clears the generated boundary + contentType = nullptr; + + for (auto &formDataPart : formData) { + IHttpContent formContent{nullptr}; + if (!formDataPart["string"].isNull()) { + formContent = HttpStringContent{to_hstring(formDataPart["string"].asString())}; + } else if (!formDataPart["uri"].empty()) { + auto filePath = to_hstring(formDataPart["uri"].asString()); + auto file = co_await StorageFile::GetFileFromPathAsync(filePath); + auto stream = co_await file.OpenReadAsync(); + formContent = HttpStreamContent{stream}; + } + + if (formContent) { + AttachMultipartHeaders(formContent, formDataPart["headers"]); + multiPartContent.Add(formContent, to_hstring(formDataPart["fieldName"].asString())); + } + } // foreach form data part + + content = multiPartContent; } // Initialize content - IHttpContent content{ nullptr }; - auto& data = reqArgs->Data; + IHttpContent content{nullptr}; + auto &data = reqArgs->Data; if (!data.isNull()) { auto bodyHandler = self->m_requestBodyHandler.lock(); if (bodyHandler && bodyHandler->Supports(data)) { @@ -225,8 +217,7 @@ namespace Microsoft::React::Networking { dynamic blob; try { blob = bodyHandler->ToRequestBody(data, contentTypeString); - } - catch (const std::invalid_argument& e) { + } catch (const std::invalid_argument &e) { if (self->m_onError) { self->m_onError(reqArgs->RequestId, e.what(), false); } @@ -234,26 +225,22 @@ namespace Microsoft::React::Networking { } auto bytes = blob["bytes"]; auto byteVector = vector(bytes.size()); - for (auto& byte : bytes) { + for (auto &byte : bytes) { byteVector.push_back(static_cast(byte.asInt())); } - auto view = winrt::array_view{ byteVector }; + 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()) { + 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()) }); + 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()) { + content = HttpStreamContent{std::move(stream)}; + } else if (!data["form"].empty()) { // #9535 - HTTP form data support // winrt::Windows::Web::Http::HttpMultipartFormDataContent() } @@ -277,16 +264,14 @@ namespace Microsoft::React::Networking { try { const auto contentLengthHeader = std::stol(contentLength); content.Headers().ContentLength(contentLengthHeader); - } - catch (const std::invalid_argument& e) { + } catch (const std::invalid_argument &e) { if (self->m_onError) - self->m_onError(reqArgs->RequestId, e.what() + string{ " [" } + contentLength + "]", false); + self->m_onError(reqArgs->RequestId, e.what() + string{" ["} + contentLength + "]", false); co_return nullptr; - } - catch (const std::out_of_range& e) { + } catch (const std::out_of_range &e) { if (self->m_onError) - self->m_onError(reqArgs->RequestId, e.what() + string{ " [" } + contentLength + "]", false); + self->m_onError(reqArgs->RequestId, e.what() + string{" ["} + contentLength + "]", false); co_return nullptr; } @@ -296,13 +281,13 @@ namespace Microsoft::React::Networking { } co_return request; - } + } #pragma endregion IWinRTHttpRequestFactory #pragma region IHttpResource - void WinRTHttpResource::SendRequest( + void WinRTHttpResource::SendRequest( string && method, string && url, int64_t requestId, @@ -312,44 +297,13 @@ namespace Microsoft::React::Networking { bool useIncrementalUpdates, int64_t timeout, bool withCredentials, - std::function && callback) noexcept /*override*/ { - // Enforce supported args - assert(responseType == responseTypeText || responseType == responseTypeBase64 || responseType == responseTypeBlob); - - try { - HttpMethod httpMethod{ to_hstring(std::move(method)) }; - Uri uri{ to_hstring(std::move(url)) }; - - 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(), false); - } - } - catch (hresult_error const& e) { - if (m_onError) { - m_onError(requestId, Utilities::HResultToString(e), false); - } - } - catch (...) { - m_onError(requestId, "Unidentified error sending HTTP request", false); - } - } + std::function &&callback) noexcept /*override*/ { + // Enforce supported args + assert(responseType == responseTypeText || responseType == responseTypeBase64 || responseType == responseTypeBlob); try { - HttpMethod httpMethod{ to_hstring(std::move(method)) }; - Uri uri{ to_hstring(std::move(url)) }; + HttpMethod httpMethod{to_hstring(std::move(method))}; + Uri uri{to_hstring(std::move(url))}; auto iReqArgs = winrt::make(); auto reqArgs = iReqArgs.as(); @@ -362,52 +316,44 @@ namespace Microsoft::React::Networking { reqArgs->Timeout = timeout; PerformSendRequest(std::move(httpMethod), std::move(uri), iReqArgs); - } - catch (std::exception const& e) { + } catch (std::exception const &e) { if (m_onError) { m_onError(requestId, e.what(), false); } - } - catch (hresult_error const& e) { + } catch (hresult_error const &e) { if (m_onError) { m_onError(requestId, Utilities::HResultToString(e), false); } - } - catch (...) { + } catch (...) { m_onError(requestId, "Unidentified error sending HTTP request", false); } + } - void WinRTHttpResource::ClearCookies() noexcept /*override*/ { - assert(false); - // NOT IMPLEMENTED - } - - void WinRTHttpResource::SetOnRequestSuccess(function && handler) noexcept /*override*/ { - m_onRequestSuccess = std::move(handler); - } - - void WinRTHttpResource::SetOnResponse(function && handler) noexcept - /*override*/ { - m_onResponse = std::move(handler); - } - - void WinRTHttpResource::SetOnData(function && handler) noexcept - /*override*/ { - m_onData = std::move(handler); - } - - void WinRTHttpResource::SetOnData(function && handler) noexcept - /*override*/ - { - m_onDataDynamic = std::move(handler); - } + try { + HttpMethod httpMethod{to_hstring(std::move(method))}; + Uri uri{to_hstring(std::move(url))}; - try { - request.Cancel(); + 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(), false); } - catch (hresult_error const& e) { + } catch (hresult_error const &e) { + if (m_onError) { m_onError(requestId, Utilities::HResultToString(e), false); } + } catch (...) { + m_onError(requestId, "Unidentified error sending HTTP request", false); } void WinRTHttpResource::ClearCookies() noexcept /*override*/ { @@ -415,311 +361,334 @@ namespace Microsoft::React::Networking { // NOT IMPLEMENTED } - void WinRTHttpResource::SetOnRequestSuccess(function&& handler) noexcept /*override*/ { + void WinRTHttpResource::SetOnRequestSuccess(function && handler) noexcept /*override*/ { m_onRequestSuccess = std::move(handler); } - void WinRTHttpResource::SetOnResponse(function&& handler) noexcept - /*override*/ { + void WinRTHttpResource::SetOnResponse(function && handler) noexcept + /*override*/ { m_onResponse = std::move(handler); } - void WinRTHttpResource::SetOnData(function&& handler) noexcept - /*override*/ { + void WinRTHttpResource::SetOnData(function && handler) noexcept + /*override*/ { m_onData = std::move(handler); } - void WinRTHttpResource::SetOnData(function&& handler) noexcept - /*override*/ + void WinRTHttpResource::SetOnData(function && handler) noexcept + /*override*/ { m_onDataDynamic = std::move(handler); } - void WinRTHttpResource::SetOnIncrementalData( - function&& handler) noexcept - /*override*/ { - m_onIncrementalData = std::move(handler); - } - - void WinRTHttpResource::SetOnDataProgress( - function&& handler) noexcept - /*override*/ { - m_onDataProgress = std::move(handler); - } - - void WinRTHttpResource::SetOnResponseComplete(function&& handler) noexcept /*override*/ { - m_onComplete = std::move(handler); - } - - void WinRTHttpResource::SetOnError( - function&& handler) noexcept - /*override*/ { - m_onError = std::move(handler); + try { + request.Cancel(); + } catch (hresult_error const &e) { + m_onError(requestId, Utilities::HResultToString(e), false); } +} + +void WinRTHttpResource::ClearCookies() noexcept /*override*/ { + assert(false); + // NOT IMPLEMENTED +} + +void WinRTHttpResource::SetOnRequestSuccess(function &&handler) noexcept /*override*/ { + m_onRequestSuccess = std::move(handler); +} + +void WinRTHttpResource::SetOnResponse(function &&handler) noexcept +/*override*/ { + m_onResponse = std::move(handler); +} + +void WinRTHttpResource::SetOnData(function &&handler) noexcept +/*override*/ { + m_onData = std::move(handler); +} + +void WinRTHttpResource::SetOnData(function &&handler) noexcept +/*override*/ +{ + m_onDataDynamic = std::move(handler); +} + +void WinRTHttpResource::SetOnIncrementalData( + function &&handler) noexcept +/*override*/ { + m_onIncrementalData = std::move(handler); +} + +void WinRTHttpResource::SetOnDataProgress( + function &&handler) noexcept +/*override*/ { + m_onDataProgress = std::move(handler); +} + +void WinRTHttpResource::SetOnResponseComplete(function &&handler) noexcept /*override*/ { + m_onComplete = std::move(handler); +} + +void WinRTHttpResource::SetOnError( + function &&handler) noexcept +/*override*/ { + m_onError = std::move(handler); +} #pragma endregion IHttpResource - void WinRTHttpResource::TrackResponse(int64_t requestId, ResponseOperation response) noexcept { - scoped_lock lock{ m_mutex }; - m_responses[requestId] = response; - } - - void WinRTHttpResource::UntrackResponse(int64_t requestId) noexcept { - scoped_lock lock{ m_mutex }; - m_responses.erase(requestId); - } - - fire_and_forget - WinRTHttpResource::PerformSendRequest(HttpMethod&& method, Uri&& rtUri, IInspectable const& args) noexcept { - // Keep references after coroutine suspension. - auto self = shared_from_this(); - auto coArgs = args; - 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 coRequestOp = CreateRequest(std::move(coMethod), std::move(coUri), props); - co_await lessthrow_await_adapter>{coRequestOp}; - auto coRequestOpHR = coRequestOp.ErrorCode(); - if (coRequestOpHR < 0) { - if (self->m_onError) { - self->m_onError(reqArgs->RequestId, Utilities::HResultToString(std::move(coRequestOpHR)), false); - } - co_return self->UntrackResponse(reqArgs->RequestId); +void WinRTHttpResource::TrackResponse(int64_t requestId, ResponseOperation response) noexcept { + scoped_lock lock{m_mutex}; + m_responses[requestId] = response; +} + +void WinRTHttpResource::UntrackResponse(int64_t requestId) noexcept { + scoped_lock lock{m_mutex}; + m_responses.erase(requestId); +} + +fire_and_forget +WinRTHttpResource::PerformSendRequest(HttpMethod &&method, Uri &&rtUri, IInspectable const &args) noexcept { + // Keep references after coroutine suspension. + auto self = shared_from_this(); + auto coArgs = args; + 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 coRequestOp = CreateRequest(std::move(coMethod), std::move(coUri), props); + co_await lessthrow_await_adapter>{coRequestOp}; + auto coRequestOpHR = coRequestOp.ErrorCode(); + if (coRequestOpHR < 0) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, Utilities::HResultToString(std::move(coRequestOpHR)), false); } + co_return self->UntrackResponse(reqArgs->RequestId); + } - auto coRequest = coRequestOp.GetResults(); + auto coRequest = coRequestOp.GetResults(); - // 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, reqArgs->ResponseType)) { - auto blob = uriHandler->Fetch(uri); - if (self->m_onDataDynamic && self->m_onRequestSuccess) { - self->m_onDataDynamic(reqArgs->RequestId, std::move(blob)); - self->m_onRequestSuccess(reqArgs->RequestId); - } - - if (self->m_onComplete) { - self->m_onComplete(reqArgs->RequestId); - } + // 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, reqArgs->ResponseType)) { + auto blob = uriHandler->Fetch(uri); + if (self->m_onDataDynamic && self->m_onRequestSuccess) { + self->m_onDataDynamic(reqArgs->RequestId, std::move(blob)); + self->m_onRequestSuccess(reqArgs->RequestId); + } - co_return; + if (self->m_onComplete) { + self->m_onComplete(reqArgs->RequestId); } + + co_return; } - catch (const hresult_error& e) { - if (self->m_onError) - 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(reqArgs->RequestId, e.what(), false); - } + } catch (const hresult_error &e) { + if (self->m_onError) + 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(reqArgs->RequestId, e.what(), false); } + } - try { - auto sendRequestOp = self->m_client.SendRequestAsync(coRequest); - - auto isText = reqArgs->ResponseType == responseTypeText; + try { + auto sendRequestOp = self->m_client.SendRequestAsync(coRequest); - self->TrackResponse(reqArgs->RequestId, sendRequestOp); + auto isText = reqArgs->ResponseType == responseTypeText; - 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); + self->TrackResponse(reqArgs->RequestId, sendRequestOp); - co_await lessthrow_await_adapter{winrt::when_any(sendRequestOp, sendRequestTimeout)}; + 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); - // Cancel either still unfinished coroutine. - sendRequestTimeout.Cancel(); - sendRequestOp.Cancel(); + co_await lessthrow_await_adapter{winrt::when_any(sendRequestOp, sendRequestTimeout)}; - if (*timedOut) { - if (self->m_onError) { - // 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 self->UntrackResponse(reqArgs->RequestId); - } - } - else { - co_await lessthrow_await_adapter{sendRequestOp}; - } + // Cancel either still unfinished coroutine. + sendRequestTimeout.Cancel(); + sendRequestOp.Cancel(); - auto result = sendRequestOp.ErrorCode(); - if (result < 0) { + if (*timedOut) { if (self->m_onError) { - self->m_onError(reqArgs->RequestId, Utilities::HResultToString(std::move(result)), false); + // 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 self->UntrackResponse(reqArgs->RequestId); } + } else { + co_await lessthrow_await_adapter{sendRequestOp}; + } - auto response = sendRequestOp.GetResults(); - if (response) { - if (self->m_onResponse) { - auto url = to_string(response.RequestMessage().RequestUri().AbsoluteUri()); + auto result = sendRequestOp.ErrorCode(); + if (result < 0) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, Utilities::HResultToString(std::move(result)), false); + } + co_return self->UntrackResponse(reqArgs->RequestId); + } - // Gather headers for both the response content and the response itself - // See Invoke-WebRequest PowerShell cmdlet or Chromium response handling - Headers responseHeaders; - for (auto header : response.Headers()) { - responseHeaders.emplace(to_string(header.Key()), to_string(header.Value())); - } - for (auto header : response.Content().Headers()) { - responseHeaders.emplace(to_string(header.Key()), to_string(header.Value())); - } + auto response = sendRequestOp.GetResults(); + if (response) { + if (self->m_onResponse) { + auto url = to_string(response.RequestMessage().RequestUri().AbsoluteUri()); - self->m_onResponse( - reqArgs->RequestId, - { static_cast(response.StatusCode()), std::move(url), std::move(responseHeaders) }); + // Gather headers for both the response content and the response itself + // See Invoke-WebRequest PowerShell cmdlet or Chromium response handling + Headers responseHeaders; + for (auto header : response.Headers()) { + responseHeaders.emplace(to_string(header.Key()), to_string(header.Value())); + } + for (auto header : response.Content().Headers()) { + responseHeaders.emplace(to_string(header.Key()), to_string(header.Value())); } + + self->m_onResponse( + reqArgs->RequestId, + {static_cast(response.StatusCode()), std::move(url), std::move(responseHeaders)}); } + } - // #9534 - Support HTTP incremental updates - if (response && response.Content()) { - auto inputStream = co_await response.Content().ReadAsInputStreamAsync(); - auto reader = DataReader{ inputStream }; - - // Accumulate all incoming request data in 8MB chunks - // Note, the minimum apparent valid chunk size is 128 KB - // Apple's implementation appears to grab 5-8 KB chunks - const uint32_t segmentSize = reqArgs->IncrementalUpdates ? 128_KiB : 8_MiB; - - // Let response handler take over, if set - if (auto responseHandler = self->m_responseHandler.lock()) { - if (responseHandler->Supports(reqArgs->ResponseType)) { - vector responseData{}; - while (auto loaded = co_await reader.LoadAsync(segmentSize)) { - auto length = reader.UnconsumedBufferLength(); - auto data = vector(length); - reader.ReadBytes(data); - - responseData.insert(responseData.cend(), data.cbegin(), data.cend()); - } + // #9534 - Support HTTP incremental updates + if (response && response.Content()) { + auto inputStream = co_await response.Content().ReadAsInputStreamAsync(); + auto reader = DataReader{inputStream}; + + // Accumulate all incoming request data in 8MB chunks + // Note, the minimum apparent valid chunk size is 128 KB + // Apple's implementation appears to grab 5-8 KB chunks + const uint32_t segmentSize = reqArgs->IncrementalUpdates ? 128_KiB : 8_MiB; + + // Let response handler take over, if set + if (auto responseHandler = self->m_responseHandler.lock()) { + if (responseHandler->Supports(reqArgs->ResponseType)) { + vector responseData{}; + while (auto loaded = co_await reader.LoadAsync(segmentSize)) { + auto length = reader.UnconsumedBufferLength(); + auto data = vector(length); + reader.ReadBytes(data); - auto blob = responseHandler->ToResponseData(std::move(responseData)); + responseData.insert(responseData.cend(), data.cbegin(), data.cend()); + } - if (self->m_onDataDynamic && self->m_onRequestSuccess) { - self->m_onDataDynamic(reqArgs->RequestId, std::move(blob)); - self->m_onRequestSuccess(reqArgs->RequestId); - } + auto blob = responseHandler->ToResponseData(std::move(responseData)); - if (self->m_onComplete) { - self->m_onComplete(reqArgs->RequestId); - } - co_return; + if (self->m_onDataDynamic && self->m_onRequestSuccess) { + self->m_onDataDynamic(reqArgs->RequestId, std::move(blob)); + self->m_onRequestSuccess(reqArgs->RequestId); } - } - if (isText) { - reader.UnicodeEncoding(UnicodeEncoding::Utf8); + if (self->m_onComplete) { + self->m_onComplete(reqArgs->RequestId); + } + co_return; } + } - int64_t receivedBytes = 0; - string responseData; - winrt::Windows::Storage::Streams::IBuffer buffer; - while (auto loaded = co_await reader.LoadAsync(segmentSize)) { - auto length = reader.UnconsumedBufferLength(); - receivedBytes += length; - - if (isText) { - auto data = vector(length); - reader.ReadBytes(data); + if (isText) { + reader.UnicodeEncoding(UnicodeEncoding::Utf8); + } - auto incrementData = string(Common::Utilities::CheckedReinterpretCast(data.data()), data.size()); - // #9534 - Send incremental updates. - // See https://github.com/facebook/react-native/blob/v0.70.6/Libraries/Network/RCTNetworking.mm#L561 - if (reqArgs->IncrementalUpdates) { - responseData = std::move(incrementData); + int64_t receivedBytes = 0; + string responseData; + winrt::Windows::Storage::Streams::IBuffer buffer; + while (auto loaded = co_await reader.LoadAsync(segmentSize)) { + auto length = reader.UnconsumedBufferLength(); + receivedBytes += length; - if (self->m_onIncrementalData) { - // For total, see #10849 - self->m_onIncrementalData(reqArgs->RequestId, std::move(responseData), receivedBytes, 0 /*total*/); - } - } - else { - responseData += std::move(incrementData); - } - } - else { - buffer = reader.ReadBuffer(length); - auto data = CryptographicBuffer::EncodeToBase64String(buffer); + if (isText) { + auto data = vector(length); + reader.ReadBytes(data); - responseData += winrt::to_string(std::wstring_view(data)); + auto incrementData = string(Common::Utilities::CheckedReinterpretCast(data.data()), data.size()); + // #9534 - Send incremental updates. + // See https://github.com/facebook/react-native/blob/v0.70.6/Libraries/Network/RCTNetworking.mm#L561 + if (reqArgs->IncrementalUpdates) { + responseData = std::move(incrementData); - if (self->m_onDataProgress) { + if (self->m_onIncrementalData) { // For total, see #10849 - self->m_onDataProgress(reqArgs->RequestId, receivedBytes, 0 /*total*/); + self->m_onIncrementalData(reqArgs->RequestId, std::move(responseData), receivedBytes, 0 /*total*/); } + } else { + responseData += std::move(incrementData); } - } + } else { + buffer = reader.ReadBuffer(length); + auto data = CryptographicBuffer::EncodeToBase64String(buffer); - // If dealing with text-incremental response data, use m_onIncrementalData instead - if (self->m_onData && !(reqArgs->IncrementalUpdates && isText)) { - self->m_onData(reqArgs->RequestId, std::move(responseData)); - } + responseData += winrt::to_string(std::wstring_view(data)); - if (self->m_onComplete) { - self->m_onComplete(reqArgs->RequestId); + if (self->m_onDataProgress) { + // For total, see #10849 + self->m_onDataProgress(reqArgs->RequestId, receivedBytes, 0 /*total*/); + } } } - else { - if (self->m_onError) { - self->m_onError(reqArgs->RequestId, response == nullptr ? "request failed" : "No response content", false); - } + + // If dealing with text-incremental response data, use m_onIncrementalData instead + if (self->m_onData && !(reqArgs->IncrementalUpdates && isText)) { + self->m_onData(reqArgs->RequestId, std::move(responseData)); } - } - catch (std::exception const& e) { - if (self->m_onError) { - self->m_onError(reqArgs->RequestId, e.what(), false); + + if (self->m_onComplete) { + self->m_onComplete(reqArgs->RequestId); } - } - catch (hresult_error const& e) { + } else { if (self->m_onError) { - self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), false); + self->m_onError(reqArgs->RequestId, response == nullptr ? "request failed" : "No response content", false); } } - catch (...) { - if (self->m_onError) { - self->m_onError(reqArgs->RequestId, "Unhandled exception during request", false); - } + } catch (std::exception const &e) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, e.what(), false); } + } catch (hresult_error const &e) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), false); + } + } catch (...) { + if (self->m_onError) { + self->m_onError(reqArgs->RequestId, "Unhandled exception during request", false); + } + } - self->UntrackResponse(reqArgs->RequestId); - } // PerformSendRequest + self->UntrackResponse(reqArgs->RequestId); +} // PerformSendRequest #pragma region IHttpModuleProxy - void WinRTHttpResource::AddUriHandler(shared_ptr /*uriHandler*/) noexcept /*override*/ - { - // TODO: Implement custom URI handling. - } +void WinRTHttpResource::AddUriHandler(shared_ptr /*uriHandler*/) noexcept /*override*/ +{ + // TODO: Implement custom URI handling. +} - void WinRTHttpResource::AddRequestBodyHandler(shared_ptr requestBodyHandler) noexcept /*override*/ - { - m_requestBodyHandler = weak_ptr(requestBodyHandler); - } +void WinRTHttpResource::AddRequestBodyHandler(shared_ptr requestBodyHandler) noexcept /*override*/ +{ + m_requestBodyHandler = weak_ptr(requestBodyHandler); +} - void WinRTHttpResource::AddResponseHandler(shared_ptr responseHandler) noexcept /*override*/ - { - m_responseHandler = weak_ptr(responseHandler); - } +void WinRTHttpResource::AddResponseHandler(shared_ptr responseHandler) noexcept /*override*/ +{ + m_responseHandler = weak_ptr(responseHandler); +} #pragma endregion IHttpModuleProxy @@ -727,46 +696,45 @@ namespace Microsoft::React::Networking { #pragma region IHttpResource - /*static*/ shared_ptr IHttpResource::Make( - winrt::Windows::Foundation::IInspectable const& inspectableProperties) noexcept { - using namespace winrt::Microsoft::ReactNative; - using winrt::Windows::Web::Http::HttpClient; +/*static*/ shared_ptr IHttpResource::Make( + winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept { + using namespace winrt::Microsoft::ReactNative; + using winrt::Windows::Web::Http::HttpClient; - auto redirFilter = winrt::make(); - HttpClient client; + auto redirFilter = winrt::make(); + HttpClient client; - if (static_cast(GetRuntimeOptionInt("Http.OriginPolicy")) == OriginPolicy::None) { - client = HttpClient{ redirFilter }; - } - else { - auto globalOrigin = GetRuntimeOptionString("Http.GlobalOrigin"); - OriginPolicyHttpFilter::SetStaticOrigin(std::move(globalOrigin)); - auto opFilter = winrt::make(redirFilter); - redirFilter.as()->SetRedirectSource(opFilter.as()); + if (static_cast(GetRuntimeOptionInt("Http.OriginPolicy")) == OriginPolicy::None) { + client = HttpClient{redirFilter}; + } else { + auto globalOrigin = GetRuntimeOptionString("Http.GlobalOrigin"); + OriginPolicyHttpFilter::SetStaticOrigin(std::move(globalOrigin)); + auto opFilter = winrt::make(redirFilter); + redirFilter.as()->SetRedirectSource(opFilter.as()); - client = HttpClient{ opFilter }; - } - - auto result = std::make_shared(std::move(client)); + client = HttpClient{opFilter}; + } - // Allow redirect filter to create requests based on the resource's state - redirFilter.as()->SetRequestFactory(weak_ptr{result}); + auto result = std::make_shared(std::move(client)); - // Register resource as HTTP module proxy. - if (inspectableProperties) { - auto propId = ReactPropertyId>>{ L"HttpModule.Proxy" }; - auto propBag = ReactPropertyBag{ inspectableProperties.try_as() }; - auto moduleProxy = weak_ptr{ result }; - propBag.Set(propId, std::move(moduleProxy)); - } + // Allow redirect filter to create requests based on the resource's state + redirFilter.as()->SetRequestFactory(weak_ptr{result}); - return result; + // Register resource as HTTP module proxy. + if (inspectableProperties) { + auto propId = ReactPropertyId>>{L"HttpModule.Proxy"}; + auto propBag = ReactPropertyBag{inspectableProperties.try_as()}; + auto moduleProxy = weak_ptr{result}; + propBag.Set(propId, std::move(moduleProxy)); } - /*static*/ shared_ptr IHttpResource::Make() noexcept { - auto inspectableProperties = IInspectable{ nullptr }; - return Make(inspectableProperties); - } + return result; +} + +/*static*/ shared_ptr IHttpResource::Make() noexcept { + auto inspectableProperties = IInspectable{nullptr}; + return Make(inspectableProperties); +} #pragma endregion IHttpResource diff --git a/vnext/Shared/Networking/WinRTHttpResource.h b/vnext/Shared/Networking/WinRTHttpResource.h index 76224ea6f63..f153098c376 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.h +++ b/vnext/Shared/Networking/WinRTHttpResource.h @@ -35,32 +35,32 @@ class WinRTHttpResource : public IHttpResource, std::function m_onDataProgress; std::function m_onComplete; - // Used for IHttpModuleProxy - std::weak_ptr m_uriHandler; - std::weak_ptr m_requestBodyHandler; - std::weak_ptr m_responseHandler; + // Used for IHttpModuleProxy + std::weak_ptr m_uriHandler; + std::weak_ptr m_requestBodyHandler; + std::weak_ptr m_responseHandler; - void TrackResponse(int64_t requestId, ResponseOperation response) noexcept; + void TrackResponse(int64_t requestId, ResponseOperation response) noexcept; - void UntrackResponse(int64_t requestId) noexcept; + void UntrackResponse(int64_t requestId) noexcept; winrt::fire_and_forget PerformSendRequest( winrt::Windows::Web::Http::HttpMethod &&method, winrt::Windows::Foundation::Uri &&uri, winrt::Windows::Foundation::IInspectable const &args) noexcept; - public: - WinRTHttpResource() noexcept; + public: + WinRTHttpResource() noexcept; - WinRTHttpResource(winrt::Windows::Web::Http::IHttpClient&& client) noexcept; + 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::IAsyncOperation CreateRequest( + winrt::Windows::Web::Http::HttpMethod &&method, + winrt::Windows::Foundation::Uri &&uri, winrt::Windows::Foundation::Collections::IMap - props) noexcept override; + props) noexcept override; #pragma endregion IWinRTHttpRequestFactory @@ -76,19 +76,19 @@ class WinRTHttpResource : public IHttpResource, #pragma region IHttpResource - void SendRequest( - std::string&& method, - std::string&& url, + void SendRequest( + std::string &&method, + std::string &&url, int64_t requestId, - Headers&& headers, - folly::dynamic&& data, - std::string&& responseType, + Headers &&headers, + folly::dynamic &&data, + std::string &&responseType, bool useIncrementalUpdates, int64_t timeout, bool withCredentials, - std::function&& callback) noexcept override; - void AbortRequest(int64_t requestId) noexcept override; - void ClearCookies() noexcept override; + std::function &&callback) noexcept override; + void AbortRequest(int64_t requestId) noexcept override; + void ClearCookies() noexcept override; void SetOnRequestSuccess(std::function &&handler) noexcept override; void SetOnResponse(std::function &&handler) noexcept override; @@ -107,13 +107,13 @@ class WinRTHttpResource : public IHttpResource, #pragma region IHttpModuleProxy - void AddUriHandler(std::shared_ptr uriHandler) noexcept override; + void AddUriHandler(std::shared_ptr uriHandler) noexcept override; - void AddRequestBodyHandler(std::shared_ptr requestBodyHandler) noexcept override; + void AddRequestBodyHandler(std::shared_ptr requestBodyHandler) noexcept override; - void AddResponseHandler(std::shared_ptr responseHandler) noexcept override; + void AddResponseHandler(std::shared_ptr responseHandler) noexcept override; #pragma endregion IHttpModuleProxy - }; +}; } // namespace Microsoft::React::Networking diff --git a/vnext/Shared/OInstance.cpp b/vnext/Shared/OInstance.cpp index ea2874903bf..327e7041218 100644 --- a/vnext/Shared/OInstance.cpp +++ b/vnext/Shared/OInstance.cpp @@ -71,187 +71,185 @@ using winrt::Microsoft::ReactNative::ReactPropertyBagHelper; namespace Microsoft::React { - /*extern*/ std::unique_ptr CreateHttpModule( - winrt::Windows::Foundation::IInspectable const& inspectableProperties) noexcept { - if (GetRuntimeOptionBool("Http.UseMonolithicModule")) { - return std::make_unique(); - } - else { - return std::make_unique(inspectableProperties); - } +/*extern*/ std::unique_ptr CreateHttpModule( + winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept { + if (GetRuntimeOptionBool("Http.UseMonolithicModule")) { + return std::make_unique(); + } else { + return std::make_unique(inspectableProperties); } +} } // namespace Microsoft::React namespace facebook { - namespace react { - - namespace { - - class OJSIExecutorFactory: public JSExecutorFactory { - public: - std::unique_ptr createJSExecutor( - std::shared_ptr delegate, - std::shared_ptr jsQueue) override { - Logger logger; - if (loggingHook_) { - // TODO :: Ensure the logLevels are mapped properly. - logger = [loggingHook = std::move(loggingHook_)](const std::string& message, unsigned int logLevel) { - loggingHook(static_cast(logLevel), message.c_str()); - }; - } - else { - logger = [loggingHook = std::move(loggingHook_)](const std::string& /*message*/, unsigned int /*logLevel*/) {}; - } - bindNativeLogger(*runtimeHolder_->getRuntime(), logger); - - auto turboModuleManager = std::make_shared(turboModuleRegistry_, jsCallInvoker_); - - // TODO: The binding here should also add the proxys that convert cxxmodules into turbomodules - // [vmoroz] Note, that we must not use the RN TurboCxxModule.h code because it uses global LongLivedObjectCollection - // instance that prevents us from using multiple RN instance in the same process. - auto binding = [turboModuleManager](const std::string& name) -> std::shared_ptr { - return turboModuleManager->getModule(name); - }; - - TurboModuleBinding::install(*runtimeHolder_->getRuntime(), std::function(binding), longLivedObjectCollection_); - - // init TurboModule - for (const auto& moduleName : turboModuleManager->getEagerInitModuleNames()) { - turboModuleManager->getModule(moduleName); - } - - return std::make_unique( - runtimeHolder_->getRuntime(), - std::move(delegate), - JSIExecutor::defaultTimeoutInvoker, - [isProfiling = isProfilingEnabled_]([[maybe_unused]] jsi::Runtime& runtime) { -#ifdef ENABLE_JS_SYSTRACE_TO_ETW - facebook::react::tracing::initializeJSHooks(runtime, isProfiling); -#endif - }); - } - - OJSIExecutorFactory( - std::shared_ptr runtimeHolder, - NativeLoggingHook loggingHook, - std::shared_ptr turboModuleRegistry, - std::shared_ptr longLivedObjectCollection, - bool isProfilingEnabled, - std::shared_ptr jsCallInvoker) noexcept - : runtimeHolder_{ std::move(runtimeHolder) }, - loggingHook_{ std::move(loggingHook) }, - turboModuleRegistry_{ std::move(turboModuleRegistry) }, - longLivedObjectCollection_{ std::move(longLivedObjectCollection) }, - jsCallInvoker_{ std::move(jsCallInvoker) }, - isProfilingEnabled_{ isProfilingEnabled } {} - - private: - std::shared_ptr runtimeHolder_; - std::shared_ptr turboModuleRegistry_; - std::shared_ptr longLivedObjectCollection_; - std::shared_ptr jsCallInvoker_; - NativeLoggingHook loggingHook_; - bool isProfilingEnabled_; +namespace react { + +namespace { + +class OJSIExecutorFactory : public JSExecutorFactory { + public: + std::unique_ptr createJSExecutor( + std::shared_ptr delegate, + std::shared_ptr jsQueue) override { + Logger logger; + if (loggingHook_) { + // TODO :: Ensure the logLevels are mapped properly. + logger = [loggingHook = std::move(loggingHook_)](const std::string &message, unsigned int logLevel) { + loggingHook(static_cast(logLevel), message.c_str()); }; + } else { + logger = [loggingHook = std::move(loggingHook_)](const std::string & /*message*/, unsigned int /*logLevel*/) {}; + } + bindNativeLogger(*runtimeHolder_->getRuntime(), logger); - } // namespace + auto turboModuleManager = std::make_shared(turboModuleRegistry_, jsCallInvoker_); - void logMarker(const facebook::react::ReactMarker::ReactMarkerId /*id*/, const char* /*tag*/) {} + // TODO: The binding here should also add the proxys that convert cxxmodules into turbomodules + // [vmoroz] Note, that we must not use the RN TurboCxxModule.h code because it uses global LongLivedObjectCollection + // instance that prevents us from using multiple RN instance in the same process. + auto binding = [turboModuleManager](const std::string &name) -> std::shared_ptr { + return turboModuleManager->getModule(name); + }; - /*static*/ std::shared_ptr InstanceImpl::MakeNoBundle( - std::shared_ptr&& instance, - std::string&& jsBundleBasePath, - std::vector< - std::tuple>> - && cxxModules, - std::shared_ptr turboModuleRegistry, - std::shared_ptr longLivedObjectCollection, - std::unique_ptr&& callback, - std::shared_ptr jsQueue, - std::shared_ptr nativeQueue, - std::shared_ptr devSettings, - std::shared_ptr devManager) noexcept { - auto inner = std::shared_ptr(new InstanceImpl( - std::move(instance), - std::move(jsBundleBasePath), - std::move(cxxModules), - std::move(turboModuleRegistry), - std::move(longLivedObjectCollection), - std::move(callback), - std::move(jsQueue), - std::move(nativeQueue), - std::move(devSettings), - std::move(devManager))); - - inner->RegisterForReloadIfNecessary(); - - return inner; - } + TurboModuleBinding::install(*runtimeHolder_->getRuntime(), std::function(binding), longLivedObjectCollection_); - /*static*/ std::shared_ptr InstanceImpl::MakeAndLoadBundle( - std::shared_ptr&& instance, - std::string&& jsBundleBasePath, - std::string&& jsBundleRelativePath, - std::vector< - std::tuple>> - && cxxModules, - std::shared_ptr turboModuleRegistry, - std::unique_ptr&& callback, - std::shared_ptr jsQueue, - std::shared_ptr nativeQueue, - std::shared_ptr devSettings, - std::shared_ptr devManager) noexcept { - auto inner = std::shared_ptr(new InstanceImpl( - std::move(instance), - std::move(jsBundleBasePath), - std::move(cxxModules), - std::move(turboModuleRegistry), - nullptr, - std::move(callback), - std::move(jsQueue), - std::move(nativeQueue), - std::move(devSettings), - std::move(devManager))); - - inner->loadBundle(std::move(jsBundleRelativePath)); - inner->RegisterForReloadIfNecessary(); - - return inner; + // init TurboModule + for (const auto &moduleName : turboModuleManager->getEagerInitModuleNames()) { + turboModuleManager->getModule(moduleName); } - void InstanceImpl::SetInError() noexcept { - m_isInError = true; - } + return std::make_unique( + runtimeHolder_->getRuntime(), + std::move(delegate), + JSIExecutor::defaultTimeoutInvoker, + [isProfiling = isProfilingEnabled_]([[maybe_unused]] jsi::Runtime &runtime) { +#ifdef ENABLE_JS_SYSTRACE_TO_ETW + facebook::react::tracing::initializeJSHooks(runtime, isProfiling); +#endif + }); + } - namespace { - bool shouldStartHermesInspector(DevSettings& devSettings) { - bool isHermes = - ((devSettings.jsiEngineOverride == JSIEngineOverride::Hermes) || - (devSettings.jsiEngineOverride == JSIEngineOverride::Default && devSettings.jsiRuntimeHolder && - devSettings.jsiRuntimeHolder->getRuntimeType() == facebook::react::JSIEngineOverride::Hermes)); - - if (isHermes && devSettings.useDirectDebugger && !devSettings.useWebDebugger) - return true; - else - return false; - } - } // namespace - - InstanceImpl::InstanceImpl( - std::shared_ptr&& instance, - std::string&& jsBundleBasePath, - std::vector< - std::tuple>> - && cxxModules, + OJSIExecutorFactory( + std::shared_ptr runtimeHolder, + NativeLoggingHook loggingHook, std::shared_ptr turboModuleRegistry, - std::shared_ptr longLivedObjectCollection, - std::unique_ptr&& callback, - std::shared_ptr jsQueue, - std::shared_ptr nativeQueue, - std::shared_ptr devSettings, - std::shared_ptr devManager) - : m_turboModuleRegistry(std::move(turboModuleRegistry)), + std::shared_ptr longLivedObjectCollection, + bool isProfilingEnabled, + std::shared_ptr jsCallInvoker) noexcept + : runtimeHolder_{std::move(runtimeHolder)}, + loggingHook_{std::move(loggingHook)}, + turboModuleRegistry_{std::move(turboModuleRegistry)}, + longLivedObjectCollection_{std::move(longLivedObjectCollection)}, + jsCallInvoker_{std::move(jsCallInvoker)}, + isProfilingEnabled_{isProfilingEnabled} {} + + private: + std::shared_ptr runtimeHolder_; + std::shared_ptr turboModuleRegistry_; + std::shared_ptr longLivedObjectCollection_; + std::shared_ptr jsCallInvoker_; + NativeLoggingHook loggingHook_; + bool isProfilingEnabled_; +}; + +} // namespace + +void logMarker(const facebook::react::ReactMarker::ReactMarkerId /*id*/, const char * /*tag*/) {} + +/*static*/ std::shared_ptr InstanceImpl::MakeNoBundle( + std::shared_ptr &&instance, + std::string &&jsBundleBasePath, + std::vector< + std::tuple>> + &&cxxModules, + std::shared_ptr turboModuleRegistry, + std::shared_ptr longLivedObjectCollection, + std::unique_ptr &&callback, + std::shared_ptr jsQueue, + std::shared_ptr nativeQueue, + std::shared_ptr devSettings, + std::shared_ptr devManager) noexcept { + auto inner = std::shared_ptr(new InstanceImpl( + std::move(instance), + std::move(jsBundleBasePath), + std::move(cxxModules), + std::move(turboModuleRegistry), + std::move(longLivedObjectCollection), + std::move(callback), + std::move(jsQueue), + std::move(nativeQueue), + std::move(devSettings), + std::move(devManager))); + + inner->RegisterForReloadIfNecessary(); + + return inner; +} + +/*static*/ std::shared_ptr InstanceImpl::MakeAndLoadBundle( + std::shared_ptr &&instance, + std::string &&jsBundleBasePath, + std::string &&jsBundleRelativePath, + std::vector< + std::tuple>> + &&cxxModules, + std::shared_ptr turboModuleRegistry, + std::unique_ptr &&callback, + std::shared_ptr jsQueue, + std::shared_ptr nativeQueue, + std::shared_ptr devSettings, + std::shared_ptr devManager) noexcept { + auto inner = std::shared_ptr(new InstanceImpl( + std::move(instance), + std::move(jsBundleBasePath), + std::move(cxxModules), + std::move(turboModuleRegistry), + nullptr, + std::move(callback), + std::move(jsQueue), + std::move(nativeQueue), + std::move(devSettings), + std::move(devManager))); + + inner->loadBundle(std::move(jsBundleRelativePath)); + inner->RegisterForReloadIfNecessary(); + + return inner; +} + +void InstanceImpl::SetInError() noexcept { + m_isInError = true; +} + +namespace { +bool shouldStartHermesInspector(DevSettings &devSettings) { + bool isHermes = + ((devSettings.jsiEngineOverride == JSIEngineOverride::Hermes) || + (devSettings.jsiEngineOverride == JSIEngineOverride::Default && devSettings.jsiRuntimeHolder && + devSettings.jsiRuntimeHolder->getRuntimeType() == facebook::react::JSIEngineOverride::Hermes)); + + if (isHermes && devSettings.useDirectDebugger && !devSettings.useWebDebugger) + return true; + else + return false; +} +} // namespace + +InstanceImpl::InstanceImpl( + std::shared_ptr &&instance, + std::string &&jsBundleBasePath, + std::vector< + std::tuple>> + &&cxxModules, + std::shared_ptr turboModuleRegistry, + std::shared_ptr longLivedObjectCollection, + std::unique_ptr &&callback, + std::shared_ptr jsQueue, + std::shared_ptr nativeQueue, + std::shared_ptr devSettings, + std::shared_ptr devManager) + : m_turboModuleRegistry(std::move(turboModuleRegistry)), m_longLivedObjectCollection(std::move(longLivedObjectCollection)), m_jsThread(std::move(jsQueue)), m_nativeQueue(nativeQueue), @@ -259,430 +257,421 @@ namespace facebook { m_devSettings(std::move(devSettings)), m_devManager(std::move(devManager)), m_innerInstance(std::move(instance)) { - // Temp set the logmarker here - facebook::react::ReactMarker::logTaggedMarker = logMarker; + // Temp set the logmarker here + facebook::react::ReactMarker::logTaggedMarker = logMarker; #ifdef ENABLE_ETW_TRACING - // TODO :: Find a better place to initialize ETW once per process. - facebook::react::tracing::initializeETW(); + // TODO :: Find a better place to initialize ETW once per process. + facebook::react::tracing::initializeETW(); #endif - if (shouldStartHermesInspector(*m_devSettings)) { - m_devManager->EnsureHermesInspector(m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort); - } + if (shouldStartHermesInspector(*m_devSettings)) { + m_devManager->EnsureHermesInspector(m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort); + } - // Default (common) NativeModules - auto modules = GetDefaultNativeModules(nativeQueue); + // Default (common) NativeModules + auto modules = GetDefaultNativeModules(nativeQueue); - // Add app provided modules. - for (auto& cxxModule : cxxModules) { - modules.push_back(std::make_unique( - m_innerInstance, move(std::get<0>(cxxModule)), move(std::get<1>(cxxModule)), move(std::get<2>(cxxModule)))); - } - m_moduleRegistry = std::make_shared(std::move(modules)); - - // Choose JSExecutor - std::shared_ptr jsef; - if (m_devSettings->useWebDebugger) { - try { - auto jseFunc = m_devManager->LoadJavaScriptInProxyMode(*m_devSettings, [weakthis = weak_from_this()]() { - if (auto strongThis = weakthis.lock()) { - strongThis->SetInError(); - } - }); - - if ((jseFunc == nullptr) || m_isInError) { - m_devSettings->errorCallback("Failed to create JavaScript Executor."); - return; - } - - jsef = std::make_shared(std::move(jseFunc)); - } - catch (std::exception& e) { - m_devSettings->errorCallback(e.what()); - return; + // Add app provided modules. + for (auto &cxxModule : cxxModules) { + modules.push_back(std::make_unique( + m_innerInstance, move(std::get<0>(cxxModule)), move(std::get<1>(cxxModule)), move(std::get<2>(cxxModule)))); + } + m_moduleRegistry = std::make_shared(std::move(modules)); + + // Choose JSExecutor + std::shared_ptr jsef; + if (m_devSettings->useWebDebugger) { + try { + auto jseFunc = m_devManager->LoadJavaScriptInProxyMode(*m_devSettings, [weakthis = weak_from_this()]() { + if (auto strongThis = weakthis.lock()) { + strongThis->SetInError(); } + }); + + if ((jseFunc == nullptr) || m_isInError) { + m_devSettings->errorCallback("Failed to create JavaScript Executor."); + return; } - else { - if (m_devSettings->useFastRefresh || m_devSettings->liveReloadCallback) { - Microsoft::ReactNative::PackagerConnection::CreateOrReusePackagerConnection(*m_devSettings); - } - // If the consumer gives us a JSI runtime, then use it. - if (m_devSettings->jsiRuntimeHolder) { - assert(m_devSettings->jsiEngineOverride == JSIEngineOverride::Default); - jsef = std::make_shared( - m_devSettings->jsiRuntimeHolder, - m_devSettings->loggingCallback, - m_turboModuleRegistry, - m_longLivedObjectCollection, - !m_devSettings->useFastRefresh, - m_innerInstance->getJSCallInvoker()); - } - else { - assert(m_devSettings->jsiEngineOverride != JSIEngineOverride::Default); - switch (m_devSettings->jsiEngineOverride) { - case JSIEngineOverride::Hermes: - m_devSettings->jsiRuntimeHolder = std::make_shared(m_devSettings, m_jsThread); - break; - case JSIEngineOverride::V8: { + jsef = std::make_shared(std::move(jseFunc)); + } catch (std::exception &e) { + m_devSettings->errorCallback(e.what()); + return; + } + } else { + if (m_devSettings->useFastRefresh || m_devSettings->liveReloadCallback) { + Microsoft::ReactNative::PackagerConnection::CreateOrReusePackagerConnection(*m_devSettings); + } + + // If the consumer gives us a JSI runtime, then use it. + if (m_devSettings->jsiRuntimeHolder) { + assert(m_devSettings->jsiEngineOverride == JSIEngineOverride::Default); + jsef = std::make_shared( + m_devSettings->jsiRuntimeHolder, + m_devSettings->loggingCallback, + m_turboModuleRegistry, + m_longLivedObjectCollection, + !m_devSettings->useFastRefresh, + m_innerInstance->getJSCallInvoker()); + } else { + assert(m_devSettings->jsiEngineOverride != JSIEngineOverride::Default); + switch (m_devSettings->jsiEngineOverride) { + case JSIEngineOverride::Hermes: + m_devSettings->jsiRuntimeHolder = std::make_shared(m_devSettings, m_jsThread); + break; + case JSIEngineOverride::V8: { #if defined(USE_V8) - std::unique_ptr scriptStore = nullptr; - std::unique_ptr preparedScriptStore = nullptr; + std::unique_ptr scriptStore = nullptr; + std::unique_ptr preparedScriptStore = nullptr; - char tempPath[MAX_PATH]; - if (GetTempPathA(MAX_PATH, tempPath)) { - preparedScriptStore = std::make_unique(tempPath); - } + char tempPath[MAX_PATH]; + if (GetTempPathA(MAX_PATH, tempPath)) { + preparedScriptStore = std::make_unique(tempPath); + } - m_devSettings->jsiRuntimeHolder = std::make_shared( + m_devSettings->jsiRuntimeHolder = std::make_shared( m_devSettings, m_jsThread, std::move(scriptStore), std::move(preparedScriptStore)); - break; + break; #else - assert(false); // V8 is not available in this build, fallthrough - [[fallthrough]]; + assert(false); // V8 is not available in this build, fallthrough + [[fallthrough]]; #endif - } - case JSIEngineOverride::V8NodeApi: { + } + case JSIEngineOverride::V8NodeApi: { #if defined(USE_V8) - std::unique_ptr preparedScriptStore; + std::unique_ptr preparedScriptStore; - wchar_t tempPath[MAX_PATH]; - if (GetTempPathW(static_cast(std::size(tempPath)), tempPath)) { - preparedScriptStore = + wchar_t tempPath[MAX_PATH]; + if (GetTempPathW(static_cast(std::size(tempPath)), tempPath)) { + preparedScriptStore = std::make_unique(winrt::to_string(tempPath)); - } + } - if (!preparedScriptStore) { - if (m_devSettings->errorCallback) - m_devSettings->errorCallback("Could not initialize prepared script store"); + if (!preparedScriptStore) { + if (m_devSettings->errorCallback) + m_devSettings->errorCallback("Could not initialize prepared script store"); - break; - } + break; + } - m_devSettings->jsiRuntimeHolder = make_shared( + m_devSettings->jsiRuntimeHolder = make_shared( m_devSettings, m_jsThread, nullptr /*scriptStore*/, std::move(preparedScriptStore)); - break; + break; #else - if (m_devSettings->errorCallback) - m_devSettings->errorCallback("JSI/V8/NAPI engine is not available in this build"); - assert(false); - [[fallthrough]]; + if (m_devSettings->errorCallback) + m_devSettings->errorCallback("JSI/V8/NAPI engine is not available in this build"); + assert(false); + [[fallthrough]]; #endif - } - case JSIEngineOverride::Chakra: - case JSIEngineOverride::ChakraCore: - default: // TODO: Add other engines once supported - m_devSettings->jsiRuntimeHolder = - std::make_shared(m_devSettings, m_jsThread, nullptr, nullptr); - break; - } - jsef = std::make_shared( - m_devSettings->jsiRuntimeHolder, - m_devSettings->loggingCallback, - m_turboModuleRegistry, - m_longLivedObjectCollection, - !m_devSettings->useFastRefresh, - m_innerInstance->getJSCallInvoker()); } + case JSIEngineOverride::Chakra: + case JSIEngineOverride::ChakraCore: + default: // TODO: Add other engines once supported + m_devSettings->jsiRuntimeHolder = + std::make_shared(m_devSettings, m_jsThread, nullptr, nullptr); + break; } - - m_innerInstance->initializeBridge(std::move(callback), jsef, m_jsThread, m_moduleRegistry); - - // All JSI runtimes do support host objects and hence the native modules - // proxy. - const bool isNativeModulesProxyAvailable = ((m_devSettings->jsiRuntimeHolder != nullptr) || - (m_devSettings->jsiEngineOverride != JSIEngineOverride::Default)) && - !m_devSettings->useWebDebugger; - if (!isNativeModulesProxyAvailable) { - folly::dynamic configArray = folly::dynamic::array; - for (auto const& moduleName : m_moduleRegistry->moduleNames()) { - auto moduleConfig = m_moduleRegistry->getConfig(moduleName); - configArray.push_back(moduleConfig ? std::move(moduleConfig->config) : nullptr); - } - - folly::dynamic configs = folly::dynamic::object("remoteModuleConfig", std::move(configArray)); - m_innerInstance->setGlobalVariable( - "__fbBatchedBridgeConfig", std::make_unique(folly::toJson(configs))); - } - } - - void InstanceImpl::loadBundle(std::string&& jsBundleRelativePath) { - loadBundleInternal(std::move(jsBundleRelativePath), /*synchronously:*/ false); + jsef = std::make_shared( + m_devSettings->jsiRuntimeHolder, + m_devSettings->loggingCallback, + m_turboModuleRegistry, + m_longLivedObjectCollection, + !m_devSettings->useFastRefresh, + m_innerInstance->getJSCallInvoker()); } + } - void InstanceImpl::loadBundleSync(std::string&& jsBundleRelativePath) { - loadBundleInternal(std::move(jsBundleRelativePath), /*synchronously:*/ true); + m_innerInstance->initializeBridge(std::move(callback), jsef, m_jsThread, m_moduleRegistry); + + // All JSI runtimes do support host objects and hence the native modules + // proxy. + const bool isNativeModulesProxyAvailable = ((m_devSettings->jsiRuntimeHolder != nullptr) || + (m_devSettings->jsiEngineOverride != JSIEngineOverride::Default)) && + !m_devSettings->useWebDebugger; + if (!isNativeModulesProxyAvailable) { + folly::dynamic configArray = folly::dynamic::array; + for (auto const &moduleName : m_moduleRegistry->moduleNames()) { + auto moduleConfig = m_moduleRegistry->getConfig(moduleName); + configArray.push_back(moduleConfig ? std::move(moduleConfig->config) : nullptr); } - // Note: Based on - // https://github.com/facebook/react-native/blob/24d91268b64c7abbd4b26547ffcc663dc90ec5e7/ReactCommon/cxxreact/Instance.cpp#L112 - bool isHBCBundle(const std::string& bundle) { - static uint32_t constexpr HBCBundleMagicNumber = 0xffe7c3c3; - - // Note:: Directly access the pointer to avoid copy/length-check. It matters as this string contains the bundle which - // can be potentially huge. - // https://herbsutter.com/2008/04/07/cringe-not-vectors-are-guaranteed-to-be-contiguous/#comment-483 - auto header = reinterpret_cast(&bundle[0]); - if (HBCBundleMagicNumber == header->magic32.value) { - return true; - } - else { - return false; - } - } + folly::dynamic configs = folly::dynamic::object("remoteModuleConfig", std::move(configArray)); + m_innerInstance->setGlobalVariable( + "__fbBatchedBridgeConfig", std::make_unique(folly::toJson(configs))); + } +} + +void InstanceImpl::loadBundle(std::string &&jsBundleRelativePath) { + loadBundleInternal(std::move(jsBundleRelativePath), /*synchronously:*/ false); +} + +void InstanceImpl::loadBundleSync(std::string &&jsBundleRelativePath) { + loadBundleInternal(std::move(jsBundleRelativePath), /*synchronously:*/ true); +} + +// Note: Based on +// https://github.com/facebook/react-native/blob/24d91268b64c7abbd4b26547ffcc663dc90ec5e7/ReactCommon/cxxreact/Instance.cpp#L112 +bool isHBCBundle(const std::string &bundle) { + static uint32_t constexpr HBCBundleMagicNumber = 0xffe7c3c3; + + // Note:: Directly access the pointer to avoid copy/length-check. It matters as this string contains the bundle which + // can be potentially huge. + // https://herbsutter.com/2008/04/07/cringe-not-vectors-are-guaranteed-to-be-contiguous/#comment-483 + auto header = reinterpret_cast(&bundle[0]); + if (HBCBundleMagicNumber == header->magic32.value) { + return true; + } else { + return false; + } +} - void InstanceImpl::loadBundleInternal(std::string&& jsBundleRelativePath, bool synchronously) { - try { - if (m_devSettings->useWebDebugger || m_devSettings->liveReloadCallback != nullptr || - m_devSettings->useFastRefresh) { - // First attempt to get download the Js locally, to catch any bundling - // errors before attempting to load the actual script. +void InstanceImpl::loadBundleInternal(std::string &&jsBundleRelativePath, bool synchronously) { + try { + if (m_devSettings->useWebDebugger || m_devSettings->liveReloadCallback != nullptr || + m_devSettings->useFastRefresh) { + // First attempt to get download the Js locally, to catch any bundling + // errors before attempting to load the actual script. - uint32_t hermesBytecodeVersion = 0; + uint32_t hermesBytecodeVersion = 0; #if defined(USE_HERMES) && defined(ENABLE_DEVSERVER_HBCBUNDLES) - hermesBytecodeVersion = ::hermes::hbc::BYTECODE_VERSION; + hermesBytecodeVersion = ::hermes::hbc::BYTECODE_VERSION; #endif - auto [jsBundleString, success] = Microsoft::ReactNative::GetJavaScriptFromServer( - m_devSettings->sourceBundleHost, - m_devSettings->sourceBundlePort, - m_devSettings->debugBundlePath.empty() ? jsBundleRelativePath : m_devSettings->debugBundlePath, - m_devSettings->platformName, - true /* dev */, - m_devSettings->useFastRefresh, - m_devSettings->inlineSourceMap, - hermesBytecodeVersion); + auto [jsBundleString, success] = Microsoft::ReactNative::GetJavaScriptFromServer( + m_devSettings->sourceBundleHost, + m_devSettings->sourceBundlePort, + m_devSettings->debugBundlePath.empty() ? jsBundleRelativePath : m_devSettings->debugBundlePath, + m_devSettings->platformName, + true /* dev */, + m_devSettings->useFastRefresh, + m_devSettings->inlineSourceMap, + hermesBytecodeVersion); - if (!success) { - m_devManager->UpdateBundleStatus(false, -1); - m_devSettings->errorCallback(jsBundleString); - return; - } + if (!success) { + m_devManager->UpdateBundleStatus(false, -1); + m_devSettings->errorCallback(jsBundleString); + return; + } - int64_t currentTimeInMilliSeconds = - std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) - .count(); - m_devManager->UpdateBundleStatus(true, currentTimeInMilliSeconds); + int64_t currentTimeInMilliSeconds = + std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) + .count(); + m_devManager->UpdateBundleStatus(true, currentTimeInMilliSeconds); - auto bundleUrl = DevServerHelper::get_BundleUrl( - m_devSettings->sourceBundleHost, - m_devSettings->sourceBundlePort, - m_devSettings->debugBundlePath.empty() ? jsBundleRelativePath : m_devSettings->debugBundlePath, - m_devSettings->platformName, - /*dev*/ true, - /*hot*/ false, - m_devSettings->inlineSourceMap, - hermesBytecodeVersion); - - // This code is based on the HBC Bundle integration on Android - // Ref: - // https://github.com/facebook/react-native/blob/24d91268b64c7abbd4b26547ffcc663dc90ec5e7/ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.cpp#L231 - if (isHBCBundle(jsBundleString)) { - auto script = std::make_unique(jsBundleString, false); - const char* buffer = script->c_str(); - uint32_t bufferLength = (uint32_t)script->size(); - - // Please refer the code here for details on the file format: - // https://github.com/facebook/metro/blob/b1bacf52070be62872d6bd3420f37a4405ed34e6/packages/metro/src/lib/bundleToBytecode.js#L29 - // Essentially, there is an 8 byte long file header with 4 bytes of a magic number followed by 4 bytes to encode - // the number of modules.The module buffers follows, each one starts with 4 byte header which encodes module - // length.A properly formatted HBCB should have at least 8 bytes.. - uint32_t offset = 8; + auto bundleUrl = DevServerHelper::get_BundleUrl( + m_devSettings->sourceBundleHost, + m_devSettings->sourceBundlePort, + m_devSettings->debugBundlePath.empty() ? jsBundleRelativePath : m_devSettings->debugBundlePath, + m_devSettings->platformName, + /*dev*/ true, + /*hot*/ false, + m_devSettings->inlineSourceMap, + hermesBytecodeVersion); + + // This code is based on the HBC Bundle integration on Android + // Ref: + // https://github.com/facebook/react-native/blob/24d91268b64c7abbd4b26547ffcc663dc90ec5e7/ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.cpp#L231 + if (isHBCBundle(jsBundleString)) { + auto script = std::make_unique(jsBundleString, false); + const char *buffer = script->c_str(); + uint32_t bufferLength = (uint32_t)script->size(); + + // Please refer the code here for details on the file format: + // https://github.com/facebook/metro/blob/b1bacf52070be62872d6bd3420f37a4405ed34e6/packages/metro/src/lib/bundleToBytecode.js#L29 + // Essentially, there is an 8 byte long file header with 4 bytes of a magic number followed by 4 bytes to encode + // the number of modules.The module buffers follows, each one starts with 4 byte header which encodes module + // length.A properly formatted HBCB should have at least 8 bytes.. + uint32_t offset = 8; #define __SAFEADD__(s1, s2, t) \ if (!msl::utilities::SafeAdd(s1, s2, t)) \ break; - while (offset < bufferLength) { - uint32_t segment; - __SAFEADD__(offset, 4, segment) - uint32_t moduleLength = (bufferLength < segment) ? 0 : *(((uint32_t*)buffer) + offset / 4); - - // Early break if the module length is computed as 0.. as the segment start may be overflowing the buffer. - if (moduleLength == 0) - break; - - uint32_t segmentEnd; - __SAFEADD__(moduleLength, segment, segmentEnd) - // Early break if the segment overflows beyond the buffer. This is unlikely for a properly formatted - // HBCB though. - if (segmentEnd > bufferLength) - break; - - m_innerInstance->loadScriptFromString( - std::make_unique(std::string(buffer + segment, buffer + segmentEnd)), - bundleUrl, - false); - - // Aligned at 4 byte boundary. - offset += ((moduleLength + 3) & ~3) + 4; - } -#undef __SAFEADD__ - } - else { - // Remote debug executor loads script from a Uri, rather than taking the actual bundle string - m_innerInstance->loadScriptFromString( - std::make_unique(m_devSettings->useWebDebugger ? bundleUrl : jsBundleString), + while (offset < bufferLength) { + uint32_t segment; + __SAFEADD__(offset, 4, segment) + uint32_t moduleLength = (bufferLength < segment) ? 0 : *(((uint32_t *)buffer) + offset / 4); + + // Early break if the module length is computed as 0.. as the segment start may be overflowing the buffer. + if (moduleLength == 0) + break; + + uint32_t segmentEnd; + __SAFEADD__(moduleLength, segment, segmentEnd) + // Early break if the segment overflows beyond the buffer. This is unlikely for a properly formatted + // HBCB though. + if (segmentEnd > bufferLength) + break; + + m_innerInstance->loadScriptFromString( + std::make_unique(std::string(buffer + segment, buffer + segmentEnd)), bundleUrl, - synchronously); - } - } - else { -#if (defined(_MSC_VER) && !defined(WINRT)) - std::string bundlePath = (fs::path(m_devSettings->bundleRootPath) / jsBundleRelativePath).string(); - auto bundleString = FileMappingBigString::fromPath(bundlePath); -#else - std::string bundlePath; - if (m_devSettings->bundleRootPath._Starts_with("resource://")) { - auto uri = winrt::Windows::Foundation::Uri( - winrt::to_hstring(m_devSettings->bundleRootPath), winrt::to_hstring(jsBundleRelativePath)); - bundlePath = winrt::to_string(uri.ToString()); - } - else { - bundlePath = (fs::path(m_devSettings->bundleRootPath) / (jsBundleRelativePath + ".bundle")).string(); - } + false); - auto bundleString = std::make_unique<::Microsoft::ReactNative::StorageFileBigString>(bundlePath); -#endif - m_innerInstance->loadScriptFromString(std::move(bundleString), std::move(jsBundleRelativePath), synchronously); + // Aligned at 4 byte boundary. + offset += ((moduleLength + 3) & ~3) + 4; } +#undef __SAFEADD__ + } else { + // Remote debug executor loads script from a Uri, rather than taking the actual bundle string + m_innerInstance->loadScriptFromString( + std::make_unique(m_devSettings->useWebDebugger ? bundleUrl : jsBundleString), + bundleUrl, + synchronously); } - catch (const std::exception& e) { - m_devSettings->errorCallback(e.what()); + } else { +#if (defined(_MSC_VER) && !defined(WINRT)) + std::string bundlePath = (fs::path(m_devSettings->bundleRootPath) / jsBundleRelativePath).string(); + auto bundleString = FileMappingBigString::fromPath(bundlePath); +#else + std::string bundlePath; + if (m_devSettings->bundleRootPath._Starts_with("resource://")) { + auto uri = winrt::Windows::Foundation::Uri( + winrt::to_hstring(m_devSettings->bundleRootPath), winrt::to_hstring(jsBundleRelativePath)); + bundlePath = winrt::to_string(uri.ToString()); + } else { + bundlePath = (fs::path(m_devSettings->bundleRootPath) / (jsBundleRelativePath + ".bundle")).string(); } - catch (const winrt::hresult_error& hrerr) { - auto error = fmt::format("[0x{:0>8x}] {}", static_cast(hrerr.code()), winrt::to_string(hrerr.message())); - m_devSettings->errorCallback(std::move(error)); - } + auto bundleString = std::make_unique<::Microsoft::ReactNative::StorageFileBigString>(bundlePath); +#endif + m_innerInstance->loadScriptFromString(std::move(bundleString), std::move(jsBundleRelativePath), synchronously); } + } catch (const std::exception &e) { + m_devSettings->errorCallback(e.what()); + } catch (const winrt::hresult_error &hrerr) { + auto error = fmt::format("[0x{:0>8x}] {}", static_cast(hrerr.code()), winrt::to_string(hrerr.message())); - InstanceImpl::~InstanceImpl() { - if (shouldStartHermesInspector(*m_devSettings) && m_devSettings->jsiRuntimeHolder) { - m_devSettings->jsiRuntimeHolder->teardown(); - } - m_nativeQueue->quitSynchronous(); - } + m_devSettings->errorCallback(std::move(error)); + } +} + +InstanceImpl::~InstanceImpl() { + if (shouldStartHermesInspector(*m_devSettings) && m_devSettings->jsiRuntimeHolder) { + m_devSettings->jsiRuntimeHolder->teardown(); + } + m_nativeQueue->quitSynchronous(); +} - std::vector> InstanceImpl::GetDefaultNativeModules( - std::shared_ptr nativeQueue) { - std::vector> modules; - auto transitionalProps{ ReactPropertyBagHelper::CreatePropertyBag() }; +std::vector> InstanceImpl::GetDefaultNativeModules( + std::shared_ptr nativeQueue) { + std::vector> modules; + auto transitionalProps{ReactPropertyBagHelper::CreatePropertyBag()}; #if (defined(_MSC_VER) && !defined(WINRT)) - modules.push_back(std::make_unique( - m_innerInstance, - Microsoft::React::GetHttpModuleName(), - [nativeQueue, transitionalProps]() -> std::unique_ptr { - return Microsoft::React::CreateHttpModule(transitionalProps); - }, - nativeQueue)); + modules.push_back(std::make_unique( + m_innerInstance, + Microsoft::React::GetHttpModuleName(), + [nativeQueue, transitionalProps]() -> std::unique_ptr { + return Microsoft::React::CreateHttpModule(transitionalProps); + }, + nativeQueue)); #endif - modules.push_back(std::make_unique( - m_innerInstance, - Microsoft::React::GetWebSocketModuleName(), - [nativeQueue, transitionalProps]() -> std::unique_ptr { - return Microsoft::React::CreateWebSocketModule(transitionalProps); - }, - nativeQueue)); - - // TODO: This is not included for UWP because we have a different module which - // is added later. However, this one is designed - // so that we can base a UWP version on it. We need to do that but is not high - // priority. + modules.push_back(std::make_unique( + m_innerInstance, + Microsoft::React::GetWebSocketModuleName(), + [nativeQueue, transitionalProps]() -> std::unique_ptr { + return Microsoft::React::CreateWebSocketModule(transitionalProps); + }, + nativeQueue)); + + // TODO: This is not included for UWP because we have a different module which + // is added later. However, this one is designed + // so that we can base a UWP version on it. We need to do that but is not high + // priority. #if (defined(_MSC_VER) && !defined(WINRT)) - modules.push_back(std::make_unique( - m_innerInstance, - "Timing", - [nativeQueue]() -> std::unique_ptr { return react::CreateTimingModule(nativeQueue); }, - nativeQueue)); + modules.push_back(std::make_unique( + m_innerInstance, + "Timing", + [nativeQueue]() -> std::unique_ptr { return react::CreateTimingModule(nativeQueue); }, + nativeQueue)); #endif - uint32_t hermesBytecodeVersion = 0; + uint32_t hermesBytecodeVersion = 0; #if defined(USE_HERMES) && defined(ENABLE_DEVSERVER_HBCBUNDLES) - hermesBytecodeVersion = ::hermes::hbc::BYTECODE_VERSION; + hermesBytecodeVersion = ::hermes::hbc::BYTECODE_VERSION; #endif - // TODO - Encapsulate this in a helpers, and make sure callers add it to their - // list - std::string bundleUrl = (m_devSettings->useWebDebugger || m_devSettings->liveReloadCallback) - ? DevServerHelper::get_BundleUrl( - m_devSettings->sourceBundleHost, - m_devSettings->sourceBundlePort, - m_devSettings->debugBundlePath, - m_devSettings->platformName, - true /*dev*/, - m_devSettings->useFastRefresh, - m_devSettings->inlineSourceMap, - hermesBytecodeVersion) - : std::string(); - modules.push_back(std::make_unique( - m_innerInstance, - facebook::react::SourceCodeModule::Name, - [bundleUrl]() -> std::unique_ptr { - return std::make_unique(bundleUrl); - }, - nativeQueue)); - - modules.push_back(std::make_unique( - m_innerInstance, - "ExceptionsManager", - [redboxHandler = m_devSettings->redboxHandler]() mutable { - return std::make_unique(redboxHandler); - }, - nativeQueue)); - - modules.push_back(std::make_unique( - m_innerInstance, - PlatformConstantsModule::Name, - []() { return std::make_unique(); }, - nativeQueue)); - - modules.push_back(std::make_unique( - m_innerInstance, - StatusBarManagerModule::Name, - []() { return std::make_unique(); }, - nativeQueue)); - - modules.push_back(std::make_unique( - m_innerInstance, - Microsoft::React::GetBlobModuleName(), - [transitionalProps]() { return Microsoft::React::CreateBlobModule(transitionalProps); }, - nativeQueue)); - - modules.push_back(std::make_unique( - m_innerInstance, - Microsoft::React::GetFileReaderModuleName(), - [transitionalProps]() { return Microsoft::React::CreateFileReaderModule(transitionalProps); }, - nativeQueue)); - - return modules; - } - - void InstanceImpl::RegisterForReloadIfNecessary() noexcept { - // setup polling for live reload - if (!m_isInError && !m_devSettings->useFastRefresh && m_devSettings->liveReloadCallback != nullptr) { - m_devManager->StartPollingLiveReload( - m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort, m_devSettings->liveReloadCallback); - } - } + // TODO - Encapsulate this in a helpers, and make sure callers add it to their + // list + std::string bundleUrl = (m_devSettings->useWebDebugger || m_devSettings->liveReloadCallback) + ? DevServerHelper::get_BundleUrl( + m_devSettings->sourceBundleHost, + m_devSettings->sourceBundlePort, + m_devSettings->debugBundlePath, + m_devSettings->platformName, + true /*dev*/, + m_devSettings->useFastRefresh, + m_devSettings->inlineSourceMap, + hermesBytecodeVersion) + : std::string(); + modules.push_back(std::make_unique( + m_innerInstance, + facebook::react::SourceCodeModule::Name, + [bundleUrl]() -> std::unique_ptr { + return std::make_unique(bundleUrl); + }, + nativeQueue)); + + modules.push_back(std::make_unique( + m_innerInstance, + "ExceptionsManager", + [redboxHandler = m_devSettings->redboxHandler]() mutable { + return std::make_unique(redboxHandler); + }, + nativeQueue)); + + modules.push_back(std::make_unique( + m_innerInstance, + PlatformConstantsModule::Name, + []() { return std::make_unique(); }, + nativeQueue)); + + modules.push_back(std::make_unique( + m_innerInstance, + StatusBarManagerModule::Name, + []() { return std::make_unique(); }, + nativeQueue)); + + modules.push_back(std::make_unique( + m_innerInstance, + Microsoft::React::GetBlobModuleName(), + [transitionalProps]() { return Microsoft::React::CreateBlobModule(transitionalProps); }, + nativeQueue)); + + modules.push_back(std::make_unique( + m_innerInstance, + Microsoft::React::GetFileReaderModuleName(), + [transitionalProps]() { return Microsoft::React::CreateFileReaderModule(transitionalProps); }, + nativeQueue)); + + return modules; +} + +void InstanceImpl::RegisterForReloadIfNecessary() noexcept { + // setup polling for live reload + if (!m_isInError && !m_devSettings->useFastRefresh && m_devSettings->liveReloadCallback != nullptr) { + m_devManager->StartPollingLiveReload( + m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort, m_devSettings->liveReloadCallback); + } +} - void InstanceImpl::DispatchEvent(int64_t viewTag, std::string eventName, folly::dynamic&& eventData) { - if (m_isInError) { - return; - } +void InstanceImpl::DispatchEvent(int64_t viewTag, std::string eventName, folly::dynamic &&eventData) { + if (m_isInError) { + return; + } - folly::dynamic params = folly::dynamic::array(viewTag, eventName, std::move(eventData)); - m_innerInstance->callJSFunction("RCTEventEmitter", "receiveEvent", std::move(params)); - } + folly::dynamic params = folly::dynamic::array(viewTag, eventName, std::move(eventData)); + m_innerInstance->callJSFunction("RCTEventEmitter", "receiveEvent", std::move(params)); +} - void InstanceImpl::invokeCallback(const int64_t callbackId, folly::dynamic&& params) { - if (m_isInError) { - return; - } +void InstanceImpl::invokeCallback(const int64_t callbackId, folly::dynamic &¶ms) { + if (m_isInError) { + return; + } - m_innerInstance->callJSCallback(callbackId, std::move(params)); - } + m_innerInstance->callJSCallback(callbackId, std::move(params)); +} - } // namespace react +} // namespace react } // namespace facebook diff --git a/vnext/Shared/OInstance.h b/vnext/Shared/OInstance.h index c14f915dd40..ff4e013ef3a 100644 --- a/vnext/Shared/OInstance.h +++ b/vnext/Shared/OInstance.h @@ -18,87 +18,87 @@ #include namespace facebook { - namespace react { - - struct IDevSupportManager; - struct IReactRootView; - - class InstanceImpl final: public InstanceWrapper, private ::std::enable_shared_from_this { - public: - static std::shared_ptr MakeNoBundle( - std::shared_ptr&& instance, - std::string&& jsBundleBasePath, - std::vector< - std::tuple>> - && cxxModules, - std::shared_ptr turboModuleRegistry, - std::shared_ptr longLivedObjectCollection, - std::unique_ptr&& callback, - std::shared_ptr jsQueue, - std::shared_ptr nativeQueue, - std::shared_ptr devSettings, - std::shared_ptr devManager) noexcept; - - static std::shared_ptr MakeAndLoadBundle( - std::shared_ptr&& instance, - std::string&& jsBundleBasePath, - std::string&& jsBundleRelativePath, - std::vector< - std::tuple>> - && cxxModules, - std::shared_ptr turboModuleRegistry, - std::unique_ptr&& callback, - std::shared_ptr jsQueue, - std::shared_ptr nativeQueue, - std::shared_ptr devSettings, - std::shared_ptr devManager) noexcept; - - // Instance methods - void loadBundle(std::string&& jsBundleRelativePath) override; - void loadBundleSync(std::string&& jsBundleRelativePath) override; - virtual const std::shared_ptr& GetInstance() const noexcept override { - return m_innerInstance; - } - - void DispatchEvent(int64_t viewTag, std::string eventName, folly::dynamic&& eventData) override; - void invokeCallback(const int64_t callbackId, folly::dynamic&& params) override; - - ~InstanceImpl(); - - private: - InstanceImpl( - std::shared_ptr&& instance, - std::string&& jsBundleFile, - std::vector< - std::tuple>> - && cxxModules, - std::shared_ptr turboModuleRegistry, - std::shared_ptr longLivedObjectCollection, - std::unique_ptr&& callback, - std::shared_ptr jsQueue, - std::shared_ptr nativeQueue, - std::shared_ptr devSettings, - std::shared_ptr devManager); - - std::vector> GetDefaultNativeModules(std::shared_ptr nativeQueue); - void RegisterForReloadIfNecessary() noexcept; - void loadBundleInternal(std::string&& jsBundleRelativePath, bool synchronously); - void SetInError() noexcept; - - private: - std::shared_ptr m_innerInstance; - - std::string m_jsBundleBasePath; - std::shared_ptr m_moduleRegistry; - std::shared_ptr m_turboModuleRegistry; - std::shared_ptr m_longLivedObjectCollection; - std::shared_ptr m_jsThread; - std::shared_ptr m_nativeQueue; - - std::shared_ptr m_devManager; - std::shared_ptr m_devSettings; - bool m_isInError{ false }; - }; - - } // namespace react +namespace react { + +struct IDevSupportManager; +struct IReactRootView; + +class InstanceImpl final : public InstanceWrapper, private ::std::enable_shared_from_this { + public: + static std::shared_ptr MakeNoBundle( + std::shared_ptr &&instance, + std::string &&jsBundleBasePath, + std::vector< + std::tuple>> + &&cxxModules, + std::shared_ptr turboModuleRegistry, + std::shared_ptr longLivedObjectCollection, + std::unique_ptr &&callback, + std::shared_ptr jsQueue, + std::shared_ptr nativeQueue, + std::shared_ptr devSettings, + std::shared_ptr devManager) noexcept; + + static std::shared_ptr MakeAndLoadBundle( + std::shared_ptr &&instance, + std::string &&jsBundleBasePath, + std::string &&jsBundleRelativePath, + std::vector< + std::tuple>> + &&cxxModules, + std::shared_ptr turboModuleRegistry, + std::unique_ptr &&callback, + std::shared_ptr jsQueue, + std::shared_ptr nativeQueue, + std::shared_ptr devSettings, + std::shared_ptr devManager) noexcept; + + // Instance methods + void loadBundle(std::string &&jsBundleRelativePath) override; + void loadBundleSync(std::string &&jsBundleRelativePath) override; + virtual const std::shared_ptr &GetInstance() const noexcept override { + return m_innerInstance; + } + + void DispatchEvent(int64_t viewTag, std::string eventName, folly::dynamic &&eventData) override; + void invokeCallback(const int64_t callbackId, folly::dynamic &¶ms) override; + + ~InstanceImpl(); + + private: + InstanceImpl( + std::shared_ptr &&instance, + std::string &&jsBundleFile, + std::vector< + std::tuple>> + &&cxxModules, + std::shared_ptr turboModuleRegistry, + std::shared_ptr longLivedObjectCollection, + std::unique_ptr &&callback, + std::shared_ptr jsQueue, + std::shared_ptr nativeQueue, + std::shared_ptr devSettings, + std::shared_ptr devManager); + + std::vector> GetDefaultNativeModules(std::shared_ptr nativeQueue); + void RegisterForReloadIfNecessary() noexcept; + void loadBundleInternal(std::string &&jsBundleRelativePath, bool synchronously); + void SetInError() noexcept; + + private: + std::shared_ptr m_innerInstance; + + std::string m_jsBundleBasePath; + std::shared_ptr m_moduleRegistry; + std::shared_ptr m_turboModuleRegistry; + std::shared_ptr m_longLivedObjectCollection; + std::shared_ptr m_jsThread; + std::shared_ptr m_nativeQueue; + + std::shared_ptr m_devManager; + std::shared_ptr m_devSettings; + bool m_isInError{false}; +}; + +} // namespace react } // namespace facebook From 8e8d7306e9d9bb0d6dfbf8fcfd4530adcf9b18d2 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Wed, 7 Dec 2022 01:22:41 -0800 Subject: [PATCH 23/45] Reset fles --- .../windows/RNTesterApp/packages.lock.json | 2 +- .../InteropTestModuleCS/packages.lock.json | 24 ++----------------- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/packages/e2e-test-app/windows/RNTesterApp/packages.lock.json b/packages/e2e-test-app/windows/RNTesterApp/packages.lock.json index e459dba5ac7..fe9dd4e8b1a 100644 --- a/packages/e2e-test-app/windows/RNTesterApp/packages.lock.json +++ b/packages/e2e-test-app/windows/RNTesterApp/packages.lock.json @@ -300,4 +300,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/integration-test-app/windows/InteropTestModuleCS/packages.lock.json b/packages/integration-test-app/windows/InteropTestModuleCS/packages.lock.json index dc39619502a..2b78fbef5aa 100644 --- a/packages/integration-test-app/windows/InteropTestModuleCS/packages.lock.json +++ b/packages/integration-test-app/windows/InteropTestModuleCS/packages.lock.json @@ -14,11 +14,6 @@ "NETStandard.Library": "2.0.3" } }, - "boost": { - "type": "Transitive", - "resolved": "1.76.0", - "contentHash": "p+w3YvNdXL8Cu9Fzrmexssu0tZbWxuf6ywsQqHjDlKFE5ojXHof1HIyMC3zDLfLnh80dIeFcEUAuR2Asg/XHRA==" - }, "Microsoft.Net.Native.Compiler": { "type": "Transitive", "resolved": "2.2.7-rel-27913-00", @@ -43,17 +38,7 @@ "Microsoft.NETCore.Platforms": { "type": "Transitive", "resolved": "2.1.0", - "contentHash": "ok+RPAtESz/9MUXeIEz6Lv5XAGQsaNmEYXMsgVALj4D7kqC8gveKWXWXbufLySR2fWrwZf8smyN5RmHu0e4BHA==" - }, - "Microsoft.UI.Xaml": { - "type": "Transitive", - "resolved": "2.7.0", - "contentHash": "dB4im13tfmMgL/V3Ei+3kD2rUF+/lTxAmR4gjJ45l577eljHfdo/KUrxpq/3I1Vp6e5GCDG1evDaEGuDxypLMg==" - }, - "Microsoft.Windows.SDK.BuildTools": { - "type": "Transitive", - "resolved": "10.0.22000.194", - "contentHash": "4L0P3zqut466SIqT3VBeLTNUQTxCBDOrTRymRuROCRJKazcK7ibLz9yAO1nKWRt50ttCj39oAa2Iuz9ZTDmLlg==" + "contentHash": "GmkKfoyerqmsHMn7OZj0AKpcBabD+GaafqphvX2Mw406IwiJRy1pKcKqdCfKJfYmkRyJ6+e+RaUylgdJoDa1jQ==" }, "Microsoft.UI.Xaml": { "type": "Transitive", @@ -68,11 +53,6 @@ "Microsoft.NETCore.Platforms": "1.1.0" } }, - "ReactNative.Hermes.Windows": { - "type": "Transitive", - "resolved": "0.0.0-2209.12005-9f3ce7e5", - "contentHash": "/RxDNnNA6w42Kz7cEep5wc6j8cmE4jepbjm5ElWXYWPHg4xu5D0AIj6RQ+WW2DxUHaDhbm6pjtS7MJSWagdi+g==" - }, "runtime.win10-arm.Microsoft.Net.Native.Compiler": { "type": "Transitive", "resolved": "2.2.7-rel-27913-00", @@ -316,4 +296,4 @@ } } } -} +} \ No newline at end of file From dd07d6e77e9d575842a9c95d5c9bd4c73b1d9cb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20C=C3=A9sar=20Rocha?= Date: Fri, 7 Oct 2022 16:35:57 -0700 Subject: [PATCH 24/45] Set User-Agent header in Origin Policy filter (#10695) * Set User-Agent and Origin in OP filter * Fix Shared filters * Change files * Set the implicit User-Agent in RedirectHttpFilter --- ...-a912b218-3e03-4b76-8b54-d9cd603a29c0.json | 7 + .../HttpResourceIntegrationTests.cpp | 131 +- .../Networking/OriginPolicyHttpFilter.cpp | 1171 +++++++++-------- .../Shared/Networking/RedirectHttpFilter.cpp | 9 + vnext/Shared/Shared.vcxitems.filters | 12 + 5 files changed, 767 insertions(+), 563 deletions(-) create mode 100644 change/react-native-windows-a912b218-3e03-4b76-8b54-d9cd603a29c0.json diff --git a/change/react-native-windows-a912b218-3e03-4b76-8b54-d9cd603a29c0.json b/change/react-native-windows-a912b218-3e03-4b76-8b54-d9cd603a29c0.json new file mode 100644 index 00000000000..db12e10e94e --- /dev/null +++ b/change/react-native-windows-a912b218-3e03-4b76-8b54-d9cd603a29c0.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Set User-Agent header in Origin Policy filter", + "packageName": "react-native-windows", + "email": "julio.rocha@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp index 9e15e7971eb..b3f9a4178db 100644 --- a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp +++ b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp @@ -40,8 +40,16 @@ namespace Microsoft::React::Test { TEST_CLASS (HttpResourceIntegrationTest) { static uint16_t s_port; - TEST_CLASS (HttpResourceIntegrationTest) { - static uint16_t s_port; + TEST_METHOD_CLEANUP(MethodCleanup) { + // Clear any runtime options that may be used by tests in this class. + MicrosoftReactSetRuntimeOptionString("Http.UserAgent", nullptr); + + // Bug in test HTTP server does not correctly release TCP port between test methods. + // Using a different por per test for now. + s_port++; + } + TEST_METHOD(RequestGetSucceeds) { + string url = "http://localhost:" + std::to_string(s_port); TEST_METHOD_CLEANUP(MethodCleanup) { // Clear any runtime options that may be used by tests in this class. @@ -161,8 +169,123 @@ TEST_CLASS (HttpResourceIntegrationTest) { } } - TEST_METHOD(RequestGetExplicitUserAgentSucceeds) { - string url = "https://api.github.com/repos/microsoft/react-native-xaml"; + TEST_METHOD(RequestGetExplicitUserAgentSucceeds) { + string url = "https://api.github.com/repos/microsoft/react-native-xaml"; + + promise rcPromise; + string error; + IHttpResource::Response response; + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { + response = callbackResponse; + rcPromise.set_value(); + }); + resource->SetOnError([&rcPromise, &error](int64_t, string &&message, bool) { + error = std::move(message); + rcPromise.set_value(); + }); + + //clang-format off + resource->SendRequest( + "GET", + std::move(url), + 0, /*requestId*/ + {{"User-Agent", "React Native Windows"}}, + {}, /*data*/ + "text", /*responseType*/ + false, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {}); + //clang-format on + + rcPromise.get_future().wait(); + + Assert::AreEqual({}, error, L"Error encountered"); + Assert::AreEqual(static_cast(200), response.StatusCode); + } + + TEST_METHOD(RequestGetImplicitUserAgentSucceeds) { + string url = "https://api.github.com/repos/microsoft/react-native-windows"; + + promise rcPromise; + string error; + IHttpResource::Response response; + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { + response = callbackResponse; + rcPromise.set_value(); + }); + resource->SetOnError([&rcPromise, &error](int64_t, string &&message, bool) { + error = std::move(message); + rcPromise.set_value(); + }); + + MicrosoftReactSetRuntimeOptionString("Http.UserAgent", "React Native Windows"); + + //clang-format off + resource->SendRequest( + "GET", + std::move(url), + 0, /*requestId*/ + {}, /*headers*/ + {}, /*data*/ + "text", /*responseType*/ + false, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {}); + //clang-format on + + rcPromise.get_future().wait(); + + Assert::AreEqual({}, error, L"Error encountered"); + Assert::AreEqual(static_cast(200), response.StatusCode); + } + + TEST_METHOD(RequestGetMissingUserAgentFails) { + // string url = "http://localhost:" + std::to_string(s_port); + string url = "https://api.github.com/repos/microsoft/react-native-macos"; + + promise rcPromise; + string error; + IHttpResource::Response response; + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { + response = callbackResponse; + rcPromise.set_value(); + }); + resource->SetOnError([&rcPromise, &error](int64_t, string &&message, bool) { + error = std::move(message); + rcPromise.set_value(); + }); + + //clang-format off + resource->SendRequest( + "GET", + std::move(url), + 0, /*requestId*/ + {}, /*headers*/ + {}, /*data*/ + "text", /*responseType*/ + false, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {}); + //clang-format on + + rcPromise.get_future().wait(); + + Assert::AreEqual({}, error, L"Error encountered"); + Assert::AreEqual(static_cast(403), response.StatusCode); + } + + TEST_METHOD(RequestGetFails) { + string error; + promise promise; auto resource = IHttpResource::Make(); resource->SetOnError([&error, &promise](int64_t, string &&message, bool) { diff --git a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp index 36feb1e9ee6..7cb90ac2e36 100644 --- a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp +++ b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp @@ -39,20 +39,20 @@ namespace Microsoft::React::Networking { #pragma region ConstWcharComparer -bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, const wchar_t *b) const { - return _wcsicmp(a, b) < 0; -} + bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t* a, const wchar_t* b) const { + return _wcsicmp(a, b) < 0; + } #pragma endregion ConstWcharComparer -// https://fetch.spec.whatwg.org/#forbidden-method -/*static*/ set OriginPolicyHttpFilter::s_forbiddenMethods = - {L"CONNECT", L"TRACE", L"TRACK"}; + // https://fetch.spec.whatwg.org/#forbidden-method + /*static*/ set OriginPolicyHttpFilter::s_forbiddenMethods = + { L"CONNECT", L"TRACE", L"TRACK" }; -/*static*/ set - OriginPolicyHttpFilter::s_simpleCorsMethods = {L"GET", L"HEAD", L"POST"}; + /*static*/ set + OriginPolicyHttpFilter::s_simpleCorsMethods = { L"GET", L"HEAD", L"POST" }; -/*static*/ set + /*static*/ set OriginPolicyHttpFilter::s_simpleCorsRequestHeaderNames = { L"Accept", L"Accept-Language", @@ -62,21 +62,21 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co L"Downlink", L"Save-Data", L"Viewport-Width", - L"Width"}; + L"Width" }; -/*static*/ set + /*static*/ set OriginPolicyHttpFilter::s_simpleCorsResponseHeaderNames = - {L"Cache-Control", L"Content-Language", L"Content-Type", L"Expires", L"Last-Modified", L"Pragma"}; + { L"Cache-Control", L"Content-Language", L"Content-Type", L"Expires", L"Last-Modified", L"Pragma" }; -/*static*/ set + /*static*/ set OriginPolicyHttpFilter::s_simpleCorsContentTypeValues = { L"application/x-www-form-urlencoded", L"multipart/form-data", - L"text/plain"}; + L"text/plain" }; -// https://fetch.spec.whatwg.org/#forbidden-header-name -// Chromium still bans "User-Agent" due to https://crbug.com/571722 -/*static*/ set + // https://fetch.spec.whatwg.org/#forbidden-header-name + // Chromium still bans "User-Agent" due to https://crbug.com/571722 + /*static*/ set OriginPolicyHttpFilter::s_corsForbiddenRequestHeaderNames = { L"Accept-Charset", L"Accept-Encoding", @@ -97,109 +97,109 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co L"Trailer", L"Transfer-Encoding", L"Upgrade", - L"Via"}; + L"Via" }; -/*static*/ set + /*static*/ set OriginPolicyHttpFilter::s_cookieSettingResponseHeaders = { L"Set-Cookie", L"Set-Cookie2", // Deprecated by the spec, but probably still used -}; - -/*static*/ set - OriginPolicyHttpFilter::s_corsForbiddenRequestHeaderNamePrefixes = {L"Proxy-", L"Sec-"}; - -/*static*/ Uri OriginPolicyHttpFilter::s_origin{nullptr}; + }; -/*static*/ void OriginPolicyHttpFilter::SetStaticOrigin(std::string &&url) { - if (!url.empty()) - s_origin = Uri{to_hstring(url)}; -} + /*static*/ set + OriginPolicyHttpFilter::s_corsForbiddenRequestHeaderNamePrefixes = { L"Proxy-", L"Sec-" }; -/*static*/ bool OriginPolicyHttpFilter::IsSameOrigin(Uri const &u1, Uri const &u2) noexcept { - return (u1 && u2) && u1.SchemeName() == u2.SchemeName() && u1.Host() == u2.Host() && u1.Port() == u2.Port(); -} + /*static*/ Uri OriginPolicyHttpFilter::s_origin{ nullptr }; -/*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const &request) noexcept { - // Ensure header is in Simple CORS allowlist - for (const auto &header : request.Headers()) { - if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend()) - return false; - - // Ensure Content-Type value is in Simple CORS allowlist, if present - if (boost::iequals(header.Key(), L"Content-Type")) { - if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) != s_simpleCorsContentTypeValues.cend()) - return false; - } + /*static*/ void OriginPolicyHttpFilter::SetStaticOrigin(std::string&& url) { + if (!url.empty()) + s_origin = Uri{ to_hstring(url) }; } - /*static*/ bool OriginPolicyHttpFilter::IsSameOrigin(Uri const &u1, Uri const &u2) noexcept { + /*static*/ bool OriginPolicyHttpFilter::IsSameOrigin(Uri const& u1, Uri const& u2) noexcept { return (u1 && u2) && u1.SchemeName() == u2.SchemeName() && u1.Host() == u2.Host() && u1.Port() == u2.Port(); } - /*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const &request) noexcept { - // Ensure header is in Simple CORS white list - for (const auto &header : request.Headers()) { + /*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const& request) noexcept { + // Ensure header is in Simple CORS allowlist + for (const auto& header : request.Headers()) { if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend()) return false; - // Ensure Content-Type value is in Simple CORS white list, if present + // Ensure Content-Type value is in Simple CORS allowlist, if present if (boost::iequals(header.Key(), L"Content-Type")) { if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) != s_simpleCorsContentTypeValues.cend()) return false; } } - // WinRT separates request headers from request content headers - if (auto content = request.Content()) { - for (const auto &header : content.Headers()) { - // WinRT automatically appends non-whitelisted header Content-Length when Content-Type is set. Skip it. - if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend() && - !boost::iequals(header.Key(), "Content-Length")) + /*static*/ bool OriginPolicyHttpFilter::IsSameOrigin(Uri const& u1, Uri const& u2) noexcept { + return (u1 && u2) && u1.SchemeName() == u2.SchemeName() && u1.Host() == u2.Host() && u1.Port() == u2.Port(); + } + + /*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const& request) noexcept { + // Ensure header is in Simple CORS white list + for (const auto& header : request.Headers()) { + if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend()) return false; // Ensure Content-Type value is in Simple CORS white list, if present if (boost::iequals(header.Key(), L"Content-Type")) { - if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) == s_simpleCorsContentTypeValues.cend()) + if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) != s_simpleCorsContentTypeValues.cend()) return false; } } - } - // Ensure method is in Simple CORS white list - return s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); - } + // WinRT separates request headers from request content headers + if (auto content = request.Content()) { + for (const auto& header : content.Headers()) { + // WinRT automatically appends non-whitelisted header Content-Length when Content-Type is set. Skip it. + if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend() && + !boost::iequals(header.Key(), "Content-Length")) + return false; - /*static*/ Uri OriginPolicyHttpFilter::GetOrigin(Uri const &uri) noexcept { - return Uri{uri.SchemeName() + L"://" + uri.Host() + L":" + to_hstring(uri.Port())}; - } + // Ensure Content-Type value is in Simple CORS white list, if present + if (boost::iequals(header.Key(), L"Content-Type")) { + if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) == s_simpleCorsContentTypeValues.cend()) + return false; + } + } + } - /*static*/ bool OriginPolicyHttpFilter::AreSafeRequestHeaders( - winrt::Windows::Web::Http::Headers::HttpRequestHeaderCollection const &headers) noexcept { - for (const auto &header : headers) { - if (s_corsForbiddenRequestHeaderNames.find(header.Key().c_str()) != s_corsForbiddenRequestHeaderNames.cend()) - return false; + // Ensure method is in Simple CORS white list + return s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); + } + + /*static*/ Uri OriginPolicyHttpFilter::GetOrigin(Uri const& uri) noexcept { + return Uri{ uri.SchemeName() + L"://" + uri.Host() + L":" + to_hstring(uri.Port()) }; + } - for (const auto &prefix : s_corsForbiddenRequestHeaderNamePrefixes) { - if (boost::istarts_with(header.Key(), prefix)) + /*static*/ bool OriginPolicyHttpFilter::AreSafeRequestHeaders( + winrt::Windows::Web::Http::Headers::HttpRequestHeaderCollection const& headers) noexcept { + for (const auto& header : headers) { + if (s_corsForbiddenRequestHeaderNames.find(header.Key().c_str()) != s_corsForbiddenRequestHeaderNames.cend()) return false; + + for (const auto& prefix : s_corsForbiddenRequestHeaderNamePrefixes) { + if (boost::istarts_with(header.Key(), prefix)) + return false; + } } + + return true; } - return true; - } + // See https://fetch.spec.whatwg.org/#cors-safelisted-request-header + // "A CORS-safelisted header is a header whose name is either one of 'Accept', 'Accept-Language', and + // 'Content-Language', or whose name is 'Content-Type' and value is one of 'application/x-www-form-urlencoded', + // 'multipart/form-data', and 'text/plain' + /*static*/ bool OriginPolicyHttpFilter::IsCorsSafelistedRequestHeader( + hstring const& name, hstring const& value) noexcept { + // 1. If value's length is greater than 128, then return false. + if (value.size() > 128) + return false; - // See https://fetch.spec.whatwg.org/#cors-safelisted-request-header - // "A CORS-safelisted header is a header whose name is either one of 'Accept', 'Accept-Language', and - // 'Content-Language', or whose name is 'Content-Type' and value is one of 'application/x-www-form-urlencoded', - // 'multipart/form-data', and 'text/plain' - /*static*/ bool OriginPolicyHttpFilter::IsCorsSafelistedRequestHeader( - hstring const &name, hstring const &value) noexcept { - // 1. If value's length is greater than 128, then return false. - if (value.size() > 128) - return false; - - // 2. Byte-lowercase name and switch on the result: - static const wchar_t *const safeHeaderNames[] = { + // 2. Byte-lowercase name and switch on the result: + static const wchar_t* const safeHeaderNames[] = { // The following four headers are from the CORS spec L"accept", L"accept-language", @@ -226,188 +226,194 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co L"sec-ch-ua-mobile", L"sec-ch-ua-full-version", L"sec-ch-ua-platform-version", - }; - - auto nameLower = boost::to_lower_copy(wstring{name.c_str()}); - if (std::find(std::cbegin(safeHeaderNames), std::cend(safeHeaderNames), nameLower) == std::end(safeHeaderNames)) - return false; - - double doubleHolder; - if (nameLower == L"device-memory" || nameLower == L"dpr") - return boost::conversion::try_lexical_convert(value.c_str(), doubleHolder); - - int intHolder; - if (nameLower == L"width" || nameLower == L"viewport-width") - return boost::conversion::try_lexical_convert(value.c_str(), intHolder); - - auto valueLower = boost::to_lower_copy(wstring{value.c_str()}); - if (nameLower == L"save-data") - return valueLower == L"on"; - - if (nameLower == L"accept") - return !std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte); - - if (nameLower == L"accept-language" || nameLower == L"content-language") { - return std::all_of(valueLower.cbegin(), valueLower.cend(), [](wchar_t c) noexcept { - return (0x30 <= c && c <= 0x39) || // 0-9 - (0x41 <= c && c <= 0x5A) || // A-Z - (0x61 <= c && c <= 0x7A) || // a-z - c == 0x20 || c == 0x2A || c == 0x2C || c == 0x2D || c == 0x2E || c == 0x3B || c == 0x3D; // *,-.;= - }); - } + }; - if (nameLower == L"content-type") { - if (std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte)) + auto nameLower = boost::to_lower_copy(wstring{ name.c_str() }); + if (std::find(std::cbegin(safeHeaderNames), std::cend(safeHeaderNames), nameLower) == std::end(safeHeaderNames)) return false; - // https://mimesniff.spec.whatwg.org/#parse-a-mime-type - HttpMediaTypeHeaderValue mediaType{nullptr}; - if (HttpMediaTypeHeaderValue::TryParse(valueLower, mediaType)) - return mediaType.ToString() == L"application/x-www-form-urlencoded" || - mediaType.ToString() == L"multipart/form-data" || mediaType.ToString() == L"text/plain"; + double doubleHolder; + if (nameLower == L"device-memory" || nameLower == L"dpr") + return boost::conversion::try_lexical_convert(value.c_str(), doubleHolder); - return false; - } + int intHolder; + if (nameLower == L"width" || nameLower == L"viewport-width") + return boost::conversion::try_lexical_convert(value.c_str(), intHolder); - return true; - } + auto valueLower = boost::to_lower_copy(wstring{ value.c_str() }); + if (nameLower == L"save-data") + return valueLower == L"on"; - // https://fetch.spec.whatwg.org/#cors-unsafe-request-header-byte - /*static*/ bool OriginPolicyHttpFilter::IsCorsUnsafeRequestHeaderByte(wchar_t c) noexcept { - return (c < 0x20 && c != 0x09) || c == 0x22 || c == 0x28 || c == 0x29 || c == 0x3a || c == 0x3c || c == 0x3e || - c == 0x3f || c == 0x40 || c == 0x5b || c == 0x5c || c == 0x5d || c == 0x7b || c == 0x7d || c == 0x7f; - } + if (nameLower == L"accept") + return !std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte); - /*static*/ set OriginPolicyHttpFilter::CorsUnsafeNotForbiddenRequestHeaderNames( - HttpRequestHeaderCollection const &headers) noexcept { - constexpr size_t maxSafelistValueSize = 1024; - size_t safelistValueSize = 0; - std::vector potentiallyUnsafeNames; - set result; - for (const auto &header : headers) { - const auto headerName = header.Key().c_str(); - - // If header is not safe - if (boost::istarts_with(headerName, L"Proxy-") || boost::istarts_with(headerName, L"Sec-") || - s_corsForbiddenRequestHeaderNames.find(headerName) != s_corsForbiddenRequestHeaderNames.cend()) - continue; + if (nameLower == L"accept-language" || nameLower == L"content-language") { + return std::all_of(valueLower.cbegin(), valueLower.cend(), [](wchar_t c) noexcept { + return (0x30 <= c && c <= 0x39) || // 0-9 + (0x41 <= c && c <= 0x5A) || // A-Z + (0x61 <= c && c <= 0x7A) || // a-z + c == 0x20 || c == 0x2A || c == 0x2C || c == 0x2D || c == 0x2E || c == 0x3B || c == 0x3D; // *,-.;= + }); + } + + if (nameLower == L"content-type") { + if (std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte)) + return false; + + // https://mimesniff.spec.whatwg.org/#parse-a-mime-type + HttpMediaTypeHeaderValue mediaType{ nullptr }; + if (HttpMediaTypeHeaderValue::TryParse(valueLower, mediaType)) + return mediaType.ToString() == L"application/x-www-form-urlencoded" || + mediaType.ToString() == L"multipart/form-data" || mediaType.ToString() == L"text/plain"; - if (!IsCorsSafelistedRequestHeader(header.Key(), header.Value())) { - result.emplace(header.Key().c_str()); - } else { - potentiallyUnsafeNames.emplace_back(std::wstring_view{header.Key()}.data()); - safelistValueSize += header.Value().size(); + return false; } + + return true; } - if (safelistValueSize > maxSafelistValueSize) - result.insert(potentiallyUnsafeNames.begin(), potentiallyUnsafeNames.end()); + // https://fetch.spec.whatwg.org/#cors-unsafe-request-header-byte + /*static*/ bool OriginPolicyHttpFilter::IsCorsUnsafeRequestHeaderByte(wchar_t c) noexcept { + return (c < 0x20 && c != 0x09) || c == 0x22 || c == 0x28 || c == 0x29 || c == 0x3a || c == 0x3c || c == 0x3e || + c == 0x3f || c == 0x40 || c == 0x5b || c == 0x5c || c == 0x5d || c == 0x7b || c == 0x7d || c == 0x7f; + } - return result; - } + /*static*/ set OriginPolicyHttpFilter::CorsUnsafeNotForbiddenRequestHeaderNames( + HttpRequestHeaderCollection const& headers) noexcept { + constexpr size_t maxSafelistValueSize = 1024; + size_t safelistValueSize = 0; + std::vector potentiallyUnsafeNames; + set result; + for (const auto& header : headers) { + const auto headerName = header.Key().c_str(); + + // If header is not safe + if (boost::istarts_with(headerName, L"Proxy-") || boost::istarts_with(headerName, L"Sec-") || + s_corsForbiddenRequestHeaderNames.find(headerName) != s_corsForbiddenRequestHeaderNames.cend()) + continue; - /*static*/ OriginPolicyHttpFilter::AccessControlValues OriginPolicyHttpFilter::ExtractAccessControlValues( - winrt::Windows::Foundation::Collections::IMap const &headers) { - using std::wregex; - using std::wsregex_token_iterator; - - // https://tools.ietf.org/html/rfc2616#section-4.2 - wregex rgx{L"\\s*,\\s*"}; - AccessControlValues result; - - auto ciStrCmp = [](const wstring &a, const wstring &b) { return _wcsicmp(a.c_str(), b.c_str()) < 0; }; - set allowedHeaders{ciStrCmp}; - set allowedMethods{ciStrCmp}; - set exposedHeaders{ciStrCmp}; - - for (const auto &header : headers) { - if (boost::iequals(header.Key(), L"Access-Control-Allow-Headers")) { - auto value = wstring{header.Value().c_str()}; - - // TODO: Avoid redundant comparison. - auto parsed = set{ - wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp}; - allowedHeaders.insert(parsed.cbegin(), parsed.cend()); - result.AllowedHeaders.insert(allowedHeaders.cbegin(), allowedHeaders.cend()); - } else if (boost::iequals(header.Key(), L"Access-Control-Allow-Methods")) { - auto value = wstring{header.Value().c_str()}; - - // TODO: Avoid redundant comparison. - auto parsed = set{ - wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp}; - allowedMethods.insert(parsed.cbegin(), parsed.cend()); - result.AllowedMethods.insert(allowedMethods.cbegin(), allowedMethods.cend()); - } else if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { - result.AllowedOrigin = header.Value(); - } else if (boost::iequals(header.Key(), L"Access-Control-Expose-Headers")) { - auto value = wstring{header.Value().c_str()}; - - // TODO: Avoid redundant comparison. - auto parsed = set{ - wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp}; - exposedHeaders.insert(parsed.cbegin(), parsed.cend()); - result.ExposedHeaders.insert(exposedHeaders.cbegin(), exposedHeaders.cend()); - } else if (boost::iequals(header.Key(), L"Access-Control-Allow-Credentials")) { - result.AllowedCredentials = header.Value(); - } else if (boost::iequals(header.Key(), L"Access-Control-Max-Age")) { - result.MaxAge = _wtoi(header.Value().c_str()); + if (!IsCorsSafelistedRequestHeader(header.Key(), header.Value())) { + result.emplace(header.Key().c_str()); + } + else { + potentiallyUnsafeNames.emplace_back(std::wstring_view{ header.Key() }.data()); + safelistValueSize += header.Value().size(); + } } - } - return result; - } // ExtractAccessControlValues + if (safelistValueSize > maxSafelistValueSize) + result.insert(potentiallyUnsafeNames.begin(), potentiallyUnsafeNames.end()); - /*static*/ void OriginPolicyHttpFilter::RemoveHttpOnlyCookiesFromResponseHeaders( - HttpResponseMessage const &response, bool removeAll) { - // Example: "Set-Cookie", L"id=a3fWa; Expires=Wed, 21 Oct 2020 07:28:00 GMT; HttpOnly" - std::queue httpOnlyCookies; - for (const auto &header : response.Headers()) { - if (s_cookieSettingResponseHeaders.find(header.Key().c_str()) == s_cookieSettingResponseHeaders.cend()) - continue; + return result; + } - if (removeAll) { - httpOnlyCookies.push(header.Key()); - continue; + /*static*/ OriginPolicyHttpFilter::AccessControlValues OriginPolicyHttpFilter::ExtractAccessControlValues( + winrt::Windows::Foundation::Collections::IMap const& headers) { + using std::wregex; + using std::wsregex_token_iterator; + + // https://tools.ietf.org/html/rfc2616#section-4.2 + wregex rgx{ L"\\s*,\\s*" }; + AccessControlValues result; + + auto ciStrCmp = [](const wstring& a, const wstring& b) { return _wcsicmp(a.c_str(), b.c_str()) < 0; }; + set allowedHeaders{ ciStrCmp }; + set allowedMethods{ ciStrCmp }; + set exposedHeaders{ ciStrCmp }; + + for (const auto& header : headers) { + if (boost::iequals(header.Key(), L"Access-Control-Allow-Headers")) { + auto value = wstring{ header.Value().c_str() }; + + // TODO: Avoid redundant comparison. + auto parsed = set{ + wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; + allowedHeaders.insert(parsed.cbegin(), parsed.cend()); + result.AllowedHeaders.insert(allowedHeaders.cbegin(), allowedHeaders.cend()); + } + else if (boost::iequals(header.Key(), L"Access-Control-Allow-Methods")) { + auto value = wstring{ header.Value().c_str() }; + + // TODO: Avoid redundant comparison. + auto parsed = set{ + wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; + allowedMethods.insert(parsed.cbegin(), parsed.cend()); + result.AllowedMethods.insert(allowedMethods.cbegin(), allowedMethods.cend()); + } + else if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { + result.AllowedOrigin = header.Value(); + } + else if (boost::iequals(header.Key(), L"Access-Control-Expose-Headers")) { + auto value = wstring{ header.Value().c_str() }; + + // TODO: Avoid redundant comparison. + auto parsed = set{ + wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; + exposedHeaders.insert(parsed.cbegin(), parsed.cend()); + result.ExposedHeaders.insert(exposedHeaders.cbegin(), exposedHeaders.cend()); + } + else if (boost::iequals(header.Key(), L"Access-Control-Allow-Credentials")) { + result.AllowedCredentials = header.Value(); + } + else if (boost::iequals(header.Key(), L"Access-Control-Max-Age")) { + result.MaxAge = _wtoi(header.Value().c_str()); + } } - // Anchors (^$) can't be part of bracket expressions ([]). - // Create 3 matching groups: 1. Beginning of string 2. Between delimiters 3. End of string - const std::wregex re(L"(^HttpOnly\\s*;)|(;\\s*HttpOnly\\s*;)|(;\\s*HttpOnly$)", std::regex_constants::icase); - if (!std::regex_search(header.Value().c_str(), re)) - continue; + return result; + } // ExtractAccessControlValues + + /*static*/ void OriginPolicyHttpFilter::RemoveHttpOnlyCookiesFromResponseHeaders( + HttpResponseMessage const& response, bool removeAll) { + // Example: "Set-Cookie", L"id=a3fWa; Expires=Wed, 21 Oct 2020 07:28:00 GMT; HttpOnly" + std::queue httpOnlyCookies; + for (const auto& header : response.Headers()) { + if (s_cookieSettingResponseHeaders.find(header.Key().c_str()) == s_cookieSettingResponseHeaders.cend()) + continue; + + if (removeAll) { + httpOnlyCookies.push(header.Key()); + continue; + } + + // Anchors (^$) can't be part of bracket expressions ([]). + // Create 3 matching groups: 1. Beginning of string 2. Between delimiters 3. End of string + const std::wregex re(L"(^HttpOnly\\s*;)|(;\\s*HttpOnly\\s*;)|(;\\s*HttpOnly$)", std::regex_constants::icase); + if (!std::regex_search(header.Value().c_str(), re)) + continue; - // HttpOnly cookie detected. Removing. - httpOnlyCookies.push(header.Key()); - } // const auto &header : response.Headers() + // HttpOnly cookie detected. Removing. + httpOnlyCookies.push(header.Key()); + } // const auto &header : response.Headers() - while (!httpOnlyCookies.empty()) { - response.Headers().Remove(httpOnlyCookies.front()); - httpOnlyCookies.pop(); + while (!httpOnlyCookies.empty()) { + response.Headers().Remove(httpOnlyCookies.front()); + httpOnlyCookies.pop(); + } } - } - OriginPolicyHttpFilter::OriginPolicyHttpFilter(IHttpFilter const &innerFilter) : m_innerFilter{innerFilter} {} + OriginPolicyHttpFilter::OriginPolicyHttpFilter(IHttpFilter const& innerFilter): m_innerFilter{ innerFilter } {} - OriginPolicyHttpFilter::OriginPolicyHttpFilter() + OriginPolicyHttpFilter::OriginPolicyHttpFilter() : OriginPolicyHttpFilter(winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter{}) {} - OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const &request) { - auto effectiveOriginPolicy = + OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const& request) { + auto effectiveOriginPolicy = static_cast(request.Properties().Lookup(L"OriginPolicy").as().GetUInt64()); - switch (effectiveOriginPolicy) { + switch (effectiveOriginPolicy) { case OriginPolicy::None: return effectiveOriginPolicy; case OriginPolicy::SameOrigin: if (!IsSameOrigin(s_origin, request.RequestUri())) - throw hresult_error{E_INVALIDARG, L"SOP (same-origin policy) is enforced"}; + throw hresult_error{ E_INVALIDARG, L"SOP (same-origin policy) is enforced" }; break; case OriginPolicy::SimpleCrossOriginResourceSharing: // Check for disallowed mixed content if (GetRuntimeOptionBool("Http.BlockMixedContentSimpleCors") && - s_origin.SchemeName() != request.RequestUri().SchemeName()) - throw hresult_error{E_INVALIDARG, L"The origin and request URLs must have the same scheme"}; + s_origin.SchemeName() != request.RequestUri().SchemeName()) + throw hresult_error{ E_INVALIDARG, L"The origin and request URLs must have the same scheme" }; if (IsSameOrigin(s_origin, request.RequestUri())) // Same origin. Therefore, skip Cross-Origin handling. @@ -415,7 +421,7 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co else if (!IsSimpleCorsRequest(request)) throw hresult_error{ E_INVALIDARG, - L"The request does not meet the requirements for Same-Origin policy or Simple Cross-Origin resource sharing"}; + L"The request does not meet the requirements for Same-Origin policy or Simple Cross-Origin resource sharing" }; break; case OriginPolicy::CrossOriginResourceSharing: @@ -426,13 +432,13 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co // https://fetch.spec.whatwg.org/#forbidden-header-name if (s_origin.SchemeName() != request.RequestUri().SchemeName()) - throw hresult_error{E_INVALIDARG, L"The origin and request URLs must have the same scheme"}; + throw hresult_error{ E_INVALIDARG, L"The origin and request URLs must have the same scheme" }; if (!AreSafeRequestHeaders(request.Headers())) - throw hresult_error{E_INVALIDARG, L"Request header not allowed in cross-origin resource sharing"}; + throw hresult_error{ E_INVALIDARG, L"Request header not allowed in cross-origin resource sharing" }; if (s_forbiddenMethods.find(request.Method().ToString().c_str()) != s_forbiddenMethods.cend()) - throw hresult_error{E_INVALIDARG, L"Request method not allowed in cross-origin resource sharing"}; + throw hresult_error{ E_INVALIDARG, L"Request method not allowed in cross-origin resource sharing" }; if (IsSameOrigin(s_origin, request.RequestUri())) effectiveOriginPolicy = OriginPolicy::SameOrigin; @@ -445,420 +451,467 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co default: throw hresult_error{ - E_INVALIDARG, L"Invalid OriginPolicy type: " + to_hstring(static_cast(effectiveOriginPolicy))}; - } - - return effectiveOriginPolicy; - } + E_INVALIDARG, L"Invalid OriginPolicy type: " + to_hstring(static_cast(effectiveOriginPolicy)) }; + } - // See https://fetch.spec.whatwg.org/#cors-check - void OriginPolicyHttpFilter::ValidateAllowOrigin( - hstring const &allowedOrigin, hstring const &allowCredentials, 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"}; + return effectiveOriginPolicy; + } // See https://fetch.spec.whatwg.org/#cors-check void OriginPolicyHttpFilter::ValidateAllowOrigin( - hstring const &allowedOrigin, hstring const &allowCredentials, IMap props) const { + hstring const& allowedOrigin, hstring const& allowCredentials, 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 = 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 - 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"; - - // A wildcard Access-Control-Allow-Origin can not be used if credentials are to be sent, - // even with Access-Control-Allow-Credentials set to true - // See https://fetch.spec.whatwg.org/#cors-protocol-and-credentials - else if (L"*" == allowedOrigin) - errorMessage = - L"Response header Access-Control-Allow-Origin can not have a wildcard value when the request includes credentials"; - - else - errorMessage = L"The Access-Control-Allow-Origin header has a value of [" + allowedOrigin + - L"] which differs from the supplied origin"; + L"Response header Access-Control-Allow-Origin has a value of [null] which differs from the supplied origin" }; - throw hresult_error{E_INVALIDARG, errorMessage}; - } + // See https://fetch.spec.whatwg.org/#cors-check + void OriginPolicyHttpFilter::ValidateAllowOrigin( + hstring const& allowedOrigin, hstring const& allowCredentials, 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 = 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 + 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"; + + // A wildcard Access-Control-Allow-Origin can not be used if credentials are to be sent, + // even with Access-Control-Allow-Credentials set to true + // See https://fetch.spec.whatwg.org/#cors-protocol-and-credentials + else if (L"*" == allowedOrigin) + errorMessage = + L"Response header Access-Control-Allow-Origin can not have a wildcard value when the request includes credentials"; + + else + errorMessage = L"The Access-Control-Allow-Origin header has a value of [" + allowedOrigin + + L"] which differs from the supplied origin"; + + throw hresult_error{ E_INVALIDARG, errorMessage }; + } - // 4.10.5 - if (!withCredentials) - return; + // 4.10.5 + if (!withCredentials) + return; - // 4.10.6-8 - // https://fetch.spec.whatwg.org/#http-access-control-allow-credentials - // This check should be case sensitive. - // See also https://fetch.spec.whatwg.org/#http-new-header-syntax - if (L"true" != allowCredentials) - throw hresult_error{ - E_INVALIDARG, - L"Access-Control-Allow-Credentials value must be \"true\" when the response includes credentials"}; - }; - - void OriginPolicyHttpFilter::ValidatePreflightResponse( - HttpRequestMessage const &request, HttpResponseMessage const &response) const { - // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed - using winrt::Windows::Web::Http::HttpStatusCode; - switch (response.StatusCode()) { + // 4.10.6-8 + // https://fetch.spec.whatwg.org/#http-access-control-allow-credentials + // This check should be case sensitive. + // See also https://fetch.spec.whatwg.org/#http-new-header-syntax + if (L"true" != allowCredentials) + throw hresult_error{ + E_INVALIDARG, + L"Access-Control-Allow-Credentials value must be \"true\" when the response includes credentials" }; + }; + + void OriginPolicyHttpFilter::ValidatePreflightResponse( + HttpRequestMessage const& request, HttpResponseMessage const& response) const { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed + using winrt::Windows::Web::Http::HttpStatusCode; + switch (response.StatusCode()) { case HttpStatusCode::MovedPermanently: case HttpStatusCode::TemporaryRedirect: case HttpStatusCode::PermanentRedirect: - throw hresult_error{INET_E_REDIRECTING, L"Redirect is not allowed in a preflight request"}; + throw hresult_error{ INET_E_REDIRECTING, L"Redirect is not allowed in a preflight request" }; default: break; - } + } - auto controlValues = ExtractAccessControlValues(response.Headers()); - - 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, props); - - // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.5 - // Check if the request method is allowed - bool withCredentials = props.Lookup(L"RequestArgs").as()->WithCredentials; - bool requestMethodAllowed = false; - for (const auto &method : controlValues.AllowedMethods) { - if (L"*" == method) { - if (!withCredentials) { - requestMethodAllowed = true; - break; + auto controlValues = ExtractAccessControlValues(response.Headers()); + + 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, props); + + // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.5 + // Check if the request method is allowed + bool withCredentials = props.Lookup(L"RequestArgs").as()->WithCredentials; + bool requestMethodAllowed = false; + for (const auto& method : controlValues.AllowedMethods) { + if (L"*" == method) { + if (!withCredentials) { + requestMethodAllowed = true; + break; + } } - } - // Preflight should always allow simple CORS methods - requestMethodAllowed |= + // Preflight should always allow simple CORS methods + requestMethodAllowed |= s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); - if (!requestMethodAllowed) - throw hresult_error{ - E_INVALIDARG, - L"Method [" + request.Method().ToString() + - L"] is not allowed by Access-Control-Allow-Methods in preflight response"}; - - // Check if request headers are allowed - // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.6-7 - // Check if the header should be allowed through wildcard, if the request does not have credentials. - bool requestHeadersAllowed = false; - if (!withCredentials && controlValues.AllowedHeaders.find(L"*") != controlValues.AllowedHeaders.cend()) { - // "Authorization" header cannot be allowed through wildcard alone. - // "Authorization" is the only member of https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name. - if (request.Headers().HasKey(L"Authorization") && - controlValues.AllowedHeaders.find(L"Authorization") == controlValues.AllowedHeaders.cend()) + if (!requestMethodAllowed) throw hresult_error{ E_INVALIDARG, - L"Request header field [Authorization] is not allowed by Access-Control-Allow-Headers in preflight response"}; - - requestHeadersAllowed = true; - } - if (!requestHeadersAllowed) { - // Forbidden headers are excluded from the JavaScript layer. - // User agents may use these headers internally. - const set unsafeNotForbidenHeaderNames = CorsUnsafeNotForbiddenRequestHeaderNames(request.Headers()); - for (const auto name : unsafeNotForbidenHeaderNames) { - if (controlValues.AllowedHeaders.find(name) == controlValues.AllowedHeaders.cend()) + L"Method [" + request.Method().ToString() + + L"] is not allowed by Access-Control-Allow-Methods in preflight response" }; + + // Check if request headers are allowed + // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.6-7 + // Check if the header should be allowed through wildcard, if the request does not have credentials. + bool requestHeadersAllowed = false; + if (!withCredentials && controlValues.AllowedHeaders.find(L"*") != controlValues.AllowedHeaders.cend()) { + // "Authorization" header cannot be allowed through wildcard alone. + // "Authorization" is the only member of https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name. + if (request.Headers().HasKey(L"Authorization") && + controlValues.AllowedHeaders.find(L"Authorization") == controlValues.AllowedHeaders.cend()) throw hresult_error{ E_INVALIDARG, - L"Request header field [" + to_hstring(name) + - L"] is not allowed by Access-Control-Allow-Headers in preflight response"}; + L"Request header field [Authorization] is not allowed by Access-Control-Allow-Headers in preflight response" }; + + requestHeadersAllowed = true; } - } + if (!requestHeadersAllowed) { + // Forbidden headers are excluded from the JavaScript layer. + // User agents may use these headers internally. + const set unsafeNotForbidenHeaderNames = CorsUnsafeNotForbiddenRequestHeaderNames(request.Headers()); + for (const auto name : unsafeNotForbidenHeaderNames) { + if (controlValues.AllowedHeaders.find(name) == controlValues.AllowedHeaders.cend()) + throw hresult_error{ + E_INVALIDARG, + L"Request header field [" + to_hstring(name) + + L"] is not allowed by Access-Control-Allow-Headers in preflight response" }; + } + } + + // When withCredentials is false, request cannot include cookies. Also, cookies will be ignored in responses. + removeAllCookies = !withCredentials && GetRuntimeOptionBool("Http.RemoveCookiesFromResponse"); + } // originPolicy == SimpleCrossOriginResourceSharing || CrossOriginResourceSharing - // #9770 - insert into preflight cache + // Don't expose HttpOnly cookies to JavaScript + RemoveHttpOnlyCookiesFromResponseHeaders(response, removeAllCookies); } - // See 10.7.4 of https://fetch.spec.whatwg.org/#http-network-or-cache-fetch - void OriginPolicyHttpFilter::ValidateResponse( - HttpResponseMessage const &response, const OriginPolicy originPolicy) const { - bool removeAllCookies = false; - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || - originPolicy == OriginPolicy::CrossOriginResourceSharing) { - auto controlValues = ExtractAccessControlValues(response.Headers()); - auto props = response.RequestMessage().Properties(); - auto withCredentials = props.Lookup(L"RequestArgs").try_as()->WithCredentials; - - if (GetRuntimeOptionBool("Http.StrictOriginCheckSimpleCors") && - originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { - bool originAllowed = false; - for (const auto &header : response.Headers()) { - if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { - originAllowed |= L"*" == header.Value() || s_origin == Uri{header.Value()}; - } - } + ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage const& request) const { + auto coRequest = request; - if (!originAllowed) { - throw hresult_error{E_INVALIDARG, L"The server does not support CORS or the origin is not allowed"}; - } - } else { - ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props); + HttpRequestMessage preflightRequest; + + // Section 4.8.2 https://fetch.spec.whatwg.org/#cors-preflight-fetch + preflightRequest.Method(HttpMethod::Options()); + preflightRequest.RequestUri(coRequest.RequestUri()); + preflightRequest.Headers().Insert(L"Accept", L"*/*"); + preflightRequest.Headers().Insert(L"Access-Control-Request-Method", coRequest.Method().ToString()); + + auto headerNames = wstring{}; + auto writeSeparator = false; + for (const auto& header : coRequest.Headers()) { + if (writeSeparator) { + headerNames += L", "; + } + else { + writeSeparator = true; } - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { - // Filter out response headers that are not in the Simple CORS allowlist - std::queue nonSimpleNames; - for (const auto &header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend()) - nonSimpleNames.push(header.Key()); - } + headerNames += header.Key(); + } - while (!nonSimpleNames.empty()) { - response.Headers().Remove(nonSimpleNames.front()); - nonSimpleNames.pop(); + if (coRequest.Content()) { + for (const auto& header : coRequest.Content().Headers()) { + if (writeSeparator) { + headerNames += L", "; + } + else { + writeSeparator = true; } - } else { - // Filter out response headers that are not simple headers and not in expose list - // Keep simple headers and those found in the expose header list. - if (withCredentials || controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { - std::queue nonSimpleNonExposedHeaders; + // See 10.7.4 of https://fetch.spec.whatwg.org/#http-network-or-cache-fetch + void OriginPolicyHttpFilter::ValidateResponse( + HttpResponseMessage const& response, const OriginPolicy originPolicy) const { + bool removeAllCookies = false; + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + originPolicy == OriginPolicy::CrossOriginResourceSharing) { + auto controlValues = ExtractAccessControlValues(response.Headers()); + auto props = response.RequestMessage().Properties(); + auto withCredentials = props.Lookup(L"RequestArgs").try_as()->WithCredentials; + + if (GetRuntimeOptionBool("Http.StrictOriginCheckSimpleCors") && + originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { + bool originAllowed = false; + for (const auto& header : response.Headers()) { + if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { + originAllowed |= L"*" == header.Value() || s_origin == Uri{ header.Value() }; + } + } - for (const auto &header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == - s_simpleCorsResponseHeaderNames.cend() && - controlValues.ExposedHeaders.find(header.Key().c_str()) == controlValues.ExposedHeaders.cend()) { - nonSimpleNonExposedHeaders.push(header.Key()); + if (!originAllowed) { + throw hresult_error{ E_INVALIDARG, L"The server does not support CORS or the origin is not allowed" }; + } + } + else { + ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props); } - } - if (!originAllowed) { - throw hresult_error{E_INVALIDARG, L"The server does not support CORS or the origin is not allowed"}; - } - } else { - ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props); - } + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { + // Filter out response headers that are not in the Simple CORS allowlist + std::queue nonSimpleNames; + for (const auto& header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend()) + nonSimpleNames.push(header.Key()); + } - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { - // Filter out response headers that are not in the Simple CORS whitelist - std::queue nonSimpleNames; - for (const auto &header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == - s_simpleCorsResponseHeaderNames.cend()) - nonSimpleNames.push(header.Key()); - } + while (!nonSimpleNames.empty()) { + response.Headers().Remove(nonSimpleNames.front()); + nonSimpleNames.pop(); + } + } + else { + // Filter out response headers that are not simple headers and not in expose list - while (!nonSimpleNames.empty()) { - response.Headers().Remove(nonSimpleNames.front()); - nonSimpleNames.pop(); - } - } else { - // Filter out response headers that are not simple headers and not in expose list + // Keep simple headers and those found in the expose header list. + if (withCredentials || controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { + std::queue nonSimpleNonExposedHeaders; - // Keep simple headers and those found in the expose header list. - if (withCredentials || controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { - std::queue nonSimpleNonExposedHeaders; + for (const auto& header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == + s_simpleCorsResponseHeaderNames.cend() && + controlValues.ExposedHeaders.find(header.Key().c_str()) == controlValues.ExposedHeaders.cend()) { + nonSimpleNonExposedHeaders.push(header.Key()); + } + } + + if (!originAllowed) { + throw hresult_error{ E_INVALIDARG, L"The server does not support CORS or the origin is not allowed" }; + } + } + else { + ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props); + } - for (const auto &header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == - s_simpleCorsResponseHeaderNames.cend() && - controlValues.ExposedHeaders.find(header.Key().c_str()) == controlValues.ExposedHeaders.cend()) { - nonSimpleNonExposedHeaders.push(header.Key()); + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { + // Filter out response headers that are not in the Simple CORS whitelist + std::queue nonSimpleNames; + for (const auto& header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == + s_simpleCorsResponseHeaderNames.cend()) + nonSimpleNames.push(header.Key()); + } + + while (!nonSimpleNames.empty()) { + response.Headers().Remove(nonSimpleNames.front()); + nonSimpleNames.pop(); + } } - } + else { + // Filter out response headers that are not simple headers and not in expose list - while (!nonSimpleNonExposedHeaders.empty()) { - response.Headers().Remove(nonSimpleNonExposedHeaders.front()); - nonSimpleNonExposedHeaders.pop(); - } - } - } + // Keep simple headers and those found in the expose header list. + if (withCredentials || controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { + std::queue nonSimpleNonExposedHeaders; - // When withCredentials is false, request cannot include cookies. Also, cookies will be ignored in - // responses. - removeAllCookies = !withCredentials && GetRuntimeOptionBool("Http.RemoveCookiesFromResponse"); - } // originPolicy == SimpleCrossOriginResourceSharing || CrossOriginResourceSharing + for (const auto& header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == + s_simpleCorsResponseHeaderNames.cend() && + controlValues.ExposedHeaders.find(header.Key().c_str()) == controlValues.ExposedHeaders.cend()) { + nonSimpleNonExposedHeaders.push(header.Key()); + } + } + + while (!nonSimpleNonExposedHeaders.empty()) { + response.Headers().Remove(nonSimpleNonExposedHeaders.front()); + nonSimpleNonExposedHeaders.pop(); + } + } + } - // Don't expose HttpOnly cookies to JavaScript - RemoveHttpOnlyCookiesFromResponseHeaders(response, removeAllCookies); - } + // When withCredentials is false, request cannot include cookies. Also, cookies will be ignored in + // responses. + removeAllCookies = !withCredentials && GetRuntimeOptionBool("Http.RemoveCookiesFromResponse"); + } // originPolicy == SimpleCrossOriginResourceSharing || CrossOriginResourceSharing - ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage const &request) const { - auto coRequest = request; + // Don't expose HttpOnly cookies to JavaScript + RemoveHttpOnlyCookiesFromResponseHeaders(response, removeAllCookies); + } - HttpRequestMessage preflightRequest; + ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage const& request) const { + auto coRequest = request; - // Section 4.8.2 https://fetch.spec.whatwg.org/#cors-preflight-fetch - preflightRequest.Method(HttpMethod::Options()); - preflightRequest.RequestUri(coRequest.RequestUri()); - preflightRequest.Headers().Insert(L"Accept", L"*/*"); - preflightRequest.Headers().Insert(L"Access-Control-Request-Method", coRequest.Method().ToString()); + HttpRequestMessage preflightRequest; - auto headerNames = wstring{}; - auto headerItr = coRequest.Headers().begin(); - if (headerItr != coRequest.Headers().end()) { - headerNames += (*headerItr).Key(); + // Section 4.8.2 https://fetch.spec.whatwg.org/#cors-preflight-fetch + preflightRequest.Method(HttpMethod::Options()); + preflightRequest.RequestUri(coRequest.RequestUri()); + preflightRequest.Headers().Insert(L"Accept", L"*/*"); + preflightRequest.Headers().Insert(L"Access-Control-Request-Method", coRequest.Method().ToString()); - while (++headerItr != coRequest.Headers().end()) - headerNames += L", " + (*headerItr).Key(); - } + auto headerNames = wstring{}; + auto headerItr = coRequest.Headers().begin(); + if (headerItr != coRequest.Headers().end()) { + headerNames += (*headerItr).Key(); - if (coRequest.Content()) { - headerItr = coRequest.Content().Headers().begin(); - if (headerItr != coRequest.Content().Headers().end()) { - headerNames += (*headerItr).Key(); + while (++headerItr != coRequest.Headers().end()) + headerNames += L", " + (*headerItr).Key(); + } - while (++headerItr != coRequest.Content().Headers().end()) - headerNames += L", " + (*headerItr).Key(); - } - } + if (coRequest.Content()) { + headerItr = coRequest.Content().Headers().begin(); + if (headerItr != coRequest.Content().Headers().end()) { + headerNames += (*headerItr).Key(); + + while (++headerItr != coRequest.Content().Headers().end()) + headerNames += L", " + (*headerItr).Key(); + } + } - preflightRequest.Headers().Insert(L"Access-Control-Request-Headers", headerNames); - preflightRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); - preflightRequest.Headers().Insert(L"Sec-Fetch-Mode", L"CORS"); + preflightRequest.Headers().Insert(L"Access-Control-Request-Headers", headerNames); + preflightRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); + preflightRequest.Headers().Insert(L"Sec-Fetch-Mode", L"CORS"); - co_return {co_await m_innerFilter.SendRequestAsync(preflightRequest)}; - } + 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; - } - } + 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; - } + return true; + } #pragma endregion IRedirectEventSource #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; - } - } + 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; - } + return true; + } #pragma endregion IRedirectEventSource #pragma region IHttpFilter - ResponseOperation OriginPolicyHttpFilter::SendRequestAsync(HttpRequestMessage const &request) { - auto coRequest = request; + ResponseOperation OriginPolicyHttpFilter::SendRequestAsync(HttpRequestMessage const& request) { + auto coRequest = request; - // Set initial origin policy to global runtime option. - request.Properties().Insert(L"OriginPolicy", winrt::box_value(GetRuntimeOptionInt("Http.OriginPolicy"))); + // Set initial origin policy to global runtime option. + request.Properties().Insert(L"OriginPolicy", winrt::box_value(GetRuntimeOptionInt("Http.OriginPolicy"))); - // Allow only HTTP or HTTPS schemes - if (GetRuntimeOptionBool("Http.StrictScheme") && coRequest.RequestUri().SchemeName() != L"https" && - coRequest.RequestUri().SchemeName() != L"http") - throw hresult_error{E_INVALIDARG, L"Invalid URL scheme: [" + s_origin.SchemeName() + L"]"}; + // Allow only HTTP or HTTPS schemes + if (GetRuntimeOptionBool("Http.StrictScheme") && coRequest.RequestUri().SchemeName() != L"https" && + coRequest.RequestUri().SchemeName() != L"http") + throw hresult_error{ E_INVALIDARG, L"Invalid URL scheme: [" + s_origin.SchemeName() + L"]" }; - if (!GetRuntimeOptionBool("Http.OmitCredentials")) { - coRequest.Properties().Lookup(L"RequestArgs").as()->WithCredentials = false; - } + if (!GetRuntimeOptionBool("Http.OmitCredentials")) { + coRequest.Properties().Lookup(L"RequestArgs").as()->WithCredentials = false; + } - // Ensure absolute URL - coRequest.RequestUri(Uri{coRequest.RequestUri().AbsoluteCanonicalUri()}); + // Ensure absolute URL + coRequest.RequestUri(Uri{ coRequest.RequestUri().AbsoluteCanonicalUri() }); - auto originPolicy = ValidateRequest(coRequest); - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || - originPolicy == OriginPolicy::CrossOriginResourceSharing) { - if (coRequest.RequestUri().UserName().size() > 0 || coRequest.RequestUri().Password().size() > 0) { - coRequest.RequestUri(Uri{coRequest.RequestUri().DisplayUri()}); - } - } + auto originPolicy = ValidateRequest(coRequest); + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + originPolicy == OriginPolicy::CrossOriginResourceSharing) { + if (coRequest.RequestUri().UserName().size() > 0 || coRequest.RequestUri().Password().size() > 0) { + coRequest.RequestUri(Uri{ coRequest.RequestUri().DisplayUri() }); + } + } - try { - // #9770 - Validate preflight cache - if (originPolicy == OriginPolicy::CrossOriginResourceSharing) { - // If inner filter can AllowRedirect, disable for preflight. - winrt::impl::com_ref baseFilter; - if (baseFilter = m_innerFilter.try_as()) { - baseFilter.AllowAutoRedirect(false); - } + try { + // #9770 - Validate preflight cache + if (originPolicy == OriginPolicy::CrossOriginResourceSharing) { + // If inner filter can AllowRedirect, disable for preflight. + winrt::impl::com_ref baseFilter; + if (baseFilter = m_innerFilter.try_as()) { + baseFilter.AllowAutoRedirect(false); + } - auto preflightResponse = co_await SendPreflightAsync(coRequest); + auto preflightResponse = co_await SendPreflightAsync(coRequest); - if (baseFilter) { - baseFilter.AllowAutoRedirect(true); - } + if (baseFilter) { + baseFilter.AllowAutoRedirect(true); + } - ValidatePreflightResponse(coRequest, preflightResponse); - } + ValidatePreflightResponse(coRequest, preflightResponse); + } - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || - originPolicy == OriginPolicy::CrossOriginResourceSharing) { - coRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); - } + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + originPolicy == OriginPolicy::CrossOriginResourceSharing) { + coRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); + } - auto response = co_await m_innerFilter.SendRequestAsync(coRequest); + auto response = co_await m_innerFilter.SendRequestAsync(coRequest); - ValidateResponse(response, originPolicy); + ValidateResponse(response, originPolicy); - co_return response; + co_return response; - } catch (hresult_error const &e) { - throw e; - } catch (const std::exception &e) { - throw hresult_error{E_FAIL, to_hstring(e.what())}; - } catch (...) { - throw hresult_error{E_FAIL, L"Unspecified error processing Origin Policy request"}; - } - } + } + catch (hresult_error const& e) { + throw e; + } + catch (const std::exception& e) { + throw hresult_error{ E_FAIL, to_hstring(e.what()) }; + } + catch (...) { + throw hresult_error{ E_FAIL, L"Unspecified error processing Origin Policy request" }; + } + } #pragma endregion IHttpFilter #pragma endregion OriginPolicyHttpFilter - } // namespace Microsoft::React::Networking + } // namespace Microsoft::React::Networking diff --git a/vnext/Shared/Networking/RedirectHttpFilter.cpp b/vnext/Shared/Networking/RedirectHttpFilter.cpp index a5248e031f8..a1fd8eedcc5 100644 --- a/vnext/Shared/Networking/RedirectHttpFilter.cpp +++ b/vnext/Shared/Networking/RedirectHttpFilter.cpp @@ -5,6 +5,8 @@ #include "RedirectHttpFilter.h" +// React Native Windows +#include #include "WinRTTypes.h" // Windows API @@ -211,6 +213,13 @@ ResponseOperation RedirectHttpFilter::SendRequestAsync(HttpRequestMessage const method = coRequest.Method(); do { + // Set User-Agent + // See https://fetch.spec.whatwg.org/#http-network-or-cache-fetch + auto userAgent = GetRuntimeOptionString("Http.UserAgent"); + if (userAgent.size() > 0) { + coRequest.Headers().Append(L"User-Agent", winrt::to_hstring(userAgent)); + } + // Send subsequent requests through the filter that doesn't have the credentials included in the first request response = co_await (redirectCount > 0 ? m_innerFilterWithNoCredentials : m_innerFilter).SendRequestAsync(coRequest); diff --git a/vnext/Shared/Shared.vcxitems.filters b/vnext/Shared/Shared.vcxitems.filters index bc53343114c..d96476057d9 100644 --- a/vnext/Shared/Shared.vcxitems.filters +++ b/vnext/Shared/Shared.vcxitems.filters @@ -155,6 +155,9 @@ Source Files\Networking + + Source Files\Modules + @@ -469,6 +472,15 @@ Header Files\Networking + + Header Files\Modules + + + Header Files\Modules + + + Header Files\Modules + From f17d42d3f7e8b07f83668b3dd36b4b86cbdf5ef5 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Wed, 7 Dec 2022 02:26:29 -0800 Subject: [PATCH 25/45] Remove change file --- ...ative-windows-a912b218-3e03-4b76-8b54-d9cd603a29c0.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 change/react-native-windows-a912b218-3e03-4b76-8b54-d9cd603a29c0.json diff --git a/change/react-native-windows-a912b218-3e03-4b76-8b54-d9cd603a29c0.json b/change/react-native-windows-a912b218-3e03-4b76-8b54-d9cd603a29c0.json deleted file mode 100644 index db12e10e94e..00000000000 --- a/change/react-native-windows-a912b218-3e03-4b76-8b54-d9cd603a29c0.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "prerelease", - "comment": "Set User-Agent header in Origin Policy filter", - "packageName": "react-native-windows", - "email": "julio.rocha@microsoft.com", - "dependentChangeType": "patch" -} From f4540ef5593a88aa5beee9955f4a9bcf7dcf8b71 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Wed, 7 Dec 2022 02:27:20 -0800 Subject: [PATCH 26/45] Fix OInstance.h --- vnext/Shared/OInstance.h | 1 + 1 file changed, 1 insertion(+) diff --git a/vnext/Shared/OInstance.h b/vnext/Shared/OInstance.h index ff4e013ef3a..d09e2d25353 100644 --- a/vnext/Shared/OInstance.h +++ b/vnext/Shared/OInstance.h @@ -10,6 +10,7 @@ #include "InstanceManager.h" // React Native +#include #include // Standard Libriary From 1bb2c962f0c67da439a1beac79f151499d8b8880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julio=20C=C3=A9sar=20Rocha?= Date: Mon, 14 Nov 2022 15:15:59 -0800 Subject: [PATCH 27/45] Always instantiate Blob module in UWP (#10848) * Always instantiate Blob module inUWP * Change files * Remove legacy HTTP module from DLL boundary --- ...-3ee0cf72-a448-421f-a738-e2aac37a7883.json | 7 ++ vnext/Desktop.DLL/react-native-win32.x64.def | 1 - vnext/Desktop.DLL/react-native-win32.x86.def | 1 - .../RNTesterIntegrationTests.cpp | 1 - .../Base/CoreNativeModules.cpp | 77 +++++-------------- vnext/Shared/OInstance.cpp | 23 +++--- vnext/Shared/Shared.vcxitems | 4 +- 7 files changed, 42 insertions(+), 72 deletions(-) create mode 100644 change/react-native-windows-3ee0cf72-a448-421f-a738-e2aac37a7883.json diff --git a/change/react-native-windows-3ee0cf72-a448-421f-a738-e2aac37a7883.json b/change/react-native-windows-3ee0cf72-a448-421f-a738-e2aac37a7883.json new file mode 100644 index 00000000000..c1e69744372 --- /dev/null +++ b/change/react-native-windows-3ee0cf72-a448-421f-a738-e2aac37a7883.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Always instantiate Blob module inUWP", + "packageName": "react-native-windows", + "email": "julio.rocha@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Desktop.DLL/react-native-win32.x64.def b/vnext/Desktop.DLL/react-native-win32.x64.def index f8d5742909f..eec2862ff77 100644 --- a/vnext/Desktop.DLL/react-native-win32.x64.def +++ b/vnext/Desktop.DLL/react-native-win32.x64.def @@ -56,7 +56,6 @@ EXPORTS ?makeChakraRuntime@JSI@Microsoft@@YA?AV?$unique_ptr@VRuntime@jsi@facebook@@U?$default_delete@VRuntime@jsi@facebook@@@std@@@std@@$$QEAUChakraRuntimeArgs@12@@Z ?Make@IHttpResource@Networking@React@Microsoft@@SA?AV?$shared_ptr@UIHttpResource@Networking@React@Microsoft@@@std@@XZ ?CreateTimingModule@react@facebook@@YA?AV?$unique_ptr@VCxxModule@module@xplat@facebook@@U?$default_delete@VCxxModule@module@xplat@facebook@@@std@@@std@@AEBV?$shared_ptr@VMessageQueueThread@react@facebook@@@4@@Z -??0NetworkingModule@React@Microsoft@@QEAA@XZ ?MakeJSQueueThread@ReactNative@Microsoft@@YA?AV?$shared_ptr@VMessageQueueThread@react@facebook@@@std@@XZ ?Hash128@SpookyHashV2@hash@folly@@SAXPEBX_KPEA_K2@Z ??1Instance@react@facebook@@QEAA@XZ diff --git a/vnext/Desktop.DLL/react-native-win32.x86.def b/vnext/Desktop.DLL/react-native-win32.x86.def index d899f933715..f9b316cb1b0 100644 --- a/vnext/Desktop.DLL/react-native-win32.x86.def +++ b/vnext/Desktop.DLL/react-native-win32.x86.def @@ -52,7 +52,6 @@ EXPORTS ?makeChakraRuntime@JSI@Microsoft@@YG?AV?$unique_ptr@VRuntime@jsi@facebook@@U?$default_delete@VRuntime@jsi@facebook@@@std@@@std@@$$QAUChakraRuntimeArgs@12@@Z ?CreateTimingModule@react@facebook@@YG?AV?$unique_ptr@VCxxModule@module@xplat@facebook@@U?$default_delete@VCxxModule@module@xplat@facebook@@@std@@@std@@ABV?$shared_ptr@VMessageQueueThread@react@facebook@@@4@@Z ?Make@IHttpResource@Networking@React@Microsoft@@SG?AV?$shared_ptr@UIHttpResource@Networking@React@Microsoft@@@std@@XZ -??0NetworkingModule@React@Microsoft@@QAE@XZ ?MakeJSQueueThread@ReactNative@Microsoft@@YG?AV?$shared_ptr@VMessageQueueThread@react@facebook@@@std@@XZ ?Hash128@SpookyHashV2@hash@folly@@SGXPBXIPA_K1@Z ?assertionFailure@detail@folly@@YGXPBD00I0H@Z diff --git a/vnext/Desktop.IntegrationTests/RNTesterIntegrationTests.cpp b/vnext/Desktop.IntegrationTests/RNTesterIntegrationTests.cpp index 0813355bf0a..4ed441749b3 100644 --- a/vnext/Desktop.IntegrationTests/RNTesterIntegrationTests.cpp +++ b/vnext/Desktop.IntegrationTests/RNTesterIntegrationTests.cpp @@ -27,7 +27,6 @@ TEST_MODULE_INITIALIZE(InitModule) { SetRuntimeOptionBool("WebSocket.AcceptSelfSigned", true); SetRuntimeOptionBool("UseBeastWebSocket", false); - SetRuntimeOptionBool("Http.UseMonolithicModule", false); SetRuntimeOptionBool("Blob.EnableModule", true); // WebSocketJSExecutor can't register native log hooks. diff --git a/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp b/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp index 692db7bf8ec..8407dc32dce 100644 --- a/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp +++ b/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp @@ -19,81 +19,44 @@ 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( - winrt::name_of()); - auto abiPackageStatics = static_cast *>( - winrt::get_abi(packageStatics)); - winrt::com_ptr> dummy; - return abiPackageStatics->get_Current(winrt::put_abi(dummy)) != - winrt::impl::hresult_from_win32(APPMODEL_ERROR_NO_PACKAGE); - }(); - - return hasPackageIdentity; -} - -ReactPropertyId HttpUseMonolithicModuleProperty() noexcept { - static ReactPropertyId propId{ - L"ReactNative.Http" - L"UseMonolithicModule"}; - return propId; -} - -} // namespace - -std::vector GetCoreModules( - const std::shared_ptr &batchingUIMessageQueue, + std::vector GetCoreModules( + const std::shared_ptr& batchingUIMessageQueue, const std::shared_ptr - &jsMessageQueue, // JS engine thread (what we use for external modules) - Mso::CntPtr &&appearanceListener, - Mso::CntPtr &&context) noexcept { - std::vector modules; + & jsMessageQueue, // JS engine thread (what we use for external modules) + Mso::CntPtr&& appearanceListener, + Mso::CntPtr&& context) noexcept { + std::vector modules; - modules.emplace_back( + modules.emplace_back( "Networking", [props = context->Properties()]() { return Microsoft::React::CreateHttpModule(props); }, jsMessageQueue); - modules.emplace_back( - "Timing", - [batchingUIMessageQueue]() { return facebook::react::CreateTimingModule(batchingUIMessageQueue); }, + modules.emplace_back( + Microsoft::React::GetBlobModuleName(), + [props = context->Properties()]() { return Microsoft::React::CreateBlobModule(props); }, 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)); }, + modules.emplace_back( + Microsoft::React::GetFileReaderModuleName(), + [props = context->Properties()]() { return Microsoft::React::CreateFileReaderModule(props); }, batchingUIMessageQueue); - modules.emplace_back( - AppearanceModule::Name, - [appearanceListener = std::move(appearanceListener)]() mutable { - return std::make_unique(std::move(appearanceListener)); - }, - jsMessageQueue); - - // AsyncStorageModule doesn't work without package identity (it indirectly depends on - // Windows.Storage.StorageFile), so check for package identity before adding it. - modules.emplace_back( + // AsyncStorageModule doesn't work without package identity (it indirectly depends on + // Windows.Storage.StorageFile), so check for package identity before adding it. + modules.emplace_back( "AsyncLocalStorage", []() -> std::unique_ptr { if (HasPackageIdentity()) { return std::make_unique(L"asyncStorage"); - } else { + } + else { return std::make_unique(); } }, jsMessageQueue); - return modules; -} + return modules; + } } // namespace Microsoft::ReactNative diff --git a/vnext/Shared/OInstance.cpp b/vnext/Shared/OInstance.cpp index 327e7041218..f070946f431 100644 --- a/vnext/Shared/OInstance.cpp +++ b/vnext/Shared/OInstance.cpp @@ -27,7 +27,6 @@ #include #include -#include #include #include #include @@ -73,11 +72,7 @@ namespace Microsoft::React { /*extern*/ std::unique_ptr CreateHttpModule( winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept { - if (GetRuntimeOptionBool("Http.UseMonolithicModule")) { - return std::make_unique(); - } else { - return std::make_unique(inspectableProperties); - } + return std::make_unique(inspectableProperties); } } // namespace Microsoft::React @@ -633,11 +628,17 @@ std::vector> InstanceImpl::GetDefaultNativeModules []() { return std::make_unique(); }, nativeQueue)); - modules.push_back(std::make_unique( - m_innerInstance, - Microsoft::React::GetBlobModuleName(), - [transitionalProps]() { return Microsoft::React::CreateBlobModule(transitionalProps); }, - nativeQueue)); + // 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")) { + modules.push_back(std::make_unique( + m_innerInstance, + Microsoft::React::GetBlobModuleName(), + [transitionalProps]() { return Microsoft::React::CreateBlobModule(transitionalProps); }, + nativeQueue)); modules.push_back(std::make_unique( m_innerInstance, diff --git a/vnext/Shared/Shared.vcxitems b/vnext/Shared/Shared.vcxitems index b08b35b58ea..ebca7b446c0 100644 --- a/vnext/Shared/Shared.vcxitems +++ b/vnext/Shared/Shared.vcxitems @@ -50,7 +50,9 @@ - + + true + From 7c754d68a47e844883425c42756e5f793b3f7819 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Wed, 7 Dec 2022 02:33:18 -0800 Subject: [PATCH 28/45] Remove change file --- ...ative-windows-3ee0cf72-a448-421f-a738-e2aac37a7883.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 change/react-native-windows-3ee0cf72-a448-421f-a738-e2aac37a7883.json diff --git a/change/react-native-windows-3ee0cf72-a448-421f-a738-e2aac37a7883.json b/change/react-native-windows-3ee0cf72-a448-421f-a738-e2aac37a7883.json deleted file mode 100644 index c1e69744372..00000000000 --- a/change/react-native-windows-3ee0cf72-a448-421f-a738-e2aac37a7883.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "prerelease", - "comment": "Always instantiate Blob module inUWP", - "packageName": "react-native-windows", - "email": "julio.rocha@microsoft.com", - "dependentChangeType": "patch" -} From 72ff282b51f52aea79f40fe1a5558d6562635ab5 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Wed, 7 Dec 2022 02:36:46 -0800 Subject: [PATCH 29/45] Fix CoreNativeModules.cpp --- .../Base/CoreNativeModules.cpp | 68 ++++++++++++++----- 1 file changed, 52 insertions(+), 16 deletions(-) diff --git a/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp b/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp index 8407dc32dce..cc0cfc04438 100644 --- a/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp +++ b/vnext/Microsoft.ReactNative/Base/CoreNativeModules.cpp @@ -19,44 +19,80 @@ namespace Microsoft::ReactNative { - std::vector GetCoreModules( - const std::shared_ptr& batchingUIMessageQueue, +namespace { + +bool HasPackageIdentity() noexcept { + static const bool hasPackageIdentity = []() noexcept { + auto packageStatics = winrt::get_activation_factory( + winrt::name_of()); + auto abiPackageStatics = static_cast *>( + winrt::get_abi(packageStatics)); + winrt::com_ptr> dummy; + return abiPackageStatics->get_Current(winrt::put_abi(dummy)) != + winrt::impl::hresult_from_win32(APPMODEL_ERROR_NO_PACKAGE); + }(); + + return hasPackageIdentity; +} + +} // namespace + +std::vector GetCoreModules( + const std::shared_ptr &batchingUIMessageQueue, const std::shared_ptr - & jsMessageQueue, // JS engine thread (what we use for external modules) - Mso::CntPtr&& appearanceListener, - Mso::CntPtr&& context) noexcept { - std::vector modules; + &jsMessageQueue, // JS engine thread (what we use for external modules) + Mso::CntPtr &&appearanceListener, + Mso::CntPtr &&context) noexcept { + std::vector modules; - modules.emplace_back( + modules.emplace_back( "Networking", [props = context->Properties()]() { return Microsoft::React::CreateHttpModule(props); }, jsMessageQueue); - modules.emplace_back( + modules.emplace_back( Microsoft::React::GetBlobModuleName(), [props = context->Properties()]() { return Microsoft::React::CreateBlobModule(props); }, batchingUIMessageQueue); - modules.emplace_back( + modules.emplace_back( Microsoft::React::GetFileReaderModuleName(), [props = context->Properties()]() { return Microsoft::React::CreateFileReaderModule(props); }, batchingUIMessageQueue); - // AsyncStorageModule doesn't work without package identity (it indirectly depends on - // Windows.Storage.StorageFile), so check for package identity before adding it. - modules.emplace_back( + 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)); }, + batchingUIMessageQueue); + + modules.emplace_back( + AppearanceModule::Name, + [appearanceListener = std::move(appearanceListener)]() mutable { + return std::make_unique(std::move(appearanceListener)); + }, + jsMessageQueue); + + // AsyncStorageModule doesn't work without package identity (it indirectly depends on + // Windows.Storage.StorageFile), so check for package identity before adding it. + modules.emplace_back( "AsyncLocalStorage", []() -> std::unique_ptr { if (HasPackageIdentity()) { return std::make_unique(L"asyncStorage"); - } - else { + } else { return std::make_unique(); } }, jsMessageQueue); - return modules; - } + return modules; +} } // namespace Microsoft::ReactNative From 2ff49bb8629e821d0e22cddea0758412211dff81 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Wed, 7 Dec 2022 02:40:14 -0800 Subject: [PATCH 30/45] Revert OP integration tests --- .../HttpOriginPolicyIntegrationTest.cpp | 1231 ++++++++--------- 1 file changed, 579 insertions(+), 652 deletions(-) diff --git a/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp b/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp index e9ba7d19bd3..c9026cc9831 100644 --- a/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp +++ b/vnext/Desktop.IntegrationTests/HttpOriginPolicyIntegrationTest.cpp @@ -30,54 +30,65 @@ constexpr char s_crossOriginUrl[]{"http://example.rnw"}; // clang-format off namespace Microsoft::React::Test { - TEST_CLASS(HttpOriginPolicyIntegrationTest) +TEST_CLASS(HttpOriginPolicyIntegrationTest) +{ + static constexpr bool s_shouldSucceed{true}; + static constexpr bool s_shouldFail{false}; + + static uint16_t s_port; + + struct ServerParams { - static constexpr bool s_shouldSucceed{ true }; - static constexpr bool s_shouldFail{ false }; + uint16_t Port; + string Url; + EmptyResponse Preflight; + StringResponse Response; + + ServerParams( + uint16_t port) noexcept + : Port{port} + , Url{s_serverHost + string{":"} + std::to_string(port)} + { + Preflight.set(http::field::access_control_allow_methods, "GET, POST, DELETE, PATCH"); - static uint16_t s_port; + Response.result(http::status::unknown); + Response.body() = "RESPONSE_CONTENT"; + } + }; - struct ServerParams + struct ClientParams + { + promise ContentPromise; + string ErrorMessage; + IHttpResource::Response Response; + string ResponseContent; + http::verb Method; + IHttpResource::Headers RequestHeaders; + bool WithCredentials{false}; + + ClientParams(http::verb method, IHttpResource::Headers&& headers) + : Method{ method } + , RequestHeaders{ std::move(headers) } { - uint16_t Port; - string Url; - EmptyResponse Preflight; - StringResponse Response; - - ServerParams( - uint16_t port) noexcept - : Port{ port } - , Url{ s_serverHost + string{":"} + std::to_string(port) } - { - Preflight.set(http::field::access_control_allow_methods, "GET, POST, DELETE, PATCH"); + } + }; - Response.result(http::status::unknown); - Response.body() = "RESPONSE_CONTENT"; - } + std::shared_ptr CreateServer(ServerParams& serverArgs, ClientParams& clientArgs) noexcept + { + auto server = make_shared(serverArgs.Port); + server->Callbacks().OnOptions = [&serverArgs](const DynamicRequest& request) -> ResponseWrapper + { + return { std::move(serverArgs.Preflight) }; }; - struct ClientParams + auto reqHandler = [&serverArgs](const DynamicRequest& request) -> ResponseWrapper { // Don't use move constructor in case of multiple requests return { serverArgs.Response }; }; - std::shared_ptr CreateServer(ServerParams& serverArgs, ClientParams& clientArgs) noexcept + switch (clientArgs.Method) { - auto server = make_shared(serverArgs.Port); - server->Callbacks().OnOptions = [&serverArgs](const DynamicRequest& request) -> ResponseWrapper - { - return { std::move(serverArgs.Preflight) }; - }; - - auto reqHandler = [&serverArgs](const DynamicRequest& request) -> ResponseWrapper - { - // Don't use move constructor in case of multiple requests - return { serverArgs.Response }; - }; - - switch (clientArgs.Method) - { case http::verb::get: server->Callbacks().OnGet = reqHandler; break; @@ -100,30 +111,39 @@ namespace Microsoft::React::Test { case http::verb::options: default: - Assert::Fail(L"Unsupported request method"); - } - - return server; + Assert::Fail(L"Unsupported request method"); } - void TestOriginPolicyWithRedirect(ServerParams& server1Args, ServerParams& server2Args, ClientParams& clientArgs, bool shouldSucceed) + return server; + } + + void TestOriginPolicyWithRedirect(ServerParams& server1Args, ServerParams& server2Args, ClientParams& clientArgs, bool shouldSucceed) + { + auto server1 = CreateServer(server1Args, clientArgs); + auto server2 = CreateServer(server2Args, clientArgs); + + server1->Start(); + server2->Start(); + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&clientArgs](int64_t, IHttpResource::Response&& response) { clientArgs.Response = std::move(response); }); resource->SetOnData([&clientArgs](int64_t, string&& content) - { + { clientArgs.ResponseContent = std::move(content); - clientArgs.ContentPromise.set_value(); - }); + clientArgs.ContentPromise.set_value(); + }); resource->SetOnError([&clientArgs](int64_t, string&& message, bool) - { - clientArgs.ErrorMessage = std::move(message); - clientArgs.ContentPromise.set_value(); - }); + { + clientArgs.ErrorMessage = std::move(message); + clientArgs.ContentPromise.set_value(); + }); resource->SendRequest( - string{ http::to_string(clientArgs.Method).data() }, - string{ server1Args.Url }, + string{http::to_string(clientArgs.Method).data()}, + string{server1Args.Url}, 0, /*requestId*/ std::move(clientArgs.RequestHeaders), dynamic::object("string", ""), /*data*/ @@ -131,7 +151,7 @@ namespace Microsoft::React::Test { false, /*useIncrementalUpdates*/ 0, /*timeout*/ clientArgs.WithCredentials, /*withCredentials*/ - [](int64_t) {} /*reactCallback*/ + [](int64_t){} /*reactCallback*/ ); clientArgs.ContentPromise.get_future().wait(); @@ -145,7 +165,7 @@ namespace Microsoft::React::Test { //TODO: chose server? // We assume 2-server tests will always redirect so the final response will come from server 2. Assert::AreEqual(server2Args.Response.result_int(), static_cast(clientArgs.Response.StatusCode)); - Assert::AreEqual({ "RESPONSE_CONTENT" }, clientArgs.ResponseContent); + Assert::AreEqual({"RESPONSE_CONTENT"}, clientArgs.ResponseContent); } else { @@ -161,39 +181,23 @@ namespace Microsoft::React::Test { auto resource = IHttpResource::Make(); resource->SetOnResponse([&clientArgs](int64_t, IHttpResource::Response&& res) - { - clientArgs.Response = std::move(res); - }); - resource->SetOnData([&clientArgs](int64_t, string&& content) - { - clientArgs.ResponseContent = std::move(content); - clientArgs.ContentPromise.set_value(); - }); - resource->SetOnError([&clientArgs](int64_t, string&& message, bool) - { - auto server = CreateServer(serverArgs, clientArgs); - - server->Start(); - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&clientArgs](int64_t, IHttpResource::Response&& res) - { + { clientArgs.Response = std::move(res); - }); + }); resource->SetOnData([&clientArgs](int64_t, string&& content) - { - clientArgs.ResponseContent = std::move(content); - clientArgs.ContentPromise.set_value(); - }); + { + clientArgs.ResponseContent = std::move(content); + clientArgs.ContentPromise.set_value(); + }); resource->SetOnError([&clientArgs](int64_t, string&& message, bool) - { - clientArgs.ErrorMessage = std::move(message); - clientArgs.ContentPromise.set_value(); - }); + { + clientArgs.ErrorMessage = std::move(message); + clientArgs.ContentPromise.set_value(); + }); resource->SendRequest( - string{ http::to_string(clientArgs.Method).data() }, - string{ serverArgs.Url }, + string{http::to_string(clientArgs.Method).data()}, + string{serverArgs.Url}, 0, /*requestId*/ std::move(clientArgs.RequestHeaders), dynamic::object("string", ""), /*data*/ @@ -211,692 +215,615 @@ namespace Microsoft::React::Test { { Assert::AreEqual({}, clientArgs.ErrorMessage); Assert::AreEqual(serverArgs.Response.result_int(), static_cast(clientArgs.Response.StatusCode)); - Assert::AreEqual({ "RESPONSE_CONTENT" }, clientArgs.ResponseContent); + Assert::AreEqual({"RESPONSE_CONTENT"}, clientArgs.ResponseContent); } else { Assert::AreNotEqual({}, clientArgs.ErrorMessage); } - } + } - 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); + 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++; + } - // Bug in HttpServer does not correctly release TCP port between test methods. - // Using a different por per test for now. - s_port++; - } + 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 + TEST_IGNORE() + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(NoCorsForbiddenMethodSucceeds) + { + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); + + constexpr uint16_t port{ 5556 }; + constexpr char url[]{ "http://localhost:5556" }; + + string error; + string getContent; + IHttpResource::Response getResponse; + promise getDataPromise; + + auto server = make_shared(port); + server->Callbacks().OnOptions = [&url](const DynamicRequest& request) -> ResponseWrapper + { + EmptyResponse response; + response.result(http::status::accepted); + + response.set(http::field::access_control_allow_credentials, "false"); + response.set(http::field::access_control_allow_headers, "ValidHeader"); + response.set(http::field::access_control_allow_methods, "GET, POST, DELETE, PATCH"); + response.set(http::field::access_control_allow_origin, url); - TEST_METHOD_CLEANUP(MethodCleanup) + return { std::move(response) }; + }; + server->Callbacks().OnTrace = [](const DynamicRequest& request) -> ResponseWrapper + { + StringResponse response; + response.result(http::status::ok); + response.body() = "GET_CONTENT"; + + return { std::move(response) }; + }; + server->Start(); + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&getResponse](int64_t, IHttpResource::Response&& res) { - // 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++; - } - - 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 - TEST_IGNORE() - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(NoCorsForbiddenMethodSucceeds) + getResponse = std::move(res); + }); + resource->SetOnData([&getDataPromise, &getContent](int64_t, string&& content) { - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); - - constexpr uint16_t port{ 5556 }; - constexpr char url[]{ "http://localhost:5556" }; - - string error; - string getContent; - IHttpResource::Response getResponse; - promise getDataPromise; - - auto server = make_shared(port); - server->Callbacks().OnOptions = [&url](const DynamicRequest& request) -> ResponseWrapper - { - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); - - constexpr uint16_t port{ 5556 }; - constexpr char url[]{ "http://localhost:5556" }; - - string error; - string getContent; - IHttpResource::Response getResponse; - promise getDataPromise; - - auto server = make_shared(port); - server->Callbacks().OnOptions = [&url](const DynamicRequest& request) -> ResponseWrapper - { - EmptyResponse response; - response.result(http::status::accepted); - - response.set(http::field::access_control_allow_credentials, "false"); - response.set(http::field::access_control_allow_headers, "ValidHeader"); - response.set(http::field::access_control_allow_methods, "GET, POST, DELETE, PATCH"); - response.set(http::field::access_control_allow_origin, url); - - return { std::move(response) }; - }; - server->Callbacks().OnTrace = [](const DynamicRequest& request) -> ResponseWrapper - { - StringResponse response; - response.result(http::status::ok); - response.body() = "GET_CONTENT"; - - return { std::move(response) }; - }; - server->Start(); - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&getResponse](int64_t, IHttpResource::Response&& res) - { - getResponse = std::move(res); - }); - resource->SetOnData([&getDataPromise, &getContent](int64_t, string&& content) - { - getContent = std::move(content); - getDataPromise.set_value(); - }); - resource->SetOnError([&server, &error, &getDataPromise](int64_t, string&& message, bool) - { - error = std::move(message); - getDataPromise.set_value(); - }); - - resource->SendRequest( - "TRACE", - url, - 0, /*requestId*/ - { - {"ValidHeader", "AnyValue"} - }, - {}, /*data*/ - //{} /*bodyData*/, - "text", - false /*useIncrementalUpdates*/, - 0 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {} /*callback*/ - ); - - getDataPromise.get_future().wait(); - server->Stop(); - - Assert::AreEqual({}, error); - Assert::AreEqual(200, static_cast(getResponse.StatusCode)); - Assert::AreEqual({ "GET_CONTENT" }, getContent); - }// NoCorsForbiddenMethodSucceeds - - BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsForbiddenMethodFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(SimpleCorsForbiddenMethodFails) - { - StringResponse response; - response.result(http::status::ok); - response.body() = "GET_CONTENT"; - - return { std::move(response) }; - }; - server->Start(); - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&getResponse](int64_t, IHttpResource::Response&& res) - { - getResponse = std::move(res); - }); - resource->SetOnData([&getDataPromise, &getContent](int64_t, string&& content) - { - getContent = std::move(content); + getContent = std::move(content); getDataPromise.set_value(); - }); - resource->SetOnError([&server, &error, &getDataPromise](int64_t, string&& message, bool) - { - error = std::move(message); + }); + resource->SetOnError([&server, &error, &getDataPromise](int64_t, string&& message, bool) + { + error = std::move(message); getDataPromise.set_value(); - }); - - resource->SendRequest( - "TRACE", - url, - 0, /*requestId*/ - { - {"ValidHeader", "AnyValue"} - }, - dynamic::object("string", ""), /*data*/ - "text", - false /*useIncrementalUpdates*/, - 0 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {} /*callback*/ - ); - - getDataPromise.get_future().wait(); - server->Stop(); - - Assert::AreEqual({}, error); - Assert::AreEqual(200, static_cast(getResponse.StatusCode)); - Assert::AreEqual({ "GET_CONTENT" }, getContent); - }// NoCorsForbiddenMethodSucceeds - - BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsForbiddenMethodFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(SimpleCorsForbiddenMethodFails) + }); + + resource->SendRequest( + "TRACE", + url, + 0, /*requestId*/ { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); + {"ValidHeader", "AnyValue"} + }, + dynamic::object("string", ""), /*data*/ + "text", + false /*useIncrementalUpdates*/, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {} /*callback*/ + ); - ClientParams clientArgs(http::verb::connect, { {"Content-Type", "text/plain"} }); + getDataPromise.get_future().wait(); + server->Stop(); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); + Assert::AreEqual({}, error); + Assert::AreEqual(200, static_cast(getResponse.StatusCode)); + Assert::AreEqual({ "GET_CONTENT" }, getContent); + }// NoCorsForbiddenMethodSucceeds - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// SimpleCorsForbiddenMethodFails + BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsForbiddenMethodFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(SimpleCorsForbiddenMethodFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); - //NoCors_ForbiddenMethodConnect_Failed + ClientParams clientArgs(http::verb::connect, {{"Content-Type", "text/plain"}}); - BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsCrossOriginFetchRequestSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(NoCorsCrossOriginFetchRequestSucceeds) - { - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::ok); + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// SimpleCorsForbiddenMethodFails - ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); + //NoCors_ForbiddenMethodConnect_Failed - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// NoCorsCrossOriginFetchRequestSucceeds + BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsCrossOriginFetchRequestSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(NoCorsCrossOriginFetchRequestSucceeds) + { + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); - BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsCrossOriginPatchSucceededs) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(NoCorsCrossOriginPatchSucceededs) - { - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); + ServerParams serverArgs(s_port); + serverArgs.Response.result(http::status::ok); - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::ok); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); - ClientParams clientArgs(http::verb::patch, { { "Content-Type", "text/plain" } }); + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// NoCorsCrossOriginFetchRequestSucceeds - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// NoCorsCrossOriginPatchSucceededs + BEGIN_TEST_METHOD_ATTRIBUTE(NoCorsCrossOriginPatchSucceededs) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(NoCorsCrossOriginPatchSucceededs) + { + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::None)); - // Simple-Cors — Prevents the method from being anything other than HEAD, GET or POST, - // and the headers from being anything other than simple headers (CORS safe listed headers). - // If any ServiceWorkers intercept these requests, they may not add or override any headers except for those that are simple headers. - // In addition, JavaScript may not access any properties of the resulting Response. - // This ensures that ServiceWorkers do not affect the semantics of the Web and prevents security and privacy issues arising from leaking data across domains. - BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsSameOriginSucceededs) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(SimpleCorsSameOriginSucceededs) - { - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::ok); + ServerParams serverArgs(s_port); + serverArgs.Response.result(http::status::ok); - ClientParams clientArgs(http::verb::patch, { { "Content-Type", "text/plain" } }); + ClientParams clientArgs(http::verb::patch, {{ "Content-Type", "text/plain" }}); - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// NoCorsCrossOriginPatchSucceededs - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// SimpleCorsSameOriginSucceededs + // Simple-Cors — Prevents the method from being anything other than HEAD, GET or POST, + // and the headers from being anything other than simple headers (CORS safe listed headers). + // If any ServiceWorkers intercept these requests, they may not add or override any headers except for those that are simple headers. + // In addition, JavaScript may not access any properties of the resulting Response. + // This ensures that ServiceWorkers do not affect the semantics of the Web and prevents security and privacy issues arising from leaking data across domains. + BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsSameOriginSucceededs) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(SimpleCorsSameOriginSucceededs) + { + ServerParams serverArgs(s_port); + serverArgs.Response.result(http::status::ok); - BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsCrossOriginFetchFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(SimpleCorsCrossOriginFetchFails) - { - ServerParams serverArgs(s_port); + ClientParams clientArgs(http::verb::patch, {{ "Content-Type", "text/plain" }}); - ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/html" } }); // text/html is a non-simple value + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// SimpleCorsSameOriginSucceededs - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// SimpleCorsCrossOriginFetchFails + BEGIN_TEST_METHOD_ATTRIBUTE(SimpleCorsCrossOriginFetchFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(SimpleCorsCrossOriginFetchFails) + { + ServerParams serverArgs(s_port); - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginRequestSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsSameOriginRequestSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::ok); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/html" }}); // text/html is a non-simple value - ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); // text/plain is a non-simple header + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::SimpleCrossOriginResourceSharing)); - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// SimpleCorsCrossOriginFetchFails - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// FullCorsSameOriginRequestSucceeds + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginRequestSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsSameOriginRequestSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Response.result(http::status::ok); - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginAllowOriginWildcardSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginAllowOriginWildcardSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, "*"); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Response.result(http::status::accepted); - serverArgs.Response.set(http::field::access_control_allow_origin, "*"); - serverArgs.Response.set(http::field::access_control_allow_credentials, "true"); - - ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); // text/plain is a non-simple header - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// FullCorsCrossOriginAllowOriginWildcardSucceeds - - // With CORS, Cross-Origin Resource Sharing, the server can decide what origins are permitted to read information from the client. - // Additionally, for non-simple requests, client should preflight the request through the HTTP Options request, and only send the - // actual request after the server has responded that the desired headers are supported. - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMatchingOriginSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginMatchingOriginSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Response.result(http::status::accepted); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Response.set(http::field::access_control_allow_credentials, "true"); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); // text/plain is a non-simple header - ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); // text/plain is a non-simple header + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// FullCorsSameOriginRequestSucceeds - TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); - }// FullCorsCrossOriginMatchingOriginSucceeds + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginAllowOriginWildcardSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginAllowOriginWildcardSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, "*"); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Response.result(http::status::accepted); + serverArgs.Response.set(http::field::access_control_allow_origin, "*"); + serverArgs.Response.set(http::field::access_control_allow_credentials, "true"); + + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); // text/plain is a non-simple header + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// FullCorsCrossOriginAllowOriginWildcardSucceeds + + // With CORS, Cross-Origin Resource Sharing, the server can decide what origins are permitted to read information from the client. + // Additionally, for non-simple requests, client should preflight the request through the HTTP Options request, and only send the + // actual request after the server has responded that the desired headers are supported. + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMatchingOriginSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginMatchingOriginSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Response.result(http::status::accepted); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Response.set(http::field::access_control_allow_credentials, "true"); - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginWithCredentialsFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginWithCredentialsFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Preflight.set(http::field::access_control_allow_credentials, "true"); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); // text/plain is a non-simple header - ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); // application/text is a non-simple header - clientArgs.WithCredentials = true; + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - SetRuntimeOptionBool("Http.OmitCredentials", true); + TestOriginPolicy(serverArgs, clientArgs, true /*shouldSucceed*/); + }// FullCorsCrossOriginMatchingOriginSucceeds - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// FullCorsCrossOriginWithCredentialsFails + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginWithCredentialsFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginWithCredentialsFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Preflight.set(http::field::access_control_allow_credentials, "true"); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); // application/text is a non-simple header + clientArgs.WithCredentials = true; - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMissingCorsHeadersFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginMissingCorsHeadersFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.erase(http::field::access_control_allow_methods); - serverArgs.Preflight.result(http::status::not_implemented); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + SetRuntimeOptionBool("Http.OmitCredentials", true); - ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); // application/text is a non-simple header + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// FullCorsCrossOriginWithCredentialsFails - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// FullCorsCrossOriginMissingCorsHeadersFails + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMissingCorsHeadersFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginMissingCorsHeadersFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.erase(http::field::access_control_allow_methods); + serverArgs.Preflight.result(http::status::not_implemented); - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMismatchedCorsHeaderFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginMismatchedCorsHeaderFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Response.result(http::status::accepted); - serverArgs.Response.set(http::field::access_control_allow_origin, "http://other.example.rnw"); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); // application/text is a non-simple header - ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); // application/text is a non-simple header + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// FullCorsCrossOriginMissingCorsHeadersFails - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// FullCorsCrossOriginMismatchedCorsHeaderFails + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginMismatchedCorsHeaderFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginMismatchedCorsHeaderFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Response.result(http::status::accepted); + serverArgs.Response.set(http::field::access_control_allow_origin, "http://other.example.rnw"); - // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginCheckFailsOnPreflightRedirectFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginCheckFailsOnPreflightRedirectFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Preflight.set(http::field::location, "http://any-host.extension"); - serverArgs.Preflight.result(http::status::moved_permanently); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); // application/text is a non-simple header - ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// FullCorsCrossOriginMismatchedCorsHeaderFails - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// FullCorsCrossOriginCheckFailsOnPreflightRedirectFails + // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginCheckFailsOnPreflightRedirectFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginCheckFailsOnPreflightRedirectFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Preflight.set(http::field::location, "http://any-host.extension"); + serverArgs.Preflight.result(http::status::moved_permanently); - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCorsCheckFailsOnResponseRedirectFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCorsCheckFailsOnResponseRedirectFails) - { - ServerParams serverArgs(s_port); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); - // server1 allowed origin header includes http://example.com - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - // This is a CORS request to server1, but server1 redirects the request to server2 - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - // Redir server's URL - serverArgs.Response.set(http::field::location, "http://localhost:6666"); - serverArgs.Response.set(http::field::server, "BaseServer"); + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// FullCorsCrossOriginCheckFailsOnPreflightRedirectFails - // Server2 does not set Access-Control-Allow-Origin for GET requests - ServerParams redirServerArgs(++s_port); - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::server, "RedirectServer"); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCorsCheckFailsOnResponseRedirectFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCorsCheckFailsOnResponseRedirectFails) + { + ServerParams serverArgs(s_port); - ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); + // server1 allowed origin header includes http://example.com + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + // This is a CORS request to server1, but server1 redirects the request to server2 + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + // Redir server's URL + serverArgs.Response.set(http::field::location, "http://localhost:6666"); + serverArgs.Response.set(http::field::server, "BaseServer"); - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); - }// FullCorsCorsCheckFailsOnResponseRedirectFails + // Server2 does not set Access-Control-Allow-Origin for GET requests + ServerParams redirServerArgs(++s_port); + redirServerArgs.Response.result(http::status::accepted); + redirServerArgs.Response.set(http::field::server, "RedirectServer"); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginToSameOriginRedirectSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsSameOriginToSameOriginRedirectSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::location, serverArgs.Url); - serverArgs.Response.result(http::status::accepted); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); + }// FullCorsCorsCheckFailsOnResponseRedirectFails - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); - } // FullCorsSameOriginToSameOriginRedirectSucceeds + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginToSameOriginRedirectSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsSameOriginToSameOriginRedirectSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::location, serverArgs.Url); + serverArgs.Response.result(http::status::accepted); - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginToCrossOriginRedirectSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsSameOriginToCrossOriginRedirectSucceeds) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); - serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::access_control_allow_origin, serverArgs.Url); + TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); + } // FullCorsSameOriginToSameOriginRedirectSucceeds - ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsSameOriginToCrossOriginRedirectSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsSameOriginToCrossOriginRedirectSucceeds) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, redirServerArgs.Url); - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); - } // FullCorsSameOriginToCrossOriginRedirectSucceeds + redirServerArgs.Response.result(http::status::accepted); + redirServerArgs.Response.set(http::field::access_control_allow_origin, serverArgs.Url); - //TODO: Seems to redirect to exact same resource. Implement second resource in same server. - // Redirects a cross origin request to cross origin request on the same server - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToCrossOriginRedirectSucceeds) - TEST_IGNORE() - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToCrossOriginRedirectSucceeds) - { - ServerParams serverArgs(s_port); - //ServerParams redirServerArgs(++s_port); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); - serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, serverArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - //redirServerArgs.Response.result(http::status::accepted); - //redirServerArgs.Response.set(http::field::access_control_allow_origin, serverArgs.Url); + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); + } // FullCorsSameOriginToCrossOriginRedirectSucceeds - ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); + //TODO: Seems to redirect to exact same resource. Implement second resource in same server. + // Redirects a cross origin request to cross origin request on the same server + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToCrossOriginRedirectSucceeds) + TEST_IGNORE() + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToCrossOriginRedirectSucceeds) + { + ServerParams serverArgs(s_port); + //ServerParams redirServerArgs(++s_port); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + serverArgs.Preflight.set(http::field::access_control_allow_origin, serverArgs.Url); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, serverArgs.Url); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - TestOriginPolicy(serverArgs, /*redirServerArgs, */clientArgs, s_shouldSucceed); - } // FullCorsCrossOriginToCrossOriginRedirectSucceeds + //redirServerArgs.Response.result(http::status::accepted); + //redirServerArgs.Response.set(http::field::access_control_allow_origin, serverArgs.Url); - // The initial request gets redirected back to the original origin, - // but it will lack the Access-Control-Allow-Origin header. - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToOriginalOriginRedirectFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToOriginalOriginRedirectFails) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, redirServerArgs.Url); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); + TestOriginPolicy(serverArgs, /*redirServerArgs, */clientArgs, s_shouldSucceed); + } // FullCorsCrossOriginToCrossOriginRedirectSucceeds - SetRuntimeOptionString("Http.GlobalOrigin", redirServerArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + // The initial request gets redirected back to the original origin, + // but it will lack the Access-Control-Allow-Origin header. + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToOriginalOriginRedirectFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToOriginalOriginRedirectFails) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); - } // FullCorsCrossOriginToOriginalOriginRedirectFails + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, redirServerArgs.Url); + serverArgs.Response.set(http::field::access_control_allow_origin, redirServerArgs.Url); - // Redirects cross origin request to server1 to cross origin request to server2 - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + SetRuntimeOptionString("Http.GlobalOrigin", redirServerArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::access_control_allow_origin, "*"); + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); + } // FullCorsCrossOriginToOriginalOriginRedirectFails - ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); + // Redirects cross origin request to server1 to cross origin request to server2 + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, redirServerArgs.Url); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); - } // FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds + redirServerArgs.Response.result(http::status::accepted); + redirServerArgs.Response.set(http::field::access_control_allow_origin, "*"); - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - // server1 redirects the GET request to server2 - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - // Since redirect tainted the origin, the server has to allow all origins for CORS to succeed - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::access_control_allow_origin, "*"); + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); + } // FullCorsCrossOriginToAnotherCrossOriginRedirectSucceeds - // PATCH is not a simple method, so preflight is required for server1 - ClientParams clientArgs(http::verb::patch, { { "Content-Type", "text/plain" } }); + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + // server1 redirects the GET request to server2 + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, redirServerArgs.Url); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); - } // FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds + // Since redirect tainted the origin, the server has to allow all origins for CORS to succeed + redirServerArgs.Response.result(http::status::accepted); + redirServerArgs.Response.set(http::field::access_control_allow_origin, "*"); - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails) - { - ServerParams serverArgs(s_port); - ServerParams redirServerArgs(++s_port); - - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - // server1 redirects the GET request to server2 - serverArgs.Response.result(http::status::moved_permanently); - serverArgs.Response.set(http::field::location, redirServerArgs.Url); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - // Since redirect tainted the origin, the server does not know what origin to allow through a single value. - // Even if server successfully guessed the single value, it will still fail on the client side. - redirServerArgs.Response.result(http::status::accepted); - redirServerArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - // PATCH is not a simple method, so preflight is required for server1 - ClientParams clientArgs(http::verb::patch, { { "Content-Type", "text/plain" } }); - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); - } // FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails - - BEGIN_TEST_METHOD_ATTRIBUTE(FullCors304ForSimpleGetFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCors304ForSimpleGetFails) - { - ServerParams serverArgs(s_port); - serverArgs.Response.result(http::status::not_modified); + // PATCH is not a simple method, so preflight is required for server1 + ClientParams clientArgs(http::verb::patch, {{ "Content-Type", "text/plain" }}); - // PATCH is not a simple method, so preflight is required for server1 - ClientParams clientArgs(http::verb::get, { { "Content-Type", "text/plain" } }); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldSucceed); + } // FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightSucceeds - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - } // FullCors304ForSimpleGetFails + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails) + { + ServerParams serverArgs(s_port); + ServerParams redirServerArgs(++s_port); + + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + // server1 redirects the GET request to server2 + serverArgs.Response.result(http::status::moved_permanently); + serverArgs.Response.set(http::field::location, redirServerArgs.Url); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + + // Since redirect tainted the origin, the server does not know what origin to allow through a single value. + // Even if server successfully guessed the single value, it will still fail on the client side. + redirServerArgs.Response.result(http::status::accepted); + redirServerArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + + // PATCH is not a simple method, so preflight is required for server1 + ClientParams clientArgs(http::verb::patch, {{ "Content-Type", "text/plain" }}); + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicyWithRedirect(serverArgs, redirServerArgs, clientArgs, s_shouldFail); + } // FullCorsCrossOriginToAnotherCrossOriginRedirectWithPreflightFails + + BEGIN_TEST_METHOD_ATTRIBUTE(FullCors304ForSimpleGetFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCors304ForSimpleGetFails) + { + ServerParams serverArgs(s_port); + serverArgs.Response.result(http::status::not_modified); - TEST_METHOD(FullCorsPreflightSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "ArbitraryHeader"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "ArbitraryHeader"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Response.result(http::status::ok); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - ClientParams clientArgs(http::verb::get, { {"Content-Type", "text/plain"}, {"ArbitraryHeader", "AnyValue"} }); - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); - }// FullCorsPreflightSucceeds - - // 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) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsCrossOriginWithCredentialsSucceeds) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); - serverArgs.Preflight.set(http::field::access_control_allow_credentials, "true"); - serverArgs.Response.result(http::status::accepted); - serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); - - ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" } }); // application/text is a non-simple header - clientArgs.WithCredentials = true; - - SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - - TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); - }// FullCorsCrossOriginWithCredentialsSucceeds - - // "Host" is one of the forbidden headers for fetch - BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsRequestWithHostHeaderFails) - // "Host" is not an accepted request header in WinRT. - TEST_IGNORE() - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(FullCorsRequestWithHostHeaderFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Response.result(http::status::accepted); + // PATCH is not a simple method, so preflight is required for server1 + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "text/plain" }}); - ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" }, { "Host", "http://sub.example.rnw" } }); + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + } // FullCors304ForSimpleGetFails - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + TEST_METHOD(FullCorsPreflightSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "ArbitraryHeader"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "ArbitraryHeader"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Response.result(http::status::ok); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + + ClientParams clientArgs(http::verb::get, { {"Content-Type", "text/plain"}, {"ArbitraryHeader", "AnyValue"} }); + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); + }// FullCorsPreflightSucceeds + + // 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) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsCrossOriginWithCredentialsSucceeds) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_origin, s_crossOriginUrl); + serverArgs.Preflight.set(http::field::access_control_allow_credentials, "true"); + serverArgs.Response.result(http::status::accepted); + serverArgs.Response.set(http::field::access_control_allow_origin, s_crossOriginUrl); + + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }}); // application/text is a non-simple header + clientArgs.WithCredentials = true; + + SetRuntimeOptionString("Http.GlobalOrigin", s_crossOriginUrl); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, s_shouldSucceed); + }// FullCorsCrossOriginWithCredentialsSucceeds + + // "Host" is one of the forbidden headers for fetch + BEGIN_TEST_METHOD_ATTRIBUTE(FullCorsRequestWithHostHeaderFails) + // "Host" is not an accepted request header in WinRT. + TEST_IGNORE() + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(FullCorsRequestWithHostHeaderFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Response.result(http::status::accepted); - Assert::Fail(L"FIX!!! Passes for the worng reason. Error: 0x80070057 : 'Invalid HTTP headers.'"); - }// FullCorsRequestWithHostHeaderFails + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }, { "Host", "http://sub.example.rnw" }}); - BEGIN_TEST_METHOD_ATTRIBUTE(RequestWithProxyAuthorizationHeaderFails) - END_TEST_METHOD_ATTRIBUTE() - TEST_METHOD(RequestWithProxyAuthorizationHeaderFails) - { - ServerParams serverArgs(s_port); - serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); - serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); - serverArgs.Response.result(http::status::accepted); + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); - ClientParams clientArgs(http::verb::get, { { "Content-Type", "application/text" }, { "Proxy-Authorization", "Basic Zm9vOmJhcg==" } }); + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); - SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + Assert::Fail(L"FIX!!! Passes for the worng reason. Error: 0x80070057 : 'Invalid HTTP headers.'"); + }// FullCorsRequestWithHostHeaderFails - TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); - }// RequestWithProxyAuthorizationHeaderFails - }; + BEGIN_TEST_METHOD_ATTRIBUTE(RequestWithProxyAuthorizationHeaderFails) + END_TEST_METHOD_ATTRIBUTE() + TEST_METHOD(RequestWithProxyAuthorizationHeaderFails) + { + ServerParams serverArgs(s_port); + serverArgs.Preflight.set(http::field::access_control_request_headers, "Content-Type"); + serverArgs.Preflight.set(http::field::access_control_allow_headers, "Content-Type"); + serverArgs.Response.result(http::status::accepted); + + ClientParams clientArgs(http::verb::get, {{ "Content-Type", "application/text" }, { "Proxy-Authorization", "Basic Zm9vOmJhcg==" }}); + + SetRuntimeOptionString("Http.GlobalOrigin", serverArgs.Url.c_str()); + SetRuntimeOptionInt("Http.OriginPolicy", static_cast(OriginPolicy::CrossOriginResourceSharing)); + + TestOriginPolicy(serverArgs, clientArgs, s_shouldFail); + }// RequestWithProxyAuthorizationHeaderFails +}; - uint16_t HttpOriginPolicyIntegrationTest::s_port = 7777; +uint16_t HttpOriginPolicyIntegrationTest::s_port = 7777; }//namespace Microsoft::React::Test // clang-format on From 1178d9d1b7637e208d174d6cbe038286fa4a7f7e Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Wed, 7 Dec 2022 02:46:02 -0800 Subject: [PATCH 31/45] clang format --- .../HttpResourceIntegrationTests.cpp | 228 ++-- .../Networking/OriginPolicyHttpFilter.cpp | 1137 ++++++++--------- vnext/Shared/OInstance.cpp | 50 +- 3 files changed, 701 insertions(+), 714 deletions(-) diff --git a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp index b3f9a4178db..4426216f49e 100644 --- a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp +++ b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp @@ -169,123 +169,123 @@ TEST_CLASS (HttpResourceIntegrationTest) { } } - TEST_METHOD(RequestGetExplicitUserAgentSucceeds) { - string url = "https://api.github.com/repos/microsoft/react-native-xaml"; - - promise rcPromise; - string error; - IHttpResource::Response response; - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { - response = callbackResponse; - rcPromise.set_value(); - }); - resource->SetOnError([&rcPromise, &error](int64_t, string &&message, bool) { - error = std::move(message); - rcPromise.set_value(); - }); - - //clang-format off - resource->SendRequest( - "GET", - std::move(url), - 0, /*requestId*/ - {{"User-Agent", "React Native Windows"}}, - {}, /*data*/ - "text", /*responseType*/ - false, - 0 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {}); - //clang-format on - - rcPromise.get_future().wait(); - - Assert::AreEqual({}, error, L"Error encountered"); - Assert::AreEqual(static_cast(200), response.StatusCode); - } + TEST_METHOD(RequestGetExplicitUserAgentSucceeds) { + string url = "https://api.github.com/repos/microsoft/react-native-xaml"; - TEST_METHOD(RequestGetImplicitUserAgentSucceeds) { - string url = "https://api.github.com/repos/microsoft/react-native-windows"; - - promise rcPromise; - string error; - IHttpResource::Response response; - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { - response = callbackResponse; - rcPromise.set_value(); - }); - resource->SetOnError([&rcPromise, &error](int64_t, string &&message, bool) { - error = std::move(message); - rcPromise.set_value(); - }); - - MicrosoftReactSetRuntimeOptionString("Http.UserAgent", "React Native Windows"); - - //clang-format off - resource->SendRequest( - "GET", - std::move(url), - 0, /*requestId*/ - {}, /*headers*/ - {}, /*data*/ - "text", /*responseType*/ - false, - 0 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {}); - //clang-format on - - rcPromise.get_future().wait(); - - Assert::AreEqual({}, error, L"Error encountered"); - Assert::AreEqual(static_cast(200), response.StatusCode); - } + promise rcPromise; + string error; + IHttpResource::Response response; - TEST_METHOD(RequestGetMissingUserAgentFails) { - // string url = "http://localhost:" + std::to_string(s_port); - string url = "https://api.github.com/repos/microsoft/react-native-macos"; - - promise rcPromise; - string error; - IHttpResource::Response response; - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { - response = callbackResponse; - rcPromise.set_value(); - }); - resource->SetOnError([&rcPromise, &error](int64_t, string &&message, bool) { - error = std::move(message); - rcPromise.set_value(); - }); - - //clang-format off - resource->SendRequest( - "GET", - std::move(url), - 0, /*requestId*/ - {}, /*headers*/ - {}, /*data*/ - "text", /*responseType*/ - false, - 0 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {}); - //clang-format on - - rcPromise.get_future().wait(); - - Assert::AreEqual({}, error, L"Error encountered"); - Assert::AreEqual(static_cast(403), response.StatusCode); - } + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { + response = callbackResponse; + rcPromise.set_value(); + }); + resource->SetOnError([&rcPromise, &error](int64_t, string &&message, bool) { + error = std::move(message); + rcPromise.set_value(); + }); + + //clang-format off + resource->SendRequest( + "GET", + std::move(url), + 0, /*requestId*/ + {{"User-Agent", "React Native Windows"}}, + {}, /*data*/ + "text", /*responseType*/ + false, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {}); + //clang-format on + + rcPromise.get_future().wait(); + + Assert::AreEqual({}, error, L"Error encountered"); + Assert::AreEqual(static_cast(200), response.StatusCode); + } + + TEST_METHOD(RequestGetImplicitUserAgentSucceeds) { + string url = "https://api.github.com/repos/microsoft/react-native-windows"; + + promise rcPromise; + string error; + IHttpResource::Response response; + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { + response = callbackResponse; + rcPromise.set_value(); + }); + resource->SetOnError([&rcPromise, &error](int64_t, string &&message, bool) { + error = std::move(message); + rcPromise.set_value(); + }); + + MicrosoftReactSetRuntimeOptionString("Http.UserAgent", "React Native Windows"); + + //clang-format off + resource->SendRequest( + "GET", + std::move(url), + 0, /*requestId*/ + {}, /*headers*/ + {}, /*data*/ + "text", /*responseType*/ + false, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {}); + //clang-format on + + rcPromise.get_future().wait(); + + Assert::AreEqual({}, error, L"Error encountered"); + Assert::AreEqual(static_cast(200), response.StatusCode); + } + + TEST_METHOD(RequestGetMissingUserAgentFails) { + // string url = "http://localhost:" + std::to_string(s_port); + string url = "https://api.github.com/repos/microsoft/react-native-macos"; + + promise rcPromise; + string error; + IHttpResource::Response response; + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { + response = callbackResponse; + rcPromise.set_value(); + }); + resource->SetOnError([&rcPromise, &error](int64_t, string &&message, bool) { + error = std::move(message); + rcPromise.set_value(); + }); - TEST_METHOD(RequestGetFails) { - string error; - promise promise; + //clang-format off + resource->SendRequest( + "GET", + std::move(url), + 0, /*requestId*/ + {}, /*headers*/ + {}, /*data*/ + "text", /*responseType*/ + false, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {}); + //clang-format on + + rcPromise.get_future().wait(); + + Assert::AreEqual({}, error, L"Error encountered"); + Assert::AreEqual(static_cast(403), response.StatusCode); + } + + TEST_METHOD(RequestGetFails) { + string error; + promise promise; auto resource = IHttpResource::Make(); resource->SetOnError([&error, &promise](int64_t, string &&message, bool) { diff --git a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp index 7cb90ac2e36..944d4e7dd09 100644 --- a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp +++ b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp @@ -39,20 +39,20 @@ namespace Microsoft::React::Networking { #pragma region ConstWcharComparer - bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t* a, const wchar_t* b) const { - return _wcsicmp(a, b) < 0; - } +bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, const wchar_t *b) const { + return _wcsicmp(a, b) < 0; +} #pragma endregion ConstWcharComparer - // https://fetch.spec.whatwg.org/#forbidden-method - /*static*/ set OriginPolicyHttpFilter::s_forbiddenMethods = - { L"CONNECT", L"TRACE", L"TRACK" }; +// https://fetch.spec.whatwg.org/#forbidden-method +/*static*/ set OriginPolicyHttpFilter::s_forbiddenMethods = + {L"CONNECT", L"TRACE", L"TRACK"}; - /*static*/ set - OriginPolicyHttpFilter::s_simpleCorsMethods = { L"GET", L"HEAD", L"POST" }; +/*static*/ set + OriginPolicyHttpFilter::s_simpleCorsMethods = {L"GET", L"HEAD", L"POST"}; - /*static*/ set +/*static*/ set OriginPolicyHttpFilter::s_simpleCorsRequestHeaderNames = { L"Accept", L"Accept-Language", @@ -62,21 +62,21 @@ namespace Microsoft::React::Networking { L"Downlink", L"Save-Data", L"Viewport-Width", - L"Width" }; + L"Width"}; - /*static*/ set +/*static*/ set OriginPolicyHttpFilter::s_simpleCorsResponseHeaderNames = - { L"Cache-Control", L"Content-Language", L"Content-Type", L"Expires", L"Last-Modified", L"Pragma" }; + {L"Cache-Control", L"Content-Language", L"Content-Type", L"Expires", L"Last-Modified", L"Pragma"}; - /*static*/ set +/*static*/ set OriginPolicyHttpFilter::s_simpleCorsContentTypeValues = { L"application/x-www-form-urlencoded", L"multipart/form-data", - L"text/plain" }; + L"text/plain"}; - // https://fetch.spec.whatwg.org/#forbidden-header-name - // Chromium still bans "User-Agent" due to https://crbug.com/571722 - /*static*/ set +// https://fetch.spec.whatwg.org/#forbidden-header-name +// Chromium still bans "User-Agent" due to https://crbug.com/571722 +/*static*/ set OriginPolicyHttpFilter::s_corsForbiddenRequestHeaderNames = { L"Accept-Charset", L"Accept-Encoding", @@ -97,109 +97,109 @@ namespace Microsoft::React::Networking { L"Trailer", L"Transfer-Encoding", L"Upgrade", - L"Via" }; + L"Via"}; - /*static*/ set +/*static*/ set OriginPolicyHttpFilter::s_cookieSettingResponseHeaders = { L"Set-Cookie", L"Set-Cookie2", // Deprecated by the spec, but probably still used - }; +}; + +/*static*/ set + OriginPolicyHttpFilter::s_corsForbiddenRequestHeaderNamePrefixes = {L"Proxy-", L"Sec-"}; + +/*static*/ Uri OriginPolicyHttpFilter::s_origin{nullptr}; - /*static*/ set - OriginPolicyHttpFilter::s_corsForbiddenRequestHeaderNamePrefixes = { L"Proxy-", L"Sec-" }; +/*static*/ void OriginPolicyHttpFilter::SetStaticOrigin(std::string &&url) { + if (!url.empty()) + s_origin = Uri{to_hstring(url)}; +} - /*static*/ Uri OriginPolicyHttpFilter::s_origin{ nullptr }; +/*static*/ bool OriginPolicyHttpFilter::IsSameOrigin(Uri const &u1, Uri const &u2) noexcept { + return (u1 && u2) && u1.SchemeName() == u2.SchemeName() && u1.Host() == u2.Host() && u1.Port() == u2.Port(); +} - /*static*/ void OriginPolicyHttpFilter::SetStaticOrigin(std::string&& url) { - if (!url.empty()) - s_origin = Uri{ to_hstring(url) }; +/*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const &request) noexcept { + // Ensure header is in Simple CORS allowlist + for (const auto &header : request.Headers()) { + if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend()) + return false; + + // Ensure Content-Type value is in Simple CORS allowlist, if present + if (boost::iequals(header.Key(), L"Content-Type")) { + if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) != s_simpleCorsContentTypeValues.cend()) + return false; + } } - /*static*/ bool OriginPolicyHttpFilter::IsSameOrigin(Uri const& u1, Uri const& u2) noexcept { + /*static*/ bool OriginPolicyHttpFilter::IsSameOrigin(Uri const &u1, Uri const &u2) noexcept { return (u1 && u2) && u1.SchemeName() == u2.SchemeName() && u1.Host() == u2.Host() && u1.Port() == u2.Port(); } - /*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const& request) noexcept { - // Ensure header is in Simple CORS allowlist - for (const auto& header : request.Headers()) { + /*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const &request) noexcept { + // Ensure header is in Simple CORS white list + for (const auto &header : request.Headers()) { if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend()) return false; - // Ensure Content-Type value is in Simple CORS allowlist, if present + // Ensure Content-Type value is in Simple CORS white list, if present if (boost::iequals(header.Key(), L"Content-Type")) { if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) != s_simpleCorsContentTypeValues.cend()) return false; } } - /*static*/ bool OriginPolicyHttpFilter::IsSameOrigin(Uri const& u1, Uri const& u2) noexcept { - return (u1 && u2) && u1.SchemeName() == u2.SchemeName() && u1.Host() == u2.Host() && u1.Port() == u2.Port(); - } - - /*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const& request) noexcept { - // Ensure header is in Simple CORS white list - for (const auto& header : request.Headers()) { - if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend()) + // WinRT separates request headers from request content headers + if (auto content = request.Content()) { + for (const auto &header : content.Headers()) { + // WinRT automatically appends non-whitelisted header Content-Length when Content-Type is set. Skip it. + if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend() && + !boost::iequals(header.Key(), "Content-Length")) return false; // Ensure Content-Type value is in Simple CORS white list, if present if (boost::iequals(header.Key(), L"Content-Type")) { - if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) != s_simpleCorsContentTypeValues.cend()) + if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) == s_simpleCorsContentTypeValues.cend()) return false; } } + } - // WinRT separates request headers from request content headers - if (auto content = request.Content()) { - for (const auto& header : content.Headers()) { - // WinRT automatically appends non-whitelisted header Content-Length when Content-Type is set. Skip it. - if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend() && - !boost::iequals(header.Key(), "Content-Length")) - return false; - - // Ensure Content-Type value is in Simple CORS white list, if present - if (boost::iequals(header.Key(), L"Content-Type")) { - if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) == s_simpleCorsContentTypeValues.cend()) - return false; - } - } - } + // Ensure method is in Simple CORS white list + return s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); + } - // Ensure method is in Simple CORS white list - return s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); - } + /*static*/ Uri OriginPolicyHttpFilter::GetOrigin(Uri const &uri) noexcept { + return Uri{uri.SchemeName() + L"://" + uri.Host() + L":" + to_hstring(uri.Port())}; + } - /*static*/ Uri OriginPolicyHttpFilter::GetOrigin(Uri const& uri) noexcept { - return Uri{ uri.SchemeName() + L"://" + uri.Host() + L":" + to_hstring(uri.Port()) }; - } + /*static*/ bool OriginPolicyHttpFilter::AreSafeRequestHeaders( + winrt::Windows::Web::Http::Headers::HttpRequestHeaderCollection const &headers) noexcept { + for (const auto &header : headers) { + if (s_corsForbiddenRequestHeaderNames.find(header.Key().c_str()) != s_corsForbiddenRequestHeaderNames.cend()) + return false; - /*static*/ bool OriginPolicyHttpFilter::AreSafeRequestHeaders( - winrt::Windows::Web::Http::Headers::HttpRequestHeaderCollection const& headers) noexcept { - for (const auto& header : headers) { - if (s_corsForbiddenRequestHeaderNames.find(header.Key().c_str()) != s_corsForbiddenRequestHeaderNames.cend()) + for (const auto &prefix : s_corsForbiddenRequestHeaderNamePrefixes) { + if (boost::istarts_with(header.Key(), prefix)) return false; - - for (const auto& prefix : s_corsForbiddenRequestHeaderNamePrefixes) { - if (boost::istarts_with(header.Key(), prefix)) - return false; - } } - - return true; } - // See https://fetch.spec.whatwg.org/#cors-safelisted-request-header - // "A CORS-safelisted header is a header whose name is either one of 'Accept', 'Accept-Language', and - // 'Content-Language', or whose name is 'Content-Type' and value is one of 'application/x-www-form-urlencoded', - // 'multipart/form-data', and 'text/plain' - /*static*/ bool OriginPolicyHttpFilter::IsCorsSafelistedRequestHeader( - hstring const& name, hstring const& value) noexcept { - // 1. If value's length is greater than 128, then return false. - if (value.size() > 128) - return false; + return true; + } - // 2. Byte-lowercase name and switch on the result: - static const wchar_t* const safeHeaderNames[] = { + // See https://fetch.spec.whatwg.org/#cors-safelisted-request-header + // "A CORS-safelisted header is a header whose name is either one of 'Accept', 'Accept-Language', and + // 'Content-Language', or whose name is 'Content-Type' and value is one of 'application/x-www-form-urlencoded', + // 'multipart/form-data', and 'text/plain' + /*static*/ bool OriginPolicyHttpFilter::IsCorsSafelistedRequestHeader( + hstring const &name, hstring const &value) noexcept { + // 1. If value's length is greater than 128, then return false. + if (value.size() > 128) + return false; + + // 2. Byte-lowercase name and switch on the result: + static const wchar_t *const safeHeaderNames[] = { // The following four headers are from the CORS spec L"accept", L"accept-language", @@ -226,194 +226,188 @@ namespace Microsoft::React::Networking { L"sec-ch-ua-mobile", L"sec-ch-ua-full-version", L"sec-ch-ua-platform-version", - }; + }; + + auto nameLower = boost::to_lower_copy(wstring{name.c_str()}); + if (std::find(std::cbegin(safeHeaderNames), std::cend(safeHeaderNames), nameLower) == std::end(safeHeaderNames)) + return false; + + double doubleHolder; + if (nameLower == L"device-memory" || nameLower == L"dpr") + return boost::conversion::try_lexical_convert(value.c_str(), doubleHolder); + + int intHolder; + if (nameLower == L"width" || nameLower == L"viewport-width") + return boost::conversion::try_lexical_convert(value.c_str(), intHolder); + + auto valueLower = boost::to_lower_copy(wstring{value.c_str()}); + if (nameLower == L"save-data") + return valueLower == L"on"; + + if (nameLower == L"accept") + return !std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte); + + if (nameLower == L"accept-language" || nameLower == L"content-language") { + return std::all_of(valueLower.cbegin(), valueLower.cend(), [](wchar_t c) noexcept { + return (0x30 <= c && c <= 0x39) || // 0-9 + (0x41 <= c && c <= 0x5A) || // A-Z + (0x61 <= c && c <= 0x7A) || // a-z + c == 0x20 || c == 0x2A || c == 0x2C || c == 0x2D || c == 0x2E || c == 0x3B || c == 0x3D; // *,-.;= + }); + } - auto nameLower = boost::to_lower_copy(wstring{ name.c_str() }); - if (std::find(std::cbegin(safeHeaderNames), std::cend(safeHeaderNames), nameLower) == std::end(safeHeaderNames)) + if (nameLower == L"content-type") { + if (std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte)) return false; - double doubleHolder; - if (nameLower == L"device-memory" || nameLower == L"dpr") - return boost::conversion::try_lexical_convert(value.c_str(), doubleHolder); - - int intHolder; - if (nameLower == L"width" || nameLower == L"viewport-width") - return boost::conversion::try_lexical_convert(value.c_str(), intHolder); - - auto valueLower = boost::to_lower_copy(wstring{ value.c_str() }); - if (nameLower == L"save-data") - return valueLower == L"on"; + // https://mimesniff.spec.whatwg.org/#parse-a-mime-type + HttpMediaTypeHeaderValue mediaType{nullptr}; + if (HttpMediaTypeHeaderValue::TryParse(valueLower, mediaType)) + return mediaType.ToString() == L"application/x-www-form-urlencoded" || + mediaType.ToString() == L"multipart/form-data" || mediaType.ToString() == L"text/plain"; - if (nameLower == L"accept") - return !std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte); - - if (nameLower == L"accept-language" || nameLower == L"content-language") { - return std::all_of(valueLower.cbegin(), valueLower.cend(), [](wchar_t c) noexcept { - return (0x30 <= c && c <= 0x39) || // 0-9 - (0x41 <= c && c <= 0x5A) || // A-Z - (0x61 <= c && c <= 0x7A) || // a-z - c == 0x20 || c == 0x2A || c == 0x2C || c == 0x2D || c == 0x2E || c == 0x3B || c == 0x3D; // *,-.;= - }); - } - - if (nameLower == L"content-type") { - if (std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte)) - return false; - - // https://mimesniff.spec.whatwg.org/#parse-a-mime-type - HttpMediaTypeHeaderValue mediaType{ nullptr }; - if (HttpMediaTypeHeaderValue::TryParse(valueLower, mediaType)) - return mediaType.ToString() == L"application/x-www-form-urlencoded" || - mediaType.ToString() == L"multipart/form-data" || mediaType.ToString() == L"text/plain"; - - return false; - } - - return true; + return false; } - // https://fetch.spec.whatwg.org/#cors-unsafe-request-header-byte - /*static*/ bool OriginPolicyHttpFilter::IsCorsUnsafeRequestHeaderByte(wchar_t c) noexcept { - return (c < 0x20 && c != 0x09) || c == 0x22 || c == 0x28 || c == 0x29 || c == 0x3a || c == 0x3c || c == 0x3e || + return true; + } + + // https://fetch.spec.whatwg.org/#cors-unsafe-request-header-byte + /*static*/ bool OriginPolicyHttpFilter::IsCorsUnsafeRequestHeaderByte(wchar_t c) noexcept { + return (c < 0x20 && c != 0x09) || c == 0x22 || c == 0x28 || c == 0x29 || c == 0x3a || c == 0x3c || c == 0x3e || c == 0x3f || c == 0x40 || c == 0x5b || c == 0x5c || c == 0x5d || c == 0x7b || c == 0x7d || c == 0x7f; - } + } - /*static*/ set OriginPolicyHttpFilter::CorsUnsafeNotForbiddenRequestHeaderNames( - HttpRequestHeaderCollection const& headers) noexcept { - constexpr size_t maxSafelistValueSize = 1024; - size_t safelistValueSize = 0; - std::vector potentiallyUnsafeNames; - set result; - for (const auto& header : headers) { - const auto headerName = header.Key().c_str(); - - // If header is not safe - if (boost::istarts_with(headerName, L"Proxy-") || boost::istarts_with(headerName, L"Sec-") || + /*static*/ set OriginPolicyHttpFilter::CorsUnsafeNotForbiddenRequestHeaderNames( + HttpRequestHeaderCollection const &headers) noexcept { + constexpr size_t maxSafelistValueSize = 1024; + size_t safelistValueSize = 0; + std::vector potentiallyUnsafeNames; + set result; + for (const auto &header : headers) { + const auto headerName = header.Key().c_str(); + + // If header is not safe + if (boost::istarts_with(headerName, L"Proxy-") || boost::istarts_with(headerName, L"Sec-") || s_corsForbiddenRequestHeaderNames.find(headerName) != s_corsForbiddenRequestHeaderNames.cend()) - continue; + continue; - if (!IsCorsSafelistedRequestHeader(header.Key(), header.Value())) { - result.emplace(header.Key().c_str()); - } - else { - potentiallyUnsafeNames.emplace_back(std::wstring_view{ header.Key() }.data()); - safelistValueSize += header.Value().size(); - } + if (!IsCorsSafelistedRequestHeader(header.Key(), header.Value())) { + result.emplace(header.Key().c_str()); + } else { + potentiallyUnsafeNames.emplace_back(std::wstring_view{header.Key()}.data()); + safelistValueSize += header.Value().size(); } + } - if (safelistValueSize > maxSafelistValueSize) - result.insert(potentiallyUnsafeNames.begin(), potentiallyUnsafeNames.end()); + if (safelistValueSize > maxSafelistValueSize) + result.insert(potentiallyUnsafeNames.begin(), potentiallyUnsafeNames.end()); - return result; - } + return result; + } - /*static*/ OriginPolicyHttpFilter::AccessControlValues OriginPolicyHttpFilter::ExtractAccessControlValues( - winrt::Windows::Foundation::Collections::IMap const& headers) { - using std::wregex; - using std::wsregex_token_iterator; - - // https://tools.ietf.org/html/rfc2616#section-4.2 - wregex rgx{ L"\\s*,\\s*" }; - AccessControlValues result; - - auto ciStrCmp = [](const wstring& a, const wstring& b) { return _wcsicmp(a.c_str(), b.c_str()) < 0; }; - set allowedHeaders{ ciStrCmp }; - set allowedMethods{ ciStrCmp }; - set exposedHeaders{ ciStrCmp }; - - for (const auto& header : headers) { - if (boost::iequals(header.Key(), L"Access-Control-Allow-Headers")) { - auto value = wstring{ header.Value().c_str() }; - - // TODO: Avoid redundant comparison. - auto parsed = set{ - wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; - allowedHeaders.insert(parsed.cbegin(), parsed.cend()); - result.AllowedHeaders.insert(allowedHeaders.cbegin(), allowedHeaders.cend()); - } - else if (boost::iequals(header.Key(), L"Access-Control-Allow-Methods")) { - auto value = wstring{ header.Value().c_str() }; - - // TODO: Avoid redundant comparison. - auto parsed = set{ - wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; - allowedMethods.insert(parsed.cbegin(), parsed.cend()); - result.AllowedMethods.insert(allowedMethods.cbegin(), allowedMethods.cend()); - } - else if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { - result.AllowedOrigin = header.Value(); - } - else if (boost::iequals(header.Key(), L"Access-Control-Expose-Headers")) { - auto value = wstring{ header.Value().c_str() }; - - // TODO: Avoid redundant comparison. - auto parsed = set{ - wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; - exposedHeaders.insert(parsed.cbegin(), parsed.cend()); - result.ExposedHeaders.insert(exposedHeaders.cbegin(), exposedHeaders.cend()); - } - else if (boost::iequals(header.Key(), L"Access-Control-Allow-Credentials")) { - result.AllowedCredentials = header.Value(); - } - else if (boost::iequals(header.Key(), L"Access-Control-Max-Age")) { - result.MaxAge = _wtoi(header.Value().c_str()); - } + /*static*/ OriginPolicyHttpFilter::AccessControlValues OriginPolicyHttpFilter::ExtractAccessControlValues( + winrt::Windows::Foundation::Collections::IMap const &headers) { + using std::wregex; + using std::wsregex_token_iterator; + + // https://tools.ietf.org/html/rfc2616#section-4.2 + wregex rgx{L"\\s*,\\s*"}; + AccessControlValues result; + + auto ciStrCmp = [](const wstring &a, const wstring &b) { return _wcsicmp(a.c_str(), b.c_str()) < 0; }; + set allowedHeaders{ciStrCmp}; + set allowedMethods{ciStrCmp}; + set exposedHeaders{ciStrCmp}; + + for (const auto &header : headers) { + if (boost::iequals(header.Key(), L"Access-Control-Allow-Headers")) { + auto value = wstring{header.Value().c_str()}; + + // TODO: Avoid redundant comparison. + auto parsed = set{ + wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp}; + allowedHeaders.insert(parsed.cbegin(), parsed.cend()); + result.AllowedHeaders.insert(allowedHeaders.cbegin(), allowedHeaders.cend()); + } else if (boost::iequals(header.Key(), L"Access-Control-Allow-Methods")) { + auto value = wstring{header.Value().c_str()}; + + // TODO: Avoid redundant comparison. + auto parsed = set{ + wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp}; + allowedMethods.insert(parsed.cbegin(), parsed.cend()); + result.AllowedMethods.insert(allowedMethods.cbegin(), allowedMethods.cend()); + } else if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { + result.AllowedOrigin = header.Value(); + } else if (boost::iequals(header.Key(), L"Access-Control-Expose-Headers")) { + auto value = wstring{header.Value().c_str()}; + + // TODO: Avoid redundant comparison. + auto parsed = set{ + wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp}; + exposedHeaders.insert(parsed.cbegin(), parsed.cend()); + result.ExposedHeaders.insert(exposedHeaders.cbegin(), exposedHeaders.cend()); + } else if (boost::iequals(header.Key(), L"Access-Control-Allow-Credentials")) { + result.AllowedCredentials = header.Value(); + } else if (boost::iequals(header.Key(), L"Access-Control-Max-Age")) { + result.MaxAge = _wtoi(header.Value().c_str()); } + } - return result; - } // ExtractAccessControlValues + return result; + } // ExtractAccessControlValues - /*static*/ void OriginPolicyHttpFilter::RemoveHttpOnlyCookiesFromResponseHeaders( - HttpResponseMessage const& response, bool removeAll) { - // Example: "Set-Cookie", L"id=a3fWa; Expires=Wed, 21 Oct 2020 07:28:00 GMT; HttpOnly" - std::queue httpOnlyCookies; - for (const auto& header : response.Headers()) { - if (s_cookieSettingResponseHeaders.find(header.Key().c_str()) == s_cookieSettingResponseHeaders.cend()) - continue; + /*static*/ void OriginPolicyHttpFilter::RemoveHttpOnlyCookiesFromResponseHeaders( + HttpResponseMessage const &response, bool removeAll) { + // Example: "Set-Cookie", L"id=a3fWa; Expires=Wed, 21 Oct 2020 07:28:00 GMT; HttpOnly" + std::queue httpOnlyCookies; + for (const auto &header : response.Headers()) { + if (s_cookieSettingResponseHeaders.find(header.Key().c_str()) == s_cookieSettingResponseHeaders.cend()) + continue; - if (removeAll) { - httpOnlyCookies.push(header.Key()); - continue; - } + if (removeAll) { + httpOnlyCookies.push(header.Key()); + continue; + } - // Anchors (^$) can't be part of bracket expressions ([]). - // Create 3 matching groups: 1. Beginning of string 2. Between delimiters 3. End of string - const std::wregex re(L"(^HttpOnly\\s*;)|(;\\s*HttpOnly\\s*;)|(;\\s*HttpOnly$)", std::regex_constants::icase); - if (!std::regex_search(header.Value().c_str(), re)) - continue; + // Anchors (^$) can't be part of bracket expressions ([]). + // Create 3 matching groups: 1. Beginning of string 2. Between delimiters 3. End of string + const std::wregex re(L"(^HttpOnly\\s*;)|(;\\s*HttpOnly\\s*;)|(;\\s*HttpOnly$)", std::regex_constants::icase); + if (!std::regex_search(header.Value().c_str(), re)) + continue; - // HttpOnly cookie detected. Removing. - httpOnlyCookies.push(header.Key()); - } // const auto &header : response.Headers() + // HttpOnly cookie detected. Removing. + httpOnlyCookies.push(header.Key()); + } // const auto &header : response.Headers() - while (!httpOnlyCookies.empty()) { - response.Headers().Remove(httpOnlyCookies.front()); - httpOnlyCookies.pop(); - } + while (!httpOnlyCookies.empty()) { + response.Headers().Remove(httpOnlyCookies.front()); + httpOnlyCookies.pop(); } + } - OriginPolicyHttpFilter::OriginPolicyHttpFilter(IHttpFilter const& innerFilter): m_innerFilter{ innerFilter } {} + OriginPolicyHttpFilter::OriginPolicyHttpFilter(IHttpFilter const &innerFilter) : m_innerFilter{innerFilter} {} - OriginPolicyHttpFilter::OriginPolicyHttpFilter() + OriginPolicyHttpFilter::OriginPolicyHttpFilter() : OriginPolicyHttpFilter(winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter{}) {} - OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const& request) { - auto effectiveOriginPolicy = + OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const &request) { + auto effectiveOriginPolicy = static_cast(request.Properties().Lookup(L"OriginPolicy").as().GetUInt64()); - switch (effectiveOriginPolicy) { + switch (effectiveOriginPolicy) { case OriginPolicy::None: return effectiveOriginPolicy; case OriginPolicy::SameOrigin: if (!IsSameOrigin(s_origin, request.RequestUri())) - throw hresult_error{ E_INVALIDARG, L"SOP (same-origin policy) is enforced" }; + throw hresult_error{E_INVALIDARG, L"SOP (same-origin policy) is enforced"}; break; case OriginPolicy::SimpleCrossOriginResourceSharing: // Check for disallowed mixed content if (GetRuntimeOptionBool("Http.BlockMixedContentSimpleCors") && - s_origin.SchemeName() != request.RequestUri().SchemeName()) - throw hresult_error{ E_INVALIDARG, L"The origin and request URLs must have the same scheme" }; + s_origin.SchemeName() != request.RequestUri().SchemeName()) + throw hresult_error{E_INVALIDARG, L"The origin and request URLs must have the same scheme"}; if (IsSameOrigin(s_origin, request.RequestUri())) // Same origin. Therefore, skip Cross-Origin handling. @@ -421,7 +415,7 @@ namespace Microsoft::React::Networking { else if (!IsSimpleCorsRequest(request)) throw hresult_error{ E_INVALIDARG, - L"The request does not meet the requirements for Same-Origin policy or Simple Cross-Origin resource sharing" }; + L"The request does not meet the requirements for Same-Origin policy or Simple Cross-Origin resource sharing"}; break; case OriginPolicy::CrossOriginResourceSharing: @@ -432,13 +426,13 @@ namespace Microsoft::React::Networking { // https://fetch.spec.whatwg.org/#forbidden-header-name if (s_origin.SchemeName() != request.RequestUri().SchemeName()) - throw hresult_error{ E_INVALIDARG, L"The origin and request URLs must have the same scheme" }; + throw hresult_error{E_INVALIDARG, L"The origin and request URLs must have the same scheme"}; if (!AreSafeRequestHeaders(request.Headers())) - throw hresult_error{ E_INVALIDARG, L"Request header not allowed in cross-origin resource sharing" }; + throw hresult_error{E_INVALIDARG, L"Request header not allowed in cross-origin resource sharing"}; if (s_forbiddenMethods.find(request.Method().ToString().c_str()) != s_forbiddenMethods.cend()) - throw hresult_error{ E_INVALIDARG, L"Request method not allowed in cross-origin resource sharing" }; + throw hresult_error{E_INVALIDARG, L"Request method not allowed in cross-origin resource sharing"}; if (IsSameOrigin(s_origin, request.RequestUri())) effectiveOriginPolicy = OriginPolicy::SameOrigin; @@ -451,217 +445,251 @@ namespace Microsoft::React::Networking { default: throw hresult_error{ - E_INVALIDARG, L"Invalid OriginPolicy type: " + to_hstring(static_cast(effectiveOriginPolicy)) }; - } - - return effectiveOriginPolicy; + E_INVALIDARG, L"Invalid OriginPolicy type: " + to_hstring(static_cast(effectiveOriginPolicy))}; } + return effectiveOriginPolicy; + } + + // See https://fetch.spec.whatwg.org/#cors-check + void OriginPolicyHttpFilter::ValidateAllowOrigin( + hstring const &allowedOrigin, hstring const &allowCredentials, 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"}; + // See https://fetch.spec.whatwg.org/#cors-check void OriginPolicyHttpFilter::ValidateAllowOrigin( - hstring const& allowedOrigin, hstring const& allowCredentials, IMap props) const { + hstring const &allowedOrigin, hstring const &allowCredentials, 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" }; + L"Response header Access-Control-Allow-Origin has a value of [null] which differs from the supplied origin"}; + + 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 + 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"; + + // A wildcard Access-Control-Allow-Origin can not be used if credentials are to be sent, + // even with Access-Control-Allow-Credentials set to true + // See https://fetch.spec.whatwg.org/#cors-protocol-and-credentials + else if (L"*" == allowedOrigin) + errorMessage = + L"Response header Access-Control-Allow-Origin can not have a wildcard value when the request includes credentials"; - // See https://fetch.spec.whatwg.org/#cors-check - void OriginPolicyHttpFilter::ValidateAllowOrigin( - hstring const& allowedOrigin, hstring const& allowCredentials, 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 = 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 - 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"; - - // A wildcard Access-Control-Allow-Origin can not be used if credentials are to be sent, - // even with Access-Control-Allow-Credentials set to true - // See https://fetch.spec.whatwg.org/#cors-protocol-and-credentials - else if (L"*" == allowedOrigin) - errorMessage = - L"Response header Access-Control-Allow-Origin can not have a wildcard value when the request includes credentials"; - - else - errorMessage = L"The Access-Control-Allow-Origin header has a value of [" + allowedOrigin + - L"] which differs from the supplied origin"; - - throw hresult_error{ E_INVALIDARG, errorMessage }; - } + else + errorMessage = L"The Access-Control-Allow-Origin header has a value of [" + allowedOrigin + + L"] which differs from the supplied origin"; - // 4.10.5 - if (!withCredentials) - return; + throw hresult_error{E_INVALIDARG, errorMessage}; + } - // 4.10.6-8 - // https://fetch.spec.whatwg.org/#http-access-control-allow-credentials - // This check should be case sensitive. - // See also https://fetch.spec.whatwg.org/#http-new-header-syntax - if (L"true" != allowCredentials) - throw hresult_error{ - E_INVALIDARG, - L"Access-Control-Allow-Credentials value must be \"true\" when the response includes credentials" }; - }; - - void OriginPolicyHttpFilter::ValidatePreflightResponse( - HttpRequestMessage const& request, HttpResponseMessage const& response) const { - // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed - using winrt::Windows::Web::Http::HttpStatusCode; - switch (response.StatusCode()) { + // 4.10.5 + if (!withCredentials) + return; + + // 4.10.6-8 + // https://fetch.spec.whatwg.org/#http-access-control-allow-credentials + // This check should be case sensitive. + // See also https://fetch.spec.whatwg.org/#http-new-header-syntax + if (L"true" != allowCredentials) + throw hresult_error{ + E_INVALIDARG, + L"Access-Control-Allow-Credentials value must be \"true\" when the response includes credentials"}; + }; + + void OriginPolicyHttpFilter::ValidatePreflightResponse( + HttpRequestMessage const &request, HttpResponseMessage const &response) const { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed + using winrt::Windows::Web::Http::HttpStatusCode; + switch (response.StatusCode()) { case HttpStatusCode::MovedPermanently: case HttpStatusCode::TemporaryRedirect: case HttpStatusCode::PermanentRedirect: - throw hresult_error{ INET_E_REDIRECTING, L"Redirect is not allowed in a preflight request" }; + throw hresult_error{INET_E_REDIRECTING, L"Redirect is not allowed in a preflight request"}; default: break; - } + } - auto controlValues = ExtractAccessControlValues(response.Headers()); - - 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, props); - - // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.5 - // Check if the request method is allowed - bool withCredentials = props.Lookup(L"RequestArgs").as()->WithCredentials; - bool requestMethodAllowed = false; - for (const auto& method : controlValues.AllowedMethods) { - if (L"*" == method) { - if (!withCredentials) { - requestMethodAllowed = true; - break; - } + auto controlValues = ExtractAccessControlValues(response.Headers()); + + 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, props); + + // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.5 + // Check if the request method is allowed + bool withCredentials = props.Lookup(L"RequestArgs").as()->WithCredentials; + bool requestMethodAllowed = false; + for (const auto &method : controlValues.AllowedMethods) { + if (L"*" == method) { + if (!withCredentials) { + requestMethodAllowed = true; + break; } + } - // Preflight should always allow simple CORS methods - requestMethodAllowed |= + // Preflight should always allow simple CORS methods + requestMethodAllowed |= s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); - if (!requestMethodAllowed) + if (!requestMethodAllowed) + throw hresult_error{ + E_INVALIDARG, + L"Method [" + request.Method().ToString() + + L"] is not allowed by Access-Control-Allow-Methods in preflight response"}; + + // Check if request headers are allowed + // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.6-7 + // Check if the header should be allowed through wildcard, if the request does not have credentials. + bool requestHeadersAllowed = false; + if (!withCredentials && controlValues.AllowedHeaders.find(L"*") != controlValues.AllowedHeaders.cend()) { + // "Authorization" header cannot be allowed through wildcard alone. + // "Authorization" is the only member of https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name. + if (request.Headers().HasKey(L"Authorization") && + controlValues.AllowedHeaders.find(L"Authorization") == controlValues.AllowedHeaders.cend()) throw hresult_error{ E_INVALIDARG, - L"Method [" + request.Method().ToString() + - L"] is not allowed by Access-Control-Allow-Methods in preflight response" }; - - // Check if request headers are allowed - // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.6-7 - // Check if the header should be allowed through wildcard, if the request does not have credentials. - bool requestHeadersAllowed = false; - if (!withCredentials && controlValues.AllowedHeaders.find(L"*") != controlValues.AllowedHeaders.cend()) { - // "Authorization" header cannot be allowed through wildcard alone. - // "Authorization" is the only member of https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name. - if (request.Headers().HasKey(L"Authorization") && - controlValues.AllowedHeaders.find(L"Authorization") == controlValues.AllowedHeaders.cend()) + L"Request header field [Authorization] is not allowed by Access-Control-Allow-Headers in preflight response"}; + + requestHeadersAllowed = true; + } + if (!requestHeadersAllowed) { + // Forbidden headers are excluded from the JavaScript layer. + // User agents may use these headers internally. + const set unsafeNotForbidenHeaderNames = CorsUnsafeNotForbiddenRequestHeaderNames(request.Headers()); + for (const auto name : unsafeNotForbidenHeaderNames) { + if (controlValues.AllowedHeaders.find(name) == controlValues.AllowedHeaders.cend()) throw hresult_error{ E_INVALIDARG, - L"Request header field [Authorization] is not allowed by Access-Control-Allow-Headers in preflight response" }; - - requestHeadersAllowed = true; - } - if (!requestHeadersAllowed) { - // Forbidden headers are excluded from the JavaScript layer. - // User agents may use these headers internally. - const set unsafeNotForbidenHeaderNames = CorsUnsafeNotForbiddenRequestHeaderNames(request.Headers()); - for (const auto name : unsafeNotForbidenHeaderNames) { - if (controlValues.AllowedHeaders.find(name) == controlValues.AllowedHeaders.cend()) - throw hresult_error{ - E_INVALIDARG, - L"Request header field [" + to_hstring(name) + - L"] is not allowed by Access-Control-Allow-Headers in preflight response" }; - } + L"Request header field [" + to_hstring(name) + + L"] is not allowed by Access-Control-Allow-Headers in preflight response"}; } + } - // When withCredentials is false, request cannot include cookies. Also, cookies will be ignored in responses. - removeAllCookies = !withCredentials && GetRuntimeOptionBool("Http.RemoveCookiesFromResponse"); - } // originPolicy == SimpleCrossOriginResourceSharing || CrossOriginResourceSharing + // When withCredentials is false, request cannot include cookies. Also, cookies will be ignored in responses. + removeAllCookies = !withCredentials && GetRuntimeOptionBool("Http.RemoveCookiesFromResponse"); + } // originPolicy == SimpleCrossOriginResourceSharing || CrossOriginResourceSharing - // Don't expose HttpOnly cookies to JavaScript - RemoveHttpOnlyCookiesFromResponseHeaders(response, removeAllCookies); - } + // Don't expose HttpOnly cookies to JavaScript + RemoveHttpOnlyCookiesFromResponseHeaders(response, removeAllCookies); + } - ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage const& request) const { - auto coRequest = request; + ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage const &request) const { + auto coRequest = request; - HttpRequestMessage preflightRequest; + HttpRequestMessage preflightRequest; - // Section 4.8.2 https://fetch.spec.whatwg.org/#cors-preflight-fetch - preflightRequest.Method(HttpMethod::Options()); - preflightRequest.RequestUri(coRequest.RequestUri()); - preflightRequest.Headers().Insert(L"Accept", L"*/*"); - preflightRequest.Headers().Insert(L"Access-Control-Request-Method", coRequest.Method().ToString()); + // Section 4.8.2 https://fetch.spec.whatwg.org/#cors-preflight-fetch + preflightRequest.Method(HttpMethod::Options()); + preflightRequest.RequestUri(coRequest.RequestUri()); + preflightRequest.Headers().Insert(L"Accept", L"*/*"); + preflightRequest.Headers().Insert(L"Access-Control-Request-Method", coRequest.Method().ToString()); - auto headerNames = wstring{}; - auto writeSeparator = false; - for (const auto& header : coRequest.Headers()) { + auto headerNames = wstring{}; + auto writeSeparator = false; + for (const auto &header : coRequest.Headers()) { + if (writeSeparator) { + headerNames += L", "; + } else { + writeSeparator = true; + } + + headerNames += header.Key(); + } + + if (coRequest.Content()) { + for (const auto &header : coRequest.Content().Headers()) { if (writeSeparator) { headerNames += L", "; - } - else { + } else { writeSeparator = true; } - headerNames += header.Key(); - } - - if (coRequest.Content()) { - for (const auto& header : coRequest.Content().Headers()) { - if (writeSeparator) { - headerNames += L", "; - } - else { - writeSeparator = true; - } - - // See 10.7.4 of https://fetch.spec.whatwg.org/#http-network-or-cache-fetch - void OriginPolicyHttpFilter::ValidateResponse( - HttpResponseMessage const& response, const OriginPolicy originPolicy) const { - bool removeAllCookies = false; - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + // See 10.7.4 of https://fetch.spec.whatwg.org/#http-network-or-cache-fetch + void OriginPolicyHttpFilter::ValidateResponse( + HttpResponseMessage const &response, const OriginPolicy originPolicy) const { + bool removeAllCookies = false; + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || originPolicy == OriginPolicy::CrossOriginResourceSharing) { - auto controlValues = ExtractAccessControlValues(response.Headers()); - auto props = response.RequestMessage().Properties(); - auto withCredentials = props.Lookup(L"RequestArgs").try_as()->WithCredentials; + auto controlValues = ExtractAccessControlValues(response.Headers()); + auto props = response.RequestMessage().Properties(); + auto withCredentials = props.Lookup(L"RequestArgs").try_as()->WithCredentials; - if (GetRuntimeOptionBool("Http.StrictOriginCheckSimpleCors") && + if (GetRuntimeOptionBool("Http.StrictOriginCheckSimpleCors") && originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { - bool originAllowed = false; - for (const auto& header : response.Headers()) { - if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { - originAllowed |= L"*" == header.Value() || s_origin == Uri{ header.Value() }; + bool originAllowed = false; + for (const auto &header : response.Headers()) { + if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { + originAllowed |= L"*" == header.Value() || s_origin == Uri{header.Value()}; + } + } + + if (!originAllowed) { + throw hresult_error{E_INVALIDARG, L"The server does not support CORS or the origin is not allowed"}; + } + } else { + ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props); + } + + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { + // Filter out response headers that are not in the Simple CORS allowlist + std::queue nonSimpleNames; + for (const auto &header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == + s_simpleCorsResponseHeaderNames.cend()) + nonSimpleNames.push(header.Key()); + } + + while (!nonSimpleNames.empty()) { + response.Headers().Remove(nonSimpleNames.front()); + nonSimpleNames.pop(); + } + } else { + // Filter out response headers that are not simple headers and not in expose list + + // Keep simple headers and those found in the expose header list. + if (withCredentials || controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { + std::queue nonSimpleNonExposedHeaders; + + for (const auto &header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == + s_simpleCorsResponseHeaderNames.cend() && + controlValues.ExposedHeaders.find(header.Key().c_str()) == + controlValues.ExposedHeaders.cend()) { + nonSimpleNonExposedHeaders.push(header.Key()); } } if (!originAllowed) { - throw hresult_error{ E_INVALIDARG, L"The server does not support CORS or the origin is not allowed" }; + throw hresult_error{E_INVALIDARG, L"The server does not support CORS or the origin is not allowed"}; } - } - else { + } else { ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props); } if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { - // Filter out response headers that are not in the Simple CORS allowlist + // Filter out response headers that are not in the Simple CORS whitelist std::queue nonSimpleNames; - for (const auto& header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend()) + for (const auto &header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == + s_simpleCorsResponseHeaderNames.cend()) nonSimpleNames.push(header.Key()); } @@ -669,249 +697,208 @@ namespace Microsoft::React::Networking { response.Headers().Remove(nonSimpleNames.front()); nonSimpleNames.pop(); } - } - else { + } else { // Filter out response headers that are not simple headers and not in expose list // Keep simple headers and those found in the expose header list. - if (withCredentials || controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { + if (withCredentials || + controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { std::queue nonSimpleNonExposedHeaders; - for (const auto& header : response.Headers().GetView()) { + for (const auto &header : response.Headers().GetView()) { if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == - s_simpleCorsResponseHeaderNames.cend() && - controlValues.ExposedHeaders.find(header.Key().c_str()) == controlValues.ExposedHeaders.cend()) { + s_simpleCorsResponseHeaderNames.cend() && + controlValues.ExposedHeaders.find(header.Key().c_str()) == + controlValues.ExposedHeaders.cend()) { nonSimpleNonExposedHeaders.push(header.Key()); } } - if (!originAllowed) { - throw hresult_error{ E_INVALIDARG, L"The server does not support CORS or the origin is not allowed" }; + while (!nonSimpleNonExposedHeaders.empty()) { + response.Headers().Remove(nonSimpleNonExposedHeaders.front()); + nonSimpleNonExposedHeaders.pop(); } } - else { - ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props); - } - - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { - // Filter out response headers that are not in the Simple CORS whitelist - std::queue nonSimpleNames; - for (const auto& header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == - s_simpleCorsResponseHeaderNames.cend()) - nonSimpleNames.push(header.Key()); - } + } - while (!nonSimpleNames.empty()) { - response.Headers().Remove(nonSimpleNames.front()); - nonSimpleNames.pop(); - } - } - else { - // Filter out response headers that are not simple headers and not in expose list - - // Keep simple headers and those found in the expose header list. - if (withCredentials || controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { - std::queue nonSimpleNonExposedHeaders; - - for (const auto& header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == - s_simpleCorsResponseHeaderNames.cend() && - controlValues.ExposedHeaders.find(header.Key().c_str()) == controlValues.ExposedHeaders.cend()) { - nonSimpleNonExposedHeaders.push(header.Key()); - } - } + // When withCredentials is false, request cannot include cookies. Also, cookies will be ignored in + // responses. + removeAllCookies = !withCredentials && GetRuntimeOptionBool("Http.RemoveCookiesFromResponse"); + } // originPolicy == SimpleCrossOriginResourceSharing || CrossOriginResourceSharing - while (!nonSimpleNonExposedHeaders.empty()) { - response.Headers().Remove(nonSimpleNonExposedHeaders.front()); - nonSimpleNonExposedHeaders.pop(); - } - } - } + // Don't expose HttpOnly cookies to JavaScript + RemoveHttpOnlyCookiesFromResponseHeaders(response, removeAllCookies); + } - // When withCredentials is false, request cannot include cookies. Also, cookies will be ignored in - // responses. - removeAllCookies = !withCredentials && GetRuntimeOptionBool("Http.RemoveCookiesFromResponse"); - } // originPolicy == SimpleCrossOriginResourceSharing || CrossOriginResourceSharing + ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage const &request) const { + auto coRequest = request; - // Don't expose HttpOnly cookies to JavaScript - RemoveHttpOnlyCookiesFromResponseHeaders(response, removeAllCookies); - } + HttpRequestMessage preflightRequest; - ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage const& request) const { - auto coRequest = request; + // Section 4.8.2 https://fetch.spec.whatwg.org/#cors-preflight-fetch + preflightRequest.Method(HttpMethod::Options()); + preflightRequest.RequestUri(coRequest.RequestUri()); + preflightRequest.Headers().Insert(L"Accept", L"*/*"); + preflightRequest.Headers().Insert(L"Access-Control-Request-Method", coRequest.Method().ToString()); - HttpRequestMessage preflightRequest; + auto headerNames = wstring{}; + auto headerItr = coRequest.Headers().begin(); + if (headerItr != coRequest.Headers().end()) { + headerNames += (*headerItr).Key(); - // Section 4.8.2 https://fetch.spec.whatwg.org/#cors-preflight-fetch - preflightRequest.Method(HttpMethod::Options()); - preflightRequest.RequestUri(coRequest.RequestUri()); - preflightRequest.Headers().Insert(L"Accept", L"*/*"); - preflightRequest.Headers().Insert(L"Access-Control-Request-Method", coRequest.Method().ToString()); + while (++headerItr != coRequest.Headers().end()) + headerNames += L", " + (*headerItr).Key(); + } - auto headerNames = wstring{}; - auto headerItr = coRequest.Headers().begin(); - if (headerItr != coRequest.Headers().end()) { + if (coRequest.Content()) { + headerItr = coRequest.Content().Headers().begin(); + if (headerItr != coRequest.Content().Headers().end()) { headerNames += (*headerItr).Key(); - while (++headerItr != coRequest.Headers().end()) + while (++headerItr != coRequest.Content().Headers().end()) headerNames += L", " + (*headerItr).Key(); } + } - if (coRequest.Content()) { - headerItr = coRequest.Content().Headers().begin(); - if (headerItr != coRequest.Content().Headers().end()) { - headerNames += (*headerItr).Key(); - - while (++headerItr != coRequest.Content().Headers().end()) - headerNames += L", " + (*headerItr).Key(); - } - } - - preflightRequest.Headers().Insert(L"Access-Control-Request-Headers", headerNames); - preflightRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); - preflightRequest.Headers().Insert(L"Sec-Fetch-Mode", L"CORS"); + preflightRequest.Headers().Insert(L"Access-Control-Request-Headers", headerNames); + preflightRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); + preflightRequest.Headers().Insert(L"Sec-Fetch-Mode", L"CORS"); - co_return{ co_await m_innerFilter.SendRequestAsync(preflightRequest) }; - } + 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()) && + 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; - } + // 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; } + return true; + } + #pragma endregion IRedirectEventSource #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()) && + 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; - } + // 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; } + return true; + } + #pragma endregion IRedirectEventSource #pragma region IHttpFilter - ResponseOperation OriginPolicyHttpFilter::SendRequestAsync(HttpRequestMessage const& request) { - auto coRequest = request; + ResponseOperation OriginPolicyHttpFilter::SendRequestAsync(HttpRequestMessage const &request) { + auto coRequest = request; - // Set initial origin policy to global runtime option. - request.Properties().Insert(L"OriginPolicy", winrt::box_value(GetRuntimeOptionInt("Http.OriginPolicy"))); + // Set initial origin policy to global runtime option. + request.Properties().Insert(L"OriginPolicy", winrt::box_value(GetRuntimeOptionInt("Http.OriginPolicy"))); - // Allow only HTTP or HTTPS schemes - if (GetRuntimeOptionBool("Http.StrictScheme") && coRequest.RequestUri().SchemeName() != L"https" && + // Allow only HTTP or HTTPS schemes + if (GetRuntimeOptionBool("Http.StrictScheme") && coRequest.RequestUri().SchemeName() != L"https" && coRequest.RequestUri().SchemeName() != L"http") - throw hresult_error{ E_INVALIDARG, L"Invalid URL scheme: [" + s_origin.SchemeName() + L"]" }; + throw hresult_error{E_INVALIDARG, L"Invalid URL scheme: [" + s_origin.SchemeName() + L"]"}; - if (!GetRuntimeOptionBool("Http.OmitCredentials")) { - coRequest.Properties().Lookup(L"RequestArgs").as()->WithCredentials = false; - } + if (!GetRuntimeOptionBool("Http.OmitCredentials")) { + coRequest.Properties().Lookup(L"RequestArgs").as()->WithCredentials = false; + } - // Ensure absolute URL - coRequest.RequestUri(Uri{ coRequest.RequestUri().AbsoluteCanonicalUri() }); + // Ensure absolute URL + coRequest.RequestUri(Uri{coRequest.RequestUri().AbsoluteCanonicalUri()}); - auto originPolicy = ValidateRequest(coRequest); - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + auto originPolicy = ValidateRequest(coRequest); + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || originPolicy == OriginPolicy::CrossOriginResourceSharing) { - if (coRequest.RequestUri().UserName().size() > 0 || coRequest.RequestUri().Password().size() > 0) { - coRequest.RequestUri(Uri{ coRequest.RequestUri().DisplayUri() }); - } + if (coRequest.RequestUri().UserName().size() > 0 || coRequest.RequestUri().Password().size() > 0) { + coRequest.RequestUri(Uri{coRequest.RequestUri().DisplayUri()}); } + } - try { - // #9770 - Validate preflight cache - if (originPolicy == OriginPolicy::CrossOriginResourceSharing) { - // If inner filter can AllowRedirect, disable for preflight. - winrt::impl::com_ref baseFilter; - if (baseFilter = m_innerFilter.try_as()) { - baseFilter.AllowAutoRedirect(false); - } - - auto preflightResponse = co_await SendPreflightAsync(coRequest); + try { + // #9770 - Validate preflight cache + if (originPolicy == OriginPolicy::CrossOriginResourceSharing) { + // If inner filter can AllowRedirect, disable for preflight. + winrt::impl::com_ref baseFilter; + if (baseFilter = m_innerFilter.try_as()) { + baseFilter.AllowAutoRedirect(false); + } - if (baseFilter) { - baseFilter.AllowAutoRedirect(true); - } + auto preflightResponse = co_await SendPreflightAsync(coRequest); - ValidatePreflightResponse(coRequest, preflightResponse); + if (baseFilter) { + baseFilter.AllowAutoRedirect(true); } - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + ValidatePreflightResponse(coRequest, preflightResponse); + } + + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || originPolicy == OriginPolicy::CrossOriginResourceSharing) { - coRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); - } + coRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); + } - auto response = co_await m_innerFilter.SendRequestAsync(coRequest); + auto response = co_await m_innerFilter.SendRequestAsync(coRequest); - ValidateResponse(response, originPolicy); + ValidateResponse(response, originPolicy); - co_return response; + co_return response; - } - catch (hresult_error const& e) { - throw e; - } - catch (const std::exception& e) { - throw hresult_error{ E_FAIL, to_hstring(e.what()) }; - } - catch (...) { - throw hresult_error{ E_FAIL, L"Unspecified error processing Origin Policy request" }; - } + } catch (hresult_error const &e) { + throw e; + } catch (const std::exception &e) { + throw hresult_error{E_FAIL, to_hstring(e.what())}; + } catch (...) { + throw hresult_error{E_FAIL, L"Unspecified error processing Origin Policy request"}; } + } #pragma endregion IHttpFilter #pragma endregion OriginPolicyHttpFilter - } // namespace Microsoft::React::Networking + } // namespace Microsoft::React::Networking diff --git a/vnext/Shared/OInstance.cpp b/vnext/Shared/OInstance.cpp index f070946f431..3f1e996f8af 100644 --- a/vnext/Shared/OInstance.cpp +++ b/vnext/Shared/OInstance.cpp @@ -640,39 +640,39 @@ std::vector> InstanceImpl::GetDefaultNativeModules [transitionalProps]() { return Microsoft::React::CreateBlobModule(transitionalProps); }, nativeQueue)); - modules.push_back(std::make_unique( - m_innerInstance, - Microsoft::React::GetFileReaderModuleName(), - [transitionalProps]() { return Microsoft::React::CreateFileReaderModule(transitionalProps); }, - nativeQueue)); - - return modules; -} + modules.push_back(std::make_unique( + m_innerInstance, + Microsoft::React::GetFileReaderModuleName(), + [transitionalProps]() { return Microsoft::React::CreateFileReaderModule(transitionalProps); }, + nativeQueue)); -void InstanceImpl::RegisterForReloadIfNecessary() noexcept { - // setup polling for live reload - if (!m_isInError && !m_devSettings->useFastRefresh && m_devSettings->liveReloadCallback != nullptr) { - m_devManager->StartPollingLiveReload( - m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort, m_devSettings->liveReloadCallback); + return modules; } -} -void InstanceImpl::DispatchEvent(int64_t viewTag, std::string eventName, folly::dynamic &&eventData) { - if (m_isInError) { - return; + void InstanceImpl::RegisterForReloadIfNecessary() noexcept { + // setup polling for live reload + if (!m_isInError && !m_devSettings->useFastRefresh && m_devSettings->liveReloadCallback != nullptr) { + m_devManager->StartPollingLiveReload( + m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort, m_devSettings->liveReloadCallback); + } } - folly::dynamic params = folly::dynamic::array(viewTag, eventName, std::move(eventData)); - m_innerInstance->callJSFunction("RCTEventEmitter", "receiveEvent", std::move(params)); -} + void InstanceImpl::DispatchEvent(int64_t viewTag, std::string eventName, folly::dynamic && eventData) { + if (m_isInError) { + return; + } -void InstanceImpl::invokeCallback(const int64_t callbackId, folly::dynamic &¶ms) { - if (m_isInError) { - return; + folly::dynamic params = folly::dynamic::array(viewTag, eventName, std::move(eventData)); + m_innerInstance->callJSFunction("RCTEventEmitter", "receiveEvent", std::move(params)); } - m_innerInstance->callJSCallback(callbackId, std::move(params)); -} + void InstanceImpl::invokeCallback(const int64_t callbackId, folly::dynamic &¶ms) { + if (m_isInError) { + return; + } + + m_innerInstance->callJSCallback(callbackId, std::move(params)); + } } // namespace react } // namespace facebook From 595e5adf3e4a28a110bc2545ad280db7ff4659b4 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Wed, 7 Dec 2022 02:49:50 -0800 Subject: [PATCH 32/45] format oinstance --- vnext/Shared/OInstance.cpp | 1038 ++++++++++++++++++------------------ 1 file changed, 524 insertions(+), 514 deletions(-) diff --git a/vnext/Shared/OInstance.cpp b/vnext/Shared/OInstance.cpp index 3f1e996f8af..c5ddb81439f 100644 --- a/vnext/Shared/OInstance.cpp +++ b/vnext/Shared/OInstance.cpp @@ -70,181 +70,182 @@ using winrt::Microsoft::ReactNative::ReactPropertyBagHelper; namespace Microsoft::React { -/*extern*/ std::unique_ptr CreateHttpModule( - winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept { - return std::make_unique(inspectableProperties); -} + /*extern*/ std::unique_ptr CreateHttpModule( + winrt::Windows::Foundation::IInspectable const& inspectableProperties) noexcept { + return std::make_unique(inspectableProperties); + } } // namespace Microsoft::React namespace facebook { -namespace react { - -namespace { - -class OJSIExecutorFactory : public JSExecutorFactory { - public: - std::unique_ptr createJSExecutor( - std::shared_ptr delegate, - std::shared_ptr jsQueue) override { - Logger logger; - if (loggingHook_) { - // TODO :: Ensure the logLevels are mapped properly. - logger = [loggingHook = std::move(loggingHook_)](const std::string &message, unsigned int logLevel) { - loggingHook(static_cast(logLevel), message.c_str()); - }; - } else { - logger = [loggingHook = std::move(loggingHook_)](const std::string & /*message*/, unsigned int /*logLevel*/) {}; - } - bindNativeLogger(*runtimeHolder_->getRuntime(), logger); + namespace react { + + namespace { + + class OJSIExecutorFactory: public JSExecutorFactory { + public: + std::unique_ptr createJSExecutor( + std::shared_ptr delegate, + std::shared_ptr jsQueue) override { + Logger logger; + if (loggingHook_) { + // TODO :: Ensure the logLevels are mapped properly. + logger = [loggingHook = std::move(loggingHook_)](const std::string& message, unsigned int logLevel) { + loggingHook(static_cast(logLevel), message.c_str()); + }; + } + else { + logger = [loggingHook = std::move(loggingHook_)](const std::string& /*message*/, unsigned int /*logLevel*/) {}; + } + bindNativeLogger(*runtimeHolder_->getRuntime(), logger); - auto turboModuleManager = std::make_shared(turboModuleRegistry_, jsCallInvoker_); + auto turboModuleManager = std::make_shared(turboModuleRegistry_, jsCallInvoker_); - // TODO: The binding here should also add the proxys that convert cxxmodules into turbomodules - // [vmoroz] Note, that we must not use the RN TurboCxxModule.h code because it uses global LongLivedObjectCollection - // instance that prevents us from using multiple RN instance in the same process. - auto binding = [turboModuleManager](const std::string &name) -> std::shared_ptr { - return turboModuleManager->getModule(name); - }; + // TODO: The binding here should also add the proxys that convert cxxmodules into turbomodules + // [vmoroz] Note, that we must not use the RN TurboCxxModule.h code because it uses global LongLivedObjectCollection + // instance that prevents us from using multiple RN instance in the same process. + auto binding = [turboModuleManager](const std::string& name) -> std::shared_ptr { + return turboModuleManager->getModule(name); + }; - TurboModuleBinding::install(*runtimeHolder_->getRuntime(), std::function(binding), longLivedObjectCollection_); + TurboModuleBinding::install(*runtimeHolder_->getRuntime(), std::function(binding), longLivedObjectCollection_); - // init TurboModule - for (const auto &moduleName : turboModuleManager->getEagerInitModuleNames()) { - turboModuleManager->getModule(moduleName); - } + // init TurboModule + for (const auto& moduleName : turboModuleManager->getEagerInitModuleNames()) { + turboModuleManager->getModule(moduleName); + } - return std::make_unique( - runtimeHolder_->getRuntime(), - std::move(delegate), - JSIExecutor::defaultTimeoutInvoker, - [isProfiling = isProfilingEnabled_]([[maybe_unused]] jsi::Runtime &runtime) { + return std::make_unique( + runtimeHolder_->getRuntime(), + std::move(delegate), + JSIExecutor::defaultTimeoutInvoker, + [isProfiling = isProfilingEnabled_]([[maybe_unused]] jsi::Runtime& runtime) { #ifdef ENABLE_JS_SYSTRACE_TO_ETW - facebook::react::tracing::initializeJSHooks(runtime, isProfiling); + facebook::react::tracing::initializeJSHooks(runtime, isProfiling); #endif - }); - } + }); + } + + OJSIExecutorFactory( + std::shared_ptr runtimeHolder, + NativeLoggingHook loggingHook, + std::shared_ptr turboModuleRegistry, + std::shared_ptr longLivedObjectCollection, + bool isProfilingEnabled, + std::shared_ptr jsCallInvoker) noexcept + : runtimeHolder_{ std::move(runtimeHolder) }, + loggingHook_{ std::move(loggingHook) }, + turboModuleRegistry_{ std::move(turboModuleRegistry) }, + longLivedObjectCollection_{ std::move(longLivedObjectCollection) }, + jsCallInvoker_{ std::move(jsCallInvoker) }, + isProfilingEnabled_{ isProfilingEnabled } {} + + private: + std::shared_ptr runtimeHolder_; + std::shared_ptr turboModuleRegistry_; + std::shared_ptr longLivedObjectCollection_; + std::shared_ptr jsCallInvoker_; + NativeLoggingHook loggingHook_; + bool isProfilingEnabled_; + }; + + } // namespace + + void logMarker(const facebook::react::ReactMarker::ReactMarkerId /*id*/, const char* /*tag*/) {} + + /*static*/ std::shared_ptr InstanceImpl::MakeNoBundle( + std::shared_ptr&& instance, + std::string&& jsBundleBasePath, + std::vector< + std::tuple>> + && cxxModules, + std::shared_ptr turboModuleRegistry, + std::shared_ptr longLivedObjectCollection, + std::unique_ptr&& callback, + std::shared_ptr jsQueue, + std::shared_ptr nativeQueue, + std::shared_ptr devSettings, + std::shared_ptr devManager) noexcept { + auto inner = std::shared_ptr(new InstanceImpl( + std::move(instance), + std::move(jsBundleBasePath), + std::move(cxxModules), + std::move(turboModuleRegistry), + std::move(longLivedObjectCollection), + std::move(callback), + std::move(jsQueue), + std::move(nativeQueue), + std::move(devSettings), + std::move(devManager))); + + inner->RegisterForReloadIfNecessary(); + + return inner; + } + + /*static*/ std::shared_ptr InstanceImpl::MakeAndLoadBundle( + std::shared_ptr&& instance, + std::string&& jsBundleBasePath, + std::string&& jsBundleRelativePath, + std::vector< + std::tuple>> + && cxxModules, + std::shared_ptr turboModuleRegistry, + std::unique_ptr&& callback, + std::shared_ptr jsQueue, + std::shared_ptr nativeQueue, + std::shared_ptr devSettings, + std::shared_ptr devManager) noexcept { + auto inner = std::shared_ptr(new InstanceImpl( + std::move(instance), + std::move(jsBundleBasePath), + std::move(cxxModules), + std::move(turboModuleRegistry), + nullptr, + std::move(callback), + std::move(jsQueue), + std::move(nativeQueue), + std::move(devSettings), + std::move(devManager))); + + inner->loadBundle(std::move(jsBundleRelativePath)); + inner->RegisterForReloadIfNecessary(); + + return inner; + } - OJSIExecutorFactory( - std::shared_ptr runtimeHolder, - NativeLoggingHook loggingHook, + void InstanceImpl::SetInError() noexcept { + m_isInError = true; + } + + namespace { + bool shouldStartHermesInspector(DevSettings& devSettings) { + bool isHermes = + ((devSettings.jsiEngineOverride == JSIEngineOverride::Hermes) || + (devSettings.jsiEngineOverride == JSIEngineOverride::Default && devSettings.jsiRuntimeHolder && + devSettings.jsiRuntimeHolder->getRuntimeType() == facebook::react::JSIEngineOverride::Hermes)); + + if (isHermes && devSettings.useDirectDebugger && !devSettings.useWebDebugger) + return true; + else + return false; + } + } // namespace + + InstanceImpl::InstanceImpl( + std::shared_ptr&& instance, + std::string&& jsBundleBasePath, + std::vector< + std::tuple>> + && cxxModules, std::shared_ptr turboModuleRegistry, - std::shared_ptr longLivedObjectCollection, - bool isProfilingEnabled, - std::shared_ptr jsCallInvoker) noexcept - : runtimeHolder_{std::move(runtimeHolder)}, - loggingHook_{std::move(loggingHook)}, - turboModuleRegistry_{std::move(turboModuleRegistry)}, - longLivedObjectCollection_{std::move(longLivedObjectCollection)}, - jsCallInvoker_{std::move(jsCallInvoker)}, - isProfilingEnabled_{isProfilingEnabled} {} - - private: - std::shared_ptr runtimeHolder_; - std::shared_ptr turboModuleRegistry_; - std::shared_ptr longLivedObjectCollection_; - std::shared_ptr jsCallInvoker_; - NativeLoggingHook loggingHook_; - bool isProfilingEnabled_; -}; - -} // namespace - -void logMarker(const facebook::react::ReactMarker::ReactMarkerId /*id*/, const char * /*tag*/) {} - -/*static*/ std::shared_ptr InstanceImpl::MakeNoBundle( - std::shared_ptr &&instance, - std::string &&jsBundleBasePath, - std::vector< - std::tuple>> - &&cxxModules, - std::shared_ptr turboModuleRegistry, - std::shared_ptr longLivedObjectCollection, - std::unique_ptr &&callback, - std::shared_ptr jsQueue, - std::shared_ptr nativeQueue, - std::shared_ptr devSettings, - std::shared_ptr devManager) noexcept { - auto inner = std::shared_ptr(new InstanceImpl( - std::move(instance), - std::move(jsBundleBasePath), - std::move(cxxModules), - std::move(turboModuleRegistry), - std::move(longLivedObjectCollection), - std::move(callback), - std::move(jsQueue), - std::move(nativeQueue), - std::move(devSettings), - std::move(devManager))); - - inner->RegisterForReloadIfNecessary(); - - return inner; -} - -/*static*/ std::shared_ptr InstanceImpl::MakeAndLoadBundle( - std::shared_ptr &&instance, - std::string &&jsBundleBasePath, - std::string &&jsBundleRelativePath, - std::vector< - std::tuple>> - &&cxxModules, - std::shared_ptr turboModuleRegistry, - std::unique_ptr &&callback, - std::shared_ptr jsQueue, - std::shared_ptr nativeQueue, - std::shared_ptr devSettings, - std::shared_ptr devManager) noexcept { - auto inner = std::shared_ptr(new InstanceImpl( - std::move(instance), - std::move(jsBundleBasePath), - std::move(cxxModules), - std::move(turboModuleRegistry), - nullptr, - std::move(callback), - std::move(jsQueue), - std::move(nativeQueue), - std::move(devSettings), - std::move(devManager))); - - inner->loadBundle(std::move(jsBundleRelativePath)); - inner->RegisterForReloadIfNecessary(); - - return inner; -} - -void InstanceImpl::SetInError() noexcept { - m_isInError = true; -} - -namespace { -bool shouldStartHermesInspector(DevSettings &devSettings) { - bool isHermes = - ((devSettings.jsiEngineOverride == JSIEngineOverride::Hermes) || - (devSettings.jsiEngineOverride == JSIEngineOverride::Default && devSettings.jsiRuntimeHolder && - devSettings.jsiRuntimeHolder->getRuntimeType() == facebook::react::JSIEngineOverride::Hermes)); - - if (isHermes && devSettings.useDirectDebugger && !devSettings.useWebDebugger) - return true; - else - return false; -} -} // namespace - -InstanceImpl::InstanceImpl( - std::shared_ptr &&instance, - std::string &&jsBundleBasePath, - std::vector< - std::tuple>> - &&cxxModules, - std::shared_ptr turboModuleRegistry, - std::shared_ptr longLivedObjectCollection, - std::unique_ptr &&callback, - std::shared_ptr jsQueue, - std::shared_ptr nativeQueue, - std::shared_ptr devSettings, - std::shared_ptr devManager) - : m_turboModuleRegistry(std::move(turboModuleRegistry)), + std::shared_ptr longLivedObjectCollection, + std::unique_ptr&& callback, + std::shared_ptr jsQueue, + std::shared_ptr nativeQueue, + std::shared_ptr devSettings, + std::shared_ptr devManager) + : m_turboModuleRegistry(std::move(turboModuleRegistry)), m_longLivedObjectCollection(std::move(longLivedObjectCollection)), m_jsThread(std::move(jsQueue)), m_nativeQueue(nativeQueue), @@ -252,427 +253,436 @@ InstanceImpl::InstanceImpl( m_devSettings(std::move(devSettings)), m_devManager(std::move(devManager)), m_innerInstance(std::move(instance)) { - // Temp set the logmarker here - facebook::react::ReactMarker::logTaggedMarker = logMarker; + // Temp set the logmarker here + facebook::react::ReactMarker::logTaggedMarker = logMarker; #ifdef ENABLE_ETW_TRACING - // TODO :: Find a better place to initialize ETW once per process. - facebook::react::tracing::initializeETW(); + // TODO :: Find a better place to initialize ETW once per process. + facebook::react::tracing::initializeETW(); #endif - if (shouldStartHermesInspector(*m_devSettings)) { - m_devManager->EnsureHermesInspector(m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort); - } - - // Default (common) NativeModules - auto modules = GetDefaultNativeModules(nativeQueue); + if (shouldStartHermesInspector(*m_devSettings)) { + m_devManager->EnsureHermesInspector(m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort); + } - // Add app provided modules. - for (auto &cxxModule : cxxModules) { - modules.push_back(std::make_unique( - m_innerInstance, move(std::get<0>(cxxModule)), move(std::get<1>(cxxModule)), move(std::get<2>(cxxModule)))); - } - m_moduleRegistry = std::make_shared(std::move(modules)); - - // Choose JSExecutor - std::shared_ptr jsef; - if (m_devSettings->useWebDebugger) { - try { - auto jseFunc = m_devManager->LoadJavaScriptInProxyMode(*m_devSettings, [weakthis = weak_from_this()]() { - if (auto strongThis = weakthis.lock()) { - strongThis->SetInError(); - } - }); + // Default (common) NativeModules + auto modules = GetDefaultNativeModules(nativeQueue); - if ((jseFunc == nullptr) || m_isInError) { - m_devSettings->errorCallback("Failed to create JavaScript Executor."); - return; + // Add app provided modules. + for (auto& cxxModule : cxxModules) { + modules.push_back(std::make_unique( + m_innerInstance, move(std::get<0>(cxxModule)), move(std::get<1>(cxxModule)), move(std::get<2>(cxxModule)))); } + m_moduleRegistry = std::make_shared(std::move(modules)); + + // Choose JSExecutor + std::shared_ptr jsef; + if (m_devSettings->useWebDebugger) { + try { + auto jseFunc = m_devManager->LoadJavaScriptInProxyMode(*m_devSettings, [weakthis = weak_from_this()]() { + if (auto strongThis = weakthis.lock()) { + strongThis->SetInError(); + } + }); + + if ((jseFunc == nullptr) || m_isInError) { + m_devSettings->errorCallback("Failed to create JavaScript Executor."); + return; + } - jsef = std::make_shared(std::move(jseFunc)); - } catch (std::exception &e) { - m_devSettings->errorCallback(e.what()); - return; - } - } else { - if (m_devSettings->useFastRefresh || m_devSettings->liveReloadCallback) { - Microsoft::ReactNative::PackagerConnection::CreateOrReusePackagerConnection(*m_devSettings); - } + jsef = std::make_shared(std::move(jseFunc)); + } + catch (std::exception& e) { + m_devSettings->errorCallback(e.what()); + return; + } + } + else { + if (m_devSettings->useFastRefresh || m_devSettings->liveReloadCallback) { + Microsoft::ReactNative::PackagerConnection::CreateOrReusePackagerConnection(*m_devSettings); + } - // If the consumer gives us a JSI runtime, then use it. - if (m_devSettings->jsiRuntimeHolder) { - assert(m_devSettings->jsiEngineOverride == JSIEngineOverride::Default); - jsef = std::make_shared( - m_devSettings->jsiRuntimeHolder, - m_devSettings->loggingCallback, - m_turboModuleRegistry, - m_longLivedObjectCollection, - !m_devSettings->useFastRefresh, - m_innerInstance->getJSCallInvoker()); - } else { - assert(m_devSettings->jsiEngineOverride != JSIEngineOverride::Default); - switch (m_devSettings->jsiEngineOverride) { - case JSIEngineOverride::Hermes: - m_devSettings->jsiRuntimeHolder = std::make_shared(m_devSettings, m_jsThread); - break; - case JSIEngineOverride::V8: { + // If the consumer gives us a JSI runtime, then use it. + if (m_devSettings->jsiRuntimeHolder) { + assert(m_devSettings->jsiEngineOverride == JSIEngineOverride::Default); + jsef = std::make_shared( + m_devSettings->jsiRuntimeHolder, + m_devSettings->loggingCallback, + m_turboModuleRegistry, + m_longLivedObjectCollection, + !m_devSettings->useFastRefresh, + m_innerInstance->getJSCallInvoker()); + } + else { + assert(m_devSettings->jsiEngineOverride != JSIEngineOverride::Default); + switch (m_devSettings->jsiEngineOverride) { + case JSIEngineOverride::Hermes: + m_devSettings->jsiRuntimeHolder = std::make_shared(m_devSettings, m_jsThread); + break; + case JSIEngineOverride::V8: { #if defined(USE_V8) - std::unique_ptr scriptStore = nullptr; - std::unique_ptr preparedScriptStore = nullptr; + std::unique_ptr scriptStore = nullptr; + std::unique_ptr preparedScriptStore = nullptr; - char tempPath[MAX_PATH]; - if (GetTempPathA(MAX_PATH, tempPath)) { - preparedScriptStore = std::make_unique(tempPath); - } + char tempPath[MAX_PATH]; + if (GetTempPathA(MAX_PATH, tempPath)) { + preparedScriptStore = std::make_unique(tempPath); + } - m_devSettings->jsiRuntimeHolder = std::make_shared( + m_devSettings->jsiRuntimeHolder = std::make_shared( m_devSettings, m_jsThread, std::move(scriptStore), std::move(preparedScriptStore)); - break; + break; #else - assert(false); // V8 is not available in this build, fallthrough - [[fallthrough]]; + assert(false); // V8 is not available in this build, fallthrough + [[fallthrough]]; #endif - } - case JSIEngineOverride::V8NodeApi: { + } + case JSIEngineOverride::V8NodeApi: { #if defined(USE_V8) - std::unique_ptr preparedScriptStore; + std::unique_ptr preparedScriptStore; - wchar_t tempPath[MAX_PATH]; - if (GetTempPathW(static_cast(std::size(tempPath)), tempPath)) { - preparedScriptStore = + wchar_t tempPath[MAX_PATH]; + if (GetTempPathW(static_cast(std::size(tempPath)), tempPath)) { + preparedScriptStore = std::make_unique(winrt::to_string(tempPath)); - } + } - if (!preparedScriptStore) { - if (m_devSettings->errorCallback) - m_devSettings->errorCallback("Could not initialize prepared script store"); + if (!preparedScriptStore) { + if (m_devSettings->errorCallback) + m_devSettings->errorCallback("Could not initialize prepared script store"); - break; - } + break; + } - m_devSettings->jsiRuntimeHolder = make_shared( + m_devSettings->jsiRuntimeHolder = make_shared( m_devSettings, m_jsThread, nullptr /*scriptStore*/, std::move(preparedScriptStore)); - break; + break; #else - if (m_devSettings->errorCallback) - m_devSettings->errorCallback("JSI/V8/NAPI engine is not available in this build"); - assert(false); - [[fallthrough]]; + if (m_devSettings->errorCallback) + m_devSettings->errorCallback("JSI/V8/NAPI engine is not available in this build"); + assert(false); + [[fallthrough]]; #endif - } - case JSIEngineOverride::Chakra: - case JSIEngineOverride::ChakraCore: - default: // TODO: Add other engines once supported - m_devSettings->jsiRuntimeHolder = + } + case JSIEngineOverride::Chakra: + case JSIEngineOverride::ChakraCore: + default: // TODO: Add other engines once supported + m_devSettings->jsiRuntimeHolder = std::make_shared(m_devSettings, m_jsThread, nullptr, nullptr); - break; + break; + } + jsef = std::make_shared( + m_devSettings->jsiRuntimeHolder, + m_devSettings->loggingCallback, + m_turboModuleRegistry, + m_longLivedObjectCollection, + !m_devSettings->useFastRefresh, + m_innerInstance->getJSCallInvoker()); + } + } + + m_innerInstance->initializeBridge(std::move(callback), jsef, m_jsThread, m_moduleRegistry); + + // All JSI runtimes do support host objects and hence the native modules + // proxy. + const bool isNativeModulesProxyAvailable = ((m_devSettings->jsiRuntimeHolder != nullptr) || + (m_devSettings->jsiEngineOverride != JSIEngineOverride::Default)) && + !m_devSettings->useWebDebugger; + if (!isNativeModulesProxyAvailable) { + folly::dynamic configArray = folly::dynamic::array; + for (auto const& moduleName : m_moduleRegistry->moduleNames()) { + auto moduleConfig = m_moduleRegistry->getConfig(moduleName); + configArray.push_back(moduleConfig ? std::move(moduleConfig->config) : nullptr); + } + + folly::dynamic configs = folly::dynamic::object("remoteModuleConfig", std::move(configArray)); + m_innerInstance->setGlobalVariable( + "__fbBatchedBridgeConfig", std::make_unique(folly::toJson(configs))); } - jsef = std::make_shared( - m_devSettings->jsiRuntimeHolder, - m_devSettings->loggingCallback, - m_turboModuleRegistry, - m_longLivedObjectCollection, - !m_devSettings->useFastRefresh, - m_innerInstance->getJSCallInvoker()); } - } - m_innerInstance->initializeBridge(std::move(callback), jsef, m_jsThread, m_moduleRegistry); - - // All JSI runtimes do support host objects and hence the native modules - // proxy. - const bool isNativeModulesProxyAvailable = ((m_devSettings->jsiRuntimeHolder != nullptr) || - (m_devSettings->jsiEngineOverride != JSIEngineOverride::Default)) && - !m_devSettings->useWebDebugger; - if (!isNativeModulesProxyAvailable) { - folly::dynamic configArray = folly::dynamic::array; - for (auto const &moduleName : m_moduleRegistry->moduleNames()) { - auto moduleConfig = m_moduleRegistry->getConfig(moduleName); - configArray.push_back(moduleConfig ? std::move(moduleConfig->config) : nullptr); + void InstanceImpl::loadBundle(std::string&& jsBundleRelativePath) { + loadBundleInternal(std::move(jsBundleRelativePath), /*synchronously:*/ false); } - folly::dynamic configs = folly::dynamic::object("remoteModuleConfig", std::move(configArray)); - m_innerInstance->setGlobalVariable( - "__fbBatchedBridgeConfig", std::make_unique(folly::toJson(configs))); - } -} - -void InstanceImpl::loadBundle(std::string &&jsBundleRelativePath) { - loadBundleInternal(std::move(jsBundleRelativePath), /*synchronously:*/ false); -} - -void InstanceImpl::loadBundleSync(std::string &&jsBundleRelativePath) { - loadBundleInternal(std::move(jsBundleRelativePath), /*synchronously:*/ true); -} - -// Note: Based on -// https://github.com/facebook/react-native/blob/24d91268b64c7abbd4b26547ffcc663dc90ec5e7/ReactCommon/cxxreact/Instance.cpp#L112 -bool isHBCBundle(const std::string &bundle) { - static uint32_t constexpr HBCBundleMagicNumber = 0xffe7c3c3; - - // Note:: Directly access the pointer to avoid copy/length-check. It matters as this string contains the bundle which - // can be potentially huge. - // https://herbsutter.com/2008/04/07/cringe-not-vectors-are-guaranteed-to-be-contiguous/#comment-483 - auto header = reinterpret_cast(&bundle[0]); - if (HBCBundleMagicNumber == header->magic32.value) { - return true; - } else { - return false; - } -} + void InstanceImpl::loadBundleSync(std::string&& jsBundleRelativePath) { + loadBundleInternal(std::move(jsBundleRelativePath), /*synchronously:*/ true); + } -void InstanceImpl::loadBundleInternal(std::string &&jsBundleRelativePath, bool synchronously) { - try { - if (m_devSettings->useWebDebugger || m_devSettings->liveReloadCallback != nullptr || - m_devSettings->useFastRefresh) { - // First attempt to get download the Js locally, to catch any bundling - // errors before attempting to load the actual script. + // Note: Based on + // https://github.com/facebook/react-native/blob/24d91268b64c7abbd4b26547ffcc663dc90ec5e7/ReactCommon/cxxreact/Instance.cpp#L112 + bool isHBCBundle(const std::string& bundle) { + static uint32_t constexpr HBCBundleMagicNumber = 0xffe7c3c3; + + // Note:: Directly access the pointer to avoid copy/length-check. It matters as this string contains the bundle which + // can be potentially huge. + // https://herbsutter.com/2008/04/07/cringe-not-vectors-are-guaranteed-to-be-contiguous/#comment-483 + auto header = reinterpret_cast(&bundle[0]); + if (HBCBundleMagicNumber == header->magic32.value) { + return true; + } + else { + return false; + } + } - uint32_t hermesBytecodeVersion = 0; + void InstanceImpl::loadBundleInternal(std::string&& jsBundleRelativePath, bool synchronously) { + try { + if (m_devSettings->useWebDebugger || m_devSettings->liveReloadCallback != nullptr || + m_devSettings->useFastRefresh) { + // First attempt to get download the Js locally, to catch any bundling + // errors before attempting to load the actual script. + + uint32_t hermesBytecodeVersion = 0; #if defined(USE_HERMES) && defined(ENABLE_DEVSERVER_HBCBUNDLES) - hermesBytecodeVersion = ::hermes::hbc::BYTECODE_VERSION; + hermesBytecodeVersion = ::hermes::hbc::BYTECODE_VERSION; #endif - auto [jsBundleString, success] = Microsoft::ReactNative::GetJavaScriptFromServer( - m_devSettings->sourceBundleHost, - m_devSettings->sourceBundlePort, - m_devSettings->debugBundlePath.empty() ? jsBundleRelativePath : m_devSettings->debugBundlePath, - m_devSettings->platformName, - true /* dev */, - m_devSettings->useFastRefresh, - m_devSettings->inlineSourceMap, - hermesBytecodeVersion); + auto [jsBundleString, success] = Microsoft::ReactNative::GetJavaScriptFromServer( + m_devSettings->sourceBundleHost, + m_devSettings->sourceBundlePort, + m_devSettings->debugBundlePath.empty() ? jsBundleRelativePath : m_devSettings->debugBundlePath, + m_devSettings->platformName, + true /* dev */, + m_devSettings->useFastRefresh, + m_devSettings->inlineSourceMap, + hermesBytecodeVersion); - if (!success) { - m_devManager->UpdateBundleStatus(false, -1); - m_devSettings->errorCallback(jsBundleString); - return; - } + if (!success) { + m_devManager->UpdateBundleStatus(false, -1); + m_devSettings->errorCallback(jsBundleString); + return; + } - int64_t currentTimeInMilliSeconds = - std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) - .count(); - m_devManager->UpdateBundleStatus(true, currentTimeInMilliSeconds); + int64_t currentTimeInMilliSeconds = + std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) + .count(); + m_devManager->UpdateBundleStatus(true, currentTimeInMilliSeconds); - auto bundleUrl = DevServerHelper::get_BundleUrl( - m_devSettings->sourceBundleHost, - m_devSettings->sourceBundlePort, - m_devSettings->debugBundlePath.empty() ? jsBundleRelativePath : m_devSettings->debugBundlePath, - m_devSettings->platformName, - /*dev*/ true, - /*hot*/ false, - m_devSettings->inlineSourceMap, - hermesBytecodeVersion); - - // This code is based on the HBC Bundle integration on Android - // Ref: - // https://github.com/facebook/react-native/blob/24d91268b64c7abbd4b26547ffcc663dc90ec5e7/ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.cpp#L231 - if (isHBCBundle(jsBundleString)) { - auto script = std::make_unique(jsBundleString, false); - const char *buffer = script->c_str(); - uint32_t bufferLength = (uint32_t)script->size(); - - // Please refer the code here for details on the file format: - // https://github.com/facebook/metro/blob/b1bacf52070be62872d6bd3420f37a4405ed34e6/packages/metro/src/lib/bundleToBytecode.js#L29 - // Essentially, there is an 8 byte long file header with 4 bytes of a magic number followed by 4 bytes to encode - // the number of modules.The module buffers follows, each one starts with 4 byte header which encodes module - // length.A properly formatted HBCB should have at least 8 bytes.. - uint32_t offset = 8; + auto bundleUrl = DevServerHelper::get_BundleUrl( + m_devSettings->sourceBundleHost, + m_devSettings->sourceBundlePort, + m_devSettings->debugBundlePath.empty() ? jsBundleRelativePath : m_devSettings->debugBundlePath, + m_devSettings->platformName, + /*dev*/ true, + /*hot*/ false, + m_devSettings->inlineSourceMap, + hermesBytecodeVersion); + + // This code is based on the HBC Bundle integration on Android + // Ref: + // https://github.com/facebook/react-native/blob/24d91268b64c7abbd4b26547ffcc663dc90ec5e7/ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.cpp#L231 + if (isHBCBundle(jsBundleString)) { + auto script = std::make_unique(jsBundleString, false); + const char* buffer = script->c_str(); + uint32_t bufferLength = (uint32_t)script->size(); + + // Please refer the code here for details on the file format: + // https://github.com/facebook/metro/blob/b1bacf52070be62872d6bd3420f37a4405ed34e6/packages/metro/src/lib/bundleToBytecode.js#L29 + // Essentially, there is an 8 byte long file header with 4 bytes of a magic number followed by 4 bytes to encode + // the number of modules.The module buffers follows, each one starts with 4 byte header which encodes module + // length.A properly formatted HBCB should have at least 8 bytes.. + uint32_t offset = 8; #define __SAFEADD__(s1, s2, t) \ if (!msl::utilities::SafeAdd(s1, s2, t)) \ break; - while (offset < bufferLength) { - uint32_t segment; - __SAFEADD__(offset, 4, segment) - uint32_t moduleLength = (bufferLength < segment) ? 0 : *(((uint32_t *)buffer) + offset / 4); - - // Early break if the module length is computed as 0.. as the segment start may be overflowing the buffer. - if (moduleLength == 0) - break; - - uint32_t segmentEnd; - __SAFEADD__(moduleLength, segment, segmentEnd) - // Early break if the segment overflows beyond the buffer. This is unlikely for a properly formatted - // HBCB though. - if (segmentEnd > bufferLength) - break; - - m_innerInstance->loadScriptFromString( - std::make_unique(std::string(buffer + segment, buffer + segmentEnd)), + while (offset < bufferLength) { + uint32_t segment; + __SAFEADD__(offset, 4, segment) + uint32_t moduleLength = (bufferLength < segment) ? 0 : *(((uint32_t*)buffer) + offset / 4); + + // Early break if the module length is computed as 0.. as the segment start may be overflowing the buffer. + if (moduleLength == 0) + break; + + uint32_t segmentEnd; + __SAFEADD__(moduleLength, segment, segmentEnd) + // Early break if the segment overflows beyond the buffer. This is unlikely for a properly formatted + // HBCB though. + if (segmentEnd > bufferLength) + break; + + m_innerInstance->loadScriptFromString( + std::make_unique(std::string(buffer + segment, buffer + segmentEnd)), + bundleUrl, + false); + + // Aligned at 4 byte boundary. + offset += ((moduleLength + 3) & ~3) + 4; + } +#undef __SAFEADD__ + } + else { + // Remote debug executor loads script from a Uri, rather than taking the actual bundle string + m_innerInstance->loadScriptFromString( + std::make_unique(m_devSettings->useWebDebugger ? bundleUrl : jsBundleString), bundleUrl, - false); - - // Aligned at 4 byte boundary. - offset += ((moduleLength + 3) & ~3) + 4; + synchronously); + } } -#undef __SAFEADD__ - } else { - // Remote debug executor loads script from a Uri, rather than taking the actual bundle string - m_innerInstance->loadScriptFromString( - std::make_unique(m_devSettings->useWebDebugger ? bundleUrl : jsBundleString), - bundleUrl, - synchronously); - } - } else { + else { #if (defined(_MSC_VER) && !defined(WINRT)) - std::string bundlePath = (fs::path(m_devSettings->bundleRootPath) / jsBundleRelativePath).string(); - auto bundleString = FileMappingBigString::fromPath(bundlePath); + std::string bundlePath = (fs::path(m_devSettings->bundleRootPath) / jsBundleRelativePath).string(); + auto bundleString = FileMappingBigString::fromPath(bundlePath); #else - std::string bundlePath; - if (m_devSettings->bundleRootPath._Starts_with("resource://")) { - auto uri = winrt::Windows::Foundation::Uri( - winrt::to_hstring(m_devSettings->bundleRootPath), winrt::to_hstring(jsBundleRelativePath)); - bundlePath = winrt::to_string(uri.ToString()); - } else { - bundlePath = (fs::path(m_devSettings->bundleRootPath) / (jsBundleRelativePath + ".bundle")).string(); - } + std::string bundlePath; + if (m_devSettings->bundleRootPath._Starts_with("resource://")) { + auto uri = winrt::Windows::Foundation::Uri( + winrt::to_hstring(m_devSettings->bundleRootPath), winrt::to_hstring(jsBundleRelativePath)); + bundlePath = winrt::to_string(uri.ToString()); + } + else { + bundlePath = (fs::path(m_devSettings->bundleRootPath) / (jsBundleRelativePath + ".bundle")).string(); + } - auto bundleString = std::make_unique<::Microsoft::ReactNative::StorageFileBigString>(bundlePath); + auto bundleString = std::make_unique<::Microsoft::ReactNative::StorageFileBigString>(bundlePath); #endif - m_innerInstance->loadScriptFromString(std::move(bundleString), std::move(jsBundleRelativePath), synchronously); - } - } catch (const std::exception &e) { - m_devSettings->errorCallback(e.what()); - } catch (const winrt::hresult_error &hrerr) { - auto error = fmt::format("[0x{:0>8x}] {}", static_cast(hrerr.code()), winrt::to_string(hrerr.message())); + m_innerInstance->loadScriptFromString(std::move(bundleString), std::move(jsBundleRelativePath), synchronously); + } + } + catch (const std::exception& e) { + m_devSettings->errorCallback(e.what()); + } + catch (const winrt::hresult_error& hrerr) { + auto error = fmt::format("[0x{:0>8x}] {}", static_cast(hrerr.code()), winrt::to_string(hrerr.message())); - m_devSettings->errorCallback(std::move(error)); - } -} + m_devSettings->errorCallback(std::move(error)); + } + } -InstanceImpl::~InstanceImpl() { - if (shouldStartHermesInspector(*m_devSettings) && m_devSettings->jsiRuntimeHolder) { - m_devSettings->jsiRuntimeHolder->teardown(); - } - m_nativeQueue->quitSynchronous(); -} + InstanceImpl::~InstanceImpl() { + if (shouldStartHermesInspector(*m_devSettings) && m_devSettings->jsiRuntimeHolder) { + m_devSettings->jsiRuntimeHolder->teardown(); + } + m_nativeQueue->quitSynchronous(); + } -std::vector> InstanceImpl::GetDefaultNativeModules( - std::shared_ptr nativeQueue) { - std::vector> modules; - auto transitionalProps{ReactPropertyBagHelper::CreatePropertyBag()}; + std::vector> InstanceImpl::GetDefaultNativeModules( + std::shared_ptr nativeQueue) { + std::vector> modules; + auto transitionalProps{ ReactPropertyBagHelper::CreatePropertyBag() }; #if (defined(_MSC_VER) && !defined(WINRT)) - modules.push_back(std::make_unique( - m_innerInstance, - Microsoft::React::GetHttpModuleName(), - [nativeQueue, transitionalProps]() -> std::unique_ptr { - return Microsoft::React::CreateHttpModule(transitionalProps); - }, - nativeQueue)); + modules.push_back(std::make_unique( + m_innerInstance, + Microsoft::React::GetHttpModuleName(), + [nativeQueue, transitionalProps]() -> std::unique_ptr { + return Microsoft::React::CreateHttpModule(transitionalProps); + }, + nativeQueue)); #endif - modules.push_back(std::make_unique( - m_innerInstance, - Microsoft::React::GetWebSocketModuleName(), - [nativeQueue, transitionalProps]() -> std::unique_ptr { - return Microsoft::React::CreateWebSocketModule(transitionalProps); - }, - nativeQueue)); - - // TODO: This is not included for UWP because we have a different module which - // is added later. However, this one is designed - // so that we can base a UWP version on it. We need to do that but is not high - // priority. + modules.push_back(std::make_unique( + m_innerInstance, + Microsoft::React::GetWebSocketModuleName(), + [nativeQueue, transitionalProps]() -> std::unique_ptr { + return Microsoft::React::CreateWebSocketModule(transitionalProps); + }, + nativeQueue)); + + // TODO: This is not included for UWP because we have a different module which + // is added later. However, this one is designed + // so that we can base a UWP version on it. We need to do that but is not high + // priority. #if (defined(_MSC_VER) && !defined(WINRT)) - modules.push_back(std::make_unique( - m_innerInstance, - "Timing", - [nativeQueue]() -> std::unique_ptr { return react::CreateTimingModule(nativeQueue); }, - nativeQueue)); + modules.push_back(std::make_unique( + m_innerInstance, + "Timing", + [nativeQueue]() -> std::unique_ptr { return react::CreateTimingModule(nativeQueue); }, + nativeQueue)); #endif - uint32_t hermesBytecodeVersion = 0; + uint32_t hermesBytecodeVersion = 0; #if defined(USE_HERMES) && defined(ENABLE_DEVSERVER_HBCBUNDLES) - hermesBytecodeVersion = ::hermes::hbc::BYTECODE_VERSION; + hermesBytecodeVersion = ::hermes::hbc::BYTECODE_VERSION; #endif - // TODO - Encapsulate this in a helpers, and make sure callers add it to their - // list - std::string bundleUrl = (m_devSettings->useWebDebugger || m_devSettings->liveReloadCallback) - ? DevServerHelper::get_BundleUrl( - m_devSettings->sourceBundleHost, - m_devSettings->sourceBundlePort, - m_devSettings->debugBundlePath, - m_devSettings->platformName, - true /*dev*/, - m_devSettings->useFastRefresh, - m_devSettings->inlineSourceMap, - hermesBytecodeVersion) - : std::string(); - modules.push_back(std::make_unique( - m_innerInstance, - facebook::react::SourceCodeModule::Name, - [bundleUrl]() -> std::unique_ptr { - return std::make_unique(bundleUrl); - }, - nativeQueue)); - - modules.push_back(std::make_unique( - m_innerInstance, - "ExceptionsManager", - [redboxHandler = m_devSettings->redboxHandler]() mutable { - return std::make_unique(redboxHandler); - }, - nativeQueue)); - - modules.push_back(std::make_unique( - m_innerInstance, - PlatformConstantsModule::Name, - []() { return std::make_unique(); }, - nativeQueue)); - - modules.push_back(std::make_unique( - m_innerInstance, - StatusBarManagerModule::Name, - []() { return std::make_unique(); }, - nativeQueue)); - - // 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")) { - modules.push_back(std::make_unique( + // TODO - Encapsulate this in a helpers, and make sure callers add it to their + // list + std::string bundleUrl = (m_devSettings->useWebDebugger || m_devSettings->liveReloadCallback) + ? DevServerHelper::get_BundleUrl( + m_devSettings->sourceBundleHost, + m_devSettings->sourceBundlePort, + m_devSettings->debugBundlePath, + m_devSettings->platformName, + true /*dev*/, + m_devSettings->useFastRefresh, + m_devSettings->inlineSourceMap, + hermesBytecodeVersion) + : std::string(); + modules.push_back(std::make_unique( m_innerInstance, - Microsoft::React::GetBlobModuleName(), - [transitionalProps]() { return Microsoft::React::CreateBlobModule(transitionalProps); }, + facebook::react::SourceCodeModule::Name, + [bundleUrl]() -> std::unique_ptr { + return std::make_unique(bundleUrl); + }, nativeQueue)); - modules.push_back(std::make_unique( + modules.push_back(std::make_unique( m_innerInstance, - Microsoft::React::GetFileReaderModuleName(), - [transitionalProps]() { return Microsoft::React::CreateFileReaderModule(transitionalProps); }, + "ExceptionsManager", + [redboxHandler = m_devSettings->redboxHandler]() mutable { + return std::make_unique(redboxHandler); + }, nativeQueue)); - return modules; - } + modules.push_back(std::make_unique( + m_innerInstance, + PlatformConstantsModule::Name, + []() { return std::make_unique(); }, + nativeQueue)); - void InstanceImpl::RegisterForReloadIfNecessary() noexcept { - // setup polling for live reload - if (!m_isInError && !m_devSettings->useFastRefresh && m_devSettings->liveReloadCallback != nullptr) { - m_devManager->StartPollingLiveReload( - m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort, m_devSettings->liveReloadCallback); - } - } + modules.push_back(std::make_unique( + m_innerInstance, + StatusBarManagerModule::Name, + []() { return std::make_unique(); }, + nativeQueue)); - void InstanceImpl::DispatchEvent(int64_t viewTag, std::string eventName, folly::dynamic && eventData) { - if (m_isInError) { - return; - } + // 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")) { + modules.push_back(std::make_unique( + m_innerInstance, + Microsoft::React::GetBlobModuleName(), + [transitionalProps]() { return Microsoft::React::CreateBlobModule(transitionalProps); }, + nativeQueue)); + + modules.push_back(std::make_unique( + m_innerInstance, + Microsoft::React::GetFileReaderModuleName(), + [transitionalProps]() { return Microsoft::React::CreateFileReaderModule(transitionalProps); }, + nativeQueue)); + + return modules; + } - folly::dynamic params = folly::dynamic::array(viewTag, eventName, std::move(eventData)); - m_innerInstance->callJSFunction("RCTEventEmitter", "receiveEvent", std::move(params)); - } + void InstanceImpl::RegisterForReloadIfNecessary() noexcept { + // setup polling for live reload + if (!m_isInError && !m_devSettings->useFastRefresh && m_devSettings->liveReloadCallback != nullptr) { + m_devManager->StartPollingLiveReload( + m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort, m_devSettings->liveReloadCallback); + } + } - void InstanceImpl::invokeCallback(const int64_t callbackId, folly::dynamic &¶ms) { - if (m_isInError) { - return; - } + void InstanceImpl::DispatchEvent(int64_t viewTag, std::string eventName, folly::dynamic && eventData) { + if (m_isInError) { + return; + } - m_innerInstance->callJSCallback(callbackId, std::move(params)); - } + folly::dynamic params = folly::dynamic::array(viewTag, eventName, std::move(eventData)); + m_innerInstance->callJSFunction("RCTEventEmitter", "receiveEvent", std::move(params)); + } + + void InstanceImpl::invokeCallback(const int64_t callbackId, folly::dynamic && params) { + if (m_isInError) { + return; + } + + m_innerInstance->callJSCallback(callbackId, std::move(params)); + } -} // namespace react -} // namespace facebook + } // namespace react + } // namespace facebook From 25adcedb1467d6d6fc22339009983ede85cae713 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Wed, 7 Dec 2022 02:53:41 -0800 Subject: [PATCH 33/45] format oinstance --- vnext/Shared/OInstance.cpp | 1038 ++++++++++++++++++------------------ 1 file changed, 514 insertions(+), 524 deletions(-) diff --git a/vnext/Shared/OInstance.cpp b/vnext/Shared/OInstance.cpp index c5ddb81439f..3f1e996f8af 100644 --- a/vnext/Shared/OInstance.cpp +++ b/vnext/Shared/OInstance.cpp @@ -70,182 +70,181 @@ using winrt::Microsoft::ReactNative::ReactPropertyBagHelper; namespace Microsoft::React { - /*extern*/ std::unique_ptr CreateHttpModule( - winrt::Windows::Foundation::IInspectable const& inspectableProperties) noexcept { - return std::make_unique(inspectableProperties); - } +/*extern*/ std::unique_ptr CreateHttpModule( + winrt::Windows::Foundation::IInspectable const &inspectableProperties) noexcept { + return std::make_unique(inspectableProperties); +} } // namespace Microsoft::React namespace facebook { - namespace react { - - namespace { - - class OJSIExecutorFactory: public JSExecutorFactory { - public: - std::unique_ptr createJSExecutor( - std::shared_ptr delegate, - std::shared_ptr jsQueue) override { - Logger logger; - if (loggingHook_) { - // TODO :: Ensure the logLevels are mapped properly. - logger = [loggingHook = std::move(loggingHook_)](const std::string& message, unsigned int logLevel) { - loggingHook(static_cast(logLevel), message.c_str()); - }; - } - else { - logger = [loggingHook = std::move(loggingHook_)](const std::string& /*message*/, unsigned int /*logLevel*/) {}; - } - bindNativeLogger(*runtimeHolder_->getRuntime(), logger); - - auto turboModuleManager = std::make_shared(turboModuleRegistry_, jsCallInvoker_); - - // TODO: The binding here should also add the proxys that convert cxxmodules into turbomodules - // [vmoroz] Note, that we must not use the RN TurboCxxModule.h code because it uses global LongLivedObjectCollection - // instance that prevents us from using multiple RN instance in the same process. - auto binding = [turboModuleManager](const std::string& name) -> std::shared_ptr { - return turboModuleManager->getModule(name); - }; - - TurboModuleBinding::install(*runtimeHolder_->getRuntime(), std::function(binding), longLivedObjectCollection_); - - // init TurboModule - for (const auto& moduleName : turboModuleManager->getEagerInitModuleNames()) { - turboModuleManager->getModule(moduleName); - } - - return std::make_unique( - runtimeHolder_->getRuntime(), - std::move(delegate), - JSIExecutor::defaultTimeoutInvoker, - [isProfiling = isProfilingEnabled_]([[maybe_unused]] jsi::Runtime& runtime) { -#ifdef ENABLE_JS_SYSTRACE_TO_ETW - facebook::react::tracing::initializeJSHooks(runtime, isProfiling); -#endif - }); - } - - OJSIExecutorFactory( - std::shared_ptr runtimeHolder, - NativeLoggingHook loggingHook, - std::shared_ptr turboModuleRegistry, - std::shared_ptr longLivedObjectCollection, - bool isProfilingEnabled, - std::shared_ptr jsCallInvoker) noexcept - : runtimeHolder_{ std::move(runtimeHolder) }, - loggingHook_{ std::move(loggingHook) }, - turboModuleRegistry_{ std::move(turboModuleRegistry) }, - longLivedObjectCollection_{ std::move(longLivedObjectCollection) }, - jsCallInvoker_{ std::move(jsCallInvoker) }, - isProfilingEnabled_{ isProfilingEnabled } {} - - private: - std::shared_ptr runtimeHolder_; - std::shared_ptr turboModuleRegistry_; - std::shared_ptr longLivedObjectCollection_; - std::shared_ptr jsCallInvoker_; - NativeLoggingHook loggingHook_; - bool isProfilingEnabled_; +namespace react { + +namespace { + +class OJSIExecutorFactory : public JSExecutorFactory { + public: + std::unique_ptr createJSExecutor( + std::shared_ptr delegate, + std::shared_ptr jsQueue) override { + Logger logger; + if (loggingHook_) { + // TODO :: Ensure the logLevels are mapped properly. + logger = [loggingHook = std::move(loggingHook_)](const std::string &message, unsigned int logLevel) { + loggingHook(static_cast(logLevel), message.c_str()); }; + } else { + logger = [loggingHook = std::move(loggingHook_)](const std::string & /*message*/, unsigned int /*logLevel*/) {}; + } + bindNativeLogger(*runtimeHolder_->getRuntime(), logger); - } // namespace + auto turboModuleManager = std::make_shared(turboModuleRegistry_, jsCallInvoker_); - void logMarker(const facebook::react::ReactMarker::ReactMarkerId /*id*/, const char* /*tag*/) {} + // TODO: The binding here should also add the proxys that convert cxxmodules into turbomodules + // [vmoroz] Note, that we must not use the RN TurboCxxModule.h code because it uses global LongLivedObjectCollection + // instance that prevents us from using multiple RN instance in the same process. + auto binding = [turboModuleManager](const std::string &name) -> std::shared_ptr { + return turboModuleManager->getModule(name); + }; - /*static*/ std::shared_ptr InstanceImpl::MakeNoBundle( - std::shared_ptr&& instance, - std::string&& jsBundleBasePath, - std::vector< - std::tuple>> - && cxxModules, - std::shared_ptr turboModuleRegistry, - std::shared_ptr longLivedObjectCollection, - std::unique_ptr&& callback, - std::shared_ptr jsQueue, - std::shared_ptr nativeQueue, - std::shared_ptr devSettings, - std::shared_ptr devManager) noexcept { - auto inner = std::shared_ptr(new InstanceImpl( - std::move(instance), - std::move(jsBundleBasePath), - std::move(cxxModules), - std::move(turboModuleRegistry), - std::move(longLivedObjectCollection), - std::move(callback), - std::move(jsQueue), - std::move(nativeQueue), - std::move(devSettings), - std::move(devManager))); - - inner->RegisterForReloadIfNecessary(); - - return inner; - } + TurboModuleBinding::install(*runtimeHolder_->getRuntime(), std::function(binding), longLivedObjectCollection_); - /*static*/ std::shared_ptr InstanceImpl::MakeAndLoadBundle( - std::shared_ptr&& instance, - std::string&& jsBundleBasePath, - std::string&& jsBundleRelativePath, - std::vector< - std::tuple>> - && cxxModules, - std::shared_ptr turboModuleRegistry, - std::unique_ptr&& callback, - std::shared_ptr jsQueue, - std::shared_ptr nativeQueue, - std::shared_ptr devSettings, - std::shared_ptr devManager) noexcept { - auto inner = std::shared_ptr(new InstanceImpl( - std::move(instance), - std::move(jsBundleBasePath), - std::move(cxxModules), - std::move(turboModuleRegistry), - nullptr, - std::move(callback), - std::move(jsQueue), - std::move(nativeQueue), - std::move(devSettings), - std::move(devManager))); - - inner->loadBundle(std::move(jsBundleRelativePath)); - inner->RegisterForReloadIfNecessary(); - - return inner; + // init TurboModule + for (const auto &moduleName : turboModuleManager->getEagerInitModuleNames()) { + turboModuleManager->getModule(moduleName); } - void InstanceImpl::SetInError() noexcept { - m_isInError = true; - } + return std::make_unique( + runtimeHolder_->getRuntime(), + std::move(delegate), + JSIExecutor::defaultTimeoutInvoker, + [isProfiling = isProfilingEnabled_]([[maybe_unused]] jsi::Runtime &runtime) { +#ifdef ENABLE_JS_SYSTRACE_TO_ETW + facebook::react::tracing::initializeJSHooks(runtime, isProfiling); +#endif + }); + } - namespace { - bool shouldStartHermesInspector(DevSettings& devSettings) { - bool isHermes = - ((devSettings.jsiEngineOverride == JSIEngineOverride::Hermes) || - (devSettings.jsiEngineOverride == JSIEngineOverride::Default && devSettings.jsiRuntimeHolder && - devSettings.jsiRuntimeHolder->getRuntimeType() == facebook::react::JSIEngineOverride::Hermes)); - - if (isHermes && devSettings.useDirectDebugger && !devSettings.useWebDebugger) - return true; - else - return false; - } - } // namespace - - InstanceImpl::InstanceImpl( - std::shared_ptr&& instance, - std::string&& jsBundleBasePath, - std::vector< - std::tuple>> - && cxxModules, + OJSIExecutorFactory( + std::shared_ptr runtimeHolder, + NativeLoggingHook loggingHook, std::shared_ptr turboModuleRegistry, - std::shared_ptr longLivedObjectCollection, - std::unique_ptr&& callback, - std::shared_ptr jsQueue, - std::shared_ptr nativeQueue, - std::shared_ptr devSettings, - std::shared_ptr devManager) - : m_turboModuleRegistry(std::move(turboModuleRegistry)), + std::shared_ptr longLivedObjectCollection, + bool isProfilingEnabled, + std::shared_ptr jsCallInvoker) noexcept + : runtimeHolder_{std::move(runtimeHolder)}, + loggingHook_{std::move(loggingHook)}, + turboModuleRegistry_{std::move(turboModuleRegistry)}, + longLivedObjectCollection_{std::move(longLivedObjectCollection)}, + jsCallInvoker_{std::move(jsCallInvoker)}, + isProfilingEnabled_{isProfilingEnabled} {} + + private: + std::shared_ptr runtimeHolder_; + std::shared_ptr turboModuleRegistry_; + std::shared_ptr longLivedObjectCollection_; + std::shared_ptr jsCallInvoker_; + NativeLoggingHook loggingHook_; + bool isProfilingEnabled_; +}; + +} // namespace + +void logMarker(const facebook::react::ReactMarker::ReactMarkerId /*id*/, const char * /*tag*/) {} + +/*static*/ std::shared_ptr InstanceImpl::MakeNoBundle( + std::shared_ptr &&instance, + std::string &&jsBundleBasePath, + std::vector< + std::tuple>> + &&cxxModules, + std::shared_ptr turboModuleRegistry, + std::shared_ptr longLivedObjectCollection, + std::unique_ptr &&callback, + std::shared_ptr jsQueue, + std::shared_ptr nativeQueue, + std::shared_ptr devSettings, + std::shared_ptr devManager) noexcept { + auto inner = std::shared_ptr(new InstanceImpl( + std::move(instance), + std::move(jsBundleBasePath), + std::move(cxxModules), + std::move(turboModuleRegistry), + std::move(longLivedObjectCollection), + std::move(callback), + std::move(jsQueue), + std::move(nativeQueue), + std::move(devSettings), + std::move(devManager))); + + inner->RegisterForReloadIfNecessary(); + + return inner; +} + +/*static*/ std::shared_ptr InstanceImpl::MakeAndLoadBundle( + std::shared_ptr &&instance, + std::string &&jsBundleBasePath, + std::string &&jsBundleRelativePath, + std::vector< + std::tuple>> + &&cxxModules, + std::shared_ptr turboModuleRegistry, + std::unique_ptr &&callback, + std::shared_ptr jsQueue, + std::shared_ptr nativeQueue, + std::shared_ptr devSettings, + std::shared_ptr devManager) noexcept { + auto inner = std::shared_ptr(new InstanceImpl( + std::move(instance), + std::move(jsBundleBasePath), + std::move(cxxModules), + std::move(turboModuleRegistry), + nullptr, + std::move(callback), + std::move(jsQueue), + std::move(nativeQueue), + std::move(devSettings), + std::move(devManager))); + + inner->loadBundle(std::move(jsBundleRelativePath)); + inner->RegisterForReloadIfNecessary(); + + return inner; +} + +void InstanceImpl::SetInError() noexcept { + m_isInError = true; +} + +namespace { +bool shouldStartHermesInspector(DevSettings &devSettings) { + bool isHermes = + ((devSettings.jsiEngineOverride == JSIEngineOverride::Hermes) || + (devSettings.jsiEngineOverride == JSIEngineOverride::Default && devSettings.jsiRuntimeHolder && + devSettings.jsiRuntimeHolder->getRuntimeType() == facebook::react::JSIEngineOverride::Hermes)); + + if (isHermes && devSettings.useDirectDebugger && !devSettings.useWebDebugger) + return true; + else + return false; +} +} // namespace + +InstanceImpl::InstanceImpl( + std::shared_ptr &&instance, + std::string &&jsBundleBasePath, + std::vector< + std::tuple>> + &&cxxModules, + std::shared_ptr turboModuleRegistry, + std::shared_ptr longLivedObjectCollection, + std::unique_ptr &&callback, + std::shared_ptr jsQueue, + std::shared_ptr nativeQueue, + std::shared_ptr devSettings, + std::shared_ptr devManager) + : m_turboModuleRegistry(std::move(turboModuleRegistry)), m_longLivedObjectCollection(std::move(longLivedObjectCollection)), m_jsThread(std::move(jsQueue)), m_nativeQueue(nativeQueue), @@ -253,436 +252,427 @@ namespace facebook { m_devSettings(std::move(devSettings)), m_devManager(std::move(devManager)), m_innerInstance(std::move(instance)) { - // Temp set the logmarker here - facebook::react::ReactMarker::logTaggedMarker = logMarker; + // Temp set the logmarker here + facebook::react::ReactMarker::logTaggedMarker = logMarker; #ifdef ENABLE_ETW_TRACING - // TODO :: Find a better place to initialize ETW once per process. - facebook::react::tracing::initializeETW(); + // TODO :: Find a better place to initialize ETW once per process. + facebook::react::tracing::initializeETW(); #endif - if (shouldStartHermesInspector(*m_devSettings)) { - m_devManager->EnsureHermesInspector(m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort); - } - - // Default (common) NativeModules - auto modules = GetDefaultNativeModules(nativeQueue); + if (shouldStartHermesInspector(*m_devSettings)) { + m_devManager->EnsureHermesInspector(m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort); + } - // Add app provided modules. - for (auto& cxxModule : cxxModules) { - modules.push_back(std::make_unique( - m_innerInstance, move(std::get<0>(cxxModule)), move(std::get<1>(cxxModule)), move(std::get<2>(cxxModule)))); - } - m_moduleRegistry = std::make_shared(std::move(modules)); - - // Choose JSExecutor - std::shared_ptr jsef; - if (m_devSettings->useWebDebugger) { - try { - auto jseFunc = m_devManager->LoadJavaScriptInProxyMode(*m_devSettings, [weakthis = weak_from_this()]() { - if (auto strongThis = weakthis.lock()) { - strongThis->SetInError(); - } - }); - - if ((jseFunc == nullptr) || m_isInError) { - m_devSettings->errorCallback("Failed to create JavaScript Executor."); - return; - } + // Default (common) NativeModules + auto modules = GetDefaultNativeModules(nativeQueue); - jsef = std::make_shared(std::move(jseFunc)); - } - catch (std::exception& e) { - m_devSettings->errorCallback(e.what()); - return; + // Add app provided modules. + for (auto &cxxModule : cxxModules) { + modules.push_back(std::make_unique( + m_innerInstance, move(std::get<0>(cxxModule)), move(std::get<1>(cxxModule)), move(std::get<2>(cxxModule)))); + } + m_moduleRegistry = std::make_shared(std::move(modules)); + + // Choose JSExecutor + std::shared_ptr jsef; + if (m_devSettings->useWebDebugger) { + try { + auto jseFunc = m_devManager->LoadJavaScriptInProxyMode(*m_devSettings, [weakthis = weak_from_this()]() { + if (auto strongThis = weakthis.lock()) { + strongThis->SetInError(); } + }); + + if ((jseFunc == nullptr) || m_isInError) { + m_devSettings->errorCallback("Failed to create JavaScript Executor."); + return; } - else { - if (m_devSettings->useFastRefresh || m_devSettings->liveReloadCallback) { - Microsoft::ReactNative::PackagerConnection::CreateOrReusePackagerConnection(*m_devSettings); - } - // If the consumer gives us a JSI runtime, then use it. - if (m_devSettings->jsiRuntimeHolder) { - assert(m_devSettings->jsiEngineOverride == JSIEngineOverride::Default); - jsef = std::make_shared( - m_devSettings->jsiRuntimeHolder, - m_devSettings->loggingCallback, - m_turboModuleRegistry, - m_longLivedObjectCollection, - !m_devSettings->useFastRefresh, - m_innerInstance->getJSCallInvoker()); - } - else { - assert(m_devSettings->jsiEngineOverride != JSIEngineOverride::Default); - switch (m_devSettings->jsiEngineOverride) { - case JSIEngineOverride::Hermes: - m_devSettings->jsiRuntimeHolder = std::make_shared(m_devSettings, m_jsThread); - break; - case JSIEngineOverride::V8: { + jsef = std::make_shared(std::move(jseFunc)); + } catch (std::exception &e) { + m_devSettings->errorCallback(e.what()); + return; + } + } else { + if (m_devSettings->useFastRefresh || m_devSettings->liveReloadCallback) { + Microsoft::ReactNative::PackagerConnection::CreateOrReusePackagerConnection(*m_devSettings); + } + + // If the consumer gives us a JSI runtime, then use it. + if (m_devSettings->jsiRuntimeHolder) { + assert(m_devSettings->jsiEngineOverride == JSIEngineOverride::Default); + jsef = std::make_shared( + m_devSettings->jsiRuntimeHolder, + m_devSettings->loggingCallback, + m_turboModuleRegistry, + m_longLivedObjectCollection, + !m_devSettings->useFastRefresh, + m_innerInstance->getJSCallInvoker()); + } else { + assert(m_devSettings->jsiEngineOverride != JSIEngineOverride::Default); + switch (m_devSettings->jsiEngineOverride) { + case JSIEngineOverride::Hermes: + m_devSettings->jsiRuntimeHolder = std::make_shared(m_devSettings, m_jsThread); + break; + case JSIEngineOverride::V8: { #if defined(USE_V8) - std::unique_ptr scriptStore = nullptr; - std::unique_ptr preparedScriptStore = nullptr; + std::unique_ptr scriptStore = nullptr; + std::unique_ptr preparedScriptStore = nullptr; - char tempPath[MAX_PATH]; - if (GetTempPathA(MAX_PATH, tempPath)) { - preparedScriptStore = std::make_unique(tempPath); - } + char tempPath[MAX_PATH]; + if (GetTempPathA(MAX_PATH, tempPath)) { + preparedScriptStore = std::make_unique(tempPath); + } - m_devSettings->jsiRuntimeHolder = std::make_shared( + m_devSettings->jsiRuntimeHolder = std::make_shared( m_devSettings, m_jsThread, std::move(scriptStore), std::move(preparedScriptStore)); - break; + break; #else - assert(false); // V8 is not available in this build, fallthrough - [[fallthrough]]; + assert(false); // V8 is not available in this build, fallthrough + [[fallthrough]]; #endif - } - case JSIEngineOverride::V8NodeApi: { + } + case JSIEngineOverride::V8NodeApi: { #if defined(USE_V8) - std::unique_ptr preparedScriptStore; + std::unique_ptr preparedScriptStore; - wchar_t tempPath[MAX_PATH]; - if (GetTempPathW(static_cast(std::size(tempPath)), tempPath)) { - preparedScriptStore = + wchar_t tempPath[MAX_PATH]; + if (GetTempPathW(static_cast(std::size(tempPath)), tempPath)) { + preparedScriptStore = std::make_unique(winrt::to_string(tempPath)); - } + } - if (!preparedScriptStore) { - if (m_devSettings->errorCallback) - m_devSettings->errorCallback("Could not initialize prepared script store"); + if (!preparedScriptStore) { + if (m_devSettings->errorCallback) + m_devSettings->errorCallback("Could not initialize prepared script store"); - break; - } + break; + } - m_devSettings->jsiRuntimeHolder = make_shared( + m_devSettings->jsiRuntimeHolder = make_shared( m_devSettings, m_jsThread, nullptr /*scriptStore*/, std::move(preparedScriptStore)); - break; + break; #else - if (m_devSettings->errorCallback) - m_devSettings->errorCallback("JSI/V8/NAPI engine is not available in this build"); - assert(false); - [[fallthrough]]; + if (m_devSettings->errorCallback) + m_devSettings->errorCallback("JSI/V8/NAPI engine is not available in this build"); + assert(false); + [[fallthrough]]; #endif - } - case JSIEngineOverride::Chakra: - case JSIEngineOverride::ChakraCore: - default: // TODO: Add other engines once supported - m_devSettings->jsiRuntimeHolder = - std::make_shared(m_devSettings, m_jsThread, nullptr, nullptr); - break; - } - jsef = std::make_shared( - m_devSettings->jsiRuntimeHolder, - m_devSettings->loggingCallback, - m_turboModuleRegistry, - m_longLivedObjectCollection, - !m_devSettings->useFastRefresh, - m_innerInstance->getJSCallInvoker()); - } - } - - m_innerInstance->initializeBridge(std::move(callback), jsef, m_jsThread, m_moduleRegistry); - - // All JSI runtimes do support host objects and hence the native modules - // proxy. - const bool isNativeModulesProxyAvailable = ((m_devSettings->jsiRuntimeHolder != nullptr) || - (m_devSettings->jsiEngineOverride != JSIEngineOverride::Default)) && - !m_devSettings->useWebDebugger; - if (!isNativeModulesProxyAvailable) { - folly::dynamic configArray = folly::dynamic::array; - for (auto const& moduleName : m_moduleRegistry->moduleNames()) { - auto moduleConfig = m_moduleRegistry->getConfig(moduleName); - configArray.push_back(moduleConfig ? std::move(moduleConfig->config) : nullptr); } - - folly::dynamic configs = folly::dynamic::object("remoteModuleConfig", std::move(configArray)); - m_innerInstance->setGlobalVariable( - "__fbBatchedBridgeConfig", std::make_unique(folly::toJson(configs))); + case JSIEngineOverride::Chakra: + case JSIEngineOverride::ChakraCore: + default: // TODO: Add other engines once supported + m_devSettings->jsiRuntimeHolder = + std::make_shared(m_devSettings, m_jsThread, nullptr, nullptr); + break; } + jsef = std::make_shared( + m_devSettings->jsiRuntimeHolder, + m_devSettings->loggingCallback, + m_turboModuleRegistry, + m_longLivedObjectCollection, + !m_devSettings->useFastRefresh, + m_innerInstance->getJSCallInvoker()); } + } - void InstanceImpl::loadBundle(std::string&& jsBundleRelativePath) { - loadBundleInternal(std::move(jsBundleRelativePath), /*synchronously:*/ false); - } - - void InstanceImpl::loadBundleSync(std::string&& jsBundleRelativePath) { - loadBundleInternal(std::move(jsBundleRelativePath), /*synchronously:*/ true); + m_innerInstance->initializeBridge(std::move(callback), jsef, m_jsThread, m_moduleRegistry); + + // All JSI runtimes do support host objects and hence the native modules + // proxy. + const bool isNativeModulesProxyAvailable = ((m_devSettings->jsiRuntimeHolder != nullptr) || + (m_devSettings->jsiEngineOverride != JSIEngineOverride::Default)) && + !m_devSettings->useWebDebugger; + if (!isNativeModulesProxyAvailable) { + folly::dynamic configArray = folly::dynamic::array; + for (auto const &moduleName : m_moduleRegistry->moduleNames()) { + auto moduleConfig = m_moduleRegistry->getConfig(moduleName); + configArray.push_back(moduleConfig ? std::move(moduleConfig->config) : nullptr); } - // Note: Based on - // https://github.com/facebook/react-native/blob/24d91268b64c7abbd4b26547ffcc663dc90ec5e7/ReactCommon/cxxreact/Instance.cpp#L112 - bool isHBCBundle(const std::string& bundle) { - static uint32_t constexpr HBCBundleMagicNumber = 0xffe7c3c3; - - // Note:: Directly access the pointer to avoid copy/length-check. It matters as this string contains the bundle which - // can be potentially huge. - // https://herbsutter.com/2008/04/07/cringe-not-vectors-are-guaranteed-to-be-contiguous/#comment-483 - auto header = reinterpret_cast(&bundle[0]); - if (HBCBundleMagicNumber == header->magic32.value) { - return true; - } - else { - return false; - } - } + folly::dynamic configs = folly::dynamic::object("remoteModuleConfig", std::move(configArray)); + m_innerInstance->setGlobalVariable( + "__fbBatchedBridgeConfig", std::make_unique(folly::toJson(configs))); + } +} + +void InstanceImpl::loadBundle(std::string &&jsBundleRelativePath) { + loadBundleInternal(std::move(jsBundleRelativePath), /*synchronously:*/ false); +} + +void InstanceImpl::loadBundleSync(std::string &&jsBundleRelativePath) { + loadBundleInternal(std::move(jsBundleRelativePath), /*synchronously:*/ true); +} + +// Note: Based on +// https://github.com/facebook/react-native/blob/24d91268b64c7abbd4b26547ffcc663dc90ec5e7/ReactCommon/cxxreact/Instance.cpp#L112 +bool isHBCBundle(const std::string &bundle) { + static uint32_t constexpr HBCBundleMagicNumber = 0xffe7c3c3; + + // Note:: Directly access the pointer to avoid copy/length-check. It matters as this string contains the bundle which + // can be potentially huge. + // https://herbsutter.com/2008/04/07/cringe-not-vectors-are-guaranteed-to-be-contiguous/#comment-483 + auto header = reinterpret_cast(&bundle[0]); + if (HBCBundleMagicNumber == header->magic32.value) { + return true; + } else { + return false; + } +} - void InstanceImpl::loadBundleInternal(std::string&& jsBundleRelativePath, bool synchronously) { - try { - if (m_devSettings->useWebDebugger || m_devSettings->liveReloadCallback != nullptr || - m_devSettings->useFastRefresh) { - // First attempt to get download the Js locally, to catch any bundling - // errors before attempting to load the actual script. +void InstanceImpl::loadBundleInternal(std::string &&jsBundleRelativePath, bool synchronously) { + try { + if (m_devSettings->useWebDebugger || m_devSettings->liveReloadCallback != nullptr || + m_devSettings->useFastRefresh) { + // First attempt to get download the Js locally, to catch any bundling + // errors before attempting to load the actual script. - uint32_t hermesBytecodeVersion = 0; + uint32_t hermesBytecodeVersion = 0; #if defined(USE_HERMES) && defined(ENABLE_DEVSERVER_HBCBUNDLES) - hermesBytecodeVersion = ::hermes::hbc::BYTECODE_VERSION; + hermesBytecodeVersion = ::hermes::hbc::BYTECODE_VERSION; #endif - auto [jsBundleString, success] = Microsoft::ReactNative::GetJavaScriptFromServer( - m_devSettings->sourceBundleHost, - m_devSettings->sourceBundlePort, - m_devSettings->debugBundlePath.empty() ? jsBundleRelativePath : m_devSettings->debugBundlePath, - m_devSettings->platformName, - true /* dev */, - m_devSettings->useFastRefresh, - m_devSettings->inlineSourceMap, - hermesBytecodeVersion); + auto [jsBundleString, success] = Microsoft::ReactNative::GetJavaScriptFromServer( + m_devSettings->sourceBundleHost, + m_devSettings->sourceBundlePort, + m_devSettings->debugBundlePath.empty() ? jsBundleRelativePath : m_devSettings->debugBundlePath, + m_devSettings->platformName, + true /* dev */, + m_devSettings->useFastRefresh, + m_devSettings->inlineSourceMap, + hermesBytecodeVersion); - if (!success) { - m_devManager->UpdateBundleStatus(false, -1); - m_devSettings->errorCallback(jsBundleString); - return; - } + if (!success) { + m_devManager->UpdateBundleStatus(false, -1); + m_devSettings->errorCallback(jsBundleString); + return; + } - int64_t currentTimeInMilliSeconds = - std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) - .count(); - m_devManager->UpdateBundleStatus(true, currentTimeInMilliSeconds); + int64_t currentTimeInMilliSeconds = + std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) + .count(); + m_devManager->UpdateBundleStatus(true, currentTimeInMilliSeconds); - auto bundleUrl = DevServerHelper::get_BundleUrl( - m_devSettings->sourceBundleHost, - m_devSettings->sourceBundlePort, - m_devSettings->debugBundlePath.empty() ? jsBundleRelativePath : m_devSettings->debugBundlePath, - m_devSettings->platformName, - /*dev*/ true, - /*hot*/ false, - m_devSettings->inlineSourceMap, - hermesBytecodeVersion); - - // This code is based on the HBC Bundle integration on Android - // Ref: - // https://github.com/facebook/react-native/blob/24d91268b64c7abbd4b26547ffcc663dc90ec5e7/ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.cpp#L231 - if (isHBCBundle(jsBundleString)) { - auto script = std::make_unique(jsBundleString, false); - const char* buffer = script->c_str(); - uint32_t bufferLength = (uint32_t)script->size(); - - // Please refer the code here for details on the file format: - // https://github.com/facebook/metro/blob/b1bacf52070be62872d6bd3420f37a4405ed34e6/packages/metro/src/lib/bundleToBytecode.js#L29 - // Essentially, there is an 8 byte long file header with 4 bytes of a magic number followed by 4 bytes to encode - // the number of modules.The module buffers follows, each one starts with 4 byte header which encodes module - // length.A properly formatted HBCB should have at least 8 bytes.. - uint32_t offset = 8; + auto bundleUrl = DevServerHelper::get_BundleUrl( + m_devSettings->sourceBundleHost, + m_devSettings->sourceBundlePort, + m_devSettings->debugBundlePath.empty() ? jsBundleRelativePath : m_devSettings->debugBundlePath, + m_devSettings->platformName, + /*dev*/ true, + /*hot*/ false, + m_devSettings->inlineSourceMap, + hermesBytecodeVersion); + + // This code is based on the HBC Bundle integration on Android + // Ref: + // https://github.com/facebook/react-native/blob/24d91268b64c7abbd4b26547ffcc663dc90ec5e7/ReactAndroid/src/main/jni/react/jni/CatalystInstanceImpl.cpp#L231 + if (isHBCBundle(jsBundleString)) { + auto script = std::make_unique(jsBundleString, false); + const char *buffer = script->c_str(); + uint32_t bufferLength = (uint32_t)script->size(); + + // Please refer the code here for details on the file format: + // https://github.com/facebook/metro/blob/b1bacf52070be62872d6bd3420f37a4405ed34e6/packages/metro/src/lib/bundleToBytecode.js#L29 + // Essentially, there is an 8 byte long file header with 4 bytes of a magic number followed by 4 bytes to encode + // the number of modules.The module buffers follows, each one starts with 4 byte header which encodes module + // length.A properly formatted HBCB should have at least 8 bytes.. + uint32_t offset = 8; #define __SAFEADD__(s1, s2, t) \ if (!msl::utilities::SafeAdd(s1, s2, t)) \ break; - while (offset < bufferLength) { - uint32_t segment; - __SAFEADD__(offset, 4, segment) - uint32_t moduleLength = (bufferLength < segment) ? 0 : *(((uint32_t*)buffer) + offset / 4); - - // Early break if the module length is computed as 0.. as the segment start may be overflowing the buffer. - if (moduleLength == 0) - break; - - uint32_t segmentEnd; - __SAFEADD__(moduleLength, segment, segmentEnd) - // Early break if the segment overflows beyond the buffer. This is unlikely for a properly formatted - // HBCB though. - if (segmentEnd > bufferLength) - break; - - m_innerInstance->loadScriptFromString( - std::make_unique(std::string(buffer + segment, buffer + segmentEnd)), - bundleUrl, - false); - - // Aligned at 4 byte boundary. - offset += ((moduleLength + 3) & ~3) + 4; - } -#undef __SAFEADD__ - } - else { - // Remote debug executor loads script from a Uri, rather than taking the actual bundle string - m_innerInstance->loadScriptFromString( - std::make_unique(m_devSettings->useWebDebugger ? bundleUrl : jsBundleString), + while (offset < bufferLength) { + uint32_t segment; + __SAFEADD__(offset, 4, segment) + uint32_t moduleLength = (bufferLength < segment) ? 0 : *(((uint32_t *)buffer) + offset / 4); + + // Early break if the module length is computed as 0.. as the segment start may be overflowing the buffer. + if (moduleLength == 0) + break; + + uint32_t segmentEnd; + __SAFEADD__(moduleLength, segment, segmentEnd) + // Early break if the segment overflows beyond the buffer. This is unlikely for a properly formatted + // HBCB though. + if (segmentEnd > bufferLength) + break; + + m_innerInstance->loadScriptFromString( + std::make_unique(std::string(buffer + segment, buffer + segmentEnd)), bundleUrl, - synchronously); - } - } - else { -#if (defined(_MSC_VER) && !defined(WINRT)) - std::string bundlePath = (fs::path(m_devSettings->bundleRootPath) / jsBundleRelativePath).string(); - auto bundleString = FileMappingBigString::fromPath(bundlePath); -#else - std::string bundlePath; - if (m_devSettings->bundleRootPath._Starts_with("resource://")) { - auto uri = winrt::Windows::Foundation::Uri( - winrt::to_hstring(m_devSettings->bundleRootPath), winrt::to_hstring(jsBundleRelativePath)); - bundlePath = winrt::to_string(uri.ToString()); - } - else { - bundlePath = (fs::path(m_devSettings->bundleRootPath) / (jsBundleRelativePath + ".bundle")).string(); - } + false); - auto bundleString = std::make_unique<::Microsoft::ReactNative::StorageFileBigString>(bundlePath); -#endif - m_innerInstance->loadScriptFromString(std::move(bundleString), std::move(jsBundleRelativePath), synchronously); + // Aligned at 4 byte boundary. + offset += ((moduleLength + 3) & ~3) + 4; } +#undef __SAFEADD__ + } else { + // Remote debug executor loads script from a Uri, rather than taking the actual bundle string + m_innerInstance->loadScriptFromString( + std::make_unique(m_devSettings->useWebDebugger ? bundleUrl : jsBundleString), + bundleUrl, + synchronously); } - catch (const std::exception& e) { - m_devSettings->errorCallback(e.what()); + } else { +#if (defined(_MSC_VER) && !defined(WINRT)) + std::string bundlePath = (fs::path(m_devSettings->bundleRootPath) / jsBundleRelativePath).string(); + auto bundleString = FileMappingBigString::fromPath(bundlePath); +#else + std::string bundlePath; + if (m_devSettings->bundleRootPath._Starts_with("resource://")) { + auto uri = winrt::Windows::Foundation::Uri( + winrt::to_hstring(m_devSettings->bundleRootPath), winrt::to_hstring(jsBundleRelativePath)); + bundlePath = winrt::to_string(uri.ToString()); + } else { + bundlePath = (fs::path(m_devSettings->bundleRootPath) / (jsBundleRelativePath + ".bundle")).string(); } - catch (const winrt::hresult_error& hrerr) { - auto error = fmt::format("[0x{:0>8x}] {}", static_cast(hrerr.code()), winrt::to_string(hrerr.message())); - m_devSettings->errorCallback(std::move(error)); - } + auto bundleString = std::make_unique<::Microsoft::ReactNative::StorageFileBigString>(bundlePath); +#endif + m_innerInstance->loadScriptFromString(std::move(bundleString), std::move(jsBundleRelativePath), synchronously); } + } catch (const std::exception &e) { + m_devSettings->errorCallback(e.what()); + } catch (const winrt::hresult_error &hrerr) { + auto error = fmt::format("[0x{:0>8x}] {}", static_cast(hrerr.code()), winrt::to_string(hrerr.message())); - InstanceImpl::~InstanceImpl() { - if (shouldStartHermesInspector(*m_devSettings) && m_devSettings->jsiRuntimeHolder) { - m_devSettings->jsiRuntimeHolder->teardown(); - } - m_nativeQueue->quitSynchronous(); - } + m_devSettings->errorCallback(std::move(error)); + } +} + +InstanceImpl::~InstanceImpl() { + if (shouldStartHermesInspector(*m_devSettings) && m_devSettings->jsiRuntimeHolder) { + m_devSettings->jsiRuntimeHolder->teardown(); + } + m_nativeQueue->quitSynchronous(); +} - std::vector> InstanceImpl::GetDefaultNativeModules( - std::shared_ptr nativeQueue) { - std::vector> modules; - auto transitionalProps{ ReactPropertyBagHelper::CreatePropertyBag() }; +std::vector> InstanceImpl::GetDefaultNativeModules( + std::shared_ptr nativeQueue) { + std::vector> modules; + auto transitionalProps{ReactPropertyBagHelper::CreatePropertyBag()}; #if (defined(_MSC_VER) && !defined(WINRT)) - modules.push_back(std::make_unique( - m_innerInstance, - Microsoft::React::GetHttpModuleName(), - [nativeQueue, transitionalProps]() -> std::unique_ptr { - return Microsoft::React::CreateHttpModule(transitionalProps); - }, - nativeQueue)); + modules.push_back(std::make_unique( + m_innerInstance, + Microsoft::React::GetHttpModuleName(), + [nativeQueue, transitionalProps]() -> std::unique_ptr { + return Microsoft::React::CreateHttpModule(transitionalProps); + }, + nativeQueue)); #endif - modules.push_back(std::make_unique( - m_innerInstance, - Microsoft::React::GetWebSocketModuleName(), - [nativeQueue, transitionalProps]() -> std::unique_ptr { - return Microsoft::React::CreateWebSocketModule(transitionalProps); - }, - nativeQueue)); - - // TODO: This is not included for UWP because we have a different module which - // is added later. However, this one is designed - // so that we can base a UWP version on it. We need to do that but is not high - // priority. + modules.push_back(std::make_unique( + m_innerInstance, + Microsoft::React::GetWebSocketModuleName(), + [nativeQueue, transitionalProps]() -> std::unique_ptr { + return Microsoft::React::CreateWebSocketModule(transitionalProps); + }, + nativeQueue)); + + // TODO: This is not included for UWP because we have a different module which + // is added later. However, this one is designed + // so that we can base a UWP version on it. We need to do that but is not high + // priority. #if (defined(_MSC_VER) && !defined(WINRT)) - modules.push_back(std::make_unique( - m_innerInstance, - "Timing", - [nativeQueue]() -> std::unique_ptr { return react::CreateTimingModule(nativeQueue); }, - nativeQueue)); + modules.push_back(std::make_unique( + m_innerInstance, + "Timing", + [nativeQueue]() -> std::unique_ptr { return react::CreateTimingModule(nativeQueue); }, + nativeQueue)); #endif - uint32_t hermesBytecodeVersion = 0; + uint32_t hermesBytecodeVersion = 0; #if defined(USE_HERMES) && defined(ENABLE_DEVSERVER_HBCBUNDLES) - hermesBytecodeVersion = ::hermes::hbc::BYTECODE_VERSION; + hermesBytecodeVersion = ::hermes::hbc::BYTECODE_VERSION; #endif - // TODO - Encapsulate this in a helpers, and make sure callers add it to their - // list - std::string bundleUrl = (m_devSettings->useWebDebugger || m_devSettings->liveReloadCallback) - ? DevServerHelper::get_BundleUrl( - m_devSettings->sourceBundleHost, - m_devSettings->sourceBundlePort, - m_devSettings->debugBundlePath, - m_devSettings->platformName, - true /*dev*/, - m_devSettings->useFastRefresh, - m_devSettings->inlineSourceMap, - hermesBytecodeVersion) - : std::string(); - modules.push_back(std::make_unique( - m_innerInstance, - facebook::react::SourceCodeModule::Name, - [bundleUrl]() -> std::unique_ptr { - return std::make_unique(bundleUrl); - }, - nativeQueue)); - - modules.push_back(std::make_unique( - m_innerInstance, - "ExceptionsManager", - [redboxHandler = m_devSettings->redboxHandler]() mutable { - return std::make_unique(redboxHandler); - }, - nativeQueue)); - - modules.push_back(std::make_unique( + // TODO - Encapsulate this in a helpers, and make sure callers add it to their + // list + std::string bundleUrl = (m_devSettings->useWebDebugger || m_devSettings->liveReloadCallback) + ? DevServerHelper::get_BundleUrl( + m_devSettings->sourceBundleHost, + m_devSettings->sourceBundlePort, + m_devSettings->debugBundlePath, + m_devSettings->platformName, + true /*dev*/, + m_devSettings->useFastRefresh, + m_devSettings->inlineSourceMap, + hermesBytecodeVersion) + : std::string(); + modules.push_back(std::make_unique( + m_innerInstance, + facebook::react::SourceCodeModule::Name, + [bundleUrl]() -> std::unique_ptr { + return std::make_unique(bundleUrl); + }, + nativeQueue)); + + modules.push_back(std::make_unique( + m_innerInstance, + "ExceptionsManager", + [redboxHandler = m_devSettings->redboxHandler]() mutable { + return std::make_unique(redboxHandler); + }, + nativeQueue)); + + modules.push_back(std::make_unique( + m_innerInstance, + PlatformConstantsModule::Name, + []() { return std::make_unique(); }, + nativeQueue)); + + modules.push_back(std::make_unique( + m_innerInstance, + StatusBarManagerModule::Name, + []() { return std::make_unique(); }, + nativeQueue)); + + // 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")) { + modules.push_back(std::make_unique( m_innerInstance, - PlatformConstantsModule::Name, - []() { return std::make_unique(); }, + Microsoft::React::GetBlobModuleName(), + [transitionalProps]() { return Microsoft::React::CreateBlobModule(transitionalProps); }, nativeQueue)); - modules.push_back(std::make_unique( + modules.push_back(std::make_unique( m_innerInstance, - StatusBarManagerModule::Name, - []() { return std::make_unique(); }, + Microsoft::React::GetFileReaderModuleName(), + [transitionalProps]() { return Microsoft::React::CreateFileReaderModule(transitionalProps); }, nativeQueue)); - // 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")) { - modules.push_back(std::make_unique( - m_innerInstance, - Microsoft::React::GetBlobModuleName(), - [transitionalProps]() { return Microsoft::React::CreateBlobModule(transitionalProps); }, - nativeQueue)); - - modules.push_back(std::make_unique( - m_innerInstance, - Microsoft::React::GetFileReaderModuleName(), - [transitionalProps]() { return Microsoft::React::CreateFileReaderModule(transitionalProps); }, - nativeQueue)); - - return modules; - } + return modules; + } - void InstanceImpl::RegisterForReloadIfNecessary() noexcept { - // setup polling for live reload - if (!m_isInError && !m_devSettings->useFastRefresh && m_devSettings->liveReloadCallback != nullptr) { - m_devManager->StartPollingLiveReload( - m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort, m_devSettings->liveReloadCallback); - } - } + void InstanceImpl::RegisterForReloadIfNecessary() noexcept { + // setup polling for live reload + if (!m_isInError && !m_devSettings->useFastRefresh && m_devSettings->liveReloadCallback != nullptr) { + m_devManager->StartPollingLiveReload( + m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort, m_devSettings->liveReloadCallback); + } + } - void InstanceImpl::DispatchEvent(int64_t viewTag, std::string eventName, folly::dynamic && eventData) { - if (m_isInError) { - return; - } + void InstanceImpl::DispatchEvent(int64_t viewTag, std::string eventName, folly::dynamic && eventData) { + if (m_isInError) { + return; + } - folly::dynamic params = folly::dynamic::array(viewTag, eventName, std::move(eventData)); - m_innerInstance->callJSFunction("RCTEventEmitter", "receiveEvent", std::move(params)); - } + folly::dynamic params = folly::dynamic::array(viewTag, eventName, std::move(eventData)); + m_innerInstance->callJSFunction("RCTEventEmitter", "receiveEvent", std::move(params)); + } - void InstanceImpl::invokeCallback(const int64_t callbackId, folly::dynamic && params) { - if (m_isInError) { - return; - } + void InstanceImpl::invokeCallback(const int64_t callbackId, folly::dynamic &¶ms) { + if (m_isInError) { + return; + } - m_innerInstance->callJSCallback(callbackId, std::move(params)); - } + m_innerInstance->callJSCallback(callbackId, std::move(params)); + } - } // namespace react - } // namespace facebook +} // namespace react +} // namespace facebook From 2d71387154423416d447a512ec4179d3bc6f6c3c Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Wed, 7 Dec 2022 02:58:20 -0800 Subject: [PATCH 34/45] Remove Monolithic module usage --- vnext/Shared/OInstance.cpp | 43 ++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/vnext/Shared/OInstance.cpp b/vnext/Shared/OInstance.cpp index 3f1e996f8af..ebb071f29a5 100644 --- a/vnext/Shared/OInstance.cpp +++ b/vnext/Shared/OInstance.cpp @@ -27,6 +27,7 @@ #include #include +#include #include #include #include @@ -645,34 +646,36 @@ std::vector> InstanceImpl::GetDefaultNativeModules Microsoft::React::GetFileReaderModuleName(), [transitionalProps]() { return Microsoft::React::CreateFileReaderModule(transitionalProps); }, nativeQueue)); - - return modules; } +#endif - void InstanceImpl::RegisterForReloadIfNecessary() noexcept { - // setup polling for live reload - if (!m_isInError && !m_devSettings->useFastRefresh && m_devSettings->liveReloadCallback != nullptr) { - m_devManager->StartPollingLiveReload( - m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort, m_devSettings->liveReloadCallback); - } - } + return modules; +} - void InstanceImpl::DispatchEvent(int64_t viewTag, std::string eventName, folly::dynamic && eventData) { - if (m_isInError) { - return; - } +void InstanceImpl::RegisterForReloadIfNecessary() noexcept { + // setup polling for live reload + if (!m_isInError && !m_devSettings->useFastRefresh && m_devSettings->liveReloadCallback != nullptr) { + m_devManager->StartPollingLiveReload( + m_devSettings->sourceBundleHost, m_devSettings->sourceBundlePort, m_devSettings->liveReloadCallback); + } +} - folly::dynamic params = folly::dynamic::array(viewTag, eventName, std::move(eventData)); - m_innerInstance->callJSFunction("RCTEventEmitter", "receiveEvent", std::move(params)); +void InstanceImpl::DispatchEvent(int64_t viewTag, std::string eventName, folly::dynamic &&eventData) { + if (m_isInError) { + return; } - void InstanceImpl::invokeCallback(const int64_t callbackId, folly::dynamic &¶ms) { - if (m_isInError) { - return; - } + folly::dynamic params = folly::dynamic::array(viewTag, eventName, std::move(eventData)); + m_innerInstance->callJSFunction("RCTEventEmitter", "receiveEvent", std::move(params)); +} - m_innerInstance->callJSCallback(callbackId, std::move(params)); +void InstanceImpl::invokeCallback(const int64_t callbackId, folly::dynamic &¶ms) { + if (m_isInError) { + return; } + m_innerInstance->callJSCallback(callbackId, std::move(params)); +} + } // namespace react } // namespace facebook From 598326eb3a7ed44147b5b50ba33d0de023d1dd33 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Wed, 7 Dec 2022 03:03:46 -0800 Subject: [PATCH 35/45] Remove Monolithic module usage --- vnext/Shared/OInstance.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/vnext/Shared/OInstance.cpp b/vnext/Shared/OInstance.cpp index ebb071f29a5..ecf7874d0ca 100644 --- a/vnext/Shared/OInstance.cpp +++ b/vnext/Shared/OInstance.cpp @@ -27,7 +27,6 @@ #include #include -#include #include #include #include From 51897b4e4d2f30ce199820d40d5c83aa530e86e8 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Wed, 7 Dec 2022 03:15:56 -0800 Subject: [PATCH 36/45] Add pre/post build compliance templates and address PoliCheck issues (#10468) This PR refactors some of our compliance-related tasks such as CredScan, PoliCheck, and Component Governance into two new templates: `run-compliance-prebuild.yml` and `run-compliance-postbuild.yml`. The pre-build tasks will run before CI, PR, Publish, and Compliance pipelines. Task failures will cause the CI, PR, and Publish pipelines to fail appropriately. The Compliance pipeline will convert errors into warnings (so that all of the tasks still run). In addition, this PR address existing PoliCheck issues (so that the PR passes). - Bug fix (non-breaking change which fixes an issue) - New feature (non-breaking change which adds functionality) Make sure we're running compliance tasks correctly, consistently, and address existing violations. Closes #10459 Refactors some tasks into new templates and fixes comments in some files. N/A For the new pipeline templates, running successfully in this PR. For the PoliCheck they're all just in comments, so if the PR checks pass, they've been resolved. --- .../Networking/OriginPolicyHttpFilter.cpp | 1310 ++++++++++------- 1 file changed, 777 insertions(+), 533 deletions(-) diff --git a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp index 944d4e7dd09..5360f0f252f 100644 --- a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp +++ b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp @@ -39,20 +39,20 @@ namespace Microsoft::React::Networking { #pragma region ConstWcharComparer -bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, const wchar_t *b) const { - return _wcsicmp(a, b) < 0; -} + bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t* a, const wchar_t* b) const { + return _wcsicmp(a, b) < 0; + } #pragma endregion ConstWcharComparer -// https://fetch.spec.whatwg.org/#forbidden-method -/*static*/ set OriginPolicyHttpFilter::s_forbiddenMethods = - {L"CONNECT", L"TRACE", L"TRACK"}; + // https://fetch.spec.whatwg.org/#forbidden-method + /*static*/ set OriginPolicyHttpFilter::s_forbiddenMethods = + { L"CONNECT", L"TRACE", L"TRACK" }; -/*static*/ set - OriginPolicyHttpFilter::s_simpleCorsMethods = {L"GET", L"HEAD", L"POST"}; + /*static*/ set + OriginPolicyHttpFilter::s_simpleCorsMethods = { L"GET", L"HEAD", L"POST" }; -/*static*/ set + /*static*/ set OriginPolicyHttpFilter::s_simpleCorsRequestHeaderNames = { L"Accept", L"Accept-Language", @@ -62,21 +62,21 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co L"Downlink", L"Save-Data", L"Viewport-Width", - L"Width"}; + L"Width" }; -/*static*/ set + /*static*/ set OriginPolicyHttpFilter::s_simpleCorsResponseHeaderNames = - {L"Cache-Control", L"Content-Language", L"Content-Type", L"Expires", L"Last-Modified", L"Pragma"}; + { L"Cache-Control", L"Content-Language", L"Content-Type", L"Expires", L"Last-Modified", L"Pragma" }; -/*static*/ set + /*static*/ set OriginPolicyHttpFilter::s_simpleCorsContentTypeValues = { L"application/x-www-form-urlencoded", L"multipart/form-data", - L"text/plain"}; + L"text/plain" }; -// https://fetch.spec.whatwg.org/#forbidden-header-name -// Chromium still bans "User-Agent" due to https://crbug.com/571722 -/*static*/ set + // https://fetch.spec.whatwg.org/#forbidden-header-name + // Chromium still bans "User-Agent" due to https://crbug.com/571722 + /*static*/ set OriginPolicyHttpFilter::s_corsForbiddenRequestHeaderNames = { L"Accept-Charset", L"Accept-Encoding", @@ -97,52 +97,35 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co L"Trailer", L"Transfer-Encoding", L"Upgrade", - L"Via"}; + L"Via" }; -/*static*/ set + /*static*/ set OriginPolicyHttpFilter::s_cookieSettingResponseHeaders = { L"Set-Cookie", L"Set-Cookie2", // Deprecated by the spec, but probably still used -}; - -/*static*/ set - OriginPolicyHttpFilter::s_corsForbiddenRequestHeaderNamePrefixes = {L"Proxy-", L"Sec-"}; - -/*static*/ Uri OriginPolicyHttpFilter::s_origin{nullptr}; - -/*static*/ void OriginPolicyHttpFilter::SetStaticOrigin(std::string &&url) { - if (!url.empty()) - s_origin = Uri{to_hstring(url)}; -} + }; -/*static*/ bool OriginPolicyHttpFilter::IsSameOrigin(Uri const &u1, Uri const &u2) noexcept { - return (u1 && u2) && u1.SchemeName() == u2.SchemeName() && u1.Host() == u2.Host() && u1.Port() == u2.Port(); -} + /*static*/ set + OriginPolicyHttpFilter::s_corsForbiddenRequestHeaderNamePrefixes = { L"Proxy-", L"Sec-" }; -/*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const &request) noexcept { - // Ensure header is in Simple CORS allowlist - for (const auto &header : request.Headers()) { - if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend()) - return false; + /*static*/ Uri OriginPolicyHttpFilter::s_origin{ nullptr }; - // Ensure Content-Type value is in Simple CORS allowlist, if present - if (boost::iequals(header.Key(), L"Content-Type")) { - if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) != s_simpleCorsContentTypeValues.cend()) - return false; - } + /*static*/ void OriginPolicyHttpFilter::SetStaticOrigin(std::string&& url) { + if (!url.empty()) + s_origin = Uri{ to_hstring(url) }; } - /*static*/ bool OriginPolicyHttpFilter::IsSameOrigin(Uri const &u1, Uri const &u2) noexcept { + /*static*/ bool OriginPolicyHttpFilter::IsSameOrigin(Uri const& u1, Uri const& u2) noexcept { return (u1 && u2) && u1.SchemeName() == u2.SchemeName() && u1.Host() == u2.Host() && u1.Port() == u2.Port(); } - /*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const &request) noexcept { - // Ensure header is in Simple CORS white list - for (const auto &header : request.Headers()) { + /*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const& request) noexcept { + // Ensure header is in Simple CORS allowlist + for (const auto& header : request.Headers()) { if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend()) return false; - // Ensure Content-Type value is in Simple CORS white list, if present + // Ensure Content-Type value is in Simple CORS allowlist, if present if (boost::iequals(header.Key(), L"Content-Type")) { if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) != s_simpleCorsContentTypeValues.cend()) return false; @@ -151,55 +134,55 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co // WinRT separates request headers from request content headers if (auto content = request.Content()) { - for (const auto &header : content.Headers()) { - // WinRT automatically appends non-whitelisted header Content-Length when Content-Type is set. Skip it. + for (const auto& header : content.Headers()) { + // WinRT automatically appends non-allowlisted header Content-Length when Content-Type is set. Skip it. if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend() && - !boost::iequals(header.Key(), "Content-Length")) + !boost::iequals(header.Key(), "Content-Length")) return false; - // Ensure Content-Type value is in Simple CORS white list, if present + // Ensure Content-Type value is in Simple CORS allowlist, if present if (boost::iequals(header.Key(), L"Content-Type")) { - if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) == s_simpleCorsContentTypeValues.cend()) + if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) != s_simpleCorsContentTypeValues.cend()) return false; } } - } - // Ensure method is in Simple CORS white list - return s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); - } - - /*static*/ Uri OriginPolicyHttpFilter::GetOrigin(Uri const &uri) noexcept { - return Uri{uri.SchemeName() + L"://" + uri.Host() + L":" + to_hstring(uri.Port())}; - } + // Ensure method is in Simple CORS allowlist + return s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); + } - /*static*/ bool OriginPolicyHttpFilter::AreSafeRequestHeaders( - winrt::Windows::Web::Http::Headers::HttpRequestHeaderCollection const &headers) noexcept { - for (const auto &header : headers) { - if (s_corsForbiddenRequestHeaderNames.find(header.Key().c_str()) != s_corsForbiddenRequestHeaderNames.cend()) - return false; + /*static*/ Uri OriginPolicyHttpFilter::GetOrigin(Uri const& uri) noexcept { + return Uri{ uri.SchemeName() + L"://" + uri.Host() + L":" + to_hstring(uri.Port()) }; + } - for (const auto &prefix : s_corsForbiddenRequestHeaderNamePrefixes) { - if (boost::istarts_with(header.Key(), prefix)) + /*static*/ bool OriginPolicyHttpFilter::AreSafeRequestHeaders( + winrt::Windows::Web::Http::Headers::HttpRequestHeaderCollection const& headers) noexcept { + for (const auto& header : headers) { + if (s_corsForbiddenRequestHeaderNames.find(header.Key().c_str()) != s_corsForbiddenRequestHeaderNames.cend()) return false; + + for (const auto& prefix : s_corsForbiddenRequestHeaderNamePrefixes) { + if (boost::istarts_with(header.Key(), prefix)) + return false; + } } + + return true; } - return true; - } + // See https://fetch.spec.whatwg.org/#cors-safelisted-request-header + // "A CORS-safelisted header is a header whose name is either one of 'Accept', 'Accept-Language', and + // 'Content-Language', or whose name is 'Content-Type' and value is one of 'application/x-www-form-urlencoded', + // 'multipart/form-data', and 'text/plain' + /*static*/ bool OriginPolicyHttpFilter::IsCorsSafelistedRequestHeader( + hstring const& name, + hstring const& value) noexcept { + // 1. If value's length is greater than 128, then return false. + if (value.size() > 128) + return false; - // See https://fetch.spec.whatwg.org/#cors-safelisted-request-header - // "A CORS-safelisted header is a header whose name is either one of 'Accept', 'Accept-Language', and - // 'Content-Language', or whose name is 'Content-Type' and value is one of 'application/x-www-form-urlencoded', - // 'multipart/form-data', and 'text/plain' - /*static*/ bool OriginPolicyHttpFilter::IsCorsSafelistedRequestHeader( - hstring const &name, hstring const &value) noexcept { - // 1. If value's length is greater than 128, then return false. - if (value.size() > 128) - return false; - - // 2. Byte-lowercase name and switch on the result: - static const wchar_t *const safeHeaderNames[] = { + // 2. Byte-lowercase name and switch on the result: + static const wchar_t* const safeHeaderNames[] = { // The following four headers are from the CORS spec L"accept", L"accept-language", @@ -226,188 +209,195 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co L"sec-ch-ua-mobile", L"sec-ch-ua-full-version", L"sec-ch-ua-platform-version", - }; + }; - auto nameLower = boost::to_lower_copy(wstring{name.c_str()}); - if (std::find(std::cbegin(safeHeaderNames), std::cend(safeHeaderNames), nameLower) == std::end(safeHeaderNames)) - return false; + auto nameLower = boost::to_lower_copy(wstring{ name.c_str() }); + if (std::find(std::cbegin(safeHeaderNames), std::cend(safeHeaderNames), nameLower) == std::end(safeHeaderNames)) + return false; - double doubleHolder; - if (nameLower == L"device-memory" || nameLower == L"dpr") - return boost::conversion::try_lexical_convert(value.c_str(), doubleHolder); + double doubleHolder; + if (nameLower == L"device-memory" || nameLower == L"dpr") + return boost::conversion::try_lexical_convert(value.c_str(), doubleHolder); - int intHolder; - if (nameLower == L"width" || nameLower == L"viewport-width") - return boost::conversion::try_lexical_convert(value.c_str(), intHolder); + int intHolder; + if (nameLower == L"width" || nameLower == L"viewport-width") + return boost::conversion::try_lexical_convert(value.c_str(), intHolder); - auto valueLower = boost::to_lower_copy(wstring{value.c_str()}); - if (nameLower == L"save-data") - return valueLower == L"on"; + auto valueLower = boost::to_lower_copy(wstring{ value.c_str() }); + if (nameLower == L"save-data") + return valueLower == L"on"; - if (nameLower == L"accept") - return !std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte); + if (nameLower == L"accept") + return !std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte); - if (nameLower == L"accept-language" || nameLower == L"content-language") { - return std::all_of(valueLower.cbegin(), valueLower.cend(), [](wchar_t c) noexcept { - return (0x30 <= c && c <= 0x39) || // 0-9 - (0x41 <= c && c <= 0x5A) || // A-Z - (0x61 <= c && c <= 0x7A) || // a-z - c == 0x20 || c == 0x2A || c == 0x2C || c == 0x2D || c == 0x2E || c == 0x3B || c == 0x3D; // *,-.;= - }); - } + if (nameLower == L"accept-language" || nameLower == L"content-language") { + return std::all_of(valueLower.cbegin(), valueLower.cend(), [](wchar_t c) noexcept { + return (0x30 <= c && c <= 0x39) || // 0-9 + (0x41 <= c && c <= 0x5A) || // A-Z + (0x61 <= c && c <= 0x7A) || // a-z + c == 0x20 || c == 0x2A || c == 0x2C || c == 0x2D || c == 0x2E || c == 0x3B || c == 0x3D; // *,-.;= + }); + } - if (nameLower == L"content-type") { - if (std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte)) - return false; + if (nameLower == L"content-type") { + if (std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte)) + return false; - // https://mimesniff.spec.whatwg.org/#parse-a-mime-type - HttpMediaTypeHeaderValue mediaType{nullptr}; - if (HttpMediaTypeHeaderValue::TryParse(valueLower, mediaType)) - return mediaType.ToString() == L"application/x-www-form-urlencoded" || - mediaType.ToString() == L"multipart/form-data" || mediaType.ToString() == L"text/plain"; + // https://mimesniff.spec.whatwg.org/#parse-a-mime-type + HttpMediaTypeHeaderValue mediaType{ nullptr }; + if (HttpMediaTypeHeaderValue::TryParse(valueLower, mediaType)) + return mediaType.ToString() == L"application/x-www-form-urlencoded" || + mediaType.ToString() == L"multipart/form-data" || mediaType.ToString() == L"text/plain"; - return false; - } + return false; + } - return true; - } + return true; + } - // https://fetch.spec.whatwg.org/#cors-unsafe-request-header-byte - /*static*/ bool OriginPolicyHttpFilter::IsCorsUnsafeRequestHeaderByte(wchar_t c) noexcept { - return (c < 0x20 && c != 0x09) || c == 0x22 || c == 0x28 || c == 0x29 || c == 0x3a || c == 0x3c || c == 0x3e || + // https://fetch.spec.whatwg.org/#cors-unsafe-request-header-byte + /*static*/ bool OriginPolicyHttpFilter::IsCorsUnsafeRequestHeaderByte(wchar_t c) noexcept { + return (c < 0x20 && c != 0x09) || c == 0x22 || c == 0x28 || c == 0x29 || c == 0x3a || c == 0x3c || c == 0x3e || c == 0x3f || c == 0x40 || c == 0x5b || c == 0x5c || c == 0x5d || c == 0x7b || c == 0x7d || c == 0x7f; - } + } - /*static*/ set OriginPolicyHttpFilter::CorsUnsafeNotForbiddenRequestHeaderNames( - HttpRequestHeaderCollection const &headers) noexcept { - constexpr size_t maxSafelistValueSize = 1024; - size_t safelistValueSize = 0; - std::vector potentiallyUnsafeNames; - set result; - for (const auto &header : headers) { - const auto headerName = header.Key().c_str(); - - // If header is not safe - if (boost::istarts_with(headerName, L"Proxy-") || boost::istarts_with(headerName, L"Sec-") || + /*static*/ set OriginPolicyHttpFilter::CorsUnsafeNotForbiddenRequestHeaderNames( + HttpRequestHeaderCollection const& headers) noexcept { + constexpr size_t maxSafelistValueSize = 1024; + size_t safelistValueSize = 0; + std::vector potentiallyUnsafeNames; + set result; + for (const auto& header : headers) { + const auto headerName = header.Key().c_str(); + + // If header is not safe + if (boost::istarts_with(headerName, L"Proxy-") || boost::istarts_with(headerName, L"Sec-") || s_corsForbiddenRequestHeaderNames.find(headerName) != s_corsForbiddenRequestHeaderNames.cend()) - continue; + continue; - if (!IsCorsSafelistedRequestHeader(header.Key(), header.Value())) { - result.emplace(header.Key().c_str()); - } else { - potentiallyUnsafeNames.emplace_back(std::wstring_view{header.Key()}.data()); - safelistValueSize += header.Value().size(); + if (!IsCorsSafelistedRequestHeader(header.Key(), header.Value())) { + result.emplace(header.Key().c_str()); + } + else { + potentiallyUnsafeNames.emplace_back(std::wstring_view{ header.Key() }.data()); + safelistValueSize += header.Value().size(); + } } - } - if (safelistValueSize > maxSafelistValueSize) - result.insert(potentiallyUnsafeNames.begin(), potentiallyUnsafeNames.end()); + if (safelistValueSize > maxSafelistValueSize) + result.insert(potentiallyUnsafeNames.begin(), potentiallyUnsafeNames.end()); - return result; - } + return result; + } - /*static*/ OriginPolicyHttpFilter::AccessControlValues OriginPolicyHttpFilter::ExtractAccessControlValues( - winrt::Windows::Foundation::Collections::IMap const &headers) { - using std::wregex; - using std::wsregex_token_iterator; - - // https://tools.ietf.org/html/rfc2616#section-4.2 - wregex rgx{L"\\s*,\\s*"}; - AccessControlValues result; - - auto ciStrCmp = [](const wstring &a, const wstring &b) { return _wcsicmp(a.c_str(), b.c_str()) < 0; }; - set allowedHeaders{ciStrCmp}; - set allowedMethods{ciStrCmp}; - set exposedHeaders{ciStrCmp}; - - for (const auto &header : headers) { - if (boost::iequals(header.Key(), L"Access-Control-Allow-Headers")) { - auto value = wstring{header.Value().c_str()}; - - // TODO: Avoid redundant comparison. - auto parsed = set{ - wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp}; - allowedHeaders.insert(parsed.cbegin(), parsed.cend()); - result.AllowedHeaders.insert(allowedHeaders.cbegin(), allowedHeaders.cend()); - } else if (boost::iequals(header.Key(), L"Access-Control-Allow-Methods")) { - auto value = wstring{header.Value().c_str()}; - - // TODO: Avoid redundant comparison. - auto parsed = set{ - wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp}; - allowedMethods.insert(parsed.cbegin(), parsed.cend()); - result.AllowedMethods.insert(allowedMethods.cbegin(), allowedMethods.cend()); - } else if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { - result.AllowedOrigin = header.Value(); - } else if (boost::iequals(header.Key(), L"Access-Control-Expose-Headers")) { - auto value = wstring{header.Value().c_str()}; - - // TODO: Avoid redundant comparison. - auto parsed = set{ - wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp}; - exposedHeaders.insert(parsed.cbegin(), parsed.cend()); - result.ExposedHeaders.insert(exposedHeaders.cbegin(), exposedHeaders.cend()); - } else if (boost::iequals(header.Key(), L"Access-Control-Allow-Credentials")) { - result.AllowedCredentials = header.Value(); - } else if (boost::iequals(header.Key(), L"Access-Control-Max-Age")) { - result.MaxAge = _wtoi(header.Value().c_str()); + /*static*/ OriginPolicyHttpFilter::AccessControlValues OriginPolicyHttpFilter::ExtractAccessControlValues( + winrt::Windows::Foundation::Collections::IMap const& headers) { + using std::wregex; + using std::wsregex_token_iterator; + + // https://tools.ietf.org/html/rfc2616#section-4.2 + wregex rgx{ L"\\s*,\\s*" }; + AccessControlValues result; + + auto ciStrCmp = [](const wstring& a, const wstring& b) { return _wcsicmp(a.c_str(), b.c_str()) < 0; }; + set allowedHeaders{ ciStrCmp }; + set allowedMethods{ ciStrCmp }; + set exposedHeaders{ ciStrCmp }; + + for (const auto& header : headers) { + if (boost::iequals(header.Key(), L"Access-Control-Allow-Headers")) { + auto value = wstring{ header.Value().c_str() }; + + // TODO: Avoid redundant comparison. + auto parsed = set{ + wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; + allowedHeaders.insert(parsed.cbegin(), parsed.cend()); + result.AllowedHeaders.insert(allowedHeaders.cbegin(), allowedHeaders.cend()); + } + else if (boost::iequals(header.Key(), L"Access-Control-Allow-Methods")) { + auto value = wstring{ header.Value().c_str() }; + + // TODO: Avoid redundant comparison. + auto parsed = set{ + wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; + allowedMethods.insert(parsed.cbegin(), parsed.cend()); + result.AllowedMethods.insert(allowedMethods.cbegin(), allowedMethods.cend()); + } + else if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { + result.AllowedOrigin = header.Value(); + } + else if (boost::iequals(header.Key(), L"Access-Control-Expose-Headers")) { + auto value = wstring{ header.Value().c_str() }; + + // TODO: Avoid redundant comparison. + auto parsed = set{ + wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; + exposedHeaders.insert(parsed.cbegin(), parsed.cend()); + result.ExposedHeaders.insert(exposedHeaders.cbegin(), exposedHeaders.cend()); + } + else if (boost::iequals(header.Key(), L"Access-Control-Allow-Credentials")) { + result.AllowedCredentials = header.Value(); + } + else if (boost::iequals(header.Key(), L"Access-Control-Max-Age")) { + result.MaxAge = _wtoi(header.Value().c_str()); + } } - } - return result; - } // ExtractAccessControlValues + return result; + } // ExtractAccessControlValues + + /*static*/ void OriginPolicyHttpFilter::RemoveHttpOnlyCookiesFromResponseHeaders( + HttpResponseMessage const& response, + bool removeAll) { + // Example: "Set-Cookie", L"id=a3fWa; Expires=Wed, 21 Oct 2020 07:28:00 GMT; HttpOnly" + std::queue httpOnlyCookies; + for (const auto& header : response.Headers()) { + if (s_cookieSettingResponseHeaders.find(header.Key().c_str()) == s_cookieSettingResponseHeaders.cend()) + continue; + + if (removeAll) { + httpOnlyCookies.push(header.Key()); + continue; + } - /*static*/ void OriginPolicyHttpFilter::RemoveHttpOnlyCookiesFromResponseHeaders( - HttpResponseMessage const &response, bool removeAll) { - // Example: "Set-Cookie", L"id=a3fWa; Expires=Wed, 21 Oct 2020 07:28:00 GMT; HttpOnly" - std::queue httpOnlyCookies; - for (const auto &header : response.Headers()) { - if (s_cookieSettingResponseHeaders.find(header.Key().c_str()) == s_cookieSettingResponseHeaders.cend()) - continue; + // Anchors (^$) can't be part of bracket expressions ([]). + // Create 3 matching groups: 1. Beginning of string 2. Between delimiters 3. End of string + const std::wregex re(L"(^HttpOnly\\s*;)|(;\\s*HttpOnly\\s*;)|(;\\s*HttpOnly$)", std::regex_constants::icase); + if (!std::regex_search(header.Value().c_str(), re)) + continue; - if (removeAll) { + // HttpOnly cookie detected. Removing. httpOnlyCookies.push(header.Key()); - continue; - } - - // Anchors (^$) can't be part of bracket expressions ([]). - // Create 3 matching groups: 1. Beginning of string 2. Between delimiters 3. End of string - const std::wregex re(L"(^HttpOnly\\s*;)|(;\\s*HttpOnly\\s*;)|(;\\s*HttpOnly$)", std::regex_constants::icase); - if (!std::regex_search(header.Value().c_str(), re)) - continue; - - // HttpOnly cookie detected. Removing. - httpOnlyCookies.push(header.Key()); - } // const auto &header : response.Headers() + } // const auto &header : response.Headers() - while (!httpOnlyCookies.empty()) { - response.Headers().Remove(httpOnlyCookies.front()); - httpOnlyCookies.pop(); + while (!httpOnlyCookies.empty()) { + response.Headers().Remove(httpOnlyCookies.front()); + httpOnlyCookies.pop(); + } } - } - OriginPolicyHttpFilter::OriginPolicyHttpFilter(IHttpFilter const &innerFilter) : m_innerFilter{innerFilter} {} + OriginPolicyHttpFilter::OriginPolicyHttpFilter(IHttpFilter && innerFilter): m_innerFilter{ std::move(innerFilter) } {} - OriginPolicyHttpFilter::OriginPolicyHttpFilter() + OriginPolicyHttpFilter::OriginPolicyHttpFilter() : OriginPolicyHttpFilter(winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter{}) {} - OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const &request) { - auto effectiveOriginPolicy = + OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const& request) { + auto effectiveOriginPolicy = static_cast(request.Properties().Lookup(L"OriginPolicy").as().GetUInt64()); - switch (effectiveOriginPolicy) { + switch (effectiveOriginPolicy) { case OriginPolicy::None: return effectiveOriginPolicy; case OriginPolicy::SameOrigin: if (!IsSameOrigin(s_origin, request.RequestUri())) - throw hresult_error{E_INVALIDARG, L"SOP (same-origin policy) is enforced"}; + throw hresult_error{ E_INVALIDARG, L"SOP (same-origin policy) is enforced" }; break; case OriginPolicy::SimpleCrossOriginResourceSharing: // Check for disallowed mixed content if (GetRuntimeOptionBool("Http.BlockMixedContentSimpleCors") && - s_origin.SchemeName() != request.RequestUri().SchemeName()) - throw hresult_error{E_INVALIDARG, L"The origin and request URLs must have the same scheme"}; + s_origin.SchemeName() != request.RequestUri().SchemeName()) + throw hresult_error{ E_INVALIDARG, L"The origin and request URLs must have the same scheme" }; if (IsSameOrigin(s_origin, request.RequestUri())) // Same origin. Therefore, skip Cross-Origin handling. @@ -415,7 +405,7 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co else if (!IsSimpleCorsRequest(request)) throw hresult_error{ E_INVALIDARG, - L"The request does not meet the requirements for Same-Origin policy or Simple Cross-Origin resource sharing"}; + L"The request does not meet the requirements for Same-Origin policy or Simple Cross-Origin resource sharing" }; break; case OriginPolicy::CrossOriginResourceSharing: @@ -426,13 +416,13 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co // https://fetch.spec.whatwg.org/#forbidden-header-name if (s_origin.SchemeName() != request.RequestUri().SchemeName()) - throw hresult_error{E_INVALIDARG, L"The origin and request URLs must have the same scheme"}; + throw hresult_error{ E_INVALIDARG, L"The origin and request URLs must have the same scheme" }; if (!AreSafeRequestHeaders(request.Headers())) - throw hresult_error{E_INVALIDARG, L"Request header not allowed in cross-origin resource sharing"}; + throw hresult_error{ E_INVALIDARG, L"Request header not allowed in cross-origin resource sharing" }; if (s_forbiddenMethods.find(request.Method().ToString().c_str()) != s_forbiddenMethods.cend()) - throw hresult_error{E_INVALIDARG, L"Request method not allowed in cross-origin resource sharing"}; + throw hresult_error{ E_INVALIDARG, L"Request method not allowed in cross-origin resource sharing" }; if (IsSameOrigin(s_origin, request.RequestUri())) effectiveOriginPolicy = OriginPolicy::SameOrigin; @@ -445,57 +435,51 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co default: throw hresult_error{ - E_INVALIDARG, L"Invalid OriginPolicy type: " + to_hstring(static_cast(effectiveOriginPolicy))}; - } - - return effectiveOriginPolicy; - } + E_INVALIDARG, L"Invalid OriginPolicy type: " + to_hstring(static_cast(effectiveOriginPolicy)) }; + } - // See https://fetch.spec.whatwg.org/#cors-check - void OriginPolicyHttpFilter::ValidateAllowOrigin( - hstring const &allowedOrigin, hstring const &allowCredentials, 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"}; + return effectiveOriginPolicy; + } // See https://fetch.spec.whatwg.org/#cors-check void OriginPolicyHttpFilter::ValidateAllowOrigin( - hstring const &allowedOrigin, hstring const &allowCredentials, IMap props) const { + hstring const& allowedOrigin, + hstring const& allowCredentials, + IInspectable const& iRequestArgs) 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"}; + L"Response header Access-Control-Allow-Origin has a value of [null] which differs from the supplied origin" }; - bool withCredentials = props.Lookup(L"RequestArgs").as()->WithCredentials; + bool withCredentials = iRequestArgs.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 - 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})) { + if (allowedOrigin.empty() || !IsSameOrigin(s_origin, Uri{ allowedOrigin })) { hstring errorMessage; if (allowedOrigin.empty()) errorMessage = L"No valid origin in response"; + // See https://fetch.spec.whatwg.org/#http-access-control-allow-origin. + else if (boost::contains(allowedOrigin, L" ,")) + errorMessage = L"Response header Access-Control-Allow-Origin can not have multiple values"; + // A wildcard Access-Control-Allow-Origin can not be used if credentials are to be sent, // even with Access-Control-Allow-Credentials set to true // See https://fetch.spec.whatwg.org/#cors-protocol-and-credentials else if (L"*" == allowedOrigin) errorMessage = - L"Response header Access-Control-Allow-Origin can not have a wildcard value when the request includes credentials"; + L"Response header Access-Control-Allow-Origin can not have a wildcard value when the request includes credentials"; else errorMessage = L"The Access-Control-Allow-Origin header has a value of [" + allowedOrigin + - L"] which differs from the supplied origin"; + L"] which differs from the supplied origin"; - throw hresult_error{E_INVALIDARG, errorMessage}; + throw hresult_error{ E_INVALIDARG, errorMessage }; } // 4.10.5 @@ -509,396 +493,656 @@ bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, co if (L"true" != allowCredentials) throw hresult_error{ E_INVALIDARG, - L"Access-Control-Allow-Credentials value must be \"true\" when the response includes credentials"}; + L"Access-Control-Allow-Credentials value must be \"true\" when the response includes credentials" }; }; void OriginPolicyHttpFilter::ValidatePreflightResponse( - HttpRequestMessage const &request, HttpResponseMessage const &response) const { + HttpRequestMessage const& request, + HttpResponseMessage const& response) const { // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed using winrt::Windows::Web::Http::HttpStatusCode; switch (response.StatusCode()) { - case HttpStatusCode::MovedPermanently: - case HttpStatusCode::TemporaryRedirect: - case HttpStatusCode::PermanentRedirect: - throw hresult_error{INET_E_REDIRECTING, L"Redirect is not allowed in a preflight request"}; + case HttpStatusCode::MovedPermanently: + case HttpStatusCode::TemporaryRedirect: + case HttpStatusCode::PermanentRedirect: + throw hresult_error{ INET_E_REDIRECTING, L"Redirect is not allowed in a preflight request" }; - default: - break; + default: + break; } auto controlValues = ExtractAccessControlValues(response.Headers()); - auto props = request.Properties(); + auto iRequestArgs = request.Properties().Lookup(L"RequestArgs"); // 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, props); + ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, iRequestArgs); // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.5 // Check if the request method is allowed - bool withCredentials = props.Lookup(L"RequestArgs").as()->WithCredentials; + bool withCredentials = iRequestArgs.as()->WithCredentials; bool requestMethodAllowed = false; - for (const auto &method : controlValues.AllowedMethods) { + for (const auto& method : controlValues.AllowedMethods) { if (L"*" == method) { if (!withCredentials) { requestMethodAllowed = true; break; } } + else if (boost::iequals(method, request.Method().ToString())) { + requestMethodAllowed = true; + break; + } + } - // Preflight should always allow simple CORS methods - requestMethodAllowed |= - s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); + // Preflight should always allow simple CORS methods + requestMethodAllowed |= s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); - if (!requestMethodAllowed) + if (!requestMethodAllowed) + throw hresult_error{ + E_INVALIDARG, + L"Method [" + request.Method().ToString() + + L"] is not allowed by Access-Control-Allow-Methods in preflight response" }; + + // Check if request headers are allowed + // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.6-7 + // Check if the header should be allowed through wildcard, if the request does not have credentials. + bool requestHeadersAllowed = false; + if (!withCredentials && controlValues.AllowedHeaders.find(L"*") != controlValues.AllowedHeaders.cend()) { + // "Authorization" header cannot be allowed through wildcard alone. + // "Authorization" is the only member of https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name. + if (request.Headers().HasKey(L"Authorization") && + controlValues.AllowedHeaders.find(L"Authorization") == controlValues.AllowedHeaders.cend()) throw hresult_error{ E_INVALIDARG, - L"Method [" + request.Method().ToString() + - L"] is not allowed by Access-Control-Allow-Methods in preflight response"}; - - // Check if request headers are allowed - // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.6-7 - // Check if the header should be allowed through wildcard, if the request does not have credentials. - bool requestHeadersAllowed = false; - if (!withCredentials && controlValues.AllowedHeaders.find(L"*") != controlValues.AllowedHeaders.cend()) { - // "Authorization" header cannot be allowed through wildcard alone. - // "Authorization" is the only member of https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name. - if (request.Headers().HasKey(L"Authorization") && - controlValues.AllowedHeaders.find(L"Authorization") == controlValues.AllowedHeaders.cend()) + L"Request header field [Authorization] is not allowed by Access-Control-Allow-Headers in preflight response" }; + + requestHeadersAllowed = true; + } + if (!requestHeadersAllowed) { + // Forbidden headers are excluded from the JavaScript layer. + // User agents may use these headers internally. + const set unsafeNotForbidenHeaderNames = CorsUnsafeNotForbiddenRequestHeaderNames(request.Headers()); + for (const auto name : unsafeNotForbidenHeaderNames) { + if (controlValues.AllowedHeaders.find(name) == controlValues.AllowedHeaders.cend()) throw hresult_error{ E_INVALIDARG, - L"Request header field [Authorization] is not allowed by Access-Control-Allow-Headers in preflight response"}; + L"Request header field [" + to_hstring(name) + + L"] is not allowed by Access-Control-Allow-Headers in preflight response" }; + } + } - requestHeadersAllowed = true; + // #9770 - insert into preflight cache + } + + // See 10.7.4 of https://fetch.spec.whatwg.org/#http-network-or-cache-fetch + void OriginPolicyHttpFilter::ValidateResponse(HttpResponseMessage const& response, const OriginPolicy originPolicy) + const { + bool removeAllCookies = false; + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + originPolicy == OriginPolicy::CrossOriginResourceSharing) { + auto controlValues = ExtractAccessControlValues(response.Headers()); + auto withCredentials = + response.RequestMessage().Properties().Lookup(L"RequestArgs").try_as()->WithCredentials; + + if (GetRuntimeOptionBool("Http.StrictOriginCheckSimpleCors") && + originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { + bool originAllowed = false; + for (const auto& header : response.Headers()) { + if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { + originAllowed |= L"*" == header.Value() || s_origin == Uri{ header.Value() }; + } + } } - if (!requestHeadersAllowed) { - // Forbidden headers are excluded from the JavaScript layer. - // User agents may use these headers internally. - const set unsafeNotForbidenHeaderNames = CorsUnsafeNotForbiddenRequestHeaderNames(request.Headers()); - for (const auto name : unsafeNotForbidenHeaderNames) { - if (controlValues.AllowedHeaders.find(name) == controlValues.AllowedHeaders.cend()) - throw hresult_error{ - E_INVALIDARG, - L"Request header field [" + to_hstring(name) + - L"] is not allowed by Access-Control-Allow-Headers in preflight response"}; + + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { + // Filter out response headers that are not in the Simple CORS allowlist + std::queue nonSimpleNames; + for (const auto& header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend()) + nonSimpleNames.push(header.Key()); } + + // Anchors (^$) can't be part of bracket expressions ([]). + // Create 3 matching groups: 1. Beginning of string 2. Between delimiters 3. End of string + const std::wregex re(L"(^HttpOnly\\s*;)|(;\\s*HttpOnly\\s*;)|(;\\s*HttpOnly$)", std::regex_constants::icase); + if (!std::regex_search(header.Value().c_str(), re)) + continue; + + // HttpOnly cookie detected. Removing. + httpOnlyCookies.push(header.Key()); + } // const auto &header : response.Headers() + + while (!httpOnlyCookies.empty()) { + response.Headers().Remove(httpOnlyCookies.front()); + httpOnlyCookies.pop(); } + } - // When withCredentials is false, request cannot include cookies. Also, cookies will be ignored in responses. - removeAllCookies = !withCredentials && GetRuntimeOptionBool("Http.RemoveCookiesFromResponse"); - } // originPolicy == SimpleCrossOriginResourceSharing || CrossOriginResourceSharing + OriginPolicyHttpFilter::OriginPolicyHttpFilter(IHttpFilter const& innerFilter): m_innerFilter{ innerFilter } {} - // Don't expose HttpOnly cookies to JavaScript - RemoveHttpOnlyCookiesFromResponseHeaders(response, removeAllCookies); - } + OriginPolicyHttpFilter::OriginPolicyHttpFilter() + : OriginPolicyHttpFilter(winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter{}) {} + + OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const& request) { + auto effectiveOriginPolicy = + static_cast(request.Properties().Lookup(L"OriginPolicy").as().GetUInt64()); + switch (effectiveOriginPolicy) { + case OriginPolicy::None: + return effectiveOriginPolicy; + + case OriginPolicy::SameOrigin: + if (!IsSameOrigin(s_origin, request.RequestUri())) + throw hresult_error{ E_INVALIDARG, L"SOP (same-origin policy) is enforced" }; + break; + + case OriginPolicy::SimpleCrossOriginResourceSharing: + // Check for disallowed mixed content + if (GetRuntimeOptionBool("Http.BlockMixedContentSimpleCors") && + s_origin.SchemeName() != request.RequestUri().SchemeName()) + throw hresult_error{ E_INVALIDARG, L"The origin and request URLs must have the same scheme" }; + + if (IsSameOrigin(s_origin, request.RequestUri())) + // Same origin. Therefore, skip Cross-Origin handling. + effectiveOriginPolicy = OriginPolicy::SameOrigin; + else if (!IsSimpleCorsRequest(request)) + throw hresult_error{ + E_INVALIDARG, + L"The request does not meet the requirements for Same-Origin policy or Simple Cross-Origin resource sharing" }; + break; + + case OriginPolicy::CrossOriginResourceSharing: + // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests + // Refer to CorsURLLoaderFactory::IsValidRequest in chrome\src\services\network\cors\cors_url_loader_factory.cc. + // Forbidden headers should be blocked regardless of origins. + // Example: On the Edge browser, an XHR request with the "Host" header set gets rejected as unsafe. + // https://fetch.spec.whatwg.org/#forbidden-header-name + + if (s_origin.SchemeName() != request.RequestUri().SchemeName()) + throw hresult_error{ E_INVALIDARG, L"The origin and request URLs must have the same scheme" }; - ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage const &request) const { - auto coRequest = request; + if (!AreSafeRequestHeaders(request.Headers())) + throw hresult_error{ E_INVALIDARG, L"Request header not allowed in cross-origin resource sharing" }; - HttpRequestMessage preflightRequest; + if (s_forbiddenMethods.find(request.Method().ToString().c_str()) != s_forbiddenMethods.cend()) + throw hresult_error{ E_INVALIDARG, L"Request method not allowed in cross-origin resource sharing" }; - // Section 4.8.2 https://fetch.spec.whatwg.org/#cors-preflight-fetch - preflightRequest.Method(HttpMethod::Options()); - preflightRequest.RequestUri(coRequest.RequestUri()); - preflightRequest.Headers().Insert(L"Accept", L"*/*"); - preflightRequest.Headers().Insert(L"Access-Control-Request-Method", coRequest.Method().ToString()); + if (IsSameOrigin(s_origin, request.RequestUri())) + effectiveOriginPolicy = OriginPolicy::SameOrigin; + else if (IsSimpleCorsRequest(request)) + effectiveOriginPolicy = OriginPolicy::SimpleCrossOriginResourceSharing; + else + effectiveOriginPolicy = OriginPolicy::CrossOriginResourceSharing; - auto headerNames = wstring{}; - auto writeSeparator = false; - for (const auto &header : coRequest.Headers()) { - if (writeSeparator) { - headerNames += L", "; - } else { - writeSeparator = true; + break; + + default: + throw hresult_error{ + E_INVALIDARG, L"Invalid OriginPolicy type: " + to_hstring(static_cast(effectiveOriginPolicy)) }; } - headerNames += header.Key(); + return effectiveOriginPolicy; } - if (coRequest.Content()) { - for (const auto &header : coRequest.Content().Headers()) { - if (writeSeparator) { - headerNames += L", "; - } else { - writeSeparator = true; + // See https://fetch.spec.whatwg.org/#cors-check + void OriginPolicyHttpFilter::ValidateAllowOrigin( + hstring const& allowedOrigin, hstring const& allowCredentials, 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" }; + + // See https://fetch.spec.whatwg.org/#cors-check + void OriginPolicyHttpFilter::ValidateAllowOrigin( + hstring const& allowedOrigin, hstring const& allowCredentials, 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 = 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 + 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"; + + // A wildcard Access-Control-Allow-Origin can not be used if credentials are to be sent, + // even with Access-Control-Allow-Credentials set to true + // See https://fetch.spec.whatwg.org/#cors-protocol-and-credentials + else if (L"*" == allowedOrigin) + errorMessage = + L"Response header Access-Control-Allow-Origin can not have a wildcard value when the request includes credentials"; + + else + errorMessage = L"The Access-Control-Allow-Origin header has a value of [" + allowedOrigin + + L"] which differs from the supplied origin"; + + throw hresult_error{ E_INVALIDARG, errorMessage }; } - // See 10.7.4 of https://fetch.spec.whatwg.org/#http-network-or-cache-fetch - void OriginPolicyHttpFilter::ValidateResponse( - HttpResponseMessage const &response, const OriginPolicy originPolicy) const { - bool removeAllCookies = false; - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || - originPolicy == OriginPolicy::CrossOriginResourceSharing) { - auto controlValues = ExtractAccessControlValues(response.Headers()); - auto props = response.RequestMessage().Properties(); - auto withCredentials = props.Lookup(L"RequestArgs").try_as()->WithCredentials; - - if (GetRuntimeOptionBool("Http.StrictOriginCheckSimpleCors") && - originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { - bool originAllowed = false; - for (const auto &header : response.Headers()) { - if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { - originAllowed |= L"*" == header.Value() || s_origin == Uri{header.Value()}; - } - } + // 4.10.5 + if (!withCredentials) + return; - if (!originAllowed) { - throw hresult_error{E_INVALIDARG, L"The server does not support CORS or the origin is not allowed"}; - } - } else { - ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props); + // 4.10.6-8 + // https://fetch.spec.whatwg.org/#http-access-control-allow-credentials + // This check should be case sensitive. + // See also https://fetch.spec.whatwg.org/#http-new-header-syntax + if (L"true" != allowCredentials) + throw hresult_error{ + E_INVALIDARG, + L"Access-Control-Allow-Credentials value must be \"true\" when the response includes credentials" }; + }; + + void OriginPolicyHttpFilter::ValidatePreflightResponse( + HttpRequestMessage const& request, HttpResponseMessage const& response) const { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed + using winrt::Windows::Web::Http::HttpStatusCode; + switch (response.StatusCode()) { + case HttpStatusCode::MovedPermanently: + case HttpStatusCode::TemporaryRedirect: + case HttpStatusCode::PermanentRedirect: + throw hresult_error{ INET_E_REDIRECTING, L"Redirect is not allowed in a preflight request" }; + + default: + break; + } + + auto controlValues = ExtractAccessControlValues(response.Headers()); + + 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, props); + + // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.5 + // Check if the request method is allowed + bool withCredentials = props.Lookup(L"RequestArgs").as()->WithCredentials; + bool requestMethodAllowed = false; + for (const auto& method : controlValues.AllowedMethods) { + if (L"*" == method) { + if (!withCredentials) { + requestMethodAllowed = true; + break; } + } - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { - // Filter out response headers that are not in the Simple CORS allowlist - std::queue nonSimpleNames; - for (const auto &header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == - s_simpleCorsResponseHeaderNames.cend()) - nonSimpleNames.push(header.Key()); - } + // Preflight should always allow simple CORS methods + requestMethodAllowed |= + s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); - while (!nonSimpleNames.empty()) { - response.Headers().Remove(nonSimpleNames.front()); - nonSimpleNames.pop(); - } - } else { - // Filter out response headers that are not simple headers and not in expose list + if (!requestMethodAllowed) + throw hresult_error{ + E_INVALIDARG, + L"Method [" + request.Method().ToString() + + L"] is not allowed by Access-Control-Allow-Methods in preflight response" }; + + // Check if request headers are allowed + // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.6-7 + // Check if the header should be allowed through wildcard, if the request does not have credentials. + bool requestHeadersAllowed = false; + if (!withCredentials && controlValues.AllowedHeaders.find(L"*") != controlValues.AllowedHeaders.cend()) { + // "Authorization" header cannot be allowed through wildcard alone. + // "Authorization" is the only member of https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name. + if (request.Headers().HasKey(L"Authorization") && + controlValues.AllowedHeaders.find(L"Authorization") == controlValues.AllowedHeaders.cend()) + throw hresult_error{ + E_INVALIDARG, + L"Request header field [Authorization] is not allowed by Access-Control-Allow-Headers in preflight response" }; + + requestHeadersAllowed = true; + } + if (!requestHeadersAllowed) { + // Forbidden headers are excluded from the JavaScript layer. + // User agents may use these headers internally. + const set unsafeNotForbidenHeaderNames = CorsUnsafeNotForbiddenRequestHeaderNames(request.Headers()); + for (const auto name : unsafeNotForbidenHeaderNames) { + if (controlValues.AllowedHeaders.find(name) == controlValues.AllowedHeaders.cend()) + throw hresult_error{ + E_INVALIDARG, + L"Request header field [" + to_hstring(name) + + L"] is not allowed by Access-Control-Allow-Headers in preflight response" }; + } + } - // Keep simple headers and those found in the expose header list. - if (withCredentials || controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { - std::queue nonSimpleNonExposedHeaders; + // When withCredentials is false, request cannot include cookies. Also, cookies will be ignored in responses. + removeAllCookies = !withCredentials && GetRuntimeOptionBool("Http.RemoveCookiesFromResponse"); + } // originPolicy == SimpleCrossOriginResourceSharing || CrossOriginResourceSharing - for (const auto &header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == - s_simpleCorsResponseHeaderNames.cend() && - controlValues.ExposedHeaders.find(header.Key().c_str()) == - controlValues.ExposedHeaders.cend()) { - nonSimpleNonExposedHeaders.push(header.Key()); + // Don't expose HttpOnly cookies to JavaScript + RemoveHttpOnlyCookiesFromResponseHeaders(response, removeAllCookies); + } + + ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage const& request) const { + auto coRequest = request; + + HttpRequestMessage preflightRequest; + + // Section 4.8.2 https://fetch.spec.whatwg.org/#cors-preflight-fetch + preflightRequest.Method(HttpMethod::Options()); + preflightRequest.RequestUri(coRequest.RequestUri()); + preflightRequest.Headers().Insert(L"Accept", L"*/*"); + preflightRequest.Headers().Insert(L"Access-Control-Request-Method", coRequest.Method().ToString()); + + auto headerNames = wstring{}; + auto writeSeparator = false; + for (const auto& header : coRequest.Headers()) { + if (writeSeparator) { + headerNames += L", "; + } + else { + writeSeparator = true; + } + + headerNames += header.Key(); + } + + if (coRequest.Content()) { + for (const auto& header : coRequest.Content().Headers()) { + if (writeSeparator) { + headerNames += L", "; + } + else { + writeSeparator = true; + } + + // See 10.7.4 of https://fetch.spec.whatwg.org/#http-network-or-cache-fetch + void OriginPolicyHttpFilter::ValidateResponse( + HttpResponseMessage const& response, const OriginPolicy originPolicy) const { + bool removeAllCookies = false; + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + originPolicy == OriginPolicy::CrossOriginResourceSharing) { + auto controlValues = ExtractAccessControlValues(response.Headers()); + auto props = response.RequestMessage().Properties(); + auto withCredentials = props.Lookup(L"RequestArgs").try_as()->WithCredentials; + + if (GetRuntimeOptionBool("Http.StrictOriginCheckSimpleCors") && + originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { + bool originAllowed = false; + for (const auto& header : response.Headers()) { + if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { + originAllowed |= L"*" == header.Value() || s_origin == Uri{ header.Value() }; + } } - } - if (!originAllowed) { - throw hresult_error{E_INVALIDARG, L"The server does not support CORS or the origin is not allowed"}; + if (!originAllowed) { + throw hresult_error{ E_INVALIDARG, L"The server does not support CORS or the origin is not allowed" }; + } + } + else { + ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props); } - } else { - ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props); - } - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { - // Filter out response headers that are not in the Simple CORS whitelist - std::queue nonSimpleNames; - for (const auto &header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { + // Filter out response headers that are not in the Simple CORS allowlist + std::queue nonSimpleNames; + for (const auto& header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend()) - nonSimpleNames.push(header.Key()); - } + nonSimpleNames.push(header.Key()); + } - while (!nonSimpleNames.empty()) { - response.Headers().Remove(nonSimpleNames.front()); - nonSimpleNames.pop(); + while (!nonSimpleNames.empty()) { + response.Headers().Remove(nonSimpleNames.front()); + nonSimpleNames.pop(); + } } - } else { - // Filter out response headers that are not simple headers and not in expose list + else { + // Filter out response headers that are not simple headers and not in expose list - // Keep simple headers and those found in the expose header list. - if (withCredentials || - controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { - std::queue nonSimpleNonExposedHeaders; + // Keep simple headers and those found in the expose header list. + if (withCredentials || controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { + std::queue nonSimpleNonExposedHeaders; - for (const auto &header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == - s_simpleCorsResponseHeaderNames.cend() && + for (const auto& header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == + s_simpleCorsResponseHeaderNames.cend() && controlValues.ExposedHeaders.find(header.Key().c_str()) == - controlValues.ExposedHeaders.cend()) { - nonSimpleNonExposedHeaders.push(header.Key()); + controlValues.ExposedHeaders.cend()) { + nonSimpleNonExposedHeaders.push(header.Key()); + } } + + if (!originAllowed) { + throw hresult_error{ E_INVALIDARG, L"The server does not support CORS or the origin is not allowed" }; + } + } + else { + ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props); } - while (!nonSimpleNonExposedHeaders.empty()) { - response.Headers().Remove(nonSimpleNonExposedHeaders.front()); - nonSimpleNonExposedHeaders.pop(); + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { + // Filter out response headers that are not in the Simple CORS whitelist + std::queue nonSimpleNames; + for (const auto& header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == + s_simpleCorsResponseHeaderNames.cend()) + nonSimpleNames.push(header.Key()); + } + + while (!nonSimpleNames.empty()) { + response.Headers().Remove(nonSimpleNames.front()); + nonSimpleNames.pop(); + } } - } - } + else { + // Filter out response headers that are not simple headers and not in expose list - // When withCredentials is false, request cannot include cookies. Also, cookies will be ignored in - // responses. - removeAllCookies = !withCredentials && GetRuntimeOptionBool("Http.RemoveCookiesFromResponse"); - } // originPolicy == SimpleCrossOriginResourceSharing || CrossOriginResourceSharing + // Keep simple headers and those found in the expose header list. + if (withCredentials || + controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { + std::queue nonSimpleNonExposedHeaders; - // Don't expose HttpOnly cookies to JavaScript - RemoveHttpOnlyCookiesFromResponseHeaders(response, removeAllCookies); - } + for (const auto& header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == + s_simpleCorsResponseHeaderNames.cend() && + controlValues.ExposedHeaders.find(header.Key().c_str()) == + controlValues.ExposedHeaders.cend()) { + nonSimpleNonExposedHeaders.push(header.Key()); + } + } + + while (!nonSimpleNonExposedHeaders.empty()) { + response.Headers().Remove(nonSimpleNonExposedHeaders.front()); + nonSimpleNonExposedHeaders.pop(); + } + } + } - ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage const &request) const { - auto coRequest = request; + // When withCredentials is false, request cannot include cookies. Also, cookies will be ignored in + // responses. + removeAllCookies = !withCredentials && GetRuntimeOptionBool("Http.RemoveCookiesFromResponse"); + } // originPolicy == SimpleCrossOriginResourceSharing || CrossOriginResourceSharing - HttpRequestMessage preflightRequest; + // Don't expose HttpOnly cookies to JavaScript + RemoveHttpOnlyCookiesFromResponseHeaders(response, removeAllCookies); + } - // Section 4.8.2 https://fetch.spec.whatwg.org/#cors-preflight-fetch - preflightRequest.Method(HttpMethod::Options()); - preflightRequest.RequestUri(coRequest.RequestUri()); - preflightRequest.Headers().Insert(L"Accept", L"*/*"); - preflightRequest.Headers().Insert(L"Access-Control-Request-Method", coRequest.Method().ToString()); + ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage const& request) const { + auto coRequest = request; - auto headerNames = wstring{}; - auto headerItr = coRequest.Headers().begin(); - if (headerItr != coRequest.Headers().end()) { - headerNames += (*headerItr).Key(); + HttpRequestMessage preflightRequest; - while (++headerItr != coRequest.Headers().end()) - headerNames += L", " + (*headerItr).Key(); - } + // Section 4.8.2 https://fetch.spec.whatwg.org/#cors-preflight-fetch + preflightRequest.Method(HttpMethod::Options()); + preflightRequest.RequestUri(coRequest.RequestUri()); + preflightRequest.Headers().Insert(L"Accept", L"*/*"); + preflightRequest.Headers().Insert(L"Access-Control-Request-Method", coRequest.Method().ToString()); - if (coRequest.Content()) { - headerItr = coRequest.Content().Headers().begin(); - if (headerItr != coRequest.Content().Headers().end()) { - headerNames += (*headerItr).Key(); + auto headerNames = wstring{}; + auto headerItr = coRequest.Headers().begin(); + if (headerItr != coRequest.Headers().end()) { + headerNames += (*headerItr).Key(); - while (++headerItr != coRequest.Content().Headers().end()) - headerNames += L", " + (*headerItr).Key(); - } - } + while (++headerItr != coRequest.Headers().end()) + headerNames += L", " + (*headerItr).Key(); + } - preflightRequest.Headers().Insert(L"Access-Control-Request-Headers", headerNames); - preflightRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); - preflightRequest.Headers().Insert(L"Sec-Fetch-Mode", L"CORS"); + if (coRequest.Content()) { + headerItr = coRequest.Content().Headers().begin(); + if (headerItr != coRequest.Content().Headers().end()) { + headerNames += (*headerItr).Key(); - co_return {co_await m_innerFilter.SendRequestAsync(preflightRequest)}; - } + while (++headerItr != coRequest.Content().Headers().end()) + headerNames += L", " + (*headerItr).Key(); + } + } -#pragma region IRedirectEventSource + preflightRequest.Headers().Insert(L"Access-Control-Request-Headers", headerNames); + preflightRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); + preflightRequest.Headers().Insert(L"Sec-Fetch-Mode", L"CORS"); - 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; + co_return{ co_await m_innerFilter.SendRequestAsync(preflightRequest) }; } - } - return true; - } +#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 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; - } - } + 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; - } + return true; + } #pragma endregion IRedirectEventSource #pragma region IHttpFilter - ResponseOperation OriginPolicyHttpFilter::SendRequestAsync(HttpRequestMessage const &request) { - auto coRequest = request; - - // Set initial origin policy to global runtime option. - request.Properties().Insert(L"OriginPolicy", winrt::box_value(GetRuntimeOptionInt("Http.OriginPolicy"))); + ResponseOperation OriginPolicyHttpFilter::SendRequestAsync(HttpRequestMessage const& request) { + auto coRequest = request; - // Allow only HTTP or HTTPS schemes - if (GetRuntimeOptionBool("Http.StrictScheme") && coRequest.RequestUri().SchemeName() != L"https" && - coRequest.RequestUri().SchemeName() != L"http") - throw hresult_error{E_INVALIDARG, L"Invalid URL scheme: [" + s_origin.SchemeName() + L"]"}; + // Set initial origin policy to global runtime option. + request.Properties().Insert(L"OriginPolicy", winrt::box_value(GetRuntimeOptionInt("Http.OriginPolicy"))); - if (!GetRuntimeOptionBool("Http.OmitCredentials")) { - coRequest.Properties().Lookup(L"RequestArgs").as()->WithCredentials = false; - } + // Allow only HTTP or HTTPS schemes + if (GetRuntimeOptionBool("Http.StrictScheme") && coRequest.RequestUri().SchemeName() != L"https" && + coRequest.RequestUri().SchemeName() != L"http") + throw hresult_error{ E_INVALIDARG, L"Invalid URL scheme: [" + s_origin.SchemeName() + L"]" }; - // Ensure absolute URL - coRequest.RequestUri(Uri{coRequest.RequestUri().AbsoluteCanonicalUri()}); + if (!GetRuntimeOptionBool("Http.OmitCredentials")) { + coRequest.Properties().Lookup(L"RequestArgs").as()->WithCredentials = false; + } - auto originPolicy = ValidateRequest(coRequest); - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || - originPolicy == OriginPolicy::CrossOriginResourceSharing) { - if (coRequest.RequestUri().UserName().size() > 0 || coRequest.RequestUri().Password().size() > 0) { - coRequest.RequestUri(Uri{coRequest.RequestUri().DisplayUri()}); - } - } + // Ensure absolute URL + coRequest.RequestUri(Uri{ coRequest.RequestUri().AbsoluteCanonicalUri() }); - try { - // #9770 - Validate preflight cache - if (originPolicy == OriginPolicy::CrossOriginResourceSharing) { - // If inner filter can AllowRedirect, disable for preflight. - winrt::impl::com_ref baseFilter; - if (baseFilter = m_innerFilter.try_as()) { - baseFilter.AllowAutoRedirect(false); + auto originPolicy = ValidateRequest(coRequest); + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + originPolicy == OriginPolicy::CrossOriginResourceSharing) { + if (coRequest.RequestUri().UserName().size() > 0 || coRequest.RequestUri().Password().size() > 0) { + coRequest.RequestUri(Uri{ coRequest.RequestUri().DisplayUri() }); + } } - auto preflightResponse = co_await SendPreflightAsync(coRequest); + try { + // #9770 - Validate preflight cache + if (originPolicy == OriginPolicy::CrossOriginResourceSharing) { + // If inner filter can AllowRedirect, disable for preflight. + winrt::impl::com_ref baseFilter; + if (baseFilter = m_innerFilter.try_as()) { + baseFilter.AllowAutoRedirect(false); + } - if (baseFilter) { - baseFilter.AllowAutoRedirect(true); - } + auto preflightResponse = co_await SendPreflightAsync(coRequest); - ValidatePreflightResponse(coRequest, preflightResponse); - } + if (baseFilter) { + baseFilter.AllowAutoRedirect(true); + } - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || - originPolicy == OriginPolicy::CrossOriginResourceSharing) { - coRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); - } + ValidatePreflightResponse(coRequest, preflightResponse); + } - auto response = co_await m_innerFilter.SendRequestAsync(coRequest); + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + originPolicy == OriginPolicy::CrossOriginResourceSharing) { + coRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); + } - ValidateResponse(response, originPolicy); + auto response = co_await m_innerFilter.SendRequestAsync(coRequest); - co_return response; + ValidateResponse(response, originPolicy); - } catch (hresult_error const &e) { - throw e; - } catch (const std::exception &e) { - throw hresult_error{E_FAIL, to_hstring(e.what())}; - } catch (...) { - throw hresult_error{E_FAIL, L"Unspecified error processing Origin Policy request"}; - } - } + co_return response; + + } + catch (hresult_error const& e) { + throw e; + } + catch (const std::exception& e) { + throw hresult_error{ E_FAIL, to_hstring(e.what()) }; + } + catch (...) { + throw hresult_error{ E_FAIL, L"Unspecified error processing Origin Policy request" }; + } + } #pragma endregion IHttpFilter #pragma endregion OriginPolicyHttpFilter - } // namespace Microsoft::React::Networking + } // namespace Microsoft::React::Networking From 47fb229c26d1f84aadbd668be078576704cca00e Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Wed, 7 Dec 2022 03:17:13 -0800 Subject: [PATCH 37/45] Fix OP filter --- .../Networking/OriginPolicyHttpFilter.cpp | 1552 +++++++---------- 1 file changed, 595 insertions(+), 957 deletions(-) diff --git a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp index 5360f0f252f..cd01c56d7dd 100644 --- a/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp +++ b/vnext/Shared/Networking/OriginPolicyHttpFilter.cpp @@ -39,20 +39,20 @@ namespace Microsoft::React::Networking { #pragma region ConstWcharComparer - bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t* a, const wchar_t* b) const { - return _wcsicmp(a, b) < 0; - } +bool OriginPolicyHttpFilter::ConstWcharComparer::operator()(const wchar_t *a, const wchar_t *b) const { + return _wcsicmp(a, b) < 0; +} #pragma endregion ConstWcharComparer - // https://fetch.spec.whatwg.org/#forbidden-method - /*static*/ set OriginPolicyHttpFilter::s_forbiddenMethods = - { L"CONNECT", L"TRACE", L"TRACK" }; +// https://fetch.spec.whatwg.org/#forbidden-method +/*static*/ set OriginPolicyHttpFilter::s_forbiddenMethods = + {L"CONNECT", L"TRACE", L"TRACK"}; - /*static*/ set - OriginPolicyHttpFilter::s_simpleCorsMethods = { L"GET", L"HEAD", L"POST" }; +/*static*/ set + OriginPolicyHttpFilter::s_simpleCorsMethods = {L"GET", L"HEAD", L"POST"}; - /*static*/ set +/*static*/ set OriginPolicyHttpFilter::s_simpleCorsRequestHeaderNames = { L"Accept", L"Accept-Language", @@ -62,21 +62,21 @@ namespace Microsoft::React::Networking { L"Downlink", L"Save-Data", L"Viewport-Width", - L"Width" }; + L"Width"}; - /*static*/ set +/*static*/ set OriginPolicyHttpFilter::s_simpleCorsResponseHeaderNames = - { L"Cache-Control", L"Content-Language", L"Content-Type", L"Expires", L"Last-Modified", L"Pragma" }; + {L"Cache-Control", L"Content-Language", L"Content-Type", L"Expires", L"Last-Modified", L"Pragma"}; - /*static*/ set +/*static*/ set OriginPolicyHttpFilter::s_simpleCorsContentTypeValues = { L"application/x-www-form-urlencoded", L"multipart/form-data", - L"text/plain" }; + L"text/plain"}; - // https://fetch.spec.whatwg.org/#forbidden-header-name - // Chromium still bans "User-Agent" due to https://crbug.com/571722 - /*static*/ set +// https://fetch.spec.whatwg.org/#forbidden-header-name +// Chromium still bans "User-Agent" due to https://crbug.com/571722 +/*static*/ set OriginPolicyHttpFilter::s_corsForbiddenRequestHeaderNames = { L"Accept-Charset", L"Accept-Encoding", @@ -97,1052 +97,690 @@ namespace Microsoft::React::Networking { L"Trailer", L"Transfer-Encoding", L"Upgrade", - L"Via" }; + L"Via"}; - /*static*/ set +/*static*/ set OriginPolicyHttpFilter::s_cookieSettingResponseHeaders = { L"Set-Cookie", L"Set-Cookie2", // Deprecated by the spec, but probably still used - }; +}; - /*static*/ set - OriginPolicyHttpFilter::s_corsForbiddenRequestHeaderNamePrefixes = { L"Proxy-", L"Sec-" }; +/*static*/ set + OriginPolicyHttpFilter::s_corsForbiddenRequestHeaderNamePrefixes = {L"Proxy-", L"Sec-"}; - /*static*/ Uri OriginPolicyHttpFilter::s_origin{ nullptr }; +/*static*/ Uri OriginPolicyHttpFilter::s_origin{nullptr}; - /*static*/ void OriginPolicyHttpFilter::SetStaticOrigin(std::string&& url) { - if (!url.empty()) - s_origin = Uri{ to_hstring(url) }; - } +/*static*/ void OriginPolicyHttpFilter::SetStaticOrigin(std::string &&url) { + if (!url.empty()) + s_origin = Uri{to_hstring(url)}; +} + +/*static*/ bool OriginPolicyHttpFilter::IsSameOrigin(Uri const &u1, Uri const &u2) noexcept { + return (u1 && u2) && u1.SchemeName() == u2.SchemeName() && u1.Host() == u2.Host() && u1.Port() == u2.Port(); +} + +/*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const &request) noexcept { + // Ensure header is in Simple CORS allowlist + for (const auto &header : request.Headers()) { + if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend()) + return false; - /*static*/ bool OriginPolicyHttpFilter::IsSameOrigin(Uri const& u1, Uri const& u2) noexcept { - return (u1 && u2) && u1.SchemeName() == u2.SchemeName() && u1.Host() == u2.Host() && u1.Port() == u2.Port(); + // Ensure Content-Type value is in Simple CORS allowlist, if present + if (boost::iequals(header.Key(), L"Content-Type")) { + if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) != s_simpleCorsContentTypeValues.cend()) + return false; + } } - /*static*/ bool OriginPolicyHttpFilter::IsSimpleCorsRequest(HttpRequestMessage const& request) noexcept { - // Ensure header is in Simple CORS allowlist - for (const auto& header : request.Headers()) { - if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend()) + // WinRT separates request headers from request content headers + if (auto content = request.Content()) { + for (const auto &header : content.Headers()) { + // WinRT automatically appends non-allowlisted header Content-Length when Content-Type is set. Skip it. + if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend() && + !boost::iequals(header.Key(), "Content-Length")) return false; // Ensure Content-Type value is in Simple CORS allowlist, if present if (boost::iequals(header.Key(), L"Content-Type")) { - if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) != s_simpleCorsContentTypeValues.cend()) + if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) == s_simpleCorsContentTypeValues.cend()) return false; } } + } - // WinRT separates request headers from request content headers - if (auto content = request.Content()) { - for (const auto& header : content.Headers()) { - // WinRT automatically appends non-allowlisted header Content-Length when Content-Type is set. Skip it. - if (s_simpleCorsRequestHeaderNames.find(header.Key().c_str()) == s_simpleCorsRequestHeaderNames.cend() && - !boost::iequals(header.Key(), "Content-Length")) - return false; - - // Ensure Content-Type value is in Simple CORS allowlist, if present - if (boost::iequals(header.Key(), L"Content-Type")) { - if (s_simpleCorsContentTypeValues.find(header.Value().c_str()) != s_simpleCorsContentTypeValues.cend()) - return false; - } - } + // Ensure method is in Simple CORS allowlist + return s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); +} - // Ensure method is in Simple CORS allowlist - return s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); - } +/*static*/ Uri OriginPolicyHttpFilter::GetOrigin(Uri const &uri) noexcept { + return Uri{uri.SchemeName() + L"://" + uri.Host() + L":" + to_hstring(uri.Port())}; +} - /*static*/ Uri OriginPolicyHttpFilter::GetOrigin(Uri const& uri) noexcept { - return Uri{ uri.SchemeName() + L"://" + uri.Host() + L":" + to_hstring(uri.Port()) }; - } +/*static*/ bool OriginPolicyHttpFilter::AreSafeRequestHeaders( + winrt::Windows::Web::Http::Headers::HttpRequestHeaderCollection const &headers) noexcept { + for (const auto &header : headers) { + if (s_corsForbiddenRequestHeaderNames.find(header.Key().c_str()) != s_corsForbiddenRequestHeaderNames.cend()) + return false; - /*static*/ bool OriginPolicyHttpFilter::AreSafeRequestHeaders( - winrt::Windows::Web::Http::Headers::HttpRequestHeaderCollection const& headers) noexcept { - for (const auto& header : headers) { - if (s_corsForbiddenRequestHeaderNames.find(header.Key().c_str()) != s_corsForbiddenRequestHeaderNames.cend()) - return false; - - for (const auto& prefix : s_corsForbiddenRequestHeaderNamePrefixes) { - if (boost::istarts_with(header.Key(), prefix)) - return false; - } - } - - return true; + for (const auto &prefix : s_corsForbiddenRequestHeaderNamePrefixes) { + if (boost::istarts_with(header.Key(), prefix)) + return false; } + } - // See https://fetch.spec.whatwg.org/#cors-safelisted-request-header - // "A CORS-safelisted header is a header whose name is either one of 'Accept', 'Accept-Language', and - // 'Content-Language', or whose name is 'Content-Type' and value is one of 'application/x-www-form-urlencoded', - // 'multipart/form-data', and 'text/plain' - /*static*/ bool OriginPolicyHttpFilter::IsCorsSafelistedRequestHeader( - hstring const& name, - hstring const& value) noexcept { - // 1. If value's length is greater than 128, then return false. - if (value.size() > 128) - return false; + return true; +} + +// See https://fetch.spec.whatwg.org/#cors-safelisted-request-header +// "A CORS-safelisted header is a header whose name is either one of 'Accept', 'Accept-Language', and +// 'Content-Language', or whose name is 'Content-Type' and value is one of 'application/x-www-form-urlencoded', +// 'multipart/form-data', and 'text/plain' +/*static*/ bool OriginPolicyHttpFilter::IsCorsSafelistedRequestHeader( + hstring const &name, + hstring const &value) noexcept { + // 1. If value's length is greater than 128, then return false. + if (value.size() > 128) + return false; + + // 2. Byte-lowercase name and switch on the result: + static const wchar_t *const safeHeaderNames[] = { + // The following four headers are from the CORS spec + L"accept", + L"accept-language", + L"content-language", + L"content-type", + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Save-Data + L"save-data", + + // https://w3c.github.io/device-memory/#sec-device-memory-client-hint-header + L"device-memory", + L"dpr", + L"width", + L"viewport-width", + + // https://tools.ietf.org/html/draft-west-lang-client-hint + L"sec-ch-lang", + + // https://tools.ietf.org/html/draft-west-ua-client-hints + L"sec-ch-ua", + L"sec-ch-ua-platform", + L"sec-ch-ua-arch", + L"sec-ch-ua-model", + L"sec-ch-ua-mobile", + L"sec-ch-ua-full-version", + L"sec-ch-ua-platform-version", + }; - // 2. Byte-lowercase name and switch on the result: - static const wchar_t* const safeHeaderNames[] = { - // The following four headers are from the CORS spec - L"accept", - L"accept-language", - L"content-language", - L"content-type", - - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Save-Data - L"save-data", - - // https://w3c.github.io/device-memory/#sec-device-memory-client-hint-header - L"device-memory", - L"dpr", - L"width", - L"viewport-width", - - // https://tools.ietf.org/html/draft-west-lang-client-hint - L"sec-ch-lang", - - // https://tools.ietf.org/html/draft-west-ua-client-hints - L"sec-ch-ua", - L"sec-ch-ua-platform", - L"sec-ch-ua-arch", - L"sec-ch-ua-model", - L"sec-ch-ua-mobile", - L"sec-ch-ua-full-version", - L"sec-ch-ua-platform-version", - }; - - auto nameLower = boost::to_lower_copy(wstring{ name.c_str() }); - if (std::find(std::cbegin(safeHeaderNames), std::cend(safeHeaderNames), nameLower) == std::end(safeHeaderNames)) - return false; + auto nameLower = boost::to_lower_copy(wstring{name.c_str()}); + if (std::find(std::cbegin(safeHeaderNames), std::cend(safeHeaderNames), nameLower) == std::end(safeHeaderNames)) + return false; - double doubleHolder; - if (nameLower == L"device-memory" || nameLower == L"dpr") - return boost::conversion::try_lexical_convert(value.c_str(), doubleHolder); + double doubleHolder; + if (nameLower == L"device-memory" || nameLower == L"dpr") + return boost::conversion::try_lexical_convert(value.c_str(), doubleHolder); - int intHolder; - if (nameLower == L"width" || nameLower == L"viewport-width") - return boost::conversion::try_lexical_convert(value.c_str(), intHolder); + int intHolder; + if (nameLower == L"width" || nameLower == L"viewport-width") + return boost::conversion::try_lexical_convert(value.c_str(), intHolder); - auto valueLower = boost::to_lower_copy(wstring{ value.c_str() }); - if (nameLower == L"save-data") - return valueLower == L"on"; + auto valueLower = boost::to_lower_copy(wstring{value.c_str()}); + if (nameLower == L"save-data") + return valueLower == L"on"; - if (nameLower == L"accept") - return !std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte); + if (nameLower == L"accept") + return !std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte); - if (nameLower == L"accept-language" || nameLower == L"content-language") { - return std::all_of(valueLower.cbegin(), valueLower.cend(), [](wchar_t c) noexcept { - return (0x30 <= c && c <= 0x39) || // 0-9 + if (nameLower == L"accept-language" || nameLower == L"content-language") { + return std::all_of(valueLower.cbegin(), valueLower.cend(), [](wchar_t c) noexcept { + return (0x30 <= c && c <= 0x39) || // 0-9 (0x41 <= c && c <= 0x5A) || // A-Z (0x61 <= c && c <= 0x7A) || // a-z c == 0x20 || c == 0x2A || c == 0x2C || c == 0x2D || c == 0x2E || c == 0x3B || c == 0x3D; // *,-.;= - }); - } + }); + } - if (nameLower == L"content-type") { - if (std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte)) - return false; + if (nameLower == L"content-type") { + if (std::any_of(valueLower.cbegin(), valueLower.cend(), IsCorsUnsafeRequestHeaderByte)) + return false; - // https://mimesniff.spec.whatwg.org/#parse-a-mime-type - HttpMediaTypeHeaderValue mediaType{ nullptr }; - if (HttpMediaTypeHeaderValue::TryParse(valueLower, mediaType)) - return mediaType.ToString() == L"application/x-www-form-urlencoded" || + // https://mimesniff.spec.whatwg.org/#parse-a-mime-type + HttpMediaTypeHeaderValue mediaType{nullptr}; + if (HttpMediaTypeHeaderValue::TryParse(valueLower, mediaType)) + return mediaType.ToString() == L"application/x-www-form-urlencoded" || mediaType.ToString() == L"multipart/form-data" || mediaType.ToString() == L"text/plain"; - return false; - } + return false; + } - return true; + return true; +} + +// https://fetch.spec.whatwg.org/#cors-unsafe-request-header-byte +/*static*/ bool OriginPolicyHttpFilter::IsCorsUnsafeRequestHeaderByte(wchar_t c) noexcept { + return (c < 0x20 && c != 0x09) || c == 0x22 || c == 0x28 || c == 0x29 || c == 0x3a || c == 0x3c || c == 0x3e || + c == 0x3f || c == 0x40 || c == 0x5b || c == 0x5c || c == 0x5d || c == 0x7b || c == 0x7d || c == 0x7f; +} + +/*static*/ set OriginPolicyHttpFilter::CorsUnsafeNotForbiddenRequestHeaderNames( + HttpRequestHeaderCollection const &headers) noexcept { + constexpr size_t maxSafelistValueSize = 1024; + size_t safelistValueSize = 0; + std::vector potentiallyUnsafeNames; + set result; + for (const auto &header : headers) { + const auto headerName = header.Key().c_str(); + + // If header is not safe + if (boost::istarts_with(headerName, L"Proxy-") || boost::istarts_with(headerName, L"Sec-") || + s_corsForbiddenRequestHeaderNames.find(headerName) != s_corsForbiddenRequestHeaderNames.cend()) + continue; + + if (!IsCorsSafelistedRequestHeader(header.Key(), header.Value())) { + result.emplace(header.Key().c_str()); + } else { + potentiallyUnsafeNames.emplace_back(std::wstring_view{header.Key()}.data()); + safelistValueSize += header.Value().size(); } + } - // https://fetch.spec.whatwg.org/#cors-unsafe-request-header-byte - /*static*/ bool OriginPolicyHttpFilter::IsCorsUnsafeRequestHeaderByte(wchar_t c) noexcept { - return (c < 0x20 && c != 0x09) || c == 0x22 || c == 0x28 || c == 0x29 || c == 0x3a || c == 0x3c || c == 0x3e || - c == 0x3f || c == 0x40 || c == 0x5b || c == 0x5c || c == 0x5d || c == 0x7b || c == 0x7d || c == 0x7f; + if (safelistValueSize > maxSafelistValueSize) + result.insert(potentiallyUnsafeNames.begin(), potentiallyUnsafeNames.end()); + + return result; +} + +/*static*/ OriginPolicyHttpFilter::AccessControlValues OriginPolicyHttpFilter::ExtractAccessControlValues( + winrt::Windows::Foundation::Collections::IMap const &headers) { + using std::wregex; + using std::wsregex_token_iterator; + + // https://tools.ietf.org/html/rfc2616#section-4.2 + wregex rgx{L"\\s*,\\s*"}; + AccessControlValues result; + + auto ciStrCmp = [](const wstring &a, const wstring &b) { return _wcsicmp(a.c_str(), b.c_str()) < 0; }; + set allowedHeaders{ciStrCmp}; + set allowedMethods{ciStrCmp}; + set exposedHeaders{ciStrCmp}; + + for (const auto &header : headers) { + if (boost::iequals(header.Key(), L"Access-Control-Allow-Headers")) { + auto value = wstring{header.Value().c_str()}; + + // TODO: Avoid redundant comparison. + auto parsed = set{ + wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp}; + allowedHeaders.insert(parsed.cbegin(), parsed.cend()); + result.AllowedHeaders.insert(allowedHeaders.cbegin(), allowedHeaders.cend()); + } else if (boost::iequals(header.Key(), L"Access-Control-Allow-Methods")) { + auto value = wstring{header.Value().c_str()}; + + // TODO: Avoid redundant comparison. + auto parsed = set{ + wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp}; + allowedMethods.insert(parsed.cbegin(), parsed.cend()); + result.AllowedMethods.insert(allowedMethods.cbegin(), allowedMethods.cend()); + } else if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { + result.AllowedOrigin = header.Value(); + } else if (boost::iequals(header.Key(), L"Access-Control-Expose-Headers")) { + auto value = wstring{header.Value().c_str()}; + + // TODO: Avoid redundant comparison. + auto parsed = set{ + wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp}; + exposedHeaders.insert(parsed.cbegin(), parsed.cend()); + result.ExposedHeaders.insert(exposedHeaders.cbegin(), exposedHeaders.cend()); + } else if (boost::iequals(header.Key(), L"Access-Control-Allow-Credentials")) { + result.AllowedCredentials = header.Value(); + } else if (boost::iequals(header.Key(), L"Access-Control-Max-Age")) { + result.MaxAge = _wtoi(header.Value().c_str()); } + } - /*static*/ set OriginPolicyHttpFilter::CorsUnsafeNotForbiddenRequestHeaderNames( - HttpRequestHeaderCollection const& headers) noexcept { - constexpr size_t maxSafelistValueSize = 1024; - size_t safelistValueSize = 0; - std::vector potentiallyUnsafeNames; - set result; - for (const auto& header : headers) { - const auto headerName = header.Key().c_str(); - - // If header is not safe - if (boost::istarts_with(headerName, L"Proxy-") || boost::istarts_with(headerName, L"Sec-") || - s_corsForbiddenRequestHeaderNames.find(headerName) != s_corsForbiddenRequestHeaderNames.cend()) - continue; - - if (!IsCorsSafelistedRequestHeader(header.Key(), header.Value())) { - result.emplace(header.Key().c_str()); - } - else { - potentiallyUnsafeNames.emplace_back(std::wstring_view{ header.Key() }.data()); - safelistValueSize += header.Value().size(); - } - } + return result; +} // ExtractAccessControlValues + +/*static*/ void OriginPolicyHttpFilter::RemoveHttpOnlyCookiesFromResponseHeaders( + HttpResponseMessage const &response, + bool removeAll) { + // Example: "Set-Cookie", L"id=a3fWa; Expires=Wed, 21 Oct 2020 07:28:00 GMT; HttpOnly" + std::queue httpOnlyCookies; + for (const auto &header : response.Headers()) { + if (s_cookieSettingResponseHeaders.find(header.Key().c_str()) == s_cookieSettingResponseHeaders.cend()) + continue; + + if (removeAll) { + httpOnlyCookies.push(header.Key()); + continue; + } - if (safelistValueSize > maxSafelistValueSize) - result.insert(potentiallyUnsafeNames.begin(), potentiallyUnsafeNames.end()); + // Anchors (^$) can't be part of bracket expressions ([]). + // Create 3 matching groups: 1. Beginning of string 2. Between delimiters 3. End of string + const std::wregex re(L"(^HttpOnly\\s*;)|(;\\s*HttpOnly\\s*;)|(;\\s*HttpOnly$)", std::regex_constants::icase); + if (!std::regex_search(header.Value().c_str(), re)) + continue; - return result; - } + // HttpOnly cookie detected. Removing. + httpOnlyCookies.push(header.Key()); + } // const auto &header : response.Headers() - /*static*/ OriginPolicyHttpFilter::AccessControlValues OriginPolicyHttpFilter::ExtractAccessControlValues( - winrt::Windows::Foundation::Collections::IMap const& headers) { - using std::wregex; - using std::wsregex_token_iterator; - - // https://tools.ietf.org/html/rfc2616#section-4.2 - wregex rgx{ L"\\s*,\\s*" }; - AccessControlValues result; - - auto ciStrCmp = [](const wstring& a, const wstring& b) { return _wcsicmp(a.c_str(), b.c_str()) < 0; }; - set allowedHeaders{ ciStrCmp }; - set allowedMethods{ ciStrCmp }; - set exposedHeaders{ ciStrCmp }; - - for (const auto& header : headers) { - if (boost::iequals(header.Key(), L"Access-Control-Allow-Headers")) { - auto value = wstring{ header.Value().c_str() }; - - // TODO: Avoid redundant comparison. - auto parsed = set{ - wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; - allowedHeaders.insert(parsed.cbegin(), parsed.cend()); - result.AllowedHeaders.insert(allowedHeaders.cbegin(), allowedHeaders.cend()); - } - else if (boost::iequals(header.Key(), L"Access-Control-Allow-Methods")) { - auto value = wstring{ header.Value().c_str() }; - - // TODO: Avoid redundant comparison. - auto parsed = set{ - wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; - allowedMethods.insert(parsed.cbegin(), parsed.cend()); - result.AllowedMethods.insert(allowedMethods.cbegin(), allowedMethods.cend()); - } - else if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { - result.AllowedOrigin = header.Value(); - } - else if (boost::iequals(header.Key(), L"Access-Control-Expose-Headers")) { - auto value = wstring{ header.Value().c_str() }; - - // TODO: Avoid redundant comparison. - auto parsed = set{ - wsregex_token_iterator{value.cbegin(), value.cend(), rgx, -1}, wsregex_token_iterator{}, ciStrCmp }; - exposedHeaders.insert(parsed.cbegin(), parsed.cend()); - result.ExposedHeaders.insert(exposedHeaders.cbegin(), exposedHeaders.cend()); - } - else if (boost::iequals(header.Key(), L"Access-Control-Allow-Credentials")) { - result.AllowedCredentials = header.Value(); - } - else if (boost::iequals(header.Key(), L"Access-Control-Max-Age")) { - result.MaxAge = _wtoi(header.Value().c_str()); - } - } + while (!httpOnlyCookies.empty()) { + response.Headers().Remove(httpOnlyCookies.front()); + httpOnlyCookies.pop(); + } +} - return result; - } // ExtractAccessControlValues - - /*static*/ void OriginPolicyHttpFilter::RemoveHttpOnlyCookiesFromResponseHeaders( - HttpResponseMessage const& response, - bool removeAll) { - // Example: "Set-Cookie", L"id=a3fWa; Expires=Wed, 21 Oct 2020 07:28:00 GMT; HttpOnly" - std::queue httpOnlyCookies; - for (const auto& header : response.Headers()) { - if (s_cookieSettingResponseHeaders.find(header.Key().c_str()) == s_cookieSettingResponseHeaders.cend()) - continue; - - if (removeAll) { - httpOnlyCookies.push(header.Key()); - continue; - } +OriginPolicyHttpFilter::OriginPolicyHttpFilter(IHttpFilter const &innerFilter) : m_innerFilter{innerFilter} {} - // Anchors (^$) can't be part of bracket expressions ([]). - // Create 3 matching groups: 1. Beginning of string 2. Between delimiters 3. End of string - const std::wregex re(L"(^HttpOnly\\s*;)|(;\\s*HttpOnly\\s*;)|(;\\s*HttpOnly$)", std::regex_constants::icase); - if (!std::regex_search(header.Value().c_str(), re)) - continue; +OriginPolicyHttpFilter::OriginPolicyHttpFilter() + : OriginPolicyHttpFilter(winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter{}) {} - // HttpOnly cookie detected. Removing. - httpOnlyCookies.push(header.Key()); - } // const auto &header : response.Headers() +OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const &request) { + auto effectiveOriginPolicy = + static_cast(request.Properties().Lookup(L"OriginPolicy").as().GetUInt64()); + switch (effectiveOriginPolicy) { + case OriginPolicy::None: + return effectiveOriginPolicy; - while (!httpOnlyCookies.empty()) { - response.Headers().Remove(httpOnlyCookies.front()); - httpOnlyCookies.pop(); - } - } + case OriginPolicy::SameOrigin: + if (!IsSameOrigin(s_origin, request.RequestUri())) + throw hresult_error{E_INVALIDARG, L"SOP (same-origin policy) is enforced"}; + break; - OriginPolicyHttpFilter::OriginPolicyHttpFilter(IHttpFilter && innerFilter): m_innerFilter{ std::move(innerFilter) } {} + case OriginPolicy::SimpleCrossOriginResourceSharing: + // Check for disallowed mixed content + if (GetRuntimeOptionBool("Http.BlockMixedContentSimpleCors") && + s_origin.SchemeName() != request.RequestUri().SchemeName()) + throw hresult_error{E_INVALIDARG, L"The origin and request URLs must have the same scheme"}; - OriginPolicyHttpFilter::OriginPolicyHttpFilter() - : OriginPolicyHttpFilter(winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter{}) {} + if (IsSameOrigin(s_origin, request.RequestUri())) + // Same origin. Therefore, skip Cross-Origin handling. + effectiveOriginPolicy = OriginPolicy::SameOrigin; + else if (!IsSimpleCorsRequest(request)) + throw hresult_error{ + E_INVALIDARG, + L"The request does not meet the requirements for Same-Origin policy or Simple Cross-Origin resource sharing"}; + break; - OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const& request) { - auto effectiveOriginPolicy = - static_cast(request.Properties().Lookup(L"OriginPolicy").as().GetUInt64()); - switch (effectiveOriginPolicy) { - case OriginPolicy::None: - return effectiveOriginPolicy; + case OriginPolicy::CrossOriginResourceSharing: + // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests + // Refer to CorsURLLoaderFactory::IsValidRequest in chrome\src\services\network\cors\cors_url_loader_factory.cc. + // Forbidden headers should be blocked regardless of origins. + // Example: On the Edge browser, an XHR request with the "Host" header set gets rejected as unsafe. + // https://fetch.spec.whatwg.org/#forbidden-header-name - case OriginPolicy::SameOrigin: - if (!IsSameOrigin(s_origin, request.RequestUri())) - throw hresult_error{ E_INVALIDARG, L"SOP (same-origin policy) is enforced" }; - break; + if (s_origin.SchemeName() != request.RequestUri().SchemeName()) + throw hresult_error{E_INVALIDARG, L"The origin and request URLs must have the same scheme"}; - case OriginPolicy::SimpleCrossOriginResourceSharing: - // Check for disallowed mixed content - if (GetRuntimeOptionBool("Http.BlockMixedContentSimpleCors") && - s_origin.SchemeName() != request.RequestUri().SchemeName()) - throw hresult_error{ E_INVALIDARG, L"The origin and request URLs must have the same scheme" }; - - if (IsSameOrigin(s_origin, request.RequestUri())) - // Same origin. Therefore, skip Cross-Origin handling. - effectiveOriginPolicy = OriginPolicy::SameOrigin; - else if (!IsSimpleCorsRequest(request)) - throw hresult_error{ - E_INVALIDARG, - L"The request does not meet the requirements for Same-Origin policy or Simple Cross-Origin resource sharing" }; - break; + if (!AreSafeRequestHeaders(request.Headers())) + throw hresult_error{E_INVALIDARG, L"Request header not allowed in cross-origin resource sharing"}; - case OriginPolicy::CrossOriginResourceSharing: - // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests - // Refer to CorsURLLoaderFactory::IsValidRequest in chrome\src\services\network\cors\cors_url_loader_factory.cc. - // Forbidden headers should be blocked regardless of origins. - // Example: On the Edge browser, an XHR request with the "Host" header set gets rejected as unsafe. - // https://fetch.spec.whatwg.org/#forbidden-header-name + if (s_forbiddenMethods.find(request.Method().ToString().c_str()) != s_forbiddenMethods.cend()) + throw hresult_error{E_INVALIDARG, L"Request method not allowed in cross-origin resource sharing"}; - if (s_origin.SchemeName() != request.RequestUri().SchemeName()) - throw hresult_error{ E_INVALIDARG, L"The origin and request URLs must have the same scheme" }; + if (IsSameOrigin(s_origin, request.RequestUri())) + effectiveOriginPolicy = OriginPolicy::SameOrigin; + else if (IsSimpleCorsRequest(request)) + effectiveOriginPolicy = OriginPolicy::SimpleCrossOriginResourceSharing; + else + effectiveOriginPolicy = OriginPolicy::CrossOriginResourceSharing; - if (!AreSafeRequestHeaders(request.Headers())) - throw hresult_error{ E_INVALIDARG, L"Request header not allowed in cross-origin resource sharing" }; + break; - if (s_forbiddenMethods.find(request.Method().ToString().c_str()) != s_forbiddenMethods.cend()) - throw hresult_error{ E_INVALIDARG, L"Request method not allowed in cross-origin resource sharing" }; + default: + throw hresult_error{ + E_INVALIDARG, L"Invalid OriginPolicy type: " + to_hstring(static_cast(effectiveOriginPolicy))}; + } - if (IsSameOrigin(s_origin, request.RequestUri())) - effectiveOriginPolicy = OriginPolicy::SameOrigin; - else if (IsSimpleCorsRequest(request)) - effectiveOriginPolicy = OriginPolicy::SimpleCrossOriginResourceSharing; - else - effectiveOriginPolicy = OriginPolicy::CrossOriginResourceSharing; + return effectiveOriginPolicy; +} + +// See https://fetch.spec.whatwg.org/#cors-check +void OriginPolicyHttpFilter::ValidateAllowOrigin( + hstring const &allowedOrigin, + hstring const &allowCredentials, + 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 = 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 + 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"; + + // See https://fetch.spec.whatwg.org/#http-access-control-allow-origin. + else if (boost::contains(allowedOrigin, L" ,")) + errorMessage = L"Response header Access-Control-Allow-Origin can not have multiple values"; + + // A wildcard Access-Control-Allow-Origin can not be used if credentials are to be sent, + // even with Access-Control-Allow-Credentials set to true + // See https://fetch.spec.whatwg.org/#cors-protocol-and-credentials + else if (L"*" == allowedOrigin) + errorMessage = + L"Response header Access-Control-Allow-Origin can not have a wildcard value when the request includes credentials"; - break; + else + errorMessage = L"The Access-Control-Allow-Origin header has a value of [" + allowedOrigin + + L"] which differs from the supplied origin"; - default: - throw hresult_error{ - E_INVALIDARG, L"Invalid OriginPolicy type: " + to_hstring(static_cast(effectiveOriginPolicy)) }; - } + throw hresult_error{E_INVALIDARG, errorMessage}; + } - return effectiveOriginPolicy; + // 4.10.5 + if (!withCredentials) + return; + + // 4.10.6-8 + // https://fetch.spec.whatwg.org/#http-access-control-allow-credentials + // This check should be case sensitive. + // See also https://fetch.spec.whatwg.org/#http-new-header-syntax + if (L"true" != allowCredentials) + throw hresult_error{ + E_INVALIDARG, + L"Access-Control-Allow-Credentials value must be \"true\" when the response includes credentials"}; +}; + +void OriginPolicyHttpFilter::ValidatePreflightResponse( + HttpRequestMessage const &request, + HttpResponseMessage const &response) const { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed + using winrt::Windows::Web::Http::HttpStatusCode; + switch (response.StatusCode()) { + case HttpStatusCode::MovedPermanently: + case HttpStatusCode::TemporaryRedirect: + case HttpStatusCode::PermanentRedirect: + throw hresult_error{INET_E_REDIRECTING, L"Redirect is not allowed in a preflight request"}; + + default: + break; + } + + auto controlValues = ExtractAccessControlValues(response.Headers()); + + 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, props); + + // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.5 + // Check if the request method is allowed + bool withCredentials = props.Lookup(L"RequestArgs").as()->WithCredentials; + bool requestMethodAllowed = false; + for (const auto &method : controlValues.AllowedMethods) { + if (L"*" == method) { + if (!withCredentials) { + requestMethodAllowed = true; + break; + } + } else if (boost::iequals(method, request.Method().ToString())) { + requestMethodAllowed = true; + break; } + } - // See https://fetch.spec.whatwg.org/#cors-check - void OriginPolicyHttpFilter::ValidateAllowOrigin( - hstring const& allowedOrigin, - hstring const& allowCredentials, - IInspectable const& iRequestArgs) const { - // 4.10.1-2 - null allow origin - if (L"null" == allowedOrigin) + // Preflight should always allow simple CORS methods + requestMethodAllowed |= s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); + + if (!requestMethodAllowed) + throw hresult_error{ + E_INVALIDARG, + L"Method [" + request.Method().ToString() + + L"] is not allowed by Access-Control-Allow-Methods in preflight response"}; + + // Check if request headers are allowed + // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.6-7 + // Check if the header should be allowed through wildcard, if the request does not have credentials. + bool requestHeadersAllowed = false; + if (!withCredentials && controlValues.AllowedHeaders.find(L"*") != controlValues.AllowedHeaders.cend()) { + // "Authorization" header cannot be allowed through wildcard alone. + // "Authorization" is the only member of https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name. + if (request.Headers().HasKey(L"Authorization") && + controlValues.AllowedHeaders.find(L"Authorization") == controlValues.AllowedHeaders.cend()) + throw hresult_error{ + E_INVALIDARG, + L"Request header field [Authorization] is not allowed by Access-Control-Allow-Headers in preflight response"}; + + requestHeadersAllowed = true; + } + if (!requestHeadersAllowed) { + // Forbidden headers are excluded from the JavaScript layer. + // User agents may use these headers internally. + const set unsafeNotForbidenHeaderNames = CorsUnsafeNotForbiddenRequestHeaderNames(request.Headers()); + for (const auto name : unsafeNotForbidenHeaderNames) { + if (controlValues.AllowedHeaders.find(name) == controlValues.AllowedHeaders.cend()) 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; - // 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 })) { - hstring errorMessage; - if (allowedOrigin.empty()) - errorMessage = L"No valid origin in response"; - - // See https://fetch.spec.whatwg.org/#http-access-control-allow-origin. - else if (boost::contains(allowedOrigin, L" ,")) - errorMessage = L"Response header Access-Control-Allow-Origin can not have multiple values"; - - // A wildcard Access-Control-Allow-Origin can not be used if credentials are to be sent, - // even with Access-Control-Allow-Credentials set to true - // See https://fetch.spec.whatwg.org/#cors-protocol-and-credentials - else if (L"*" == allowedOrigin) - errorMessage = - L"Response header Access-Control-Allow-Origin can not have a wildcard value when the request includes credentials"; + L"Request header field [" + to_hstring(name) + + L"] is not allowed by Access-Control-Allow-Headers in preflight response"}; + } + } - else - errorMessage = L"The Access-Control-Allow-Origin header has a value of [" + allowedOrigin + - L"] which differs from the supplied origin"; + // #9770 - insert into preflight cache +} + +// See 10.7.4 of https://fetch.spec.whatwg.org/#http-network-or-cache-fetch +void OriginPolicyHttpFilter::ValidateResponse(HttpResponseMessage const &response, const OriginPolicy originPolicy) + const { + bool removeAllCookies = false; + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + originPolicy == OriginPolicy::CrossOriginResourceSharing) { + auto controlValues = ExtractAccessControlValues(response.Headers()); + auto props = response.RequestMessage().Properties(); + auto withCredentials = props.Lookup(L"RequestArgs").try_as()->WithCredentials; + + if (GetRuntimeOptionBool("Http.StrictOriginCheckSimpleCors") && + originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { + bool originAllowed = false; + for (const auto &header : response.Headers()) { + if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { + originAllowed |= L"*" == header.Value() || s_origin == Uri{header.Value()}; + } + } - throw hresult_error{ E_INVALIDARG, errorMessage }; + if (!originAllowed) { + throw hresult_error{E_INVALIDARG, L"The server does not support CORS or the origin is not allowed"}; } + } else { + ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props); + } - // 4.10.5 - if (!withCredentials) - return; + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { + // Filter out response headers that are not in the Simple CORS allowlist + std::queue nonSimpleNames; + for (const auto &header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend()) + nonSimpleNames.push(header.Key()); + } - // 4.10.6-8 - // https://fetch.spec.whatwg.org/#http-access-control-allow-credentials - // This check should be case sensitive. - // See also https://fetch.spec.whatwg.org/#http-new-header-syntax - if (L"true" != allowCredentials) - throw hresult_error{ - E_INVALIDARG, - L"Access-Control-Allow-Credentials value must be \"true\" when the response includes credentials" }; - }; - - void OriginPolicyHttpFilter::ValidatePreflightResponse( - HttpRequestMessage const& request, - HttpResponseMessage const& response) const { - // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed - using winrt::Windows::Web::Http::HttpStatusCode; - switch (response.StatusCode()) { - case HttpStatusCode::MovedPermanently: - case HttpStatusCode::TemporaryRedirect: - case HttpStatusCode::PermanentRedirect: - throw hresult_error{ INET_E_REDIRECTING, L"Redirect is not allowed in a preflight request" }; - - default: - break; + while (!nonSimpleNames.empty()) { + response.Headers().Remove(nonSimpleNames.front()); + nonSimpleNames.pop(); } + } else { + // Filter out response headers that are not simple headers and not in expose list + + // Keep simple headers and those found in the expose header list. + if (withCredentials || controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { + std::queue nonSimpleNonExposedHeaders; - auto controlValues = ExtractAccessControlValues(response.Headers()); - - auto iRequestArgs = request.Properties().Lookup(L"RequestArgs"); - // 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); - - // 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 requestMethodAllowed = false; - for (const auto& method : controlValues.AllowedMethods) { - if (L"*" == method) { - if (!withCredentials) { - requestMethodAllowed = true; - break; + for (const auto &header : response.Headers().GetView()) { + if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend() && + controlValues.ExposedHeaders.find(header.Key().c_str()) == controlValues.ExposedHeaders.cend()) { + nonSimpleNonExposedHeaders.push(header.Key()); } } - else if (boost::iequals(method, request.Method().ToString())) { - requestMethodAllowed = true; - break; + + while (!nonSimpleNonExposedHeaders.empty()) { + response.Headers().Remove(nonSimpleNonExposedHeaders.front()); + nonSimpleNonExposedHeaders.pop(); } } + } - // Preflight should always allow simple CORS methods - requestMethodAllowed |= s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); + // When withCredentials is false, request cannot include cookies. Also, cookies will be ignored in responses. + removeAllCookies = !withCredentials && GetRuntimeOptionBool("Http.RemoveCookiesFromResponse"); + } // originPolicy == SimpleCrossOriginResourceSharing || CrossOriginResourceSharing - if (!requestMethodAllowed) - throw hresult_error{ - E_INVALIDARG, - L"Method [" + request.Method().ToString() + - L"] is not allowed by Access-Control-Allow-Methods in preflight response" }; - - // Check if request headers are allowed - // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.6-7 - // Check if the header should be allowed through wildcard, if the request does not have credentials. - bool requestHeadersAllowed = false; - if (!withCredentials && controlValues.AllowedHeaders.find(L"*") != controlValues.AllowedHeaders.cend()) { - // "Authorization" header cannot be allowed through wildcard alone. - // "Authorization" is the only member of https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name. - if (request.Headers().HasKey(L"Authorization") && - controlValues.AllowedHeaders.find(L"Authorization") == controlValues.AllowedHeaders.cend()) - throw hresult_error{ - E_INVALIDARG, - L"Request header field [Authorization] is not allowed by Access-Control-Allow-Headers in preflight response" }; - - requestHeadersAllowed = true; - } - if (!requestHeadersAllowed) { - // Forbidden headers are excluded from the JavaScript layer. - // User agents may use these headers internally. - const set unsafeNotForbidenHeaderNames = CorsUnsafeNotForbiddenRequestHeaderNames(request.Headers()); - for (const auto name : unsafeNotForbidenHeaderNames) { - if (controlValues.AllowedHeaders.find(name) == controlValues.AllowedHeaders.cend()) - throw hresult_error{ - E_INVALIDARG, - L"Request header field [" + to_hstring(name) + - L"] is not allowed by Access-Control-Allow-Headers in preflight response" }; - } - } + // Don't expose HttpOnly cookies to JavaScript + RemoveHttpOnlyCookiesFromResponseHeaders(response, removeAllCookies); +} - // #9770 - insert into preflight cache - } +ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage const &request) const { + auto coRequest = request; - // See 10.7.4 of https://fetch.spec.whatwg.org/#http-network-or-cache-fetch - void OriginPolicyHttpFilter::ValidateResponse(HttpResponseMessage const& response, const OriginPolicy originPolicy) - const { - bool removeAllCookies = false; - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || - originPolicy == OriginPolicy::CrossOriginResourceSharing) { - auto controlValues = ExtractAccessControlValues(response.Headers()); - auto withCredentials = - response.RequestMessage().Properties().Lookup(L"RequestArgs").try_as()->WithCredentials; - - if (GetRuntimeOptionBool("Http.StrictOriginCheckSimpleCors") && - originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { - bool originAllowed = false; - for (const auto& header : response.Headers()) { - if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { - originAllowed |= L"*" == header.Value() || s_origin == Uri{ header.Value() }; - } - } - } + HttpRequestMessage preflightRequest; - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { - // Filter out response headers that are not in the Simple CORS allowlist - std::queue nonSimpleNames; - for (const auto& header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == s_simpleCorsResponseHeaderNames.cend()) - nonSimpleNames.push(header.Key()); - } + // Section 4.8.2 https://fetch.spec.whatwg.org/#cors-preflight-fetch + preflightRequest.Method(HttpMethod::Options()); + preflightRequest.RequestUri(coRequest.RequestUri()); + preflightRequest.Headers().Insert(L"Accept", L"*/*"); + preflightRequest.Headers().Insert(L"Access-Control-Request-Method", coRequest.Method().ToString()); - // Anchors (^$) can't be part of bracket expressions ([]). - // Create 3 matching groups: 1. Beginning of string 2. Between delimiters 3. End of string - const std::wregex re(L"(^HttpOnly\\s*;)|(;\\s*HttpOnly\\s*;)|(;\\s*HttpOnly$)", std::regex_constants::icase); - if (!std::regex_search(header.Value().c_str(), re)) - continue; + auto headerNames = wstring{}; + auto writeSeparator = false; + for (const auto &header : coRequest.Headers()) { + if (writeSeparator) { + headerNames += L", "; + } else { + writeSeparator = true; + } - // HttpOnly cookie detected. Removing. - httpOnlyCookies.push(header.Key()); - } // const auto &header : response.Headers() + headerNames += header.Key(); + } - while (!httpOnlyCookies.empty()) { - response.Headers().Remove(httpOnlyCookies.front()); - httpOnlyCookies.pop(); - } + if (coRequest.Content()) { + for (const auto &header : coRequest.Content().Headers()) { + if (writeSeparator) { + headerNames += L", "; + } else { + writeSeparator = true; } - OriginPolicyHttpFilter::OriginPolicyHttpFilter(IHttpFilter const& innerFilter): m_innerFilter{ innerFilter } {} - - OriginPolicyHttpFilter::OriginPolicyHttpFilter() - : OriginPolicyHttpFilter(winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter{}) {} - - OriginPolicy OriginPolicyHttpFilter::ValidateRequest(HttpRequestMessage const& request) { - auto effectiveOriginPolicy = - static_cast(request.Properties().Lookup(L"OriginPolicy").as().GetUInt64()); - switch (effectiveOriginPolicy) { - case OriginPolicy::None: - return effectiveOriginPolicy; - - case OriginPolicy::SameOrigin: - if (!IsSameOrigin(s_origin, request.RequestUri())) - throw hresult_error{ E_INVALIDARG, L"SOP (same-origin policy) is enforced" }; - break; - - case OriginPolicy::SimpleCrossOriginResourceSharing: - // Check for disallowed mixed content - if (GetRuntimeOptionBool("Http.BlockMixedContentSimpleCors") && - s_origin.SchemeName() != request.RequestUri().SchemeName()) - throw hresult_error{ E_INVALIDARG, L"The origin and request URLs must have the same scheme" }; - - if (IsSameOrigin(s_origin, request.RequestUri())) - // Same origin. Therefore, skip Cross-Origin handling. - effectiveOriginPolicy = OriginPolicy::SameOrigin; - else if (!IsSimpleCorsRequest(request)) - throw hresult_error{ - E_INVALIDARG, - L"The request does not meet the requirements for Same-Origin policy or Simple Cross-Origin resource sharing" }; - break; - - case OriginPolicy::CrossOriginResourceSharing: - // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests - // Refer to CorsURLLoaderFactory::IsValidRequest in chrome\src\services\network\cors\cors_url_loader_factory.cc. - // Forbidden headers should be blocked regardless of origins. - // Example: On the Edge browser, an XHR request with the "Host" header set gets rejected as unsafe. - // https://fetch.spec.whatwg.org/#forbidden-header-name - - if (s_origin.SchemeName() != request.RequestUri().SchemeName()) - throw hresult_error{ E_INVALIDARG, L"The origin and request URLs must have the same scheme" }; - - if (!AreSafeRequestHeaders(request.Headers())) - throw hresult_error{ E_INVALIDARG, L"Request header not allowed in cross-origin resource sharing" }; - - if (s_forbiddenMethods.find(request.Method().ToString().c_str()) != s_forbiddenMethods.cend()) - throw hresult_error{ E_INVALIDARG, L"Request method not allowed in cross-origin resource sharing" }; - - if (IsSameOrigin(s_origin, request.RequestUri())) - effectiveOriginPolicy = OriginPolicy::SameOrigin; - else if (IsSimpleCorsRequest(request)) - effectiveOriginPolicy = OriginPolicy::SimpleCrossOriginResourceSharing; - else - effectiveOriginPolicy = OriginPolicy::CrossOriginResourceSharing; - - break; - - default: - throw hresult_error{ - E_INVALIDARG, L"Invalid OriginPolicy type: " + to_hstring(static_cast(effectiveOriginPolicy)) }; - } + headerNames += header.Key(); + } + } - return effectiveOriginPolicy; - } + preflightRequest.Headers().Insert(L"Access-Control-Request-Headers", headerNames); + preflightRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); + preflightRequest.Headers().Insert(L"Sec-Fetch-Mode", L"CORS"); - // See https://fetch.spec.whatwg.org/#cors-check - void OriginPolicyHttpFilter::ValidateAllowOrigin( - hstring const& allowedOrigin, hstring const& allowCredentials, 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" }; - - // See https://fetch.spec.whatwg.org/#cors-check - void OriginPolicyHttpFilter::ValidateAllowOrigin( - hstring const& allowedOrigin, hstring const& allowCredentials, 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 = 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 - 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"; - - // A wildcard Access-Control-Allow-Origin can not be used if credentials are to be sent, - // even with Access-Control-Allow-Credentials set to true - // See https://fetch.spec.whatwg.org/#cors-protocol-and-credentials - else if (L"*" == allowedOrigin) - errorMessage = - L"Response header Access-Control-Allow-Origin can not have a wildcard value when the request includes credentials"; - - else - errorMessage = L"The Access-Control-Allow-Origin header has a value of [" + allowedOrigin + - L"] which differs from the supplied origin"; - - throw hresult_error{ E_INVALIDARG, errorMessage }; - } + co_return {co_await m_innerFilter.SendRequestAsync(preflightRequest)}; +} - // 4.10.5 - if (!withCredentials) - return; - - // 4.10.6-8 - // https://fetch.spec.whatwg.org/#http-access-control-allow-credentials - // This check should be case sensitive. - // See also https://fetch.spec.whatwg.org/#http-new-header-syntax - if (L"true" != allowCredentials) - throw hresult_error{ - E_INVALIDARG, - L"Access-Control-Allow-Credentials value must be \"true\" when the response includes credentials" }; - }; - - void OriginPolicyHttpFilter::ValidatePreflightResponse( - HttpRequestMessage const& request, HttpResponseMessage const& response) const { - // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSExternalRedirectNotAllowed - using winrt::Windows::Web::Http::HttpStatusCode; - switch (response.StatusCode()) { - case HttpStatusCode::MovedPermanently: - case HttpStatusCode::TemporaryRedirect: - case HttpStatusCode::PermanentRedirect: - throw hresult_error{ INET_E_REDIRECTING, L"Redirect is not allowed in a preflight request" }; - - default: - break; - } +#pragma region IRedirectEventSource - auto controlValues = ExtractAccessControlValues(response.Headers()); - - 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, props); - - // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.5 - // Check if the request method is allowed - bool withCredentials = props.Lookup(L"RequestArgs").as()->WithCredentials; - bool requestMethodAllowed = false; - for (const auto& method : controlValues.AllowedMethods) { - if (L"*" == method) { - if (!withCredentials) { - requestMethodAllowed = true; - break; - } - } - - // Preflight should always allow simple CORS methods - requestMethodAllowed |= - s_simpleCorsMethods.find(request.Method().ToString().c_str()) != s_simpleCorsMethods.cend(); - - if (!requestMethodAllowed) - throw hresult_error{ - E_INVALIDARG, - L"Method [" + request.Method().ToString() + - L"] is not allowed by Access-Control-Allow-Methods in preflight response" }; - - // Check if request headers are allowed - // See https://fetch.spec.whatwg.org/#cors-preflight-fetch, section 4.8.7.6-7 - // Check if the header should be allowed through wildcard, if the request does not have credentials. - bool requestHeadersAllowed = false; - if (!withCredentials && controlValues.AllowedHeaders.find(L"*") != controlValues.AllowedHeaders.cend()) { - // "Authorization" header cannot be allowed through wildcard alone. - // "Authorization" is the only member of https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name. - if (request.Headers().HasKey(L"Authorization") && - controlValues.AllowedHeaders.find(L"Authorization") == controlValues.AllowedHeaders.cend()) - throw hresult_error{ - E_INVALIDARG, - L"Request header field [Authorization] is not allowed by Access-Control-Allow-Headers in preflight response" }; - - requestHeadersAllowed = true; - } - if (!requestHeadersAllowed) { - // Forbidden headers are excluded from the JavaScript layer. - // User agents may use these headers internally. - const set unsafeNotForbidenHeaderNames = CorsUnsafeNotForbiddenRequestHeaderNames(request.Headers()); - for (const auto name : unsafeNotForbidenHeaderNames) { - if (controlValues.AllowedHeaders.find(name) == controlValues.AllowedHeaders.cend()) - throw hresult_error{ - E_INVALIDARG, - L"Request header field [" + to_hstring(name) + - L"] is not allowed by Access-Control-Allow-Headers in preflight response" }; - } - } - - // When withCredentials is false, request cannot include cookies. Also, cookies will be ignored in responses. - removeAllCookies = !withCredentials && GetRuntimeOptionBool("Http.RemoveCookiesFromResponse"); - } // originPolicy == SimpleCrossOriginResourceSharing || CrossOriginResourceSharing - - // Don't expose HttpOnly cookies to JavaScript - RemoveHttpOnlyCookiesFromResponseHeaders(response, removeAllCookies); - } +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; + } + } - ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage const& request) const { - auto coRequest = request; + return true; +} - HttpRequestMessage preflightRequest; +#pragma endregion IRedirectEventSource - // Section 4.8.2 https://fetch.spec.whatwg.org/#cors-preflight-fetch - preflightRequest.Method(HttpMethod::Options()); - preflightRequest.RequestUri(coRequest.RequestUri()); - preflightRequest.Headers().Insert(L"Accept", L"*/*"); - preflightRequest.Headers().Insert(L"Access-Control-Request-Method", coRequest.Method().ToString()); +#pragma region IHttpFilter - auto headerNames = wstring{}; - auto writeSeparator = false; - for (const auto& header : coRequest.Headers()) { - if (writeSeparator) { - headerNames += L", "; - } - else { - writeSeparator = true; - } +ResponseOperation OriginPolicyHttpFilter::SendRequestAsync(HttpRequestMessage const &request) { + auto coRequest = request; - headerNames += header.Key(); - } + // Set initial origin policy to global runtime option. + request.Properties().Insert(L"OriginPolicy", winrt::box_value(GetRuntimeOptionInt("Http.OriginPolicy"))); - if (coRequest.Content()) { - for (const auto& header : coRequest.Content().Headers()) { - if (writeSeparator) { - headerNames += L", "; - } - else { - writeSeparator = true; - } - - // See 10.7.4 of https://fetch.spec.whatwg.org/#http-network-or-cache-fetch - void OriginPolicyHttpFilter::ValidateResponse( - HttpResponseMessage const& response, const OriginPolicy originPolicy) const { - bool removeAllCookies = false; - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || - originPolicy == OriginPolicy::CrossOriginResourceSharing) { - auto controlValues = ExtractAccessControlValues(response.Headers()); - auto props = response.RequestMessage().Properties(); - auto withCredentials = props.Lookup(L"RequestArgs").try_as()->WithCredentials; - - if (GetRuntimeOptionBool("Http.StrictOriginCheckSimpleCors") && - originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { - bool originAllowed = false; - for (const auto& header : response.Headers()) { - if (boost::iequals(header.Key(), L"Access-Control-Allow-Origin")) { - originAllowed |= L"*" == header.Value() || s_origin == Uri{ header.Value() }; - } - } - - if (!originAllowed) { - throw hresult_error{ E_INVALIDARG, L"The server does not support CORS or the origin is not allowed" }; - } - } - else { - ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props); - } - - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { - // Filter out response headers that are not in the Simple CORS allowlist - std::queue nonSimpleNames; - for (const auto& header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == - s_simpleCorsResponseHeaderNames.cend()) - nonSimpleNames.push(header.Key()); - } - - while (!nonSimpleNames.empty()) { - response.Headers().Remove(nonSimpleNames.front()); - nonSimpleNames.pop(); - } - } - else { - // Filter out response headers that are not simple headers and not in expose list - - // Keep simple headers and those found in the expose header list. - if (withCredentials || controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { - std::queue nonSimpleNonExposedHeaders; - - for (const auto& header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == - s_simpleCorsResponseHeaderNames.cend() && - controlValues.ExposedHeaders.find(header.Key().c_str()) == - controlValues.ExposedHeaders.cend()) { - nonSimpleNonExposedHeaders.push(header.Key()); - } - } - - if (!originAllowed) { - throw hresult_error{ E_INVALIDARG, L"The server does not support CORS or the origin is not allowed" }; - } - } - else { - ValidateAllowOrigin(controlValues.AllowedOrigin, controlValues.AllowedCredentials, props); - } - - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing) { - // Filter out response headers that are not in the Simple CORS whitelist - std::queue nonSimpleNames; - for (const auto& header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == - s_simpleCorsResponseHeaderNames.cend()) - nonSimpleNames.push(header.Key()); - } - - while (!nonSimpleNames.empty()) { - response.Headers().Remove(nonSimpleNames.front()); - nonSimpleNames.pop(); - } - } - else { - // Filter out response headers that are not simple headers and not in expose list - - // Keep simple headers and those found in the expose header list. - if (withCredentials || - controlValues.ExposedHeaders.find(L"*") == controlValues.ExposedHeaders.cend()) { - std::queue nonSimpleNonExposedHeaders; - - for (const auto& header : response.Headers().GetView()) { - if (s_simpleCorsResponseHeaderNames.find(header.Key().c_str()) == - s_simpleCorsResponseHeaderNames.cend() && - controlValues.ExposedHeaders.find(header.Key().c_str()) == - controlValues.ExposedHeaders.cend()) { - nonSimpleNonExposedHeaders.push(header.Key()); - } - } - - while (!nonSimpleNonExposedHeaders.empty()) { - response.Headers().Remove(nonSimpleNonExposedHeaders.front()); - nonSimpleNonExposedHeaders.pop(); - } - } - } - - // When withCredentials is false, request cannot include cookies. Also, cookies will be ignored in - // responses. - removeAllCookies = !withCredentials && GetRuntimeOptionBool("Http.RemoveCookiesFromResponse"); - } // originPolicy == SimpleCrossOriginResourceSharing || CrossOriginResourceSharing - - // Don't expose HttpOnly cookies to JavaScript - RemoveHttpOnlyCookiesFromResponseHeaders(response, removeAllCookies); - } - - ResponseOperation OriginPolicyHttpFilter::SendPreflightAsync(HttpRequestMessage const& request) const { - auto coRequest = request; - - HttpRequestMessage preflightRequest; - - // Section 4.8.2 https://fetch.spec.whatwg.org/#cors-preflight-fetch - preflightRequest.Method(HttpMethod::Options()); - preflightRequest.RequestUri(coRequest.RequestUri()); - preflightRequest.Headers().Insert(L"Accept", L"*/*"); - preflightRequest.Headers().Insert(L"Access-Control-Request-Method", coRequest.Method().ToString()); - - auto headerNames = wstring{}; - auto headerItr = coRequest.Headers().begin(); - if (headerItr != coRequest.Headers().end()) { - headerNames += (*headerItr).Key(); - - while (++headerItr != coRequest.Headers().end()) - headerNames += L", " + (*headerItr).Key(); - } - - if (coRequest.Content()) { - headerItr = coRequest.Content().Headers().begin(); - if (headerItr != coRequest.Content().Headers().end()) { - headerNames += (*headerItr).Key(); - - while (++headerItr != coRequest.Content().Headers().end()) - headerNames += L", " + (*headerItr).Key(); - } - } - - preflightRequest.Headers().Insert(L"Access-Control-Request-Headers", headerNames); - preflightRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); - preflightRequest.Headers().Insert(L"Sec-Fetch-Mode", L"CORS"); - - co_return{ co_await m_innerFilter.SendRequestAsync(preflightRequest) }; - } + // Allow only HTTP or HTTPS schemes + if (GetRuntimeOptionBool("Http.StrictScheme") && coRequest.RequestUri().SchemeName() != L"https" && + coRequest.RequestUri().SchemeName() != L"http") + throw hresult_error{E_INVALIDARG, L"Invalid URL scheme: [" + s_origin.SchemeName() + L"]"}; -#pragma region IRedirectEventSource + if (!GetRuntimeOptionBool("Http.OmitCredentials")) { + coRequest.Properties().Lookup(L"RequestArgs").as()->WithCredentials = false; + } - 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; - } + // Ensure absolute URL + coRequest.RequestUri(Uri{coRequest.RequestUri().AbsoluteCanonicalUri()}); -#pragma endregion IRedirectEventSource + auto originPolicy = ValidateRequest(coRequest); + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + originPolicy == OriginPolicy::CrossOriginResourceSharing) { + if (coRequest.RequestUri().UserName().size() > 0 || coRequest.RequestUri().Password().size() > 0) { + coRequest.RequestUri(Uri{coRequest.RequestUri().DisplayUri()}); + } + } -#pragma region IRedirectEventSource + try { + // #9770 - Validate preflight cache + if (originPolicy == OriginPolicy::CrossOriginResourceSharing) { + // If inner filter can AllowRedirect, disable for preflight. + winrt::impl::com_ref baseFilter; + if (baseFilter = m_innerFilter.try_as()) { + baseFilter.AllowAutoRedirect(false); + } - 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; - } + auto preflightResponse = co_await SendPreflightAsync(coRequest); -#pragma endregion IRedirectEventSource + if (baseFilter) { + baseFilter.AllowAutoRedirect(true); + } -#pragma region IHttpFilter + ValidatePreflightResponse(coRequest, preflightResponse); + } + + if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || + originPolicy == OriginPolicy::CrossOriginResourceSharing) { + coRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); + } - ResponseOperation OriginPolicyHttpFilter::SendRequestAsync(HttpRequestMessage const& request) { - auto coRequest = request; - - // Set initial origin policy to global runtime option. - request.Properties().Insert(L"OriginPolicy", winrt::box_value(GetRuntimeOptionInt("Http.OriginPolicy"))); - - // Allow only HTTP or HTTPS schemes - if (GetRuntimeOptionBool("Http.StrictScheme") && coRequest.RequestUri().SchemeName() != L"https" && - coRequest.RequestUri().SchemeName() != L"http") - throw hresult_error{ E_INVALIDARG, L"Invalid URL scheme: [" + s_origin.SchemeName() + L"]" }; - - if (!GetRuntimeOptionBool("Http.OmitCredentials")) { - coRequest.Properties().Lookup(L"RequestArgs").as()->WithCredentials = false; - } - - // Ensure absolute URL - coRequest.RequestUri(Uri{ coRequest.RequestUri().AbsoluteCanonicalUri() }); - - auto originPolicy = ValidateRequest(coRequest); - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || - originPolicy == OriginPolicy::CrossOriginResourceSharing) { - if (coRequest.RequestUri().UserName().size() > 0 || coRequest.RequestUri().Password().size() > 0) { - coRequest.RequestUri(Uri{ coRequest.RequestUri().DisplayUri() }); - } - } - - try { - // #9770 - Validate preflight cache - if (originPolicy == OriginPolicy::CrossOriginResourceSharing) { - // If inner filter can AllowRedirect, disable for preflight. - winrt::impl::com_ref baseFilter; - if (baseFilter = m_innerFilter.try_as()) { - baseFilter.AllowAutoRedirect(false); - } - - auto preflightResponse = co_await SendPreflightAsync(coRequest); - - if (baseFilter) { - baseFilter.AllowAutoRedirect(true); - } - - ValidatePreflightResponse(coRequest, preflightResponse); - } - - if (originPolicy == OriginPolicy::SimpleCrossOriginResourceSharing || - originPolicy == OriginPolicy::CrossOriginResourceSharing) { - coRequest.Headers().Insert(L"Origin", s_origin.AbsoluteCanonicalUri()); - } - - auto response = co_await m_innerFilter.SendRequestAsync(coRequest); - - ValidateResponse(response, originPolicy); - - co_return response; - - } - catch (hresult_error const& e) { - throw e; - } - catch (const std::exception& e) { - throw hresult_error{ E_FAIL, to_hstring(e.what()) }; - } - catch (...) { - throw hresult_error{ E_FAIL, L"Unspecified error processing Origin Policy request" }; - } - } + auto response = co_await m_innerFilter.SendRequestAsync(coRequest); + + ValidateResponse(response, originPolicy); + + co_return response; + + } catch (hresult_error const &e) { + throw e; + } catch (const std::exception &e) { + throw hresult_error{E_FAIL, to_hstring(e.what())}; + } catch (...) { + throw hresult_error{E_FAIL, L"Unspecified error processing Origin Policy request"}; + } +} #pragma endregion IHttpFilter #pragma endregion OriginPolicyHttpFilter - } // namespace Microsoft::React::Networking +} // namespace Microsoft::React::Networking From 9041bf6fb144c7f2931b7fdd39ded8a4aa21e487 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Wed, 7 Dec 2022 03:34:02 -0800 Subject: [PATCH 38/45] Fix FileReader --- vnext/Shared/Modules/FileReaderModule.cpp | 63 +---------------------- 1 file changed, 1 insertion(+), 62 deletions(-) diff --git a/vnext/Shared/Modules/FileReaderModule.cpp b/vnext/Shared/Modules/FileReaderModule.cpp index 4da7f20902e..67cb040b52e 100644 --- a/vnext/Shared/Modules/FileReaderModule.cpp +++ b/vnext/Shared/Modules/FileReaderModule.cpp @@ -4,6 +4,7 @@ #include "FileReaderModule.h" #include +#include // Boost Library #include @@ -71,68 +72,6 @@ std::vector FileReaderModule::getMethods() { auto offset = blob["offset"].asInt(); auto size = blob["size"].asInt(); - winrt::array_view bytes; - try { - bytes = blobPersistor->ResolveMessage(std::move(blobId), offset, size); - } catch (const std::exception &e) { - return reject({e.what()}); - } - - auto result = string{"data:"}; - auto typeItr = blob.find("type"); - if (typeItr == blob.items().end()) { - result += "application/octet-stream"; - } else { - result += (*typeItr).second.asString(); - } - result += ";base64,"; - - // https://www.boost.org/doc/libs/1_76_0/libs/serialization/doc/dataflow.html - using namespace boost::archive::iterators; - typedef base64_from_binary> encode_base64; - std::ostringstream oss; - std::copy(encode_base64(bytes.cbegin()), encode_base64(bytes.cend()), ostream_iterator(oss)); - result += oss.str(); - - resolve({std::move(result)}); - }}, - {/// - /// - /// Array of arguments passed from the JavaScript layer. - /// [0] - dynamic blob object { blobId, offset, size } - /// [1] - string encoding - /// - /// - "readAsText", - [blobPersistor = m_weakBlobPersistor.lock()](dynamic args, Callback resolve, Callback reject) { - if (!blobPersistor) { - return reject({"Could not find Blob persistor"}); - } - - auto blob = jsArgAsObject(args, 0); - auto encoding = jsArgAsString(args, 1); // Default: "UTF-8" - - auto blobId = blob["blobId"].asString(); - auto offset = blob["offset"].asInt(); - auto size = blob["size"].asInt(); - - winrt::array_view bytes; - try { - bytes = blobPersistor->ResolveMessage(std::move(blobId), offset, size); - } catch (const std::exception &e) { - return reject({e.what()}); - } - - // #9982 - Handle non-UTF8 encodings - // See https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/charset/Charset.html - auto result = string{bytes.cbegin(), bytes.cend()}; - - auto blob = jsArgAsObject(args, 0); - - auto blobId = blob["blobId"].asString(); - auto offset = blob["offset"].asInt(); - auto size = blob["size"].asInt(); - winrt::array_view bytes; try { bytes = blobPersistor->ResolveMessage(std::move(blobId), offset, size); From a3a7b0a22573ec6f7c30e2d168dc83be15651e0a Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Thu, 28 Jul 2022 12:15:36 -0400 Subject: [PATCH 39/45] Adds header to fix clang build in FileReaderModule (#10329) * Adds header to fix clang build in FileReaderModule Adds `#include ` to fix clang compile issue for std::ostringstream. * Change files --- ...ative-windows-1e43cf9b-edff-4998-83e5-74d1fc046cf5.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/react-native-windows-1e43cf9b-edff-4998-83e5-74d1fc046cf5.json diff --git a/change/react-native-windows-1e43cf9b-edff-4998-83e5-74d1fc046cf5.json b/change/react-native-windows-1e43cf9b-edff-4998-83e5-74d1fc046cf5.json new file mode 100644 index 00000000000..b3a409cac5f --- /dev/null +++ b/change/react-native-windows-1e43cf9b-edff-4998-83e5-74d1fc046cf5.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Adds header to fix clang build in FileReaderModule", + "packageName": "react-native-windows", + "email": "erozell@outlook.com", + "dependentChangeType": "patch" +} From c24b4adf9372ecc663bbc42075304bbb16ff7955 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Wed, 7 Dec 2022 03:39:59 -0800 Subject: [PATCH 40/45] Remove change file --- ...ative-windows-1e43cf9b-edff-4998-83e5-74d1fc046cf5.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 change/react-native-windows-1e43cf9b-edff-4998-83e5-74d1fc046cf5.json diff --git a/change/react-native-windows-1e43cf9b-edff-4998-83e5-74d1fc046cf5.json b/change/react-native-windows-1e43cf9b-edff-4998-83e5-74d1fc046cf5.json deleted file mode 100644 index b3a409cac5f..00000000000 --- a/change/react-native-windows-1e43cf9b-edff-4998-83e5-74d1fc046cf5.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "prerelease", - "comment": "Adds header to fix clang build in FileReaderModule", - "packageName": "react-native-windows", - "email": "erozell@outlook.com", - "dependentChangeType": "patch" -} From 99b5db4e85618f1fc9255d5913ac796de3774a9b Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Wed, 7 Dec 2022 03:54:44 -0800 Subject: [PATCH 41/45] Fix blob module --- vnext/Shared/Modules/BlobModule.cpp | 495 ++++++++++++---------------- 1 file changed, 208 insertions(+), 287 deletions(-) diff --git a/vnext/Shared/Modules/BlobModule.cpp b/vnext/Shared/Modules/BlobModule.cpp index 68803d03d4a..867e45c0193 100644 --- a/vnext/Shared/Modules/BlobModule.cpp +++ b/vnext/Shared/Modules/BlobModule.cpp @@ -91,200 +91,122 @@ std::map BlobModule::getConstants() { vector BlobModule::getMethods() { return { - {"addNetworkingHandler", - [propBag = ReactPropertyBag{m_inspectableProperties.try_as()}, - requestBodyHandler = m_requestBodyHandler, - responseHandler = m_responseHandler](dynamic args) { - auto propId = ReactPropertyId>>{L"HttpModule.Proxy"}; - - if (auto prop = propBag.Get(propId)) { - if (auto httpHandler = prop.Value().lock()) { - httpHandler->AddRequestBodyHandler(requestBodyHandler); - httpHandler->AddResponseHandler(responseHandler); - } - } - // TODO: else emit error? - }}, - - {"addWebSocketHandler", - [contentHandler = m_contentHandler](dynamic args) { - auto id = jsArgAsInt(args, 0); - - contentHandler->Register(id); - }}, - - {"removeWebSocketHandler", - [contentHandler = m_contentHandler](dynamic args) { - auto id = jsArgAsInt(args, 0); - - contentHandler->Unregister(id); - }}, - - {"sendOverSocket", - [weakState = weak_ptr(m_sharedState), - persistor = m_blobPersistor, - propBag = ReactPropertyBag{m_inspectableProperties.try_as()}](dynamic args) { - auto propId = ReactPropertyId>>{L"WebSocketModule.Proxy"}; - shared_ptr wsProxy; - if (auto prop = propBag.Get(propId)) { - wsProxy = prop.Value().lock(); - } - if (!wsProxy) { - return; + {"addNetworkingHandler", + [propBag = ReactPropertyBag{m_inspectableProperties.try_as()}, + requestBodyHandler = m_requestBodyHandler, + responseHandler = m_responseHandler](dynamic args) { + auto propId = ReactPropertyId>>{L"HttpModule.Proxy"}; + + if (auto prop = propBag.Get(propId)) { + if (auto httpHandler = prop.Value().lock()) { + httpHandler->AddRequestBodyHandler(requestBodyHandler); + httpHandler->AddResponseHandler(responseHandler); } + } + // TODO: else emit error? + }}, + + {"addWebSocketHandler", + [contentHandler = m_contentHandler](dynamic args) { + auto id = jsArgAsInt(args, 0); + + contentHandler->Register(id); + }}, + + {"removeWebSocketHandler", + [contentHandler = m_contentHandler](dynamic args) { + auto id = jsArgAsInt(args, 0); + + contentHandler->Unregister(id); + }}, + + {"sendOverSocket", + [weakState = weak_ptr(m_sharedState), + persistor = m_blobPersistor, + propBag = ReactPropertyBag{m_inspectableProperties.try_as()}](dynamic args) { + auto propId = ReactPropertyId>>{L"WebSocketModule.Proxy"}; + shared_ptr wsProxy; + if (auto prop = propBag.Get(propId)) { + wsProxy = prop.Value().lock(); + } + if (!wsProxy) { + return; + } - auto blob = jsArgAsObject(args, 0); - auto blobId = blob[blobIdKey].getString(); - auto offset = blob[offsetKey].getInt(); - auto size = blob[sizeKey].getInt(); - auto socketID = jsArgAsInt(args, 1); - - winrt::array_view data; - try { - data = persistor->ResolveMessage(std::move(blobId), offset, size); - } catch (const std::exception &e) { - if (auto sharedState = weakState.lock()) { - Modules::SendEvent(sharedState->Module->getInstance(), "blobFailed", e.what()); - } - return; + auto blob = jsArgAsObject(args, 0); + auto blobId = blob[blobIdKey].getString(); + auto offset = blob[offsetKey].getInt(); + auto size = blob[sizeKey].getInt(); + auto socketID = jsArgAsInt(args, 1); + + winrt::array_view data; + try { + data = persistor->ResolveMessage(std::move(blobId), offset, size); + } catch (const std::exception &e) { + if (auto sharedState = weakState.lock()) { + Modules::SendEvent(sharedState->Module->getInstance(), "blobFailed", e.what()); } + return; + } - auto buffer = CryptographicBuffer::CreateFromByteArray(data); - auto winrtString = CryptographicBuffer::EncodeToBase64String(std::move(buffer)); - auto base64String = Common::Unicode::Utf16ToUtf8(std::move(winrtString)); - - wsProxy->SendBinary(std::move(base64String), socketID); - }}, - - {"createFromParts", - // As of React Native 0.67, instance is set AFTER CxxModule::getMethods() is invoked. - // Use getInstance() directly once - // https://github.com/facebook/react-native/commit/1d45b20b6c6ba66df0485cdb9be36463d96cf182 becomes available. - [persistor = m_blobPersistor, weakState = weak_ptr(m_sharedState)](dynamic args) { - auto parts = jsArgAsArray(args, 0); // Array - auto blobId = jsArgAsString(args, 1); - vector buffer{}; - - for (const auto &part : parts) { - auto type = part[typeKey].asString(); - if (blobKey == type) { - auto blob = part[dataKey]; - winrt::array_view bufferPart; - try { - bufferPart = persistor->ResolveMessage( - blob[blobIdKey].asString(), blob[offsetKey].asInt(), blob[sizeKey].asInt()); - } catch (const std::exception &e) { - if (auto sharedState = weakState.lock()) { - Modules::SendEvent(sharedState->Module->getInstance(), "blobFailed", e.what()); - } - return; + auto buffer = CryptographicBuffer::CreateFromByteArray(data); + auto winrtString = CryptographicBuffer::EncodeToBase64String(std::move(buffer)); + auto base64String = Common::Unicode::Utf16ToUtf8(std::move(winrtString)); + + wsProxy->SendBinary(std::move(base64String), socketID); + }}, + + {"createFromParts", + // As of React Native 0.67, instance is set AFTER CxxModule::getMethods() is invoked. + // Use getInstance() directly once + // https://github.com/facebook/react-native/commit/1d45b20b6c6ba66df0485cdb9be36463d96cf182 becomes available. + [persistor = m_blobPersistor, weakState = weak_ptr(m_sharedState)](dynamic args) { + auto parts = jsArgAsArray(args, 0); // Array + auto blobId = jsArgAsString(args, 1); + vector buffer{}; + + for (const auto &part : parts) { + auto type = part[typeKey].asString(); + if (blobKey == type) { + auto blob = part[dataKey]; + winrt::array_view bufferPart; + try { + bufferPart = persistor->ResolveMessage( + blob[blobIdKey].asString(), blob[offsetKey].asInt(), blob[sizeKey].asInt()); + } catch (const std::exception &e) { + if (auto sharedState = weakState.lock()) { + Modules::SendEvent(sharedState->Module->getInstance(), "blobFailed", e.what()); } + return; + } + + buffer.reserve(buffer.size() + bufferPart.size()); + buffer.insert(buffer.end(), bufferPart.begin(), bufferPart.end()); + } else if ("string" == type) { + auto data = part[dataKey].asString(); + + buffer.reserve(buffer.size() + data.size()); + buffer.insert(buffer.end(), data.begin(), data.end()); + } else { + if (auto state = weakState.lock()) { + auto message = "Invalid type for blob: " + type; + Modules::SendEvent(state->Module->getInstance(), "blobFailed", std::move(message)); } - // TODO: else emit error? + return; } - }, - - {"addWebSocketHandler", - [contentHandler = m_contentHandler](dynamic args) { - auto id = jsArgAsInt(args, 0); - - contentHandler->Register(id); - }}, - - {"removeWebSocketHandler", - [contentHandler = m_contentHandler](dynamic args) { - auto id = jsArgAsInt(args, 0); - - contentHandler->Unregister(id); - }}, - - {"sendOverSocket", - [weakState = weak_ptr(m_sharedState), - persistor = m_blobPersistor, - propBag = ReactPropertyBag{m_inspectableProperties.try_as()}](dynamic args) { - auto propId = ReactPropertyId>>{L"WebSocketModule.Proxy"}; - shared_ptr wsProxy; - if (auto prop = propBag.Get(propId)) { - wsProxy = prop.Value().lock(); - } - if (!wsProxy) { - return; - } - - auto blob = jsArgAsObject(args, 0); - auto blobId = blob[blobIdKey].getString(); - auto offset = blob[offsetKey].getInt(); - auto size = blob[sizeKey].getInt(); - auto socketID = jsArgAsInt(args, 1); - - winrt::array_view data; - try { - data = persistor->ResolveMessage(std::move(blobId), offset, size); - } catch (const std::exception &e) { - if (auto sharedState = weakState.lock()) { - Modules::SendEvent(sharedState->Module->getInstance(), "blobFailed", e.what()); - } - return; - } - - auto buffer = CryptographicBuffer::CreateFromByteArray(data); - auto winrtString = CryptographicBuffer::EncodeToBase64String(std::move(buffer)); - auto base64String = Common::Unicode::Utf16ToUtf8(std::move(winrtString)); - - wsProxy->SendBinary(std::move(base64String), socketID); - }}, - - {"createFromParts", - // As of React Native 0.67, instance is set AFTER CxxModule::getMethods() is invoked. - // Use getInstance() directly once - // https://github.com/facebook/react-native/commit/1d45b20b6c6ba66df0485cdb9be36463d96cf182 becomes available. - [persistor = m_blobPersistor, weakState = weak_ptr(m_sharedState)](dynamic args) { - auto parts = jsArgAsArray(args, 0); // Array - auto blobId = jsArgAsString(args, 1); - vector buffer{}; - - for (const auto &part : parts) { - auto type = part[typeKey].asString(); - if (blobKey == type) { - auto blob = part[dataKey]; - winrt::array_view bufferPart; - try { - bufferPart = persistor->ResolveMessage( - blob[blobIdKey].asString(), blob[offsetKey].asInt(), blob[sizeKey].asInt()); - } catch (const std::exception &e) { - if (auto sharedState = weakState.lock()) { - Modules::SendEvent(sharedState->Module->getInstance(), "blobFailed", e.what()); - } - return; - } - - buffer.reserve(buffer.size() + bufferPart.size()); - buffer.insert(buffer.end(), bufferPart.begin(), bufferPart.end()); - } else if ("string" == type) { - auto data = part[dataKey].asString(); - - buffer.reserve(buffer.size() + data.size()); - buffer.insert(buffer.end(), data.begin(), data.end()); - } else { - if (auto state = weakState.lock()) { - auto message = "Invalid type for blob: " + type; - Modules::SendEvent(state->Module->getInstance(), "blobFailed", std::move(message)); - } - return; - } - } - - persistor->StoreMessage(std::move(buffer), std::move(blobId)); - }}, - - {"release", - [persistor = m_blobPersistor](dynamic args) // blobId: string - { - auto blobId = jsArgAsString(args, 0); - - persistor->RemoveMessage(std::move(blobId)); - }}}; - } + } + + persistor->StoreMessage(std::move(buffer), std::move(blobId)); + }}, + + {"release", + [persistor = m_blobPersistor](dynamic args) // blobId: string + { + auto blobId = jsArgAsString(args, 0); + + persistor->RemoveMessage(std::move(blobId)); + }}}; +} #pragma endregion CxxModule @@ -294,47 +216,47 @@ vector BlobModule::getMethods() { #pragma region IBlobPersistor - winrt::array_view MemoryBlobPersistor::ResolveMessage(string && blobId, int64_t offset, int64_t size) { - if (size < 1) - return {}; +winrt::array_view MemoryBlobPersistor::ResolveMessage(string &&blobId, int64_t offset, int64_t size) { + if (size < 1) + return {}; - scoped_lock lock{m_mutex}; + scoped_lock lock{m_mutex}; - auto dataItr = m_blobs.find(std::move(blobId)); - // Not found. - if (dataItr == m_blobs.cend()) - throw std::invalid_argument("Blob object not found"); + auto dataItr = m_blobs.find(std::move(blobId)); + // Not found. + if (dataItr == m_blobs.cend()) + throw std::invalid_argument("Blob object not found"); - auto &bytes = (*dataItr).second; - auto endBound = static_cast(offset + size); - // Out of bounds. - if (endBound > bytes.size() || offset >= static_cast(bytes.size()) || offset < 0) - throw std::out_of_range("Offset or size out of range"); + auto &bytes = (*dataItr).second; + auto endBound = static_cast(offset + size); + // Out of bounds. + 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 { - scoped_lock lock{m_mutex}; +void MemoryBlobPersistor::RemoveMessage(string &&blobId) noexcept { + scoped_lock lock{m_mutex}; - m_blobs.erase(std::move(blobId)); - } + m_blobs.erase(std::move(blobId)); +} - void MemoryBlobPersistor::StoreMessage(vector && message, string && blobId) noexcept { - scoped_lock lock{m_mutex}; +void MemoryBlobPersistor::StoreMessage(vector &&message, string &&blobId) noexcept { + scoped_lock lock{m_mutex}; - m_blobs.insert_or_assign(std::move(blobId), std::move(message)); - } + m_blobs.insert_or_assign(std::move(blobId), std::move(message)); +} - string MemoryBlobPersistor::StoreMessage(vector && message) noexcept { - // substr(1, 36) strips curly braces from a GUID. - auto blobId = winrt::to_string(winrt::to_hstring(GuidHelper::CreateNewGuid())).substr(1, 36); +string MemoryBlobPersistor::StoreMessage(vector &&message) noexcept { + // substr(1, 36) strips curly braces from a GUID. + auto blobId = winrt::to_string(winrt::to_hstring(GuidHelper::CreateNewGuid())).substr(1, 36); - scoped_lock lock{m_mutex}; - m_blobs.insert_or_assign(blobId, std::move(message)); + scoped_lock lock{m_mutex}; + m_blobs.insert_or_assign(blobId, std::move(message)); - return blobId; - } + return blobId; +} #pragma endregion IBlobPersistor @@ -342,114 +264,113 @@ vector BlobModule::getMethods() { #pragma region BlobWebSocketModuleContentHandler - BlobWebSocketModuleContentHandler::BlobWebSocketModuleContentHandler( - shared_ptr blobPersistor) noexcept - : m_blobPersistor{blobPersistor} {} +BlobWebSocketModuleContentHandler::BlobWebSocketModuleContentHandler(shared_ptr blobPersistor) noexcept + : m_blobPersistor{blobPersistor} {} #pragma region IWebSocketModuleContentHandler - void BlobWebSocketModuleContentHandler::ProcessMessage(string && message, dynamic & params) /*override*/ { - params[dataKey] = std::move(message); - } +void BlobWebSocketModuleContentHandler::ProcessMessage(string &&message, dynamic ¶ms) /*override*/ { + params[dataKey] = std::move(message); +} - void BlobWebSocketModuleContentHandler::ProcessMessage(vector && message, dynamic & params) /*override*/ { - auto blob = dynamic::object(); - blob(offsetKey, 0); - blob(sizeKey, message.size()); - blob(blobIdKey, m_blobPersistor->StoreMessage(std::move(message))); +void BlobWebSocketModuleContentHandler::ProcessMessage(vector &&message, dynamic ¶ms) /*override*/ { + auto blob = dynamic::object(); + blob(offsetKey, 0); + blob(sizeKey, message.size()); + blob(blobIdKey, m_blobPersistor->StoreMessage(std::move(message))); - params[dataKey] = std::move(blob); - params[typeKey] = blobKey; - } + params[dataKey] = std::move(blob); + params[typeKey] = blobKey; +} #pragma endregion IWebSocketModuleContentHandler - void BlobWebSocketModuleContentHandler::Register(int64_t socketID) noexcept { - scoped_lock lock{m_mutex}; - m_socketIds.insert(socketID); - } +void BlobWebSocketModuleContentHandler::Register(int64_t socketID) noexcept { + scoped_lock lock{m_mutex}; + m_socketIds.insert(socketID); +} - void BlobWebSocketModuleContentHandler::Unregister(int64_t socketID) noexcept { - scoped_lock lock{m_mutex}; +void BlobWebSocketModuleContentHandler::Unregister(int64_t socketID) noexcept { + scoped_lock lock{m_mutex}; - auto itr = m_socketIds.find(socketID); - if (itr != m_socketIds.end()) - m_socketIds.erase(itr); - } + auto itr = m_socketIds.find(socketID); + if (itr != m_socketIds.end()) + m_socketIds.erase(itr); +} #pragma endregion BlobWebSocketModuleContentHandler #pragma region BlobModuleRequestBodyHandler - BlobModuleRequestBodyHandler::BlobModuleRequestBodyHandler(shared_ptr blobPersistor) noexcept - : m_blobPersistor{blobPersistor} {} +BlobModuleRequestBodyHandler::BlobModuleRequestBodyHandler(shared_ptr blobPersistor) noexcept + : m_blobPersistor{blobPersistor} {} #pragma region IRequestBodyHandler - bool BlobModuleRequestBodyHandler::Supports(dynamic & data) /*override*/ { - auto itr = data.find(blobKey); +bool BlobModuleRequestBodyHandler::Supports(dynamic &data) /*override*/ { + auto itr = data.find(blobKey); - return itr != data.items().end() && !(*itr).second.empty(); - } + return itr != data.items().end() && !(*itr).second.empty(); +} - dynamic BlobModuleRequestBodyHandler::ToRequestBody(dynamic & data, string & contentType) /*override*/ { - auto type = contentType; - if (!data[typeKey].isNull() && !data[typeKey].asString().empty()) { - type = data[typeKey].asString(); - } - if (type.empty()) { - type = "application/octet-stream"; - } - - auto blob = data[blobKey]; - auto blobId = blob[blobIdKey].asString(); - auto bytes = m_blobPersistor->ResolveMessage(std::move(blobId), blob[offsetKey].asInt(), blob[sizeKey].asInt()); - - auto result = dynamic::object(); - result(typeKey, type); - result(sizeKey, bytes.size()); - result("bytes", dynamic(bytes.cbegin(), bytes.cend())); - - return result; +dynamic BlobModuleRequestBodyHandler::ToRequestBody(dynamic &data, string &contentType) /*override*/ { + auto type = contentType; + if (!data[typeKey].isNull() && !data[typeKey].asString().empty()) { + type = data[typeKey].asString(); + } + if (type.empty()) { + type = "application/octet-stream"; } + auto blob = data[blobKey]; + auto blobId = blob[blobIdKey].asString(); + auto bytes = m_blobPersistor->ResolveMessage(std::move(blobId), blob[offsetKey].asInt(), blob[sizeKey].asInt()); + + auto result = dynamic::object(); + result(typeKey, type); + result(sizeKey, bytes.size()); + result("bytes", dynamic(bytes.cbegin(), bytes.cend())); + + return result; +} + #pragma endregion IRequestBodyHandler #pragma endregion BlobModuleRequestBodyHandler #pragma region BlobModuleResponseHandler - BlobModuleResponseHandler::BlobModuleResponseHandler(shared_ptr blobPersistor) noexcept - : m_blobPersistor{blobPersistor} {} +BlobModuleResponseHandler::BlobModuleResponseHandler(shared_ptr blobPersistor) noexcept + : m_blobPersistor{blobPersistor} {} #pragma region IResponseHandler - bool BlobModuleResponseHandler::Supports(string & responseType) /*override*/ { - return blobKey == responseType; - } +bool BlobModuleResponseHandler::Supports(string &responseType) /*override*/ { + return blobKey == responseType; +} - dynamic BlobModuleResponseHandler::ToResponseData(vector && content) /*override*/ { - auto blob = dynamic::object(); - blob(offsetKey, 0); - blob(sizeKey, content.size()); - blob(blobIdKey, m_blobPersistor->StoreMessage(std::move(content))); +dynamic BlobModuleResponseHandler::ToResponseData(vector &&content) /*override*/ { + auto blob = dynamic::object(); + blob(offsetKey, 0); + blob(sizeKey, content.size()); + blob(blobIdKey, m_blobPersistor->StoreMessage(std::move(content))); - return blob; - } + return blob; +} #pragma endregion IResponseHandler #pragma endregion BlobModuleResponseHandler - /*extern*/ const char *GetBlobModuleName() noexcept { - return moduleName; - } +/*extern*/ const char *GetBlobModuleName() noexcept { + return moduleName; +} - /*extern*/ std::unique_ptr CreateBlobModule( - IInspectable const &inspectableProperties) noexcept { - if (auto properties = inspectableProperties.try_as()) - return std::make_unique(properties); +/*extern*/ std::unique_ptr CreateBlobModule( + IInspectable const &inspectableProperties) noexcept { + if (auto properties = inspectableProperties.try_as()) + return std::make_unique(properties); - return nullptr; - } + return nullptr; +} } // namespace Microsoft::React From 3a5f561397689520f4e3d5a5ca08dad2c1394658 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Wed, 7 Dec 2022 03:55:59 -0800 Subject: [PATCH 42/45] Fix format --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 4295a933d1f..1e2e92eba7b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,7 +10,7 @@ "**/lib/**/*.js": true, "**/dist/**/*.js": true }, - "editor.formatOnSave": true, + "editor.formatOnSave": false, "eslint.format.enable": true, "eslint.packageManager": "yarn", "eslint.enable": true, From 0916e82dafe069d658c21a5a3a9e5eda1e0c5216 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Wed, 7 Dec 2022 04:02:24 -0800 Subject: [PATCH 43/45] Fix overrides --- packages/@react-native-windows/tester/overrides.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/@react-native-windows/tester/overrides.json b/packages/@react-native-windows/tester/overrides.json index 953622a6d8c..ca338fe1574 100644 --- a/packages/@react-native-windows/tester/overrides.json +++ b/packages/@react-native-windows/tester/overrides.json @@ -14,12 +14,6 @@ "baseHash": "a1d1c6638e815f6dd53504983813fda92f3b5478", "issue": 6341 }, - { - "type": "derived", - "file": "src/js/examples-win/Button/ButtonExample.windows.js", - "baseFile": "packages/rn-tester/js/examples/Button/ButtonExample.js", - "baseHash": "36c55afe75ac9c6efdec22e5ceb1f114a84ebd45" - }, { "type": "platform", "file": "src/js/examples/HTTP/HTTPExample.js" From a13a21c40962b10e562307a12cc4b8660f13006c Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Wed, 7 Dec 2022 04:20:53 -0800 Subject: [PATCH 44/45] Fix HTTPRc --- vnext/Shared/Networking/WinRTHttpResource.cpp | 216 +++++++----------- vnext/Shared/Networking/WinRTHttpResource.h | 10 - 2 files changed, 77 insertions(+), 149 deletions(-) diff --git a/vnext/Shared/Networking/WinRTHttpResource.cpp b/vnext/Shared/Networking/WinRTHttpResource.cpp index f0445d868c5..6022450205b 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.cpp +++ b/vnext/Shared/Networking/WinRTHttpResource.cpp @@ -133,6 +133,7 @@ IAsyncOperation WinRTHttpResource::CreateRequest( if (self->m_onError) { self->m_onError(reqArgs->RequestId, "Failed to parse Content-Type", false); } + co_return nullptr; } } else if (boost::iequals(name.c_str(), "Content-Encoding")) { contentEncoding = value; @@ -144,6 +145,7 @@ IAsyncOperation WinRTHttpResource::CreateRequest( if (self->m_onError) { self->m_onError(reqArgs->RequestId, "Failed to append Authorization", false); } + co_return nullptr; } } else { try { @@ -152,12 +154,26 @@ IAsyncOperation WinRTHttpResource::CreateRequest( if (self->m_onError) { self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), false); } - catch (hresult_error const &e) { - if (self->m_onError) { - self->m_onError(reqArgs->RequestId, Utilities::HResultToString(e), false); - } - co_return nullptr; + 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()); @@ -167,20 +183,16 @@ IAsyncOperation WinRTHttpResource::CreateRequest( auto view = winrt::array_view{byteVector}; auto buffer = CryptographicBuffer::CreateFromByteArray(view); content = HttpBufferContent{std::move(buffer)}; - } - else if (!data["string"].isNull()) { + } else if (!data["string"].isNull()) { content = HttpStringContent{to_hstring(data["string"].asString())}; - } - else if (!data["base64"].empty()) { + } else if (!data["base64"].empty()) { auto buffer = CryptographicBuffer::DecodeFromBase64String(to_hstring(data["base64"].asString())); content = HttpBufferContent{std::move(buffer)}; - } - else if (!data["uri"].empty()) { + } 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["formData"].empty()) { + } else if (!data["formData"].empty()) { winrt::Windows::Web::Http::HttpMultipartFormDataContent multiPartContent; auto formData = data["formData"]; @@ -206,127 +218,65 @@ IAsyncOperation WinRTHttpResource::CreateRequest( content = multiPartContent; } + } - // 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); - // 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; - } + 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); + 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; + } catch (const std::out_of_range &e) { + if (self->m_onError) + self->m_onError(reqArgs->RequestId, e.what() + string{" ["} + contentLength + "]", false); - co_return nullptr; - } + co_return nullptr; } - - request.Content(content); } - co_return request; + request.Content(content); } + co_return request; +} + #pragma endregion IWinRTHttpRequestFactory #pragma region IHttpResource - void WinRTHttpResource::SendRequest( - string && method, - string && url, - int64_t requestId, - Headers && headers, - dynamic && data, - string && responseType, - bool useIncrementalUpdates, - int64_t timeout, - bool withCredentials, - std::function &&callback) noexcept /*override*/ { - // Enforce supported args - assert(responseType == responseTypeText || responseType == responseTypeBase64 || responseType == responseTypeBlob); - - try { - HttpMethod httpMethod{to_hstring(std::move(method))}; - Uri uri{to_hstring(std::move(url))}; - - 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(), false); - } - } catch (hresult_error const &e) { - if (m_onError) { - m_onError(requestId, Utilities::HResultToString(e), false); - } - } catch (...) { - m_onError(requestId, "Unidentified error sending HTTP request", false); - } +void WinRTHttpResource::SendRequest( + string &&method, + string &&url, + int64_t requestId, + Headers &&headers, + dynamic &&data, + string &&responseType, + bool useIncrementalUpdates, + int64_t timeout, + bool withCredentials, + std::function &&callback) noexcept /*override*/ { + // Enforce supported args + assert(responseType == responseTypeText || responseType == responseTypeBase64 || responseType == responseTypeBlob); + + if (callback) { + callback(requestId); } try { @@ -355,30 +305,18 @@ IAsyncOperation WinRTHttpResource::CreateRequest( } catch (...) { m_onError(requestId, "Unidentified error sending HTTP request", false); } +} - void WinRTHttpResource::ClearCookies() noexcept /*override*/ { - assert(false); - // NOT IMPLEMENTED - } - - void WinRTHttpResource::SetOnRequestSuccess(function && handler) noexcept /*override*/ { - m_onRequestSuccess = std::move(handler); - } - - void WinRTHttpResource::SetOnResponse(function && handler) noexcept - /*override*/ { - m_onResponse = std::move(handler); - } +void WinRTHttpResource::AbortRequest(int64_t requestId) noexcept /*override*/ { + ResponseOperation request{nullptr}; - void WinRTHttpResource::SetOnData(function && handler) noexcept - /*override*/ { - m_onData = std::move(handler); - } - - void WinRTHttpResource::SetOnData(function && handler) noexcept - /*override*/ { - m_onDataDynamic = std::move(handler); + scoped_lock lock{m_mutex}; + auto iter = m_responses.find(requestId); + if (iter == std::end(m_responses)) { + return; + } + request = iter->second; } try { diff --git a/vnext/Shared/Networking/WinRTHttpResource.h b/vnext/Shared/Networking/WinRTHttpResource.h index f153098c376..7b18c69c6aa 100644 --- a/vnext/Shared/Networking/WinRTHttpResource.h +++ b/vnext/Shared/Networking/WinRTHttpResource.h @@ -64,16 +64,6 @@ class WinRTHttpResource : public IHttpResource, #pragma endregion IWinRTHttpRequestFactory -#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( From 25220f469369ccce8d41843a20df6a456b744999 Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Wed, 7 Dec 2022 13:58:24 -0800 Subject: [PATCH 45/45] Fix tests --- .../HttpResourceIntegrationTests.cpp | 1155 ++++++++--------- 1 file changed, 577 insertions(+), 578 deletions(-) diff --git a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp index 4426216f49e..dbe74138144 100644 --- a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp +++ b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp @@ -51,612 +51,611 @@ TEST_CLASS (HttpResourceIntegrationTest) { TEST_METHOD(RequestGetSucceeds) { string url = "http://localhost:" + std::to_string(s_port); - TEST_METHOD_CLEANUP(MethodCleanup) { - // Clear any runtime options that may be used by tests in this class. - MicrosoftReactSetRuntimeOptionString("Http.UserAgent", nullptr); + promise resPromise; + string error; + int statusCode = 0; + + auto server = make_shared(s_port); + server->Callbacks().OnGet = [&resPromise](const DynamicRequest &request) -> ResponseWrapper { + DynamicResponse response; + response.result(http::status::ok); + response.body() = Test::CreateStringResponseBody("some response content"); + + return {std::move(response)}; + }; + server->Start(); + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&resPromise, &statusCode](int64_t, IHttpResource::Response response) { + statusCode = static_cast(response.StatusCode); + }); + resource->SetOnData([&resPromise](int64_t, string &&content) { resPromise.set_value(); }); + resource->SetOnError([&resPromise, &error, &server](int64_t, string &&message, bool) { + error = std::move(message); + resPromise.set_value(); - // Bug in test HTTP server does not correctly release TCP port between test methods. - // Using a different por per test for now. - s_port++; - } - TEST_METHOD(RequestGetSucceeds) { - string url = "http://localhost:" + std::to_string(s_port); - - promise resPromise; - string error; - int statusCode = 0; - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&resPromise, &statusCode](int64_t, IHttpResource::Response response) { - statusCode = static_cast(response.StatusCode); - }); - resource->SetOnData([&resPromise](int64_t, string &&content) { resPromise.set_value(); }); - resource->SetOnError([&resPromise, &error, &server](int64_t, string &&message, bool) { - error = std::move(message); - resPromise.set_value(); - - server->Stop(); - }); - resource->SendRequest( - "GET", - std::move(url), - 0, /*requestId*/ - {}, /*header*/ - {}, /*data*/ - "text", - false, - 0 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {}); - - // Synchronize response. - resPromise.get_future().wait(); server->Stop(); + }); + resource->SendRequest( + "GET", + std::move(url), + 0, /*requestId*/ + {}, /*header*/ + {}, /*data*/ + "text", + false, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {}); + + // Synchronize response. + resPromise.get_future().wait(); + server->Stop(); + + Assert::AreEqual({}, error); + Assert::AreEqual(200, statusCode); + } - Assert::AreEqual({}, error); - Assert::AreEqual(200, statusCode); - } - - TEST_METHOD(RequestGetHeadersSucceeds) { - string url = "http://localhost:" + std::to_string(s_port); - - promise rcPromise; - string error; - IHttpResource::Response response; - - auto server = make_shared(s_port); - server->Callbacks().OnGet = [](const DynamicRequest &request) -> ResponseWrapper { - DynamicResponse response; - response.result(http::status::ok); - - // Response header - response.set(http::field::server, "Microsoft::React::Test::HttpServer"); - // Response content header - response.set(http::field::content_length, "0"); - // Response arbitrary header - response.set("ResponseHeaderName1", "ResponseHeaderValue1"); - - return {std::move(response)}; - }; - server->Start(); - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { - response = callbackResponse; - rcPromise.set_value(); - }); - resource->SetOnError([&rcPromise, &error, &server](int64_t, string &&message, bool) { - error = std::move(message); - rcPromise.set_value(); - - server->Abort(); - }); - - //clang-format off - resource->SendRequest( - "GET", - std::move(url), - 0, /*requestId*/ - { - {"Content-Type", "application/json"}, - {"Content-Encoding", "ASCII"}, - {"name3", "value3"}, - {"name4", "value4"}, - }, - {}, /*data*/ - "text", - false, - 0 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {}); - //clang-format on - - rcPromise.get_future().wait(); - server->Stop(); + TEST_METHOD(RequestGetHeadersSucceeds) { + string url = "http://localhost:" + std::to_string(s_port); - Assert::AreEqual({}, error, L"Error encountered"); - for (auto header : response.Headers) { - if (header.first == "Server") { - Assert::AreEqual({"Microsoft::React::Test::HttpServer"}, header.second, L"Wrong header"); - } else if (header.first == "Content-Length") { - Assert::AreEqual({"0"}, header.second, L"Wrong header"); - } else if (header.first == "ResponseHeaderName1") { - Assert::AreEqual({"ResponseHeaderValue1"}, header.second, L"Wrong header"); - } else { - string message = "Unexpected header: [" + header.first + "]=[" + header.second + "]"; - Assert::Fail(Microsoft::Common::Unicode::Utf8ToUtf16(message).c_str()); - } + promise rcPromise; + string error; + IHttpResource::Response response; + + auto server = make_shared(s_port); + server->Callbacks().OnGet = [](const DynamicRequest &request) -> ResponseWrapper { + DynamicResponse response; + response.result(http::status::ok); + + // Response header + response.set(http::field::server, "Microsoft::React::Test::HttpServer"); + // Response content header + response.set(http::field::content_length, "0"); + // Response arbitrary header + response.set("ResponseHeaderName1", "ResponseHeaderValue1"); + + return {std::move(response)}; + }; + server->Start(); + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { + response = callbackResponse; + rcPromise.set_value(); + }); + resource->SetOnError([&rcPromise, &error, &server](int64_t, string &&message, bool) { + error = std::move(message); + rcPromise.set_value(); + + server->Abort(); + }); + + //clang-format off + resource->SendRequest( + "GET", + std::move(url), + 0, /*requestId*/ + { + {"Content-Type", "application/json"}, + {"Content-Encoding", "ASCII"}, + {"name3", "value3"}, + {"name4", "value4"}, + }, + {}, /*data*/ + "text", + false, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {}); + //clang-format on + + rcPromise.get_future().wait(); + server->Stop(); + + Assert::AreEqual({}, error, L"Error encountered"); + for (auto header : response.Headers) { + if (header.first == "Server") { + Assert::AreEqual({"Microsoft::React::Test::HttpServer"}, header.second, L"Wrong header"); + } else if (header.first == "Content-Length") { + Assert::AreEqual({"0"}, header.second, L"Wrong header"); + } else if (header.first == "ResponseHeaderName1") { + Assert::AreEqual({"ResponseHeaderValue1"}, header.second, L"Wrong header"); + } else { + string message = "Unexpected header: [" + header.first + "]=[" + header.second + "]"; + Assert::Fail(Microsoft::Common::Unicode::Utf8ToUtf16(message).c_str()); } } + } - TEST_METHOD(RequestGetExplicitUserAgentSucceeds) { - string url = "https://api.github.com/repos/microsoft/react-native-xaml"; - - promise rcPromise; - string error; - IHttpResource::Response response; - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { - response = callbackResponse; - rcPromise.set_value(); - }); - resource->SetOnError([&rcPromise, &error](int64_t, string &&message, bool) { - error = std::move(message); - rcPromise.set_value(); - }); - - //clang-format off - resource->SendRequest( - "GET", - std::move(url), - 0, /*requestId*/ - {{"User-Agent", "React Native Windows"}}, - {}, /*data*/ - "text", /*responseType*/ - false, - 0 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {}); - //clang-format on - - rcPromise.get_future().wait(); - - Assert::AreEqual({}, error, L"Error encountered"); - Assert::AreEqual(static_cast(200), response.StatusCode); - } + TEST_METHOD(RequestGetExplicitUserAgentSucceeds) { + string url = "https://api.github.com/repos/microsoft/react-native-xaml"; + + promise rcPromise; + string error; + IHttpResource::Response response; + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { + response = callbackResponse; + rcPromise.set_value(); + }); + resource->SetOnError([&rcPromise, &error](int64_t, string &&message, bool) { + error = std::move(message); + rcPromise.set_value(); + }); + + //clang-format off + resource->SendRequest( + "GET", + std::move(url), + 0, /*requestId*/ + {{"User-Agent", "React Native Windows"}}, + {}, /*data*/ + "text", /*responseType*/ + false, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {}); + //clang-format on + + rcPromise.get_future().wait(); + + Assert::AreEqual({}, error, L"Error encountered"); + Assert::AreEqual(static_cast(200), response.StatusCode); + } - TEST_METHOD(RequestGetImplicitUserAgentSucceeds) { - string url = "https://api.github.com/repos/microsoft/react-native-windows"; - - promise rcPromise; - string error; - IHttpResource::Response response; - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { - response = callbackResponse; - rcPromise.set_value(); - }); - resource->SetOnError([&rcPromise, &error](int64_t, string &&message, bool) { - error = std::move(message); - rcPromise.set_value(); - }); - - MicrosoftReactSetRuntimeOptionString("Http.UserAgent", "React Native Windows"); - - //clang-format off - resource->SendRequest( - "GET", - std::move(url), - 0, /*requestId*/ - {}, /*headers*/ - {}, /*data*/ - "text", /*responseType*/ - false, - 0 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {}); - //clang-format on - - rcPromise.get_future().wait(); - - Assert::AreEqual({}, error, L"Error encountered"); - Assert::AreEqual(static_cast(200), response.StatusCode); - } + TEST_METHOD(RequestGetImplicitUserAgentSucceeds) { + string url = "https://api.github.com/repos/microsoft/react-native-windows"; + + promise rcPromise; + string error; + IHttpResource::Response response; + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { + response = callbackResponse; + rcPromise.set_value(); + }); + resource->SetOnError([&rcPromise, &error](int64_t, string &&message, bool) { + error = std::move(message); + rcPromise.set_value(); + }); + + MicrosoftReactSetRuntimeOptionString("Http.UserAgent", "React Native Windows"); + + //clang-format off + resource->SendRequest( + "GET", + std::move(url), + 0, /*requestId*/ + {}, /*headers*/ + {}, /*data*/ + "text", /*responseType*/ + false, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {}); + //clang-format on + + rcPromise.get_future().wait(); + + Assert::AreEqual({}, error, L"Error encountered"); + Assert::AreEqual(static_cast(200), response.StatusCode); + } - TEST_METHOD(RequestGetMissingUserAgentFails) { - // string url = "http://localhost:" + std::to_string(s_port); - string url = "https://api.github.com/repos/microsoft/react-native-macos"; - - promise rcPromise; - string error; - IHttpResource::Response response; - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { - response = callbackResponse; - rcPromise.set_value(); - }); - resource->SetOnError([&rcPromise, &error](int64_t, string &&message, bool) { - error = std::move(message); - rcPromise.set_value(); - }); - - //clang-format off - resource->SendRequest( - "GET", - std::move(url), - 0, /*requestId*/ - {}, /*headers*/ - {}, /*data*/ - "text", /*responseType*/ - false, - 0 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {}); - //clang-format on - - rcPromise.get_future().wait(); - - Assert::AreEqual({}, error, L"Error encountered"); - Assert::AreEqual(static_cast(403), response.StatusCode); - } + TEST_METHOD(RequestGetMissingUserAgentFails) { + // string url = "http://localhost:" + std::to_string(s_port); + string url = "https://api.github.com/repos/microsoft/react-native-macos"; + + promise rcPromise; + string error; + IHttpResource::Response response; + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&rcPromise, &response](int64_t, IHttpResource::Response callbackResponse) { + response = callbackResponse; + rcPromise.set_value(); + }); + resource->SetOnError([&rcPromise, &error](int64_t, string &&message, bool) { + error = std::move(message); + rcPromise.set_value(); + }); + + //clang-format off + resource->SendRequest( + "GET", + std::move(url), + 0, /*requestId*/ + {}, /*headers*/ + {}, /*data*/ + "text", /*responseType*/ + false, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {}); + //clang-format on + + rcPromise.get_future().wait(); + + Assert::AreEqual({}, error, L"Error encountered"); + Assert::AreEqual(static_cast(403), response.StatusCode); + } - TEST_METHOD(RequestGetFails) { - string error; - promise promise; + TEST_METHOD(RequestGetFails) { + string error; + promise promise; - auto resource = IHttpResource::Make(); - resource->SetOnError([&error, &promise](int64_t, string &&message, bool) { - error = message; - promise.set_value(); - }); + auto resource = IHttpResource::Make(); + resource->SetOnError([&error, &promise](int64_t, string &&message, bool) { + error = message; + promise.set_value(); + }); - resource->SendRequest("GET", "http://nonexistinghost", 0, {}, {}, "text", false, 0, false, [](int64_t) {}); + resource->SendRequest("GET", "http://nonexistinghost", 0, {}, {}, "text", false, 0, false, [](int64_t) {}); - promise.get_future().wait(); + promise.get_future().wait(); - Logger::WriteMessage(error.c_str()); - Assert::AreNotEqual(string{}, error); - } + Logger::WriteMessage(error.c_str()); + Assert::AreNotEqual(string{}, error); + } - TEST_METHOD(RequestOptionsSucceeds) { - string url = "http://localhost:" + std::to_string(s_port); - - promise getResponsePromise; - promise getDataPromise; - promise optionsPromise; - string error; - IHttpResource::Response getResponse; - IHttpResource::Response optionsResponse; - string content; - - auto server = make_shared(s_port); - server->Callbacks().OnOptions = [](const DynamicRequest &request) -> ResponseWrapper { - EmptyResponse response; - response.result(http::status::partial_content); - response.set("PreflightName", "PreflightValue"); - - return {std::move(response)}; - }; - server->Callbacks().OnGet = [](const DynamicRequest &request) -> ResponseWrapper { - DynamicResponse response; - response.result(http::status::ok); - response.body() = Test::CreateStringResponseBody("Response Body"); - - return {std::move(response)}; - }; - server->Start(); - - auto resource = IHttpResource::Make(); - resource->SetOnResponse([&getResponse, &getResponsePromise, &optionsResponse, &optionsPromise]( - int64_t, IHttpResource::Response callbackResponse) { - if (callbackResponse.StatusCode == static_cast(http::status::ok)) { - getResponse = callbackResponse; - getResponsePromise.set_value(); - } else if (callbackResponse.StatusCode == static_cast(http::status::partial_content)) { - optionsResponse = callbackResponse; - optionsPromise.set_value(); - } - }); - resource->SetOnData([&getDataPromise, &content](int64_t, string &&responseData) { - content = std::move(responseData); + TEST_METHOD(RequestOptionsSucceeds) { + string url = "http://localhost:" + std::to_string(s_port); + + promise getResponsePromise; + promise getDataPromise; + promise optionsPromise; + string error; + IHttpResource::Response getResponse; + IHttpResource::Response optionsResponse; + string content; + + auto server = make_shared(s_port); + server->Callbacks().OnOptions = [](const DynamicRequest &request) -> ResponseWrapper { + EmptyResponse response; + response.result(http::status::partial_content); + response.set("PreflightName", "PreflightValue"); + + return {std::move(response)}; + }; + server->Callbacks().OnGet = [](const DynamicRequest &request) -> ResponseWrapper { + DynamicResponse response; + response.result(http::status::ok); + response.body() = Test::CreateStringResponseBody("Response Body"); + + return {std::move(response)}; + }; + server->Start(); + + auto resource = IHttpResource::Make(); + resource->SetOnResponse([&getResponse, &getResponsePromise, &optionsResponse, &optionsPromise]( + int64_t, IHttpResource::Response callbackResponse) { + if (callbackResponse.StatusCode == static_cast(http::status::ok)) { + getResponse = callbackResponse; + getResponsePromise.set_value(); + } else if (callbackResponse.StatusCode == static_cast(http::status::partial_content)) { + optionsResponse = callbackResponse; + optionsPromise.set_value(); + } + }); + resource->SetOnData([&getDataPromise, &content](int64_t, string &&responseData) { + content = std::move(responseData); - if (!content.empty()) + if (!content.empty()) + getDataPromise.set_value(); + }); + resource->SetOnError( + [&optionsPromise, &getResponsePromise, &getDataPromise, &error, &server](int64_t, string &&message, bool) { + error = std::move(message); + + optionsPromise.set_value(); + getResponsePromise.set_value(); getDataPromise.set_value(); - }); - resource->SetOnError( - [&optionsPromise, &getResponsePromise, &getDataPromise, &error, &server](int64_t, string &&message, bool) { - error = std::move(message); - - optionsPromise.set_value(); - getResponsePromise.set_value(); - getDataPromise.set_value(); - - server->Stop(); - }); - - //clang-format off - resource->SendRequest( - "OPTIONS", - string{url}, - 0, /*requestId*/ - {}, /*headers*/ - {}, /*data*/ - "text", - false, - 1000 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {}); - resource->SendRequest( - "GET", - std::move(url), - 0, /*requestId*/ - {}, /*headers*/ - {}, /*data*/ - "text", - false, - 0 /*timeout*/, - false /*withCredentials*/, - [](int64_t) {}); - //clang-format on - - optionsPromise.get_future().wait(); - getResponsePromise.get_future().wait(); - getDataPromise.get_future().wait(); - server->Stop(); - Assert::AreEqual({}, error, L"Error encountered"); - Assert::AreEqual(static_cast(1), optionsResponse.Headers.size()); - for (auto header : optionsResponse.Headers) { - if (header.first == "PreflightName") { - Assert::AreEqual({"PreflightValue"}, header.second, L"Wrong header"); - } else { - string message = "Unexpected header: [" + header.first + "]=[" + header.second + "]"; - Assert::Fail(Microsoft::Common::Unicode::Utf8ToUtf16(message).c_str()); - } + server->Stop(); + }); + + //clang-format off + resource->SendRequest( + "OPTIONS", + string{url}, + 0, /*requestId*/ + {}, /*headers*/ + {}, /*data*/ + "text", + false, + 1000 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {}); + resource->SendRequest( + "GET", + std::move(url), + 0, /*requestId*/ + {}, /*headers*/ + {}, /*data*/ + "text", + false, + 0 /*timeout*/, + false /*withCredentials*/, + [](int64_t) {}); + //clang-format on + + optionsPromise.get_future().wait(); + getResponsePromise.get_future().wait(); + getDataPromise.get_future().wait(); + server->Stop(); + + Assert::AreEqual({}, error, L"Error encountered"); + Assert::AreEqual(static_cast(1), optionsResponse.Headers.size()); + for (auto header : optionsResponse.Headers) { + if (header.first == "PreflightName") { + Assert::AreEqual({"PreflightValue"}, header.second, L"Wrong header"); + } else { + string message = "Unexpected header: [" + header.first + "]=[" + header.second + "]"; + Assert::Fail(Microsoft::Common::Unicode::Utf8ToUtf16(message).c_str()); } - Assert::AreEqual({"Response Body"}, content); } + Assert::AreEqual({"Response Body"}, content); + } - TEST_METHOD(SimpleRedirectGetSucceeds) { - 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().OnGet = [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().OnGet = [](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); - + TEST_METHOD(SimpleRedirectGetSucceeds) { + 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().OnGet = [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().OnGet = [](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(); - contentPromise.set_value(); - }); - - //clang-format off - resource->SendRequest( - "GET", - 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(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); + } + }); + resource->SetOnData([&contentPromise, &content](int64_t, string &&responseData) { + content = std::move(responseData); - responsePromise.set_value(); + if (!content.empty()) 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); - } + }); + 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( + "GET", + 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(); + 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); - Assert::AreEqual({}, error); - Assert::IsFalse(timeoutError); - Assert::AreEqual(200, statusCode); - } + 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(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(); + 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); + } - Assert::IsTrue(timeoutError); - Assert::AreEqual({"[0x800705b4] This operation returned because the timeout period expired."}, error); - Assert::AreEqual(0, 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; +/*static*/ uint16_t HttpResourceIntegrationTest::s_port = 4444; } // namespace Microsoft::React::Test