io-endpoint-0.17.2/0000755000004100000410000000000015150651645014066 5ustar www-datawww-dataio-endpoint-0.17.2/lib/0000755000004100000410000000000015150651645014634 5ustar www-datawww-dataio-endpoint-0.17.2/lib/io/0000755000004100000410000000000015150651645015243 5ustar www-datawww-dataio-endpoint-0.17.2/lib/io/endpoint/0000755000004100000410000000000015150651645017063 5ustar www-datawww-dataio-endpoint-0.17.2/lib/io/endpoint/bound_endpoint.rb0000644000004100000410000000654115150651645022425 0ustar www-datawww-data# frozen_string_literal: true # Released under the MIT License. # Copyright, 2024-2025, by Samuel Williams. require_relative "generic" require_relative "composite_endpoint" require_relative "address_endpoint" module IO::Endpoint # Represents an endpoint that has been bound to one or more sockets. class BoundEndpoint < Generic # Create a bound endpoint from an existing endpoint. # @parameter endpoint [Generic] The endpoint to bind. # @option backlog [Integer] The maximum length of the queue of pending connections. # @option close_on_exec [Boolean] Whether to close sockets on exec. # @returns [BoundEndpoint] A new bound endpoint instance. def self.bound(endpoint, backlog: Socket::SOMAXCONN, close_on_exec: false) sockets = endpoint.bind sockets.each do |socket| socket.close_on_exec = close_on_exec end return self.new(endpoint, sockets, **endpoint.options) end # Initialize a new bound endpoint. # @parameter endpoint [Generic] The original endpoint that was bound. # @parameter sockets [Array(Socket)] The sockets that were bound. # @parameter options [Hash] Additional options to pass to the parent class. def initialize(endpoint, sockets, **options) super(**options) @endpoint = endpoint @sockets = sockets end # @attribute [Generic] The original endpoint that was bound. attr :endpoint # @attribute [Array(Socket)] The sockets that were bound. attr :sockets # A endpoint for the local end of the bound socket. # @returns [CompositeEndpoint] A composite endpoint for the local end of the bound socket. def local_address_endpoint(**options) endpoints = @sockets.map do |socket| AddressEndpoint.new(socket.to_io.local_address, **options) end return CompositeEndpoint.new(endpoints) end # A endpoint for the remote end of the bound socket. # @returns [CompositeEndpoint] A composite endpoint for the remote end of the bound socket. def remote_address_endpoint(**options) endpoints = @sockets.map do |wrapper| AddressEndpoint.new(socket.to_io.remote_address, **options) end return CompositeEndpoint.new(endpoints) end # Close all bound sockets. def close @sockets.each(&:close) @sockets.clear end # Get a string representation of the bound endpoint. # @returns [String] A string representation of the bound endpoint. def to_s "bound:#{@endpoint}" end # Get a detailed string representation of the bound endpoint. # @returns [String] A detailed string representation including socket count. def inspect "\#<#{self.class} #{@sockets.size} bound sockets for #{@endpoint}>" end # Bind the endpoint using the given wrapper. # @parameter wrapper [Wrapper] The wrapper to use for binding. # @yields {|socket| ...} If a block is given, yields each bound socket. # @parameter socket [Socket] A bound socket. # @returns [Array(Socket)] An array of bound sockets. def bind(wrapper = self.wrapper, &block) @sockets.map do |server| if block_given? wrapper.schedule do yield server end else server.dup end end end end class Generic # Create a bound endpoint from this endpoint. # @parameter options [Hash] Options to pass to {BoundEndpoint.bound}. # @returns [BoundEndpoint] A new bound endpoint instance. def bound(**options) BoundEndpoint.bound(self, **options) end end end io-endpoint-0.17.2/lib/io/endpoint/shared_endpoint.rb0000644000004100000410000000026615150651645022562 0ustar www-datawww-data# frozen_string_literal: true # Released under the MIT License. # Copyright, 2023-2025, by Samuel Williams. require_relative "bound_endpoint" require_relative "connected_endpoint" io-endpoint-0.17.2/lib/io/endpoint/unix_endpoint.rb0000644000004100000410000001203415150651645022273 0ustar www-datawww-data# frozen_string_literal: true # Released under the MIT License. # Copyright, 2023-2026, by Samuel Williams. # Copyright, 2026, by Delton Ding. require "digest" require "fileutils" require "tmpdir" require_relative "address_endpoint" module IO::Endpoint # This class doesn't exert ownership over the specified unix socket and ensures exclusive access by using `flock` where possible. class UNIXEndpoint < AddressEndpoint # Compute a stable temporary UNIX socket path for an overlong path. # @parameter path [String] The original (possibly overlong) path. # @returns [String] A short, stable path suitable for {Address.unix}. def self.short_path_for(path) # We need to ensure the path is absolute and canonical, otherwise the SHA1 hash will not be consistent: path = File.expand_path(path) # We then use the SHA1 hash of the path to create a short, stable path: File.join(Dir.tmpdir, Digest::SHA1.hexdigest(path) + ".ipc") end # Initialize a new UNIX domain socket endpoint. # @parameter path [String] The path to the UNIX socket. # @parameter type [Integer] The socket type (defaults to Socket::SOCK_STREAM). # @parameter options [Hash] Additional options to pass to the parent class. def initialize(path, type = Socket::SOCK_STREAM, **options) @path = path begin address = Address.unix(path, type) rescue ArgumentError path = self.class.short_path_for(path) address = Address.unix(path, type) end super(address, **options) end # Get a string representation of the UNIX endpoint. # @returns [String] A string representation showing the socket path. def to_s "unix:#{@path}" end # Get a detailed string representation of the UNIX endpoint. # @returns [String] A detailed string representation including the path. def inspect target_path = @address.unix_path if @path == target_path "\#<#{self.class} path=#{@path.inspect}>" else "\#<#{self.class} path=#{@path.inspect} target=#{target_path.inspect}>" end end # @attribute [String] The path to the UNIX socket. def path @path end # Check if a symlink is used for this endpoint. # # 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. # # @returns [Boolean] True if the original path differs from the socket path, indicating a symlink is required. def symlink? File.symlink?(@path) end # Check if the socket is currently bound and accepting connections. # @returns [Boolean] True if the socket is bound and accepting connections, false otherwise. def bound? self.connect do return true end rescue Errno::ECONNREFUSED return false rescue Errno::ENOENT return false end # Bind the UNIX socket, handling stale socket files. # @yields {|socket| ...} If a block is given, yields the bound socket. # @parameter socket [Socket] The bound socket. # @returns [Array(Socket)] The bound socket. # @raises [Errno::EADDRINUSE] If the socket is still in use by another process. def bind(...) result = super create_symlink_if_required! return result rescue Errno::EADDRINUSE # 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. if !bound? unlink_stale_paths! retry else raise end end # Read a symlink, returning nil if the file does not exist. # # @parameter path [String] The path to the symlink. # @returns [String | Nil] The target of the symlink, or nil if the file does not exist. private def read_link(path) File.readlink(path) rescue # Errno::ENOENT, Errno::EINVAL # The file is not a symlink, or the symlink is invalid. nil end # Create a symlink to the actual socket path if required. private def create_symlink_if_required! # Ensure the directory exists: FileUtils.mkdir_p(File.dirname(@path)) # This is the actual path we want to use for the socket: target_path = @address.unix_path # If it's the same as the original path, we are done: return if @path == target_path # Otherwise, we need might need to create a symlink: if read_link(target_path) == @path return else File.unlink(@path) rescue nil end # Create symlink at @path (original long path) pointing to target_path (short socket path) File.symlink(target_path, @path) end private def unlink_stale_paths! File.unlink(@path) rescue nil target_path = @address.unix_path if @path != target_path File.unlink(target_path) rescue nil end end end # @parameter path [String] # @parameter type Socket type # @parameter options keyword arguments passed through to {UNIXEndpoint#initialize} # # @returns [UNIXEndpoint] def self.unix(path = "", type = ::Socket::SOCK_STREAM, **options) UNIXEndpoint.new(path, type, **options) end end io-endpoint-0.17.2/lib/io/endpoint/address_endpoint.rb0000644000004100000410000000365515150651645022746 0ustar www-datawww-data# frozen_string_literal: true # Released under the MIT License. # Copyright, 2023-2026, by Samuel Williams. require "socket" require_relative "generic" require_relative "wrapper" module IO::Endpoint # Represents an endpoint for a specific network address. class AddressEndpoint < Generic # Initialize a new address endpoint. # @parameter address [Address] The network address for this endpoint. # @parameter options [Hash] Additional options to pass to the parent class. def initialize(address, **options) super(**options) @address = address end # Get a string representation of the endpoint. # @returns [String] A string representation of the endpoint address. def to_s case @address.afamily when Socket::AF_INET "inet:#{@address.inspect_sockaddr}" when Socket::AF_INET6 "inet6:#{@address.inspect_sockaddr}" when Socket::AF_UNIX "unix:#{@address.unix_path}" else "address:#{@address.inspect_sockaddr}" end end # Get a detailed string representation of the endpoint. # @returns [String] A detailed string representation including the address. def inspect "\#<#{self.class} address=#{@address.inspect}>" end # @attribute [Address] The network address for this endpoint. attr :address # Bind a socket to the given address. If a block is given, the socket will be automatically closed when the block exits. # @yields {|socket| ...} If a block is given, yields the bound socket. # @parameter socket [Socket] The socket which has been bound. # @returns [Array(Socket)] the bound socket def bind(wrapper = self.wrapper, &block) [wrapper.bind(@address, **@options, &block)] end # Connects a socket to the given address. If a block is given, the socket will be automatically closed when the block exits. # @returns [Socket] the connected socket def connect(wrapper = self.wrapper, &block) wrapper.connect(@address, **@options, &block) end end end io-endpoint-0.17.2/lib/io/endpoint/connected_endpoint.rb0000644000004100000410000000612015150651645023251 0ustar www-datawww-data# frozen_string_literal: true # Released under the MIT License. # Copyright, 2024-2025, by Samuel Williams. require_relative "generic" require_relative "composite_endpoint" require_relative "socket_endpoint" require "openssl" module IO::Endpoint # Represents an endpoint that has been connected to a socket. class ConnectedEndpoint < Generic # Create a connected endpoint from an existing endpoint. # @parameter endpoint [Generic] The endpoint to connect. # @option close_on_exec [Boolean] Whether to close the socket on exec. # @returns [ConnectedEndpoint] A new connected endpoint instance. def self.connected(endpoint, close_on_exec: false) socket = endpoint.connect socket.close_on_exec = close_on_exec return self.new(endpoint, socket, **endpoint.options) end # Initialize a new connected endpoint. # @parameter endpoint [Generic] The original endpoint that was connected. # @parameter socket [Socket] The socket that was connected. # @parameter options [Hash] Additional options to pass to the parent class. def initialize(endpoint, socket, **options) super(**options) @endpoint = endpoint @socket = socket end # @attribute [Generic] The original endpoint that was connected. attr :endpoint # @attribute [Socket] The socket that was connected. attr :socket # A endpoint for the local end of the bound socket. # @returns [AddressEndpoint] A endpoint for the local end of the connected socket. def local_address_endpoint(**options) AddressEndpoint.new(socket.to_io.local_address, **options) end # A endpoint for the remote end of the bound socket. # @returns [AddressEndpoint] A endpoint for the remote end of the connected socket. def remote_address_endpoint(**options) AddressEndpoint.new(socket.to_io.remote_address, **options) end # Connect using the already connected socket. # @parameter wrapper [Wrapper] The wrapper to use (unused, socket is already connected). # @yields {|socket| ...} If a block is given, yields the connected socket. # @parameter socket [Socket] The connected socket. # @returns [Socket] The connected socket or a duplicate if no block is given. def connect(wrapper = self.wrapper, &block) if block_given? yield @socket else return @socket.dup end end # Close the connected socket. def close if @socket @socket.close @socket = nil end end # Get a string representation of the connected endpoint. # @returns [String] A string representation of the connected endpoint. def to_s "connected:#{@endpoint}" end # Get a detailed string representation of the connected endpoint. # @returns [String] A detailed string representation including the socket. def inspect "\#<#{self.class} #{@socket} connected for #{@endpoint}>" end end class Generic # Create a connected endpoint from this endpoint. # @parameter options [Hash] Options to pass to {ConnectedEndpoint.connected}. # @returns [ConnectedEndpoint] A new connected endpoint instance. def connected(**options) ConnectedEndpoint.connected(self, **options) end end end io-endpoint-0.17.2/lib/io/endpoint/generic.rb0000644000004100000410000001077415150651645021035 0ustar www-datawww-data# frozen_string_literal: true # Released under the MIT License. # Copyright, 2023-2025, by Samuel Williams. # require_relative 'address' require "uri" require "socket" module IO::Endpoint Address = Addrinfo # Endpoints represent a way of connecting or binding to an address. class Generic # Initialize a new generic endpoint. # @parameter options [Hash] Configuration options for the endpoint. def initialize(**options) @options = options.freeze end # Create a new endpoint with merged options. # @parameter options [Hash] Additional options to merge with existing options. # @returns [Generic] A new endpoint instance with merged options. def with(**options) dup = self.dup dup.options = @options.merge(options) return dup end attr_accessor :options # @returns [String] The hostname of the bound socket. def hostname @options[:hostname] end # 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. # @returns [Boolean, nil] The value for `SO_REUSEPORT`. def reuse_port? @options[:reuse_port] end # 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. # @returns [Boolean] The value for `SO_REUSEADDR`. def reuse_address? @options[:reuse_address] end # Controls SO_LINGER. The amount of time the socket will stay in the `TIME_WAIT` state after being closed. # @returns [Integer, nil] The value for SO_LINGER. def linger @options[:linger] end # @returns [Numeric] The default timeout for socket operations. def timeout @options[:timeout] end # @returns [Address] the address to bind to before connecting. def local_address @options[:local_address] end # Bind a socket to the given address. If a block is given, the socket will be automatically closed when the block exits. # @parameter wrapper [Wrapper] The wrapper to use for binding. # @yields {|socket| ...} If a block is given, yields the bound socket. # @parameter socket [Socket] The socket which has been bound. # @returns [Array(Socket)] the bound socket def bind(wrapper = self.wrapper, &block) raise NotImplementedError end # Connects a socket to the given address. If a block is given, the socket will be automatically closed when the block exits. # @parameter wrapper [Wrapper] The wrapper to use for connecting. # @returns [Socket] the connected socket def connect(wrapper = self.wrapper, &block) raise NotImplementedError end # Bind and accept connections on the given address. # @parameter wrapper [Wrapper] The wrapper to use for accepting connections. # @yields {|socket| ...} For each accepted connection, yields the socket. # @parameter socket [Socket] The accepted socket. def accept(wrapper = self.wrapper, &block) bind(wrapper) do |server| wrapper.accept(server, **@options, &block) end end # Enumerate all discrete paths as endpoints. # @yields {|endpoint| ...} For each endpoint, yields it. # @parameter endpoint [Endpoint] The endpoint. def each return to_enum unless block_given? yield self end # 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. # # You should not use untrusted input as it may execute arbitrary code. # # @parameter string [String] URI as string. Scheme will decide implementation used. # @parameter options keyword arguments passed through to {#initialize} # # @see Endpoint.ssl ssl - invoked when parsing a URL with the ssl scheme "ssl://127.0.0.1" # @see Endpoint.tcp tcp - invoked when parsing a URL with the tcp scheme: "tcp://127.0.0.1" # @see Endpoint.udp udp - invoked when parsing a URL with the udp scheme: "udp://127.0.0.1" # @see Endpoint.unix unix - invoked when parsing a URL with the unix scheme: "unix://127.0.0.1" def self.parse(string, **options) uri = URI.parse(string) IO::Endpoint.public_send(uri.scheme, uri.host, uri.port, **options) end # The default wrapper to use for binding, connecting, and accepting connections. def wrapper @options[:wrapper] || Wrapper.default end end end io-endpoint-0.17.2/lib/io/endpoint/socket_endpoint.rb0000644000004100000410000000350715150651645022605 0ustar www-datawww-data# frozen_string_literal: true # Released under the MIT License. # Copyright, 2023-2025, by Samuel Williams. require_relative "generic" module IO::Endpoint # This class doesn't exert ownership over the specified socket, wraps a native ::IO. class SocketEndpoint < Generic # Initialize a new socket endpoint. # @parameter socket [Socket] The socket to wrap. # @parameter options [Hash] Additional options to pass to the parent class. def initialize(socket, **options) super(**options) @socket = socket end # Get a string representation of the socket endpoint. # @returns [String] A string representation showing the socket. def to_s "socket:#{@socket}" end # Get a detailed string representation of the socket endpoint. # @returns [String] A detailed string representation including the socket. def inspect "\#<#{self.class} #{@socket.inspect}>" end # @attribute [Socket] The wrapped socket. attr :socket # Bind using the wrapped socket. # @yields {|socket| ...} If a block is given, yields the socket. # @parameter socket [Socket] The socket. # @returns [Socket] The socket. def bind(&block) if block_given? yield @socket else return @socket end end # Connect using the wrapped socket. # @yields {|socket| ...} If a block is given, yields the socket. # @parameter socket [Socket] The socket. # @returns [Socket] The socket. def connect(&block) if block_given? yield @socket else return @socket end end end # Create a socket endpoint from an existing socket. # @parameter socket [Socket] The socket to wrap. # @parameter options [Hash] Additional options to pass to the socket endpoint. # @returns [SocketEndpoint] A new socket endpoint instance. def self.socket(socket, **options) SocketEndpoint.new(socket, **options) end end io-endpoint-0.17.2/lib/io/endpoint/version.rb0000644000004100000410000000030115150651645021067 0ustar www-datawww-data# frozen_string_literal: true # Released under the MIT License. # Copyright, 2023-2026, by Samuel Williams. # @namespace class IO # @namespace module Endpoint VERSION = "0.17.2" end end io-endpoint-0.17.2/lib/io/endpoint/composite_endpoint.rb0000644000004100000410000000670615150651645023323 0ustar www-datawww-data# frozen_string_literal: true # Released under the MIT License. # Copyright, 2023-2025, by Samuel Williams. require_relative "generic" module IO::Endpoint # A composite endpoint is a collection of endpoints that are used in order. class CompositeEndpoint < Generic # Initialize a new composite endpoint. # @parameter endpoints [Array(Generic)] The endpoints to compose. # @parameter options [Hash] Additional options to pass to the parent class and propagate to endpoints. def initialize(endpoints, **options) super(**options) # If any options were provided, propagate them to the endpoints: if options.any? endpoints = endpoints.map{|endpoint| endpoint.with(**options)} end @endpoints = endpoints end # Get a string representation of the composite endpoint. # @returns [String] A string representation listing all endpoints. def to_s "composite:#{@endpoints.join(",")}" end # Get a detailed string representation of the composite endpoint. # @returns [String] A detailed string representation including all endpoints. def inspect "\#<#{self.class} endpoints=#{@endpoints}>" end # Create a new composite endpoint with merged options. # @parameter options [Hash] Additional options to merge with existing options. # @returns [CompositeEndpoint] A new composite endpoint instance with merged options. def with(**options) self.class.new(endpoints.map{|endpoint| endpoint.with(**options)}, **@options.merge(options)) end # @attribute [Array(Generic)] The endpoints in this composite endpoint. attr :endpoints # The number of endpoints in the composite endpoint. def size @endpoints.size end # Enumerate all endpoints in the composite endpoint. # @yields {|endpoint| ...} For each endpoint in the composite, yields it. # @parameter endpoint [Generic] An endpoint in the composite. def each(&block) @endpoints.each do |endpoint| endpoint.each(&block) end end # Connect to the first endpoint that succeeds. # @parameter wrapper [Wrapper] The wrapper to use for connecting. # @yields {|socket| ...} If a block is given, yields the connected socket from the first successful endpoint. # @parameter socket [Socket] The connected socket. # @returns [Socket] The connected socket. # @raises [Exception] If all endpoints fail to connect, raises the last error encountered. def connect(wrapper = self.wrapper, &block) last_error = nil @endpoints.each do |endpoint| begin return endpoint.connect(wrapper, &block) rescue => last_error end end raise last_error end # Bind all endpoints in the composite. # @parameter wrapper [Wrapper] The wrapper to use for binding. # @yields {|socket| ...} For each endpoint that is bound, yields the bound socket. # @parameter socket [Socket] A bound socket. # @returns [Array(Socket)] An array of bound sockets if no block is given. def bind(wrapper = self.wrapper, &block) if block_given? @endpoints.each do |endpoint| endpoint.bind(&block) end else @endpoints.map(&:bind).flatten.compact end end end # Create a composite endpoint from multiple endpoints. # @parameter endpoints [Array(Generic)] The endpoints to compose. # @parameter options [Hash] Additional options to pass to the composite endpoint. # @returns [CompositeEndpoint] A new composite endpoint instance. def self.composite(*endpoints, **options) CompositeEndpoint.new(endpoints, **options) end end io-endpoint-0.17.2/lib/io/endpoint/named_endpoints.rb0000644000004100000410000000640115150651645022560 0ustar www-datawww-data# frozen_string_literal: true # Released under the MIT License. # Copyright, 2026, by Samuel Williams. require_relative "generic" module IO::Endpoint # A named endpoints collection is a hash of endpoints that can be accessed by name. # # Unlike {CompositeEndpoint}, which treats endpoints as an ordered list for failover, `NamedEndpoints` allows you to access endpoints by symbolic names, making it useful for scenarios where you need to run the same application on multiple endpoints with different configurations (e.g., HTTP/1 and HTTP/2 on different ports). class NamedEndpoints # Initialize a new named endpoints collection. # @parameter endpoints [Hash(Symbol, Generic)] A hash mapping endpoint names to endpoint instances. def initialize(endpoints) @endpoints = endpoints end # Get a string representation of the named endpoints. # @returns [String] A string representation listing all named endpoints. def to_s parts = @endpoints.map do |name, endpoint| "#{name}:#{endpoint}" end "named:#{parts.join(",")}" end # Get a detailed string representation of the named endpoints. # @returns [String] A detailed string representation including all named endpoints. def inspect parts = @endpoints.map do |name, endpoint| "#{name}: #{endpoint.inspect}" end "\#<#{self.class} #{parts.join(", ")}>" end # @attribute [Hash(Symbol, Generic)] The endpoints hash mapping names to endpoint instances. attr :endpoints # Access an endpoint by its name. # @parameter key [Symbol] The name of the endpoint to access. # @returns [Generic, nil] The endpoint with the given name, or nil if not found. def [] key @endpoints[key] end # Enumerate all endpoints with their names. # @yields {|name, endpoint| ...} For each endpoint, yields the name and endpoint. # @parameter name [Symbol] The name of the endpoint. # @parameter endpoint [Generic] The endpoint. def each(&block) @endpoints.each(&block) end # Create a new named endpoints instance with all endpoints bound. # @parameter options [Hash] Options to pass to each endpoint's bound method. # @returns [NamedEndpoints] A new instance with bound endpoints. def bound(**options) self.class.new( @endpoints.transform_values{|endpoint| endpoint.bound(**options)} ) end # Create a new named endpoints instance with all endpoints connected. # @parameter options [Hash] Options to pass to each endpoint's connected method. # @returns [NamedEndpoints] A new instance with connected endpoints. def connected(**options) self.class.new( @endpoints.transform_values{|endpoint| endpoint.connected(**options)} ) end # Close all endpoints in the collection. # Calls `close` on each endpoint value. # @returns [void] def close @endpoints.each_value(&:close) end end # Create a named endpoints collection from keyword arguments. # @parameter endpoints [Hash(Symbol, Generic)] Named endpoints as keyword arguments. # @returns [NamedEndpoints] A new named endpoints instance. # @example Create a named endpoints collection # endpoints = IO::Endpoint.named( # http1: IO::Endpoint.tcp("localhost", 8080), # http2: IO::Endpoint.tcp("localhost", 8090) # ) def self.named(**endpoints) NamedEndpoints.new(endpoints) end end io-endpoint-0.17.2/lib/io/endpoint/host_endpoint.rb0000644000004100000410000001000715150651645022263 0ustar www-datawww-data# frozen_string_literal: true # Released under the MIT License. # Copyright, 2023-2026, by Samuel Williams. require_relative "address_endpoint" module IO::Endpoint # Represents an endpoint for a hostname and service that resolves to multiple addresses. class HostEndpoint < Generic # Initialize a new host endpoint. # @parameter specification [Array] The host specification array containing nodename, service, family, socktype, protocol, and flags. # @parameter options [Hash] Additional options to pass to the parent class. def initialize(specification, **options) super(**options) @specification = specification end # Get a string representation of the host endpoint. # @returns [String] A string representation showing hostname and service. def to_s "host:#{@specification[0]}:#{@specification[1]}" end # Get a detailed string representation of the host endpoint. # @returns [String] A detailed string representation including all specification parameters. def inspect nodename, service, family, socktype, protocol, flags = @specification "\#<#{self.class} name=#{nodename.inspect} service=#{service.inspect} family=#{family.inspect} type=#{socktype.inspect} protocol=#{protocol.inspect} flags=#{flags.inspect}>" end # @attribute [Array] The host specification array. attr :specification # Get the hostname from the specification. # @returns [String, nil] The hostname (nodename) from the specification. def hostname @specification[0] end # Get the service from the specification. # @returns [String, Integer, nil] The service (port) from the specification. def service @specification[1] end # Try to connect to the given host by connecting to each address in sequence until a connection is made. # @yields {|socket| ...} If a block is given, yields the connected socket (may be invoked multiple times during connection attempts). # @parameter socket [Socket] The socket which is being connected. # @returns [Socket] the connected socket # @raises [Exception] if no connection could complete successfully def connect(wrapper = self.wrapper, &block) last_error = nil Addrinfo.foreach(*@specification) do |address| begin socket = wrapper.connect(address, **@options) rescue => last_error Console.debug(self, "Failed to connect:", address, exception: last_error) # Try again unless if possible, otherwise raise... else return socket unless block_given? begin return yield(socket) ensure socket.close end end end raise last_error end # Invokes the given block for every address which can be bound to. # @yields {|socket| ...} For each address that can be bound, yields the bound socket. # @parameter socket [Socket] The bound socket. # @returns [Array] an array of bound sockets def bind(wrapper = self.wrapper, &block) Addrinfo.foreach(*@specification).map do |address| wrapper.bind(address, **@options, &block) end end # @yields {|endpoint| ...} For each resolved address, yields an address endpoint. # @parameter endpoint [AddressEndpoint] An address endpoint. def each return to_enum unless block_given? Addrinfo.foreach(*@specification) do |address| yield AddressEndpoint.new(address, **@options) end end end # @parameter arguments nodename, service, family, socktype, protocol, flags. `socktype` will be set to Socket::SOCK_STREAM. # @parameter options keyword arguments passed on to {HostEndpoint#initialize} # # @returns [HostEndpoint] def self.tcp(*arguments, **options) arguments[3] = ::Socket::SOCK_STREAM HostEndpoint.new(arguments, **options) end # @parameter arguments nodename, service, family, socktype, protocol, flags. `socktype` will be set to Socket::SOCK_DGRAM. # @parameter options keyword arguments passed on to {HostEndpoint#initialize} # # @returns [HostEndpoint] def self.udp(*arguments, **options) arguments[3] = ::Socket::SOCK_DGRAM HostEndpoint.new(arguments, **options) end end io-endpoint-0.17.2/lib/io/endpoint/wrapper.rb0000644000004100000410000001613315150651645021074 0ustar www-datawww-data# frozen_string_literal: true # Released under the MIT License. # Copyright, 2023-2025, by Samuel Williams. require "socket" module IO::Endpoint # Represents a wrapper for socket operations that provides scheduling and configuration. class Wrapper include ::Socket::Constants if Fiber.respond_to?(:scheduler) # Schedule a block to run asynchronously. # Uses Fiber scheduler if available, otherwise falls back to Thread. # @yields { ...} The block to schedule. # @returns [Fiber, Thread] The scheduled fiber or thread. def schedule(&block) if Fiber.scheduler Fiber.schedule(&block) else Thread.new(&block) end end else # Schedule a block to run asynchronously. # Uses Thread for scheduling. # @yields { ...} The block to schedule. # @returns [Thread] The scheduled thread. def schedule(&block) Thread.new(&block) end end # Legacy method for compatibility with older code. def async(&block) schedule(&block) end # Set the timeout for an IO object. # @parameter io [IO] The IO object to set timeout on. # @parameter timeout [Numeric, nil] The timeout value. def set_timeout(io, timeout) if io.respond_to?(:timeout=) io.timeout = timeout end end # Set whether a socket should be buffered. # @parameter socket [Socket] The socket to configure. # @parameter buffered [Boolean] Whether the socket should be buffered. def set_buffered(socket, buffered) case buffered when true socket.setsockopt(IPPROTO_TCP, TCP_NODELAY, 0) when false socket.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1) end rescue Errno::EINVAL # On Darwin, sometimes occurs when the connection is not yet fully formed. Empirically, TCP_NODELAY is enabled despite this result. rescue Errno::EOPNOTSUPP # Some platforms may simply not support the operation. rescue Errno::ENOPROTOOPT # It may not be supported by the protocol (e.g. UDP). ¯\_(ツ)_/¯ end # Connect a socket to a remote address. # This is an extension point for subclasses to provide additional functionality. # # @parameter socket [Socket] The socket to connect. # @parameter remote_address [Address] The remote address to connect to. def socket_connect(socket, remote_address) socket.connect(remote_address.to_sockaddr) end # Establish a connection to a given `remote_address`. # @example # socket = Async::IO::Socket.connect(Async::IO::Address.tcp("8.8.8.8", 53)) # @parameter remote_address [Address] The remote address to connect to. # @parameter linger [Boolean] Wait for data to be sent before closing the socket. # @parameter local_address [Address] The local address to bind to before connecting. def connect(remote_address, local_address: nil, linger: nil, timeout: nil, buffered: false, **options) socket = nil begin socket = ::Socket.new(remote_address.afamily, remote_address.socktype, remote_address.protocol) if linger socket.setsockopt(SOL_SOCKET, SO_LINGER, 1) end if buffered == false set_buffered(socket, buffered) end if timeout set_timeout(socket, timeout) end if local_address if defined?(IP_BIND_ADDRESS_NO_PORT) # 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. socket.setsockopt(SOL_IP, IP_BIND_ADDRESS_NO_PORT, 1) end socket.bind(local_address.to_sockaddr) end rescue socket&.close raise end begin socket_connect(socket, remote_address) rescue Exception socket.close raise end return socket unless block_given? begin yield socket ensure socket.close end end # JRuby requires ServerSocket if defined?(::ServerSocket) ServerSocket = ::ServerSocket else ServerSocket = ::Socket end # Bind to a local address. # @example # socket = Async::IO::Socket.bind(Async::IO::Address.tcp("0.0.0.0", 9090)) # @parameter local_address [Address] The local address to bind to. # @parameter reuse_port [Boolean] Allow this port to be bound in multiple processes. # @parameter reuse_address [Boolean] Allow this port to be bound in multiple processes. # @parameter linger [Boolean] Wait for data to be sent before closing the socket. # @parameter protocol [Integer] The socket protocol to use. def bind(local_address, protocol: 0, reuse_address: true, reuse_port: nil, linger: nil, bound_timeout: nil, backlog: Socket::SOMAXCONN, **options, &block) socket = nil begin socket = ServerSocket.new(local_address.afamily, local_address.socktype, protocol) if reuse_address socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) end if reuse_port socket.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1) end if linger socket.setsockopt(SOL_SOCKET, SO_LINGER, 1) end # Set the timeout: if bound_timeout set_timeout(socket, bound_timeout) end socket.bind(local_address.to_sockaddr) if backlog begin # Generally speaking, bind/listen is a common pattern, but it's not applicable to all socket types. We ignore the error if it's not supported as the alternative is exposing this upstream, which seems less desirable than handling it here. In other words, `bind` in this context means "prepare it to accept connections", whatever that means for the given socket type. socket.listen(backlog) rescue Errno::EOPNOTSUPP # Ignore. end end rescue socket&.close raise end return socket unless block_given? schedule do begin yield socket ensure socket.close end end end # Accept a connection from a bound socket. # This is an extension point for subclasses to provide additional functionality. # # @parameter server [Socket] The bound server socket. # @returns [Tuple(Socket, Address)] The connected socket and the remote address. def socket_accept(server) server.accept end # Bind to a local address and accept connections in a loop. def accept(server, timeout: nil, linger: nil, **options, &block) # Ensure we use a `loop do ... end` so that state is not leaked between iterations: loop do socket, address = socket_accept(server) if linger socket.setsockopt(SOL_SOCKET, SO_LINGER, 1) end if timeout set_timeout(socket, timeout) end schedule do # Some sockets, notably SSL sockets, need application level negotiation before they are ready: if socket.respond_to?(:start) begin socket.start rescue socket.close raise end end # It seems like OpenSSL doesn't return the address of the peer when using `accept`, so we need to get it from the socket: address ||= socket.remote_address yield socket, address end end end DEFAULT = new # Get the default wrapper instance. # @returns [Wrapper] The default wrapper instance. def self.default DEFAULT end end end io-endpoint-0.17.2/lib/io/endpoint/ssl_endpoint.rb0000644000004100000410000001635715150651645022125 0ustar www-datawww-data# frozen_string_literal: true # Released under the MIT License. # Copyright, 2023-2025, by Samuel Williams. require_relative "host_endpoint" require_relative "generic" require "openssl" # @namespace module OpenSSL # @namespace module SSL # Represents an SSL socket with additional methods for compatibility. class SSLSocket unless method_defined?(:start) # Start the SSL handshake (alias for accept). def start self.accept end end end # Represents a module that forwards socket methods to the underlying IO object. module SocketForwarder unless method_defined?(:close_on_exec=) # Set whether the socket should be closed on exec. # @parameter value [Boolean] Whether to close on exec. def close_on_exec=(value) to_io.close_on_exec = value end end unless method_defined?(:local_address) # Get the local address of the socket. # @returns [Addrinfo] The local address. def local_address to_io.local_address end end unless method_defined?(:remote_address) # Get the remote address of the socket. # @returns [Addrinfo] The remote address. def remote_address to_io.remote_address end end unless method_defined?(:wait) # Wait for the socket to become ready. # @parameter arguments [Array] Arguments to pass to the underlying IO wait method. # @returns [IO, nil] The socket if ready, nil otherwise. def wait(*arguments) to_io.wait(*arguments) end end unless method_defined?(:wait_readable) # Wait for the socket to become readable. # @parameter arguments [Array] Arguments to pass to the underlying IO wait_readable method. # @returns [IO, nil] The socket if readable, nil otherwise. def wait_readable(*arguments) to_io.wait_readable(*arguments) end end unless method_defined?(:wait_writable) # Wait for the socket to become writable. # @parameter arguments [Array] Arguments to pass to the underlying IO wait_writable method. # @returns [IO, nil] The socket if writable, nil otherwise. def wait_writable(*arguments) to_io.wait_writable(*arguments) end end if IO.method_defined?(:timeout) unless method_defined?(:timeout) # Get the timeout for socket operations. # @returns [Numeric, nil] The timeout value. def timeout to_io.timeout end end unless method_defined?(:timeout=) # Set the timeout for socket operations. # @parameter value [Numeric, nil] The timeout value. def timeout=(value) to_io.timeout = value end end end end end end module IO::Endpoint # Represents an SSL/TLS endpoint that wraps another endpoint. class SSLEndpoint < Generic # Initialize a new SSL endpoint. # @parameter endpoint [Generic] The underlying endpoint to wrap with SSL. # @option ssl_context [OpenSSL::SSL::SSLContext, nil] An optional SSL context to use. # @parameter options [Hash] Additional options including `:ssl_params` and `:hostname`. def initialize(endpoint, **options) super(**options) @endpoint = endpoint if ssl_context = options[:ssl_context] @context = build_context(ssl_context) else @context = nil end end # Get a string representation of the SSL endpoint. # @returns [String] A string representation showing the underlying endpoint. def to_s "ssl:#{@endpoint}" end # Get a detailed string representation of the SSL endpoint. # @returns [String] A detailed string representation including the underlying endpoint. def inspect "\#<#{self.class} endpoint=#{@endpoint.inspect}>" end # Get the address from the underlying endpoint. # @returns [Address, nil] The address from the underlying endpoint. def address @endpoint.address end # Get the hostname for SSL verification. # @returns [String, nil] The hostname from options or the underlying endpoint. def hostname @options[:hostname] || @endpoint.hostname end # @attribute [Generic] The underlying endpoint. attr :endpoint # @attribute [Hash] The options hash. attr :options # Get SSL parameters from options. # @returns [Hash, nil] SSL parameters if specified in options. def params @options[:ssl_params] end # Build an SSL context with configured parameters. # @parameter context [OpenSSL::SSL::SSLContext] An optional SSL context to configure. # @returns [OpenSSL::SSL::SSLContext] The configured SSL context. def build_context(context = ::OpenSSL::SSL::SSLContext.new) if params = self.params context.set_params(params) end # context.setup # context.freeze return context end # Get or build the SSL context. # @returns [OpenSSL::SSL::SSLContext] The SSL context. def context @context ||= build_context end # Create an SSL server socket from an IO object. # @parameter io [IO] The underlying IO object. # @returns [OpenSSL::SSL::SSLServer] A new SSL server socket. def make_server(io) ::OpenSSL::SSL::SSLServer.new(io, self.context).tap do |server| server.start_immediately = false end end # Create an SSL client socket from an IO object. # @parameter io [IO] The underlying IO object. # @returns [OpenSSL::SSL::SSLSocket] A new SSL client socket. def make_socket(io) ::OpenSSL::SSL::SSLSocket.new(io, self.context).tap do |socket| # We consider the underlying IO is owned by the SSL socket: socket.sync_close = true end end # Connect to the underlying endpoint and establish a SSL connection. # @yields {|socket| ...} If a block is given, yields the SSL server socket. # @parameter socket [Socket] The SSL server socket. # @returns [Socket] the connected socket def bind(*arguments, **options, &block) if block_given? @endpoint.bind(*arguments, **options) do |server| yield self.make_server(server) end else @endpoint.bind(*arguments, **options).map do |server| self.make_server(server) end end end # Connect to the underlying endpoint and establish a SSL connection. # @yields {|socket| ...} If a block is given, yields the connected SSL socket. # @parameter socket [Socket] The connected SSL socket. # @returns [Socket] the connected socket def connect(&block) socket = self.make_socket(@endpoint.connect) if hostname = self.hostname socket.hostname = hostname end begin socket.connect rescue socket.close raise end return socket unless block_given? begin yield socket ensure socket.close end end # Enumerate all endpoints by wrapping each underlying endpoint with SSL. # @yields {|endpoint| ...} For each underlying endpoint, yields an SSL-wrapped endpoint. # @parameter endpoint [SSLEndpoint] An SSL endpoint. def each return to_enum unless block_given? @endpoint.each do |endpoint| yield self.class.new(endpoint, **@options) end end end # @parameter arguments # @parameter ssl_context [OpenSSL::SSL::SSLContext, nil] # @parameter hostname [String, nil] # @parameter options keyword arguments passed through to {Endpoint.tcp} # # @returns [SSLEndpoint] def self.ssl(*arguments, ssl_context: nil, hostname: nil, **options) SSLEndpoint.new(self.tcp(*arguments, **options), ssl_context: ssl_context, hostname: hostname) end end io-endpoint-0.17.2/lib/io/endpoint.rb0000644000004100000410000000104415150651645017407 0ustar www-datawww-data# frozen_string_literal: true # Released under the MIT License. # Copyright, 2023-2026, by Samuel Williams. require_relative "endpoint/version" require_relative "endpoint/generic" require_relative "endpoint/shared_endpoint" # Represents a collection of endpoint classes for network I/O operations. module IO::Endpoint # Get the current file descriptor limit for the process. # @returns [Integer] The soft limit for the number of open file descriptors. def self.file_descriptor_limit Process.getrlimit(Process::RLIMIT_NOFILE).first end end io-endpoint-0.17.2/checksums.yaml.gz.sig0000444000004100000410000000060015150651645020131 0ustar www-datawww-data7*"1a/u:,tQs>[e ed')i3du/%z5(Y`ծdi*bN,=s+ĸuGPtn+2]{3j}^oJʛ_-tZk$.ӽЈ]I a \@ļ;[ tq晗06Ma}NRQ50 ~Yݱo}JTWbգ84 kȪUM!@}(]o}A)sFg.s:b 3[!I!u" {ceio-endpoint-0.17.2/readme.md0000644000004100000410000000633615150651645015655 0ustar www-datawww-data# IO::Endpoint Provides a separation of concerns interface for IO endpoints. This allows you to write code which is agnostic to the underlying IO implementation. [![Development Status](https://github.com/socketry/io-endpoint/workflows/Test/badge.svg)](https://github.com/socketry/io-endpoint/actions?workflow=Test) ## Usage Please see the [project documentation](https://socketry.github.io/io-endpoint) for more details. - [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. - [Named Endpoints](https://socketry.github.io/io-endpointguides/named-endpoints/index) - This guide explains how to use `IO::Endpoint::NamedEndpoints` to manage multiple endpoints by name, enabling scenarios like running the same application on different protocols or ports. ## Releases Please see the [project releases](https://socketry.github.io/io-endpointreleases/index) for all releases. ### v0.17.2 - 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. ### v0.17.1 - Add `#to_s` and `#inspect` for `IO::Endpoint::NamedEndpoints`. ### v0.17.0 - Added `IO::Endpoint::NamedEndpoints` for accessing endpoints by symbolic names, useful for running applications on multiple endpoints with different configurations. ### v0.16.0 - Improved error handling in `#connect` for more robust connection handling. - Added getting started guide and improved documentation coverage. ### v0.15.2 - Fixed `UNIXEndpoint#bind` to pass all arguments through to super. ### v0.15.1 - Added `async-dns` to externals and restored removed method. ### v0.15.0 - Allow wrapper to be customized using endpoint `options[:wrapper]`. - Expose wrapper extension points for `connect` and `accept`. ### v0.14.0 - Uniform `#to_s` and `#inspect` implementations across all endpoints. ### v0.13.1 - Fixed state leak between iterations of the accept loop. ### v0.13.0 - Propagate options assigned to composite endpoint to nested endpoints. ## See Also - [async-io](https://github.com/socketry/async-io) — Where this implementation originally came from. ## Contributing We welcome contributions to this project. 1. Fork it. 2. Create your feature branch (`git checkout -b my-new-feature`). 3. Commit your changes (`git commit -am 'Add some feature'`). 4. Push to the branch (`git push origin my-new-feature`). 5. Create new Pull Request. ### Developer Certificate of Origin In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed. ### Community Guidelines This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers. io-endpoint-0.17.2/io-endpoint.gemspec0000644000004100000410000000701415150651645017662 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: io-endpoint 0.17.2 ruby lib Gem::Specification.new do |s| s.name = "io-endpoint".freeze s.version = "0.17.2".freeze s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.metadata = { "documentation_uri" => "https://socketry.github.io/io-endpoint", "source_code_uri" => "https://github.com/socketry/io-endpoint.git" } if s.respond_to? :metadata= s.require_paths = ["lib".freeze] s.authors = ["Samuel Williams".freeze, "Delton Ding".freeze] s.cert_chain = ["-----BEGIN CERTIFICATE-----\nMIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11\nZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK\nCZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz\nMjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd\nMBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj\nbzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB\nigKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2\n9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW\nsGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE\ne5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN\nXibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss\nRZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn\ntUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM\nzp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW\nxm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O\nBBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs\naWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs\naWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE\ncBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl\nxCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/\nc1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp\n8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws\nJkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP\neX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt\nQ2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8\nvoD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=\n-----END CERTIFICATE-----\n".freeze] s.date = "1980-01-02" s.files = ["context/getting-started.md".freeze, "context/index.yaml".freeze, "context/named-endpoints.md".freeze, "lib/io/endpoint.rb".freeze, "lib/io/endpoint/address_endpoint.rb".freeze, "lib/io/endpoint/bound_endpoint.rb".freeze, "lib/io/endpoint/composite_endpoint.rb".freeze, "lib/io/endpoint/connected_endpoint.rb".freeze, "lib/io/endpoint/generic.rb".freeze, "lib/io/endpoint/host_endpoint.rb".freeze, "lib/io/endpoint/named_endpoints.rb".freeze, "lib/io/endpoint/shared_endpoint.rb".freeze, "lib/io/endpoint/socket_endpoint.rb".freeze, "lib/io/endpoint/ssl_endpoint.rb".freeze, "lib/io/endpoint/unix_endpoint.rb".freeze, "lib/io/endpoint/version.rb".freeze, "lib/io/endpoint/wrapper.rb".freeze, "license.md".freeze, "readme.md".freeze, "releases.md".freeze] s.homepage = "https://github.com/socketry/io-endpoint".freeze s.licenses = ["MIT".freeze] s.required_ruby_version = Gem::Requirement.new(">= 3.2".freeze) s.rubygems_version = "4.0.3".freeze s.summary = "Provides a separation of concerns interface for IO endpoints.".freeze end io-endpoint-0.17.2/data.tar.gz.sig0000444000004100000410000000060015150651645016701 0ustar www-datawww-data$E[`bg:SЂ:E7:cwEp]"KYM-h"'d-zXO&QzݏdZ19u>Cm_Fx# 0m\@ģPkn݅dio}ErR<|ЉlUA-$@U2[͵Z;U~+ݏV5g? IJ"(`d^_#_>rEB*@#񬣵z 8Y>MǼ 8,i I:H4E_z5)-ݎJp\a`Uu1fmVՒ'Beкy*}0VLA="LK)hi! _He_o8'fio-endpoint-0.17.2/license.md0000644000004100000410000000213615150651645016034 0ustar www-datawww-data# MIT License Copyright, 2023-2026, by Samuel Williams. Copyright, 2026, by Delton Ding. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. io-endpoint-0.17.2/metadata.gz.sig0000444000004100000410000000060015150651645016763 0ustar www-datawww-data~$=Bm̞Gۏ2#F[@>֡ 1pi}4YYQRUOA+2JSH+-R;]% 't3sx5fLc,^|KjkW519ڂjaVZJ-:t>d?ŲΰdQ(2U쿖~TH` g#Kڟmg .9;{'%HAũt7j0j9;4sfTh֤^ e) W?f`1ǵ;BKGf1t$R2L+zTݝHJL Zo[S-d#9҂ux#(;4` 9aTio-endpoint-0.17.2/releases.md0000644000004100000410000000463715150651645016225 0ustar www-datawww-data# Releases ## v0.17.2 - 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. ## v0.17.1 - Add `#to_s` and `#inspect` for `IO::Endpoint::NamedEndpoints`. ## v0.17.0 - Added `IO::Endpoint::NamedEndpoints` for accessing endpoints by symbolic names, useful for running applications on multiple endpoints with different configurations. ## v0.16.0 - Improved error handling in `#connect` for more robust connection handling. - Added getting started guide and improved documentation coverage. ## v0.15.2 - Fixed `UNIXEndpoint#bind` to pass all arguments through to super. ## v0.15.1 - Added `async-dns` to externals and restored removed method. ## v0.15.0 - Allow wrapper to be customized using endpoint `options[:wrapper]`. - Expose wrapper extension points for `connect` and `accept`. ## v0.14.0 - Uniform `#to_s` and `#inspect` implementations across all endpoints. ## v0.13.1 - Fixed state leak between iterations of the accept loop. ## v0.13.0 - Propagate options assigned to composite endpoint to nested endpoints. ## v0.12.0 - Expose `size` and internal endpoints for composite endpoint. ## v0.10.3 - Fixed `SSLServer#accept` failures causing accept loop to exit. (\#10) ## v0.10.2 - Centralized usage of `listen` to wrapper. ## v0.10.1 - Ensure `listen` is called. ## v0.10.0 - Don't hardcode timeout support - detect at run-time. ## v0.9.0 - Correctly set `sync_close`. (\#7) ## v0.8.1 - Remove broken `require_relative 'readable'`. ## v0.8.0 - Removed `IO#readable?` dependency in favor of `io-stream` gem. ## v0.7.2 - Added missing `SSLSocket#remote_address`. ## v0.7.1 - Improved shims for `IO#readable?`. ## v0.7.0 - Fixed shim for `OpenSSL::SSL::SSLSocket#local_address`. - Added shim for `IO#readable?`. ## v0.6.0 - Allow `Wrapper#accept` to ignore unknown options. ## v0.5.0 - Fixed the OpenSSL shims. (\#5) - Simplified implementation and separated connected/bound options. (\#4) ## v0.4.0 - Improved compatibility with `async-http`/`async-io`. (\#3) ## v0.3.0 - Fixed OpenSSL integration. (\#2) ## v0.2.0 - Added option `buffered:` for controlling `TCP_NODELAY`. ## v0.1.0 - Initial implementation extracted from `async-io`. - Added support for thread and fiber wrappers. - Support Ruby 2.7+ with optional `set_timeout`. io-endpoint-0.17.2/context/0000755000004100000410000000000015150651645015552 5ustar www-datawww-dataio-endpoint-0.17.2/context/index.yaml0000644000004100000410000000160215150651645017544 0ustar www-datawww-data# Automatically generated context index for Utopia::Project guides. # Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`. --- description: Provides a separation of concerns interface for IO endpoints. metadata: documentation_uri: https://socketry.github.io/io-endpoint source_code_uri: https://github.com/socketry/io-endpoint.git files: - path: getting-started.md title: Getting Started description: This guide explains how to get started with `io-endpoint`, a library that provides a separation of concerns interface for network I/O endpoints. - path: named-endpoints.md title: Named Endpoints description: This guide explains how to use `IO::Endpoint::NamedEndpoints` to manage multiple endpoints by name, enabling scenarios like running the same application on different protocols or ports. io-endpoint-0.17.2/context/named-endpoints.md0000644000004100000410000000760315150651645021167 0ustar www-datawww-data# Named Endpoints This guide explains how to use `IO::Endpoint::NamedEndpoints` to manage multiple endpoints by name, enabling scenarios like running the same application on different protocols or ports. ## Overview `NamedEndpoints` is a collection of endpoints that can be accessed by symbolic names. Unlike {ruby IO::Endpoint::CompositeEndpoint}, which treats endpoints as an ordered list for failover, `NamedEndpoints` allows you to: - **Access endpoints by name**: Use symbolic keys like `:http1` or `:http2` instead of array indices. - **Run multiple configurations**: Serve the same application on different protocols, ports, or transports simultaneously. - **Iterate over endpoints**: Process all endpoints while maintaining their names for configuration lookup. ## When to Use NamedEndpoints Use `NamedEndpoints` when you need to: - Run the same server application on multiple endpoints with different configurations (e.g., HTTP/1 and HTTP/2). - Access endpoints by symbolic names rather than position. - Bind multiple endpoints and create servers for each one. - Manage a collection of endpoints where each has a specific role or configuration. If you need failover behavior (trying endpoints in order until one succeeds), use {ruby IO::Endpoint::CompositeEndpoint} instead. ## Creating Named Endpoints ### Using the Constructor Create a `NamedEndpoints` instance by passing a hash of endpoints: ```ruby require "io/endpoint" http1_endpoint = IO::Endpoint.tcp("localhost", 8080) http2_endpoint = IO::Endpoint.tcp("localhost", 8090) named = IO::Endpoint::NamedEndpoints.new( http1: http1_endpoint, http2: http2_endpoint ) ``` ### Using the Factory Method The `IO::Endpoint.named` factory method provides a convenient way to create named endpoints: ```ruby require "io/endpoint" named = IO::Endpoint.named( http1: IO::Endpoint.tcp("localhost", 8080), http2: IO::Endpoint.tcp("localhost", 8090), https: IO::Endpoint.ssl("localhost", 8443) ) ``` ## Accessing Endpoints Access endpoints by their names using the `[]` operator: ```ruby named = IO::Endpoint.named( http1: IO::Endpoint.tcp("localhost", 8080), http2: IO::Endpoint.tcp("localhost", 8090) ) # Access by name http1 = named[:http1] http2 = named[:http2] # Returns nil if not found missing = named[:nonexistent] # => nil ``` ## Iterating Over Endpoints ### Using `each` The `each` method yields both the name and endpoint: ```ruby named = IO::Endpoint.named( http1: IO::Endpoint.tcp("localhost", 8080), http2: IO::Endpoint.tcp("localhost", 8090) ) named.each do |name, endpoint| puts "Endpoint #{name} is bound to #{endpoint}" end ``` To map over endpoint values, use `endpoints.values.map`: ```ruby protocols = named.endpoints.values.map do |endpoint| endpoint.protocol.to_s end # => ["HTTP1", "HTTP2"] ``` ## Binding Endpoints To bind endpoints, iterate over the collection and bind each endpoint individually, or use the `bound` method to create a new collection with all endpoints bound. The `bound` method creates a new `NamedEndpoints` instance where all endpoints are bound: ```ruby named = IO::Endpoint.named( http1: IO::Endpoint.tcp("localhost", 8080), http2: IO::Endpoint.tcp("localhost", 8090) ) bound_named = named.bound(reuse_address: true) # All endpoints are now bound bound_named.each do |name, bound_endpoint| server = bound_endpoint.sockets.first server.listen(10) end ``` ## Connecting to Endpoints To connect to a specific endpoint, access it by name and call `connect` on that endpoint: ```ruby named = IO::Endpoint.named( primary: IO::Endpoint.tcp("server1.example.com", 80), secondary: IO::Endpoint.tcp("server2.example.com", 80) ) # Connect to a specific endpoint by name named[:primary].connect do |socket| socket.write("GET / HTTP/1.1\r\n\r\n") response = socket.read puts response end ``` If you need failover behavior (trying endpoints in order until one succeeds), use {ruby IO::Endpoint::CompositeEndpoint} instead. io-endpoint-0.17.2/context/getting-started.md0000644000004100000410000000644115150651645021206 0ustar www-datawww-data# Getting Started This guide explains how to get started with `io-endpoint`, a library that provides a separation of concerns interface for network I/O endpoints. ## Installation Add the gem to your project: ~~~ bash $ bundle add io-endpoint ~~~ ## Core Concepts `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: - **Write transport-agnostic code**: Your application logic doesn't need to know whether it's using TCP, UDP, or UNIX sockets. - **Test with different transports**: Easily swap between transports during testing. - **Handle multiple addresses**: Automatically handle IPv4 and IPv6 addresses. - **Compose endpoints**: Combine multiple endpoints for failover or load distribution. 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: - {ruby IO::Endpoint::HostEndpoint} - Resolves hostnames to addresses (e.g., "localhost:8080") - {ruby IO::Endpoint::AddressEndpoint} - Works with specific network addresses - {ruby IO::Endpoint::UNIXEndpoint} - Handles UNIX domain sockets - {ruby IO::Endpoint::SSLEndpoint} - Wraps endpoints with SSL/TLS encryption - {ruby IO::Endpoint::CompositeEndpoint} - Combines multiple endpoints ## Usage ### Creating a TCP Server 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: ```ruby require "io/endpoint" # Create a TCP endpoint listening on localhost port 8080: endpoint = IO::Endpoint.tcp("localhost", 8080) # Bind to the endpoint and accept connections: endpoint.bind do |server| # The server socket is automatically closed when the block exits server.listen(10) loop do client, address = server.accept # Handle the client connection client.close end end ``` ### Creating a TCP Client To connect to a remote server, use the `connect` method: ```ruby require "io/endpoint" # Create a TCP endpoint for the remote server: endpoint = IO::Endpoint.tcp("example.com", 80) # Connect to the server: endpoint.connect do |socket| # The socket is automatically closed when the block exits socket.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") response = socket.read puts response end ``` ### Using UNIX Domain Sockets For inter-process communication on the same machine, UNIX domain sockets provide better performance than TCP: ```ruby require "io/endpoint" # Create a UNIX socket endpoint: endpoint = IO::Endpoint.unix("/tmp/myapp.sock") # Bind to the socket: endpoint.bind do |server| server.listen(10) loop do client, address = server.accept # Handle the client connection client.close end end ``` ### Using SSL/TLS To add encryption to your connections, wrap a TCP endpoint with SSL: ```ruby require "io/endpoint" # Create an SSL endpoint: endpoint = IO::Endpoint.ssl("example.com", 443, hostname: "example.com") # Connect with SSL encryption: endpoint.connect do |socket| # The socket is automatically encrypted socket.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") response = socket.read puts response end ```