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, socket_backlog: SOCKET_BACKLOG, inherited_fds: {}) ⇒ 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). When inherited_fds supplies file descriptors for a URI, the listener is reconstructed from those FDs instead of binding fresh.

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

  • socket_backlog (Integer) (defaults to: SOCKET_BACKLOG)

    kernel listen() queue depth for TCP/SSL listeners

  • inherited_fds (Hash{String => Array<Integer>}) (defaults to: {})

    inherited listener FDs keyed by bind URI

  • socket_backlog: (Integer) (defaults to: SOCKET_BACKLOG)
  • inherited_fds: (Hash[String, Array[Integer]]) (defaults to: {})

Raises:



97
98
99
100
101
102
103
104
# File 'lib/raptor/binder.rb', line 97

def initialize(bind_uris, socket_backlog: SOCKET_BACKLOG, inherited_fds: {})
  @bind_uris = bind_uris
  @socket_backlog = socket_backlog
  @inherited_fds = inherited_fds
  @listeners = nil
  @uri_listeners = nil
  parse
end

Instance Attribute Details

#bind_urisArray<String> (readonly)

Array of bind URIs.

Returns:

  • (Array<String>)

    the bind URIs



67
68
69
# File 'lib/raptor/binder.rb', line 67

def bind_uris
  @bind_uris
end

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

Array of listening sockets.

Returns:

  • (Array<TCPServer, UNIXServer, SslListener>)

    the server sockets



77
78
79
# File 'lib/raptor/binder.rb', line 77

def listeners
  @listeners
end

#socket_backlogInteger (readonly)

Kernel listen() queue depth for TCP/SSL listeners.

Returns:

  • (Integer)

    the socket backlog



72
73
74
# File 'lib/raptor/binder.rb', line 72

def socket_backlog
  @socket_backlog
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



117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/raptor/binder.rb', line 117

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

#build_ssl_context(ssl_params) ⇒ OpenSSL::SSL::SSLContext

Builds a frozen OpenSSL::SSL::SSLContext configured for HTTP/2 and HTTP/1.1 ALPN negotiation.

Parameters:

  • ssl_params (Hash<String, String>)

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

Returns:

  • (OpenSSL::SSL::SSLContext)


333
334
335
336
337
338
339
340
341
342
343
# File 'lib/raptor/binder.rb', line 333

def build_ssl_context(ssl_params)
  require "openssl"

  OpenSSL::SSL::SSLContext.new.tap do |ssl_context|
    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
  end
end

#clear_close_on_execvoid

This method returns an undefined value.

Clears the close-on-exec flag on every listener so the file descriptors survive Kernel.exec.



173
174
175
# File 'lib/raptor/binder.rb', line 173

def clear_close_on_exec
  @listeners.each { |listener| listener.to_io.close_on_exec = false }
end

#closevoid

This method returns an undefined value.

Closes all listening sockets.



152
153
154
# File 'lib/raptor/binder.rb', line 152

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

#create_listeners(bind_uri) ⇒ Array<TCPServer, UNIXServer, SslListener>

Creates fresh listeners for the given bind URI.

Parameters:

  • bind_uri (String)

    the URI to bind

Returns:

Raises:



204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/raptor/binder.rb', line 204

def create_listeners(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

#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)



320
321
322
323
324
# File 'lib/raptor/binder.rb', line 320

def create_ssl_listeners(host, port, ssl_params)
  tcp_servers = create_tcp_listeners(host, port)
  ssl_context = build_ssl_context(ssl_params)
  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)



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/raptor/binder.rb', line 250

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?("[")

  addrinfo = Addrinfo.tcp(host, port)
  socket = Socket.new(addrinfo.afamily, Socket::SOCK_STREAM, 0)
  socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
  socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true) if Socket.const_defined?(:SO_REUSEPORT)
  socket.bind(addrinfo)
  socket.listen(@socket_backlog)

  tcp_server = TCPServer.for_fd(socket.fileno)
  socket.autoclose = false
  [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



280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/raptor/binder.rb', line 280

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

  register_unix_socket_cleanup(path)

  [UNIXServer.new(path)]
end

#inheritable_fdsHash{String => Array<Integer>}

Returns the file descriptors of every listener, grouped by the bind URI they were created from. The result is the payload to hand to a successor process via the inherited_fds: constructor argument.

Returns:

  • (Hash{String => Array<Integer>})


163
164
165
# File 'lib/raptor/binder.rb', line 163

def inheritable_fds
  @uri_listeners.transform_values { |listeners| listeners.map { |listener| listener.to_io.fileno } }
end

#loopback_addressesArray<String>

Returns all available loopback IP addresses.

Returns:

  • (Array<String>)

    unique loopback addresses (IPv4 and IPv6)



350
351
352
353
354
355
356
# File 'lib/raptor/binder.rb', line 350

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, reusing inherited file descriptors for URIs supplied in @inherited_fds.

Raises:



186
187
188
189
190
191
192
193
194
195
# File 'lib/raptor/binder.rb', line 186

def parse
  @uri_listeners = @bind_uris.to_h do |bind_uri|
    if filenos = @inherited_fds[bind_uri]
      [bind_uri, restore_listeners(bind_uri, filenos)]
    else
      [bind_uri, create_listeners(bind_uri)]
    end
  end
  @listeners = @uri_listeners.values.flatten
end

#register_unix_socket_cleanup(path) ⇒ void

This method returns an undefined value.

Registers an at_exit hook that removes the Unix socket file on the owning master's clean exit. Each call records the current process so forked workers won't delete a socket their master still owns.

Parameters:

  • path (String)

    filesystem path of the Unix socket



303
304
305
306
# File 'lib/raptor/binder.rb', line 303

def register_unix_socket_cleanup(path)
  master_pid = Process.pid
  at_exit { File.delete(path) rescue nil if Process.pid == master_pid }
end

#restore_listeners(bind_uri, filenos) ⇒ Array<TCPServer, UNIXServer, SslListener>

Reconstructs listeners for the given bind URI from inherited file descriptors.

Parameters:

  • bind_uri (String)

    the URI the FDs were bound to

  • filenos (Array<Integer>)

    file descriptors to wrap

Returns:

Raises:



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/raptor/binder.rb', line 227

def restore_listeners(bind_uri, filenos)
  uri = URI.parse(bind_uri)
  case uri.scheme
  when "tcp"
    filenos.map { |fileno| TCPServer.for_fd(fileno) }
  when "unix"
    register_unix_socket_cleanup(uri.path)
    filenos.map { |fileno| UNIXServer.for_fd(fileno) }
  when "ssl"
    ssl_context = build_ssl_context(URI.decode_www_form(uri.query || "").to_h)
    filenos.map { |fileno| SslListener.new(tcp_server: TCPServer.for_fd(fileno), ssl_context: ssl_context) }
  else
    raise UnknownBindSchemeError.new(uri.scheme)
  end
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



140
141
142
143
144
145
# File 'lib/raptor/binder.rb', line 140

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