Class: Raptor::Binder

Inherits:
Object
  • Object
show all
Defined in:
lib/raptor/binder.rb,
sig/generated/raptor/binder.rbs

Overview

Manages binding to network addresses and creating listening sockets.

Binder handles parsing URI bind specifications, creating TCP, Unix, and SSL server sockets, and managing socket options for optimal performance. It supports binding to multiple addresses simultaneously.

Examples:

TCP binding

binder = Binder.new(["tcp://0.0.0.0:3000", "tcp://[::1]:3000"])
puts binder.addresses #=> ["0.0.0.0:3000", "[::1]:3000"]
binder.close

Unix socket binding

binder = Binder.new(["unix:///tmp/raptor.sock"])
puts binder.addresses #=> ["/tmp/raptor.sock"]
binder.close

SSL binding

binder = Binder.new(["ssl://0.0.0.0:443?cert=/path/to.crt&key=/path/to.key"])
puts binder.addresses #=> ["ssl://0.0.0.0:443"]
binder.close

Localhost binding

binder = Binder.new(["tcp://localhost:8080"])
# Binds to both IPv4 and IPv6 loopback addresses

Defined Under Namespace

Classes: SslListener, UnknownBindSchemeError

Constant Summary collapse

SOCKET_BACKLOG =

Returns:

  • (::Integer)
1024

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(bind_uris) ⇒ Binder

Creates a new Binder with the specified bind URIs.

Parses the provided bind URIs and creates listening sockets for each one. Supports tcp://, unix://, and ssl:// schemes. Localhost is expanded to all available loopback addresses (both IPv4 and IPv6).

Examples:

binder = Binder.new(["tcp://0.0.0.0:3000", "unix:///tmp/raptor.sock"])

Parameters:

  • bind_uris (Array<String>)

    array of URI strings to bind to

Raises:



80
81
82
83
84
# File 'lib/raptor/binder.rb', line 80

def initialize(bind_uris)
  @bind_uris = bind_uris
  @listeners = nil
  parse
end

Instance Attribute Details

#listenersArray<TCPServer, UNIXServer, SslListener> (readonly)

Array of listening sockets.

Returns:

  • (Array<TCPServer, UNIXServer, SslListener>)

    the server sockets



64
65
66
# File 'lib/raptor/binder.rb', line 64

def listeners
  @listeners
end

Instance Method Details

#addressesArray<String>

Returns the bound addresses as strings.

TCP listeners are returned as "host:port", Unix listeners as the socket path, and SSL listeners as "ssl://host:port".

Examples:

binder.addresses #=> ["127.0.0.1:3000", "/tmp/raptor.sock", "ssl://0.0.0.0:443"]

Returns:

  • (Array<String>)

    address strings for each bound listener



97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/raptor/binder.rb', line 97

def addresses
  @listeners.map do |listener|
    case listener
    when UNIXServer
      listener.path
    when SslListener
      address = listener.local_address
      "ssl://#{address.ip_address}:#{address.ip_port}"
    else
      address = listener.local_address
      "#{address.ip_address}:#{address.ip_port}"
    end
  end
end

#closevoid

This method returns an undefined value.

Closes all listening sockets.



132
133
134
# File 'lib/raptor/binder.rb', line 132

def close
  @listeners.each(&:close)
end

#create_ssl_listeners(host, port, ssl_params) ⇒ Array<SslListener>

Creates SSL server sockets for the given host, port, and SSL parameters.

Wraps each TCP listener with an SSL context to produce SslListener objects. The ssl_params hash must include "cert" and "key" entries pointing to the certificate and private key files respectively.

Parameters:

  • host (String, nil)

    hostname or IP address to bind to

  • port (Integer, nil)

    port number to bind to

  • ssl_params (Hash<String, String>)

    SSL options ("cert" and "key" paths)

Returns:

  • (Array<SslListener>)

    array containing the created SSL listener(s)



221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/raptor/binder.rb', line 221

