Skip to content

Commit

Permalink
Add Crystal::System::Addrinfo
Browse files Browse the repository at this point in the history
  • Loading branch information
HertzDevil committed Sep 2, 2024
1 parent 556e875 commit 981c2e9
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 87 deletions.
26 changes: 26 additions & 0 deletions src/crystal/system/addrinfo.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module Crystal::System::Addrinfo
# alias Handle

# protected def initialize(addrinfo : Handle)

# def system_ip_address : ::Socket::IPAddress

# only used by `#system_ip_address`?
# def to_unsafe

# def self.getaddrinfo(domain, service, family, type, protocol, timeout) : Handle

# def self.next_addrinfo(addrinfo : Handle) : Handle

# def self.free_addrinfo(addrinfo : Handle)
end

{% if flag?(:wasi) %}
require "./wasi/addrinfo"
{% elsif flag?(:unix) %}
require "./unix/addrinfo"
{% elsif flag?(:win32) %}
require "./win32/addrinfo"
{% else %}
{% raise "No Crystal::System::Addrinfo implementation available" %}
{% end %}
71 changes: 71 additions & 0 deletions src/crystal/system/unix/addrinfo.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
module Crystal::System::Addrinfo
alias Handle = LibC::Addrinfo*

@addr : LibC::SockaddrIn6

protected def initialize(addrinfo : Handle)
@family = ::Socket::Family.from_value(addrinfo.value.ai_family)
@type = ::Socket::Type.from_value(addrinfo.value.ai_socktype)
@protocol = ::Socket::Protocol.from_value(addrinfo.value.ai_protocol)
@size = addrinfo.value.ai_addrlen.to_i

@addr = uninitialized LibC::SockaddrIn6

case @family
when ::Socket::Family::INET6
addrinfo.value.ai_addr.as(LibC::SockaddrIn6*).copy_to(pointerof(@addr).as(LibC::SockaddrIn6*), 1)
when ::Socket::Family::INET
addrinfo.value.ai_addr.as(LibC::SockaddrIn*).copy_to(pointerof(@addr).as(LibC::SockaddrIn*), 1)
else
# TODO: (asterite) UNSPEC and UNIX unsupported?
end
end

def system_ip_address : ::Socket::IPAddress
::Socket::IPAddress.from(to_unsafe, size)
end

def to_unsafe
pointerof(@addr).as(LibC::Sockaddr*)
end

def self.getaddrinfo(domain, service, family, type, protocol, timeout) : Handle
hints = LibC::Addrinfo.new
hints.ai_family = (family || ::Socket::Family::UNSPEC).to_i32
hints.ai_socktype = type
hints.ai_protocol = protocol
hints.ai_flags = 0

if service.is_a?(Int)
hints.ai_flags |= LibC::AI_NUMERICSERV
end

# On OS X < 10.12, the libsystem implementation of getaddrinfo segfaults
# if AI_NUMERICSERV is set, and servname is NULL or 0.
{% if flag?(:darwin) %}
if service.in?(0, nil) && (hints.ai_flags & LibC::AI_NUMERICSERV)
hints.ai_flags |= LibC::AI_NUMERICSERV
service = "00"
end
{% end %}

ret = LibC.getaddrinfo(domain, service.to_s, pointerof(hints), out ptr)
unless ret.zero?
if ret == LibC::EAI_SYSTEM
raise ::Socket::Addrinfo::Error.from_os_error nil, Errno.value, domain: domain
end

error = Errno.new(ret)
raise ::Socket::Addrinfo::Error.from_os_error(nil, error, domain: domain, type: type, protocol: protocol, service: service)
end
ptr
end

def self.next_addrinfo(addrinfo : Handle) : Handle
addrinfo.value.ai_next
end

def self.free_addrinfo(addrinfo : Handle)
LibC.freeaddrinfo(addrinfo)
end
end
27 changes: 27 additions & 0 deletions src/crystal/system/wasi/addrinfo.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module Crystal::System::Addrinfo
alias Handle = NoReturn

protected def initialize(addrinfo : Handle)
raise NotImplementedError.new("Crystal::System::Addrinfo#initialize")
end

