diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b71a70b..4a818fc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,6 +2,8 @@ add_library(internal ${CMAKE_CURRENT_SOURCE_DIR}/dummy.cpp ${CMAKE_CURRENT_SOURCE_DIR}/url.cpp ${CMAKE_CURRENT_SOURCE_DIR}/platform/native_socket.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/platform/network_runtime.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/net/resolver.cpp ) add_library(${PROJECT_NAME}::internal ALIAS internal) diff --git a/src/net/endpoint.hpp b/src/net/endpoint.hpp new file mode 100644 index 0000000..777b783 --- /dev/null +++ b/src/net/endpoint.hpp @@ -0,0 +1,72 @@ +#pragma once + +#ifdef _WIN32 +#include +#else +#include +#endif + +#include +#include + +namespace cpp_request::detail::net { + +class Endpoint final { +public: + Endpoint() noexcept = default; + + Endpoint( + const sockaddr* address, + std::size_t address_size, + int socket_type, + int protocol) noexcept + : address_size_(address_size), + socket_type_(socket_type), + protocol_(protocol) { + if (address != nullptr && address_size <= sizeof(address_)) { + std::memcpy(&address_, address, address_size); + } else { + address_size_ = 0; + } + } + + [[nodiscard]] bool valid() const noexcept { + return address_size_ != 0; + } + + [[nodiscard]] int family() const noexcept { + return valid() ? address_.ss_family : AF_UNSPEC; + } + + [[nodiscard]] int socket_type() const noexcept { + return socket_type_; + } + + [[nodiscard]] int protocol() const noexcept { + return protocol_; + } + + [[nodiscard]] bool is_ipv4() const noexcept { + return family() == AF_INET; + } + + [[nodiscard]] bool is_ipv6() const noexcept { + return family() == AF_INET6; + } + + [[nodiscard]] const sockaddr* native_address() const noexcept { + return reinterpret_cast(&address_); + } + + [[nodiscard]] std::size_t native_address_size() const noexcept { + return address_size_; + } + +private: + sockaddr_storage address_{}; + std::size_t address_size_{0}; + int socket_type_{SOCK_STREAM}; + int protocol_{0}; +}; + +} // namespace cpp_request::detail::net diff --git a/src/net/resolver.cpp b/src/net/resolver.cpp new file mode 100644 index 0000000..d9f13c1 --- /dev/null +++ b/src/net/resolver.cpp @@ -0,0 +1,108 @@ +#include "net/resolver.hpp" + +#include "platform/network_runtime.hpp" + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +#include +#include +#include +#include +#include + +namespace cpp_request::detail::net { +namespace { + +struct AddrInfoDeleter { + void operator()(addrinfo* value) const noexcept { + if (value != nullptr) { + ::freeaddrinfo(value); + } + } +}; + +using AddrInfoPtr = std::unique_ptr; + +} // namespace + +Result> Resolver::resolve( + std::string_view host, + std::uint16_t port) { + if (host.empty() || host.find('\0') != std::string_view::npos) { + return Error{ErrorCode::ResolveFailed}; + } + + const auto runtime = platform::ensure_network_runtime(); + if (!runtime.ok) { + return Error{ErrorCode::ResolveFailed, runtime.native_code}; + } + + std::string host_storage{host}; + + std::array service{}; + const auto converted = std::to_chars( + service.data(), + service.data() + service.size() - 1, + port); + if (converted.ec != std::errc{}) { + return Error{ErrorCode::ResolveFailed}; + } + *converted.ptr = '\0'; + + addrinfo hints{}; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + hints.ai_flags = AI_NUMERICSERV; + + addrinfo* raw_results = nullptr; + const int status = ::getaddrinfo( + host_storage.c_str(), + service.data(), + &hints, + &raw_results); + + if (status != 0) { + return Error{ErrorCode::ResolveFailed, status}; + } + + AddrInfoPtr results{raw_results}; + std::vector endpoints; + + for (const addrinfo* current = results.get(); current != nullptr; current = current->ai_next) { + if (current->ai_addr == nullptr) { + continue; + } + + if (current->ai_family != AF_INET && current->ai_family != AF_INET6) { + continue; + } + + const auto address_size = static_cast(current->ai_addrlen); + if (address_size == 0 || address_size > sizeof(sockaddr_storage)) { + continue; + } + + const int socket_type = current->ai_socktype != 0 ? current->ai_socktype : SOCK_STREAM; + const int protocol = current->ai_protocol != 0 ? current->ai_protocol : IPPROTO_TCP; + + endpoints.emplace_back( + current->ai_addr, + address_size, + socket_type, + protocol); + } + + if (endpoints.empty()) { + return Error{ErrorCode::ResolveFailed}; + } + + return endpoints; +} + +} // namespace cpp_request::detail::net diff --git a/src/net/resolver.hpp b/src/net/resolver.hpp new file mode 100644 index 0000000..c888ca9 --- /dev/null +++ b/src/net/resolver.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +#include + +#include "net/endpoint.hpp" + +namespace cpp_request::detail::net { + +class Resolver final { +public: + [[nodiscard]] static Result> resolve( + std::string_view host, + std::uint16_t port); +}; + +} // namespace cpp_request::detail::net diff --git a/src/platform/network_runtime.cpp b/src/platform/network_runtime.cpp new file mode 100644 index 0000000..1847f29 --- /dev/null +++ b/src/platform/network_runtime.cpp @@ -0,0 +1,60 @@ +#include "platform/network_runtime.hpp" + +#ifdef _WIN32 +#include +#endif + +namespace cpp_request::detail::platform { + +#ifdef _WIN32 +namespace { + +class WinsockRuntime final { +public: + WinsockRuntime() noexcept { + WSADATA data{}; + status_ = ::WSAStartup(MAKEWORD(2, 2), &data); + if (status_ != 0) { + return; + } + + if (LOBYTE(data.wVersion) != 2 || HIBYTE(data.wVersion) != 2) { + status_ = WSAVERNOTSUPPORTED; + ::WSACleanup(); + return; + } + + initialized_ = true; + } + + ~WinsockRuntime() noexcept { + if (initialized_) { + ::WSACleanup(); + } + } + + WinsockRuntime(const WinsockRuntime&) = delete; + WinsockRuntime& operator=(const WinsockRuntime&) = delete; + + [[nodiscard]] NetworkRuntimeStatus status() const noexcept { + return {initialized_, initialized_ ? 0 : status_}; + } + +private: + int status_{0}; + bool initialized_{false}; +}; + +} // namespace +#endif + +NetworkRuntimeStatus ensure_network_runtime() noexcept { +#ifdef _WIN32 + static const WinsockRuntime runtime; + return runtime.status(); +#else + return {}; +#endif +} + +} // namespace cpp_request::detail::platform diff --git a/src/platform/network_runtime.hpp b/src/platform/network_runtime.hpp new file mode 100644 index 0000000..9082ccf --- /dev/null +++ b/src/platform/network_runtime.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace cpp_request::detail::platform { + +struct NetworkRuntimeStatus { + bool ok{true}; + int native_code{0}; +}; + +[[nodiscard]] NetworkRuntimeStatus ensure_network_runtime() noexcept; + +} // namespace cpp_request::detail::platform diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 839873a..8a48995 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,6 +3,7 @@ include(${CMAKE_SOURCE_DIR}/cmake/packages/google-test.cmake) add_executable(cpp_request_tests ${CMAKE_CURRENT_SOURCE_DIR}/url_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/native_socket_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/resolver_test.cpp ) target_include_directories(cpp_request_tests diff --git a/tests/resolver_test.cpp b/tests/resolver_test.cpp new file mode 100644 index 0000000..80931cc --- /dev/null +++ b/tests/resolver_test.cpp @@ -0,0 +1,99 @@ +#include + +#include "net/endpoint.hpp" +#include "net/resolver.hpp" + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +#endif + +#include + +namespace { + +using cpp_request::ErrorCode; +using cpp_request::detail::net::Endpoint; +using cpp_request::detail::net::Resolver; + +TEST(EndpointTest, CopiesNativeAddressStorage) { + sockaddr_in source{}; + source.sin_family = AF_INET; + source.sin_port = htons(8080); + source.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + Endpoint endpoint{ + reinterpret_cast(&source), + sizeof(source), + SOCK_STREAM, + IPPROTO_TCP}; + + ASSERT_TRUE(endpoint.valid()); + ASSERT_TRUE(endpoint.is_ipv4()); + EXPECT_FALSE(endpoint.is_ipv6()); + EXPECT_EQ(endpoint.socket_type(), SOCK_STREAM); + EXPECT_EQ(endpoint.protocol(), IPPROTO_TCP); + + source.sin_port = 0; + + const auto* copied = reinterpret_cast(endpoint.native_address()); + EXPECT_EQ(ntohs(copied->sin_port), 8080); + EXPECT_EQ(endpoint.native_address_size(), sizeof(sockaddr_in)); +} + +TEST(ResolverTest, ResolvesNumericIpv4Loopback) { + auto result = Resolver::resolve("127.0.0.1", 8080); + + ASSERT_TRUE(result) << "native resolver code: " << result.error().native_code; + ASSERT_FALSE(result.value().empty()); + + const auto candidate = std::find_if( + result.value().begin(), + result.value().end(), + [](const Endpoint& endpoint) { return endpoint.is_ipv4(); }); + + ASSERT_NE(candidate, result.value().end()); + EXPECT_EQ(candidate->socket_type(), SOCK_STREAM); + EXPECT_EQ(candidate->protocol(), IPPROTO_TCP); + + const auto* address = reinterpret_cast(candidate->native_address()); + EXPECT_EQ(ntohs(address->sin_port), 8080); +} + +TEST(ResolverTest, ResolvesNumericIpv6Loopback) { + auto result = Resolver::resolve("::1", 8081); + + ASSERT_TRUE(result) << "native resolver code: " << result.error().native_code; + + const auto candidate = std::find_if( + result.value().begin(), + result.value().end(), + [](const Endpoint& endpoint) { return endpoint.is_ipv6(); }); + + ASSERT_NE(candidate, result.value().end()); + EXPECT_EQ(candidate->socket_type(), SOCK_STREAM); + EXPECT_EQ(candidate->protocol(), IPPROTO_TCP); + + const auto* address = reinterpret_cast(candidate->native_address()); + EXPECT_EQ(ntohs(address->sin6_port), 8081); +} + +TEST(ResolverTest, InvalidHostnameReturnsResolveFailed) { + auto result = Resolver::resolve("not a valid host name", 80); + + ASSERT_FALSE(result); + EXPECT_EQ(result.error().code, ErrorCode::ResolveFailed); +} + +TEST(ResolverTest, EmptyHostnameReturnsResolveFailed) { + auto result = Resolver::resolve({}, 80); + + ASSERT_FALSE(result); + EXPECT_EQ(result.error().code, ErrorCode::ResolveFailed); +} + +} // namespace