diff --git a/spec/std/socket/address_spec.cr b/spec/std/socket/address_spec.cr index 6a6f0d926fd7..e2793e4f1ce0 100644 --- a/spec/std/socket/address_spec.cr +++ b/spec/std/socket/address_spec.cr @@ -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 diff --git a/src/socket/address.cr b/src/socket/address.cr index 4a6ea10dfb62..c077e4d74e48 100644 --- a/src/socket/address.cr +++ b/src/socket/address.cr @@ -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 @@ -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. @@ -313,7 +408,7 @@ 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 @@ -321,12 +416,12 @@ class Socket 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 %}