def system_ip_address : ::Socket::IPAddress
raise NotImplementedError.new("Crystal::System::Addrinfo#system_ip_address")
end

def to_unsafe
raise NotImplementedError.new("Crystal::System::Addrinfo#to_unsafe")
end

def self.getaddrinfo(domain, service, family, type, protocol, timeout) : Handle
raise NotImplementedError.new("Crystal::System::Addrinfo.getaddrinfo")
end

def self.next_addrinfo(addrinfo : Handle) : Handle
raise NotImplementedError.new("Crystal::System::Addrinfo.next_addrinfo")
end

def self.free_addrinfo(addrinfo : Handle)
raise NotImplementedError.new("Crystal::System::Addrinfo.free_addrinfo")
end
end
61 changes: 61 additions & 0 deletions src/crystal/system/win32/addrinfo.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
module Crystal::System::Addrinfo
alias Handle = LibC::Addrinfo*

@addr : LibC::SockaddrIn6

protected def initialize(addrinfo : Handle)
@family = ::Socket::Family.from_value(addrinfo.value.ai_family)
@type = ::Socket::Type.from_value(addrinfo.value.ai_socktype)
@protocol = ::Socket::Protocol.from_value(addrinfo.value.ai_protocol)
@size = addrinfo.value.ai_addrlen.to_i

@addr = uninitialized LibC::SockaddrIn6

case @family
when ::Socket::Family::INET6
addrinfo.value.ai_addr.as(LibC::SockaddrIn6*).copy_to(pointerof(@addr).as(LibC::SockaddrIn6*), 1)
when ::Socket::Family::INET
addrinfo.value.ai_addr.as(LibC::SockaddrIn*).copy_to(pointerof(@addr).as(LibC::SockaddrIn*), 1)
else
# TODO: (asterite) UNSPEC and UNIX unsupported?
end
end

def system_ip_address : ::Socket::IPAddress
::Socket::IPAddress.from(to_unsafe, size)
end

def to_unsafe
pointerof(@addr).as(LibC::Sockaddr*)
end

def self.getaddrinfo(domain, service, family, type, protocol, timeout) : Handle
hints = LibC::Addrinfo.new
hints.ai_family = (family || ::Socket::Family::UNSPEC).to_i32
hints.ai_socktype = type
hints.ai_protocol = protocol
hints.ai_flags = 0

if service.is_a?(Int)
hints.ai_flags |= LibC::AI_NUMERICSERV
if service < 0
raise ::Socket::Addrinfo::Error.from_os_error(nil, WinError::WSATYPE_NOT_FOUND, domain: domain, type: type, protocol: protocol, service: service)
end
end

ret = LibC.getaddrinfo(domain, service.to_s, pointerof(hints), out ptr)
unless ret.zero?
error = WinError.new(ret.to_u32!)
raise ::Socket::Addrinfo::Error.from_os_error(nil, error, domain: domain, type: type, protocol: protocol, service: service)
end
ptr
end

def self.next_addrinfo(addrinfo : Handle) : Handle
addrinfo.value.ai_next
end

def self.free_addrinfo(addrinfo : Handle)
LibC.freeaddrinfo(addrinfo)
end
end
107 changes: 20 additions & 87 deletions src/socket/addrinfo.cr
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
require "uri/punycode"
require "./address"
require "crystal/system/addrinfo"

class Socket
# Domain name resolver.
struct Addrinfo
include Crystal::System::Addrinfo

getter family : Family
getter type : Type
getter protocol : Protocol
getter size : Int32

@addr : LibC::SockaddrIn6

# Resolves a domain that best matches the given options.
#
# - *domain* may be an IP address or a domain name.
Expand Down Expand Up @@ -126,66 +127,22 @@ class Socket
end

private def self.getaddrinfo(domain, service, family, type, protocol, timeout, &)
{% if flag?(:wasm32) %}
raise NotImplementedError.new "Socket::Addrinfo.getaddrinfo"
{% else %}
# RFC 3986 says:
# > When a non-ASCII registered name represents an internationalized domain name
# > intended for resolution via the DNS, the name must be transformed to the IDNA
# > encoding [RFC3490] prior to name lookup.
domain = URI::Punycode.to_ascii domain

