io-endpoint 0.17.1 → 0.18.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a1fd8b9900456445fa95fc7de22135fc86c96c9ed05586acec98c492de11ab7
4
- data.tar.gz: b179c79964d3fba2d92753fc4494fd0fa2a88bcb825dc20561a61784adc60fec
3
+ metadata.gz: 01c66c778ce2edc823029e630d48e6240d7540fc277604e47341fba40a2a0377
4
+ data.tar.gz: 4da0e58ab7270e0edde4442049dc6e6fc6a5b818798da77383c2e99ef6fa2cfe
5
5
  SHA512:
6
- metadata.gz: 6370da98fec490d732f957fd5cf947396551bed010d88211a4e6684529d4ea930bff17753ae672c3cc8e482315f96bd2d31ee4365a77bb85476fa12da23ace4f
7
- data.tar.gz: c478357360832dd7c225979f2531025025f711fca06616b4efd2a7398d700a1c248021a5d2be6749fd9b5dc5950cd04a953ac383527c8d2cb301745ee5ac5038
6
+ metadata.gz: 2b1cf8108ad055f3e40ec41e4ee82d510c72d2ae17bd49bc895c4944ada35a9d1c45e97ea44aab6a7bc75d8bdc0f5c808009fe5012d843e6b591af1fdbef62e9
7
+ data.tar.gz: 0e0eee1759843f2c045be325730706ee85c730603172b76c252955bb2121f3c962bb4a60fbf6617e67d7ebc50ba83b0b3e304ba7d9d2c468c0fa77dd35b0bc46
checksums.yaml.gz.sig CHANGED
@@ -1,4 +1 @@
1
- M��GpČt���鯀@rd���*����^��O��������~YV�!n��OJ j�:'�=j��<;��a?
2
- �����O%bdP���� ���#��P�!�6�C�;���TIo�l�D��y�h�T����>#y<��D�~��������Lo�;����������H��:����W�g�QF�u���|��F|iCJ7q��$��j��h�;�3�
3
- 5����C��L���᳻�L`!� Q1�gN@��B4^S
4
- Wr`RO|so�~�;� j�v�ϜV*
1
+ 4y"�� K'"��k����Ә������6��'hM�����xe����+Dy�h�:��S�`��?W��K��#p,�qqNGؔw�z��"�6 M�BP��RS�x���b*��~.�l��b��/�2�R�|h!�����q0��,yW0?d�m
@@ -111,3 +111,37 @@ endpoint.connect do |socket|
111
111
  puts response
112
112
  end
