Class: Raptor::Binder

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

Overview

Binds tcp://, unix://, and ssl:// URIs to listening sockets and holds them for the server. Reconstructs listeners from inherited file descriptors when provided (systemd socket activation, hot restart).

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 and binds each URI. localhost expands to both IPv4 and IPv6 loopback; when inherited_fds supplies file descriptors for a URI the listener is rebuilt from those instead of binding fresh.

RBS:

  • (Array[String] bind_uris, ?socket_backlog: Integer, ?inherited_fds: Hash[String, Array[Integer]]) -> void

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:



66
67
68
69
70
71
72
73
# File 'lib/raptor/binder.rb', line 66

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)

Returns the array of bind URIs.

Returns:

  • (Array<String>)


43
44
45
# File 'lib/raptor/binder.rb', line 43

def bind_uris
  @bind_uris
end

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

Returns the array of listening sockets.

Returns:



53
54
55
# File 'lib/raptor/binder.rb', line 53

def listeners
  @listeners
end

#socket_backlogInteger (readonly)

Returns the kernel listen() queue depth for TCP/SSL listeners.

Returns:

  • (Integer)


48
49
50
# File 'lib/raptor/binder.rb', line 48

def socket_backlog
  @socket_backlog
end

Instance Method Details

#addressesArray<String>

Returns the bound addresses as strings: TCP as host:port, Unix as the socket path, SSL as ssl://host:port.

RBS:

  • () -> Array[String]

Returns:

  • (Array<String>)


81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/raptor/binder.rb', line 81

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.

RBS:

  • (Hash[String, String] ssl_params) -> OpenSSL::SSL::SSLContext

Parameters:

  • ssl_params (Hash<String, String>)

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

Returns:

  • (OpenSSL::SSL::SSLContext)


293
294
295
296
297
298
299
300
301
302
303
# File 'lib/raptor/binder.rb', line 293

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.

RBS:

  • () -> void



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

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.

RBS:

  • () -> void



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

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

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

Creates fresh listeners for the given URI.

RBS:

  • (URI::Generic uri) -> Array[TCPServer | UNIXServer | SslListener]

Parameters:

  • uri (URI)

    the parsed bind URI

Returns:

Raises:



171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/raptor/binder.rb', line 171

def create_listeners(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 and port.

RBS:

  • (String? host, Integer? port, Hash[String, String] ssl_params) -> Array[SslListener]

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 file paths)

Returns:

  • (Array<SslListener>)

    array containing the created SSL listener(s)



280
281
282
283
284
# File 'lib/raptor/binder.rb', line 280

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.

RBS:

  • (String? host, Integer? port) -> Array[TCPServer]

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)



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/raptor/binder.rb', line 215

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.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
  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, and cleans the file up on the master's clean exit.

RBS:

  • (String path) -> Array[UNIXServer]

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



244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/raptor/binder.rb', line 244

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

#http2?Boolean

Returns whether any listener can negotiate HTTP/2.

RBS:

  • () -> bool

Returns:

  • (Boolean)


114
115
116
# File 'lib/raptor/binder.rb', line 114

def http2?
  @listeners.any? { |listener| listener.is_a?(SslListener) }
end

#inheritable_fdsHash{String => Array<Integer>}

Returns the file descriptors of every listener, grouped by bind URI.

RBS:

  • () -> Hash[String, Array[Integer]]

Returns:

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


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

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.

RBS:

  • () -> Array[String]

Returns:

  • (Array<String>)

    unique loopback addresses (IPv4 and IPv6)



310
311
312
313
314
315
316
# File 'lib/raptor/binder.rb', line 310

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.

RBS:

  • () -> void

Raises:



155
156
157
158
159
160
161
162
# File 'lib/raptor/binder.rb', line 155

def parse
  @uri_listeners = @bind_uris.to_h do |bind_uri|
    uri = URI.parse(bind_uri)
    filenos = @inherited_fds[bind_uri]
    [bind_uri, filenos ? restore_listeners(uri, filenos) : create_listeners(uri)]
  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.

RBS:

  • (String path) -> void

Parameters:

  • path (String)

    filesystem path of the Unix socket



267
268
269
270
# File 'lib/raptor/binder.rb', line 267

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(uri, filenos) ⇒ Array<TCPServer, UNIXServer, SslListener>

Reconstructs listeners for the given URI from inherited file descriptors.

RBS:

  • (URI::Generic uri, Array[Integer] filenos) -> Array[TCPServer | UNIXServer | SslListener]

Parameters:

  • uri (URI)

    the parsed bind URI the FDs were bound to

  • filenos (Array<Integer>)

    file descriptors to wrap

Returns:

Raises:



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

def restore_listeners(uri, filenos)
  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 of the first TCP or SSL listener, or 0 when none is configured.

RBS:

  • () -> Integer

Returns:

  • (Integer)


102
103
104
105
106
107
# File 'lib/raptor/binder.rb', line 102

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