Skip to content

Commit

Permalink
Add Socket::IPaddress.v4, .v6, .v4_mapped_v6 (#13422)
Browse files Browse the repository at this point in the history
* Add `Socket.v4`, `.v6`, `.v4_mapped_v6`

* format

* stricter int types

* Revert "stricter int types"

This reverts commit f0474ef.

* add `StaticArray` constructors

* construct from `UInt16`s directly

* `.v4_mapped_v6`
  • Loading branch information
HertzDevil committed May 11, 2023
1 parent 9714499 commit a515985
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 6 deletions.
110 changes: 110 additions & 0 deletions spec/std/socket/address_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,116 @@ describe Socket::IPAddress do
end
end

describe ".v4" do
it "constructs an IPv4 address" do
Socket::IPAddress.v4(0, 0, 0, 0, port: 0).should eq Socket::IPAddress.new("0.0.0.0", 0)
Socket::IPAddress.v4(127, 0, 0, 1, port: 1234).should eq Socket::IPAddress.new("127.0.0.1", 1234)
Socket::IPAddress.v4(192, 168, 0, 1, port: 8081).should eq Socket::IPAddress.new("192.168.0.1", 8081)
Socket::IPAddress.v4(255, 255, 255, 254, port: 65535).should eq Socket::IPAddress.new("255.255.255.254", 65535)
end

it "raises on out of bound field" do
expect_raises(Socket::Error, "Invalid IPv4 field: 256") { Socket::IPAddress.v4(256, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: 256") { Socket::IPAddress.v4(0, 256, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: 256") { Socket::IPAddress.v4(0, 0, 256, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: 256") { Socket::IPAddress.v4(0, 0, 0, 256, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: -1") { Socket::IPAddress.v4(-1, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: -1") { Socket::IPAddress.v4(0, -1, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: -1") { Socket::IPAddress.v4(0, 0, -1, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: -1") { Socket::IPAddress.v4(0, 0, 0, -1, port: 0) }
end

it "raises on out of bound port number" do
expect_raises(Socket::Error, "Invalid port number: 65536") { Socket::IPAddress.v4(0, 0, 0, 0, port: 65536) }
expect_raises(Socket::Error, "Invalid port number: -1") { Socket::IPAddress.v4(0, 0, 0, 0, port: -1) }
end

it "constructs from StaticArray" do
Socket::IPAddress.v4(UInt8.static_array(0, 0, 0, 0), 0).should eq Socket::IPAddress.new("0.0.0.0", 0)
Socket::IPAddress.v4(UInt8.static_array(127, 0, 0, 1), 1234).should eq Socket::IPAddress.new("127.0.0.1", 1234)
Socket::IPAddress.v4(UInt8.static_array(192, 168, 0, 1), 8081).should eq Socket::IPAddress.new("192.168.0.1", 8081)
Socket::IPAddress.v4(UInt8.static_array(255, 255, 255, 254), 65535).should eq Socket::IPAddress.new("255.255.255.254", 65535)
end
end

describe ".v6" do
it "constructs an IPv6 address" do
Socket::IPAddress.v6(0, 0, 0, 0, 0, 0, 0, 0, port: 0).should eq Socket::IPAddress.new("::", 0)
Socket::IPAddress.v6(1, 2, 3, 4, 5, 6, 7, 8, port: 8080).should eq Socket::IPAddress.new("1:2:3:4:5:6:7:8", 8080)
Socket::IPAddress.v6(0xfe80, 0, 0, 0, 0x4860, 0x4860, 0x4860, 0x1234, port: 55001).should eq Socket::IPAddress.new("fe80::4860:4860:4860:1234", 55001)
Socket::IPAddress.v6(0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xfffe, port: 65535).should eq Socket::IPAddress.new("ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe", 65535)
Socket::IPAddress.v6(0, 0, 0, 0, 0, 0xffff, 0xc0a8, 0x0001, port: 0).should eq Socket::IPAddress.new("::ffff:192.168.0.1", 0)
end

it "raises on out of bound field" do
expect_raises(Socket::Error, "Invalid IPv6 field: 65536") { Socket::IPAddress.v6(65536, 0, 0, 0, 0, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: 65536") { Socket::IPAddress.v6(0, 65536, 0, 0, 0, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: 65536") { Socket::IPAddress.v6(0, 0, 65536, 0, 0, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: 65536") { Socket::IPAddress.v6(0, 0, 0, 65536, 0, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: 65536") { Socket::IPAddress.v6(0, 0, 0, 0, 65536, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: 65536") { Socket::IPAddress.v6(0, 0, 0, 0, 0, 65536, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: 65536") { Socket::IPAddress.v6(0, 0, 0, 0, 0, 0, 65536, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: 65536") { Socket::IPAddress.v6(0, 0, 0, 0, 0, 0, 0, 65536, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: -1") { Socket::IPAddress.v6(-1, 0, 0, 0, 0, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: -1") { Socket::IPAddress.v6(0, -1, 0, 0, 0, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: -1") { Socket::IPAddress.v6(0, 0, -1, 0, 0, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: -1") { Socket::IPAddress.v6(0, 0, 0, -1, 0, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: -1") { Socket::IPAddress.v6(0, 0, 0, 0, -1, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: -1") { Socket::IPAddress.v6(0, 0, 0, 0, 0, -1, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: -1") { Socket::IPAddress.v6(0, 0, 0, 0, 0, 0, -1, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv6 field: -1") { Socket::IPAddress.v6(0, 0, 0, 0, 0, 0, 0, -1, port: 0) }
end

it "raises on out of bound port number" do
expect_raises(Socket::Error, "Invalid port number: 65536") { Socket::IPAddress.v6(0, 0, 0, 0, 0, 0, 0, 0, port: 65536) }
expect_raises(Socket::Error, "Invalid port number: -1") { Socket::IPAddress.v6(0, 0, 0, 0, 0, 0, 0, 0, port: -1) }
end

it "constructs from StaticArray" do
Socket::IPAddress.v6(UInt16.static_array(0, 0, 0, 0, 0, 0, 0, 0), 0).should eq Socket::IPAddress.new("::", 0)
Socket::IPAddress.v6(UInt16.static_array(1, 2, 3, 4, 5, 6, 7, 8), 8080).should eq Socket::IPAddress.new("1:2:3:4:5:6:7:8", 8080)
Socket::IPAddress.v6(UInt16.static_array(0xfe80, 0, 0, 0, 0x4860, 0x4860, 0x4860, 0x1234), 55001).should eq Socket::IPAddress.new("fe80::4860:4860:4860:1234", 55001)
Socket::IPAddress.v6(UInt16.static_array(0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xfffe), 65535).should eq Socket::IPAddress.new("ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe", 65535)
Socket::IPAddress.v6(UInt16.static_array(0, 0, 0, 0, 0, 0xffff, 0xc0a8, 0x0001), 0).should eq Socket::IPAddress.new("::ffff:192.168.0.1", 0)
end
end

describe ".v4_mapped_v6" do
it "constructs an IPv4-mapped IPv6 address" do
# windows returns the former which, while correct, is not canonical
# TODO: implement also `#to_s` in Crystal
{Socket::IPAddress.new("::ffff:0:0", 0), Socket::IPAddress.new("::ffff:0.0.0.0", 0)}.should contain Socket::IPAddress.v4_mapped_v6(0, 0, 0, 0, port: 0)
Socket::IPAddress.v4_mapped_v6(127, 0, 0, 1, port: 1234).should eq Socket::IPAddress.new("::ffff:127.0.0.1", 1234)
Socket::IPAddress.v4_mapped_v6(192, 168, 0, 1, port: 8081).should eq Socket::IPAddress.new("::ffff:192.168.0.1", 8081)
Socket::IPAddress.v4_mapped_v6(255, 255, 255, 254, port: 65535).should eq Socket::IPAddress.new("::ffff:255.255.255.254", 65535)
end

it "raises on out of bound field" do
expect_raises(Socket::Error, "Invalid IPv4 field: 256") { Socket::IPAddress.v4_mapped_v6(256, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: 256") { Socket::IPAddress.v4_mapped_v6(0, 256, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: 256") { Socket::IPAddress.v4_mapped_v6(0, 0, 256, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: 256") { Socket::IPAddress.v4_mapped_v6(0, 0, 0, 256, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: -1") { Socket::IPAddress.v4_mapped_v6(-1, 0, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: -1") { Socket::IPAddress.v4_mapped_v6(0, -1, 0, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: -1") { Socket::IPAddress.v4_mapped_v6(0, 0, -1, 0, port: 0) }
expect_raises(Socket::Error, "Invalid IPv4 field: -1") { Socket::IPAddress.v4_mapped_v6(0, 0, 0, -1, port: 0) }
end

it "raises on out of bound port number" do
expect_raises(Socket::Error, "Invalid port number: 65536") { Socket::IPAddress.v4_mapped_v6(0, 0, 0, 0, port: 65536) }
expect_raises(Socket::Error, "Invalid port number: -1") { Socket::IPAddress.v4_mapped_v6(0, 0, 0, 0, port: -1) }
end

it "constructs from StaticArray" do
# windows returns the former which, while correct, is not canonical
# TODO: implement also `#to_s` in Crystal
{Socket::IPAddress.new("::ffff:0:0", 0), Socket::IPAddress.new("::ffff:0.0.0.0", 0)}.should contain Socket::IPAddress.v4_mapped_v6(UInt8.static_array(0, 0, 0, 0), 0)
Socket::IPAddress.v4_mapped_v6(UInt8.static_array(127, 0, 0, 1), 1234).should eq Socket::IPAddress.new("::ffff:127.0.0.1", 1234)
Socket::IPAddress.v4_mapped_v6(UInt8.static_array(192, 168, 0, 1), 8081).should eq Socket::IPAddress.new("::ffff:192.168.0.1", 8081)
Socket::IPAddress.v4_mapped_v6(UInt8.static_array(255, 255, 255, 254), 65535).should eq Socket::IPAddress.new("::ffff:255.255.255.254", 65535)
end
end

it ".valid_v6?" do
Socket::IPAddress.valid_v6?("::1").should be_true
Socket::IPAddress.valid_v6?("x").should be_false
Expand Down
107 changes: 101 additions & 6 deletions src/socket/address.cr
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class Socket
@addr : LibC::In6Addr | LibC::InAddr

def initialize(@address : String, @port : Int32)
raise Error.new("Invalid port number: #{port}") unless 0 <= port <= UInt16::MAX
raise Error.new("Invalid port number: #{port}") unless IPAddress.valid_port?(port)

if addr = IPAddress.address_v6?(address)
@addr = addr
Expand Down Expand Up @@ -144,16 +144,111 @@ class Socket
parse URI.parse(uri)
end

# Returns the IPv4 address with the given address *fields* and *port*
# number.
def self.v4(fields : UInt8[4], port : UInt16) : self
addr_value = UInt32.zero
fields.each_with_index do |field, i|
addr_value = (addr_value << 8) | field
end

addr = LibC::SockaddrIn.new(
sin_family: LibC::AF_INET,
sin_port: endian_swap(port),
sin_addr: LibC::InAddr.new(s_addr: endian_swap(addr_value)),
)
new(pointerof(addr), sizeof(typeof(addr)))
end

# Returns the IPv4 address `x0.x1.x2.x3:port`.
#
# Raises `Socket::Error` if any field or the port number is out of range.
def self.v4(x0 : Int, x1 : Int, x2 : Int, x3 : Int, *, port : Int) : self
fields = StaticArray[x0, x1, x2, x3].map { |field| to_v4_field(field) }
port = valid_port?(port) ? port.to_u16! : raise Error.new("Invalid port number: #{port}")
v4(fields, port)
end

private def self.to_v4_field(field)
0 <= field <= 0xff ? field.to_u8! : raise Error.new("Invalid IPv4 field: #{field}")
end

# Returns the IPv6 address with the given address *fields* and *port*
# number.
def self.v6(fields : UInt16[8], port : UInt16) : self
fields.map! { |field| endian_swap(field) }
addr = LibC::SockaddrIn6.new(
sin6_family: LibC::AF_INET6,
sin6_port: endian_swap(port),
sin6_addr: ipv6_from_addr16(fields),
)
new(pointerof(addr), sizeof(typeof(addr)))
end

# Returns the IPv6 address `[x0:x1:x2:x3:x4:x5:x6:x7]:port`.
#
# Raises `Socket::Error` if any field or the port number is out of range.
def self.v6(x0 : Int, x1 : Int, x2 : Int, x3 : Int, x4 : Int, x5 : Int, x6 : Int, x7 : Int, *, port : Int) : self
fields = StaticArray[x0, x1, x2, x3, x4, x5, x6, x7].map { |field| to_v6_field(field) }
port = valid_port?(port) ? port.to_u16! : raise Error.new("Invalid port number: #{port}")
v6(fields, port)
end

private def self.to_v6_field(field)
0 <= field <= 0xffff ? field.to_u16! : raise Error.new("Invalid IPv6 field: #{field}")
end

# Returns the IPv4-mapped IPv6 address with the given IPv4 address *fields*
# and *port* number.
def self.v4_mapped_v6(fields : UInt8[4], port : UInt16) : self
v6_fields = StaticArray[
0_u16, 0_u16, 0_u16, 0_u16, 0_u16, 0xffff_u16,
fields[0].to_u16! << 8 | fields[1],
fields[2].to_u16! << 8 | fields[3],
]
v6(v6_fields, port)
end

# Returns the IPv4-mapped IPv6 address `[::ffff:x0.x1.x2.x3]:port`.
#
# Raises `Socket::Error` if any field or the port number is out of range.
def self.v4_mapped_v6(x0 : Int, x1 : Int, x2 : Int, x3 : Int, *, port : Int) : self
v4_fields = StaticArray[x0, x1, x2, x3].map { |field| to_v4_field(field) }
port = valid_port?(port) ? port.to_u16! : raise Error.new("Invalid port number: #{port}")
v4_mapped_v6(v4_fields, port)
end

private def self.ipv6_from_addr16(bytes : UInt16[8])
addr = LibC::In6Addr.new
{% if flag?(:darwin) || flag?(:bsd) %}
addr.__u6_addr.__u6_addr16 = bytes
{% elsif flag?(:linux) && flag?(:musl) %}
addr.__in6_union.__s6_addr16 = bytes
{% elsif flag?(:wasm32) %}
bytes.each_with_index do |byte, i|
addr.s6_addr[2 * i] = byte.to_u8!
addr.s6_addr[2 * i + 1] = (byte >> 8).to_u8!
end
{% elsif flag?(:linux) %}
addr.__in6_u.__u6_addr16 = bytes
{% elsif flag?(:win32) %}
addr.u.word = bytes
{% else %}
{% raise "Unsupported platform" %}
{% end %}
addr
end

protected def initialize(sockaddr : LibC::SockaddrIn6*, @size)
@family = Family::INET6
@addr = sockaddr.value.sin6_addr
@port = endian_swap(sockaddr.value.sin6_port).to_i
@port = IPAddress.endian_swap(sockaddr.value.sin6_port).to_i
end

protected def initialize(sockaddr : LibC::SockaddrIn*, @size)
@family = Family::INET
@addr = sockaddr.value.sin_addr
@port = endian_swap(sockaddr.value.sin_port).to_i
@port = IPAddress.endian_swap(sockaddr.value.sin_port).to_i
end

# Returns `true` if *address* is a valid IPv4 or IPv6 address.
Expand Down Expand Up @@ -313,20 +408,20 @@ class Socket
private def to_sockaddr_in6(addr)
sockaddr = Pointer(LibC::SockaddrIn6).malloc
sockaddr.value.sin6_family = family
sockaddr.value.sin6_port = endian_swap(port.to_u16!)
sockaddr.value.sin6_port = IPAddress.endian_swap(port.to_u16!)
sockaddr.value.sin6_addr = addr
sockaddr.as(LibC::Sockaddr*)
end

private def to_sockaddr_in(addr)
sockaddr = Pointer(LibC::SockaddrIn).malloc
sockaddr.value.sin_family = family
sockaddr.value.sin_port = endian_swap(port.to_u16!)
sockaddr.value.sin_port = IPAddress.endian_swap(port.to_u16!)
sockaddr.value.sin_addr = addr
sockaddr.as(LibC::Sockaddr*)
end

private def endian_swap(x : UInt16) : UInt16
protected def self.endian_swap(x : Int::Primitive) : Int::Primitive
{% if IO::ByteFormat::NetworkEndian != IO::ByteFormat::SystemEndian %}
x.byte_swap
{% else %}
Expand Down

0 comments on commit a515985

Please sign in to comment.