require "./common" require "uri" class Socket abstract struct Address getter family : Family getter size : Int32 # Returns either an `IPAddress` or `UNIXAddress` from the internal OS # representation. Only INET, INET6 and UNIX families are supported. def self.from(sockaddr : LibC::Sockaddr*, addrlen) : Address case family = Family.new(sockaddr.value.sa_family) when Family::INET6 IPAddress.new(sockaddr.as(LibC::SockaddrIn6*), addrlen.to_i) when Family::INET IPAddress.new(sockaddr.as(LibC::SockaddrIn*), addrlen.to_i) when Family::UNIX UNIXAddress.new(sockaddr.as(LibC::SockaddrUn*), addrlen.to_i) else raise "Unsupported family type: #{family} (#{family.value})" end end # :ditto: def self.from(sockaddr : LibC::Sockaddr*) : Address case family = Family.new(sockaddr.value.sa_family) when Family::INET6 sockaddr = sockaddr.as(LibC::SockaddrIn6*) IPAddress.new(sockaddr, sizeof(typeof(sockaddr))) when Family::INET sockaddr = sockaddr.as(LibC::SockaddrIn*) IPAddress.new(sockaddr, sizeof(typeof(sockaddr))) when Family::UNIX sockaddr = sockaddr.as(LibC::SockaddrUn*) UNIXAddress.new(sockaddr, sizeof(typeof(sockaddr))) else raise "Unsupported family type: #{family} (#{family.value})" end end # Parses a `Socket::Address` from an URI. # # Supported formats: # * `ip://:` # * `tcp://:` # * `udp://:` # * `unix://` # # See `IPAddress.parse` and `UNIXAddress.parse` for details. def self.parse(uri : URI) : self case uri.scheme when "ip", "tcp", "udp" IPAddress.parse uri when "unix" UNIXAddress.parse uri else raise Socket::Error.new "Unsupported address type: #{uri.scheme}" end end # :ditto: def self.parse(uri : String) : self parse URI.parse(uri) end def initialize(@family : Family, @size : Int32) end abstract def to_unsafe : LibC::Sockaddr* end # IP address representation. # # Holds a binary representation of an IP address, either translated from a # `String`, or directly received from an opened connection (e.g. # `Socket#local_address`, `Socket#receive`). # # `IPAddress` won't resolve domains, including `localhost`. If you must # resolve an IP, or don't know whether a `String` contains an IP or a domain # name, you should use `Addrinfo.resolve` instead. struct IPAddress < Address UNSPECIFIED = "0.0.0.0" UNSPECIFIED6 = "::" LOOPBACK = "127.0.0.1" LOOPBACK6 = "::1" BROADCAST = "255.255.255.255" BROADCAST6 = "ff0X::1" getter port : Int32 getter zone_id : Int32 @addr : LibC::In6Addr | LibC::InAddr # Creates an `IPAddress` from the given IPv4 or IPv6 *address* and *port* # number. # # *address* is parsed using `.parse_v4_fields?` and `.parse_v6_fields?`. # Raises `Socket::Error` if *address* does not contain a valid IP address or # the port number is out of range. # # Scoped/Zoned IPv6 link-local addresses are supported per RFC4007, e.g. # `fe80::abcd%eth0` but will always use their numerical interface index # in the `#inspect` representation. The interface name can be retrieved later # using `#link_local_interface` on the `IPAddress` object. # # ``` # require "socket" # # Socket::IPAddress.new("127.0.0.1", 8080) # => Socket::IPAddress(127.0.0.1:8080) # Socket::IPAddress.new("fe80::2ab2:bdff:fe59:8e2c", 1234) # => Socket::IPAddress([fe80::2ab2:bdff:fe59:8e2c]:1234) # Socket::IPAddress.new("fe80::4567:8:9%eth0", 443) # => Socket::IPAddress([fe80::4567:8:9%2]:443) # ``` def self.new(address : String, port : Int32) raise Error.new("Invalid port number: #{port}") unless IPAddress.valid_port?(port) if v4_fields = parse_v4_fields?(address) addr = v4(v4_fields, port.to_u16!) else v6_fields_tpl = parse_v6_fields?(address.to_slice) raise Error.new("Invalid IP address: #{address}") if v6_fields_tpl.nil? v6_fields, zone_slice = v6_fields_tpl zone_id = 0 unless zone_slice.nil? # `zone_id` is only relevant for link-local addresses, i.e. beginning with "fe80:". if v6_fields[0] != 0xfe80 raise Error.new("Zoned/scoped IPv6 addresses are only allowed for link-local (supplied '#{address}' is not within fe80::/10)") end # Scope/Zone can be given either as a network interface name or directly as the interface index. # When given a name we need to find the corresponding interface index. zone = String.new(zone_slice) if zone_id = zone.to_i? raise Error.new("Invalid IPv6 link-local zone index '#{zone}' in address '#{address}'") unless zone_id.positive? else zone_id = LibC.if_nametoindex(zone).to_i raise Error.new("IPv6 link-local zone interface '#{zone}' not found (in address '#{address}')") unless zone_id.positive? end end addr = v6(v6_fields, port.to_u16!, zone_id) end addr end # Creates an `IPAddress` from the internal OS representation. Supports both # INET and INET6 families. def self.from(sockaddr : LibC::Sockaddr*, addrlen) : IPAddress case family = Family.new(sockaddr.value.sa_family) when Family::INET6 new(sockaddr.as(LibC::SockaddrIn6*), addrlen.to_i) when Family::INET new(sockaddr.as(LibC::SockaddrIn*), addrlen.to_i) else raise "Unsupported family type: #{family} (#{family.value})" end end # :ditto: def self.from(sockaddr : LibC::Sockaddr*) : IPAddress case family = Family.new(sockaddr.value.sa_family) when Family::INET6 sockaddr = sockaddr.as(LibC::SockaddrIn6*) new(sockaddr, sizeof(typeof(sockaddr))) when Family::INET sockaddr = sockaddr.as(LibC::SockaddrIn*) new(sockaddr, sizeof(typeof(sockaddr))) else raise "Unsupported family type: #{family} (#{family.value})" end end # Parses a `Socket::IPAddress` from an URI. # # It expects the URI to include `://:` where `scheme` as # well as any additional URI components (such as `path` or `query`) are ignored. # # `host` must be an IP address (v4 or v6), otherwise `Socket::Error` will be # raised. Domain names will not be resolved. # # ``` # require "socket" # # Socket::IPAddress.parse("tcp://127.0.0.1:8080") # => Socket::IPAddress.new("127.0.0.1", 8080) # Socket::IPAddress.parse("udp://[::1]:8080") # => Socket::IPAddress.new("::1", 8080) # ``` def self.parse(uri : URI) : IPAddress host = uri.host.presence raise Socket::Error.new("Invalid IP address: missing host") unless host port = uri.port raise Socket::Error.new("Invalid IP address: missing port") unless port # remove ipv6 brackets if host.starts_with?('[') && host.ends_with?(']') host = host.byte_slice(1, host.bytesize - 2) end new(host, port) end # :ditto: def self.parse(uri : String) : self parse URI.parse(uri) end # Parses *str* as an IPv4 address and returns its fields, or returns `nil` # if *str* does not contain a valid IPv4 address. # # The format of IPv4 addresses follows # [RFC 3493, section 6.3](https://datatracker.ietf.org/doc/html/rfc3493#section-6.3). # No extensions (e.g. octal fields, fewer than 4 fields) are supported. # # ``` # require "socket" # # Socket::IPAddress.parse_v4_fields?("192.168.0.1") # => UInt8.static_array(192, 168, 0, 1) # Socket::IPAddress.parse_v4_fields?("255.255.255.254") # => UInt8.static_array(255, 255, 255, 254) # Socket::IPAddress.parse_v4_fields?("01.2.3.4") # => nil # ``` def self.parse_v4_fields?(str : String) : UInt8[4]? parse_v4_fields?(str.to_slice) end private def self.parse_v4_fields?(bytes : Bytes) # port of https://git.musl-libc.org/cgit/musl/tree/src/network/inet_pton.c?id=7e13e5ae69a243b90b90d2f4b79b2a150f806335 fields = StaticArray(UInt8, 4).new(0) ptr = bytes.to_unsafe finish = ptr + bytes.size 4.times do |i| decimal = 0_u32 old_ptr = ptr 3.times do break unless ptr < finish ch = ptr.value &- 0x30 break unless ch <= 0x09 decimal = decimal &* 10 &+ ch ptr += 1 end return nil if ptr == old_ptr # no decimal return nil if ptr - old_ptr > 1 && old_ptr.value === '0' # octal etc. return nil if decimal > 0xFF # overflow fields[i] = decimal.to_u8! break if i == 3 return nil unless ptr < finish && ptr.value === '.' ptr += 1 end fields if ptr == finish end # Parses *str* as an IPv6 address and returns its fields, or returns `nil` # if *str* does not contain a valid IPv6 address. # # The format of IPv6 addresses follows # [RFC 4291, section 2.2](https://datatracker.ietf.org/doc/html/rfc4291#section-2.2). # Both canonical and non-canonical forms are supported. # # ``` # require "socket" # # Socket::IPAddress.parse_v6_fields?("::1") # => UInt16.static_array(0, 0, 0, 0, 0, 0, 0, 1) # Socket::IPAddress.parse_v6_fields?("a:0b:00c:000d:E:F::") # => UInt16.static_array(10, 11, 12, 13, 14, 15, 0, 0) # Socket::IPAddress.parse_v6_fields?("::ffff:192.168.1.1") # => UInt16.static_array(0, 0, 0, 0, 0, 0xffff, 0xc0a8, 0x0101) # Socket::IPAddress.parse_v6_fields?("1::2::") # => nil # Socket::IPAddress.parse_v6_fields?("fe80::a:b%eth0") # => StaticArray[65152, 0, 0, 0, 0, 0, 10, 11] # ``` def self.parse_v6_fields?(str : String) : UInt16[8]? parse_v6_fields?(str.to_slice).try &.[0] end # This private method additionally supports IPv6 scoped addresses and # returns its fields plus the zone/scope subslice (if present). # Invalid addresses will immediately return just `nil` while correctly # formatted addresses return a tuple. # # The format of IPv6 scoped addresses follows # [RFC 4007, section 11](https://datatracker.ietf.org/doc/html/rfc4007#section-11). private def self.parse_v6_fields?(bytes : Bytes) : Tuple(UInt16[8], Slice(UInt8)?)? # port of https://git.musl-libc.org/cgit/musl/tree/src/network/inet_pton.c?id=7e13e5ae69a243b90b90d2f4b79b2a150f806335 ptr = bytes.to_unsafe finish = ptr + bytes.size if ptr < finish && ptr.value === ':' ptr += 1 return nil unless ptr < finish && ptr.value === ':' end fields = StaticArray(UInt16, 8).new(0) brk = -1 need_v4 = false zone_slice = nil i = 0 while true if ptr < finish && ptr.value === ':' && brk < 0 brk = i fields[i] = 0 ptr += 1 break if ptr == finish return nil if i == 7 i &+= 1 next end field = 0_u16 old_ptr = ptr 4.times do break unless ptr < finish ch = from_hex(ptr.value) break unless ch <= 0x0F field = field.unsafe_shl(4) | ch ptr += 1 end return nil if ptr == old_ptr # no field fields[i] = field break if ptr == finish && (brk >= 0 || i == 7) return nil if i == 7 unless ptr < finish && ptr.value === ':' if (ptr < finish && ptr.value === '%') zone_slice = Bytes.new(ptr + 1, finish - ptr - 1) break end return nil if !(ptr < finish && ptr.value === '.') || (i < 6 && brk < 0) need_v4 = true i &+= 1 fields[i] = 0 ptr = old_ptr break end ptr += 1 i &+= 1 end if brk >= 0 fields_brk = fields.to_unsafe + brk (fields_brk + 7 - i).move_from(fields_brk, i &+ 1 &- brk) fields_brk.clear(7 &- i) end if need_v4 slice = Slice.new(ptr, finish - ptr) if suffix = slice.index('%'.ord.to_u8!) zone_slice = slice[suffix..] + 1 slice = slice[0, suffix] end x0, x1, x2, x3 = parse_v4_fields?(slice) || return nil fields[6] = x0.to_u16! << 8 | x1 fields[7] = x2.to_u16! << 8 | x3 end {fields, zone_slice} end private def self.from_hex(ch : UInt8) if 0x30 <= ch <= 0x39 ch &- 0x30 elsif 0x41 <= ch <= 0x46 ch &- 0x37 elsif 0x61 <= ch <= 0x66 ch &- 0x57 else 0xFF_u8 end 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*, *port* number # and scope identifier. def self.v6(fields : UInt16[8], port : UInt16, zone_id : Int32 = 0) : 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), sin6_scope_id: zone_id, ) 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, zone_id : Int32 = 0) : 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, zone_id) 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?(:solaris) %} addr._S6_un._S6_u16 = 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 @zone_id = sockaddr.value.sin6_scope_id.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 @zone_id = 0 @port = IPAddress.endian_swap(sockaddr.value.sin_port).to_i end # Returns `true` if *address* is a valid IPv4 or IPv6 address. def self.valid?(address : String) : Bool valid_v4?(address) || valid_v6?(address) end # Returns `true` if *address* is a valid IPv6 address. def self.valid_v6?(address : String) : Bool !parse_v6_fields?(address).nil? end # Returns `true` if *address* is a valid IPv4 address. def self.valid_v4?(address : String) : Bool !parse_v4_fields?(address).nil? end # Returns a `String` representation of the IP address, without the port # number. # # IPv6 addresses are canonicalized according to # [RFC 5952, section 4](https://datatracker.ietf.org/doc/html/rfc5952#section-4). # IPv4-mapped IPv6 addresses use the mixed notation according to RFC 5952, # section 5. # # ``` # require "socket" # # v4 = Socket::IPAddress.v4(UInt8.static_array(127, 0, 0, 1), 8080) # v4.address # => "127.0.0.1" # # v6 = Socket::IPAddress.v6(UInt16.static_array(0x2001, 0xdb8, 0, 0, 1, 0, 0, 1), 443) # v6.address # => "2001:db8::1:0:0:1" # # mapped = Socket::IPAddress.v4_mapped_v6(UInt8.static_array(192, 168, 1, 15), 55001) # mapped.address # => "::ffff:192.168.1.15" # ``` # # To obtain both the address and the port number in one string, see `#to_s`. def address : String case addr = @addr in LibC::InAddr String.build(IPV4_MAX_SIZE) do |io| address_to_s(io, addr) end in LibC::In6Addr String.build(IPV6_MAX_SIZE) do |io| address_to_s(io, addr) end end end private IPV4_MAX_SIZE = 15 # "255.255.255.255".size # NOTE: INET6_ADDRSTRLEN is 46 bytes (including the terminating null # character), but it is only attainable for mixed-notation addresses that # use all 24 hexadecimal digits in the IPv6 field part, which we do not # support private IPV6_MAX_SIZE = 39 # "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff".size # Returns `true` if this IP is a loopback address. # # In the IPv4 family, loopback addresses are all addresses in the subnet # `127.0.0.0/24`. In IPv6 `::1` is the loopback address. def loopback? : Bool case addr = @addr in LibC::InAddr addr.s_addr & 0x000000ff_u32 == 0x0000007f_u32 in LibC::In6Addr addr8 = ipv6_addr8(addr) num = addr8.unsafe_as(UInt128) # TODO: Use UInt128 literals num == (1_u128 << 120) || # "::1" num & UInt128::MAX >> 24 == 0x7fffff_u128 << 80 # "::ffff:127.0.0.1/104" end end # Returns `true` if this IP is an unspecified address, either the IPv4 address `0.0.0.0` or the IPv6 address `::`. def unspecified? : Bool case addr = @addr in LibC::InAddr addr.s_addr == 0_u32 in LibC::In6Addr ipv6_addr8(addr) == StaticArray[0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8, 0_u8] end end # Returns `true` if this IP is a private address. # # IPv4 addresses in `10.0.0.0/8`, `172.16.0.0/12` and `192.168.0.0/16` as defined in [RFC 1918](https://tools.ietf.org/html/rfc1918) # and IPv6 Unique Local Addresses in `fc00::/7` as defined in [RFC 4193](https://tools.ietf.org/html/rfc4193) are considered private. def private? : Bool case addr = @addr in LibC::InAddr addr.s_addr & 0x000000ff_u32 == 0x00000000a_u32 || # 10.0.0.0/8 addr.s_addr & 0x000000f0ff_u32 == 0x0000010ac_u32 || # 172.16.0.0/12 addr.s_addr & 0x000000ffff_u32 == 0x0000a8c0_u32 # 192.168.0.0/16 in LibC::In6Addr ipv6_addr8(addr)[0] & 0xfe_u8 == 0xfc_u8 end end # Returns `true` if this IP is a link-local address. # # IPv4 addresses in `169.254.0.0/16` reserved by [RFC 3927](https://www.rfc-editor.org/rfc/rfc3927) and Link-Local IPv6 # Unicast Addresses in `fe80::/10` reserved by [RFC 4291](https://tools.ietf.org/html/rfc4291) are considered link-local. def link_local? case addr = @addr in LibC::InAddr addr.s_addr & 0x000000ffff_u32 == 0x0000fea9_u32 # 169.254.0.0/16 in LibC::In6Addr ipv6_addr8(addr).unsafe_as(UInt128) & 0xc0ff_u128 == 0x80fe_u128 end end private def ipv6_addr8(addr : LibC::In6Addr) {% if flag?(:darwin) || flag?(:bsd) %} addr.__u6_addr.__u6_addr8 {% elsif flag?(:linux) && flag?(:musl) %} addr.__in6_union.__s6_addr {% elsif flag?(:wasm32) %} addr.s6_addr {% elsif flag?(:linux) %} addr.__in6_u.__u6_addr8 {% elsif flag?(:solaris) %} addr._S6_un._S6_u8 {% elsif flag?(:win32) %} addr.u.byte {% else %} {% raise "Unsupported platform" %} {% end %} end def_equals_and_hash family, port, address_value protected def address_value case addr = @addr in LibC::InAddr addr.s_addr in LibC::In6Addr ipv6_addr8(addr).unsafe_as(UInt128) end end # Writes the `String` representation of the IP address plus the port number # to the given *io*. # # IPv6 addresses are canonicalized according to # [RFC 5952, section 4](https://datatracker.ietf.org/doc/html/rfc5952#section-4), # and surrounded within a pair of square brackets according to # [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986). # IPv4-mapped IPv6 addresses use the mixed notation according to RFC 5952, # section 5. # # To obtain the address alone without the port number, see `#address`. def to_s(io : IO) : Nil case addr = @addr in LibC::InAddr address_to_s(io, addr) io << ':' << port in LibC::In6Addr io << '[' address_to_s(io, addr) io << '%' << @zone_id if @zone_id.positive? io << ']' << ':' << port end end private def address_to_s(io : IO, addr : LibC::InAddr) io << (addr.s_addr & 0xFF) io << '.' << (addr.s_addr >> 8 & 0xFF) io << '.' << (addr.s_addr >> 16 & 0xFF) io << '.' << (addr.s_addr >> 24) end private def address_to_s(io : IO, addr : LibC::In6Addr) bytes = ipv6_addr8(addr) if Slice.new(bytes.to_unsafe, 10).all?(&.zero?) && bytes[10] == 0xFF && bytes[11] == 0xFF io << "::ffff:" << bytes[12] << '.' << bytes[13] << '.' << bytes[14] << '.' << bytes[15] else fields = bytes.unsafe_as(StaticArray(UInt16, 8)).map! { |field| IPAddress.endian_swap(field) } zeros_start = nil zeros_count_max = 1 count = 0 fields.each_with_index do |field, i| if field == 0 count += 1 if count > zeros_count_max zeros_start = i - count + 1 zeros_count_max = count end else count = 0 end end i = 0 while i < 8 if i == zeros_start io << ':' i += zeros_count_max io << ':' if i == 8 else io << ':' if i > 0 fields[i].to_s(io, base: 16) i += 1 end end end end private IPV4_FULL_MAX_SIZE = IPV4_MAX_SIZE + 6 # ":65535".size private IPV6_FULL_MAX_SIZE = IPV6_MAX_SIZE + 8 # "[".size + "]:65535".size # Returns a `String` representation of the IP address plus the port number. # # IPv6 addresses are canonicalized according to # [RFC 5952, section 4](https://datatracker.ietf.org/doc/html/rfc5952#section-4), # and surrounded within a pair of square brackets according to # [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986). # IPv4-mapped IPv6 addresses use the mixed notation according to RFC 5952, # section 5. # # ``` # require "socket" # # v4 = Socket::IPAddress.v4(UInt8.static_array(127, 0, 0, 1), 8080) # v4.to_s # => "127.0.0.1:8080" # # v6 = Socket::IPAddress.v6(UInt16.static_array(0x2001, 0xdb8, 0, 0, 1, 0, 0, 1), 443) # v6.to_s # => "[2001:db8::1:0:0:1]:443" # # mapped = Socket::IPAddress.v4_mapped_v6(UInt8.static_array(192, 168, 1, 15), 55001) # mapped.to_s # => "[::ffff:192.168.1.15]:55001" # ``` # # To obtain the address alone without the port number, see `#address`. def to_s : String String.build(@addr.is_a?(LibC::InAddr) ? IPV4_FULL_MAX_SIZE : IPV6_FULL_MAX_SIZE) do |io| to_s(io) end end def inspect(io : IO) : Nil io << "Socket::IPAddress(" to_s(io) io << ")" end def inspect : String # 19 == "Socket::IPAddress(".size + ")".size String.build((@addr.is_a?(LibC::InAddr) ? IPV4_FULL_MAX_SIZE : IPV6_FULL_MAX_SIZE) + 19) do |io| inspect(io) end end def pretty_print(pp) pp.text inspect end def to_unsafe : LibC::Sockaddr* case addr = @addr in LibC::InAddr to_sockaddr_in(addr) in LibC::In6Addr to_sockaddr_in6(addr) end end private def to_sockaddr_in6(addr) sockaddr = Pointer(LibC::SockaddrIn6).malloc sockaddr.value.sin6_family = family sockaddr.value.sin6_port = IPAddress.endian_swap(port.to_u16!) sockaddr.value.sin6_addr = addr if @family == Family::INET6 && link_local? sockaddr.value.sin6_scope_id = @zone_id else sockaddr.value.sin6_scope_id = 0 end 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 = IPAddress.endian_swap(port.to_u16!) sockaddr.value.sin_addr = addr sockaddr.as(LibC::Sockaddr*) end # Returns the interface name for a scoped/zoned link-local IPv6 address. # This only works on properly initialized link-local IPv6 address objects. # In any other case this will return nil. # # The OS tracks the zone via a numerical interface index as enumerated # by the kernel. To keep our abstraction class in line, we also only # keep the interface index around. # # This helper method exists to look up the interface name based on the # associated zone_id property. def link_local_interface : String | Nil {% unless LibC.has_method?(:if_nametoindex) %} raise NotImplementedError.new "Socket::Address.link_local_interface" {% end %} return nil if @zone_id.zero? return nil unless (@family == Socket::Family::INET6 && link_local?) buf = uninitialized StaticArray(UInt8, LibC::IF_NAMESIZE) result = LibC.if_indextoname(@zone_id, buf) if result.null? message = "Failed to look up interface name for index #{@zone_id}" {% if flag?(:windows) %} # In windows it is not possible to determine an error code here raise Error.new(message) {% else %} raise Error.from_errno(message) {% end %} end String.new(buf.to_unsafe) end protected def self.endian_swap(x : Int::Primitive) : Int::Primitive {% if IO::ByteFormat::NetworkEndian != IO::ByteFormat::SystemEndian %} x.byte_swap {% else %} x {% end %} end # Returns `true` if *port* is a valid port number. # # Valid port numbers are in the range `0..65_535`. def self.valid_port?(port : Int) : Bool port.in?(0..UInt16::MAX) end end # UNIX address representation. # # Holds the local path of an UNIX address, usually coming from an opened # connection (e.g. `Socket#local_address`, `Socket#receive`). # # Example: # ``` # require "socket" # # Socket::UNIXAddress.new("/tmp/my.sock") # ``` struct UNIXAddress < Address getter path : String # :nodoc: MAX_PATH_SIZE = {% if flag?(:wasm32) %} 0 {% else %} sizeof(typeof(LibC::SockaddrUn.new.sun_path)) - 1 {% end %} def initialize(path : Path | String) @path = path.to_s if @path.bytesize > MAX_PATH_SIZE raise ArgumentError.new("Path size exceeds the maximum size of #{MAX_PATH_SIZE} bytes") end @family = Family::UNIX @size = {% if flag?(:wasm32) %} 1 {% else %} sizeof(LibC::SockaddrUn) {% end %} end # Creates an `UNIXSocket` from the internal OS representation. def self.from(sockaddr : LibC::Sockaddr*, addrlen) : UNIXAddress {% if flag?(:wasm32) %} raise NotImplementedError.new "Socket::UNIXAddress.from" {% else %} new(sockaddr.as(LibC::SockaddrUn*), addrlen.to_i) {% end %} end # :ditto: def self.from(sockaddr : LibC::Sockaddr*) : UNIXAddress {% if flag?(:wasm32) %} raise NotImplementedError.new "Socket::UNIXAddress.from" {% else %} sockaddr = sockaddr.as(LibC::SockaddrUn*) new(sockaddr, sizeof(typeof(sockaddr))) {% end %} end # Parses a `Socket::UNIXAddress` from an URI. # # It expects the URI to include `://` where `scheme` as well # as any additional URI components (such as `fragment` or `query`) are ignored. # # If `host` is not empty, it will be prepended to `path` to form a relative # path. # # ``` # require "socket" # # Socket::UNIXAddress.parse("unix:///foo.sock") # => Socket::UNIXAddress.new("/foo.sock") # Socket::UNIXAddress.parse("unix://foo.sock") # => Socket::UNIXAddress.new("foo.sock") # ``` def self.parse(uri : URI) : UNIXAddress unix_path = String.build do |io| io << uri.host if port = uri.port io << ':' << port end if path = uri.path.presence io << path end end raise Socket::Error.new("Invalid UNIX address: missing path") if unix_path.empty? {% if flag?(:wasm32) %} raise NotImplementedError.new "Socket::UNIXAddress.parse" {% else %} UNIXAddress.new(unix_path) {% end %} end # :ditto: def self.parse(uri : String) : self parse URI.parse(uri) end {% unless flag?(:wasm32) %} protected def initialize(sockaddr : LibC::SockaddrUn*, size) @family = Family::UNIX @path = String.new(sockaddr.value.sun_path.to_slice, truncate_at_null: true) @size = size || sizeof(LibC::SockaddrUn) end {% end %} def_equals_and_hash path def to_s(io : IO) : Nil io << path end def to_unsafe : LibC::Sockaddr* {% if flag?(:wasm32) %} raise NotImplementedError.new "Socket::UNIXAddress#to_unsafe" {% else %} sockaddr = Pointer(LibC::SockaddrUn).malloc sockaddr.value.sun_family = family sockaddr.value.sun_path.to_unsafe.copy_from(@path.to_unsafe, {@path.bytesize + 1, sockaddr.value.sun_path.size}.min) sockaddr.as(LibC::Sockaddr*) {% end %} end end # Returns `true` if the string represents a valid IPv4 or IPv6 address. @[Deprecated("Use `IPAddress.valid?` instead")] def self.ip?(string : String) IPAddress.valid?(string) end end