Skip to content

Commit

Permalink
Add abstract UNIX socket handling
Browse files Browse the repository at this point in the history
  • Loading branch information
bew committed Oct 24, 2017
1 parent 007899a commit e4de6e2
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 23 deletions.
87 changes: 86 additions & 1 deletion spec/std/socket_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -175,16 +175,35 @@ describe Socket::UNIXAddress do

addr2.family.should eq(addr1.family)
addr2.path.should eq(addr1.path)
addr2.abstract?.should eq(addr1.abstract?)
addr2.to_s.should eq("/tmp/service.sock")
end

it "transforms an abstract address into a C struct and back" do
path = "/abstract-service.sock"

addr1 = Socket::UNIXAddress.new(path, abstract: true)
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(path)
addr2.abstract?.should be_true
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) }
end

it "to_s" do
Socket::UNIXAddress.new("some_path").to_s.should eq("some_path")
path = "some_path"
Socket::UNIXAddress.new(path).to_s.should eq(path)
Socket::UNIXAddress.new(path, abstract: true).to_s.should eq(path)
end
end

Expand All @@ -205,6 +224,17 @@ describe UNIXServer do
File.exists?(path).should be_false
end

it "does not create any file for abstract server" do
path = "/tmp/crystal-test-unix-abstract-sock"

File.exists?(path).should be_false

UNIXServer.open(path, abstract: true) do |server|
server.abstract?.should be_true
File.exists?(path).should be_false
end
end

it "deletes socket file on close" do
path = "/tmp/crystal-test-unix-sock"

Expand All @@ -217,6 +247,21 @@ describe UNIXServer do
end
end

it "does not delete any file on close for abstract server" do
path = "/tmp/crystal-test-close-unix-abstract-sock"

File.write(path, "")
File.exists?(path).should be_true

begin
server = UNIXServer.new(path, abstract: true)
server.close
File.exists?(path).should be_true
ensure
File.delete(path) if File.exists?(path)
end
end

it "raises when socket file already exists" do
path = "/tmp/crystal-test-unix-sock"
server = UNIXServer.new(path)
Expand Down Expand Up @@ -256,6 +301,19 @@ describe UNIXServer do
end
end

it "returns an abstract client UNIXSocket for abstract server" do
path = "/tmp/crystal-test-abstract-unix-sock"

UNIXServer.open(path, abstract: true) do |server|
UNIXSocket.open(path, abstract: true) do |_|
client = server.accept
client.should be_a(UNIXSocket)
client.abstract?.should be_true
client.close
end
end
end

it "raises when server is closed" do
server = UNIXServer.new("/tmp/crystal-test-unix-sock")
exception = nil
Expand Down Expand Up @@ -339,6 +397,33 @@ describe UNIXSocket do
end
end

it "sends and receives messages over an abstract STREAM socket" do
path = "/abstract-service.sock"

UNIXServer.open(path, abstract: true) do |server|
server.local_address.abstract?.should be_true
server.local_address.path.should eq(path)

UNIXSocket.open(path, abstract: true) 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

it "sync flag after accept" do
path = "/tmp/crystal-test-unix-sock"

Expand Down
26 changes: 23 additions & 3 deletions src/socket/address.cr
Original file line number Diff line number Diff line change
Expand Up @@ -179,17 +179,21 @@ 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.
#
# Example:
# ```
# Socket::UNIXAddress.new("/tmp/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)
def initialize(@path : String, @abstract = false)
if @path.bytesize + 1 > MAX_PATH_SIZE
raise ArgumentError.new("Path size exceeds the maximum size of #{MAX_PATH_SIZE} bytes")
end
Expand All @@ -204,7 +208,16 @@ 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
@abstract = true
@path = String.new(path.to_unsafe + 1)
else
@abstract = false
@path = String.new(path.to_unsafe)
end

@size = size || sizeof(LibC::SockaddrUn)
end

Expand All @@ -219,7 +232,14 @@ class Socket
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_unsafe
if @abstract
destination[0] = 0_u8
destination += 1
end
destination.copy_from(@path.to_unsafe, @path.bytesize + 1)

sockaddr.as(LibC::Sockaddr*)
end
end
Expand Down
19 changes: 10 additions & 9 deletions src/socket/unix_server.cr
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,18 @@ 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)
super(Family::UNIX, type)
def initialize(@path : String, type : Type = Type::STREAM, backlog = 128, abstract is_abstract = false)
super(Family::UNIX, type, is_abstract)

bind(UNIXAddress.new(path)) do |error|
bind(UNIXAddress.new(path, is_abstract)) do |error|
close(delete: false)
raise error
end
Expand All @@ -43,8 +44,8 @@ class UNIXServer < UNIXSocket
# server socket when the block returns.
#
# Returns the value of the block.
def self.open(path, type : Type = Type::STREAM, backlog = 128)
server = new(path, type, backlog)
def self.open(path, type : Type = Type::STREAM, backlog = 128, abstract is_abstract = false)
server = new(path, type, backlog, is_abstract)
begin
yield server
ensure
Expand All @@ -58,7 +59,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
Expand All @@ -68,9 +69,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
21 changes: 11 additions & 10 deletions src/socket/unix_socket.cr
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# A local interprocess communication clientsocket.
# A local interprocess communication client socket.
#
# Only available on UNIX and UNIX-like operating systems.
#
Expand All @@ -13,31 +13,32 @@
# ```
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 filesystem.
def initialize(@path : String, type : Type = Type::STREAM, @abstract = false)
super(Family::UNIX, type, Protocol::IP)

connect(UNIXAddress.new(path)) do |error|
connect(UNIXAddress.new(path, @abstract)) 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

# Opens an UNIX socket to a filesystem pathname, yields it to the block, then
# eventually closes the socket when the block returns.
#
# Returns the value of the block.
def self.open(path, type : Type = Type::STREAM)
sock = new(path, type)
def self.open(path, type : Type = Type::STREAM, abstract is_abstract = false)
sock = new(path, type, is_abstract)
begin
yield sock
ensure
Expand Down Expand Up @@ -89,11 +90,11 @@ class UNIXSocket < Socket
end

def local_address
UNIXAddress.new(path.to_s)
UNIXAddress.new(path.to_s, @abstract)
end

def remote_address
UNIXAddress.new(path.to_s)
UNIXAddress.new(path.to_s, @abstract)
end

def receive
Expand Down

0 comments on commit e4de6e2

Please sign in to comment.