diff --git a/spec/std/socket/address_spec.cr b/spec/std/socket/address_spec.cr index 61ff42380b47..59167fc9c041 100644 --- a/spec/std/socket/address_spec.cr +++ b/spec/std/socket/address_spec.cr @@ -97,6 +97,27 @@ describe Socket::IPAddress do end end end + + it "#loopback?" do + Socket::IPAddress.new("127.0.0.1", 0).loopback?.should be_true + Socket::IPAddress.new("127.255.255.254", 0).loopback?.should be_true + Socket::IPAddress.new("128.0.0.1", 0).loopback?.should be_false + Socket::IPAddress.new("0.0.0.0", 0).loopback?.should be_false + Socket::IPAddress.new("::1", 0).loopback?.should be_true + Socket::IPAddress.new("0000:0000:0000:0000:0000:0000:0000:0001", 0).loopback?.should be_true + Socket::IPAddress.new("::2", 0).loopback?.should be_false + Socket::IPAddress.new(Socket::IPAddress::LOOPBACK, 0).loopback?.should be_true + Socket::IPAddress.new(Socket::IPAddress::LOOPBACK6, 0).loopback?.should be_true + end + + it "#unspecified?" do + Socket::IPAddress.new("0.0.0.0", 0).unspecified?.should be_true + Socket::IPAddress.new("127.0.0.1", 0).unspecified?.should be_false + Socket::IPAddress.new("::", 0).unspecified?.should be_true + Socket::IPAddress.new("0000:0000:0000:0000:0000:0000:0000:0000", 0).unspecified?.should be_true + Socket::IPAddress.new(Socket::IPAddress::UNSPECIFIED, 0).unspecified?.should be_true + Socket::IPAddress.new(Socket::IPAddress::UNSPECIFIED6, 0).unspecified?.should be_true + end end describe Socket::UNIXAddress do diff --git a/src/compiler/crystal/tools/playground/server.cr b/src/compiler/crystal/tools/playground/server.cr index 460c1ec19ed5..2746bc21d7e2 100644 --- a/src/compiler/crystal/tools/playground/server.cr +++ b/src/compiler/crystal/tools/playground/server.cr @@ -528,17 +528,12 @@ module Crystal::Playground server = HTTP::Server.new handlers - host = @host - if host - address = server.bind_tcp host, @port - else - address = server.bind_tcp @port - end + address = server.bind_tcp @host || Socket::IPAddress::LOOPBACK, @port @port = address.port puts "Listening on http://#{address}" - if host == "0.0.0.0" - puts "WARNING running playground with 0.0.0.0 is unsecure." + if address.unspecified? + puts "WARNING running playground on #{address.address} is insecure." end begin diff --git a/src/http/server.cr b/src/http/server.cr index 7be04efcc5a5..ca0ffbccc4e2 100644 --- a/src/http/server.cr +++ b/src/http/server.cr @@ -154,7 +154,7 @@ class HTTP::Server # If *reuse_port* is `true`, it enables the `SO_REUSEPORT` socket option, # which allows multiple processes to bind to the same port. def bind_tcp(port : Int32, reuse_port : Bool = false) : Socket::IPAddress - bind_tcp "127.0.0.1", port, reuse_port + bind_tcp Socket::IPAddress::LOOPBACK, port, reuse_port end # Creates a `TCPServer` listenting on *address* and adds it as a socket, returning the local address @@ -180,7 +180,7 @@ class HTTP::Server # server = HTTP::Server.new { } # server.bind_unused_port # => Socket::IPAddress.new("127.0.0.1", 12345) # ``` - def bind_unused_port(host : String = "127.0.0.1", reuse_port : Bool = false) : Socket::IPAddress + def bind_unused_port(host : String = Socket::IPAddress::LOOPBACK, reuse_port : Bool = false) : Socket::IPAddress bind_tcp host, 0, reuse_port end diff --git a/src/socket/address.cr b/src/socket/address.cr index 72f88edb9dc6..685c16e095cc 100644 --- a/src/socket/address.cr +++ b/src/socket/address.cr @@ -72,6 +72,13 @@ class Socket # resolve an IP, or don't know whether a `String` constains 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 @address : String? @@ -190,6 +197,43 @@ class Socket end end + # 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 + if addr = @addr4 + addr.s_addr & 0x00000000ff_u32 == 0x0000007f_u32 + elsif addr = @addr6 + 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, 1_u8] + else + raise "unreachable!" + 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 + if addr = @addr4 + addr.s_addr == 0_u32 + elsif addr = @addr6 + 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] + else + raise "unreachable!" + end + end + + private def ipv6_addr8(addr : LibC::In6Addr) + {% if flag?(:darwin) || flag?(:openbsd) || flag?(:freebsd) %} + addr.__u6_addr.__u6_addr8 + {% elsif flag?(:linux) && flag?(:musl) %} + addr.__in6_union.__s6_addr + {% elsif flag?(:linux) %} + addr.__in6_u.__u6_addr8 + {% else %} + {% raise "Unsupported platform" %} + {% end %} + end + def ==(other : IPAddress) family == other.family && port == other.port &&