diff --git a/Package.swift b/Package.swift index 534c3547..2412cd33 100644 --- a/Package.swift +++ b/Package.swift @@ -416,6 +416,7 @@ let package = Package( "internal/platform/implementation/BUILD", "internal/platform/BUILD", "internal/analytics/BUILD", + "internal/network/BUILD", // tests "connections/listeners_test.cc", "connections/strategy_test.cc", @@ -520,6 +521,13 @@ let package = Package( "internal/platform/byte_utils_test.cc", "internal/platform/direct_executor_test.cc", "internal/platform/borrowable_test.cc", + "internal/platform/implementation/windows/http_loader_test.cc", + "internal/network/utils_test.cc", + "internal/network/url_test.cc", + "internal/network/http_response_test.cc", + "internal/network/http_request_test.cc", + "internal/network/http_client_impl_test.cc", + "internal/network/http_status_code_test.cc", // simulation "connections/implementation/offline_simulation_user.cc", "connections/implementation/simulation_user.cc", diff --git a/internal/network/BUILD b/internal/network/BUILD new file mode 100644 index 00000000..b6fee676 --- /dev/null +++ b/internal/network/BUILD @@ -0,0 +1,96 @@ +licenses(["notice"]) + +cc_library( + name = "types", + srcs = [ + "http_request.cc", + "http_response.cc", + "http_status_code.cc", + "url.cc", + "utils.cc", + ], + hdrs = [ + "http_body.h", + "http_client.h", + "http_client_factory.h", + "http_request.h", + "http_response.h", + "http_status_code.h", + "url.h", + "utils.h", + ], + visibility = [ + "//connections:__subpackages__", + "//fastpair:__subpackages__", + "//internal:__pkg__", + "//internal/platform:__subpackages__", + "//internal/platform/implementation:__subpackages__", + "//location/nearby/cpp/fastpair:__subpackages__", + "//presence:__subpackages__", + ], + deps = [ + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + ], +) + +cc_library( + name = "nearby_http_client", + srcs = [ + "http_client_impl.cc", + ], + hdrs = [ + "debug.h", + "http_client_factory_impl.h", + "http_client_impl.h", + ], + defines = ["_SILENCE_CLANG_COROUTINE_MESSAGE"], + visibility = [ + "//connections:__subpackages__", + "//fastpair:__subpackages__", + "//internal:__pkg__", + "//internal/platform:__subpackages__", + "//internal/platform/implementation:__subpackages__", + "//location/nearby/cpp/fastpair:__subpackages__", + "//presence:__subpackages__", + ], + deps = [ + ":types", + "//internal/platform:logging", + "//internal/platform/implementation:platform", + "@com_google_absl//absl/base:core_headers", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/strings:str_format", + "@com_google_absl//absl/synchronization", + ], +) + +cc_test( + name = "nearby_http_client_test", + size = "small", + timeout = "short", + srcs = [ + "http_client_impl_test.cc", + "http_request_test.cc", + "http_response_test.cc", + "http_status_code_test.cc", + "url_test.cc", + "utils_test.cc", + ], + shard_count = 8, + deps = [ + ":nearby_http_client", + ":types", + "//internal/platform/implementation:comm", + "//internal/platform/implementation:platform", + "//internal/platform/implementation/g3", + "@com_github_protobuf_matchers//protobuf-matchers", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/internal/network/debug.h b/internal/network/debug.h new file mode 100644 index 00000000..9236cb07 --- /dev/null +++ b/internal/network/debug.h @@ -0,0 +1,34 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_INTERNAL_NETWORK_DEBUG_H_ +#define THIRD_PARTY_NEARBY_INTERNAL_NETWORK_DEBUG_H_ + +namespace location { +namespace nearby { +namespace network { +namespace debug { + +// Indicates to whether output HTTP request. +constexpr bool kRequestEnabled = false; + +// Indicates to whether output HTTP response. +constexpr bool kResponseEnabled = false; + +} // namespace debug +} // namespace network +} // namespace nearby +} // namespace location + +#endif // THIRD_PARTY_NEARBY_INTERNAL_NETWORK_DEBUG_H_ diff --git a/internal/network/http_body.h b/internal/network/http_body.h new file mode 100644 index 00000000..57d89081 --- /dev/null +++ b/internal/network/http_body.h @@ -0,0 +1,67 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_BODY_H_ +#define THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_BODY_H_ + +#include +#include + +#include "absl/strings/string_view.h" + +namespace location { +namespace nearby { +namespace network { + +class HttpBody { + public: + HttpBody() = default; + ~HttpBody() = default; + + HttpBody(const HttpBody&) = default; + HttpBody& operator=(const HttpBody&) = default; + HttpBody(HttpBody&&) = default; + HttpBody& operator=(HttpBody&&) = default; + + void SetData(const char* data, size_t size) { + if (data == nullptr) { + size = 0; + } + data_.assign(data, size); + } + + void SetData(absl::string_view data) { + data_.assign(data.data(), data.size()); + } + + bool empty() { return data_.empty(); } + + const char* data() const { return &data_[0]; } + + size_t size() const { return data_.size(); } + + absl::string_view GetRawData() const { return data_; } + + private: + std::string data_; +}; + +using HttpRequestBody = HttpBody; +using HttpResponseBody = HttpBody; + +} // namespace network +} // namespace nearby +} // namespace location + +#endif // THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_BODY_H_ diff --git a/internal/network/http_client.h b/internal/network/http_client.h new file mode 100644 index 00000000..b28361c5 --- /dev/null +++ b/internal/network/http_client.h @@ -0,0 +1,41 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_CLIENT_H_ +#define THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_CLIENT_H_ + +#include + +#include "absl/status/statusor.h" +#include "internal/network/http_request.h" +#include "internal/network/http_response.h" + +namespace location { +namespace nearby { +namespace network { + +class HttpClient { + public: + virtual ~HttpClient() = default; + + virtual void StartRequest( + const HttpRequest& request, + std::function&)> callback) = 0; +}; + +} // namespace network +} // namespace nearby +} // namespace location + +#endif // THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_CLIENT_H_ diff --git a/internal/network/http_client_factory.h b/internal/network/http_client_factory.h new file mode 100644 index 00000000..d9c560cd --- /dev/null +++ b/internal/network/http_client_factory.h @@ -0,0 +1,38 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_CLIENT_FACTORY_H_ +#define THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_CLIENT_FACTORY_H_ + +#include +#include + +#include "internal/network/http_client.h" + +namespace location { +namespace nearby { +namespace network { + +class HttpClientFactory { + public: + virtual ~HttpClientFactory() = default; + + virtual std::unique_ptr CreateInstance() = 0; +}; + +} // namespace network +} // namespace nearby +} // namespace location + +#endif // THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_CLIENT_FACTORY_H_ diff --git a/internal/network/http_client_factory_impl.h b/internal/network/http_client_factory_impl.h new file mode 100644 index 00000000..c1bd6e8e --- /dev/null +++ b/internal/network/http_client_factory_impl.h @@ -0,0 +1,38 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_CLIENT_FACTORY_IMPL_H_ +#define THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_CLIENT_FACTORY_IMPL_H_ + +#include + +#include "internal/network/http_client_factory.h" +#include "internal/network/http_client_impl.h" + +namespace location { +namespace nearby { +namespace network { + +class HttpClientFactoryImpl : public HttpClientFactory { + public: + std::unique_ptr CreateInstance() override { + return std::make_unique(); + } +}; + +} // namespace network +} // namespace nearby +} // namespace location + +#endif // THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_CLIENT_FACTORY_IMPL_H_ diff --git a/internal/network/http_client_impl.cc b/internal/network/http_client_impl.cc new file mode 100644 index 00000000..d0cde7d9 --- /dev/null +++ b/internal/network/http_client_impl.cc @@ -0,0 +1,123 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "internal/network/http_client_impl.h" + +#include // NOLINT +#include +#include // NOLINT +#include +#include +#include + +#include "internal/network/debug.h" +#include "internal/platform/implementation/platform.h" +#include "internal/platform/logging.h" + +namespace location { +namespace nearby { +namespace network { + +void NearbyHttpClient::StartRequest( + const HttpRequest& request, + std::function&)> callback) { + absl::MutexLock lock(&mutex_); + CleanThreads(); + + std::future http_thread = std::async(std::launch::async, [&, request, + callback]() { + api::WebRequest web_request; + web_request.url = request.GetUrl().GetUrlPath(); + web_request.method = absl::StrCat(request.GetMethodString()); + for (const auto& header : request.GetAllHeaders()) { + for (const auto& value : header.second) { + web_request.headers.emplace(header.first, value); + } + } + web_request.body = absl::StrCat(request.GetBody().GetRawData()); + + if (debug::kRequestEnabled) { + std::stringstream request_stream; + request_stream << "HTTP REQUEST====>" << std::endl; + request_stream << web_request.method << " " << web_request.url + << std::endl; + for (const auto& header : web_request.headers) { + request_stream << header.first << ": " << header.second << std::endl; + } + request_stream << std::endl; + request_stream << "body size: " << request.GetBody().GetRawData().size() + << std::endl; + NEARBY_LOGS(VERBOSE) << request_stream.str(); + } + + absl::StatusOr web_response = + api::ImplementationPlatform::SendRequest(web_request); + + if (!web_response.ok()) { + if (callback != nullptr) { + callback(web_response.status()); + } + return; + } + + if (debug::kResponseEnabled) { + std::stringstream response_stream; + response_stream << "HTTP RESPONSE====>" << std::endl; + response_stream << "url: " << web_request.url << std::endl; + response_stream << web_response->status_code << " " + << web_response->status_text << std::endl; + for (const auto& header : web_response->headers) { + response_stream << header.first << ": " << header.second << std::endl; + } + response_stream << std::endl; + response_stream << "body size: " << web_response->body.size() + << std::endl; + NEARBY_LOGS(VERBOSE) << response_stream.str(); + } + + HttpResponse response; + + response.SetStatusCode( + static_cast(web_response->status_code)); + response.SetReasonPhrase(web_response->status_text); + for (const auto& header : web_response->headers) { + response.AddHeader(header.first, header.second); + } + response.SetBody(web_response->body); + + if (callback != nullptr) { + callback(response); + } + }); + + http_threads_.push_back(std::move(http_thread)); +} + +void NearbyHttpClient::CleanThreads() { + auto it = http_threads_.begin(); + + while (it != http_threads_.end()) { + // Delete the thread if it is ready + auto status = it->wait_for(std::chrono::seconds(0)); + if (status == std::future_status::ready) { + it = http_threads_.erase(it); + } else { + ++it; + } + } +} + +} // namespace network +} // namespace nearby +} // namespace location diff --git a/internal/network/http_client_impl.h b/internal/network/http_client_impl.h new file mode 100644 index 00000000..5d0d1089 --- /dev/null +++ b/internal/network/http_client_impl.h @@ -0,0 +1,57 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_CLIENT_IMPL_H_ +#define THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_CLIENT_IMPL_H_ + +#include +#include // NOLINT +#include // NOLINT +#include +#include + +#include "absl/base/thread_annotations.h" +#include "absl/synchronization/mutex.h" +#include "internal/network/http_client.h" + +namespace location { +namespace nearby { +namespace network { + +class NearbyHttpClient : public HttpClient { + public: + NearbyHttpClient() = default; + ~NearbyHttpClient() override = default; + + NearbyHttpClient(const NearbyHttpClient&) = default; + NearbyHttpClient& operator=(const NearbyHttpClient&) = default; + NearbyHttpClient(NearbyHttpClient&&) = default; + NearbyHttpClient& operator=(NearbyHttpClient&&) = default; + + void StartRequest(const HttpRequest& request, + std::function&)> + callback) override ABSL_LOCKS_EXCLUDED(mutex_); + + private: + void CleanThreads() ABSL_SHARED_LOCKS_REQUIRED(mutex_); + + absl::Mutex mutex_; + std::vector> http_threads_ ABSL_GUARDED_BY(mutex_); +}; + +} // namespace network +} // namespace nearby +} // namespace location + +#endif // THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_CLIENT_IMPL_H_ diff --git a/internal/network/http_client_impl_test.cc b/internal/network/http_client_impl_test.cc new file mode 100644 index 00000000..88fd3c85 --- /dev/null +++ b/internal/network/http_client_impl_test.cc @@ -0,0 +1,275 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "internal/network/http_client_impl.h" + +#include +#include +#include + +#include "gmock/gmock.h" +#include "protobuf-matchers/protocol-buffer-matchers.h" +#include "gtest/gtest.h" +#include "absl/container/flat_hash_map.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "internal/network/http_status_code.h" +#include "internal/platform/implementation/http_loader.h" +#include "internal/platform/implementation/platform.h" + +namespace location { +namespace nearby { +namespace api { +namespace { + +struct HttpTestContext { + WebRequest web_request; + WebResponse web_response; + absl::Status status; +}; + +HttpTestContext* GetContext() { + static HttpTestContext* context = new HttpTestContext(); + return context; +} + +} // namespace + +// Mock web implementation of the platform +absl::StatusOr ImplementationPlatform::SendRequest( + const WebRequest& request) { + GetContext()->web_request = request; + if (GetContext()->status.ok()) { + return GetContext()->web_response; + } + return GetContext()->status; +} + +} // namespace api + +namespace network { + +using ::testing::SizeIs; + +class NearbyHttpClientTest : public ::testing::Test { + public: + void SetUp() override { + api::GetContext()->web_request = api::WebRequest(); + api::GetContext()->web_response = api::WebResponse(); + api::GetContext()->status = absl::Status(); + } + + void MockFailedResponse(absl::Status status) { + api::GetContext()->status = status; + } + + void MockResponse(network::HttpStatusCode status, + absl::string_view status_message, + const std::multimap& headers, + absl::string_view body) { + api::WebResponse web_response; + web_response.status_code = static_cast(status); + web_response.status_text = absl::StrCat(status_message); + web_response.headers = headers; + web_response.body = absl::StrCat(body); + api::GetContext()->web_response = web_response; + } + + api::WebRequest GetWebRequest() { return api::GetContext()->web_request; } + + absl::StatusOr GetResponse( + absl::string_view url, HttpRequestMethod method, + const std::multimap& headers, + absl::string_view body) { + absl::StatusOr result; + absl::StatusOr request_url = Url::Create(url); + if (!request_url.ok()) { + return request_url.status(); + } + + HttpRequest request{request_url.value()}; + auto it = headers.begin(); + while (it != headers.end()) { + request.AddHeader(it->first, it->second); + ++it; + } + request.SetMethod(method); + request.SetBody(body); + + absl::Notification notification; + client_.StartRequest( + request, [&result, ¬ification]( + const absl::StatusOr& http_response) { + result = http_response; + notification.Notify(); + }); + + // No timeout to emulate original behavior + notification.WaitForNotification(); + return result; + } + + void CheckHeader(const std::multimap& headers, + absl::string_view key, absl::string_view expected_value) { + auto it = headers.find(std::string(key)); + ASSERT_TRUE(it != headers.end()); + + bool found = false; + while (it != headers.end()) { + if (it->second == absl::StrCat(expected_value)) { + found = true; + break; + } + ++it; + } + + EXPECT_TRUE(found); + } + + private: + NearbyHttpClient client_; +}; + +namespace { +TEST_F(NearbyHttpClientTest, TestGet) { + MockResponse(HttpStatusCode::kHttpOk, "OK", {{"Content_Type", "text/html"}}, + "web content"); + auto result = + GetResponse("http://www.google.com", HttpRequestMethod::kGet, {}, ""); + + // Checks request. + api::WebRequest web_request = GetWebRequest(); + EXPECT_EQ(web_request.url, "http://www.google.com"); + EXPECT_EQ(web_request.method, "GET"); + + // Checks response. + ASSERT_TRUE(result.ok()); + EXPECT_EQ(result->GetStatusCode(), HttpStatusCode::kHttpOk); + EXPECT_EQ(result->GetBody().GetRawData(), "web content"); + ASSERT_THAT(result->GetAllHeaders(), SizeIs(1)); + EXPECT_EQ(result->GetAllHeaders().at("Content_Type")[0], "text/html"); +} + +TEST_F(NearbyHttpClientTest, TestGetWithQuery) { + MockResponse(HttpStatusCode::kHttpOk, "OK", {{"Content_Type", "text/html"}}, + "web content"); + auto result = GetResponse("http://www.google.com?name=name1&age=36", + HttpRequestMethod::kGet, {}, ""); + + // Checks request. + api::WebRequest web_request = GetWebRequest(); + EXPECT_EQ(web_request.url, "http://www.google.com?name=name1&age=36"); + EXPECT_EQ(web_request.method, "GET"); + + // Checks response. + ASSERT_TRUE(result.ok()); + EXPECT_EQ(result->GetStatusCode(), HttpStatusCode::kHttpOk); +} + +TEST_F(NearbyHttpClientTest, TestGetWithErrorResult) { + MockFailedResponse(absl::InternalError("no connection.")); + auto result = GetResponse("http://www.google.com?name=name1&age=36", + HttpRequestMethod::kGet, {}, ""); + + // Checks request. + api::WebRequest web_request = GetWebRequest(); + EXPECT_EQ(web_request.url, "http://www.google.com?name=name1&age=36"); + EXPECT_EQ(web_request.method, "GET"); + + // Checks response. + EXPECT_FALSE(result.ok()); +} + +TEST_F(NearbyHttpClientTest, TestPost) { + MockResponse(HttpStatusCode::kHttpNoContent, "OK", + {{"Content_Type", "text/html"}}, ""); + auto result = + GetResponse("http://www.google.com", HttpRequestMethod::kPost, {}, ""); + + // Checks request. + api::WebRequest web_request = GetWebRequest(); + EXPECT_EQ(web_request.url, "http://www.google.com"); + EXPECT_EQ(web_request.method, "POST"); + + // Checks response. + ASSERT_TRUE(result.ok()); + EXPECT_EQ(result->GetStatusCode(), HttpStatusCode::kHttpNoContent); + HttpResponseBody body = result->GetBody(); + EXPECT_TRUE(body.empty()); +} + +TEST_F(NearbyHttpClientTest, TestPostWithHeader) { + MockResponse(HttpStatusCode::kHttpNoContent, "OK", + {{"Content_Type", "text/html"}}, ""); + auto result = + GetResponse("http://www.google.com", HttpRequestMethod::kPost, + {{"Content_Type", "text/json"}, {"size", "596"}}, ""); + + // Checks request. + api::WebRequest web_request = GetWebRequest(); + EXPECT_EQ(web_request.url, "http://www.google.com"); + EXPECT_EQ(web_request.method, "POST"); + ASSERT_NO_FATAL_FAILURE( + CheckHeader(web_request.headers, "Content_Type", "text/json")); + ASSERT_NO_FATAL_FAILURE(CheckHeader(web_request.headers, "size", "596")); + + // Checks response. + ASSERT_TRUE(result.ok()); + EXPECT_EQ(result->GetStatusCode(), HttpStatusCode::kHttpNoContent); + EXPECT_EQ(result->GetBody().GetRawData(), ""); +} + +TEST_F(NearbyHttpClientTest, TestPostWithErrorResult) { + MockFailedResponse(absl::UnauthenticatedError("no user.")); + auto result = + GetResponse("http://www.google.com", HttpRequestMethod::kPost, + {{"Content_Type", "text/json"}, {"size", "596"}}, ""); + + // Checks request. + api::WebRequest web_request = GetWebRequest(); + EXPECT_EQ(web_request.url, "http://www.google.com"); + EXPECT_EQ(web_request.method, "POST"); + ASSERT_NO_FATAL_FAILURE( + CheckHeader(web_request.headers, "Content_Type", "text/json")); + ASSERT_NO_FATAL_FAILURE(CheckHeader(web_request.headers, "size", "596")); + + // Checks response. + ASSERT_FALSE(result.ok()); +} + +TEST_F(NearbyHttpClientTest, TestRequestWithCleanThreads) { + MockResponse(HttpStatusCode::kHttpOk, "OK", {{"Content_Type", "text/html"}}, + "web content"); + auto result = + GetResponse("http://www.google.com", HttpRequestMethod::kGet, {}, ""); + + // Checks request. + api::WebRequest web_request = GetWebRequest(); + EXPECT_EQ(web_request.url, "http://www.google.com"); + EXPECT_EQ(web_request.method, "GET"); + + // Checks response. + ASSERT_TRUE(result.ok()); + + result = + GetResponse("http://www.youtube.com", HttpRequestMethod::kGet, {}, ""); + ASSERT_TRUE(result.ok()); +} + +} // namespace +} // namespace network +} // namespace nearby +} // namespace location diff --git a/internal/network/http_request.cc b/internal/network/http_request.cc new file mode 100644 index 00000000..d58f9696 --- /dev/null +++ b/internal/network/http_request.cc @@ -0,0 +1,104 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "internal/network/http_request.h" + +#include +#include + +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" + +namespace location { +namespace nearby { +namespace network { + +HttpRequest::HttpRequest(const Url& url) : url_(url) { + method_ = HttpRequestMethod::kGet; +} + +void HttpRequest::SetUrl(const Url& url) { url_ = url; } + +const Url& HttpRequest::GetUrl() const { return url_; } + +void HttpRequest::SetMethod(const HttpRequestMethod& method) { + method_ = method; +} + +const HttpRequestMethod& HttpRequest::GetMethod() const { return method_; } + +absl::string_view HttpRequest::GetMethodString() const { + switch (method_) { + case HttpRequestMethod::kConnect: + return "CONNECT"; + case HttpRequestMethod::kDelete: + return "DELETE"; + case HttpRequestMethod::kGet: + return "GET"; + case HttpRequestMethod::kHead: + return "HEAD"; + case HttpRequestMethod::kOptions: + return "OPTIONS"; + case HttpRequestMethod::kPatch: + return "PATCH"; + case HttpRequestMethod::kPost: + return "POST"; + case HttpRequestMethod::kPut: + return "PUT"; + case HttpRequestMethod::kTrace: + return "TRACE"; + } +} + +void HttpRequest::AddHeader(absl::string_view header, absl::string_view value) { + auto it = headers_.find(header); + if (it == headers_.end()) { + headers_.emplace(header, std::vector({std::string(value)})); + } else { + it->second.push_back(std::string(value)); + } +} + +void HttpRequest::RemoveHeader(absl::string_view header) { + headers_.erase(header); +} + +const absl::flat_hash_map>& +HttpRequest::GetAllHeaders() const { + return headers_; +} + +void HttpRequest::AddQueryParameter(absl::string_view query, + absl::string_view value) { + url_.AddQueryParameter(query, value); +} + +void HttpRequest::RemoveQueryParameter(absl::string_view query) { + url_.RemoveQueryParameter(query); +} +const Url::QueryParameters& HttpRequest::GetAllQueryParameters() const { + return url_.GetAllQueryStrings(); +} + +void HttpRequest::SetBody(const HttpRequestBody& body) { body_ = body; } + +void HttpRequest::SetBody(absl::string_view body) { + body_.SetData(body.data(), body.size()); +} + +const HttpRequestBody& HttpRequest::GetBody() const { return body_; } + +} // namespace network +} // namespace nearby +} // namespace location diff --git a/internal/network/http_request.h b/internal/network/http_request.h new file mode 100644 index 00000000..2f90ff6e --- /dev/null +++ b/internal/network/http_request.h @@ -0,0 +1,92 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_REQUEST_H_ +#define THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_REQUEST_H_ + +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/strings/string_view.h" +#include "internal/network/http_body.h" +#include "internal/network/url.h" + +namespace location { +namespace nearby { +namespace network { + +enum class HttpRequestMethod { + kOptions, + kGet, + kHead, + kPost, + kPut, + kDelete, + kTrace, + kConnect, + kPatch +}; + +class HttpRequest { + public: + HttpRequest() = default; + explicit HttpRequest(const Url& url); + ~HttpRequest() = default; + + HttpRequest(const HttpRequest&) = default; + HttpRequest& operator=(const HttpRequest&) = default; + HttpRequest(HttpRequest&&) = default; + HttpRequest& operator=(HttpRequest&&) = default; + + void SetUrl(const Url& url); + const Url& GetUrl() const; + + void SetMethod(const HttpRequestMethod& method); + const HttpRequestMethod& GetMethod() const; + absl::string_view GetMethodString() const; + + void AddHeader(absl::string_view header, absl::string_view value); + void RemoveHeader(absl::string_view header); + const absl::flat_hash_map>& + GetAllHeaders() const; + + void AddQueryParameter(absl::string_view query, absl::string_view value); + void RemoveQueryParameter(absl::string_view query); + const Url::QueryParameters& GetAllQueryParameters() const; + + void SetBody(const HttpRequestBody& body); + void SetBody(absl::string_view body); + const HttpRequestBody& GetBody() const; + + private: + // The url of the request + Url url_; + + // The request method: GET, POST, etc. + HttpRequestMethod method_ = HttpRequestMethod::kGet; + + // The request headers, may include repeat keys + absl::flat_hash_map> headers_; + + // The request body, it may be empty. + HttpRequestBody body_; +}; + +} // namespace network +} // namespace nearby +} // namespace location + +#endif // THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_REQUEST_H_ diff --git a/internal/network/http_request_test.cc b/internal/network/http_request_test.cc new file mode 100644 index 00000000..9149363c --- /dev/null +++ b/internal/network/http_request_test.cc @@ -0,0 +1,88 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "internal/network/http_request.h" + +#include "gmock/gmock.h" +#include "protobuf-matchers/protocol-buffer-matchers.h" +#include "gtest/gtest.h" +#include "internal/network/http_body.h" + +namespace location { +namespace nearby { +namespace network { +namespace { + +using ::testing::SizeIs; + +TEST(HttpRequest, TestBuildRequest) { + auto url = Url::Create("http://www.google.com"); + ASSERT_TRUE(url.ok()); + + HttpRequest request_explicit(*url); + EXPECT_EQ(request_explicit.GetMethodString(), "GET"); + EXPECT_EQ(request_explicit.GetUrl(), *url); + + HttpRequest request; + EXPECT_EQ(request.GetMethodString(), "GET"); + request.SetUrl(*url); + EXPECT_EQ(request.GetUrl(), *url); + request.AddQueryParameter("name", "google"); + EXPECT_THAT(request.GetAllQueryParameters(), SizeIs(1)); + request.AddQueryParameter("name", "android"); + EXPECT_THAT(request.GetAllQueryParameters(), SizeIs(2)); + request.AddHeader("content-type", "text/html"); + EXPECT_THAT(request.GetAllHeaders(), SizeIs(1)); + request.SetMethod(HttpRequestMethod::kPost); + EXPECT_EQ(request.GetMethodString(), "POST"); + EXPECT_EQ(request.GetMethod(), HttpRequestMethod::kPost); + request.RemoveQueryParameter("name"); + EXPECT_THAT(request.GetAllQueryParameters(), SizeIs(0)); + request.RemoveHeader("content-type"); + EXPECT_THAT(request.GetAllHeaders(), SizeIs(0)); + request.SetBody("test body"); + EXPECT_EQ(request.GetBody().GetRawData(), "test body"); + HttpRequestBody body; + body.SetData("new body"); + request.SetBody(body); + EXPECT_EQ(request.GetBody().GetRawData(), "new body"); + body.SetData(nullptr, 100); + request.SetBody(body); + EXPECT_TRUE(request.GetBody().GetRawData().empty()); +} + +TEST(HttpRequest, TestGetMethodString) { + HttpRequest request; + request.SetMethod(HttpRequestMethod::kPost); + EXPECT_EQ(request.GetMethodString(), "POST"); + request.SetMethod(HttpRequestMethod::kPut); + EXPECT_EQ(request.GetMethodString(), "PUT"); + request.SetMethod(HttpRequestMethod::kDelete); + EXPECT_EQ(request.GetMethodString(), "DELETE"); + request.SetMethod(HttpRequestMethod::kConnect); + EXPECT_EQ(request.GetMethodString(), "CONNECT"); + request.SetMethod(HttpRequestMethod::kHead); + EXPECT_EQ(request.GetMethodString(), "HEAD"); + request.SetMethod(HttpRequestMethod::kOptions); + EXPECT_EQ(request.GetMethodString(), "OPTIONS"); + request.SetMethod(HttpRequestMethod::kPatch); + EXPECT_EQ(request.GetMethodString(), "PATCH"); + request.SetMethod(HttpRequestMethod::kTrace); + EXPECT_EQ(request.GetMethodString(), "TRACE"); +} + +} // namespace +} // namespace network +} // namespace nearby +} // namespace location diff --git a/internal/network/http_response.cc b/internal/network/http_response.cc new file mode 100644 index 00000000..c00c7e36 --- /dev/null +++ b/internal/network/http_response.cc @@ -0,0 +1,74 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "internal/network/http_response.h" + +#include +#include + +#include "absl/strings/str_cat.h" + +namespace location { +namespace nearby { +namespace network { + +void HttpResponse::SetStatusCode(const HttpStatusCode& status_code) { + status_code_ = status_code; +} + +HttpStatusCode HttpResponse::GetStatusCode() const { return status_code_; } + +void HttpResponse::SetReasonPhrase(absl::string_view reason_phrase) { + reason_phrase_ = std::string(reason_phrase); +} + +absl::string_view HttpResponse::GetReasonPhrase() const { + return reason_phrase_; +} + +void HttpResponse::AddHeader(absl::string_view header, + absl::string_view value) { + auto it = headers_.find(header); + if (it == headers_.end()) { + headers_.emplace(header, std::vector({std::string(value)})); + } else { + it->second.push_back(std::string(value)); + } +} + +void HttpResponse::SetHeaders( + const absl::flat_hash_map>& headers) { + headers_ = headers; +} + +const absl::flat_hash_map>& +HttpResponse::GetAllHeaders() const { + return headers_; +} + +void HttpResponse::RemoveHeader(absl::string_view header) { + headers_.erase(header); +} + +void HttpResponse::SetBody(const HttpResponseBody& body) { body_ = body; } + +void HttpResponse::SetBody(absl::string_view body) { + body_.SetData(body.data(), body.size()); +} + +const HttpResponseBody& HttpResponse::GetBody() const { return body_; } + +} // namespace network +} // namespace nearby +} // namespace location diff --git a/internal/network/http_response.h b/internal/network/http_response.h new file mode 100644 index 00000000..2791cdac --- /dev/null +++ b/internal/network/http_response.h @@ -0,0 +1,77 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_RESPONSE_H_ +#define THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_RESPONSE_H_ + +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/strings/string_view.h" +#include "internal/network/http_body.h" +#include "internal/network/http_request.h" +#include "internal/network/http_status_code.h" + +namespace location { +namespace nearby { +namespace network { + +class HttpResponse { + public: + HttpResponse() = default; + ~HttpResponse() = default; + + HttpResponse(const HttpResponse&) = default; + HttpResponse& operator=(const HttpResponse&) = default; + HttpResponse(HttpResponse&&) = default; + HttpResponse& operator=(HttpResponse&&) = default; + + void SetStatusCode(const HttpStatusCode& status_code); + HttpStatusCode GetStatusCode() const; + + void SetReasonPhrase(absl::string_view reason_phrase); + absl::string_view GetReasonPhrase() const; + + void AddHeader(absl::string_view header, absl::string_view value); + void RemoveHeader(absl::string_view header); + void SetHeaders(const absl::flat_hash_map>& headers); + const absl::flat_hash_map>& + GetAllHeaders() const; + + void SetBody(const HttpResponseBody& body); + void SetBody(absl::string_view body); + const HttpResponseBody& GetBody() const; + + private: + // The status code returned from remote server + HttpStatusCode status_code_ = HttpStatusCode::kHttpOk; + + // Provides a short textual description of the Status Code. + std::string reason_phrase_; + + // The response headers returned from remove server + absl::flat_hash_map> headers_; + + // The response content from remote server, it may be empty + HttpResponseBody body_; +}; + +} // namespace network +} // namespace nearby +} // namespace location + +#endif // THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_RESPONSE_H_ diff --git a/internal/network/http_response_test.cc b/internal/network/http_response_test.cc new file mode 100644 index 00000000..53114a00 --- /dev/null +++ b/internal/network/http_response_test.cc @@ -0,0 +1,47 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "internal/network/http_response.h" + +#include "gmock/gmock.h" +#include "protobuf-matchers/protocol-buffer-matchers.h" +#include "gtest/gtest.h" + +namespace location { +namespace nearby { +namespace network { +namespace { + +using ::testing::SizeIs; + +TEST(HttpResponse, TestBuildResponse) { + HttpResponse response; + response.SetStatusCode(HttpStatusCode::kHttpNotFound); + EXPECT_EQ(response.GetStatusCode(), HttpStatusCode::kHttpNotFound); + response.SetReasonPhrase("OK"); + EXPECT_EQ(response.GetReasonPhrase(), "OK"); + response.AddHeader("Content-Type", "text/html"); + EXPECT_THAT(response.GetAllHeaders(), SizeIs(1)); + response.RemoveHeader("Content-Type"); + EXPECT_THAT(response.GetAllHeaders(), SizeIs(0)); + HttpResponseBody body; + body.SetData("test body"); + response.SetBody(body); + EXPECT_EQ(response.GetBody().GetRawData(), "test body"); +} + +} // namespace +} // namespace network +} // namespace nearby +} // namespace location diff --git a/internal/network/http_status_code.cc b/internal/network/http_status_code.cc new file mode 100644 index 00000000..25c010cd --- /dev/null +++ b/internal/network/http_status_code.cc @@ -0,0 +1,118 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "internal/network/http_status_code.h" + +namespace location { +namespace nearby { +namespace network { + +absl::string_view GetHttpReasonPhrase(HttpStatusCode code) { + switch (code) { + case HttpStatusCode::kHttpContinue: + return "Continue"; + case HttpStatusCode::kHttpSwitchingProtocols: + return "Switching Protocols"; + case HttpStatusCode::kEarlyHints: + return "Early Hints"; + case HttpStatusCode::kHttpOk: + return "OK"; + case HttpStatusCode::kHttpCreated: + return "Created"; + case HttpStatusCode::kHttpAccepted: + return "Accepted"; + case HttpStatusCode::kHttpNonAuthoritativeInformation: + return "Non-Authoritative Information"; + case HttpStatusCode::kHttpNoContent: + return "No Content"; + case HttpStatusCode::kHttpResetContent: + return "Reset Content"; + case HttpStatusCode::kHttpPartialContent: + return "Partial Content"; + case HttpStatusCode::kHttpMultipleChocies: + return "Multiple Choices"; + case HttpStatusCode::kHttpMovedPermancently: + return "Moved Permanently"; + case HttpStatusCode::kHttpFound: + return "Found"; + case HttpStatusCode::kHttpSeeOther: + return "See Other"; + case HttpStatusCode::kHttpNotModified: + return "Not Modified"; + case HttpStatusCode::kHttpUseProxy: + return "Use Proxy"; + case HttpStatusCode::kHttpTemporaryRedirect: + return "Temporary Redirect"; + case HttpStatusCode::kHttpPermanentRedirect: + return "Permanent Redirect"; + case HttpStatusCode::kHttpBadRequest: + return "Bad Request"; + case HttpStatusCode::kHttpUnauthorized: + return "Unauthorized"; + case HttpStatusCode::kHttpPaymentRequest: + return "Payment Required"; + case HttpStatusCode::kHttpForbidden: + return "Forbidden"; + case HttpStatusCode::kHttpNotFound: + return "Not Found"; + case HttpStatusCode::kHttpMethodNotAllowed: + return "Method Not Allowed"; + case HttpStatusCode::kHttpNotAcceptable: + return "Not Acceptable"; + case HttpStatusCode::kHttpProxyAuthenticationRequired: + return "Proxy Authentication Required"; + case HttpStatusCode::kHttpRequestTimeout: + return "Request Timeout"; + case HttpStatusCode::kHttpConflict: + return "Conflict"; + case HttpStatusCode::kHttpGone: + return "Gone"; + case HttpStatusCode::kHttpLengthRequired: + return "Length Required"; + case HttpStatusCode::kHttpPreconditionFailed: + return "Precondition Failed"; + case HttpStatusCode::kHttpRequestEntityTooLarge: + return "Request Entity Too Large"; + case HttpStatusCode::kHttpRequestUriTooLong: + return "Request-URI Too Long"; + case HttpStatusCode::kHttpUnsupportedMediaType: + return "Unsupported Media Type"; + case HttpStatusCode::kHttpRequestedRangeNotSatisfiable: + return "Requested Range Not Satisfiable"; + case HttpStatusCode::kHttpExpectationFailed: + return "Expectation Failed"; + case HttpStatusCode::kHttpInvalidXprivetToken: + return "Invalid XPrivet Token"; + case HttpStatusCode::kHttpTooEarly: + return "Too Early"; + case HttpStatusCode::kHttpTooManyRequests: + return "Too Many Requests"; + case HttpStatusCode::kHttpInternalServerError: + return "Internal Server Error"; + case HttpStatusCode::kHttpNotImplemented: + return "Not Implemented"; + case HttpStatusCode::kHttpBadGateway: + return "Bad Gateway"; + case HttpStatusCode::kHttpServiceUnavailable: + return "Service Unavailable"; + case HttpStatusCode::kHttpGatewayTimeout: + return "Gateway Timeout"; + case HttpStatusCode::kHttpVersionNotSupported: + return "HTTP Version Not Supported"; + } +} + +} // namespace network +} // namespace nearby +} // namespace location diff --git a/internal/network/http_status_code.h b/internal/network/http_status_code.h new file mode 100644 index 00000000..4f8f8c8d --- /dev/null +++ b/internal/network/http_status_code.h @@ -0,0 +1,93 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_STATUS_CODE_H_ +#define THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_STATUS_CODE_H_ + +#include "absl/strings/string_view.h" + +namespace location { +namespace nearby { +namespace network { + +// HTTP response status codes are separated into five categories. Details can be +// found in https://en.wikipedia.org/wiki/List_of_HTTP_status_codes +enum class HttpStatusCode { + // Informational 1xx: The request has been accepted and needs to be processed + // further. + kHttpContinue = 100, + kHttpSwitchingProtocols = 101, + kEarlyHints = 103, + + // Information 2xx: The request has been accepted and approved by the server. + kHttpOk = 200, + kHttpCreated = 201, + kHttpAccepted = 202, + kHttpNonAuthoritativeInformation = 203, + kHttpNoContent = 204, + kHttpResetContent = 205, + kHttpPartialContent = 206, + + // Redirection 3xx: The request needs client's further action to be completed. + kHttpMultipleChocies = 300, + kHttpMovedPermancently = 301, + kHttpFound = 302, + kHttpSeeOther = 303, + kHttpNotModified = 304, + kHttpUseProxy = 305, + // 306 is no longer used. + kHttpTemporaryRedirect = 307, + kHttpPermanentRedirect = 308, + + // Client error 4xx: There may be errors in the client that prevent the server + // from processing the request. + kHttpBadRequest = 400, + kHttpUnauthorized = 401, + kHttpPaymentRequest = 402, + kHttpForbidden = 403, + kHttpNotFound = 404, + kHttpMethodNotAllowed = 405, + kHttpNotAcceptable = 406, + kHttpProxyAuthenticationRequired = 407, + kHttpRequestTimeout = 408, + kHttpConflict = 409, + kHttpGone = 410, + kHttpLengthRequired = 411, + kHttpPreconditionFailed = 412, + kHttpRequestEntityTooLarge = 413, + kHttpRequestUriTooLong = 414, + kHttpUnsupportedMediaType = 415, + kHttpRequestedRangeNotSatisfiable = 416, + kHttpExpectationFailed = 417, + // 418 returned by Cloud Print. + kHttpInvalidXprivetToken = 418, + kHttpTooEarly = 425, + kHttpTooManyRequests = 429, + + // Server error 5xx + kHttpInternalServerError = 500, + kHttpNotImplemented = 501, + kHttpBadGateway = 502, + kHttpServiceUnavailable = 503, + kHttpGatewayTimeout = 504, + kHttpVersionNotSupported = 505 +}; + +absl::string_view GetHttpReasonPhrase(HttpStatusCode code); + +} // namespace network +} // namespace nearby +} // namespace location + +#endif // THIRD_PARTY_NEARBY_INTERNAL_NETWORK_HTTP_STATUS_CODE_H_ diff --git a/internal/network/http_status_code_test.cc b/internal/network/http_status_code_test.cc new file mode 100644 index 00000000..9e674553 --- /dev/null +++ b/internal/network/http_status_code_test.cc @@ -0,0 +1,109 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "internal/network/http_status_code.h" + +#include "gtest/gtest.h" + +namespace location { +namespace nearby { +namespace network { +namespace { + +TEST(HttpStatusCode, TestReturnCorrectReasonPhrase) { + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpContinue), "Continue"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpSwitchingProtocols), + "Switching Protocols"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kEarlyHints), "Early Hints"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpOk), "OK"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpCreated), "Created"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpAccepted), "Accepted"); + EXPECT_EQ( + GetHttpReasonPhrase(HttpStatusCode::kHttpNonAuthoritativeInformation), + "Non-Authoritative Information"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpNoContent), "No Content"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpResetContent), + "Reset Content"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpPartialContent), + "Partial Content"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpMultipleChocies), + "Multiple Choices"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpMovedPermancently), + "Moved Permanently"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpFound), "Found"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpSeeOther), "See Other"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpNotModified), + "Not Modified"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpUseProxy), "Use Proxy"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpTemporaryRedirect), + "Temporary Redirect"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpPermanentRedirect), + "Permanent Redirect"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpBadRequest), + "Bad Request"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpUnauthorized), + "Unauthorized"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpPaymentRequest), + "Payment Required"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpForbidden), "Forbidden"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpNotFound), "Not Found"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpMethodNotAllowed), + "Method Not Allowed"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpNotAcceptable), + "Not Acceptable"); + EXPECT_EQ( + GetHttpReasonPhrase(HttpStatusCode::kHttpProxyAuthenticationRequired), + "Proxy Authentication Required"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpRequestTimeout), + "Request Timeout"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpConflict), "Conflict"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpGone), "Gone"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpLengthRequired), + "Length Required"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpPreconditionFailed), + "Precondition Failed"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpRequestEntityTooLarge), + "Request Entity Too Large"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpUnsupportedMediaType), + "Unsupported Media Type"); + EXPECT_EQ( + GetHttpReasonPhrase(HttpStatusCode::kHttpRequestedRangeNotSatisfiable), + "Requested Range Not Satisfiable"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpExpectationFailed), + "Expectation Failed"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpInvalidXprivetToken), + "Invalid XPrivet Token"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpTooEarly), "Too Early"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpTooManyRequests), + "Too Many Requests"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpInternalServerError), + "Internal Server Error"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpNotImplemented), + "Not Implemented"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpBadGateway), + "Bad Gateway"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpServiceUnavailable), + "Service Unavailable"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpGatewayTimeout), + "Gateway Timeout"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpVersionNotSupported), + "HTTP Version Not Supported"); + EXPECT_EQ(GetHttpReasonPhrase(HttpStatusCode::kHttpRequestUriTooLong), + "Request-URI Too Long"); +} + +} // namespace +} // namespace network +} // namespace nearby +} // namespace location diff --git a/internal/network/url.cc b/internal/network/url.cc new file mode 100644 index 00000000..dfc39457 --- /dev/null +++ b/internal/network/url.cc @@ -0,0 +1,259 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "internal/network/url.h" + +#include +#include //NOLINT +#include +#include + +#include "absl/status/status.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "internal/network/utils.h" + +namespace location { +namespace nearby { +namespace network { + +absl::StatusOr Url::Create(absl::string_view url_string) { + Url url; + if (!url.SetUrlPath(url_string)) { + return absl::InvalidArgumentError("bad url."); + } + + return url; +} + +bool Url::SetUrlPath(absl::string_view url_path) { + return ApplyUrlString(url_path); +} + +std::string Url::GetUrlPath() const { + return scheme_ + "://" + host_ + GetPortString() + path_ + GetQueryString() + + GetFragmentString(); +} + +absl::string_view Url::GetScheme() const { return scheme_; } + +absl::string_view Url::GetHostName() const { return host_; } + +absl::string_view Url::GetPath() const { return path_; } + +uint16_t Url::GetPort() const { return port_; } + +absl::string_view Url::GetFragment() const { return fragment_; } + +bool Url::ApplyUrlString(absl::string_view url_string) { + // Refer to: https://www.rfc-editor.org/rfc/rfc3986#page-50 + std::regex url_reg( + R"(^(([^:\/?#]+):)?(//([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?)", + std::regex::extended); + + std::smatch matches; + std::string url{url_string}; + + std::regex_search(url, matches, url_reg); + scheme_ = matches[2]; + if (!(scheme_ == "http" || scheme_ == "https")) { + return false; + } + + std::string authority = matches[4]; + if (authority.empty()) { + return false; + } + + size_t pos = authority.find(':'); + if (pos != std::string::npos) { + host_ = authority.substr(0, pos); + port_ = std::stoi(authority.substr(pos + 1)); + } else { + host_ = authority; + port_ = scheme_ == "http" ? 80 : 443; + } + path_ = matches[5]; + if (path_ == "/") { + path_ = ""; + } + + query_parameters_.clear(); + std::string query = matches[7]; + if (!query.empty()) { + // split by & + size_t start = 0; + size_t end = 0; + while ((end = query.find('&', start)) != std::string::npos) { + std::string kv = query.substr(start, end - start); + start = end + 1; + size_t eq_pos = kv.find('='); + if (eq_pos <= 0 || eq_pos == std::string::npos) { + continue; + } + query_parameters_.push_back( + {UrlDecode(kv.substr(0, eq_pos)), UrlDecode(kv.substr(eq_pos + 1))}); + } + + if (start < query.size()) { + size_t eq_pos = query.find('=', start); + if (eq_pos > start && eq_pos < query.size() - 1) { + query_parameters_.push_back( + {UrlDecode(query.substr(start, eq_pos - start)), + UrlDecode(query.substr(eq_pos + 1))}); + } + } + } + + fragment_ = matches[9]; + return true; +} + +void Url::AddQueryParameter(absl::string_view query, absl::string_view value) { + query_parameters_.push_back({absl::StrCat(query), absl::StrCat(value)}); +} + +void Url::RemoveQueryParameter(absl::string_view query) { + auto it = query_parameters_.begin(); + + while (it != query_parameters_.end()) { + if (it->first == query) { + it = query_parameters_.erase(it); + } else { + ++it; + } + } +} + +std::vector Url::GetQueryValues(absl::string_view query) const { + std::vector values; + auto it = query_parameters_.begin(); + while (it != query_parameters_.end()) { + if (it->first == query) { + values.push_back(it->second); + } + ++it; + } + return values; +} + +const Url::QueryParameters& Url::GetAllQueryStrings() const { + return query_parameters_; +} + +std::string Url::GetQueryString() const { + if (query_parameters_.empty()) { + return ""; + } + + std::string query = ""; + auto it = query_parameters_.begin(); + bool first = true; + + while (it != query_parameters_.end()) { + if (first) { + query += "?"; + first = false; + } else { + query += "&"; + } + + query += UrlEncode(it->first) + "=" + UrlEncode(it->second); + + it++; + } + + return query; +} + +std::string Url::GetPortString() const { + if ((scheme_ == "http" && port_ == 80) || + (scheme_ == "https" && port_ == 443)) { + return ""; + } + + return absl::StrCat(":", port_); +} + +std::string Url::GetFragmentString() const { + if (fragment_.empty()) { + return ""; + } + + return "#" + fragment_; +} + +std::ostream& operator<<(std::ostream& os, const Url& url) { + os << url.GetUrlPath(); + return os; +} + +bool operator==(const Url& url1, const Url& url2) { + if (url1.GetScheme() != url2.GetScheme()) { + return false; + } + + if (url1.GetHostName() != url2.GetHostName()) { + return false; + } + + if (url1.GetPort() != url2.GetPort()) { + return false; + } + + if (url1.GetPath() != url2.GetPath()) { + return false; + } + + auto queries1 = url1.GetAllQueryStrings(); + auto queries2 = url2.GetAllQueryStrings(); + if (queries1.size() != queries2.size()) { + return false; + } + + std::vector state; + state.reserve(queries1.size()); + for (int i = 0; i < queries1.size(); ++i) { + state.push_back(false); + } + + for (int i = 0; i < queries1.size(); ++i) { + bool found = false; + for (int j = 0; j < queries2.size(); ++j) { + if (queries1[i].first == queries2[j].first && + queries1[i].second == queries2[j].second) { + if (state[j]) { + return false; + } + state[j] = true; + found = true; + break; + } + } + + if (!found) { + return false; + } + } + + if (url1.GetFragment() != url2.GetFragment()) { + return false; + } + + return true; +} + +} // namespace network +} // namespace nearby +} // namespace location diff --git a/internal/network/url.h b/internal/network/url.h new file mode 100644 index 00000000..9ba06f5e --- /dev/null +++ b/internal/network/url.h @@ -0,0 +1,84 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_INTERNAL_NETWORK_URL_H_ +#define THIRD_PARTY_NEARBY_INTERNAL_NETWORK_URL_H_ + +#include +#include +#include +#include +#include + +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" + +namespace location { +namespace nearby { +namespace network { + +class Url { + public: + using QueryParameters = std::vector>; + + Url() = default; + ~Url() = default; + + Url(const Url&) = default; + Url& operator=(const Url&) = default; + Url(Url&&) = default; + Url& operator=(Url&&) = default; + + static absl::StatusOr Create(absl::string_view url_string); + + bool SetUrlPath(absl::string_view url_path); + std::string GetUrlPath() const; + + absl::string_view GetScheme() const; + absl::string_view GetHostName() const; + absl::string_view GetPath() const; + uint16_t GetPort() const; + absl::string_view GetFragment() const; + + void AddQueryParameter(absl::string_view query, absl::string_view value); + void RemoveQueryParameter(absl::string_view query); + std::vector GetQueryValues(absl::string_view query) const; + const QueryParameters& GetAllQueryStrings() const; + + private: + bool ApplyUrlString(absl::string_view url_string); + std::string GetQueryString() const; + std::string GetPortString() const; + std::string GetFragmentString() const; + + // The absolute requested URL encoded in ASCII per the rules of RFC-2396. + std::string scheme_; + std::string host_; + uint16_t port_; + std::string path_; + std::string fragment_; + + // Query strings of the request, no URL encode + QueryParameters query_parameters_; +}; + +std::ostream& operator<<(std::ostream& os, const Url& url); + +bool operator==(const Url& url1, const Url& url2); + +} // namespace network +} // namespace nearby +} // namespace location + +#endif // THIRD_PARTY_NEARBY_INTERNAL_NETWORK_URL_H_ diff --git a/internal/network/url_test.cc b/internal/network/url_test.cc new file mode 100644 index 00000000..a564212e --- /dev/null +++ b/internal/network/url_test.cc @@ -0,0 +1,159 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#include "internal/network/url.h" + +#include + +#include "gmock/gmock.h" +#include "protobuf-matchers/protocol-buffer-matchers.h" +#include "gtest/gtest.h" + +namespace location { +namespace nearby { +namespace network { +namespace { + +TEST(Url, TestCreateUrl) { + auto url = Url::Create("http://www.google.com"); + ASSERT_TRUE(url.ok()); + + EXPECT_EQ(url->GetScheme(), "http"); + EXPECT_EQ(url->GetPort(), 80); + EXPECT_EQ(url->GetHostName(), "www.google.com"); + + url = Url::Create("https://www.google.com"); + ASSERT_TRUE(url.ok()); + EXPECT_EQ(url->GetScheme(), "https"); + EXPECT_EQ(url->GetPort(), 443); + + url = Url::Create("https://www.google.com:8443"); + ASSERT_TRUE(url.ok()); + EXPECT_EQ(url->GetPort(), 8443); + EXPECT_EQ(url->GetUrlPath(), "https://www.google.com:8443"); +} + +TEST(Url, TestUrlWithPath) { + auto url = Url::Create("http://www.google.com/users/user/1234"); + ASSERT_TRUE(url.ok()); + EXPECT_EQ(url->GetPath(), "/users/user/1234"); +} + +TEST(Url, TestUrlWithQuery) { + auto url = Url::Create("http://www.google.com/user?name=google&age=45"); + ASSERT_TRUE(url.ok()); + auto query_values = url->GetQueryValues("name"); + EXPECT_THAT(query_values, testing::SizeIs(1)); + EXPECT_EQ(query_values[0], "google"); +} + +TEST(Url, TestUrlWithOneQuery) { + auto url = Url::Create("http://www.google.com/user?name=google&"); + ASSERT_TRUE(url.ok()); + auto query_values = url->GetQueryValues("name"); + EXPECT_EQ(1u, query_values.size()); + EXPECT_EQ(query_values[0], "google"); +} + +TEST(Url, TestUrlWithFragment) { + auto url = + Url::Create("http://www.google.com/user?name=google&pos=mtv#hello"); + ASSERT_TRUE(url.ok()); + auto query_values = url->GetQueryValues("pos"); + EXPECT_EQ(1u, query_values.size()); + EXPECT_EQ(query_values[0], "mtv"); + EXPECT_EQ(url->GetUrlPath(), + "http://www.google.com/user?name=google&pos=mtv#hello"); + url->RemoveQueryParameter("pos"); + EXPECT_EQ(0u, url->GetQueryValues("pos").size()); +} + +TEST(Url, TestGetUrl) { + auto url = Url::Create("http://www.google.com/user?name=google&pos=mtv"); + ASSERT_TRUE(url.ok()); + EXPECT_EQ(url->GetUrlPath(), + "http://www.google.com/user?name=google&pos=mtv"); +} + +TEST(Url, TestAddQueryString) { + auto url = Url::Create("http://www.google.com/user?name=google&pos=mtv"); + ASSERT_TRUE(url.ok()); + url->AddQueryParameter("test", "good"); + auto new_url = Url::Create(url->GetUrlPath()); + ASSERT_TRUE(new_url.ok()); + auto query_values = new_url->GetQueryValues("test"); + EXPECT_EQ(1u, query_values.size()); + EXPECT_EQ(query_values[0], "good"); +} + +TEST(Url, TestAddQueryStringWithEncode) { + auto url = Url::Create("http://www.google.com"); + ASSERT_TRUE(url.ok()); + url->AddQueryParameter("test", "good & bad"); + EXPECT_EQ(url->GetUrlPath(), "http://www.google.com?test=good%20%26%20bad"); + auto new_url = Url::Create(url->GetUrlPath()); + ASSERT_TRUE(new_url.ok()); + auto query_values = new_url->GetQueryValues("test"); + EXPECT_EQ(1u, query_values.size()); + EXPECT_EQ(query_values[0], "good & bad"); +} + +TEST(Url, TestInvalidUrl) { + auto url = Url::Create("http:/www.google.com"); + ASSERT_FALSE(url.ok()); + url = Url::Create("ftp://www.google.com"); + ASSERT_FALSE(url.ok()); + url = Url::Create("::::hjskoiskjk"); + ASSERT_FALSE(url.ok()); +} + +TEST(Url, TestStreamOutput) { + std::ostringstream stream; + auto url = Url::Create("https://www.google.com"); + ASSERT_TRUE(url.ok()); + stream << url.value(); + EXPECT_EQ(stream.str(), "https://www.google.com"); +} + +TEST(Url, TestCompareUrl) { + auto url1 = Url::Create("https://www.google.com"); + auto url2 = Url::Create("https://www.google.com/"); + ASSERT_TRUE(url1.ok()); + ASSERT_TRUE(url2.ok()); + ASSERT_EQ(url1, url2); + url1 = Url::Create("https://www.google.com/home?name=test&age=38"); + ASSERT_NE(url1, url2); + url2 = Url::Create("https://www.google.com/home?age=38&name=test"); + ASSERT_EQ(url1, url2); + url2 = Url::Create("https://www.google.com/home?age=38&name=test&grade=5"); + ASSERT_NE(url1, url2); + url2 = Url::Create("https://www.google.com/home?age=38&name=test#fragment"); + ASSERT_NE(url1, url2); + url2 = Url::Create("https://www.google.com/home?age1=38&name1=test"); + ASSERT_NE(url1, url2); + url2 = Url::Create("http://www.google.com/home?age1=38&name1=test"); + ASSERT_NE(url1, url2); + url2 = Url::Create("https://www.google1.com/home?age1=38&name1=test"); + ASSERT_NE(url1, url2); + url2 = Url::Create("https://www.google.com:8433/home?age=38&name=test"); + ASSERT_NE(url1, url2); + url2 = Url::Create("https://www.google.com/home?age=38&age=38"); + ASSERT_NE(url2, url1); + url2 = Url::Create("https://www.google.com/home?age=38&name=test&kk&=888"); + ASSERT_EQ(url1, url2); +} + +} // namespace +} // namespace network +} // namespace nearby +} // namespace location diff --git a/internal/network/utils.cc b/internal/network/utils.cc new file mode 100644 index 00000000..93c58f3b --- /dev/null +++ b/internal/network/utils.cc @@ -0,0 +1,92 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "internal/network/utils.h" + +#include +#include +#include +#include +#include + +namespace location { +namespace nearby { +namespace network { + +// Encodes string to URL-encoded string. +// +// If a character c is a decimal digit, an uppercase or lowercase letter, or in +// [-_.!~*'()] then nothing is done; otherwise c is encoded as %XX where XX is +// the hex value of the character c. +std::string UrlEncode(absl::string_view str) { + std::ostringstream encoded_string_stream; + encoded_string_stream.fill(0); + encoded_string_stream << std::hex; + auto it = str.begin(); + while (it != str.end()) { + char ch = (*it); + if (std::isalnum(ch) || ch == '-' || ch == '_' || ch == '.' || ch == '!' || + ch == '~' || ch == '*' || ch == '\'' || ch == '(' || ch == ')') { + encoded_string_stream << ch; + } else { + encoded_string_stream << std::uppercase; + encoded_string_stream << '%' << std::setw(2) << (ch & 0xFF); + encoded_string_stream << std::nouppercase; + } + + ++it; + } + + return encoded_string_stream.str(); +} + +// Decodes URL string to normal string. +// +// Does the reverse operation comparing to UrlEncode. +// More details can refer to https://www.ietf.org/rfc/rfc2396.txt +std::string UrlDecode(absl::string_view url_string) { + std::ostringstream decoded_string_stream; + decoded_string_stream.fill(0); + decoded_string_stream << std::hex; + auto it = url_string.begin(); + + while (it != url_string.end()) { + char ch = (*it); + + if (ch == '%') { + if (it + 1 != url_string.end() && it + 2 != url_string.end()) { + char chh = *(it + 1); + char chl = *(it + 2); + ch = (chh >= '0' && chh <= '9' ? chh - '0' + : std::tolower(chh) - 'a' + 10) + << 4 | + (chl >= '0' && chl <= '9' ? chl - '0' + : std::tolower(chl) - 'a' + 10); + decoded_string_stream << ch; + it += 3; + continue; + } + } else { + decoded_string_stream << ch; + } + + ++it; + } + + return decoded_string_stream.str(); +} + +} // namespace network +} // namespace nearby +} // namespace location diff --git a/internal/network/utils.h b/internal/network/utils.h new file mode 100644 index 00000000..d8a9b24c --- /dev/null +++ b/internal/network/utils.h @@ -0,0 +1,45 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_INTERNAL_NETWORK_UTILS_H_ +#define THIRD_PARTY_NEARBY_INTERNAL_NETWORK_UTILS_H_ + +#include + +#include "absl/strings/string_view.h" +#include "internal/network/url.h" + +namespace location { +namespace nearby { +namespace network { + +// Encodes string to URL-encoded format. URLs can only be sent over the Internet +// using the ASCII character-set. This method converts characters into a format +// that can be transmitted over the Internet. +// +// @param str The string to be encoded. +// @return Encoded URL string from input. +std::string UrlEncode(absl::string_view str); + +// Decodes URL string to normal string. +// +// @param url_string URL string to be decoded. +// @return Decoded string. +std::string UrlDecode(absl::string_view url_string); + +} // namespace network +} // namespace nearby +} // namespace location + +#endif // THIRD_PARTY_NEARBY_INTERNAL_NETWORK_UTILS_H_ diff --git a/internal/network/utils_test.cc b/internal/network/utils_test.cc new file mode 100644 index 00000000..7715b45b --- /dev/null +++ b/internal/network/utils_test.cc @@ -0,0 +1,48 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "internal/network/utils.h" + +#include + +#include "gtest/gtest.h" + +namespace location { +namespace nearby { +namespace network { +namespace { + +TEST(UrlEncode, TestEncode) { + EXPECT_EQ(UrlEncode("abc12&()*t"), "abc12%26()*t"); +} + +TEST(UrlDecode, TestDecode) { + EXPECT_EQ(UrlDecode("abc12%26()*t"), "abc12&()*t"); +} + +TEST(UrlDecode, TestDecodeMatch) { + std::string encode = + UrlEncode("fsfs 1892427891$^%$*#$@ZDRIYIUHiheiuah bfd~+_()[]\\|©"); + std::string decode = UrlDecode(encode); + + EXPECT_EQ(decode, "fsfs 1892427891$^%$*#$@ZDRIYIUHiheiuah bfd~+_()[]\\|©"); + EXPECT_EQ(encode, + "fsfs%201892427891%24%5E%25%24*%23%24%40ZDRIYIUHiheiuah%20bfd~%2B_(" + ")%5B%5D%5C%7C%C2%A9"); +} + +} // namespace +} // namespace network +} // namespace nearby +} // namespace location diff --git a/internal/platform/BUILD b/internal/platform/BUILD index f6ee4cac..ca272e81 100644 --- a/internal/platform/BUILD +++ b/internal/platform/BUILD @@ -100,7 +100,9 @@ cc_library( visibility = [ "//connections:__subpackages__", "//fastpair:__subpackages__", + "//internal:__pkg__", "//internal/crypto:__subpackages__", + "//internal/network:__subpackages__", "//internal/platform:__subpackages__", "//internal/proto/analytics:__subpackages__", "//location/nearby/analytics/cpp:__subpackages__", @@ -393,11 +395,11 @@ cc_library( "//presence:__subpackages__", ], deps = [ + ":base", + ":cancellation_flag", ":logging", ":types", ":uuid", - "//internal/platform:base", - "//internal/platform:cancellation_flag", "//internal/platform/implementation:comm", "//internal/platform/implementation:platform", "@com_google_absl//absl/container:flat_hash_map", diff --git a/internal/platform/implementation/BUILD b/internal/platform/implementation/BUILD index 70855628..d224f3b6 100644 --- a/internal/platform/implementation/BUILD +++ b/internal/platform/implementation/BUILD @@ -59,6 +59,7 @@ cc_library( "bluetooth_classic.h", "credential_callbacks.h", "credential_storage.h", + "http_loader.h", "server_sync.h", "wifi.h", "wifi_direct.h", @@ -74,6 +75,8 @@ cc_library( defines = ["NO_WEBRTC"], visibility = [ "//connections/implementation:__subpackages__", + "//internal:__pkg__", + "//internal/network:__subpackages__", "//internal/platform:__pkg__", "//internal/platform/implementation:__subpackages__", "//presence:__pkg__", @@ -88,6 +91,7 @@ cc_library( "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/functional:any_invocable", "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", "@com_google_absl//absl/strings:str_format", ], @@ -101,15 +105,20 @@ cc_library( defines = ["NO_WEBRTC"], visibility = [ "//connections/implementation:__subpackages__", + "//fastpair:__subpackages__", "//googlemac/iPhone/Shared/Nearby/Connections:__subpackages__", + "//internal:__pkg__", + "//internal/network:__subpackages__", "//internal/platform:__pkg__", "//internal/platform/implementation:__subpackages__", "//location/nearby/analytics/cpp:__subpackages__", + "//location/nearby/cpp/fastpair:__subpackages__", ], deps = [ ":comm", ":types", "//internal/platform:base", + "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", ], ) diff --git a/internal/platform/implementation/apple/platform.mm b/internal/platform/implementation/apple/platform.mm index 8ba6e227..cd9b13f7 100644 --- a/internal/platform/implementation/apple/platform.mm +++ b/internal/platform/implementation/apple/platform.mm @@ -15,6 +15,7 @@ #include "internal/platform/implementation/platform.h" #include +#include #include "internal/platform/implementation/apple/atomic_boolean.h" #include "internal/platform/implementation/apple/atomic_uint32.h" @@ -170,6 +171,12 @@ std::unique_ptr ImplementationPlatform::CreateWifiDirectMedium std::unique_ptr ImplementationPlatform::CreateWebRtcMedium() { return nullptr; } #endif +// TODO(b/261511669): Add implementation. +absl::StatusOr ImplementationPlatform::SendRequest( + const WebRequest& request) { + return absl::UnimplementedError(""); +} + } // namespace api } // namespace nearby } // namespace location diff --git a/internal/platform/implementation/g3/BUILD b/internal/platform/implementation/g3/BUILD index efeea4da..1920fca9 100644 --- a/internal/platform/implementation/g3/BUILD +++ b/internal/platform/implementation/g3/BUILD @@ -122,7 +122,9 @@ cc_library( visibility = [ "//connections:__subpackages__", "//fastpair:__subpackages__", + "//internal:__pkg__", "//internal/analytics:__subpackages__", + "//internal/network:__subpackages__", "//internal/platform:__subpackages__", "//internal/proto/analytics:__subpackages__", "//location/nearby/cpp/fastpair:__subpackages__", @@ -140,7 +142,9 @@ cc_library( "//internal/platform/implementation/shared:count_down_latch", "//internal/platform/implementation/shared:file", "@com_google_absl//absl/base:core_headers", + "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/memory", + "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", "@com_google_absl//absl/time", ], diff --git a/internal/platform/implementation/g3/platform.cc b/internal/platform/implementation/g3/platform.cc index 8f46e46c..7a151381 100644 --- a/internal/platform/implementation/g3/platform.cc +++ b/internal/platform/implementation/g3/platform.cc @@ -21,7 +21,9 @@ #include "file/base/path.h" #include "absl/memory/memory.h" +#include "absl/status/statusor.h" #include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" #include "absl/time/time.h" #include "internal/platform/implementation/atomic_boolean.h" #include "internal/platform/implementation/atomic_reference.h" @@ -205,6 +207,11 @@ std::unique_ptr ImplementationPlatform::CreateWebRtcMedium() { } #endif +absl::StatusOr ImplementationPlatform::SendRequest( + const api::WebRequest& request) { + return absl::UnimplementedError(""); +} + std::unique_ptr ImplementationPlatform::CreateMutex(Mutex::Mode mode) { if (mode == Mutex::Mode::kRecursive) return std::make_unique(); diff --git a/internal/platform/implementation/http_loader.h b/internal/platform/implementation/http_loader.h new file mode 100644 index 00000000..ba4d11a4 --- /dev/null +++ b/internal/platform/implementation/http_loader.h @@ -0,0 +1,43 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_HTTP_LOADER_H_ +#define THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_HTTP_LOADER_H_ + +#include +#include + +namespace location { +namespace nearby { +namespace api { + +struct WebRequest { + std::string url; + std::string method; + std::multimap headers; + std::string body; +}; + +struct WebResponse { + int status_code; + std::string status_text; + std::multimap headers; + std::string body; +}; + +} // namespace api +} // namespace nearby +} // namespace location + +#endif // THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_HTTP_LOADER_H_ diff --git a/internal/platform/implementation/platform.h b/internal/platform/implementation/platform.h index b22f76f1..a6b9a87b 100644 --- a/internal/platform/implementation/platform.h +++ b/internal/platform/implementation/platform.h @@ -19,6 +19,7 @@ #include #include +#include "absl/status/statusor.h" #include "absl/strings/string_view.h" #include "internal/platform/implementation/atomic_boolean.h" #include "internal/platform/implementation/atomic_reference.h" @@ -30,6 +31,7 @@ #include "internal/platform/implementation/count_down_latch.h" #include "internal/platform/implementation/credential_storage.h" #include "internal/platform/implementation/crypto.h" +#include "internal/platform/implementation/http_loader.h" #include "internal/platform/implementation/input_file.h" #include "internal/platform/implementation/log_message.h" #include "internal/platform/implementation/mutex.h" @@ -130,6 +132,15 @@ class ImplementationPlatform { #ifndef NO_WEBRTC static std::unique_ptr CreateWebRtcMedium(); #endif + + // Gets HTTP response from remote server. + // + // @param request Webrequest + // + // @return returns absl::FailedPreconditionError if having platform error. + // return WebResponse if HTTP status code between 200 and 300. + // other cases will return absl Status in error. + static absl::StatusOr SendRequest(const WebRequest& request); }; } // namespace api diff --git a/internal/platform/implementation/windows/BUILD b/internal/platform/implementation/windows/BUILD index 1984b459..7d84e59d 100644 --- a/internal/platform/implementation/windows/BUILD +++ b/internal/platform/implementation/windows/BUILD @@ -62,6 +62,7 @@ cc_library( "executor.h", "file.h", "file_path.h", + "http_loader.h", "mutex.h", "scheduled_executor.h", "server_sync.h", @@ -84,6 +85,9 @@ cc_library( "@com_google_absl//absl/container:flat_hash_map", "@com_google_absl//absl/container:flat_hash_set", "@com_google_absl//absl/memory", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", "@com_google_absl//absl/synchronization", "@com_google_absl//absl/types:optional", ], @@ -128,6 +132,7 @@ cc_library( "executor.cc", "file.cc", "file_path.cc", + "http_loader.cc", "platform.cc", "scheduled_executor.cc", "submittable_executor.cc", @@ -157,6 +162,7 @@ cc_library( ":types", "//internal/platform:base", "//internal/platform:cancellation_flag", + "//internal/platform:logging", "//internal/platform:types", "//internal/platform/implementation:platform", "//internal/platform/implementation:types", @@ -164,6 +170,8 @@ cc_library( "//internal/platform/implementation/shared:file", "//internal/platform/implementation/windows/generated:types", "//internal/platform/implementation/windows/json:types", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/status", "@com_google_absl//absl/strings", "@com_google_absl//absl/strings:str_format", ], @@ -199,6 +207,7 @@ cc_test( "crypto_test.cc", "executor_test.cc", "file_path_test.cc", + "http_loader_test.cc", "scheduled_executor_test.cc", "submittable_executor_test.cc", "thread_pool_test.cc", @@ -216,6 +225,8 @@ cc_test( "//internal/platform/implementation/windows", "//internal/platform/implementation/windows/generated:types", "@com_github_protobuf_matchers//protobuf-matchers", + "@com_google_absl//absl/status", + "@com_google_absl//absl/strings", "@com_google_googletest//:gtest_main", ], ) diff --git a/internal/platform/implementation/windows/generated/BUILD b/internal/platform/implementation/windows/generated/BUILD index 2bfc78dd..155027be 100644 --- a/internal/platform/implementation/windows/generated/BUILD +++ b/internal/platform/implementation/windows/generated/BUILD @@ -16,6 +16,7 @@ licenses(["notice"]) cc_library( name = "types", linkopts = [ + "wininet.lib", "advapi32.lib", "comdlg32.lib", "gdi32.lib", diff --git a/internal/platform/implementation/windows/http_loader.cc b/internal/platform/implementation/windows/http_loader.cc new file mode 100644 index 00000000..c30adecd --- /dev/null +++ b/internal/platform/implementation/windows/http_loader.cc @@ -0,0 +1,413 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "internal/platform/implementation/windows/http_loader.h" + +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/ascii.h" +#include "absl/strings/numbers.h" +#include "absl/strings/str_cat.h" +#include "internal/platform/logging.h" + +namespace location { +namespace nearby { +namespace windows { +namespace { + +constexpr DWORD kSchemaMaximumLength = 10; +constexpr DWORD kHostNameMaximumLength = 256; + +using ::location::nearby::api::WebResponse; + +} // namespace + +absl::StatusOr HttpLoader::GetResponse() { + absl::Status status; + + status = ParseUrl(); + if (!status.ok()) { + return status; + } + + status = ConnectWebServer(); + if (!status.ok()) { + return status; + } + + // Sends request to web server. + status = SendRequest(); + if (!status.ok()) { + return status; + } + + // Processes response from web server + absl::StatusOr result = ProcessResponse(); + if (!result.ok()) { + return result; + } + + DisconnectWebServer(); + return result; +} + +absl::StatusOr HttpLoader::QueryStatusCode(HINTERNET file_handle) { + int result; + std::string status_code; + absl::Status status; + status = QueryResponseInfo(file_handle, HTTP_QUERY_STATUS_CODE, status_code); + if (!status.ok()) { + return status; + } + + if (!absl::SimpleAtoi( + absl::string_view(status_code.data(), status_code.size() - 1), + &result)) { + return absl::InternalError("Failed to parse status code."); + } + + if (result < 0) { + return absl::InternalError("Invalid status code."); + } + + return result; +} + +absl::StatusOr HttpLoader::QueryStatusText( + HINTERNET request_handle) { + absl::Status status; + std::string status_text; + + status = + QueryResponseInfo(request_handle, HTTP_QUERY_STATUS_TEXT, status_text); + if (!status.ok()) { + return status; + } + + // status_text is ended with char '\0', remove it in returned result. + return status_text.substr(0, status_text.size() - 1); +} + +absl::StatusOr> +HttpLoader::QueryResponseHeaders(HINTERNET request_handle) { + absl::Status status; + std::string headers_string; + + status = QueryResponseInfo(request_handle, HTTP_QUERY_RAW_HEADERS_CRLF, + headers_string); + if (!status.ok()) { + return status; + } + + std::multimap headers; + // Parse headers in response + size_t start = 0; + size_t pos = 0; + while ((pos = headers_string.find("\r\n", start)) != std::string::npos) { + std::string header = headers_string.substr(start, pos - start); + // Get key and value in header + size_t split_pos = 0; + if ((split_pos = header.find(": ")) != std::string::npos) { + std::string key = header.substr(0, split_pos); + std::string value = header.substr(split_pos + 2); + headers.emplace(key, value); + } + + start = pos + 2; + } + + return headers; +} + +absl::Status HttpLoader::QueryResponseInfo(HINTERNET request_handle, + DWORD info_level, + std::string& info) { + DWORD buffer_size = 0; + BOOL query_result = HttpQueryInfoA(request_handle, info_level, nullptr, + &buffer_size, nullptr); + + if (query_result) { + return absl::OkStatus(); + } + + if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { + info.resize(buffer_size); + query_result = HttpQueryInfoA(request_handle, info_level, info.data(), + &buffer_size, nullptr); + + if (query_result) { + return absl::OkStatus(); + } + } + + return absl::InvalidArgumentError("Failed to query HTTP information."); +} + +absl::Status HttpLoader::ParseUrl() { + const std::string& url = request_.url; + URL_COMPONENTSA url_components; + char schema[kSchemaMaximumLength]; + char host_name[kHostNameMaximumLength]; + std::string url_path; + url_path.resize(url.size()); + + memset(&url_components, 0, sizeof(URL_COMPONENTSA)); + url_components.dwStructSize = sizeof(URL_COMPONENTSA); + url_components.lpszScheme = schema; + url_components.dwSchemeLength = kSchemaMaximumLength; + url_components.lpszHostName = host_name; + url_components.dwHostNameLength = kHostNameMaximumLength; + url_components.lpszUrlPath = url_path.data(); + url_components.dwUrlPathLength = url.size(); + + if (!InternetCrackUrlA(url.data(), url.size(), 0, &url_components)) { + return absl::InvalidArgumentError("Invalid URL."); + } + + schema_.assign(url_components.lpszScheme, url_components.dwSchemeLength); + + if (!(schema_ == "http" || schema_ == "https")) { + return absl::InvalidArgumentError("URL supports HTTP and HTTPS only."); + } + + if (schema_ == "https") { + is_secure_ = true; + } + + host_.assign(url_components.lpszHostName, url_components.dwHostNameLength); + path_.assign(url_components.lpszUrlPath, url_components.dwUrlPathLength); + port_ = url_components.nPort; + + return absl::OkStatus(); +} + +absl::Status HttpLoader::ConnectWebServer() { + internet_handle_ = InternetOpenA("Mozilla/5.0", /*Agent*/ + INTERNET_OPEN_TYPE_PRECONFIG, /*Access Type*/ + nullptr, /*Proxy*/ + nullptr, /*Proxy bypass*/ + 0); /*Flags*/ + + if (internet_handle_ == nullptr) { + NEARBY_LOGS(ERROR) << "Failed to open internet with error " + << GetLastError() << "."; + return absl::FailedPreconditionError(absl::StrCat(GetLastError())); + } + + connect_handle_ = InternetConnectA(internet_handle_, /*Internet*/ + host_.c_str(), /*Server name*/ + port_, /*Port*/ + nullptr, /*User name*/ + nullptr, /*Password*/ + INTERNET_SERVICE_HTTP, /*Service*/ + 0, /*Flags*/ + 0); /*Context*/ + + if (connect_handle_ == nullptr) { + NEARBY_LOGS(ERROR) << "Failed to connect remote web server with error " + << GetLastError() << "."; + InternetCloseHandle(internet_handle_); + return absl::FailedPreconditionError(absl::StrCat(GetLastError())); + } + + return absl::OkStatus(); +} + +absl::Status HttpLoader::SendRequest() { + DWORD flags = INTERNET_FLAG_NO_AUTO_REDIRECT; + if (is_secure_) { + flags |= INTERNET_FLAG_SECURE; + } + + request_handle_ = + HttpOpenRequestA(connect_handle_, request_.method.c_str(), /*Method*/ + path_.c_str(), /*Path*/ + nullptr, /*HTTP version*/ + nullptr, /*Referrer*/ + nullptr, /*Accept types*/ + flags, /*Internet options*/ + 0); + + if (request_handle_ == nullptr) { + NEARBY_LOGS(ERROR) + << "Failed to open request to remote web server with error " + << GetLastError() << "."; + InternetCloseHandle(internet_handle_); + InternetCloseHandle(connect_handle_); + + return absl::FailedPreconditionError(absl::StrCat(GetLastError())); + } + + // Prepare headers + LPCSTR headers_ptr = nullptr; + DWORD headers_size = 0; + std::string request_headers; + for (const auto& header : request_.headers) { + if (header.second.empty()) { + continue; + } + + request_headers.append(header.first + ": " + header.second + "\r\n"); + } + + if (!request_headers.empty()) { + headers_ptr = request_headers.data(); + headers_size = request_headers.size(); + } + + LPVOID data_ptr = nullptr; + DWORD data_size = 0; + + // Prepare request data + if (!request_.body.empty()) { + data_ptr = (LPVOID)(request_.body.data()); + data_size = request_.body.size(); + } + + BOOL result = HttpSendRequestA(request_handle_, /*Request*/ + headers_ptr, /*Headers*/ + headers_size, /*Header size*/ + data_ptr, /*Data*/ + data_size); /*Data size*/ + + if (result == FALSE) { + NEARBY_LOGS(ERROR) + << "Failed to send request to remote web server with error " + << GetLastError() << "."; + InternetCloseHandle(request_handle_); + InternetCloseHandle(connect_handle_); + InternetCloseHandle(internet_handle_); + + return absl::FailedPreconditionError(absl::StrCat(GetLastError())); + } + + return absl::OkStatus(); +} + +absl::StatusOr HttpLoader::ProcessResponse() { + absl::Status status; + WebResponse web_response; + auto status_code = QueryStatusCode(request_handle_); + if (!status_code.ok()) { + return absl::InternalError("Failed to read HTTP status"); + } + + web_response.status_code = status_code.value(); + auto status_text = QueryStatusText(request_handle_); + if (!status_text.ok()) { + return absl::InternalError("Failed to read HTTP status"); + } + + web_response.status_text = status_text.value(); + auto headers = QueryResponseHeaders(request_handle_); + if (!headers.ok()) { + headers.status(); + } + web_response.headers = *headers; + + // Get response data + char buffer[kReceiveBufferSize]; + + while (true) { + DWORD read_size; + BOOL read_result = InternetReadFile(request_handle_, buffer, + kReceiveBufferSize, &read_size); + + if (read_result) { + if (read_size == 0) { + break; + } else { + // Append data to response + web_response.body.append(buffer, read_size); + } + } else { + NEARBY_LOGS(ERROR) + << "Failed to read response from remote web server with error " + << GetLastError() << "."; + InternetCloseHandle(request_handle_); + InternetCloseHandle(connect_handle_); + InternetCloseHandle(internet_handle_); + return absl::FailedPreconditionError(absl::StrCat(GetLastError())); + } + } + + status = HTTPCodeToStatus(web_response.status_code, web_response.status_text); + if (!status.ok()) { + return status; + } + + return web_response; +} + +void HttpLoader::DisconnectWebServer() { + if (request_handle_ != nullptr) { + InternetCloseHandle(request_handle_); + request_handle_ = nullptr; + } + + if (connect_handle_ != nullptr) { + InternetCloseHandle(connect_handle_); + connect_handle_ = nullptr; + } + + if (internet_handle_ != nullptr) { + InternetCloseHandle(internet_handle_); + internet_handle_ = nullptr; + } +} + +absl::Status HttpLoader::HTTPCodeToStatus(int status_code, + absl::string_view status_message) { + switch (status_code) { + case 400: + return absl::InvalidArgumentError(status_message); + case 401: + return absl::UnauthenticatedError(status_message); + case 403: + return absl::PermissionDeniedError(status_message); + case 404: + return absl::NotFoundError(status_message); + case 409: + return absl::AbortedError(status_message); + case 416: + return absl::OutOfRangeError(status_message); + case 429: + return absl::ResourceExhaustedError(status_message); + case 499: + return absl::CancelledError(status_message); + case 504: + return absl::DeadlineExceededError(status_message); + case 501: + return absl::UnimplementedError(status_message); + case 503: + return absl::UnavailableError(status_message); + default: + break; + } + if (status_code >= 200 && status_code < 300) { + return absl::OkStatus(); + } else if (status_code >= 400 && status_code < 500) { + return absl::FailedPreconditionError(status_message); + } else if (status_code >= 500 && status_code < 600) { + return absl::InternalError(status_message); + } + return absl::UnknownError(status_message); +} + +} // namespace windows +} // namespace nearby +} // namespace location diff --git a/internal/platform/implementation/windows/http_loader.h b/internal/platform/implementation/windows/http_loader.h new file mode 100644 index 00000000..9eedad1b --- /dev/null +++ b/internal/platform/implementation/windows/http_loader.h @@ -0,0 +1,87 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_WINDOWS_HTTP_LOADER_H_ +#define THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_WINDOWS_HTTP_LOADER_H_ + +#include // NOLINT +#include + +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "internal/platform/implementation/http_loader.h" + +namespace location { +namespace nearby { +namespace windows { + +// HttpLoader is used to get HTTP response from remote server. +// +// HttpLoader gets HTTP request information from caller, and calling Windows +// WinInet APIs to get HTTP response. The platform handles HTTP/HTTPS sessions. +class HttpLoader { + public: + explicit HttpLoader(const location::nearby::api::WebRequest& request) + : request_(request) {} + ~HttpLoader() = default; + + absl::StatusOr GetResponse(); + + private: + // Defines the buffer size. It is used to init a buffer for receiving HTTP + // response. The unit is byte. + static constexpr int kReceiveBufferSize = 8 * 1024; + + absl::Status ConnectWebServer(); + absl::Status SendRequest(); + absl::StatusOr ProcessResponse(); + void DisconnectWebServer(); + + absl::StatusOr QueryStatusCode(HINTERNET file_handle); + absl::StatusOr QueryStatusText(HINTERNET request_handle); + absl::StatusOr> QueryResponseHeaders( + HINTERNET request_handle); + absl::Status QueryResponseInfo(HINTERNET request_handle, DWORD info_level, + std::string& info); + + absl::Status ParseUrl(); + + // Converts HTTP status code to absl Status. + // + // @param status_code HTTP status code, such 200, 404 etc. + // @param status_message short description of the status code. + // @return converted absl status. + absl::Status HTTPCodeToStatus(int status_code, + absl::string_view status_message); + + location::nearby::api::WebRequest request_; + std::string host_; + std::string path_; + std::string schema_; + bool is_secure_ = false; + int port_ = 80; + + HINTERNET internet_handle_ = nullptr; + HINTERNET connect_handle_ = nullptr; + HINTERNET request_handle_ = nullptr; +}; + +} // namespace windows +} // namespace nearby +} // namespace location + +#endif // THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_IMPLEMENTATION_WINDOWS_HTTP_LOADER_H_ diff --git a/internal/platform/implementation/windows/http_loader_test.cc b/internal/platform/implementation/windows/http_loader_test.cc new file mode 100644 index 00000000..d0bf65ca --- /dev/null +++ b/internal/platform/implementation/windows/http_loader_test.cc @@ -0,0 +1,53 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "internal/platform/implementation/windows/http_loader.h" + +#include + +#include "gtest/gtest.h" + +namespace location { +namespace nearby { +namespace windows { +namespace { +using ::location::nearby::api::WebRequest; + +TEST(HttpLoader, DISABLED_TestGetUrl) { + WebRequest request; + request.url = "https://www.google.com?id=456#fragment"; + request.method = "GET"; + auto response = HttpLoader(request).GetResponse(); + ASSERT_TRUE(response.ok()); + EXPECT_EQ(response->status_code, 200); +} + +TEST(HttpLoader, DISABLED_TestGetNotExistingUrl) { + WebRequest request; + request.url = "https://www.abcdefgabcdefg.com"; + request.method = "GET"; + EXPECT_FALSE(HttpLoader(request).GetResponse().ok()); +} + +TEST(HttpLoader, DISABLED_TestInvalidUrl) { + WebRequest request; + request.url = "https:/www.abcdefgabcdefg.com/name?id=456"; + request.method = "GET"; + EXPECT_FALSE(HttpLoader(request).GetResponse().ok()); +} + +} // namespace +} // namespace windows +} // namespace nearby +} // namespace location diff --git a/internal/platform/implementation/windows/platform.cc b/internal/platform/implementation/windows/platform.cc index 7535709c..84d2b1fc 100644 --- a/internal/platform/implementation/windows/platform.cc +++ b/internal/platform/implementation/windows/platform.cc @@ -33,6 +33,9 @@ #include #include +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "internal/platform/implementation/http_loader.h" #include "internal/platform/implementation/shared/count_down_latch.h" #include "internal/platform/implementation/windows/atomic_boolean.h" #include "internal/platform/implementation/windows/atomic_reference.h" @@ -45,6 +48,7 @@ #include "internal/platform/implementation/windows/file.h" #include "internal/platform/implementation/windows/file_path.h" #include "internal/platform/implementation/windows/future.h" +#include "internal/platform/implementation/windows/http_loader.h" #include "internal/platform/implementation/windows/listenable_future.h" #include "internal/platform/implementation/windows/log_message.h" #include "internal/platform/implementation/windows/mutex.h" @@ -298,6 +302,12 @@ std::unique_ptr ImplementationPlatform::CreateWebRtcMedium() { return absl::make_unique(); } +absl::StatusOr ImplementationPlatform::SendRequest( + const WebRequest& request) { + windows::HttpLoader http_loader{request}; + return http_loader.GetResponse(); +} + } // namespace api } // namespace nearby } // namespace location