io-endpoint 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +3 -0
- data/lib/io/endpoint/address_endpoint.rb +38 -0
- data/lib/io/endpoint/composite_endpoint.rb +42 -0
- data/lib/io/endpoint/generic.rb +124 -0
- data/lib/io/endpoint/host_endpoint.rb +93 -0
- data/lib/io/endpoint/shared_endpoint.rb +103 -0
- data/lib/io/endpoint/socket_endpoint.rb +43 -0
- data/lib/io/endpoint/ssl_endpoint.rb +123 -0
- data/lib/io/endpoint/unix_endpoint.rb +53 -0
- data/lib/io/endpoint/version.rb +10 -0
- data/lib/io/endpoint/wrapper.rb +143 -0
- data/lib/io/endpoint.rb +13 -0
- data/license.md +21 -0
- data/readme.md +23 -0
- data.tar.gz.sig +1 -0
- metadata +127 -0
- metadata.gz.sig +0 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c10e7e5b154e1ad990982315ba03565d5e880f392eb404a696c63eb3c9240cb7
|
4
|
+
data.tar.gz: 37ebf4a7b0c0d7905c0e4b9d23373e1665f595637ab049163dafab7d7c9df535
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f8446d2169e82a34b836edaaf76b4e13409202dc1f0cebf64c80f693728bfe4dcd133dddd2ee7fd932bbc13b45e3d37b8ab2a7b928b3a39629455cd1fd9e62bf
|
7
|
+
data.tar.gz: 0ca66dd6bffc9925e7e782d46234b61777d432df527b8595ed10d26655a2f4d30a0eb74e5cff54ae5ec31b21a3e94af79eb1609c546d9c2b552817937ef47adc
|
checksums.yaml.gz.sig
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2023, by Samuel Williams.
|
5
|
+
|
6
|
+
require 'socket'
|
7
|
+
|
8
|
+
require_relative 'generic'
|
9
|
+
require_relative 'wrapper'
|
10
|
+
|
11
|
+
module IO::Endpoint
|
12
|
+
class AddressEndpoint < Generic
|
13
|
+
def initialize(address, **options)
|
14
|
+
super(**options)
|
15
|
+
|
16
|
+
@address = address
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
"\#<#{self.class} #{@address.inspect}>"
|
21
|
+
end
|
22
|
+
|
23
|
+
attr :address
|
24
|
+
|
25
|
+
# Bind a socket to the given address. If a block is given, the socket will be automatically closed when the block exits.
|
26
|
+
# @yield [Socket] the bound socket
|
27
|
+
# @return [Socket] the bound socket
|
28
|
+
def bind(wrapper = Wrapper.default, &block)
|
29
|
+
wrapper.bind(@address, **@options, &block)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Connects a socket to the given address. If a block is given, the socket will be automatically closed when the block exits.
|
33
|
+
# @return [Socket] the connected socket
|
34
|
+
def connect(wrapper = Wrapper.default, &block)
|
35
|
+
wrapper.connect(@address, **@options, &block)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2023, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative 'generic'
|
7
|
+
|
8
|
+
module IO::Endpoint
|
9
|
+
class CompositeEndpoint < Generic
|
10
|
+
def initialize(endpoints, **options)
|
11
|
+
super(**options)
|
12
|
+
@endpoints = endpoints
|
13
|
+
end
|
14
|
+
|
15
|
+
def each(&block)
|
16
|
+
@endpoints.each(&block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def connect(&block)
|
20
|
+
last_error = nil
|
21
|
+
|
22
|
+
@endpoints.each do |endpoint|
|
23
|
+
begin
|
24
|
+
return endpoint.connect(&block)
|
25
|
+
rescue => last_error
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
raise last_error
|
30
|
+
end
|
31
|
+
|
32
|
+
def bind(&block)
|
33
|
+
@endpoints.map(&:bind)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Endpoint
|
38
|
+
def self.composite(*endpoints, **options)
|
39
|
+
CompositeEndpoint.new(endpoints, **options)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2023, by Samuel Williams.
|
5
|
+
|
6
|
+
# require_relative 'address'
|
7
|
+
require 'uri'
|
8
|
+
require 'socket'
|
9
|
+
|
10
|
+
module IO::Endpoint
|
11
|
+
Address = Addrinfo
|
12
|
+
|
13
|
+
# Endpoints represent a way of connecting or binding to an address.
|
14
|
+
class Generic
|
15
|
+
def initialize(**options)
|
16
|
+
@options = options.freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
def with(**options)
|
20
|
+
dup = self.dup
|
21
|
+
|
22
|
+
dup.options = @options.merge(options)
|
23
|
+
|
24
|
+
return dup
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_accessor :options
|
28
|
+
|
29
|
+
# @return [String] The hostname of the bound socket.
|
30
|
+
def hostname
|
31
|
+
@options[:hostname]
|
32
|
+
end
|
33
|
+
|
34
|
+
# If `SO_REUSEPORT` is enabled on a socket, the socket can be successfully bound even if there are existing sockets bound to the same address, as long as all prior bound sockets also had `SO_REUSEPORT` set before they were bound.
|
35
|
+
# @return [Boolean, nil] The value for `SO_REUSEPORT`.
|
36
|
+
def reuse_port?
|
37
|
+
@options[:reuse_port]
|
38
|
+
end
|
39
|
+
|
40
|
+
# If `SO_REUSEADDR` is enabled on a socket prior to binding it, the socket can be successfully bound unless there is a conflict with another socket bound to exactly the same combination of source address and port. Additionally, when set, binding a socket to the address of an existing socket in `TIME_WAIT` is not an error.
|
41
|
+
# @return [Boolean] The value for `SO_REUSEADDR`.
|
42
|
+
def reuse_address?
|
43
|
+
@options[:reuse_address]
|
44
|
+
end
|
45
|
+
|
46
|
+
# Controls SO_LINGER. The amount of time the socket will stay in the `TIME_WAIT` state after being closed.
|
47
|
+
# @return [Integer, nil] The value for SO_LINGER.
|
48
|
+
def linger
|
49
|
+
@options[:linger]
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [Numeric] The default timeout for socket operations.
|
53
|
+
def timeout
|
54
|
+
@options[:timeout]
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [Address] the address to bind to before connecting.
|
58
|
+
def local_address
|
59
|
+
@options[:local_address]
|
60
|
+
end
|
61
|
+
|
62
|
+
# Endpoints sometimes have multiple paths.
|
63
|
+
# @yield [Endpoint] Enumerate all discrete paths as endpoints.
|
64
|
+
def each
|
65
|
+
return to_enum unless block_given?
|
66
|
+
|
67
|
+
yield self
|
68
|
+
end
|
69
|
+
|
70
|
+
# Accept connections from the specified endpoint.
|
71
|
+
# @param backlog [Integer] the number of connections to listen for.
|
72
|
+
def accept(backlog: Socket::SOMAXCONN, &block)
|
73
|
+
bind do |server|
|
74
|
+
server.listen(backlog) if backlog
|
75
|
+
|
76
|
+
while true
|
77
|
+
socket, address = server.accept
|
78
|
+
|
79
|
+
Fiber.schedule do
|
80
|
+
yield accepted(socket), address
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Map all endpoints by invoking `#bind`.
|
87
|
+
# @yield the bound wrapper.
|
88
|
+
def bound
|
89
|
+
wrappers = []
|
90
|
+
|
91
|
+
self.each do |endpoint|
|
92
|
+
wrapper = endpoint.bind
|
93
|
+
wrappers << wrapper
|
94
|
+
|
95
|
+
yield wrapper
|
96
|
+
end
|
97
|
+
|
98
|
+
return wrappers
|
99
|
+
ensure
|
100
|
+
wrappers.each(&:close) if $!
|
101
|
+
end
|
102
|
+
|
103
|
+
# Create an Endpoint instance by URI scheme. The host and port of the URI will be passed to the Endpoint factory method, along with any options.
|
104
|
+
#
|
105
|
+
# @param string [String] URI as string. Scheme will decide implementation used.
|
106
|
+
# @param options keyword arguments passed through to {#initialize}
|
107
|
+
#
|
108
|
+
# @see Endpoint.ssl ssl - invoked when parsing a URL with the ssl scheme "ssl://127.0.0.1"
|
109
|
+
# @see Endpoint.tcp tcp - invoked when parsing a URL with the tcp scheme: "tcp://127.0.0.1"
|
110
|
+
# @see Endpoint.udp udp - invoked when parsing a URL with the udp scheme: "udp://127.0.0.1"
|
111
|
+
# @see Endpoint.unix unix - invoked when parsing a URL with the unix scheme: "unix://127.0.0.1"
|
112
|
+
def self.parse(string, **options)
|
113
|
+
uri = URI.parse(string)
|
114
|
+
|
115
|
+
self.public_send(uri.scheme, uri.host, uri.port, **options)
|
116
|
+
end
|
117
|
+
|
118
|
+
protected
|
119
|
+
|
120
|
+
def accepted(socket)
|
121
|
+
socket
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2023, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative 'address_endpoint'
|
7
|
+
|
8
|
+
module IO::Endpoint
|
9
|
+
class HostEndpoint < Generic
|
10
|
+
def initialize(specification, **options)
|
11
|
+
super(**options)
|
12
|
+
|
13
|
+
@specification = specification
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
nodename, service, family, socktype, protocol, flags = @specification
|
18
|
+
|
19
|
+
"\#<#{self.class} name=#{nodename.inspect} service=#{service.inspect} family=#{family.inspect} type=#{socktype.inspect} protocol=#{protocol.inspect} flags=#{flags.inspect}>"
|
20
|
+
end
|
21
|
+
|
22
|
+
def address
|
23
|
+
@specification
|
24
|
+
end
|
25
|
+
|
26
|
+
def hostname
|
27
|
+
@specification.first
|
28
|
+
end
|
29
|
+
|
30
|
+
# Try to connect to the given host by connecting to each address in sequence until a connection is made.
|
31
|
+
# @yield [Socket] the socket which is being connected, may be invoked more than once
|
32
|
+
# @return [Socket] the connected socket
|
33
|
+
# @raise if no connection could complete successfully
|
34
|
+
def connect
|
35
|
+
last_error = nil
|
36
|
+
|
37
|
+
Addrinfo.foreach(*@specification) do |address|
|
38
|
+
begin
|
39
|
+
socket = Socket.connect(address, **@options)
|
40
|
+
rescue Errno::ECONNREFUSED, Errno::ENETUNREACH, Errno::EAGAIN => last_error
|
41
|
+
else
|
42
|
+
return socket unless block_given?
|
43
|
+
|
44
|
+
begin
|
45
|
+
return yield(socket)
|
46
|
+
ensure
|
47
|
+
socket.close
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
raise last_error
|
53
|
+
end
|
54
|
+
|
55
|
+
# Invokes the given block for every address which can be bound to.
|
56
|
+
# @yield [Socket] the bound socket
|
57
|
+
# @return [Array<Socket>] an array of bound sockets
|
58
|
+
def bind(&block)
|
59
|
+
Addrinfo.foreach(*@specification).map do |address|
|
60
|
+
Socket.bind(address, **@options, &block)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# @yield [AddressEndpoint] address endpoints by resolving the given host specification
|
65
|
+
def each
|
66
|
+
return to_enum unless block_given?
|
67
|
+
|
68
|
+
Addrinfo.foreach(*@specification) do |address|
|
69
|
+
yield AddressEndpoint.new(address, **@options)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# @param args nodename, service, family, socktype, protocol, flags. `socktype` will be set to Socket::SOCK_STREAM.
|
75
|
+
# @param options keyword arguments passed on to {HostEndpoint#initialize}
|
76
|
+
#
|
77
|
+
# @return [HostEndpoint]
|
78
|
+
def self.tcp(*args, **options)
|
79
|
+
args[3] = ::Socket::SOCK_STREAM
|
80
|
+
|
81
|
+
HostEndpoint.new(args, **options)
|
82
|
+
end
|
83
|
+
|
84
|
+
# @param args nodename, service, family, socktype, protocol, flags. `socktype` will be set to Socket::SOCK_DGRAM.
|
85
|
+
# @param options keyword arguments passed on to {HostEndpoint#initialize}
|
86
|
+
#
|
87
|
+
# @return [HostEndpoint]
|
88
|
+
def self.udp(*args, **options)
|
89
|
+
args[3] = ::Socket::SOCK_DGRAM
|
90
|
+
|
91
|
+
HostEndpoint.new(args, **options)
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2023, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative 'generic'
|
7
|
+
require_relative 'composite_endpoint'
|
8
|
+
|
9
|
+
module IO::Endpoint
|
10
|
+
# Pre-connect and pre-bind sockets so that it can be used between processes.
|
11
|
+
class SharedEndpoint < Generic
|
12
|
+
# Create a new `SharedEndpoint` by binding to the given endpoint.
|
13
|
+
def self.bound(endpoint, backlog: Socket::SOMAXCONN, close_on_exec: false)
|
14
|
+
wrappers = endpoint.bound do |server|
|
15
|
+
# This is somewhat optional. We want to have a generic interface as much as possible so that users of this interface can just call it without knowing a lot of internal details. Therefore, we ignore errors here if it's because the underlying socket does not support the operation.
|
16
|
+
begin
|
17
|
+
server.listen(backlog)
|
18
|
+
rescue Errno::EOPNOTSUPP
|
19
|
+
# Ignore.
|
20
|
+
end
|
21
|
+
|
22
|
+
server.close_on_exec = close_on_exec
|
23
|
+
end
|
24
|
+
|
25
|
+
return self.new(endpoint, wrappers)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Create a new `SharedEndpoint` by connecting to the given endpoint.
|
29
|
+
def self.connected(endpoint, close_on_exec: false)
|
30
|
+
wrapper = endpoint.connect
|
31
|
+
|
32
|
+
wrapper.close_on_exec = close_on_exec
|
33
|
+
|
34
|
+
return self.new(endpoint, [wrapper])
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(endpoint, wrappers, **options)
|
38
|
+
super(**options)
|
39
|
+
|
40
|
+
@endpoint = endpoint
|
41
|
+
@wrappers = wrappers
|
42
|
+
end
|
43
|
+
|
44
|
+
attr :endpoint
|
45
|
+
attr :wrappers
|
46
|
+
|
47
|
+
def local_address_endpoint(**options)
|
48
|
+
endpoints = @wrappers.map do |wrapper|
|
49
|
+
AddressEndpoint.new(wrapper.to_io.local_address)
|
50
|
+
end
|
51
|
+
|
52
|
+
return CompositeEndpoint.new(endpoints, **options)
|
53
|
+
end
|
54
|
+
|
55
|
+
def remote_address_endpoint(**options)
|
56
|
+
endpoints = @wrappers.map do |wrapper|
|
57
|
+
AddressEndpoint.new(wrapper.to_io.remote_address)
|
58
|
+
end
|
59
|
+
|
60
|
+
return CompositeEndpoint.new(endpoints, **options)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Close all the internal wrappers.
|
64
|
+
def close
|
65
|
+
@wrappers.each(&:close)
|
66
|
+
@wrappers.clear
|
67
|
+
end
|
68
|
+
|
69
|
+
def bind
|
70
|
+
@wrappers.each do |server|
|
71
|
+
server = server.dup
|
72
|
+
|
73
|
+
begin
|
74
|
+
yield server
|
75
|
+
ensure
|
76
|
+
server.close
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def connect
|
82
|
+
@wrappers.each do |peer|
|
83
|
+
peer = peer.dup
|
84
|
+
|
85
|
+
begin
|
86
|
+
yield peer
|
87
|
+
ensure
|
88
|
+
peer.close
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def accept(backlog = nil, &block)
|
94
|
+
bind do |server|
|
95
|
+
server.accept_each(&block)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def to_s
|
100
|
+
"\#<#{self.class} #{@wrappers.size} descriptors for #{@endpoint}>"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2023, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative 'generic'
|
7
|
+
|
8
|
+
module IO::Endpoint
|
9
|
+
# This class doesn't exert ownership over the specified socket, wraps a native ::IO.
|
10
|
+
class SocketEndpoint < Generic
|
11
|
+
def initialize(socket, **options)
|
12
|
+
super(**options)
|
13
|
+
|
14
|
+
@socket = socket
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
"\#<#{self.class} #{@socket.inspect}>"
|
19
|
+
end
|
20
|
+
|
21
|
+
attr :socket
|
22
|
+
|
23
|
+
def bind(&block)
|
24
|
+
if block_given?
|
25
|
+
yield @socket
|
26
|
+
else
|
27
|
+
return @socket
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def connect(&block)
|
32
|
+
if block_given?
|
33
|
+
yield @socket
|
34
|
+
else
|
35
|
+
return @socket
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.socket(socket, **options)
|
41
|
+
SocketEndpoint.new(socket, **options)
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2023, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative 'host_endpoint'
|
7
|
+
require_relative 'generic'
|
8
|
+
|
9
|
+
require 'openssl'
|
10
|
+
|
11
|
+
module IO::Endpoint
|
12
|
+
class SSLEndpoint < Generic
|
13
|
+
def initialize(endpoint, **options)
|
14
|
+
super(**options)
|
15
|
+
|
16
|
+
@endpoint = endpoint
|
17
|
+
|
18
|
+
if ssl_context = options[:ssl_context]
|
19
|
+
@context = build_context(ssl_context)
|
20
|
+
else
|
21
|
+
@context = nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
"\#<#{self.class} #{@endpoint}>"
|
27
|
+
end
|
28
|
+
|
29
|
+
def address
|
30
|
+
@endpoint.address
|
31
|
+
end
|
32
|
+
|
33
|
+
def hostname
|
34
|
+
@options[:hostname] || @endpoint.hostname
|
35
|
+
end
|
36
|
+
|
37
|
+
attr :endpoint
|
38
|
+
attr :options
|
39
|
+
|
40
|
+
def params
|
41
|
+
@options[:ssl_params]
|
42
|
+
end
|
43
|
+
|
44
|
+
def build_context(context = OpenSSL::SSL::SSLContext.new)
|
45
|
+
if params = self.params
|
46
|
+
context.set_params(params)
|
47
|
+
end
|
48
|
+
|
49
|
+
context.setup
|
50
|
+
context.freeze
|
51
|
+
|
52
|
+
return context
|
53
|
+
end
|
54
|
+
|
55
|
+
def context
|
56
|
+
@context ||= build_context
|
57
|
+
end
|
58
|
+
|
59
|
+
# Connect to the underlying endpoint and establish a SSL connection.
|
60
|
+
# @yield [Socket] the socket which is being connected
|
61
|
+
# @return [Socket] the connected socket
|
62
|
+
def bind
|
63
|
+
if block_given?
|
64
|
+
@endpoint.bind do |server|
|
65
|
+
yield OpenSSL::SSL::SSLServer.new(server, context)
|
66
|
+
end
|
67
|
+
else
|
68
|
+
return OpenSSL::SSL::SSLServer.new(@endpoint.bind, context)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Connect to the underlying endpoint and establish a SSL connection.
|
73
|
+
# @yield [Socket] the socket which is being connected
|
74
|
+
# @return [Socket] the connected socket
|
75
|
+
def connect(&block)
|
76
|
+
socket = OpenSSL::SSL::SSLSocket.new(@endpoint.connect, context)
|
77
|
+
|
78
|
+
if hostname = self.hostname
|
79
|
+
socket.hostname = hostname
|
80
|
+
end
|
81
|
+
|
82
|
+
begin
|
83
|
+
socket.connect
|
84
|
+
rescue
|
85
|
+
socket.close
|
86
|
+
raise
|
87
|
+
end
|
88
|
+
|
89
|
+
return socket unless block_given?
|
90
|
+
|
91
|
+
begin
|
92
|
+
yield socket
|
93
|
+
ensure
|
94
|
+
socket.close
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def each
|
99
|
+
return to_enum unless block_given?
|
100
|
+
|
101
|
+
@endpoint.each do |endpoint|
|
102
|
+
yield self.class.new(endpoint, **@options)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
protected
|
107
|
+
|
108
|
+
def accepted(socket)
|
109
|
+
socket.accept
|
110
|
+
socket
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# @param args
|
115
|
+
# @param ssl_context [OpenSSL::SSL::SSLContext, nil]
|
116
|
+
# @param hostname [String, nil]
|
117
|
+
# @param options keyword arguments passed through to {Endpoint.tcp}
|
118
|
+
#
|
119
|
+
# @return [SSLEndpoint]
|
120
|
+
def self.ssl(*args, ssl_context: nil, hostname: nil, **options)
|
121
|
+
SSLEndpoint.new(self.tcp(*args, **options), ssl_context: ssl_context, hostname: hostname)
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2023, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative 'address_endpoint'
|
7
|
+
|
8
|
+
module IO::Endpoint
|
9
|
+
# This class doesn't exert ownership over the specified unix socket and ensures exclusive access by using `flock` where possible.
|
10
|
+
class UNIXEndpoint < AddressEndpoint
|
11
|
+
def initialize(path, type = Socket::SOCK_STREAM, **options)
|
12
|
+
# I wonder if we should implement chdir behaviour in here if path is longer than 104 characters.
|
13
|
+
super(Address.unix(path, type), **options)
|
14
|
+
|
15
|
+
@path = path
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
"\#<#{self.class} #{@path.inspect}>"
|
20
|
+
end
|
21
|
+
|
22
|
+
attr :path
|
23
|
+
|
24
|
+
def bound?
|
25
|
+
self.connect do
|
26
|
+
return true
|
27
|
+
end
|
28
|
+
rescue Errno::ECONNREFUSED
|
29
|
+
return false
|
30
|
+
end
|
31
|
+
|
32
|
+
def bind(&block)
|
33
|
+
super
|
34
|
+
rescue Errno::EADDRINUSE
|
35
|
+
# If you encounter EADDRINUSE from `bind()`, you can check if the socket is actually accepting connections by attempting to `connect()` to it. If the socket is still bound by an active process, the connection will succeed. Otherwise, it should be safe to `unlink()` the path and try again.
|
36
|
+
if !bound? && File.exist?(@path)
|
37
|
+
File.unlink(@path)
|
38
|
+
retry
|
39
|
+
else
|
40
|
+
raise
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# @param path [String]
|
46
|
+
# @param type Socket type
|
47
|
+
# @param options keyword arguments passed through to {UNIXEndpoint#initialize}
|
48
|
+
#
|
49
|
+
# @return [UNIXEndpoint]
|
50
|
+
def self.unix(path = "", type = ::Socket::SOCK_STREAM, **options)
|
51
|
+
UNIXEndpoint.new(path, type, **options)
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2023, by Samuel Williams.
|
5
|
+
|
6
|
+
require 'socket'
|
7
|
+
|
8
|
+
module IO::Endpoint
|
9
|
+
class Wrapper
|
10
|
+
include ::Socket::Constants
|
11
|
+
|
12
|
+
if $stdin.respond_to?(:timeout=)
|
13
|
+
def self.set_timeout(io, timeout)
|
14
|
+
io.timeout = timeout
|
15
|
+
end
|
16
|
+
else
|
17
|
+
def self.set_timeout(io, timeout)
|
18
|
+
warn "IO#timeout= not supported on this platform."
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def async
|
23
|
+
raise NotImplementedError
|
24
|
+
end
|
25
|
+
|
26
|
+
# Build and wrap the underlying io.
|
27
|
+
# @option reuse_port [Boolean] Allow this port to be bound in multiple processes.
|
28
|
+
# @option reuse_address [Boolean] Allow this port to be bound in multiple processes.
|
29
|
+
def build(*arguments, timeout: nil, reuse_address: true, reuse_port: nil, linger: nil)
|
30
|
+
socket = ::Socket.new(*arguments)
|
31
|
+
|
32
|
+
# Set the timeout:
|
33
|
+
if timeout
|
34
|
+
set_timeout(socket, timeout)
|
35
|
+
end
|
36
|
+
|
37
|
+
if reuse_address
|
38
|
+
socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
39
|
+
end
|
40
|
+
|
41
|
+
if reuse_port
|
42
|
+
socket.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1)
|
43
|
+
end
|
44
|
+
|
45
|
+
if linger
|
46
|
+
socket.setsockopt(SOL_SOCKET, SO_LINGER, linger)
|
47
|
+
end
|
48
|
+
|
49
|
+
yield socket if block_given?
|
50
|
+
|
51
|
+
return socket
|
52
|
+
rescue
|
53
|
+
socket&.close
|
54
|
+
end
|
55
|
+
|
56
|
+
# Establish a connection to a given `remote_address`.
|
57
|
+
# @example
|
58
|
+
# socket = Async::IO::Socket.connect(Async::IO::Address.tcp("8.8.8.8", 53))
|
59
|
+
# @param remote_address [Address] The remote address to connect to.
|
60
|
+
# @option local_address [Address] The local address to bind to before connecting.
|
61
|
+
def connect(remote_address, local_address: nil, **options)
|
62
|
+
socket = build(remote_address.afamily, remote_address.socktype, remote_address.protocol, **options) do |socket|
|
63
|
+
if local_address
|
64
|
+
if defined?(IP_BIND_ADDRESS_NO_PORT)
|
65
|
+
# Inform the kernel (Linux 4.2+) to not reserve an ephemeral port when using bind(2) with a port number of 0. The port will later be automatically chosen at connect(2) time, in a way that allows sharing a source port as long as the 4-tuple is unique.
|
66
|
+
socket.setsockopt(SOL_IP, IP_BIND_ADDRESS_NO_PORT, 1)
|
67
|
+
end
|
68
|
+
|
69
|
+
socket.bind(local_address.to_sockaddr)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
begin
|
74
|
+
socket.connect(remote_address.to_sockaddr)
|
75
|
+
rescue Exception
|
76
|
+
socket.close
|
77
|
+
raise
|
78
|
+
end
|
79
|
+
|
80
|
+
return socket unless block_given?
|
81
|
+
|
82
|
+
begin
|
83
|
+
yield socket
|
84
|
+
ensure
|
85
|
+
socket.close
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Bind to a local address.
|
90
|
+
# @example
|
91
|
+
# socket = Async::IO::Socket.bind(Async::IO::Address.tcp("0.0.0.0", 9090))
|
92
|
+
# @param local_address [Address] The local address to bind to.
|
93
|
+
# @option protocol [Integer] The socket protocol to use.
|
94
|
+
def bind(local_address, protocol: 0, **options, &block)
|
95
|
+
socket = build(local_address.afamily, local_address.socktype, protocol, **options) do |socket|
|
96
|
+
socket.bind(local_address.to_sockaddr)
|
97
|
+
end
|
98
|
+
|
99
|
+
return socket unless block_given?
|
100
|
+
|
101
|
+
async do
|
102
|
+
begin
|
103
|
+
yield socket
|
104
|
+
ensure
|
105
|
+
socket.close
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Bind to a local address and accept connections in a loop.
|
111
|
+
def accept(*arguments, backlog: SOMAXCONN, **options, &block)
|
112
|
+
bind(*arguments, **options) do |server|
|
113
|
+
server.listen(backlog) if backlog
|
114
|
+
|
115
|
+
async do
|
116
|
+
while true
|
117
|
+
server.accept(&block)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
class ThreadWrapper < Wrapper
|
125
|
+
def async(&block)
|
126
|
+
Thread.new(&block)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class FiberWrapper < Wrapper
|
131
|
+
def async(&block)
|
132
|
+
Fiber.schedule(&block)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def Wrapper.default
|
137
|
+
if Fiber.scheduler
|
138
|
+
FiberWrapper.new
|
139
|
+
else
|
140
|
+
ThreadWrapper.new
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
data/lib/io/endpoint.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2023, by Samuel Williams.
|
5
|
+
|
6
|
+
require_relative "endpoint/version"
|
7
|
+
require_relative "endpoint/generic"
|
8
|
+
|
9
|
+
module IO::Endpoint
|
10
|
+
def self.file_descriptor_limit
|
11
|
+
Process.getrlimit(Process::RLIMIT_NOFILE).first
|
12
|
+
end
|
13
|
+
end
|
data/license.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# MIT License
|
2
|
+
|
3
|
+
Copyright, 2023, by Samuel Williams.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/readme.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# IO::Endpoint
|
2
|
+
|
3
|
+
Provides a separation of concerns interface for IO endpoints. This allows you to write code which is agnostic to the underlying IO implementation.
|
4
|
+
|
5
|
+
[![Development Status](https://github.com/socketry/io-endpoint/workflows/Test/badge.svg)](https://github.com/socketry/io-endpoint/actions?workflow=Test)
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
Please see the [documentation](https://socketry.github.io/io-endpoint) for more information.
|
10
|
+
|
11
|
+
## Contributing
|
12
|
+
|
13
|
+
We welcome contributions to this project.
|
14
|
+
|
15
|
+
1. Fork it.
|
16
|
+
2. Create your feature branch (`git checkout -b my-new-feature`).
|
17
|
+
3. Commit your changes (`git commit -am 'Add some feature'`).
|
18
|
+
4. Push to the branch (`git push origin my-new-feature`).
|
19
|
+
5. Create new Pull Request.
|
20
|
+
|
21
|
+
## See Also
|
22
|
+
|
23
|
+
- [async-io](https://github.com/socketry/async-io) — Asynchronous IO primitives.
|
data.tar.gz.sig
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
���m,�ԡ,��!FWp�$2@�A>���(�����d:y��h�7D��?�Wm�F'���}.uQ�}�ٛ����P��+[�o�1�ˏh2Z�Ճj�;*���,�X�$|�]�T6b�q����VC�m �u����h�u�KeL����yi��\�O����p$,��
|
metadata
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: io-endpoint
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Samuel Williams
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain:
|
11
|
+
- |
|
12
|
+
-----BEGIN CERTIFICATE-----
|
13
|
+
MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
|
14
|
+
ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
|
15
|
+
CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
|
16
|
+
MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
|
17
|
+
MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
|
18
|
+
bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
|
19
|
+
igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
|
20
|
+
9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
|
21
|
+
sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
|
22
|
+
e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
|
23
|
+
XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
|
24
|
+
RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
|
25
|
+
tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
|
26
|
+
zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
|
27
|
+
xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
|
28
|
+
BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
|
29
|
+
aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
|
30
|
+
aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
|
31
|
+
cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl
|
32
|
+
xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/
|
33
|
+
c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp
|
34
|
+
8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws
|
35
|
+
JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
|
36
|
+
eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
|
37
|
+
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
38
|
+
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
39
|
+
-----END CERTIFICATE-----
|
40
|
+
date: 2023-06-15 00:00:00.000000000 Z
|
41
|
+
dependencies:
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: bake
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: covered
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: sus
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
description:
|
85
|
+
email:
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- lib/io/endpoint.rb
|
91
|
+
- lib/io/endpoint/address_endpoint.rb
|
92
|
+
- lib/io/endpoint/composite_endpoint.rb
|
93
|
+
- lib/io/endpoint/generic.rb
|
94
|
+
- lib/io/endpoint/host_endpoint.rb
|
95
|
+
- lib/io/endpoint/shared_endpoint.rb
|
96
|
+
- lib/io/endpoint/socket_endpoint.rb
|
97
|
+
- lib/io/endpoint/ssl_endpoint.rb
|
98
|
+
- lib/io/endpoint/unix_endpoint.rb
|
99
|
+
- lib/io/endpoint/version.rb
|
100
|
+
- lib/io/endpoint/wrapper.rb
|
101
|
+
- license.md
|
102
|
+
- readme.md
|
103
|
+
homepage: https://github.com/socketry/io-endpoint
|
104
|
+
licenses:
|
105
|
+
- MIT
|
106
|
+
metadata:
|
107
|
+
documentation_uri: https://socketry.github.io/io-endpoint
|
108
|
+
post_install_message:
|
109
|
+
rdoc_options: []
|
110
|
+
require_paths:
|
111
|
+
- lib
|
112
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '3.0'
|
117
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
requirements: []
|
123
|
+
rubygems_version: 3.4.7
|
124
|
+
signing_key:
|
125
|
+
specification_version: 4
|
126
|
+
summary: Provides a separation of concerns interface for IO endpoints.
|
127
|
+
test_files: []
|
metadata.gz.sig
ADDED
Binary file
|