def create_ssl_listeners(host, port, ssl_params)
  require "openssl"

  tcp_servers = create_tcp_listeners(host, port)

  ssl_context = OpenSSL::SSL::SSLContext.new
  ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(ssl_params["cert"]))
  ssl_context.key = OpenSSL::PKey.read(File.read(ssl_params["key"]))
  ssl_context.alpn_protocols = ["h2", "http/1.1"]
  ssl_context.alpn_select_cb = ->(protocols) { protocols.include?("h2") ? "h2" : "http/1.1" }
  ssl_context.freeze

  tcp_servers.map { |tcp_server| SslListener.new(tcp_server: tcp_server, ssl_context: ssl_context) }
end

#create_tcp_listeners(host, port) ⇒ Array<TCPServer>

Creates TCP server sockets for the given host and port.

Parameters:

  • host (String, nil)

    hostname or IP address to bind to

  • port (Integer, nil)

    port number to bind to

Returns:

  • (Array<TCPServer>)

    array containing the created TCP server socket(s)



167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/raptor/binder.rb', line 167

def create_tcp_listeners(host, port)
  if host == "localhost"
    return loopback_addresses.map { |address| create_tcp_listeners(address, port) }.flatten
  end

  host = host[1..-2] if host&.start_with?("[")

  tcp_server = TCPServer.new(host, port)
  tcp_server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
  tcp_server.listen SOCKET_BACKLOG

  [tcp_server]
end

#create_unix_listeners(path) ⇒ Array<UNIXServer>

Creates a Unix domain server socket at the given path.

Removes stale socket files left by crashed processes (when the socket is not currently in use). Registers an at_exit hook to clean up the socket file on normal process exit.

Parameters:

  • path (String)

    filesystem path for the Unix socket

Returns:

  • (Array<UNIXServer>)

    array containing the created Unix server socket

Raises:

  • (RuntimeError)

    if the socket path is already in active use



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/raptor/binder.rb', line 192

def create_unix_listeners(path)
  if File.exist?(path)
    begin
      UNIXSocket.new(path).close
      raise "Socket #{path.inspect} is already in use"
    rescue Errno::ECONNREFUSED
      File.delete(path)
    end
  end

  server = UNIXServer.new(path)
  master_pid = Process.pid
  at_exit { File.delete(path) rescue nil if Process.pid == master_pid }

  [server]
end

#loopback_addressesArray<String>

Returns all available loopback IP addresses.

Returns:

  • (Array<String>)

    unique loopback addresses (IPv4 and IPv6)



241
242
243
244
245
246
247
# File 'lib/raptor/binder.rb', line 241

def loopback_addresses
  Socket.ip_address_list.filter_map do |addrinfo|
    next unless addrinfo.ipv4_loopback? || addrinfo.ipv6_loopback?

    addrinfo.ip_address
  end.tap(&:uniq!)
end

#parsevoid

This method returns an undefined value.

Parses bind URIs and creates listening sockets.

Raises:



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/raptor/binder.rb', line 144

def parse
  @listeners = @bind_uris.map do |bind_uri|
    uri = URI.parse(bind_uri)
    case uri.scheme
    when "tcp"
      create_tcp_listeners(uri.host, uri.port)
    when "unix"
      create_unix_listeners(uri.path)
    when "ssl"
      create_ssl_listeners(uri.host, uri.port, URI.decode_www_form(uri.query || "").to_h)
    else
      raise UnknownBindSchemeError.new(uri.scheme)
    end
  end.tap(&:flatten!)
end

#server_portInteger

Returns the port number of the first TCP or SSL listener.

Used to populate SERVER_PORT in the Rack environment. Returns 0 if no TCP or SSL listener is configured (e.g., Unix socket only).

Returns:

  • (Integer)

    the port number, or 0 if no TCP listener exists



120
121
122
123
124
125
# File 'lib/raptor/binder.rb', line 120

def server_port
  tcp_listener = @listeners.find { |listener| listener.is_a?(TCPServer) || listener.is_a?(SslListener) }
  return 0 unless tcp_listener

  tcp_listener.local_address.ip_port
end