From 8cc7a8b79dc15295a22ff38245e43ab04f13ad3b Mon Sep 17 00:00:00 2001 From: cnkwocha Date: Mon, 16 Sep 2024 15:43:14 +0100 Subject: [PATCH 1/3] Implement non-blocking async DNS resolver Motivation: The GRPCHTTP2Core module needs a DNS resolver, as currently, IP addresses have to be passed in manually. Modifications: - Add a new type `SimpleAsyncDNSResolver` with a method `resolve(host:port:)` that calls the `getaddrinfo` `libc` function to resolve a hostname and port number to a list of IP addresses and port numbers. `resolve(host:port:)` is non-blocking and asynchronous. As calls to `getaddrinfo` are blocking, `resolve(host:port)` executes `getaddrinfo` in a `DispatchQueue` and uses a `CheckedContinuation` to interface with the execution. Result: The GRPCHTTP2Core module will have a DNS resolver. --- .../Resolver/SimpleAsyncDNSResolver.swift | 240 ++++++++++++++++++ .../SimpleAsyncDNSResolverTests.swift | 31 +++ 2 files changed, 271 insertions(+) create mode 100644 Sources/GRPCHTTP2Core/Client/Resolver/SimpleAsyncDNSResolver.swift create mode 100644 Tests/GRPCHTTP2CoreTests/Client/Resolver/SimpleAsyncDNSResolverTests.swift diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/SimpleAsyncDNSResolver.swift b/Sources/GRPCHTTP2Core/Client/Resolver/SimpleAsyncDNSResolver.swift new file mode 100644 index 000000000..33aeb7a4f --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/Resolver/SimpleAsyncDNSResolver.swift @@ -0,0 +1,240 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * 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 + * + * http://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. + */ + +import Dispatch + +#if canImport(Darwin) +import Darwin +#elseif os(Linux) || os(FreeBSD) || os(Android) +#if canImport(Glibc) +import Glibc +#elseif canImport(Musl) +import Musl +#endif +import CNIOLinux +#else +#error("The GRPCHTTP2Core module was unable to identify your C library.") +#endif + +/// An asynchronous non-blocking DNS resolver built on top of the libc `getaddrinfo` function. +@available(macOS 10.15, *) +package enum SimpleAsyncDNSResolver { + private static let dispatchQueue = DispatchQueue( + label: "io.grpc.SimpleAsyncDNSResolver.dispatchQueue" + ) + + /// Resolves a hostname and port number to a list of IP addresses and port numbers. + /// + /// This method is non-blocking. As calls to `getaddrinfo` are blocking, this method executes `getaddrinfo` in a + /// `DispatchQueue` and uses a `CheckedContinuation` to interface with the execution. + package static func resolve(host: String, port: Int) async throws -> [SocketAddress] { + if Task.isCancelled { + return [] + } + + return try await withCheckedThrowingContinuation { continuation in + dispatchQueue.async { + do { + let result = try Self.resolveBlocking(host: host, port: port) + continuation.resume(returning: result) + } catch { + continuation.resume(throwing: error) + } + } + } + } + + /// Resolves a hostname and port number to a list of IP addresses and port numbers. + /// + /// Calls to `getaddrinfo` are blocking and this method calls `getaddrinfo` directly. Hence, this method is also blocking. + private static func resolveBlocking(host: String, port: Int) throws -> [SocketAddress] { + var result: UnsafeMutablePointer? + defer { + if let result { + // Release memory allocated by a successful call to getaddrinfo + freeaddrinfo(result) + } + } + + var hints = addrinfo() + hints.ai_socktype = SOCK_STREAM + hints.ai_protocol = IPPROTO_TCP + + let errorCode = getaddrinfo(host, String(port), &hints, &result) + + guard errorCode == 0, var result else { + throw SimpleAsyncDNSResolverError(code: errorCode) + } + + var socketAddressList = [SocketAddress]() + + while true { + let addressBytes = UnsafeRawPointer(result.pointee.ai_addr) + let socketAddress: SocketAddress? + + switch result.pointee.ai_family { // Enum with two cases + case AF_INET: // IPv4 address + let ipv4NetworkAddressStructure = addressBytes!.load(as: sockaddr_in.self) + let ipv4PresentationAddress = Self.convertFromNetworkToPresentationFormat( + address: ipv4NetworkAddressStructure.sin_addr, + family: AF_INET, + length: INET_ADDRSTRLEN + ) + + socketAddress = .ipv4( + .init( + host: ipv4PresentationAddress, + port: Int(in_port_t(bigEndian: ipv4NetworkAddressStructure.sin_port)) + ) + ) + case AF_INET6: // IPv6 address + let ipv6NetworkAddressStructure = addressBytes!.load(as: sockaddr_in6.self) + let ipv6PresentationAddress = Self.convertFromNetworkToPresentationFormat( + address: ipv6NetworkAddressStructure.sin6_addr, + family: AF_INET6, + length: INET6_ADDRSTRLEN + ) + + socketAddress = .ipv6( + .init( + host: ipv6PresentationAddress, + port: Int(in_port_t(bigEndian: ipv6NetworkAddressStructure.sin6_port)) + ) + ) + default: + socketAddress = nil + } + + if let socketAddress { + socketAddressList.append(socketAddress) + } + + guard let nextResult = result.pointee.ai_next else { break } + result = nextResult + } + + return socketAddressList + } + + /// Converts an address from a network format to a presentation format using `inet_ntop`. + private static func convertFromNetworkToPresentationFormat( + address: T, + family: Int32, + length: Int32 + ) -> String { + var resultingAddressBytes = [Int8](repeating: 0, count: Int(length)) + + return withUnsafePointer(to: address) { addressPtr in + return resultingAddressBytes.withUnsafeMutableBufferPointer { + (resultingAddressBytesPtr: inout UnsafeMutableBufferPointer) -> String in + + // Convert + inet_ntop(family, addressPtr, resultingAddressBytesPtr.baseAddress!, socklen_t(length)) + + // Create the result string from now-filled resultingAddressBytes. + return resultingAddressBytesPtr.baseAddress!.withMemoryRebound( + to: UInt8.self, + capacity: Int(length) + ) { resultingAddressBytesPtr -> String in + String(cString: resultingAddressBytesPtr) + } + } + } + } +} + +/// `Error` that may be thrown based on the error code returned by `getaddrinfo`. +package enum SimpleAsyncDNSResolverError: Error { + /// Address family for nodename not supported. + case addressFamilyForNodenameNotSupported + + /// Temporary failure in name resolution. + case temporaryFailure + + /// Invalid value for `ai_flags`. + case invalidAIFlags + + /// Invalid value for `hints`. + case invalidHints + + /// Non-recoverable failure in name resolution. + case nonRecoverableFailure + + /// `ai_family` not supported. + case aiFamilyNotSupported + + /// Memory allocation failure. + case memoryAllocationFailure + + /// No address associated with nodename. + case noAddressAssociatedWithNodename + + /// `hostname` or `servname` not provided, or not known. + case hostnameOrServnameNotProvidedOrNotKnown + + /// Argument buffer overflow. + case argumentBufferOverflow + + /// Resolved protocol is unknown. + case resolvedProtocolIsUnknown + + /// `servname` not supported for `ai_socktype`. + case servnameNotSupportedForSocktype + + /// `ai_socktype` not supported. + case socktypeNotSupported + + /// System error returned in `errno`. + case systemError + + /// Unknown error. + case unknown + + package init(code: Int32) { + switch code { + case EAI_ADDRFAMILY: + self = .addressFamilyForNodenameNotSupported + case EAI_AGAIN: + self = .temporaryFailure + case EAI_BADFLAGS: + self = .invalidAIFlags + case EAI_BADHINTS: + self = .invalidHints + case EAI_FAIL: + self = .nonRecoverableFailure + case EAI_FAMILY: + self = .aiFamilyNotSupported + case EAI_MEMORY: + self = .memoryAllocationFailure + case EAI_NODATA: + self = .noAddressAssociatedWithNodename + case EAI_NONAME: + self = .hostnameOrServnameNotProvidedOrNotKnown + case EAI_OVERFLOW: + self = .argumentBufferOverflow + case EAI_PROTOCOL: + self = .resolvedProtocolIsUnknown + case EAI_SERVICE: + self = .servnameNotSupportedForSocktype + case EAI_SOCKTYPE: + self = .socktypeNotSupported + case EAI_SYSTEM: + self = .systemError + default: + self = .unknown + } + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Resolver/SimpleAsyncDNSResolverTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Resolver/SimpleAsyncDNSResolverTests.swift new file mode 100644 index 000000000..343b594ac --- /dev/null +++ b/Tests/GRPCHTTP2CoreTests/Client/Resolver/SimpleAsyncDNSResolverTests.swift @@ -0,0 +1,31 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * 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 + * + * http://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. + */ + +import GRPCHTTP2Core +import XCTest + +class SimpleAsyncDNSResolverTests: XCTestCase { + func testResolve() async throws { + let expectedResult: [SocketAddress] = [ + .ipv6(host: "::1", port: 80), + .ipv4(host: "127.0.0.1", port: 80), + ] + + let result = try await SimpleAsyncDNSResolver.resolve(host: "localhost", port: 80) + + XCTAssertEqual(result, expectedResult) + } +} From ca73f7229279fa2b84bf49bfd77279b653d654f5 Mon Sep 17 00:00:00 2001 From: cnkwocha Date: Mon, 16 Sep 2024 17:31:42 +0100 Subject: [PATCH 2/3] Remove unnecessary comment --- .../GRPCHTTP2Core/Client/Resolver/SimpleAsyncDNSResolver.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/SimpleAsyncDNSResolver.swift b/Sources/GRPCHTTP2Core/Client/Resolver/SimpleAsyncDNSResolver.swift index 33aeb7a4f..15dd99a6b 100644 --- a/Sources/GRPCHTTP2Core/Client/Resolver/SimpleAsyncDNSResolver.swift +++ b/Sources/GRPCHTTP2Core/Client/Resolver/SimpleAsyncDNSResolver.swift @@ -85,7 +85,7 @@ package enum SimpleAsyncDNSResolver { let addressBytes = UnsafeRawPointer(result.pointee.ai_addr) let socketAddress: SocketAddress? - switch result.pointee.ai_family { // Enum with two cases + switch result.pointee.ai_family { case AF_INET: // IPv4 address let ipv4NetworkAddressStructure = addressBytes!.load(as: sockaddr_in.self) let ipv4PresentationAddress = Self.convertFromNetworkToPresentationFormat( From c47f86eccbfc0304fa2ffeebacb70c47160a9896 Mon Sep 17 00:00:00 2001 From: cnkwocha Date: Mon, 23 Sep 2024 10:46:03 +0100 Subject: [PATCH 3/3] Implement feedback --- .../Client/Resolver/DNSResolver.swift | 260 ++++++++++++++++++ .../Resolver/SimpleAsyncDNSResolver.swift | 240 ---------------- ...lverTests.swift => DNSResolverTests.swift} | 12 +- 3 files changed, 267 insertions(+), 245 deletions(-) create mode 100644 Sources/GRPCHTTP2Core/Client/Resolver/DNSResolver.swift delete mode 100644 Sources/GRPCHTTP2Core/Client/Resolver/SimpleAsyncDNSResolver.swift rename Tests/GRPCHTTP2CoreTests/Client/Resolver/{SimpleAsyncDNSResolverTests.swift => DNSResolverTests.swift} (76%) diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/DNSResolver.swift b/Sources/GRPCHTTP2Core/Client/Resolver/DNSResolver.swift new file mode 100644 index 000000000..e9d93b956 --- /dev/null +++ b/Sources/GRPCHTTP2Core/Client/Resolver/DNSResolver.swift @@ -0,0 +1,260 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * 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 + * + * http://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. + */ + +private import Dispatch + +#if canImport(Darwin) +package import Darwin +#elseif canImport(Glibc) +package import Glibc +#elseif canImport(Musl) +package import Musl +#else +#error("The GRPCHTTP2Core module was unable to identify your C library.") +#endif +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +/// An asynchronous non-blocking DNS resolver built on top of the libc `getaddrinfo` function. +package enum DNSResolver { + private static let dispatchQueue = DispatchQueue( + label: "io.grpc.DNSResolver" + ) + + /// Resolves a hostname and port number to a list of socket addresses. This method is non-blocking. + package static func resolve(host: String, port: Int) async throws -> [SocketAddress] { + if Task.isCancelled { + return [] + } + + return try await withCheckedThrowingContinuation { continuation in + Self.dispatchQueue.async { + do { + let result = try Self.resolveBlocking(host: host, port: port) + continuation.resume(returning: result) + } catch { + continuation.resume(throwing: error) + } + } + } + } + + /// Resolves a hostname and port number to a list of socket addresses. + /// + /// Calls to `getaddrinfo` are blocking and this method calls `getaddrinfo` directly. Hence, this method is also blocking. + private static func resolveBlocking(host: String, port: Int) throws -> [SocketAddress] { + var result: UnsafeMutablePointer? + defer { + if let result { + // Release memory allocated by a successful call to getaddrinfo + freeaddrinfo(result) + } + } + + var hints = addrinfo() + hints.ai_socktype = SOCK_STREAM + hints.ai_protocol = IPPROTO_TCP + + let errorCode = getaddrinfo(host, String(port), &hints, &result) + + guard errorCode == 0, let result else { + throw DNSResolver.GetAddrInfoError(code: errorCode) + } + + return try Self.parseResult(result) + } + + /// Parses the linked list of DNS results (`addrinfo`), returning an array of socket addresses. + private static func parseResult( + _ result: UnsafeMutablePointer + ) throws -> [SocketAddress] { + var result = result + var socketAddresses = [SocketAddress]() + + while true { + let addressBytes: UnsafeRawPointer = .init(result.pointee.ai_addr) + + switch result.pointee.ai_family { + case AF_INET: // IPv4 address + let ipv4AddressStructure = addressBytes.load(as: sockaddr_in.self) + try socketAddresses.append(.ipv4(.init(ipv4AddressStructure))) + case AF_INET6: // IPv6 address + let ipv6AddressStructure = addressBytes.load(as: sockaddr_in6.self) + try socketAddresses.append(.ipv6(.init(ipv6AddressStructure))) + default: + () + } + + guard let nextResult = result.pointee.ai_next else { break } + result = nextResult + } + + return socketAddresses + } + + /// Converts an address from a network format to a presentation format using `inet_ntop`. + fileprivate static func convertAddressFromNetworkToPresentationFormat( + addressPtr: UnsafePointer, + family: CInt, + length: CInt + ) throws -> String { + var presentationAddressBytes = [CChar](repeating: 0, count: Int(length)) + + return try presentationAddressBytes.withUnsafeMutableBufferPointer { + (presentationAddressBytesPtr: inout UnsafeMutableBufferPointer) throws -> String in + + // Convert + let presentationAddressStringPtr = inet_ntop( + family, + addressPtr, + presentationAddressBytesPtr.baseAddress!, + socklen_t(length) + ) + + if let presentationAddressStringPtr { + return String(cString: presentationAddressStringPtr) + } else { + throw DNSResolver.InetNetworkToPresentationError.systemError(errno: errno) + } + } + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension DNSResolver { + /// `Error` that may be thrown based on the error code returned by `getaddrinfo`. + package enum GetAddrInfoError: Error, Hashable { + /// Address family for nodename not supported. + case addressFamilyForNodenameNotSupported + + /// Temporary failure in name resolution. + case temporaryFailure + + /// Invalid value for `ai_flags`. + case invalidAIFlags + + /// Invalid value for `hints`. + case invalidHints + + /// Non-recoverable failure in name resolution. + case nonRecoverableFailure + + /// `ai_family` not supported. + case aiFamilyNotSupported + + /// Memory allocation failure. + case memoryAllocationFailure + + /// No address associated with nodename. + case noAddressAssociatedWithNodename + + /// `hostname` or `servname` not provided, or not known. + case hostnameOrServnameNotProvidedOrNotKnown + + /// Argument buffer overflow. + case argumentBufferOverflow + + /// Resolved protocol is unknown. + case resolvedProtocolIsUnknown + + /// `servname` not supported for `ai_socktype`. + case servnameNotSupportedForSocktype + + /// `ai_socktype` not supported. + case socktypeNotSupported + + /// System error returned in `errno`. + case systemError + + /// Unknown error. + case unknown + + package init(code: CInt) { + switch code { + case EAI_ADDRFAMILY: + self = .addressFamilyForNodenameNotSupported + case EAI_AGAIN: + self = .temporaryFailure + case EAI_BADFLAGS: + self = .invalidAIFlags + case EAI_BADHINTS: + self = .invalidHints + case EAI_FAIL: + self = .nonRecoverableFailure + case EAI_FAMILY: + self = .aiFamilyNotSupported + case EAI_MEMORY: + self = .memoryAllocationFailure + case EAI_NODATA: + self = .noAddressAssociatedWithNodename + case EAI_NONAME: + self = .hostnameOrServnameNotProvidedOrNotKnown + case EAI_OVERFLOW: + self = .argumentBufferOverflow + case EAI_PROTOCOL: + self = .resolvedProtocolIsUnknown + case EAI_SERVICE: + self = .servnameNotSupportedForSocktype + case EAI_SOCKTYPE: + self = .socktypeNotSupported + case EAI_SYSTEM: + self = .systemError + default: + self = .unknown + } + } + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension DNSResolver { + /// `Error` that may be thrown based on the system error encountered by `inet_ntop`. + package enum InetNetworkToPresentationError: Error, Hashable { + case systemError(errno: errno_t) + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension SocketAddress.IPv4 { + fileprivate init(_ address: sockaddr_in) throws { + var presentationAddress = "" + + try withUnsafePointer(to: address.sin_addr) { addressPtr in + presentationAddress = try DNSResolver.convertAddressFromNetworkToPresentationFormat( + addressPtr: addressPtr, + family: AF_INET, + length: INET_ADDRSTRLEN + ) + } + + self = .init(host: presentationAddress, port: Int(in_port_t(bigEndian: address.sin_port))) + } +} + +@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +extension SocketAddress.IPv6 { + fileprivate init(_ address: sockaddr_in6) throws { + var presentationAddress = "" + + try withUnsafePointer(to: address.sin6_addr) { addressPtr in + presentationAddress = try DNSResolver.convertAddressFromNetworkToPresentationFormat( + addressPtr: addressPtr, + family: AF_INET6, + length: INET6_ADDRSTRLEN + ) + } + + self = .init(host: presentationAddress, port: Int(in_port_t(bigEndian: address.sin6_port))) + } +} diff --git a/Sources/GRPCHTTP2Core/Client/Resolver/SimpleAsyncDNSResolver.swift b/Sources/GRPCHTTP2Core/Client/Resolver/SimpleAsyncDNSResolver.swift deleted file mode 100644 index 15dd99a6b..000000000 --- a/Sources/GRPCHTTP2Core/Client/Resolver/SimpleAsyncDNSResolver.swift +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright 2024, gRPC Authors All rights reserved. - * - * 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 - * - * http://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. - */ - -import Dispatch - -#if canImport(Darwin) -import Darwin -#elseif os(Linux) || os(FreeBSD) || os(Android) -#if canImport(Glibc) -import Glibc -#elseif canImport(Musl) -import Musl -#endif -import CNIOLinux -#else -#error("The GRPCHTTP2Core module was unable to identify your C library.") -#endif - -/// An asynchronous non-blocking DNS resolver built on top of the libc `getaddrinfo` function. -@available(macOS 10.15, *) -package enum SimpleAsyncDNSResolver { - private static let dispatchQueue = DispatchQueue( - label: "io.grpc.SimpleAsyncDNSResolver.dispatchQueue" - ) - - /// Resolves a hostname and port number to a list of IP addresses and port numbers. - /// - /// This method is non-blocking. As calls to `getaddrinfo` are blocking, this method executes `getaddrinfo` in a - /// `DispatchQueue` and uses a `CheckedContinuation` to interface with the execution. - package static func resolve(host: String, port: Int) async throws -> [SocketAddress] { - if Task.isCancelled { - return [] - } - - return try await withCheckedThrowingContinuation { continuation in - dispatchQueue.async { - do { - let result = try Self.resolveBlocking(host: host, port: port) - continuation.resume(returning: result) - } catch { - continuation.resume(throwing: error) - } - } - } - } - - /// Resolves a hostname and port number to a list of IP addresses and port numbers. - /// - /// Calls to `getaddrinfo` are blocking and this method calls `getaddrinfo` directly. Hence, this method is also blocking. - private static func resolveBlocking(host: String, port: Int) throws -> [SocketAddress] { - var result: UnsafeMutablePointer? - defer { - if let result { - // Release memory allocated by a successful call to getaddrinfo - freeaddrinfo(result) - } - } - - var hints = addrinfo() - hints.ai_socktype = SOCK_STREAM - hints.ai_protocol = IPPROTO_TCP - - let errorCode = getaddrinfo(host, String(port), &hints, &result) - - guard errorCode == 0, var result else { - throw SimpleAsyncDNSResolverError(code: errorCode) - } - - var socketAddressList = [SocketAddress]() - - while true { - let addressBytes = UnsafeRawPointer(result.pointee.ai_addr) - let socketAddress: SocketAddress? - - switch result.pointee.ai_family { - case AF_INET: // IPv4 address - let ipv4NetworkAddressStructure = addressBytes!.load(as: sockaddr_in.self) - let ipv4PresentationAddress = Self.convertFromNetworkToPresentationFormat( - address: ipv4NetworkAddressStructure.sin_addr, - family: AF_INET, - length: INET_ADDRSTRLEN - ) - - socketAddress = .ipv4( - .init( - host: ipv4PresentationAddress, - port: Int(in_port_t(bigEndian: ipv4NetworkAddressStructure.sin_port)) - ) - ) - case AF_INET6: // IPv6 address - let ipv6NetworkAddressStructure = addressBytes!.load(as: sockaddr_in6.self) - let ipv6PresentationAddress = Self.convertFromNetworkToPresentationFormat( - address: ipv6NetworkAddressStructure.sin6_addr, - family: AF_INET6, - length: INET6_ADDRSTRLEN - ) - - socketAddress = .ipv6( - .init( - host: ipv6PresentationAddress, - port: Int(in_port_t(bigEndian: ipv6NetworkAddressStructure.sin6_port)) - ) - ) - default: - socketAddress = nil - } - - if let socketAddress { - socketAddressList.append(socketAddress) - } - - guard let nextResult = result.pointee.ai_next else { break } - result = nextResult - } - - return socketAddressList - } - - /// Converts an address from a network format to a presentation format using `inet_ntop`. - private static func convertFromNetworkToPresentationFormat( - address: T, - family: Int32, - length: Int32 - ) -> String { - var resultingAddressBytes = [Int8](repeating: 0, count: Int(length)) - - return withUnsafePointer(to: address) { addressPtr in - return resultingAddressBytes.withUnsafeMutableBufferPointer { - (resultingAddressBytesPtr: inout UnsafeMutableBufferPointer) -> String in - - // Convert - inet_ntop(family, addressPtr, resultingAddressBytesPtr.baseAddress!, socklen_t(length)) - - // Create the result string from now-filled resultingAddressBytes. - return resultingAddressBytesPtr.baseAddress!.withMemoryRebound( - to: UInt8.self, - capacity: Int(length) - ) { resultingAddressBytesPtr -> String in - String(cString: resultingAddressBytesPtr) - } - } - } - } -} - -/// `Error` that may be thrown based on the error code returned by `getaddrinfo`. -package enum SimpleAsyncDNSResolverError: Error { - /// Address family for nodename not supported. - case addressFamilyForNodenameNotSupported - - /// Temporary failure in name resolution. - case temporaryFailure - - /// Invalid value for `ai_flags`. - case invalidAIFlags - - /// Invalid value for `hints`. - case invalidHints - - /// Non-recoverable failure in name resolution. - case nonRecoverableFailure - - /// `ai_family` not supported. - case aiFamilyNotSupported - - /// Memory allocation failure. - case memoryAllocationFailure - - /// No address associated with nodename. - case noAddressAssociatedWithNodename - - /// `hostname` or `servname` not provided, or not known. - case hostnameOrServnameNotProvidedOrNotKnown - - /// Argument buffer overflow. - case argumentBufferOverflow - - /// Resolved protocol is unknown. - case resolvedProtocolIsUnknown - - /// `servname` not supported for `ai_socktype`. - case servnameNotSupportedForSocktype - - /// `ai_socktype` not supported. - case socktypeNotSupported - - /// System error returned in `errno`. - case systemError - - /// Unknown error. - case unknown - - package init(code: Int32) { - switch code { - case EAI_ADDRFAMILY: - self = .addressFamilyForNodenameNotSupported - case EAI_AGAIN: - self = .temporaryFailure - case EAI_BADFLAGS: - self = .invalidAIFlags - case EAI_BADHINTS: - self = .invalidHints - case EAI_FAIL: - self = .nonRecoverableFailure - case EAI_FAMILY: - self = .aiFamilyNotSupported - case EAI_MEMORY: - self = .memoryAllocationFailure - case EAI_NODATA: - self = .noAddressAssociatedWithNodename - case EAI_NONAME: - self = .hostnameOrServnameNotProvidedOrNotKnown - case EAI_OVERFLOW: - self = .argumentBufferOverflow - case EAI_PROTOCOL: - self = .resolvedProtocolIsUnknown - case EAI_SERVICE: - self = .servnameNotSupportedForSocktype - case EAI_SOCKTYPE: - self = .socktypeNotSupported - case EAI_SYSTEM: - self = .systemError - default: - self = .unknown - } - } -} diff --git a/Tests/GRPCHTTP2CoreTests/Client/Resolver/SimpleAsyncDNSResolverTests.swift b/Tests/GRPCHTTP2CoreTests/Client/Resolver/DNSResolverTests.swift similarity index 76% rename from Tests/GRPCHTTP2CoreTests/Client/Resolver/SimpleAsyncDNSResolverTests.swift rename to Tests/GRPCHTTP2CoreTests/Client/Resolver/DNSResolverTests.swift index 343b594ac..dcd748de3 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/Resolver/SimpleAsyncDNSResolverTests.swift +++ b/Tests/GRPCHTTP2CoreTests/Client/Resolver/DNSResolverTests.swift @@ -15,17 +15,19 @@ */ import GRPCHTTP2Core -import XCTest +import Testing -class SimpleAsyncDNSResolverTests: XCTestCase { - func testResolve() async throws { +@Suite("DNSResolver") +struct DNSResolverTests { + @Test("Resolve hostname") + func resolve() async throws { let expectedResult: [SocketAddress] = [ .ipv6(host: "::1", port: 80), .ipv4(host: "127.0.0.1", port: 80), ] - let result = try await SimpleAsyncDNSResolver.resolve(host: "localhost", port: 80) + let result = try await DNSResolver.resolve(host: "localhost", port: 80) - XCTAssertEqual(result, expectedResult) + #expect(result == expectedResult) } }