mirror of
https://github.com/puma/puma.git
synced 2022-11-09 13:48:40 -05:00
5af91ff6aa
Bind to (systemd) activated sockets, regardless of configured binds. Systemd can present sockets as file descriptors that are already opened. By default Puma will use these but only if it was explicitly told to bind to the socket. If not, it will close the activated sockets. This means all configuration is duplicated. Binds can contain additional configuration, but only SSL config is really relevant since the unix and TCP socket options are ignored. This means there is a lot of duplicated configuration for no additional value in most setups. This option tells the launcher to bind to all activated sockets, regardless of existing binds. The special value 'only' can be passed. If systemd activated sockets are detected, all other binds are cleared. When they aren't detected, the regular binds will be used.
456 lines
13 KiB
Ruby
456 lines
13 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative "helper"
|
|
require_relative "helpers/ssl" if ::Puma::HAS_SSL
|
|
require_relative "helpers/tmp_path"
|
|
|
|
require "puma/binder"
|
|
require "puma/events"
|
|
require "puma/configuration"
|
|
|
|
class TestBinderBase < Minitest::Test
|
|
include SSLHelper if ::Puma::HAS_SSL
|
|
include TmpPath
|
|
|
|
def setup
|
|
@events = Puma::Events.strings
|
|
@binder = Puma::Binder.new(@events)
|
|
end
|
|
|
|
def teardown
|
|
@binder.ios.reject! { |io| Minitest::Mock === io || io.to_io.closed? }
|
|
@binder.close
|
|
@binder.unix_paths.select! { |path| File.exist? path }
|
|
@binder.close_listeners
|
|
end
|
|
|
|
private
|
|
|
|
def ssl_context_for_binder(binder = @binder)
|
|
binder.ios[0].instance_variable_get(:@ctx)
|
|
end
|
|
end
|
|
|
|
class TestBinder < TestBinderBase
|
|
parallelize_me!
|
|
|
|
def test_synthesize_binds_from_activated_fds_no_sockets
|
|
binds = ['tcp://0.0.0.0:3000']
|
|
result = @binder.synthesize_binds_from_activated_fs(binds, true)
|
|
|
|
assert_equal ['tcp://0.0.0.0:3000'], result
|
|
end
|
|
|
|
def test_synthesize_binds_from_activated_fds_non_matching_together
|
|
binds = ['tcp://0.0.0.0:3000']
|
|
sockets = {['tcp', '0.0.0.0', '5000'] => nil}
|
|
@binder.instance_variable_set(:@activated_sockets, sockets)
|
|
result = @binder.synthesize_binds_from_activated_fs(binds, false)
|
|
|
|
assert_equal ['tcp://0.0.0.0:3000', 'tcp://0.0.0.0:5000'], result
|
|
end
|
|
|
|
def test_synthesize_binds_from_activated_fds_non_matching_only
|
|
binds = ['tcp://0.0.0.0:3000']
|
|
sockets = {['tcp', '0.0.0.0', '5000'] => nil}
|
|
@binder.instance_variable_set(:@activated_sockets, sockets)
|
|
result = @binder.synthesize_binds_from_activated_fs(binds, true)
|
|
|
|
assert_equal ['tcp://0.0.0.0:5000'], result
|
|
end
|
|
|
|
def test_synthesize_binds_from_activated_fds_complex_binds
|
|
binds = [
|
|
'tcp://0.0.0.0:3000',
|
|
'ssl://192.0.2.100:5000',
|
|
'ssl://192.0.2.101:5000?no_tlsv1=true',
|
|
'unix:///run/puma.sock'
|
|
]
|
|
sockets = {
|
|
['tcp', '0.0.0.0', '5000'] => nil,
|
|
['tcp', '192.0.2.100', '5000'] => nil,
|
|
['tcp', '192.0.2.101', '5000'] => nil,
|
|
['unix', '/run/puma.sock'] => nil
|
|
}
|
|
@binder.instance_variable_set(:@activated_sockets, sockets)
|
|
result = @binder.synthesize_binds_from_activated_fs(binds, false)
|
|
|
|
expected = ['tcp://0.0.0.0:3000', 'ssl://192.0.2.100:5000', 'ssl://192.0.2.101:5000?no_tlsv1=true', 'unix:///run/puma.sock', 'tcp://0.0.0.0:5000']
|
|
assert_equal expected, result
|
|
end
|
|
|
|
def test_localhost_addresses_dont_alter_listeners_for_tcp_addresses
|
|
@binder.parse ["tcp://localhost:0"], @events
|
|
|
|
assert_empty @binder.listeners
|
|
end
|
|
|
|
def test_home_alters_listeners_for_tcp_addresses
|
|
port = UniquePort.call
|
|
@binder.parse ["tcp://127.0.0.1:#{port}"], @events
|
|
|
|
assert_equal "tcp://127.0.0.1:#{port}", @binder.listeners[0][0]
|
|
assert_kind_of TCPServer, @binder.listeners[0][1]
|
|
end
|
|
|
|
def test_connected_ports
|
|
ports = (1..3).map { |_| UniquePort.call }
|
|
|
|
@binder.parse(ports.map { |p| "tcp://localhost:#{p}" }, @events)
|
|
|
|
assert_equal ports, @binder.connected_ports
|
|
end
|
|
|
|
def test_localhost_addresses_dont_alter_listeners_for_ssl_addresses
|
|
skip 'No ssl support' unless ::Puma::HAS_SSL
|
|
@binder.parse ["ssl://localhost:0?#{ssl_query}"], @events
|
|
|
|
assert_empty @binder.listeners
|
|
end
|
|
|
|
def test_home_alters_listeners_for_ssl_addresses
|
|
skip 'No ssl support' unless ::Puma::HAS_SSL
|
|
port = UniquePort.call
|
|
@binder.parse ["ssl://127.0.0.1:#{port}?#{ssl_query}"], @events
|
|
|
|
assert_equal "ssl://127.0.0.1:#{port}?#{ssl_query}", @binder.listeners[0][0]
|
|
assert_kind_of TCPServer, @binder.listeners[0][1]
|
|
end
|
|
|
|
def test_correct_zero_port
|
|
@binder.parse ["tcp://localhost:0"], @events
|
|
|
|
m = %r!http://127.0.0.1:(\d+)!.match(@events.stdout.string)
|
|
port = m[1].to_i
|
|
|
|
refute_equal 0, port
|
|
end
|
|
|
|
def test_correct_zero_port_ssl
|
|
skip 'No ssl support' unless ::Puma::HAS_SSL
|
|
skip("Implement later")
|
|
|
|
ssl_regex = %r!ssl://127.0.0.1:(\d+)!
|
|
|
|
@binder.parse ["ssl://localhost:0?#{ssl_query}"], @events
|
|
|
|
port = ssl_regex.match(@events.stdout.string)[1].to_i
|
|
|
|
refute_equal 0, port
|
|
end
|
|
|
|
def test_logs_all_localhost_bindings
|
|
@binder.parse ["tcp://localhost:0"], @events
|
|
|
|
assert_match %r!http://127.0.0.1:(\d+)!, @events.stdout.string
|
|
if Socket.ip_address_list.any? {|i| i.ipv6_loopback? }
|
|
assert_match %r!http://\[::1\]:(\d+)!, @events.stdout.string
|
|
end
|
|
end
|
|
|
|
def test_logs_all_localhost_bindings_ssl
|
|
skip 'No ssl support' unless ::Puma::HAS_SSL
|
|
skip("Incorrectly logs localhost, not 127.0.0.1")
|
|
|
|
@binder.parse ["ssl://localhost:0?#{ssl_query}"], @events
|
|
|
|
assert_match %r!ssl://127.0.0.1:(\d+)!, @events.stdout.string
|
|
if Socket.ip_address_list.any? {|i| i.ipv6_loopback? }
|
|
assert_match %r!ssl://\[::1\]:(\d+)!, @events.stdout.string
|
|
end
|
|
end
|
|
|
|
def test_allows_both_ssl_and_tcp
|
|
assert_parsing_logs_uri [:ssl, :tcp]
|
|
end
|
|
|
|
def test_allows_both_unix_and_tcp
|
|
skip_on :jruby # Undiagnosed thread race. TODO fix
|
|
assert_parsing_logs_uri [:unix, :tcp]
|
|
end
|
|
|
|
def test_allows_both_tcp_and_unix
|
|
assert_parsing_logs_uri [:tcp, :unix]
|
|
end
|
|
|
|
def test_pre_existing_unix
|
|
skip UNIX_SKT_MSG unless UNIX_SKT_EXIST
|
|
|
|
unix_path = tmp_path('.sock')
|
|
File.open(unix_path, mode: 'wb') { |f| f.puts 'pre existing' }
|
|
@binder.parse ["unix://#{unix_path}"], @events
|
|
|
|
assert_match %r!unix://#{unix_path}!, @events.stdout.string
|
|
|
|
refute_includes @binder.unix_paths, unix_path
|
|
|
|
@binder.close_listeners
|
|
|
|
assert File.exist?(unix_path)
|
|
|
|
ensure
|
|
if UNIX_SKT_EXIST
|
|
File.unlink unix_path if File.exist? unix_path
|
|
end
|
|
end
|
|
|
|
def test_binder_parses_tlsv1_disabled
|
|
skip 'No ssl support' unless ::Puma::HAS_SSL
|
|
@binder.parse ["ssl://0.0.0.0:0?#{ssl_query}&no_tlsv1=true"], @events
|
|
|
|
assert ssl_context_for_binder.no_tlsv1
|
|
end
|
|
|
|
def test_binder_parses_tlsv1_enabled
|
|
skip 'No ssl support' unless ::Puma::HAS_SSL
|
|
@binder.parse ["ssl://0.0.0.0:0?#{ssl_query}&no_tlsv1=false"], @events
|
|
|
|
refute ssl_context_for_binder.no_tlsv1
|
|
end
|
|
|
|
def test_binder_parses_tlsv1_tlsv1_1_unspecified_defaults_to_enabled
|
|
skip 'No ssl support' unless ::Puma::HAS_SSL
|
|
@binder.parse ["ssl://0.0.0.0:0?#{ssl_query}"], @events
|
|
|
|
refute ssl_context_for_binder.no_tlsv1
|
|
refute ssl_context_for_binder.no_tlsv1_1
|
|
end
|
|
|
|
def test_binder_parses_tlsv1_1_disabled
|
|
skip 'No ssl support' unless ::Puma::HAS_SSL
|
|
@binder.parse ["ssl://0.0.0.0:0?#{ssl_query}&no_tlsv1_1=true"], @events
|
|
|
|
assert ssl_context_for_binder.no_tlsv1_1
|
|
end
|
|
|
|
def test_binder_parses_tlsv1_1_enabled
|
|
skip 'No ssl support' unless ::Puma::HAS_SSL
|
|
@binder.parse ["ssl://0.0.0.0:0?#{ssl_query}&no_tlsv1_1=false"], @events
|
|
|
|
refute ssl_context_for_binder.no_tlsv1_1
|
|
end
|
|
|
|
def test_env_contains_protoenv
|
|
skip 'No ssl support' unless ::Puma::HAS_SSL
|
|
@binder.parse ["ssl://localhost:0?#{ssl_query}"], @events
|
|
|
|
env_hash = @binder.envs[@binder.ios.first]
|
|
|
|
@binder.proto_env.each do |k,v|
|
|
assert_equal env_hash[k], v
|
|
end
|
|
end
|
|
|
|
def test_env_contains_stderr
|
|
skip 'No ssl support' unless ::Puma::HAS_SSL
|
|
@binder.parse ["ssl://localhost:0?#{ssl_query}"], @events
|
|
|
|
env_hash = @binder.envs[@binder.ios.first]
|
|
|
|
assert_equal @events.stderr, env_hash["rack.errors"]
|
|
end
|
|
|
|
def test_close_calls_close_on_ios
|
|
@mocked_ios = [Minitest::Mock.new, Minitest::Mock.new]
|
|
@mocked_ios.each { |m| m.expect(:close, true) }
|
|
@binder.ios = @mocked_ios
|
|
|
|
@binder.close
|
|
|
|
assert @mocked_ios.map(&:verify).all?
|
|
end
|
|
|
|
def test_redirects_for_restart_creates_a_hash
|
|
@binder.parse ["tcp://127.0.0.1:0"], @events
|
|
|
|
result = @binder.redirects_for_restart
|
|
ios = @binder.listeners.map { |_l, io| io.to_i }
|
|
|
|
ios.each { |int| assert_equal int, result[int] }
|
|
assert result[:close_others]
|
|
end
|
|
|
|
def test_redirects_for_restart_env
|
|
@binder.parse ["tcp://127.0.0.1:0"], @events
|
|
|
|
result = @binder.redirects_for_restart_env
|
|
|
|
@binder.listeners.each_with_index do |l, i|
|
|
assert_equal "#{l[1].to_i}:#{l[0]}", result["PUMA_INHERIT_#{i}"]
|
|
end
|
|
end
|
|
|
|
def test_close_listeners_closes_ios
|
|
@binder.parse ["tcp://127.0.0.1:#{UniquePort.call}"], @events
|
|
|
|
refute @binder.listeners.any? { |u, l| l.closed? }
|
|
|
|
@binder.close_listeners
|
|
|
|
assert @binder.listeners.all? { |u, l| l.closed? }
|
|
end
|
|
|
|
def test_close_listeners_closes_ios_unless_closed?
|
|
@binder.parse ["tcp://127.0.0.1:0"], @events
|
|
|
|
bomb = @binder.listeners.first[1]
|
|
bomb.close
|
|
def bomb.close; raise "Boom!"; end # the bomb has been planted
|
|
|
|
assert @binder.listeners.any? { |u, l| l.closed? }
|
|
|
|
@binder.close_listeners
|
|
|
|
assert @binder.listeners.all? { |u, l| l.closed? }
|
|
end
|
|
|
|
def test_listeners_file_unlink_if_unix_listener
|
|
skip UNIX_SKT_MSG unless UNIX_SKT_EXIST
|
|
|
|
unix_path = tmp_path('.sock')
|
|
@binder.parse ["unix://#{unix_path}"], @events
|
|
assert File.socket?(unix_path)
|
|
|
|
@binder.close_listeners
|
|
refute File.socket?(unix_path)
|
|
end
|
|
|
|
def test_import_from_env_listen_inherit
|
|
@binder.parse ["tcp://127.0.0.1:0"], @events
|
|
removals = @binder.create_inherited_fds(@binder.redirects_for_restart_env)
|
|
|
|
@binder.listeners.each do |url, io|
|
|
assert_equal io.to_i, @binder.inherited_fds[url]
|
|
end
|
|
assert_includes removals, "PUMA_INHERIT_0"
|
|
end
|
|
|
|
# Socket activation tests. We have to skip all of these on non-UNIX platforms
|
|
# because the check that we do in the code only works if you support UNIX sockets.
|
|
# This is OK, because systemd obviously only works on Linux.
|
|
def test_socket_activation_tcp
|
|
skip UNIX_SKT_MSG unless UNIX_SKT_EXIST
|
|
url = "127.0.0.1"
|
|
port = UniquePort.call
|
|
sock = Addrinfo.tcp(url, port).listen
|
|
assert_activates_sockets(url: url, port: port, sock: sock)
|
|
end
|
|
|
|
def test_socket_activation_tcp_ipv6
|
|
skip UNIX_SKT_MSG unless UNIX_SKT_EXIST
|
|
url = "::"
|
|
port = UniquePort.call
|
|
sock = Addrinfo.tcp(url, port).listen
|
|
assert_activates_sockets(url: url, port: port, sock: sock)
|
|
end
|
|
|
|
def test_socket_activation_unix
|
|
skip_on :jruby # Failing with what I think is a JRuby bug
|
|
skip UNIX_SKT_MSG unless UNIX_SKT_EXIST
|
|
|
|
state_path = tmp_path('.state')
|
|
sock = Addrinfo.unix(state_path).listen
|
|
assert_activates_sockets(path: state_path, sock: sock)
|
|
ensure
|
|
File.unlink(state_path) rescue nil # JRuby race?
|
|
end
|
|
|
|
def test_rack_multithread_default_configuration
|
|
binder = Puma::Binder.new(@events)
|
|
|
|
assert binder.proto_env["rack.multithread"]
|
|
end
|
|
|
|
def test_rack_multithread_custom_configuration
|
|
conf = Puma::Configuration.new(max_threads: 1)
|
|
|
|
binder = Puma::Binder.new(@events, conf)
|
|
|
|
refute binder.proto_env["rack.multithread"]
|
|
end
|
|
|
|
def test_rack_multiprocess_default_configuration
|
|
binder = Puma::Binder.new(@events)
|
|
|
|
refute binder.proto_env["rack.multiprocess"]
|
|
end
|
|
|
|
def test_rack_multiprocess_custom_configuration
|
|
conf = Puma::Configuration.new(workers: 1)
|
|
|
|
binder = Puma::Binder.new(@events, conf)
|
|
|
|
assert binder.proto_env["rack.multiprocess"]
|
|
end
|
|
|
|
private
|
|
|
|
def assert_activates_sockets(path: nil, port: nil, url: nil, sock: nil)
|
|
hash = { "LISTEN_FDS" => 1, "LISTEN_PID" => $$ }
|
|
@events.instance_variable_set(:@debug, true)
|
|
|
|
@binder.instance_variable_set(:@sock_fd, sock.fileno)
|
|
def @binder.socket_activation_fd(int); @sock_fd; end
|
|
@result = @binder.create_activated_fds(hash)
|
|
|
|
url = "[::]" if url == "::"
|
|
ary = path ? [:unix, path] : [:tcp, url, port]
|
|
|
|
assert_kind_of TCPServer, @binder.activated_sockets[ary]
|
|
assert_match "Registered #{ary.join(":")} for activation from LISTEN_FDS", @events.stdout.string
|
|
assert_equal ["LISTEN_FDS", "LISTEN_PID"], @result
|
|
end
|
|
|
|
def assert_parsing_logs_uri(order = [:unix, :tcp])
|
|
skip UNIX_SKT_MSG if order.include?(:unix) && !UNIX_SKT_EXIST
|
|
skip 'No ssl support' unless ::Puma::HAS_SSL
|
|
|
|
unix_path = tmp_path('.sock')
|
|
prepared_paths = {
|
|
ssl: "ssl://127.0.0.1:#{UniquePort.call}?#{ssl_query}",
|
|
tcp: "tcp://127.0.0.1:#{UniquePort.call}",
|
|
unix: "unix://#{unix_path}"
|
|
}
|
|
|
|
expected_logs = prepared_paths.dup.tap do |logs|
|
|
logs[:tcp] = logs[:tcp].gsub('tcp://', 'http://')
|
|
end
|
|
|
|
tested_paths = [prepared_paths[order[0]], prepared_paths[order[1]]]
|
|
|
|
@binder.parse tested_paths, @events
|
|
stdout = @events.stdout.string
|
|
|
|
order.each do |prot|
|
|
assert_match expected_logs[prot], stdout
|
|
end
|
|
ensure
|
|
@binder.close_listeners if order.include?(:unix) && UNIX_SKT_EXIST
|
|
end
|
|
end
|
|
|
|
class TestBinderJRuby < TestBinderBase
|
|
def test_binder_parses_jruby_ssl_options
|
|
skip 'No ssl support' unless ::Puma::HAS_SSL
|
|
|
|
keystore = File.expand_path "../../examples/puma/keystore.jks", __FILE__
|
|
ssl_cipher_list = "TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
|
|
|
|
@binder.parse ["ssl://0.0.0.0:8080?#{ssl_query}"], @events
|
|
|
|
assert_equal keystore, ssl_context_for_binder.keystore
|
|
assert_equal ssl_cipher_list, ssl_context_for_binder.ssl_cipher_list
|
|
end
|
|
end if ::Puma::IS_JRUBY
|
|
|
|
class TestBinderMRI < TestBinderBase
|
|
def test_binder_parses_ssl_cipher_filter
|
|
skip 'No ssl support' unless ::Puma::HAS_SSL
|
|
|
|
ssl_cipher_filter = "AES@STRENGTH"
|
|
|
|
@binder.parse ["ssl://0.0.0.0?#{ssl_query}&ssl_cipher_filter=#{ssl_cipher_filter}"], @events
|
|
|
|
assert_equal ssl_cipher_filter, ssl_context_for_binder.ssl_cipher_filter
|
|
end
|
|
end unless ::Puma::IS_JRUBY
|