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.

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.

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.

Parameters:

  • ssl_params (Hash<String, String>)

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

Returns:

  • (OpenSSL::SSL::SSLContext)


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

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.



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

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.



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

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

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

Creates fresh listeners for the given URI.

Parameters:

  • uri (URI)

    the parsed bind URI

Returns:

Raises:



162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/raptor/binder.rb', line 162

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.

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)



271
272
273
274
275
# File 'lib/raptor/binder.rb', line 271

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)



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/raptor/binder.rb', line 206

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



235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/raptor/binder.rb', line 235

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 bind URI.

Returns:

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


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

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)



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

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:



146
147
148
149
150
151
152
153
# File 'lib/raptor/binder.rb', line 146

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



258
259
260
261
# File 'lib/raptor/binder.rb', line 258

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.

Parameters:

  • uri (URI)

    the parsed bind URI the FDs were bound to

  • filenos (Array<Integer>)

    file descriptors to wrap

Returns:

Raises:



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

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.

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