113
113
  ```
114
+
115
+ ### Configuring TLS Certificate Material
116
+
117
+ Use {ruby IO::Endpoint::TLS::Configuration} to provide certificate and private key material without coupling application configuration to OpenSSL. Certificate material is supplied as PEM content rather than file paths, so it can be loaded from files, environment variables, or secret stores:
118
+
119
+ ```ruby
120
+ trust_store = IO::Endpoint::TLS::TrustStore.parse(root_certificates)
121
+ certificate_chain = IO::Endpoint::TLS::Certificates.parse(certificate_chain_bundle)
122
+
123
+ tls_configuration = IO::Endpoint::TLS::Configuration.new(
124
+ trust_store: trust_store,
125
+ certificate_chain: certificate_chain,
126
+ private_key: private_key,
127
+ )
128
+
129
+ endpoint = IO::Endpoint.ssl(
130
+ "example.com",
131
+ 443,
132
+ hostname: "example.com",
133
+ tls_configuration: tls_configuration,
134
+ )
135
+ ```
136
+
137
+ Use `TrustStore.new(certificates: certificates)` when certificates are already represented as an array of individual PEM strings. Use `TrustStore.parse(certificate_bundle)` to split a concatenated PEM bundle into that canonical representation, or `TrustStore.load("/path/to/certificates.pem")` to load and parse a bundle from a file. Options such as `system_certificates: true` are forwarded from `load` to `parse`.
138
+
139
+ The local certificate chain is an ordered array of individual PEM strings, with the leaf certificate followed by any intermediate certificates. The root certificate is normally omitted. Use `Certificates.parse(certificate_bundle)` to split a concatenated bundle while preserving that order.
140
+
141
+ Set `system_certificates: true` to include system-provided trusted certificates. System and custom certificates can be combined in the same trust store.
142
+
143
+ The SSL endpoint converts the trust store into an OpenSSL certificate store and the complete configuration into an OpenSSL context. Other endpoint implementations can consume the same trust, identity, and verification configuration using their native TLS implementation.
144
+
145
+ The OpenSSL conversion is also available directly using `IO::Endpoint::TLS::OpenSSL.build_certificate_store(trust_store)`.
146
+
147
+ For a server which requires clients to provide a trusted certificate, set `verification: :required`. Use `:peer` to verify a certificate when the peer provides one, and `:none` to explicitly disable peer verification. When a trust store is supplied and no policy is specified, peer verification is enabled by default. On client connections with a hostname, peer verification also checks that the certificate identifies that hostname.
data/context/index.yaml CHANGED
@@ -3,6 +3,8 @@
3
3
  ---
4
4
  description: Provides a separation of concerns interface for IO endpoints.
5
5
  metadata:
6
+ bug_tracker_uri: https://github.com/socketry/io-endpoint/issues
7
+ changelog_uri: https://github.com/socketry/io-endpoint/blob/main/releases.md
6
8
  documentation_uri: https://socketry.github.io/io-endpoint
7
9
  source_code_uri: https://github.com/socketry/io-endpoint.git
8
10
  files:
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2023-2025, by Samuel Williams.
4
+ # Copyright, 2023-2026, by Samuel Williams.
5
5
 
6
6
  require "socket"
7
7
 
@@ -28,6 +28,8 @@ module IO::Endpoint
28
28
  "inet:#{@address.inspect_sockaddr}"
29
29
  when Socket::AF_INET6
30
30
  "inet6:#{@address.inspect_sockaddr}"
31
+ when Socket::AF_UNIX
32
+ "unix:#{@address.unix_path}"
31
33
  else
32
34
  "address:#{@address.inspect_sockaddr}"
33
35
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2023-2025, by Samuel Williams.
4
+ # Copyright, 2023-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "address_endpoint"
7
7
 
@@ -31,7 +31,6 @@ module IO::Endpoint
31
31
  "\#<#{self.class} name=#{nodename.inspect} service=#{service.inspect} family=#{family.inspect} type=#{socktype.inspect} protocol=#{protocol.inspect} flags=#{flags.inspect}>"
32
32
  end
33
33
 
34
-
35
34
  # @attribute [Array] The host specification array.
36
35
  attr :specification
37
36
 
@@ -5,6 +5,7 @@
5
5
 
6
6
  require_relative "host_endpoint"
7
7
  require_relative "generic"
8
+ require_relative "tls/openssl"
8
9
 
9
10
  require "openssl"
10
11
 
@@ -22,77 +23,6 @@ module OpenSSL
22
23
  end
23
24
  end
24
25
 
25
- # Represents a module that forwards socket methods to the underlying IO object.
26
- module SocketForwarder
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.
30
- def close_on_exec=(value)
31
- to_io.close_on_exec = value
32
- end
33
- end
34
-
35
- unless method_defined?(:local_address)
36
- # Get the local address of the socket.
37
- # @returns [Addrinfo] The local address.
38
- def local_address
39
- to_io.local_address
40
- end
41
- end
42
-
43
- unless method_defined?(:remote_address)
44
- # Get the remote address of the socket.
45
- # @returns [Addrinfo] The remote address.
46
- def remote_address
47
- to_io.remote_address
48
- end
49
- end
50
-
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.
55
- def wait(*arguments)
56
- to_io.wait(*arguments)
57
- end
58
- end
59
-
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.
64
- def wait_readable(*arguments)
65
- to_io.wait_readable(*arguments)
66
- end
67
- end
68
-
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.
73
- def wait_writable(*arguments)
74
- to_io.wait_writable(*arguments)
75
- end
76
- end
77
-
78
- if IO.method_defined?(:timeout)
79
- unless method_defined?(:timeout)
80
- # Get the timeout for socket operations.
81
- # @returns [Numeric, nil] The timeout value.
82
- def timeout
83
- to_io.timeout
84
- end
85
- end
86
-
87
- unless method_defined?(:timeout=)
88
- # Set the timeout for socket operations.
89
- # @parameter value [Numeric, nil] The timeout value.
90
- def timeout=(value)
91
- to_io.timeout = value
92
- end
93
- end
94
- end
95
- end
96
26
  end
97
27
  end
98
28
 
@@ -102,6 +32,7 @@ module IO::Endpoint
102
32
  # Initialize a new SSL endpoint.
103
33
  # @parameter endpoint [Generic] The underlying endpoint to wrap with SSL.
104
34
  # @option ssl_context [OpenSSL::SSL::SSLContext, nil] An optional SSL context to use.
35
+ # @option tls_configuration [TLS::Configuration, nil] Transport-neutral TLS certificate and verification configuration.
105
36
  # @parameter options [Hash] Additional options including `:ssl_params` and `:hostname`.
106
37
  def initialize(endpoint, **options)
107
38
  super(**options)
@@ -150,6 +81,12 @@ module IO::Endpoint
150
81
  @options[:ssl_params]
151
82
  end
152
83
 
84
+ # Get the transport-neutral TLS configuration from options.
85
+ # @returns [TLS::Configuration, nil] The TLS configuration if specified.
86
+ def tls_configuration
87
+ @options[:tls_configuration]
88
+ end
89
+
153
90
  # Build an SSL context with configured parameters.
154
91
  # @parameter context [OpenSSL::SSL::SSLContext] An optional SSL context to configure.
155
92
  # @returns [OpenSSL::SSL::SSLContext] The configured SSL context.
@@ -158,6 +95,10 @@ module IO::Endpoint
158
95
  context.set_params(params)
159
96
  end
160
97
 
98
+ if tls_configuration = self.tls_configuration
99
+ TLS::OpenSSL.apply(context, tls_configuration, hostname: self.hostname)
100
+ end
101
+
161
102
  # context.setup
162
103
  # context.freeze
163
104
 
@@ -246,11 +187,12 @@ module IO::Endpoint
246
187
 
247
188
  # @parameter arguments
248
189
  # @parameter ssl_context [OpenSSL::SSL::SSLContext, nil]
190
+ # @parameter tls_configuration [TLS::Configuration, nil]
249
191
  # @parameter hostname [String, nil]
250
192
  # @parameter options keyword arguments passed through to {Endpoint.tcp}
251
193
  #
252
194
  # @returns [SSLEndpoint]
253
- def self.ssl(*arguments, ssl_context: nil, hostname: nil, **options)
254
- SSLEndpoint.new(self.tcp(*arguments, **options), ssl_context: ssl_context, hostname: hostname)
195
+ def self.ssl(*arguments, ssl_context: nil, tls_configuration: nil, hostname: nil, **options)
196
+ SSLEndpoint.new(self.tcp(*arguments, **options), ssl_context: ssl_context, tls_configuration: tls_configuration, hostname: hostname)
255
197
  end
256
198
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ module IO::Endpoint
7
+ # @namespace
8
+ module TLS
9
+ # Utilities for parsing PEM-encoded certificates.
10
+ module Certificates
11
+ # Matches individual certificates in a PEM bundle.
12
+ CERTIFICATE_PATTERN = /-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/m
13
+ private_constant :CERTIFICATE_PATTERN
14
+
15
+ # Parse a PEM-encoded certificate bundle into individual certificates.
16
+ # @parameter certificate_bundle [String] One or more certificates encoded as PEM.
17
+ # @returns [Array(String)] The individual PEM-encoded certificates in their original order.
18
+ # @raises [ArgumentError] If the bundle does not contain any certificates.
19
+ # @raises [TypeError] If the bundle is not a string.
20
+ def self.parse(certificate_bundle)
21
+ unless certificate_bundle.is_a?(String)
22
+ raise TypeError, "The certificate bundle must be provided as a string!"
23
+ end
24
+
25
+ certificates = certificate_bundle.scan(CERTIFICATE_PATTERN)
26
+
27
+ unless certificates.any?
28
+ raise ArgumentError, "The certificate bundle does not contain any certificates!"
29
+ end
30
+
31
+ return certificates
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require_relative "trust_store"
7
+
8
+ module IO::Endpoint
9
+ # @namespace
10
+ module TLS
11
+ # Represents transport-neutral TLS certificate and verification configuration.
12
+ class Configuration
13
+ # Initialize a TLS configuration from PEM-encoded certificate and private key material.
14
+ # @parameter trust_store [TrustStore | Nil] The trusted certificate sources.
15
+ # @parameter certificate_chain [Array(String) | Nil] The ordered local certificate chain encoded as individual PEM strings, with the leaf certificate followed by any intermediates.
16
+ # @parameter private_key [String | Nil] The private key encoded as PEM.
17
+ # @parameter verification [Symbol | Nil] The peer verification policy: `:none`, `:peer`, or `:required`. When omitted, `:peer` is used if a trust store is provided.
18
+ # @raises [ArgumentError] If the certificate chain and private key are not provided together, or the verification policy is invalid.
19
+ # @raises [TypeError] If the certificate chain or private key uses an unsupported representation.
20
+ def initialize(trust_store: nil, certificate_chain: nil, private_key: nil, verification: nil)
21
+ if certificate_chain
22
+ unless certificate_chain.is_a?(Array) && certificate_chain.all?{|certificate| certificate.is_a?(String)}
23
+ raise TypeError, "The certificate chain must be provided as an array of strings!"
24
+ end
25
+
26
+ unless certificate_chain.any?
27
+ raise ArgumentError, "The certificate chain must contain at least one certificate!"
28
+ end
29
+ end
30
+
31
+ unless private_key.nil? || private_key.is_a?(String)
32
+ raise TypeError, "The private key must be provided as a string!"
33
+ end
34
+
35
+ if certificate_chain.nil? != private_key.nil?
36
+ raise ArgumentError, "The certificate chain and private key must be provided together!"
37
+ end
38
+
39
+ verification = :peer if verification.nil? && trust_store
40
+ unless [nil, :none, :peer, :required].include?(verification)
41
+ raise ArgumentError, "Unsupported verification policy: #{verification.inspect}!"
42
+ end
43
+
44
+ @trust_store = trust_store
45
+ @certificate_chain = certificate_chain
46
+ @private_key = private_key
47
+ @verification = verification
48
+ end
49
+
50
+ # @attribute [TrustStore | Nil] The trusted certificate sources.
51
+ attr :trust_store
52
+
53
+ # @attribute [Array(String) | Nil] The ordered local certificate chain encoded as individual PEM strings, with the leaf certificate followed by any intermediates.
54
+ attr :certificate_chain
55
+
56
+ # @attribute [String | Nil] The private key encoded as PEM.
57
+ attr :private_key
58
+
59
+ # @attribute [Symbol | Nil] The peer verification policy.
60
+ attr :verification
61
+
62
+ # Whether peer certificates should be verified.
63
+ # @returns [Boolean] Whether peer verification is enabled.
64
+ def verify_peer?
65
+ return @verification == :peer || @verification == :required
66
+ end
67
+
68
+ # Get a representation of the configuration without exposing certificate or private key material.
69
+ # @returns [String] A redacted representation of the configuration.
70
+ def inspect
71
+ attributes = {
72
+ trust_store: !@trust_store.nil?,
73
+ certificate_chain: !@certificate_chain.nil?,
74
+ private_key: !@private_key.nil?,
75
+ verification: @verification,
76
+ }
77
+
78
+ return "\#<#{self.class} #{attributes.inspect}>"
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require_relative "trust_store"
7
+ require_relative "configuration"
8
+
9
+ require "openssl"
10
+
11
+ module IO::Endpoint
12
+ module TLS
13
+ # Provides OpenSSL compilation for transport-neutral TLS configuration.
14
+ module OpenSSL
15
+ # Build an OpenSSL certificate store from transport-neutral trusted certificate configuration.
16
+ # @parameter trust_store [TrustStore] The trusted certificate sources.
17
+ # @returns [OpenSSL::X509::Store] The configured OpenSSL certificate store.
18
+ def self.build_certificate_store(trust_store)
19
+ ::OpenSSL::X509::Store.new.tap do |store|
20
+ store.set_default_paths if trust_store.system_certificates?
21
+
22
+ trust_store.certificates.each do |certificate_pem|
23
+ store.add_cert(::OpenSSL::X509::Certificate.new(certificate_pem))
24
+ end
25
+ end
26
+ end
27
+
28
+ # Apply transport-neutral TLS configuration to an OpenSSL context.
29
+ # @parameter context [OpenSSL::SSL::SSLContext] The OpenSSL context to configure.
30
+ # @parameter configuration [Configuration] The transport-neutral TLS configuration.
31
+ # @parameter hostname [String | Nil] The hostname to verify for client connections.
32
+ # @returns [OpenSSL::SSL::SSLContext] The configured OpenSSL context.
33
+ def self.apply(context, configuration, hostname: nil)
34
+ if trust_store = configuration.trust_store
35
+ context.cert_store = build_certificate_store(trust_store)
36
+ end
37
+
38
+ if certificate_chain = configuration.certificate_chain
39
+ certificates = certificate_chain.map do |certificate|
40
+ ::OpenSSL::X509::Certificate.new(certificate)
41
+ end
42
+
43
+ context.cert = certificates.shift
44
+ context.extra_chain_cert = certificates
45
+ context.key = ::OpenSSL::PKey.read(configuration.private_key)
46
+ end
47
+
48
+ case configuration.verification
49
+ when :none
50
+ context.verify_mode = ::OpenSSL::SSL::VERIFY_NONE
51
+ context.verify_hostname = false
52
+ when :peer
53
+ context.verify_mode = ::OpenSSL::SSL::VERIFY_PEER
54
+ when :required
55
+ context.verify_mode = ::OpenSSL::SSL::VERIFY_PEER | ::OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
56
+ end
57
+
58
+ if hostname && configuration.verify_peer?
59
+ context.verify_hostname = true
60
+ end
61
+
62
+ return context
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require_relative "certificates"
7
+
8
+ module IO::Endpoint
9
+ # @namespace
10
+ module TLS
11
+ # Represents transport-neutral trusted certificate sources.
12
+ class TrustStore
13
+ # Load a PEM-encoded certificate bundle from the given path.
14
+ # @parameter path [String | Interface(:to_path)] The path to the certificate bundle.
15
+ # @parameter options [Hash] Options forwarded to {.parse}.
16
+ # @returns [TrustStore] The loaded trust store.
17
+ def self.load(path, **options)
18
+ return parse(File.read(path), **options)
19
+ end
20
+
21
+ # Parse a PEM-encoded certificate bundle into a trust store.
22
+ # @parameter certificate_bundle [String] One or more trusted certificates encoded as PEM.
23
+ # @parameter system_certificates [Boolean] Whether system-provided trusted certificates should be included.
24
+ # @returns [TrustStore] The parsed trust store.
25
+ # @raises [ArgumentError] If the bundle does not contain any certificates.
26
+ # @raises [TypeError] If the bundle is not a string.
27
+ def self.parse(certificate_bundle, system_certificates: false)
28
+ return self.new(certificates: Certificates.parse(certificate_bundle), system_certificates: system_certificates)
29
+ end
30
+
31
+ # Initialize a trust store from PEM-encoded trusted certificates.
32
+ # @parameter certificates [Array(String)] The trusted certificates encoded as PEM.
33
+ # @parameter system_certificates [Boolean] Whether system-provided trusted certificates should be included.
34
+ # @raises [ArgumentError] If no source of trusted certificates is specified.
35
+ # @raises [TypeError] If certificates are not provided as strings.
36
+ def initialize(certificates: [], system_certificates: false)
37
+ unless certificates.is_a?(Array) && certificates.all?{|certificate| certificate.is_a?(String)}
38
+ raise TypeError, "Certificates must be provided as an array of strings!"
39
+ end
40
+
41
+ unless certificates.any? || system_certificates
42
+ raise ArgumentError, "At least one source of trusted certificates must be specified!"
43
+ end
44
+
45
+ @certificates = certificates
46
+ @system_certificates = system_certificates
47
+ end
48
+
49
+ # @attribute [Array(String)] The individual trusted certificates encoded as PEM.
50
+ attr :certificates
51
+
52
+ # Whether system-provided trusted certificates should be included.
53
+ # @returns [Boolean] `true` if system-provided trusted certificates should be included.
54
+ def system_certificates?
55
+ @system_certificates
56
+ end
57
+
58
+ # Get a representation of the trust store without exposing certificate material.
59
+ # @returns [String] A redacted representation of the trust store.
60
+ def inspect
61
+ attributes = {
62
+ certificates: @certificates.size,
63
+ system_certificates: @system_certificates,
64
+ }
65
+
66
+ return "\#<#{self.class} #{attributes.inspect}>"
67
+ end
68
+ end
69
+ end
70
+ end
@@ -1,22 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2023-2025, by Samuel Williams.
4
+ # Copyright, 2023-2026, by Samuel Williams.
5
+ # Copyright, 2026, by Delton Ding.
6
+
7
+ require "digest"
8
+ require "fileutils"
9
+ require "tmpdir"
5
10
 
6
11
  require_relative "address_endpoint"
7
12
 
8
13
  module IO::Endpoint
9
14
  # This class doesn't exert ownership over the specified unix socket and ensures exclusive access by using `flock` where possible.
10
15
  class UNIXEndpoint < AddressEndpoint
16
+ # Compute a stable temporary UNIX socket path for an overlong path.
17
+ # @parameter path [String] The original (possibly overlong) path.
18
+ # @returns [String] A short, stable path suitable for {Address.unix}.
19
+ def self.short_path_for(path)
20
+ # We need to ensure the path is absolute and canonical, otherwise the SHA1 hash will not be consistent:
21
+ path = File.expand_path(path)
22
+
23
+ # We then use the SHA1 hash of the path to create a short, stable path:
24
+ File.join(Dir.tmpdir, Digest::SHA1.hexdigest(path) + ".ipc")
25
+ end
26
+
11
27
  # Initialize a new UNIX domain socket endpoint.
12
28
  # @parameter path [String] The path to the UNIX socket.
13
29
  # @parameter type [Integer] The socket type (defaults to Socket::SOCK_STREAM).
14
30
  # @parameter options [Hash] Additional options to pass to the parent class.
15
31
  def initialize(path, type = Socket::SOCK_STREAM, **options)
16
- # I wonder if we should implement chdir behaviour in here if path is longer than 104 characters.
17
- super(Address.unix(path, type), **options)
18
-
19
32
  @path = path
33
+
34
+ begin
35
+ address = Address.unix(path, type)
36
+ rescue ArgumentError
37
+ path = self.class.short_path_for(path)
38
+ address = Address.unix(path, type)
39
+ end
40
+
41
+ super(address, **options)
20
42
  end
21
43
 
22
44
  # Get a string representation of the UNIX endpoint.
@@ -28,11 +50,28 @@ module IO::Endpoint
28
50
  # Get a detailed string representation of the UNIX endpoint.
29
51
  # @returns [String] A detailed string representation including the path.
30
52
  def inspect
31
- "\#<#{self.class} path=#{@path.inspect}>"
53
+ target_path = @address.unix_path
54
+
55
+ if @path == target_path
56
+ "\#<#{self.class} path=#{@path.inspect}>"
57
+ else
58
+ "\#<#{self.class} path=#{@path.inspect} target=#{target_path.inspect}>"
59
+ end
32
60
  end
33
61
 
34
62
  # @attribute [String] The path to the UNIX socket.
35
- attr :path
63
+ def path
64
+ @path
65
+ end
66
+
67
+ # Check if a symlink is used for this endpoint.
68
+ #
69
+ # A symlink is created when the original path exceeds the system's maximum UNIX socket path length and a shorter temporary path is used for the actual socket.
70
+ #
71
+ # @returns [Boolean] True if the original path differs from the socket path, indicating a symlink is required.
72
+ def symlink?
73
+ File.symlink?(@path)
74
+ end
36
75
 
37
76
  # Check if the socket is currently bound and accepting connections.
38
77
  # @returns [Boolean] True if the socket is bound and accepting connections, false otherwise.
@@ -52,16 +91,61 @@ module IO::Endpoint
52
91
  # @returns [Array(Socket)] The bound socket.
53
92
  # @raises [Errno::EADDRINUSE] If the socket is still in use by another process.
54
93
  def bind(...)
55
- super
94
+ result = super
95
+ create_symlink_if_required!
96
+ return result
56
97
  rescue Errno::EADDRINUSE
57
98
  # 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.
58
99
  if !bound?
59
- File.unlink(@path) rescue nil
100
+ unlink_stale_paths!
60
101
  retry
61
102
  else
62
103
  raise
63
104
  end
64
105
  end
106
+
107
+ # Read a symlink, returning nil if the file does not exist.
108
+ #
109
+ # @parameter path [String] The path to the symlink.
110
+ # @returns [String | Nil] The target of the symlink, or nil if the file does not exist.
111
+ private def read_link(path)
112
+ File.readlink(path)
113
+ rescue # Errno::ENOENT, Errno::EINVAL
114
+ # The file is not a symlink, or the symlink is invalid.
115
+ nil
116
+ end
117
+
118
+ # Create a symlink to the actual socket path if required.
119
+ private def create_symlink_if_required!
120
+ # Ensure the directory exists:
121
+ FileUtils.mkdir_p(File.dirname(@path))
122
+
123
+ # This is the actual path we want to use for the socket:
124
+ target_path = @address.unix_path
125
+
126
+ # If it's the same as the original path, we are done:
127
+ return if @path == target_path
128
+
129
+ # Otherwise, we need might need to create a symlink:
130
+ if read_link(target_path) == @path
131
+ return
132
+ else
133
+ File.unlink(@path) rescue nil
134
+ end
135
+
136
+ # Create symlink at @path (original long path) pointing to target_path (short socket path)
137
+ File.symlink(target_path, @path)
138
+ end
139
+
140
+ private def unlink_stale_paths!
141
+ File.unlink(@path) rescue nil
142
+
143
+ target_path = @address.unix_path
144
+
145
+ if @path != target_path
146
+ File.unlink(target_path) rescue nil
147
+ end
148
+ end
65
149
  end
66
150
 
67
151
  # @parameter path [String]
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2023-2025, by Samuel Williams.
4
+ # Copyright, 2023-2026, by Samuel Williams.
5
5
 
6
6
  # @namespace
7
7
  class IO
8
8
  # @namespace
9
9
  module Endpoint
10
- VERSION = "0.17.1"
10
+ VERSION = "0.18.0"
11
11
  end
12
12
  end
data/lib/io/endpoint.rb CHANGED
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2023-2025, by Samuel Williams.
4
+ # Copyright, 2023-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "endpoint/version"
7
7
  require_relative "endpoint/generic"
8
8
  require_relative "endpoint/shared_endpoint"
9
+ require_relative "endpoint/tls/certificates"
10
+ require_relative "endpoint/tls/trust_store"
11
+ require_relative "endpoint/tls/configuration"
9
12
 
10
13
  # Represents a collection of endpoint classes for network I/O operations.
11
14
  module IO::Endpoint
data/license.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # MIT License
2
2
 
3
3
  Copyright, 2023-2026, by Samuel Williams.
4
+ Copyright, 2026, by Delton Ding.
4
5
 
5
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
7
  of this software and associated documentation files (the "Software"), to deal
data/readme.md CHANGED
@@ -16,6 +16,16 @@ Please see the [project documentation](https://socketry.github.io/io-endpoint) f
16
16
 
17
17
  Please see the [project releases](https://socketry.github.io/io-endpointreleases/index) for all releases.
18
18
 
19
+ ### v0.18.0
20
+
21
+ - The `openssl` gem 3.3.0 or newer is now required.
22
+ - Added transport-neutral TLS trust stores, certificate chains and configuration, suitable for future QUIC integration.
23
+ - Added OpenSSL conversion and SSL endpoint integration, including hostname verification and mutual TLS.
24
+
25
+ ### v0.17.2
26
+
27
+ - When the unix path is bigger than what can fit into `struct sockaddr_un`, a shorter temporary path will be used instead and a symlink created at the original path.
28
+
19
29
  ### v0.17.1
20
30
 
21
31
  - Add `#to_s` and `#inspect` for `IO::Endpoint::NamedEndpoints`.
