diff --git a/spec/std/socket_spec.cr b/spec/std/socket_spec.cr index 0caa51af5b04..0d2169edd420 100644 --- a/spec/std/socket_spec.cr +++ b/spec/std/socket_spec.cr @@ -169,15 +169,67 @@ describe Socket::IPAddress do end describe Socket::UNIXAddress do + {% if flag?(:linux) %} + it "can be abstract on Linux" do + path = "/abstract.sock" + addr = Socket::UNIXAddress.new('@' + path) + addr.abstract?.should be_true + addr.path.should eq(path) + end + {% else %} + it "raises for abstract UNIX address on non-Linux" do + expect_raises(ArgumentError, "Unsupported") do + Socket::UNIXAddress.new("@/abstract.sock") + end + end + {% end %} + + it "is not abstract for a non-abstract address" do + path = "/tmp/unix.sock" + addr = Socket::UNIXAddress.new(path) + addr.abstract?.should be_false + addr.path.should eq(path) + end + + it "creates a non-abstract UNIX address starting with @" do + path = "./@non-abstract.sock" + addr = Socket::UNIXAddress.new(path) + addr.abstract?.should be_false + addr.path.should eq(path) + end + it "transforms into a C struct and back" do - addr1 = Socket::UNIXAddress.new("/tmp/service.sock") - addr2 = Socket::UNIXAddress.from(addr1.to_unsafe, addr1.size) + path = "/tmp/service.sock" + addr1 = Socket::UNIXAddress.new(path) + addr1.abstract?.should be_false + addr1.path.should eq(path) + + addr2 = Socket::UNIXAddress.from(addr1.to_unsafe, addr1.size) addr2.family.should eq(addr1.family) addr2.path.should eq(addr1.path) - addr2.to_s.should eq("/tmp/service.sock") + addr2.abstract?.should eq(addr1.abstract?) + addr2.to_s.should eq(path) end + {% if flag?(:linux) %} + it "transforms an abstract address into a C struct and back" do + path = "/abstract-service.sock" + + addr1 = Socket::UNIXAddress.new('@' + path) + addr1.path.should eq(path) + addr1.abstract?.should be_true + + sockaddr_un = addr1.to_unsafe.as(LibC::SockaddrUn*).value + sockaddr_un.sun_path[0].should eq(0_u8) + String.new(sockaddr_un.sun_path.to_unsafe + 1).should eq(path) + + addr2 = Socket::UNIXAddress.new(pointerof(sockaddr_un), nil) + addr2.path.should eq(addr1.path) + addr2.abstract?.should eq(addr1.abstract?) + end + {% end %} + it "raises when path is too long" do path = "/tmp/crystal-test-too-long-unix-socket-#{("a" * 2048)}.sock" expect_raises(ArgumentError, "Path size exceeds the maximum size") { Socket::UNIXAddress.new(path) } @@ -186,6 +238,12 @@ describe Socket::UNIXAddress do it "to_s" do Socket::UNIXAddress.new("some_path").to_s.should eq("some_path") end + + {% if flag?(:linux) %} + it "to_s for abstract UNIX address" do + Socket::UNIXAddress.new("@some_path").to_s.should eq("@some_path") + end + {% end %} end describe UNIXServer do @@ -195,6 +253,26 @@ describe UNIXServer do File.exists?(path).should be_false end + {% if flag?(:linux) %} + it "is not abstract when path is not an abstract address" do + path = "/tmp/crystal-test-unix-sock" + + UNIXServer.open(path) do |server| + server.abstract?.should be_false + end + end + {% end %} + + {% if flag?(:linux) %} + it "is abstract when path is an abstract address" do + path = "/tmp/crystal-test-unix-abstract-sock" + + UNIXServer.open('@' + path) do |server| + server.abstract?.should be_true + end + end + {% end %} + it "creates the socket file" do path = "/tmp/crystal-test-unix-sock" @@ -205,6 +283,34 @@ describe UNIXServer do File.exists?(path).should be_false end + it "creates the socket file with path starting with @" do + path = "@crystal-test-unix-sock-starting-with-at-symbol" + forced_non_abstract_path = "./#{path}" + + File.exists?(path).should be_false + + UNIXServer.open(forced_non_abstract_path) do + File.exists?(path).should be_true + end + + File.exists?(path).should be_false + end + + {% if flag?(:linux) %} + it "does not create any file for abstract server" do + path = "/tmp/crystal-test-unix-abstract-sock" + abstract_path = '@' + path + + File.exists?(path).should be_false + File.exists?(abstract_path).should be_false + + UNIXServer.open(abstract_path) do + File.exists?(path).should be_false + File.exists?(abstract_path).should be_false + end + end + {% end %} + it "deletes socket file on close" do path = "/tmp/crystal-test-unix-sock" @@ -217,6 +323,23 @@ describe UNIXServer do end end + {% if flag?(:linux) %} + it "does not delete any file on close for abstract server" do + path = "/tmp/crystal-test-close-unix-abstract-sock" + + File.touch(path) + File.exists?(path).should be_true + + begin + server = UNIXServer.new('@' + path) + server.close + File.exists?(path).should be_true + ensure + File.delete(path) if File.exists?(path) + end + end + {% end %} + it "raises when socket file already exists" do path = "/tmp/crystal-test-unix-sock" server = UNIXServer.new(path) @@ -231,7 +354,7 @@ describe UNIXServer do it "won't delete existing file on bind failure" do path = "/tmp/crystal-test-unix.sock" - File.write(path, "") + File.touch(path) File.exists?(path).should be_true begin @@ -247,15 +370,34 @@ describe UNIXServer do describe "accept" do it "returns the client UNIXSocket" do - UNIXServer.open("/tmp/crystal-test-unix-sock") do |server| - UNIXSocket.open("/tmp/crystal-test-unix-sock") do |_| + path = "/tmp/crystal-test-unix-sock" + + UNIXServer.open(path) do |server| + UNIXSocket.open(path) do |_| client = server.accept client.should be_a(UNIXSocket) + client.abstract?.should be_false client.close end end end + {% if flag?(:linux) %} + it "returns an abstract client UNIXSocket for abstract server" do + path = "/tmp/crystal-test-abstract-unix-sock" + abstract_path = '@' + path + + UNIXServer.open(abstract_path) do |server| + UNIXSocket.open(abstract_path) do |_| + client = server.accept + client.should be_a(UNIXSocket) + client.abstract?.should be_true + client.close + end + end + end + {% end %} + it "raises when server is closed" do server = UNIXServer.new("/tmp/crystal-test-unix-sock") exception = nil @@ -280,8 +422,10 @@ describe UNIXServer do describe "accept?" do it "returns the client UNIXSocket" do - UNIXServer.open("/tmp/crystal-test-unix-sock") do |server| - UNIXSocket.open("/tmp/crystal-test-unix-sock") do |_| + path = "/tmp/crystal-test-unix-sock" + + UNIXServer.open(path) do |server| + UNIXSocket.open(path) do |_| client = server.accept?.not_nil! client.should be_a(UNIXSocket) client.close @@ -339,6 +483,36 @@ describe UNIXSocket do end end + {% if flag?(:linux) %} + it "sends and receives messages over an abstract STREAM socket" do + path = "/abstract-service.sock" + abstract_path = '@' + path + + UNIXServer.open(abstract_path) do |server| + server.local_address.abstract?.should be_true + server.local_address.path.should eq(path) + + UNIXSocket.open(abstract_path) do |client| + client.local_address.abstract?.should be_true + client.local_address.path.should eq(path) + + server.accept do |sock| + sock.local_address.path.should eq("") + sock.local_address.abstract?.should be_true + + sock.remote_address.path.should eq("") + sock.remote_address.abstract?.should be_true + + client << "ping" + sock.gets(4).should eq("ping") + sock << "pong" + client.gets(4).should eq("pong") + end + end + end + end + {% end %} + it "sync flag after accept" do path = "/tmp/crystal-test-unix-sock" diff --git a/src/socket/address.cr b/src/socket/address.cr index 94e7b644a6f7..ecc7f0946356 100644 --- a/src/socket/address.cr +++ b/src/socket/address.cr @@ -179,20 +179,46 @@ class Socket # Holds the local path of an UNIX address, usually coming from an opened # connection (e.g. `Socket#local_address`, `Socket#receive`). # + # You may also declare an abstract UNIX address, that is a virtual file + # that will never be created on the filesystem. An abstract UNIX address + # path is prefixed by a `@` character. + # + # NOTE: + # - Abstract UNIX addresses are supported only on some Linux systems. + # - If you want to use a non-abstract UNIX address starting with a `@`, + # prefix it with `./`, like `./@foo`. + # # Example: # ``` # Socket::UNIXAddress.new("/tmp/my.sock") + # + # # Abstract UNIX socket on Linux only + # Socket::UNIXAddress.new("@/my.sock") # ``` struct UNIXAddress < Address getter path : String + getter? abstract : Bool # :nodoc: MAX_PATH_SIZE = LibC::SockaddrUn.new.sun_path.size - 1 - def initialize(@path : String) - if @path.bytesize + 1 > MAX_PATH_SIZE + def initialize(path : String) + if path.bytesize + 1 > MAX_PATH_SIZE raise ArgumentError.new("Path size exceeds the maximum size of #{MAX_PATH_SIZE} bytes") end + + if path.starts_with?('@') + {% if flag?(:linux) %} + @abstract = true + @path = path[1..-1] + {% else %} + raise ArgumentError.new("Unsupported: cannot use abstract UNIX socket on non-Linux") + {% end %} + else + @abstract = false + @path = path + end + @family = Family::UNIX @size = sizeof(LibC::SockaddrUn) end @@ -204,7 +230,20 @@ class Socket protected def initialize(sockaddr : LibC::SockaddrUn*, size) @family = Family::UNIX - @path = String.new(sockaddr.value.sun_path.to_unsafe) + + path = sockaddr.value.sun_path + if path[0]? == 0_u8 + {% if flag?(:linux) %} + @abstract = true + @path = String.new(path.to_unsafe + 1) + {% else %} + raise ArgumentError.new("Unsupported: cannot use abstract UNIX socket on non-Linux") + {% end %} + else + @abstract = false + @path = String.new(path.to_unsafe) + end + @size = size || sizeof(LibC::SockaddrUn) end @@ -213,13 +252,24 @@ class Socket end def to_s(io) + if abstract? + io << '@' + end io << path end def to_unsafe : LibC::Sockaddr* sockaddr = Pointer(LibC::SockaddrUn).malloc sockaddr.value.sun_family = family - sockaddr.value.sun_path.to_unsafe.copy_from(@path.to_unsafe, @path.bytesize + 1) + + destination = sockaddr.value.sun_path.to_slice + if @abstract + destination[0] = 0_u8 + destination += 1 + end + destination.copy_from(@path.to_unsafe, @path.bytesize) + destination[@path.bytesize] = 0_u8 + sockaddr.as(LibC::Sockaddr*) end end diff --git a/src/socket/unix_server.cr b/src/socket/unix_server.cr index ea8f758e4257..f33e6de716ce 100644 --- a/src/socket/unix_server.cr +++ b/src/socket/unix_server.cr @@ -24,17 +24,21 @@ class UNIXServer < UNIXSocket # Creates a named UNIX socket, listening on a filesystem pathname. # # Always deletes any existing filesystam pathname first, in order to cleanup - # any leftover socket file. + # + # NOTE: An abstract UNIX server act on virtual files, thus not creating nor deleting anything. # # The server is of stream type by default, but this can be changed for # another type. For example datagram messages: # ``` # UNIXServer.new("/tmp/dgram.sock", Socket::Type::DGRAM) # ``` - def initialize(@path : String, type : Type = Type::STREAM, backlog = 128) + def initialize(path : String, type : Type = Type::STREAM, backlog = 128) super(Family::UNIX, type) - bind(UNIXAddress.new(path)) do |error| + addr = UNIXAddress.new(path) + @abstract = addr.abstract? + @path = addr.path + bind(addr) do |error| close(delete: false) raise error end @@ -64,7 +68,7 @@ class UNIXServer < UNIXSocket # this method. def accept? : UNIXSocket? if client_fd = accept_impl - sock = UNIXSocket.new(client_fd, type) + sock = UNIXSocket.new(client_fd, type, @abstract) sock.sync = sync? sock end @@ -74,9 +78,9 @@ class UNIXServer < UNIXSocket def close(delete = true) super() ensure - if delete && (path = @path) + if !abstract? && delete && (path = @path) File.delete(path) if File.exists?(path) - @path = nil end + @path = nil end end diff --git a/src/socket/unix_socket.cr b/src/socket/unix_socket.cr index 777941efca6f..b7a47001886d 100644 --- a/src/socket/unix_socket.cr +++ b/src/socket/unix_socket.cr @@ -1,4 +1,4 @@ -# A local interprocess communication clientsocket. +# A local interprocess communication client socket. # # Only available on UNIX and UNIX-like operating systems. # @@ -13,22 +13,26 @@ # ``` class UNIXSocket < Socket getter path : String? + getter? abstract : Bool - # Connects a named UNIX socket, bound to a filesystem pathname. - def initialize(@path : String, type : Type = Type::STREAM) + # Connects a named UNIX socket, bound to a path. + def initialize(path : String, type : Type = Type::STREAM) super(Family::UNIX, type, Protocol::IP) - connect(UNIXAddress.new(path)) do |error| + addr = UNIXAddress.new(path) + @abstract = addr.abstract? + @path = addr.path + connect(addr) do |error| close raise error end end - protected def initialize(family : Family, type : Type) + protected def initialize(family : Family, type : Type, @abstract = false) super family, type, Protocol::IP end - protected def initialize(fd : Int32, type : Type) + protected def initialize(fd : Int32, type : Type, @abstract = false) super fd, Family::UNIX, type, Protocol::IP end @@ -89,11 +93,19 @@ class UNIXSocket < Socket end def local_address - UNIXAddress.new(path.to_s) + if abstract? + UNIXAddress.new('@' + @path.to_s) + else + UNIXAddress.new(@path.to_s) + end end def remote_address - UNIXAddress.new(path.to_s) + if abstract? + UNIXAddress.new('@' + @path.to_s) + else + UNIXAddress.new(@path.to_s) + end end def receive