hints = LibC::Addrinfo.new
hints.ai_family = (family || Family::UNSPEC).to_i32
hints.ai_socktype = type
hints.ai_protocol = protocol
hints.ai_flags = 0

if service.is_a?(Int)
hints.ai_flags |= LibC::AI_NUMERICSERV
end

# On OS X < 10.12, the libsystem implementation of getaddrinfo segfaults
# if AI_NUMERICSERV is set, and servname is NULL or 0.
{% if flag?(:darwin) %}
if service.in?(0, nil) && (hints.ai_flags & LibC::AI_NUMERICSERV)
hints.ai_flags |= LibC::AI_NUMERICSERV
service = "00"
end
{% end %}
{% if flag?(:win32) %}
if service.is_a?(Int) && service < 0
raise Error.from_os_error(nil, WinError::WSATYPE_NOT_FOUND, domain: domain, type: type, protocol: protocol, service: service)
end
{% end %}

ret = LibC.getaddrinfo(domain, service.to_s, pointerof(hints), out ptr)
unless ret.zero?
{% if flag?(:unix) %}
# EAI_SYSTEM is not defined on win32
if ret == LibC::EAI_SYSTEM
raise Error.from_os_error nil, Errno.value, domain: domain
end
{% end %}

error = {% if flag?(:win32) %}
WinError.new(ret.to_u32!)
{% else %}
Errno.new(ret)
{% end %}
raise Error.from_os_error(nil, error, domain: domain, type: type, protocol: protocol, service: service)
# RFC 3986 says:
# > When a non-ASCII registered name represents an internationalized domain name
# > intended for resolution via the DNS, the name must be transformed to the IDNA
# > encoding [RFC3490] prior to name lookup.
domain = URI::Punycode.to_ascii domain

addrinfo = root = Crystal::System::Addrinfo.getaddrinfo(domain, service, family, type, protocol, timeout)

begin
while addrinfo
yield new(addrinfo)
addrinfo = Crystal::System::Addrinfo.next_addrinfo(addrinfo)
end

addrinfo = ptr
begin
while addrinfo
yield new(addrinfo)
addrinfo = addrinfo.value.ai_next
end
ensure
LibC.freeaddrinfo(ptr)
end
{% end %}
ensure
Crystal::System::Addrinfo.free_addrinfo(root)
end
end

# Resolves *domain* for the TCP protocol and returns an `Array` of possible
Expand Down Expand Up @@ -226,29 +183,9 @@ class Socket
resolve(domain, service, family, Type::DGRAM, Protocol::UDP) { |addrinfo| yield addrinfo }
end

protected def initialize(addrinfo : LibC::Addrinfo*)
@family = Family.from_value(addrinfo.value.ai_family)
@type = Type.from_value(addrinfo.value.ai_socktype)
@protocol = Protocol.from_value(addrinfo.value.ai_protocol)
@size = addrinfo.value.ai_addrlen.to_i

@addr = uninitialized LibC::SockaddrIn6

case @family
when Family::INET6
addrinfo.value.ai_addr.as(LibC::SockaddrIn6*).copy_to(pointerof(@addr).as(LibC::SockaddrIn6*), 1)
when Family::INET
addrinfo.value.ai_addr.as(LibC::SockaddrIn*).copy_to(pointerof(@addr).as(LibC::SockaddrIn*), 1)
else
# TODO: (asterite) UNSPEC and UNIX unsupported?
end
end

@ip_address : IPAddress?

# Returns an `IPAddress` matching this addrinfo.
def ip_address : Socket::IPAddress
@ip_address ||= IPAddress.from(to_unsafe, size)
getter(ip_address : Socket::IPAddress) do
system_ip_address
end

def inspect(io : IO)
Expand All @@ -259,9 +196,5 @@ class Socket
io << protocol
io << ")"
end

def to_unsafe
pointerof(@addr).as(LibC::Sockaddr*)
end
end
end

0 comments on commit 981c2e9

Please sign in to comment.