@@ -50,14 +60,6 @@ Please see the [project releases](https://socketry.github.io/io-endpointreleases
50
60
 
51
61
  - Fixed state leak between iterations of the accept loop.
52
62
 
53
- ### v0.13.0
54
-
55
- - Propagate options assigned to composite endpoint to nested endpoints.
56
-
57
- ### v0.12.0
58
-
59
- - Expose `size` and internal endpoints for composite endpoint.
60
-
61
63
  ## See Also
62
64
 
63
65
  - [async-io](https://github.com/socketry/async-io) — Where this implementation originally came from.
@@ -66,11 +68,27 @@ Please see the [project releases](https://socketry.github.io/io-endpointreleases
66
68
 
67
69
  We welcome contributions to this project.
68
70
 
69
- 1. Fork it.
71
+ 1. Fork the repository.
70
72
  2. Create your feature branch (`git checkout -b my-new-feature`).
71
- 3. Commit your changes (`git commit -am 'Add some feature'`).
73
+ 3. Commit your changes (`git commit -am 'Add some feature.'`).
72
74
  4. Push to the branch (`git push origin my-new-feature`).
73
- 5. Create new Pull Request.
75
+ 5. Create a new pull request.
76
+
77
+ ### Running Tests
78
+
79
+ To run the test suite:
80
+
81
+ ``` bash
82
+ $ bundle exec sus
83
+ ```
84
+
85
+ ### Making Releases
86
+
87
+ To make a new release:
88
+
89
+ ``` bash
90
+ $ bundle exec bake gem:release:patch # or minor or major
91
+ ```
74
92
 
75
93
  ### Developer Certificate of Origin
76
94
 
data/releases.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Releases
2
2
 
3
+ ## v0.18.0
4
+
5
+ - The `openssl` gem 3.3.0 or newer is now required.
6
+ - Added transport-neutral TLS trust stores, certificate chains and configuration, suitable for future QUIC integration.
7
+ - Added OpenSSL conversion and SSL endpoint integration, including hostname verification and mutual TLS.
8
+
9
+ ## v0.17.2
10
+
11
+ - When the unix path is bigger than what can fit into `struct sockaddr_un`, a shorter temporary path will be used instead and a symlink created at the original path.
12
+
3
13
  ## v0.17.1
4
14
 
5
15
  - Add `#to_s` and `#inspect` for `IO::Endpoint::NamedEndpoints`.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,10 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: io-endpoint
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.1
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
+ - Delton Ding
8
9
  bindir: bin
9
10
  cert_chain:
10
11
  - |
@@ -37,7 +38,21 @@ cert_chain:
37
38
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
38
39
  -----END CERTIFICATE-----
39
40
  date: 1980-01-02 00:00:00.000000000 Z
40
- dependencies: []
41
+ dependencies:
42
+ - !ruby/object:Gem::Dependency
43
+ name: openssl
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 3.3.0
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 3.3.0
41
56
  executables: []
42
57
  extensions: []
43
58
  extra_rdoc_files: []
@@ -56,6 +71,10 @@ files:
56
71
  - lib/io/endpoint/shared_endpoint.rb
57
72
  - lib/io/endpoint/socket_endpoint.rb
58
73
  - lib/io/endpoint/ssl_endpoint.rb
74
+ - lib/io/endpoint/tls/certificates.rb
75
+ - lib/io/endpoint/tls/configuration.rb
76
+ - lib/io/endpoint/tls/openssl.rb
77
+ - lib/io/endpoint/tls/trust_store.rb
59
78
  - lib/io/endpoint/unix_endpoint.rb
60
79
  - lib/io/endpoint/version.rb
61
80
  - lib/io/endpoint/wrapper.rb
@@ -66,6 +85,8 @@ homepage: https://github.com/socketry/io-endpoint
66
85
  licenses:
67
86
  - MIT
68
87
  metadata:
88
+ bug_tracker_uri: https://github.com/socketry/io-endpoint/issues
89
+ changelog_uri: https://github.com/socketry/io-endpoint/blob/main/releases.md
69
90
  documentation_uri: https://socketry.github.io/io-endpoint
70
91
  source_code_uri: https://github.com/socketry/io-endpoint.git
71
92
  rdoc_options: []
@@ -75,14 +96,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
75
96
  requirements:
76
97
  - - ">="
77
98
  - !ruby/object:Gem::Version
78
- version: '3.2'
99
+ version: '3.3'
79
100
  required_rubygems_version: !ruby/object:Gem::Requirement
80
101
  requirements:
81
102
  - - ">="
82
103
  - !ruby/object:Gem::Version
83
104
  version: '0'
84
105
  requirements: []
85
- rubygems_version: 4.0.3
106
+ rubygems_version: 4.0.10
86
107
  specification_version: 4
87
108
  summary: Provides a separation of concerns interface for IO endpoints.
88
109
  test_files: []
metadata.gz.sig CHANGED
Binary file