Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
72 changes: 72 additions & 0 deletions src/net/endpoint.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#pragma once

#ifdef _WIN32
#include <winsock2.h>
#else
#include <sys/socket.h>
#endif

#include <cstddef>
#include <cstring>

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<const sockaddr*>(&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
108 changes: 108 additions & 0 deletions src/net/resolver.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#include "net/resolver.hpp"

#include "platform/network_runtime.hpp"

#ifdef _WIN32
#include <ws2tcpip.h>
#else
#include <netdb.h>
#include <netinet/in.h>
#endif

#include <array>
#include <charconv>
#include <memory>
#include <string>
#include <system_error>

namespace cpp_request::detail::net {
namespace {

struct AddrInfoDeleter {
void operator()(addrinfo* value) const noexcept {
if (value != nullptr) {
::freeaddrinfo(value);
}
}
};

using AddrInfoPtr = std::unique_ptr<addrinfo, AddrInfoDeleter>;

} // namespace

Result<std::vector<Endpoint>> 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<char, 6> 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<Endpoint> 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<std::size_t>(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
20 changes: 20 additions & 0 deletions src/net/resolver.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#pragma once

#include <cstdint>
#include <string_view>
#include <vector>

#include <cpp_request/result.hpp>

#include "net/endpoint.hpp"

namespace cpp_request::detail::net {

class Resolver final {
public:
[[nodiscard]] static Result<std::vector<Endpoint>> resolve(
std::string_view host,
std::uint16_t port);
};

} // namespace cpp_request::detail::net
60 changes: 60 additions & 0 deletions src/platform/network_runtime.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#include "platform/network_runtime.hpp"

#ifdef _WIN32
#include <winsock2.h>
#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
12 changes: 12 additions & 0 deletions src/platform/network_runtime.hpp
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
99 changes: 99 additions & 0 deletions tests/resolver_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#include <gtest/gtest.h>

#include "net/endpoint.hpp"
#include "net/resolver.hpp"

#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#endif

#include <algorithm>

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<const sockaddr*>(&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<const sockaddr_in*>(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<const sockaddr_in*>(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<const sockaddr_in6*>(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
Loading