io-endpoint 0.15.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/getting-started.md +113 -0
- data/context/index.yaml +12 -0
- data/lib/io/endpoint/address_endpoint.rb +13 -4
- data/lib/io/endpoint/bound_endpoint.rb +25 -0
- data/lib/io/endpoint/composite_endpoint.rb +29 -0
- data/lib/io/endpoint/connected_endpoint.rb +25 -1
- data/lib/io/endpoint/generic.rb +19 -13
- data/lib/io/endpoint/host_endpoint.rb +31 -14
- data/lib/io/endpoint/shared_endpoint.rb +1 -1
- data/lib/io/endpoint/socket_endpoint.rb +21 -1
- data/lib/io/endpoint/ssl_endpoint.rb +69 -12
- data/lib/io/endpoint/unix_endpoint.rb +22 -6
- data/lib/io/endpoint/version.rb +4 -2
- data/lib/io/endpoint/wrapper.rb +17 -0
- data/lib/io/endpoint.rb +3 -0
- data/readme.md +3 -1
- data.tar.gz.sig +0 -0
- metadata +6 -4
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85b2a321334cd16a75da466ebff7b9f380410788f173936e9bffd5482599e2ad
|
|
4
|
+
data.tar.gz: f207fe739fe89220f59037ec54b1020795509598d4f0deb7a9caaf2cb36e8ea5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7154bdc644a2e8f2689567b9804d4019c263f07dd28f44ec98084ed4c3459ea1521724dcf0d4b90d2be9928a9eb4bbcb58895e18e10d461109f91dbdc6cb45ea
|
|
7
|
+
data.tar.gz: d8f01abe1ee5d1c93184505f03c2ec4226b6b4eed20754d413b8dcdfa5d5e339d3169069a420fac31b26eca2e157548ad7479e51f8457a14c8f3db5d1efe9e78
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
This guide explains how to get started with `io-endpoint`, a library that provides a separation of concerns interface for network I/O endpoints.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add the gem to your project:
|
|
8
|
+
|
|
9
|
+
~~~ bash
|
|
10
|
+
$ bundle add io-endpoint
|
|
11
|
+
~~~
|
|
12
|
+
|
|
13
|
+
## Core Concepts
|
|
14
|
+
|
|
15
|
+
`io-endpoint` provides a unified interface for working with network endpoints, allowing you to write code that is agnostic to the underlying transport mechanism (TCP, UDP, UNIX sockets, SSL/TLS). This separation of concerns makes it easier to:
|
|
16
|
+
|
|
17
|
+
- **Write transport-agnostic code**: Your application logic doesn't need to know whether it's using TCP, UDP, or UNIX sockets.
|
|
18
|
+
- **Test with different transports**: Easily swap between transports during testing.
|
|
19
|
+
- **Handle multiple addresses**: Automatically handle IPv4 and IPv6 addresses.
|
|
20
|
+
- **Compose endpoints**: Combine multiple endpoints for failover or load distribution.
|
|
21
|
+
|
|
22
|
+
The library centers around the {ruby IO::Endpoint::Generic} class, which represents a network endpoint that can be bound (for servers) or connected to (for clients). Different endpoint types handle different scenarios:
|
|
23
|
+
|
|
24
|
+
- {ruby IO::Endpoint::HostEndpoint} - Resolves hostnames to addresses (e.g., "localhost:8080")
|
|
25
|
+
- {ruby IO::Endpoint::AddressEndpoint} - Works with specific network addresses
|
|
26
|
+
- {ruby IO::Endpoint::UNIXEndpoint} - Handles UNIX domain sockets
|
|
27
|
+
- {ruby IO::Endpoint::SSLEndpoint} - Wraps endpoints with SSL/TLS encryption
|
|
28
|
+
- {ruby IO::Endpoint::CompositeEndpoint} - Combines multiple endpoints
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
### Creating a TCP Server
|
|
33
|
+
|
|
34
|
+
When you need to create a server that listens on a specific port, you can use {ruby IO::Endpoint.tcp} to create a TCP endpoint:
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
require "io/endpoint"
|
|
38
|
+
|
|
39
|
+
# Create a TCP endpoint listening on localhost port 8080:
|
|
40
|
+
endpoint = IO::Endpoint.tcp("localhost", 8080)
|
|
41
|
+
|
|
42
|
+
# Bind to the endpoint and accept connections:
|
|
43
|
+
endpoint.bind do |server|
|
|
44
|
+
# The server socket is automatically closed when the block exits
|
|
45
|
+
server.listen(10)
|
|
46
|
+
|
|
47
|
+
loop do
|
|
48
|
+
client, address = server.accept
|
|
49
|
+
# Handle the client connection
|
|
50
|
+
client.close
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Creating a TCP Client
|
|
56
|
+
|
|
57
|
+
To connect to a remote server, use the `connect` method:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
require "io/endpoint"
|
|
61
|
+
|
|
62
|
+
# Create a TCP endpoint for the remote server:
|
|
63
|
+
endpoint = IO::Endpoint.tcp("example.com", 80)
|
|
64
|
+
|
|
65
|
+
# Connect to the server:
|
|
66
|
+
endpoint.connect do |socket|
|
|
67
|
+
# The socket is automatically closed when the block exits
|
|
68
|
+
socket.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
|
|
69
|
+
response = socket.read
|
|
70
|
+
puts response
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Using UNIX Domain Sockets
|
|
75
|
+
|
|
76
|
+
For inter-process communication on the same machine, UNIX domain sockets provide better performance than TCP:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
require "io/endpoint"
|
|
80
|
+
|
|
81
|
+
# Create a UNIX socket endpoint:
|
|
82
|
+
endpoint = IO::Endpoint.unix("/tmp/myapp.sock")
|
|
83
|
+
|
|
84
|
+
# Bind to the socket:
|
|
85
|
+
endpoint.bind do |server|
|
|
86
|
+
server.listen(10)
|
|
87
|
+
|
|
88
|
+
loop do
|
|
89
|
+
client, address = server.accept
|
|
90
|
+
# Handle the client connection
|
|
91
|
+
client.close
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Using SSL/TLS
|
|
97
|
+
|
|
98
|
+
To add encryption to your connections, wrap a TCP endpoint with SSL:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
require "io/endpoint"
|
|
102
|
+
|
|
103
|
+
# Create an SSL endpoint:
|
|
104
|
+
endpoint = IO::Endpoint.ssl("example.com", 443, hostname: "example.com")
|
|
105
|
+
|
|
106
|
+
# Connect with SSL encryption:
|
|
107
|
+
endpoint.connect do |socket|
|
|
108
|
+
# The socket is automatically encrypted
|
|
109
|
+
socket.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
|
|
110
|
+
response = socket.read
|
|
111
|
+
puts response
|
|
112
|
+
end
|
|
113
|
+
```
|
data/context/index.yaml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Automatically generated context index for Utopia::Project guides.
|
|
2
|
+
# Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
|
|
3
|
+
---
|
|
4
|
+
description: Provides a separation of concerns interface for IO endpoints.
|
|
5
|
+
metadata:
|
|
6
|
+
documentation_uri: https://socketry.github.io/io-endpoint
|
|
7
|
+
source_code_uri: https://github.com/socketry/io-endpoint.git
|
|
8
|
+
files:
|
|
9
|
+
- path: getting-started.md
|
|
10
|
+
title: Getting Started
|
|
11
|
+
description: This guide explains how to get started with `io-endpoint`, a library
|
|
12
|
+
that provides a separation of concerns interface for network I/O endpoints.
|
|
@@ -9,13 +9,19 @@ require_relative "generic"
|
|
|
9
9
|
require_relative "wrapper"
|
|
10
10
|
|
|
11
11
|
module IO::Endpoint
|
|
12
|
+
# Represents an endpoint for a specific network address.
|
|
12
13
|
class AddressEndpoint < Generic
|
|
14
|
+
# Initialize a new address endpoint.
|
|
15
|
+
# @parameter address [Address] The network address for this endpoint.
|
|
16
|
+
# @parameter options [Hash] Additional options to pass to the parent class.
|
|
13
17
|
def initialize(address, **options)
|
|
14
18
|
super(**options)
|
|
15
19
|
|
|
16
20
|
@address = address
|
|
17
21
|
end
|
|
18
22
|
|
|
23
|
+
# Get a string representation of the endpoint.
|
|
24
|
+
# @returns [String] A string representation of the endpoint address.
|
|
19
25
|
def to_s
|
|
20
26
|
case @address.afamily
|
|
21
27
|
when Socket::AF_INET
|
|
@@ -27,22 +33,25 @@ module IO::Endpoint
|
|
|
27
33
|
end
|
|
28
34
|
end
|
|
29
35
|
|
|
36
|
+
# Get a detailed string representation of the endpoint.
|
|
37
|
+
# @returns [String] A detailed string representation including the address.
|
|
30
38
|
def inspect
|
|
31
39
|
"\#<#{self.class} address=#{@address.inspect}>"
|
|
32
40
|
end
|
|
33
41
|
|
|
42
|
+
# @attribute [Address] The network address for this endpoint.
|
|
34
43
|
attr :address
|
|
35
44
|
|
|
36
45
|
# Bind a socket to the given address. If a block is given, the socket will be automatically closed when the block exits.
|
|
37
|
-
# @
|
|
38
|
-
#
|
|
39
|
-
# @
|
|
46
|
+
# @yields {|socket| ...} If a block is given, yields the bound socket.
|
|
47
|
+
# @parameter socket [Socket] The socket which has been bound.
|
|
48
|
+
# @returns [Array(Socket)] the bound socket
|
|
40
49
|
def bind(wrapper = self.wrapper, &block)
|
|
41
50
|
[wrapper.bind(@address, **@options, &block)]
|
|
42
51
|
end
|
|
43
52
|
|
|
44
53
|
# Connects a socket to the given address. If a block is given, the socket will be automatically closed when the block exits.
|
|
45
|
-
# @
|
|
54
|
+
# @returns [Socket] the connected socket
|
|
46
55
|
def connect(wrapper = self.wrapper, &block)
|
|
47
56
|
wrapper.connect(@address, **@options, &block)
|
|
48
57
|
end
|
|
@@ -8,7 +8,13 @@ require_relative "composite_endpoint"
|
|
|
8
8
|
require_relative "address_endpoint"
|
|
9
9
|
|
|
10
10
|
module IO::Endpoint
|
|
11
|
+
# Represents an endpoint that has been bound to one or more sockets.
|
|
11
12
|
class BoundEndpoint < Generic
|
|
13
|
+
# Create a bound endpoint from an existing endpoint.
|
|
14
|
+
# @parameter endpoint [Generic] The endpoint to bind.
|
|
15
|
+
# @option backlog [Integer] The maximum length of the queue of pending connections.
|
|
16
|
+
# @option close_on_exec [Boolean] Whether to close sockets on exec.
|
|
17
|
+
# @returns [BoundEndpoint] A new bound endpoint instance.
|
|
12
18
|
def self.bound(endpoint, backlog: Socket::SOMAXCONN, close_on_exec: false)
|
|
13
19
|
sockets = endpoint.bind
|
|
14
20
|
|
|
@@ -19,6 +25,10 @@ module IO::Endpoint
|
|
|
19
25
|
return self.new(endpoint, sockets, **endpoint.options)
|
|
20
26
|
end
|
|
21
27
|
|
|
28
|
+
# Initialize a new bound endpoint.
|
|
29
|
+
# @parameter endpoint [Generic] The original endpoint that was bound.
|
|
30
|
+
# @parameter sockets [Array(Socket)] The sockets that were bound.
|
|
31
|
+
# @parameter options [Hash] Additional options to pass to the parent class.
|
|
22
32
|
def initialize(endpoint, sockets, **options)
|
|
23
33
|
super(**options)
|
|
24
34
|
|
|
@@ -26,7 +36,9 @@ module IO::Endpoint
|
|
|
26
36
|
@sockets = sockets
|
|
27
37
|
end
|
|
28
38
|
|
|
39
|
+
# @attribute [Generic] The original endpoint that was bound.
|
|
29
40
|
attr :endpoint
|
|
41
|
+
# @attribute [Array(Socket)] The sockets that were bound.
|
|
30
42
|
attr :sockets
|
|
31
43
|
|
|
32
44
|
# A endpoint for the local end of the bound socket.
|
|
@@ -49,19 +61,29 @@ module IO::Endpoint
|
|
|
49
61
|
return CompositeEndpoint.new(endpoints)
|
|
50
62
|
end
|
|
51
63
|
|
|
64
|
+
# Close all bound sockets.
|
|
52
65
|
def close
|
|
53
66
|
@sockets.each(&:close)
|
|
54
67
|
@sockets.clear
|
|
55
68
|
end
|
|
56
69
|
|
|
70
|
+
# Get a string representation of the bound endpoint.
|
|
71
|
+
# @returns [String] A string representation of the bound endpoint.
|
|
57
72
|
def to_s
|
|
58
73
|
"bound:#{@endpoint}"
|
|
59
74
|
end
|
|
60
75
|
|
|
76
|
+
# Get a detailed string representation of the bound endpoint.
|
|
77
|
+
# @returns [String] A detailed string representation including socket count.
|
|
61
78
|
def inspect
|
|
62
79
|
"\#<#{self.class} #{@sockets.size} bound sockets for #{@endpoint}>"
|
|
63
80
|
end
|
|
64
81
|
|
|
82
|
+
# Bind the endpoint using the given wrapper.
|
|
83
|
+
# @parameter wrapper [Wrapper] The wrapper to use for binding.
|
|
84
|
+
# @yields {|socket| ...} If a block is given, yields each bound socket.
|
|
85
|
+
# @parameter socket [Socket] A bound socket.
|
|
86
|
+
# @returns [Array(Socket)] An array of bound sockets.
|
|
65
87
|
def bind(wrapper = self.wrapper, &block)
|
|
66
88
|
@sockets.map do |server|
|
|
67
89
|
if block_given?
|
|
@@ -76,6 +98,9 @@ module IO::Endpoint
|
|
|
76
98
|
end
|
|
77
99
|
|
|
78
100
|
class Generic
|
|
101
|
+
# Create a bound endpoint from this endpoint.
|
|
102
|
+
# @parameter options [Hash] Options to pass to {BoundEndpoint.bound}.
|
|
103
|
+
# @returns [BoundEndpoint] A new bound endpoint instance.
|
|
79
104
|
def bound(**options)
|
|
80
105
|
BoundEndpoint.bound(self, **options)
|
|
81
106
|
end
|
|
@@ -8,6 +8,9 @@ require_relative "generic"
|
|
|
8
8
|
module IO::Endpoint
|
|
9
9
|
# A composite endpoint is a collection of endpoints that are used in order.
|
|
10
10
|
class CompositeEndpoint < Generic
|
|
11
|
+
# Initialize a new composite endpoint.
|
|
12
|
+
# @parameter endpoints [Array(Generic)] The endpoints to compose.
|
|
13
|
+
# @parameter options [Hash] Additional options to pass to the parent class and propagate to endpoints.
|
|
11
14
|
def initialize(endpoints, **options)
|
|
12
15
|
super(**options)
|
|
13
16
|
|
|
@@ -19,18 +22,26 @@ module IO::Endpoint
|
|
|
19
22
|
@endpoints = endpoints
|
|
20
23
|
end
|
|
21
24
|
|
|
25
|
+
# Get a string representation of the composite endpoint.
|
|
26
|
+
# @returns [String] A string representation listing all endpoints.
|
|
22
27
|
def to_s
|
|
23
28
|
"composite:#{@endpoints.join(",")}"
|
|
24
29
|
end
|
|
25
30
|
|
|
31
|
+
# Get a detailed string representation of the composite endpoint.
|
|
32
|
+
# @returns [String] A detailed string representation including all endpoints.
|
|
26
33
|
def inspect
|
|
27
34
|
"\#<#{self.class} endpoints=#{@endpoints}>"
|
|
28
35
|
end
|
|
29
36
|
|
|
37
|
+
# Create a new composite endpoint with merged options.
|
|
38
|
+
# @parameter options [Hash] Additional options to merge with existing options.
|
|
39
|
+
# @returns [CompositeEndpoint] A new composite endpoint instance with merged options.
|
|
30
40
|
def with(**options)
|
|
31
41
|
self.class.new(endpoints.map{|endpoint| endpoint.with(**options)}, **@options.merge(options))
|
|
32
42
|
end
|
|
33
43
|
|
|
44
|
+
# @attribute [Array(Generic)] The endpoints in this composite endpoint.
|
|
34
45
|
attr :endpoints
|
|
35
46
|
|
|
36
47
|
# The number of endpoints in the composite endpoint.
|
|
@@ -38,12 +49,21 @@ module IO::Endpoint
|
|
|
38
49
|
@endpoints.size
|
|
39
50
|
end
|
|
40
51
|
|
|
52
|
+
# Enumerate all endpoints in the composite endpoint.
|
|
53
|
+
# @yields {|endpoint| ...} For each endpoint in the composite, yields it.
|
|
54
|
+
# @parameter endpoint [Generic] An endpoint in the composite.
|
|
41
55
|
def each(&block)
|
|
42
56
|
@endpoints.each do |endpoint|
|
|
43
57
|
endpoint.each(&block)
|
|
44
58
|
end
|
|
45
59
|
end
|
|
46
60
|
|
|
61
|
+
# Connect to the first endpoint that succeeds.
|
|
62
|
+
# @parameter wrapper [Wrapper] The wrapper to use for connecting.
|
|
63
|
+
# @yields {|socket| ...} If a block is given, yields the connected socket from the first successful endpoint.
|
|
64
|
+
# @parameter socket [Socket] The connected socket.
|
|
65
|
+
# @returns [Socket] The connected socket.
|
|
66
|
+
# @raises [Exception] If all endpoints fail to connect, raises the last error encountered.
|
|
47
67
|
def connect(wrapper = self.wrapper, &block)
|
|
48
68
|
last_error = nil
|
|
49
69
|
|
|
@@ -57,6 +77,11 @@ module IO::Endpoint
|
|
|
57
77
|
raise last_error
|
|
58
78
|
end
|
|
59
79
|
|
|
80
|
+
# Bind all endpoints in the composite.
|
|
81
|
+
# @parameter wrapper [Wrapper] The wrapper to use for binding.
|
|
82
|
+
# @yields {|socket| ...} For each endpoint that is bound, yields the bound socket.
|
|
83
|
+
# @parameter socket [Socket] A bound socket.
|
|
84
|
+
# @returns [Array(Socket)] An array of bound sockets if no block is given.
|
|
60
85
|
def bind(wrapper = self.wrapper, &block)
|
|
61
86
|
if block_given?
|
|
62
87
|
@endpoints.each do |endpoint|
|
|
@@ -68,6 +93,10 @@ module IO::Endpoint
|
|
|
68
93
|
end
|
|
69
94
|
end
|
|
70
95
|
|
|
96
|
+
# Create a composite endpoint from multiple endpoints.
|
|
97
|
+
# @parameter endpoints [Array(Generic)] The endpoints to compose.
|
|
98
|
+
# @parameter options [Hash] Additional options to pass to the composite endpoint.
|
|
99
|
+
# @returns [CompositeEndpoint] A new composite endpoint instance.
|
|
71
100
|
def self.composite(*endpoints, **options)
|
|
72
101
|
CompositeEndpoint.new(endpoints, **options)
|
|
73
102
|
end
|
|
@@ -10,7 +10,12 @@ require_relative "socket_endpoint"
|
|
|
10
10
|
require "openssl"
|
|
11
11
|
|
|
12
12
|
module IO::Endpoint
|
|
13
|
+
# Represents an endpoint that has been connected to a socket.
|
|
13
14
|
class ConnectedEndpoint < Generic
|
|
15
|
+
# Create a connected endpoint from an existing endpoint.
|
|
16
|
+
# @parameter endpoint [Generic] The endpoint to connect.
|
|
17
|
+
# @option close_on_exec [Boolean] Whether to close the socket on exec.
|
|
18
|
+
# @returns [ConnectedEndpoint] A new connected endpoint instance.
|
|
14
19
|
def self.connected(endpoint, close_on_exec: false)
|
|
15
20
|
socket = endpoint.connect
|
|
16
21
|
|
|
@@ -19,6 +24,10 @@ module IO::Endpoint
|
|
|
19
24
|
return self.new(endpoint, socket, **endpoint.options)
|
|
20
25
|
end
|
|
21
26
|
|
|
27
|
+
# Initialize a new connected endpoint.
|
|
28
|
+
# @parameter endpoint [Generic] The original endpoint that was connected.
|
|
29
|
+
# @parameter socket [Socket] The socket that was connected.
|
|
30
|
+
# @parameter options [Hash] Additional options to pass to the parent class.
|
|
22
31
|
def initialize(endpoint, socket, **options)
|
|
23
32
|
super(**options)
|
|
24
33
|
|
|
@@ -26,7 +35,9 @@ module IO::Endpoint
|
|
|
26
35
|
@socket = socket
|
|
27
36
|
end
|
|
28
37
|
|
|
38
|
+
# @attribute [Generic] The original endpoint that was connected.
|
|
29
39
|
attr :endpoint
|
|
40
|
+
# @attribute [Socket] The socket that was connected.
|
|
30
41
|
attr :socket
|
|
31
42
|
|
|
32
43
|
# A endpoint for the local end of the bound socket.
|
|
@@ -41,6 +52,11 @@ module IO::Endpoint
|
|
|
41
52
|
AddressEndpoint.new(socket.to_io.remote_address, **options)
|
|
42
53
|
end
|
|
43
54
|
|
|
55
|
+
# Connect using the already connected socket.
|
|
56
|
+
# @parameter wrapper [Wrapper] The wrapper to use (unused, socket is already connected).
|
|
57
|
+
# @yields {|socket| ...} If a block is given, yields the connected socket.
|
|
58
|
+
# @parameter socket [Socket] The connected socket.
|
|
59
|
+
# @returns [Socket] The connected socket or a duplicate if no block is given.
|
|
44
60
|
def connect(wrapper = self.wrapper, &block)
|
|
45
61
|
if block_given?
|
|
46
62
|
yield @socket
|
|
@@ -49,6 +65,7 @@ module IO::Endpoint
|
|
|
49
65
|
end
|
|
50
66
|
end
|
|
51
67
|
|
|
68
|
+
# Close the connected socket.
|
|
52
69
|
def close
|
|
53
70
|
if @socket
|
|
54
71
|
@socket.close
|
|
@@ -56,16 +73,23 @@ module IO::Endpoint
|
|
|
56
73
|
end
|
|
57
74
|
end
|
|
58
75
|
|
|
76
|
+
# Get a string representation of the connected endpoint.
|
|
77
|
+
# @returns [String] A string representation of the connected endpoint.
|
|
59
78
|
def to_s
|
|
60
79
|
"connected:#{@endpoint}"
|
|
61
80
|
end
|
|
62
81
|
|
|
82
|
+
# Get a detailed string representation of the connected endpoint.
|
|
83
|
+
# @returns [String] A detailed string representation including the socket.
|
|
63
84
|
def inspect
|
|
64
85
|
"\#<#{self.class} #{@socket} connected for #{@endpoint}>"
|
|
65
86
|
end
|
|
66
87
|
end
|
|
67
|
-
|
|
88
|
+
|
|
68
89
|
class Generic
|
|
90
|
+
# Create a connected endpoint from this endpoint.
|
|
91
|
+
# @parameter options [Hash] Options to pass to {ConnectedEndpoint.connected}.
|
|
92
|
+
# @returns [ConnectedEndpoint] A new connected endpoint instance.
|
|
69
93
|
def connected(**options)
|
|
70
94
|
ConnectedEndpoint.connected(self, **options)
|
|
71
95
|
end
|
data/lib/io/endpoint/generic.rb
CHANGED
|
@@ -12,10 +12,15 @@ module IO::Endpoint
|
|
|
12
12
|
|
|
13
13
|
# Endpoints represent a way of connecting or binding to an address.
|
|
14
14
|
class Generic
|
|
15
|
+
# Initialize a new generic endpoint.
|
|
16
|
+
# @parameter options [Hash] Configuration options for the endpoint.
|
|
15
17
|
def initialize(**options)
|
|
16
18
|
@options = options.freeze
|
|
17
19
|
end
|
|
18
20
|
|
|
21
|
+
# Create a new endpoint with merged options.
|
|
22
|
+
# @parameter options [Hash] Additional options to merge with existing options.
|
|
23
|
+
# @returns [Generic] A new endpoint instance with merged options.
|
|
19
24
|
def with(**options)
|
|
20
25
|
dup = self.dup
|
|
21
26
|
|
|
@@ -26,43 +31,43 @@ module IO::Endpoint
|
|
|
26
31
|
|
|
27
32
|
attr_accessor :options
|
|
28
33
|
|
|
29
|
-
# @
|
|
34
|
+
# @returns [String] The hostname of the bound socket.
|
|
30
35
|
def hostname
|
|
31
36
|
@options[:hostname]
|
|
32
37
|
end
|
|
33
38
|
|
|
34
39
|
# 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
|
-
# @
|
|
40
|
+
# @returns [Boolean, nil] The value for `SO_REUSEPORT`.
|
|
36
41
|
def reuse_port?
|
|
37
42
|
@options[:reuse_port]
|
|
38
43
|
end
|
|
39
44
|
|
|
40
45
|
# 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
|
-
# @
|
|
46
|
+
# @returns [Boolean] The value for `SO_REUSEADDR`.
|
|
42
47
|
def reuse_address?
|
|
43
48
|
@options[:reuse_address]
|
|
44
49
|
end
|
|
45
50
|
|
|
46
51
|
# Controls SO_LINGER. The amount of time the socket will stay in the `TIME_WAIT` state after being closed.
|
|
47
|
-
# @
|
|
52
|
+
# @returns [Integer, nil] The value for SO_LINGER.
|
|
48
53
|
def linger
|
|
49
54
|
@options[:linger]
|
|
50
55
|
end
|
|
51
56
|
|
|
52
|
-
# @
|
|
57
|
+
# @returns [Numeric] The default timeout for socket operations.
|
|
53
58
|
def timeout
|
|
54
59
|
@options[:timeout]
|
|
55
60
|
end
|
|
56
61
|
|
|
57
|
-
# @
|
|
62
|
+
# @returns [Address] the address to bind to before connecting.
|
|
58
63
|
def local_address
|
|
59
64
|
@options[:local_address]
|
|
60
65
|
end
|
|
61
66
|
|
|
62
67
|
# Bind a socket to the given address. If a block is given, the socket will be automatically closed when the block exits.
|
|
63
68
|
# @parameter wrapper [Wrapper] The wrapper to use for binding.
|
|
64
|
-
# @yields {|socket| ...}
|
|
65
|
-
#
|
|
69
|
+
# @yields {|socket| ...} If a block is given, yields the bound socket.
|
|
70
|
+
# @parameter socket [Socket] The socket which has been bound.
|
|
66
71
|
# @returns [Array(Socket)] the bound socket
|
|
67
72
|
def bind(wrapper = self.wrapper, &block)
|
|
68
73
|
raise NotImplementedError
|
|
@@ -70,14 +75,15 @@ module IO::Endpoint
|
|
|
70
75
|
|
|
71
76
|
# Connects a socket to the given address. If a block is given, the socket will be automatically closed when the block exits.
|
|
72
77
|
# @parameter wrapper [Wrapper] The wrapper to use for connecting.
|
|
73
|
-
# @
|
|
78
|
+
# @returns [Socket] the connected socket
|
|
74
79
|
def connect(wrapper = self.wrapper, &block)
|
|
75
80
|
raise NotImplementedError
|
|
76
81
|
end
|
|
77
82
|
|
|
78
83
|
# Bind and accept connections on the given address.
|
|
79
84
|
# @parameter wrapper [Wrapper] The wrapper to use for accepting connections.
|
|
80
|
-
# @yields
|
|
85
|
+
# @yields {|socket| ...} For each accepted connection, yields the socket.
|
|
86
|
+
# @parameter socket [Socket] The accepted socket.
|
|
81
87
|
def accept(wrapper = self.wrapper, &block)
|
|
82
88
|
bind(wrapper) do |server|
|
|
83
89
|
wrapper.accept(server, **@options, &block)
|
|
@@ -85,7 +91,7 @@ module IO::Endpoint
|
|
|
85
91
|
end
|
|
86
92
|
|
|
87
93
|
# Enumerate all discrete paths as endpoints.
|
|
88
|
-
# @yields {|endpoint| ...}
|
|
94
|
+
# @yields {|endpoint| ...} For each endpoint, yields it.
|
|
89
95
|
# @parameter endpoint [Endpoint] The endpoint.
|
|
90
96
|
def each
|
|
91
97
|
return to_enum unless block_given?
|
|
@@ -97,8 +103,8 @@ module IO::Endpoint
|
|
|
97
103
|
#
|
|
98
104
|
# You should not use untrusted input as it may execute arbitrary code.
|
|
99
105
|
#
|
|
100
|
-
# @
|
|
101
|
-
# @
|
|
106
|
+
# @parameter string [String] URI as string. Scheme will decide implementation used.
|
|
107
|
+
# @parameter options keyword arguments passed through to {#initialize}
|
|
102
108
|
#
|
|
103
109
|
# @see Endpoint.ssl ssl - invoked when parsing a URL with the ssl scheme "ssl://127.0.0.1"
|
|
104
110
|
# @see Endpoint.tcp tcp - invoked when parsing a URL with the tcp scheme: "tcp://127.0.0.1"
|
|
@@ -6,17 +6,25 @@
|
|
|
6
6
|
require_relative "address_endpoint"
|
|
7
7
|
|
|
8
8
|
module IO::Endpoint
|
|
9
|
+
# Represents an endpoint for a hostname and service that resolves to multiple addresses.
|
|
9
10
|
class HostEndpoint < Generic
|
|
11
|
+
# Initialize a new host endpoint.
|
|
12
|
+
# @parameter specification [Array] The host specification array containing nodename, service, family, socktype, protocol, and flags.
|
|
13
|
+
# @parameter options [Hash] Additional options to pass to the parent class.
|
|
10
14
|
def initialize(specification, **options)
|
|
11
15
|
super(**options)
|
|
12
16
|
|
|
13
17
|
@specification = specification
|
|
14
18
|
end
|
|
15
19
|
|
|
20
|
+
# Get a string representation of the host endpoint.
|
|
21
|
+
# @returns [String] A string representation showing hostname and service.
|
|
16
22
|
def to_s
|
|
17
23
|
"host:#{@specification[0]}:#{@specification[1]}"
|
|
18
24
|
end
|
|
19
25
|
|
|
26
|
+
# Get a detailed string representation of the host endpoint.
|
|
27
|
+
# @returns [String] A detailed string representation including all specification parameters.
|
|
20
28
|
def inspect
|
|
21
29
|
nodename, service, family, socktype, protocol, flags = @specification
|
|
22
30
|
|
|
@@ -24,27 +32,34 @@ module IO::Endpoint
|
|
|
24
32
|
end
|
|
25
33
|
|
|
26
34
|
|
|
35
|
+
# @attribute [Array] The host specification array.
|
|
27
36
|
attr :specification
|
|
28
37
|
|
|
38
|
+
# Get the hostname from the specification.
|
|
39
|
+
# @returns [String, nil] The hostname (nodename) from the specification.
|
|
29
40
|
def hostname
|
|
30
41
|
@specification[0]
|
|
31
42
|
end
|
|
32
43
|
|
|
44
|
+
# Get the service from the specification.
|
|
45
|
+
# @returns [String, Integer, nil] The service (port) from the specification.
|
|
33
46
|
def service
|
|
34
47
|
@specification[1]
|
|
35
48
|
end
|
|
36
49
|
|
|
37
50
|
# Try to connect to the given host by connecting to each address in sequence until a connection is made.
|
|
38
|
-
# @
|
|
39
|
-
#
|
|
40
|
-
# @
|
|
51
|
+
# @yields {|socket| ...} If a block is given, yields the connected socket (may be invoked multiple times during connection attempts).
|
|
52
|
+
# @parameter socket [Socket] The socket which is being connected.
|
|
53
|
+
# @returns [Socket] the connected socket
|
|
54
|
+
# @raises [Exception] if no connection could complete successfully
|
|
41
55
|
def connect(wrapper = self.wrapper, &block)
|
|
42
56
|
last_error = nil
|
|
43
57
|
|
|
44
58
|
Addrinfo.foreach(*@specification) do |address|
|
|
45
59
|
begin
|
|
46
60
|
socket = wrapper.connect(address, **@options)
|
|
47
|
-
rescue
|
|
61
|
+
rescue => last_error
|
|
62
|
+
Console.debug(self, "Failed to connect:", address, exception: last_error)
|
|
48
63
|
# Try again unless if possible, otherwise raise...
|
|
49
64
|
else
|
|
50
65
|
return socket unless block_given?
|
|
@@ -61,15 +76,17 @@ module IO::Endpoint
|
|
|
61
76
|
end
|
|
62
77
|
|
|
63
78
|
# Invokes the given block for every address which can be bound to.
|
|
64
|
-
# @
|
|
65
|
-
#
|
|
79
|
+
# @yields {|socket| ...} For each address that can be bound, yields the bound socket.
|
|
80
|
+
# @parameter socket [Socket] The bound socket.
|
|
81
|
+
# @returns [Array<Socket>] an array of bound sockets
|
|
66
82
|
def bind(wrapper = self.wrapper, &block)
|
|
67
83
|
Addrinfo.foreach(*@specification).map do |address|
|
|
68
84
|
wrapper.bind(address, **@options, &block)
|
|
69
85
|
end
|
|
70
86
|
end
|
|
71
87
|
|
|
72
|
-
# @
|
|
88
|
+
# @yields {|endpoint| ...} For each resolved address, yields an address endpoint.
|
|
89
|
+
# @parameter endpoint [AddressEndpoint] An address endpoint.
|
|
73
90
|
def each
|
|
74
91
|
return to_enum unless block_given?
|
|
75
92
|
|
|
@@ -79,20 +96,20 @@ module IO::Endpoint
|
|
|
79
96
|
end
|
|
80
97
|
end
|
|
81
98
|
|
|
82
|
-
# @
|
|
83
|
-
# @
|
|
99
|
+
# @parameter arguments nodename, service, family, socktype, protocol, flags. `socktype` will be set to Socket::SOCK_STREAM.
|
|
100
|
+
# @parameter options keyword arguments passed on to {HostEndpoint#initialize}
|
|
84
101
|
#
|
|
85
|
-
# @
|
|
102
|
+
# @returns [HostEndpoint]
|
|
86
103
|
def self.tcp(*arguments, **options)
|
|
87
104
|
arguments[3] = ::Socket::SOCK_STREAM
|
|
88
105
|
|
|
89
106
|
HostEndpoint.new(arguments, **options)
|
|
90
107
|
end
|
|
91
|
-
|
|
92
|
-
# @
|
|
93
|
-
# @
|
|
108
|
+
|
|
109
|
+
# @parameter arguments nodename, service, family, socktype, protocol, flags. `socktype` will be set to Socket::SOCK_DGRAM.
|
|
110
|
+
# @parameter options keyword arguments passed on to {HostEndpoint#initialize}
|
|
94
111
|
#
|
|
95
|
-
# @
|
|
112
|
+
# @returns [HostEndpoint]
|
|
96
113
|
def self.udp(*arguments, **options)
|
|
97
114
|
arguments[3] = ::Socket::SOCK_DGRAM
|
|
98
115
|
|
|
@@ -1,29 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2023-
|
|
4
|
+
# Copyright, 2023-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "generic"
|
|
7
7
|
|
|
8
8
|
module IO::Endpoint
|
|
9
9
|
# This class doesn't exert ownership over the specified socket, wraps a native ::IO.
|
|
10
10
|
class SocketEndpoint < Generic
|
|
11
|
+
# Initialize a new socket endpoint.
|
|
12
|
+
# @parameter socket [Socket] The socket to wrap.
|
|
13
|
+
# @parameter options [Hash] Additional options to pass to the parent class.
|
|
11
14
|
def initialize(socket, **options)
|
|
12
15
|
super(**options)
|
|
13
16
|
|
|
14
17
|
@socket = socket
|
|
15
18
|
end
|
|
16
19
|
|
|
20
|
+
# Get a string representation of the socket endpoint.
|
|
21
|
+
# @returns [String] A string representation showing the socket.
|
|
17
22
|
def to_s
|
|
18
23
|
"socket:#{@socket}"
|
|
19
24
|
end
|
|
20
25
|
|
|
26
|
+
# Get a detailed string representation of the socket endpoint.
|
|
27
|
+
# @returns [String] A detailed string representation including the socket.
|
|
21
28
|
def inspect
|
|
22
29
|
"\#<#{self.class} #{@socket.inspect}>"
|
|
23
30
|
end
|
|
24
31
|
|
|
32
|
+
# @attribute [Socket] The wrapped socket.
|
|
25
33
|
attr :socket
|
|
26
34
|
|
|
35
|
+
# Bind using the wrapped socket.
|
|
36
|
+
# @yields {|socket| ...} If a block is given, yields the socket.
|
|
37
|
+
# @parameter socket [Socket] The socket.
|
|
38
|
+
# @returns [Socket] The socket.
|
|
27
39
|
def bind(&block)
|
|
28
40
|
if block_given?
|
|
29
41
|
yield @socket
|
|
@@ -32,6 +44,10 @@ module IO::Endpoint
|
|
|
32
44
|
end
|
|
33
45
|
end
|
|
34
46
|
|
|
47
|
+
# Connect using the wrapped socket.
|
|
48
|
+
# @yields {|socket| ...} If a block is given, yields the socket.
|
|
49
|
+
# @parameter socket [Socket] The socket.
|
|
50
|
+
# @returns [Socket] The socket.
|
|
35
51
|
def connect(&block)
|
|
36
52
|
if block_given?
|
|
37
53
|
yield @socket
|
|
@@ -41,6 +57,10 @@ module IO::Endpoint
|
|
|
41
57
|
end
|
|
42
58
|
end
|
|
43
59
|
|
|
60
|
+
# Create a socket endpoint from an existing socket.
|
|
61
|
+
# @parameter socket [Socket] The socket to wrap.
|
|
62
|
+
# @parameter options [Hash] Additional options to pass to the socket endpoint.
|
|
63
|
+
# @returns [SocketEndpoint] A new socket endpoint instance.
|
|
44
64
|
def self.socket(socket, **options)
|
|
45
65
|
SocketEndpoint.new(socket, **options)
|
|
46
66
|
end
|
|
@@ -1,55 +1,75 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2023-
|
|
4
|
+
# Copyright, 2023-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "host_endpoint"
|
|
7
7
|
require_relative "generic"
|
|
8
8
|
|
|
9
9
|
require "openssl"
|
|
10
10
|
|
|
11
|
+
# @namespace
|
|
11
12
|
module OpenSSL
|
|
13
|
+
# @namespace
|
|
12
14
|
module SSL
|
|
15
|
+
# Represents an SSL socket with additional methods for compatibility.
|
|
13
16
|
class SSLSocket
|
|
14
17
|
unless method_defined?(:start)
|
|
18
|
+
# Start the SSL handshake (alias for accept).
|
|
15
19
|
def start
|
|
16
20
|
self.accept
|
|
17
21
|
end
|
|
18
22
|
end
|
|
19
23
|
end
|
|
20
24
|
|
|
25
|
+
# Represents a module that forwards socket methods to the underlying IO object.
|
|
21
26
|
module SocketForwarder
|
|
22
27
|
unless method_defined?(:close_on_exec=)
|
|
28
|
+
# Set whether the socket should be closed on exec.
|
|
29
|
+
# @parameter value [Boolean] Whether to close on exec.
|
|
23
30
|
def close_on_exec=(value)
|
|
24
31
|
to_io.close_on_exec = value
|
|
25
32
|
end
|
|
26
33
|
end
|
|
27
34
|
|
|
28
35
|
unless method_defined?(:local_address)
|
|
36
|
+
# Get the local address of the socket.
|
|
37
|
+
# @returns [Addrinfo] The local address.
|
|
29
38
|
def local_address
|
|
30
39
|
to_io.local_address
|
|
31
40
|
end
|
|
32
41
|
end
|
|
33
42
|
|
|
34
43
|
unless method_defined?(:remote_address)
|
|
44
|
+
# Get the remote address of the socket.
|
|
45
|
+
# @returns [Addrinfo] The remote address.
|
|
35
46
|
def remote_address
|
|
36
47
|
to_io.remote_address
|
|
37
48
|
end
|
|
38
49
|
end
|
|
39
50
|
|
|
40
51
|
unless method_defined?(:wait)
|
|
52
|
+
# Wait for the socket to become ready.
|
|
53
|
+
# @parameter arguments [Array] Arguments to pass to the underlying IO wait method.
|
|
54
|
+
# @returns [IO, nil] The socket if ready, nil otherwise.
|
|
41
55
|
def wait(*arguments)
|
|
42
56
|
to_io.wait(*arguments)
|
|
43
57
|
end
|
|
44
58
|
end
|
|
45
59
|
|
|
46
60
|
unless method_defined?(:wait_readable)
|
|
61
|
+
# Wait for the socket to become readable.
|
|
62
|
+
# @parameter arguments [Array] Arguments to pass to the underlying IO wait_readable method.
|
|
63
|
+
# @returns [IO, nil] The socket if readable, nil otherwise.
|
|
47
64
|
def wait_readable(*arguments)
|
|
48
65
|
to_io.wait_readable(*arguments)
|
|
49
66
|
end
|
|
50
67
|
end
|
|
51
68
|
|
|
52
69
|
unless method_defined?(:wait_writable)
|
|
70
|
+
# Wait for the socket to become writable.
|
|
71
|
+
# @parameter arguments [Array] Arguments to pass to the underlying IO wait_writable method.
|
|
72
|
+
# @returns [IO, nil] The socket if writable, nil otherwise.
|
|
53
73
|
def wait_writable(*arguments)
|
|
54
74
|
to_io.wait_writable(*arguments)
|
|
55
75
|
end
|
|
@@ -57,12 +77,16 @@ module OpenSSL
|
|
|
57
77
|
|
|
58
78
|
if IO.method_defined?(:timeout)
|
|
59
79
|
unless method_defined?(:timeout)
|
|
80
|
+
# Get the timeout for socket operations.
|
|
81
|
+
# @returns [Numeric, nil] The timeout value.
|
|
60
82
|
def timeout
|
|
61
83
|
to_io.timeout
|
|
62
84
|
end
|
|
63
85
|
end
|
|
64
86
|
|
|
65
87
|
unless method_defined?(:timeout=)
|
|
88
|
+
# Set the timeout for socket operations.
|
|
89
|
+
# @parameter value [Numeric, nil] The timeout value.
|
|
66
90
|
def timeout=(value)
|
|
67
91
|
to_io.timeout = value
|
|
68
92
|
end
|
|
@@ -73,7 +97,12 @@ module OpenSSL
|
|
|
73
97
|
end
|
|
74
98
|
|
|
75
99
|
module IO::Endpoint
|
|
100
|
+
# Represents an SSL/TLS endpoint that wraps another endpoint.
|
|
76
101
|
class SSLEndpoint < Generic
|
|
102
|
+
# Initialize a new SSL endpoint.
|
|
103
|
+
# @parameter endpoint [Generic] The underlying endpoint to wrap with SSL.
|
|
104
|
+
# @option ssl_context [OpenSSL::SSL::SSLContext, nil] An optional SSL context to use.
|
|
105
|
+
# @parameter options [Hash] Additional options including `:ssl_params` and `:hostname`.
|
|
77
106
|
def initialize(endpoint, **options)
|
|
78
107
|
super(**options)
|
|
79
108
|
|
|
@@ -86,29 +115,44 @@ module IO::Endpoint
|
|
|
86
115
|
end
|
|
87
116
|
end
|
|
88
117
|
|
|
118
|
+
# Get a string representation of the SSL endpoint.
|
|
119
|
+
# @returns [String] A string representation showing the underlying endpoint.
|
|
89
120
|
def to_s
|
|
90
121
|
"ssl:#{@endpoint}"
|
|
91
122
|
end
|
|
92
123
|
|
|
124
|
+
# Get a detailed string representation of the SSL endpoint.
|
|
125
|
+
# @returns [String] A detailed string representation including the underlying endpoint.
|
|
93
126
|
def inspect
|
|
94
127
|
"\#<#{self.class} endpoint=#{@endpoint.inspect}>"
|
|
95
128
|
end
|
|
96
129
|
|
|
130
|
+
# Get the address from the underlying endpoint.
|
|
131
|
+
# @returns [Address, nil] The address from the underlying endpoint.
|
|
97
132
|
def address
|
|
98
133
|
@endpoint.address
|
|
99
134
|
end
|
|
100
135
|
|
|
136
|
+
# Get the hostname for SSL verification.
|
|
137
|
+
# @returns [String, nil] The hostname from options or the underlying endpoint.
|
|
101
138
|
def hostname
|
|
102
139
|
@options[:hostname] || @endpoint.hostname
|
|
103
140
|
end
|
|
104
141
|
|
|
142
|
+
# @attribute [Generic] The underlying endpoint.
|
|
105
143
|
attr :endpoint
|
|
144
|
+
# @attribute [Hash] The options hash.
|
|
106
145
|
attr :options
|
|
107
146
|
|
|
147
|
+
# Get SSL parameters from options.
|
|
148
|
+
# @returns [Hash, nil] SSL parameters if specified in options.
|
|
108
149
|
def params
|
|
109
150
|
@options[:ssl_params]
|
|
110
151
|
end
|
|
111
152
|
|
|
153
|
+
# Build an SSL context with configured parameters.
|
|
154
|
+
# @parameter context [OpenSSL::SSL::SSLContext] An optional SSL context to configure.
|
|
155
|
+
# @returns [OpenSSL::SSL::SSLContext] The configured SSL context.
|
|
112
156
|
def build_context(context = ::OpenSSL::SSL::SSLContext.new)
|
|
113
157
|
if params = self.params
|
|
114
158
|
context.set_params(params)
|
|
@@ -120,16 +164,24 @@ module IO::Endpoint
|
|
|
120
164
|
return context
|
|
121
165
|
end
|
|
122
166
|
|
|
167
|
+
# Get or build the SSL context.
|
|
168
|
+
# @returns [OpenSSL::SSL::SSLContext] The SSL context.
|
|
123
169
|
def context
|
|
124
170
|
@context ||= build_context
|
|
125
171
|
end
|
|
126
172
|
|
|
173
|
+
# Create an SSL server socket from an IO object.
|
|
174
|
+
# @parameter io [IO] The underlying IO object.
|
|
175
|
+
# @returns [OpenSSL::SSL::SSLServer] A new SSL server socket.
|
|
127
176
|
def make_server(io)
|
|
128
177
|
::OpenSSL::SSL::SSLServer.new(io, self.context).tap do |server|
|
|
129
178
|
server.start_immediately = false
|
|
130
179
|
end
|
|
131
180
|
end
|
|
132
181
|
|
|
182
|
+
# Create an SSL client socket from an IO object.
|
|
183
|
+
# @parameter io [IO] The underlying IO object.
|
|
184
|
+
# @returns [OpenSSL::SSL::SSLSocket] A new SSL client socket.
|
|
133
185
|
def make_socket(io)
|
|
134
186
|
::OpenSSL::SSL::SSLSocket.new(io, self.context).tap do |socket|
|
|
135
187
|
# We consider the underlying IO is owned by the SSL socket:
|
|
@@ -138,8 +190,9 @@ module IO::Endpoint
|
|
|
138
190
|
end
|
|
139
191
|
|
|
140
192
|
# Connect to the underlying endpoint and establish a SSL connection.
|
|
141
|
-
# @
|
|
142
|
-
#
|
|
193
|
+
# @yields {|socket| ...} If a block is given, yields the SSL server socket.
|
|
194
|
+
# @parameter socket [Socket] The SSL server socket.
|
|
195
|
+
# @returns [Socket] the connected socket
|
|
143
196
|
def bind(*arguments, **options, &block)
|
|
144
197
|
if block_given?
|
|
145
198
|
@endpoint.bind(*arguments, **options) do |server|
|
|
@@ -153,8 +206,9 @@ module IO::Endpoint
|
|
|
153
206
|
end
|
|
154
207
|
|
|
155
208
|
# Connect to the underlying endpoint and establish a SSL connection.
|
|
156
|
-
# @
|
|
157
|
-
#
|
|
209
|
+
# @yields {|socket| ...} If a block is given, yields the connected SSL socket.
|
|
210
|
+
# @parameter socket [Socket] The connected SSL socket.
|
|
211
|
+
# @returns [Socket] the connected socket
|
|
158
212
|
def connect(&block)
|
|
159
213
|
socket = self.make_socket(@endpoint.connect)
|
|
160
214
|
|
|
@@ -170,7 +224,7 @@ module IO::Endpoint
|
|
|
170
224
|
end
|
|
171
225
|
|
|
172
226
|
return socket unless block_given?
|
|
173
|
-
|
|
227
|
+
|
|
174
228
|
begin
|
|
175
229
|
yield socket
|
|
176
230
|
ensure
|
|
@@ -178,6 +232,9 @@ module IO::Endpoint
|
|
|
178
232
|
end
|
|
179
233
|
end
|
|
180
234
|
|
|
235
|
+
# Enumerate all endpoints by wrapping each underlying endpoint with SSL.
|
|
236
|
+
# @yields {|endpoint| ...} For each underlying endpoint, yields an SSL-wrapped endpoint.
|
|
237
|
+
# @parameter endpoint [SSLEndpoint] An SSL endpoint.
|
|
181
238
|
def each
|
|
182
239
|
return to_enum unless block_given?
|
|
183
240
|
|
|
@@ -186,13 +243,13 @@ module IO::Endpoint
|
|
|
186
243
|
end
|
|
187
244
|
end
|
|
188
245
|
end
|
|
189
|
-
|
|
190
|
-
# @
|
|
191
|
-
# @
|
|
192
|
-
# @
|
|
193
|
-
# @
|
|
246
|
+
|
|
247
|
+
# @parameter arguments
|
|
248
|
+
# @parameter ssl_context [OpenSSL::SSL::SSLContext, nil]
|
|
249
|
+
# @parameter hostname [String, nil]
|
|
250
|
+
# @parameter options keyword arguments passed through to {Endpoint.tcp}
|
|
194
251
|
#
|
|
195
|
-
# @
|
|
252
|
+
# @returns [SSLEndpoint]
|
|
196
253
|
def self.ssl(*arguments, ssl_context: nil, hostname: nil, **options)
|
|
197
254
|
SSLEndpoint.new(self.tcp(*arguments, **options), ssl_context: ssl_context, hostname: hostname)
|
|
198
255
|
end
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2023-
|
|
4
|
+
# Copyright, 2023-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "address_endpoint"
|
|
7
7
|
|
|
8
8
|
module IO::Endpoint
|
|
9
9
|
# This class doesn't exert ownership over the specified unix socket and ensures exclusive access by using `flock` where possible.
|
|
10
10
|
class UNIXEndpoint < AddressEndpoint
|
|
11
|
+
# Initialize a new UNIX domain socket endpoint.
|
|
12
|
+
# @parameter path [String] The path to the UNIX socket.
|
|
13
|
+
# @parameter type [Integer] The socket type (defaults to Socket::SOCK_STREAM).
|
|
14
|
+
# @parameter options [Hash] Additional options to pass to the parent class.
|
|
11
15
|
def initialize(path, type = Socket::SOCK_STREAM, **options)
|
|
12
16
|
# I wonder if we should implement chdir behaviour in here if path is longer than 104 characters.
|
|
13
17
|
super(Address.unix(path, type), **options)
|
|
@@ -15,16 +19,23 @@ module IO::Endpoint
|
|
|
15
19
|
@path = path
|
|
16
20
|
end
|
|
17
21
|
|
|
22
|
+
# Get a string representation of the UNIX endpoint.
|
|
23
|
+
# @returns [String] A string representation showing the socket path.
|
|
18
24
|
def to_s
|
|
19
25
|
"unix:#{@path}"
|
|
20
26
|
end
|
|
21
27
|
|
|
28
|
+
# Get a detailed string representation of the UNIX endpoint.
|
|
29
|
+
# @returns [String] A detailed string representation including the path.
|
|
22
30
|
def inspect
|
|
23
31
|
"\#<#{self.class} path=#{@path.inspect}>"
|
|
24
32
|
end
|
|
25
33
|
|
|
34
|
+
# @attribute [String] The path to the UNIX socket.
|
|
26
35
|
attr :path
|
|
27
36
|
|
|
37
|
+
# Check if the socket is currently bound and accepting connections.
|
|
38
|
+
# @returns [Boolean] True if the socket is bound and accepting connections, false otherwise.
|
|
28
39
|
def bound?
|
|
29
40
|
self.connect do
|
|
30
41
|
return true
|
|
@@ -35,7 +46,12 @@ module IO::Endpoint
|
|
|
35
46
|
return false
|
|
36
47
|
end
|
|
37
48
|
|
|
38
|
-
|
|
49
|
+
# Bind the UNIX socket, handling stale socket files.
|
|
50
|
+
# @yields {|socket| ...} If a block is given, yields the bound socket.
|
|
51
|
+
# @parameter socket [Socket] The bound socket.
|
|
52
|
+
# @returns [Array(Socket)] The bound socket.
|
|
53
|
+
# @raises [Errno::EADDRINUSE] If the socket is still in use by another process.
|
|
54
|
+
def bind(...)
|
|
39
55
|
super
|
|
40
56
|
rescue Errno::EADDRINUSE
|
|
41
57
|
# 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.
|
|
@@ -48,11 +64,11 @@ module IO::Endpoint
|
|
|
48
64
|
end
|
|
49
65
|
end
|
|
50
66
|
|
|
51
|
-
# @
|
|
52
|
-
# @
|
|
53
|
-
# @
|
|
67
|
+
# @parameter path [String]
|
|
68
|
+
# @parameter type Socket type
|
|
69
|
+
# @parameter options keyword arguments passed through to {UNIXEndpoint#initialize}
|
|
54
70
|
#
|
|
55
|
-
# @
|
|
71
|
+
# @returns [UNIXEndpoint]
|
|
56
72
|
def self.unix(path = "", type = ::Socket::SOCK_STREAM, **options)
|
|
57
73
|
UNIXEndpoint.new(path, type, **options)
|
|
58
74
|
end
|
data/lib/io/endpoint/version.rb
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2023-
|
|
4
|
+
# Copyright, 2023-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
|
+
# @namespace
|
|
6
7
|
class IO
|
|
8
|
+
# @namespace
|
|
7
9
|
module Endpoint
|
|
8
|
-
VERSION = "0.
|
|
10
|
+
VERSION = "0.16.0"
|
|
9
11
|
end
|
|
10
12
|
end
|
data/lib/io/endpoint/wrapper.rb
CHANGED
|
@@ -6,10 +6,15 @@
|
|
|
6
6
|
require "socket"
|
|
7
7
|
|
|
8
8
|
module IO::Endpoint
|
|
9
|
+
# Represents a wrapper for socket operations that provides scheduling and configuration.
|
|
9
10
|
class Wrapper
|
|
10
11
|
include ::Socket::Constants
|
|
11
12
|
|
|
12
13
|
if Fiber.respond_to?(:scheduler)
|
|
14
|
+
# Schedule a block to run asynchronously.
|
|
15
|
+
# Uses Fiber scheduler if available, otherwise falls back to Thread.
|
|
16
|
+
# @yields { ...} The block to schedule.
|
|
17
|
+
# @returns [Fiber, Thread] The scheduled fiber or thread.
|
|
13
18
|
def schedule(&block)
|
|
14
19
|
if Fiber.scheduler
|
|
15
20
|
Fiber.schedule(&block)
|
|
@@ -18,6 +23,10 @@ module IO::Endpoint
|
|
|
18
23
|
end
|
|
19
24
|
end
|
|
20
25
|
else
|
|
26
|
+
# Schedule a block to run asynchronously.
|
|
27
|
+
# Uses Thread for scheduling.
|
|
28
|
+
# @yields { ...} The block to schedule.
|
|
29
|
+
# @returns [Thread] The scheduled thread.
|
|
21
30
|
def schedule(&block)
|
|
22
31
|
Thread.new(&block)
|
|
23
32
|
end
|
|
@@ -28,12 +37,18 @@ module IO::Endpoint
|
|
|
28
37
|
schedule(&block)
|
|
29
38
|
end
|
|
30
39
|
|
|
40
|
+
# Set the timeout for an IO object.
|
|
41
|
+
# @parameter io [IO] The IO object to set timeout on.
|
|
42
|
+
# @parameter timeout [Numeric, nil] The timeout value.
|
|
31
43
|
def set_timeout(io, timeout)
|
|
32
44
|
if io.respond_to?(:timeout=)
|
|
33
45
|
io.timeout = timeout
|
|
34
46
|
end
|
|
35
47
|
end
|
|
36
48
|
|
|
49
|
+
# Set whether a socket should be buffered.
|
|
50
|
+
# @parameter socket [Socket] The socket to configure.
|
|
51
|
+
# @parameter buffered [Boolean] Whether the socket should be buffered.
|
|
37
52
|
def set_buffered(socket, buffered)
|
|
38
53
|
case buffered
|
|
39
54
|
when true
|
|
@@ -220,6 +235,8 @@ module IO::Endpoint
|
|
|
220
235
|
|
|
221
236
|
DEFAULT = new
|
|
222
237
|
|
|
238
|
+
# Get the default wrapper instance.
|
|
239
|
+
# @returns [Wrapper] The default wrapper instance.
|
|
223
240
|
def self.default
|
|
224
241
|
DEFAULT
|
|
225
242
|
end
|
data/lib/io/endpoint.rb
CHANGED
|
@@ -7,7 +7,10 @@ require_relative "endpoint/version"
|
|
|
7
7
|
require_relative "endpoint/generic"
|
|
8
8
|
require_relative "endpoint/shared_endpoint"
|
|
9
9
|
|
|
10
|
+
# Represents a collection of endpoint classes for network I/O operations.
|
|
10
11
|
module IO::Endpoint
|
|
12
|
+
# Get the current file descriptor limit for the process.
|
|
13
|
+
# @returns [Integer] The soft limit for the number of open file descriptors.
|
|
11
14
|
def self.file_descriptor_limit
|
|
12
15
|
Process.getrlimit(Process::RLIMIT_NOFILE).first
|
|
13
16
|
end
|
data/readme.md
CHANGED
|
@@ -6,7 +6,9 @@ Provides a separation of concerns interface for IO endpoints. This allows you to
|
|
|
6
6
|
|
|
7
7
|
## Usage
|
|
8
8
|
|
|
9
|
-
Please see the [documentation](https://socketry.github.io/io-endpoint) for more
|
|
9
|
+
Please see the [project documentation](https://socketry.github.io/io-endpoint) for more details.
|
|
10
|
+
|
|
11
|
+
- [Getting Started](https://socketry.github.io/io-endpointguides/getting-started/index) - This guide explains how to get started with `io-endpoint`, a library that provides a separation of concerns interface for network I/O endpoints.
|
|
10
12
|
|
|
11
13
|
## Contributing
|
|
12
14
|
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: io-endpoint
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.16.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -36,12 +36,14 @@ cert_chain:
|
|
|
36
36
|
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
|
37
37
|
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
|
38
38
|
-----END CERTIFICATE-----
|
|
39
|
-
date:
|
|
39
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
40
40
|
dependencies: []
|
|
41
41
|
executables: []
|
|
42
42
|
extensions: []
|
|
43
43
|
extra_rdoc_files: []
|
|
44
44
|
files:
|
|
45
|
+
- context/getting-started.md
|
|
46
|
+
- context/index.yaml
|
|
45
47
|
- lib/io/endpoint.rb
|
|
46
48
|
- lib/io/endpoint/address_endpoint.rb
|
|
47
49
|
- lib/io/endpoint/bound_endpoint.rb
|
|
@@ -70,14 +72,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
70
72
|
requirements:
|
|
71
73
|
- - ">="
|
|
72
74
|
- !ruby/object:Gem::Version
|
|
73
|
-
version: '3.
|
|
75
|
+
version: '3.2'
|
|
74
76
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
77
|
requirements:
|
|
76
78
|
- - ">="
|
|
77
79
|
- !ruby/object:Gem::Version
|
|
78
80
|
version: '0'
|
|
79
81
|
requirements: []
|
|
80
|
-
rubygems_version: 3.6.
|
|
82
|
+
rubygems_version: 3.6.9
|
|
81
83
|
specification_version: 4
|
|
82
84
|
summary: Provides a separation of concerns interface for IO endpoints.
|
|
83
85
|
test_files: []
|
metadata.gz.sig
CHANGED
|
Binary file
|