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:



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

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


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

def bind_uris
  @bind_uris
end

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

Returns the array of listening sockets.

Returns:



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

def listeners
  @listeners
end

#socket_backlog ⇒ Integer (readonly)

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

Returns:

  • (Integer)


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

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


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

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

Returns:

  • (OpenSSL::SSL::SSLContext)


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

def build_ssl_context(ssl_params)
  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.



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

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.



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

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

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

Creates fresh listeners for the given URI.

Parameters:

  • uri (URI::Generic) —

    the parsed bind URI

Returns:

Raises:



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

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)



308
309
310
311
312
# File 'lib/raptor/binder.rb', line 308

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)



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

def create_tcp_listeners(host, port)
  if host == "localhost"
    return loopback_addresses.flat_map { |address| create_tcp_listeners(address, port) }
  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.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_DEFER_ACCEPT, 1) if Socket.const_defined?(:TCP_DEFER_ACCEPT)
  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



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

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)


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

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


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

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:



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

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



295
296
297
298
# File 'lib/raptor/binder.rb', line 295

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::Generic) —

    the parsed bind URI the FDs were bound to

  • filenos (Array<Integer>) —

    file descriptors to wrap

Returns:

Raises:



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

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, SSL, or cleartext HTTP/2 listener, or 0 when none is configured.

Returns:

  • (Integer)


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

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