Class: Raptor::Binder

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

Overview

Binds TCP, Unix, SSL, and cleartext HTTP/2 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: H2cListener, 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.

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:



82
83
84
85
86
87
88
89
# File 'lib/raptor/binder.rb', line 82

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_uris ⇒ Array<String> (readonly)

Returns the array of bind URIs.

Returns:

  • (Array<String>)


59
60
61
# File 'lib/raptor/binder.rb', line 59

def bind_uris
  @bind_uris
end

#listeners ⇒ Array<TCPServer, UNIXServer, SslListener, H2cListener> (readonly)

Returns the array of listening sockets.

Returns:



69
70
71
# File 'lib/raptor/binder.rb', line 69

def listeners
  @listeners
end

#socket_backlog ⇒ Integer (readonly)

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

Returns:

  • (Integer)


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

def socket_backlog
  @socket_backlog
end

Instance Method Details

#addresses ⇒ Array<String>

Returns the bound addresses as strings: TCP as host:port, Unix as the socket path, SSL as ssl://host:port, and cleartext HTTP/2 as h2c://host:port.

Returns:

  • (Array<String>)


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

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}"
    when H2cListener
      address = listener.local_address
      "h2c://#{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)


319
320
321
322
323
324
325
326
327
328
329
# File 'lib/raptor/binder.rb', line 319

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_exec ⇒ void

This method returns an undefined value.

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



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

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

#close ⇒ void

This method returns an undefined value.

Closes all listening sockets.



145
146
147
# File 'lib/raptor/binder.rb', line 145

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

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

Creates fresh listeners for the given URI.

Parameters:

  • uri (URI) —

    the parsed bind URI

Returns:

Raises:



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

def create_listeners(uri)
  case uri.scheme
  when "tcp"
    create_tcp_listeners(uri.host, uri.port)
  when "h2c"
    create_tcp_listeners(uri.host, uri.port).map { |listener| H2cListener.new(tcp_server: listener) }
  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.

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)



306
307
308
309
310
# File 'lib/raptor/binder.rb', line 306

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)



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

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.

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



270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/raptor/binder.rb', line 270

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.

Returns:

  • (Boolean)


136
137
138
# File 'lib/raptor/binder.rb', line 136

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

#inheritable_fds ⇒ Hash{String => Array<Integer>}

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

Returns:

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


154
155
156
# File 'lib/raptor/binder.rb', line 154

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

#loopback_addresses ⇒ Array<String>

Returns all available loopback IP addresses.

Returns:

  • (Array<String>) —

    unique loopback addresses (IPv4 and IPv6)



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

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

#parse ⇒ void

This method returns an undefined value.

Parses bind URIs and creates listening sockets, reusing inherited file descriptors for URIs supplied in @inherited_fds.

Raises:



177
178
179
180
181
182
183
184
# File 'lib/raptor/binder.rb', line 177

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.

Parameters:

  • path (String) —

    filesystem path of the Unix socket



293
294
295
296
# File 'lib/raptor/binder.rb', line 293

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, H2cListener>

Reconstructs listeners for the given URI from inherited file descriptors.

Parameters:

  • uri (URI) —

    the parsed bind URI the FDs were bound to

  • filenos (Array<Integer>) —

    file descriptors to wrap

Returns:

Raises:



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

def restore_listeners(uri, filenos)
  case uri.scheme
  when "tcp"
    filenos.map { |fileno| TCPServer.for_fd(fileno) }
  when "h2c"
    filenos.map { |fileno| H2cListener.new(tcp_server: 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_port ⇒ Integer

Returns the port of the first TCP or SSL listener, or 0 when none is configured.

Returns:

  • (Integer)


122
123
124
125
126
127
128
129
# File 'lib/raptor/binder.rb', line 122

def server_port
  tcp_listener = @listeners.find do |listener|
    listener.is_a?(TCPServer) || listener.is_a?(SslListener) || listener.is_a?(H2cListener)
  end
  return 0 unless tcp_listener

  tcp_listener.local_address.ip_port
end