require_relative '../helper' require 'fluent/plugin_helper/server' require 'fluent/plugin_helper/cert_option' # to create certs for tests require 'fluent/plugin/base' require 'timeout' require 'serverengine' require 'fileutils' class ServerPluginHelperTest < Test::Unit::TestCase class Dummy < Fluent::Plugin::TestBase helpers :server end TMP_DIR = File.expand_path(File.dirname(__FILE__) + "/../tmp/plugin_helper_server") PORT = unused_port setup do @socket_manager_path = ServerEngine::SocketManager::Server.generate_path if @socket_manager_path.is_a?(String) && File.exist?(@socket_manager_path) FileUtils.rm_f @socket_manager_path end @socket_manager_server = ServerEngine::SocketManager::Server.open(@socket_manager_path) ENV['SERVERENGINE_SOCKETMANAGER_PATH'] = @socket_manager_path.to_s @d = Dummy.new @d.start @d.after_start end teardown do (@d.stopped? || @d.stop) rescue nil (@d.before_shutdown? || @d.before_shutdown) rescue nil (@d.shutdown? || @d.shutdown) rescue nil (@d.after_shutdown? || @d.after_shutdown) rescue nil (@d.closed? || @d.close) rescue nil (@d.terminated? || @d.terminate) rescue nil @socket_manager_server.close if @socket_manager_server.is_a?(String) && File.exist?(@socket_manager_path) FileUtils.rm_f @socket_manager_path end end sub_test_case 'plugin instance' do test 'can be instantiated to be able to create threads' do d = Dummy.new assert d.respond_to?(:_servers) assert d._servers.empty? assert d.respond_to?(:server_wait_until_start) assert d.respond_to?(:server_wait_until_stop) assert d.respond_to?(:server_create_connection) assert d.respond_to?(:server_create) assert d.respond_to?(:server_create_tcp) assert d.respond_to?(:server_create_udp) assert d.respond_to?(:server_create_tls) end test 'can be configured' do d = Dummy.new assert_nothing_raised do d.configure(config_element()) end assert d.plugin_id assert d.log end end # run tests for tcp, udp, tls and unix sub_test_case '#server_create and #server_create_connection' do methods = {server_create: :server_create, server_create_connection: :server_create_connection} data(methods) test 'raise error if title is not specified or not a symbol' do |m| assert_raise(ArgumentError.new("BUG: title must be a symbol")) do @d.__send__(m, nil, PORT){|x| x } end assert_raise(ArgumentError.new("BUG: title must be a symbol")) do @d.__send__(m, "", PORT){|x| x } end assert_raise(ArgumentError.new("BUG: title must be a symbol")) do @d.__send__(m, "title", PORT){|x| x } end assert_nothing_raised do @d.__send__(m, :myserver, PORT){|x| x } end end data(methods) test 'raise error if port is not specified or not an integer' do |m| assert_raise(ArgumentError.new("BUG: port must be an integer")) do @d.__send__(m, :myserver, nil){|x| x } end assert_raise(ArgumentError.new("BUG: port must be an integer")) do @d.__send__(m, :myserver, "1"){|x| x } end assert_raise(ArgumentError.new("BUG: port must be an integer")) do @d.__send__(m, :myserver, 1.5){|x| x } end assert_nothing_raised do @d.__send__(m, :myserver, PORT){|x| x } end end data(methods) test 'raise error if block is not specified' do |m| assert_raise(ArgumentError) do @d.__send__(m, :myserver, PORT) end assert_nothing_raised do @d.__send__(m, :myserver, PORT){|x| x } end end data(methods) test 'creates tcp server, binds 0.0.0.0 in default' do |m| @d.__send__(m, :myserver, PORT){|x| x } assert_equal 1, @d._servers.size created_server_info = @d._servers.first assert_equal :myserver, created_server_info.title assert_equal PORT, created_server_info.port assert_equal :tcp, created_server_info.proto assert_equal "0.0.0.0", created_server_info.bind created_server = created_server_info.server assert created_server.is_a?(Coolio::TCPServer) assert_equal "0.0.0.0", created_server.instance_eval{ @listen_socket }.addr[3] end data(methods) test 'creates tcp server if specified in proto' do |m| @d.__send__(m, :myserver, PORT, proto: :tcp){|x| x } created_server_info = @d._servers.first assert_equal :tcp, created_server_info.proto created_server = created_server_info.server assert created_server.is_a?(Coolio::TCPServer) end data(methods) test 'creates tls server in default if transport section and tcp protocol specified' do |m| @d = d = Dummy.new transport_conf = config_element('transport', 'tcp', {}, []) d.configure(config_element('ROOT', '', {}, [transport_conf])) d.start d.after_start d.__send__(m, :myserver, PORT){|x| x } created_server_info = @d._servers.first assert_equal :tcp, created_server_info.proto created_server = created_server_info.server assert created_server.is_a?(Coolio::TCPServer) end data(methods) test 'creates tls server if specified in proto' do |m| assert_raise(ArgumentError.new("BUG: TLS transport specified, but certification options are not specified")) do @d.__send__(m, :myserver, PORT, proto: :tls){|x| x } end @d.__send__(m, :myserver, PORT, proto: :tls, tls_options: {insecure: true}){|x| x } created_server_info = @d._servers.first assert_equal :tls, created_server_info.proto created_server = created_server_info.server assert created_server.is_a?(Coolio::TCPServer) # yes, TCP here end data(methods) test 'creates tls server in default if transport section and tls protocol specified' do |m| @d = d = Dummy.new transport_conf = config_element('transport', 'tls', {'insecure' => 'true'}, []) d.configure(config_element('ROOT', '', {}, [transport_conf])) d.start d.after_start d.__send__(m, :myserver, PORT){|x| x } created_server_info = @d._servers.first assert_equal :tls, created_server_info.proto created_server = created_server_info.server assert created_server.is_a?(Coolio::TCPServer) # OK, it's Coolio::TCPServer end data(methods) test 'creates unix server if specified in proto' do |m| # pend "not implemented yet" end data(methods) test 'raise error if unknown protocol specified' do |m| assert_raise(ArgumentError.new("BUG: invalid protocol name")) do @d.__send__(m, :myserver, PORT, proto: :quic){|x| x } end end data( 'server_create tcp' => [:server_create, :tcp], 'server_create tls' => [:server_create, :tls], # 'server_create unix' => [:server_create, :unix], 'server_create_connection tcp' => [:server_create_connection, :tcp], 'server_create_connection tls' => [:server_create_connection, :tls], # 'server_create_connection tcp' => [:server_create_connection, :unix], ) test 'raise error if udp options specified for tcp/tls/unix' do |(m, proto)| assert_raise ArgumentError do @d.__send__(m, :myserver, PORT, proto: proto, max_bytes: 128){|x| x } end assert_raise ArgumentError do @d.__send__(m, :myserver, PORT, proto: proto, flags: 1){|x| x } end end data( 'server_create udp' => [:server_create, :udp], ) test 'raise error if tcp/tls options specified for udp' do |(m, proto)| assert_raise(ArgumentError.new("BUG: linger_timeout is available for tcp/tls")) do @d.__send__(m, :myserver, PORT, proto: proto, linger_timeout: 1, max_bytes: 128){|x| x } end end data( 'server_create udp' => [:server_create, :udp], ) test 'raise error if tcp/tls/unix options specified for udp' do |(m, proto)| assert_raise(ArgumentError.new("BUG: backlog is available for tcp/tls")) do @d.__send__(m, :myserver, PORT, proto: proto, backlog: 500){|x| x } end end data( 'server_create tcp' => [:server_create, :tcp, {}], 'server_create udp' => [:server_create, :udp, {max_bytes: 128}], # 'server_create unix' => [:server_create, :unix, {}], 'server_create_connection tcp' => [:server_create_connection, :tcp, {}], # 'server_create_connection unix' => [:server_create_connection, :unix, {}], ) test 'raise error if tls options specified for tcp/udp/unix' do |(m, proto, kwargs)| assert_raise(ArgumentError.new("BUG: tls_options is available only for tls")) do @d.__send__(m, :myserver, PORT, proto: proto, tls_options: {}, **kwargs){|x| x } end end data( 'server_create tcp' => [:server_create, :tcp, {}], 'server_create udp' => [:server_create, :udp, {max_bytes: 128}], 'server_create tls' => [:server_create, :tls, {tls_options: {insecure: true}}], 'server_create_connection tcp' => [:server_create_connection, :tcp, {}], 'server_create_connection tls' => [:server_create_connection, :tls, {tls_options: {insecure: true}}], ) test 'can bind specified IPv4 address' do |(m, proto, kwargs)| @d.__send__(m, :myserver, PORT, proto: proto, bind: "127.0.0.1", **kwargs){|x| x } assert_equal "127.0.0.1", @d._servers.first.bind assert_equal "127.0.0.1", @d._servers.first.server.instance_eval{ instance_variable_defined?(:@listen_socket) ? @listen_socket : @_io }.addr[3] end data( 'server_create tcp' => [:server_create, :tcp, {}], 'server_create udp' => [:server_create, :udp, {max_bytes: 128}], 'server_create tls' => [:server_create, :tls, {tls_options: {insecure: true}}], 'server_create_connection tcp' => [:server_create_connection, :tcp, {}], 'server_create_connection tls' => [:server_create_connection, :tls, {tls_options: {insecure: true}}], ) test 'can bind specified IPv6 address' do |(m, proto, kwargs)| # if available omit "IPv6 unavailable here" unless ipv6_enabled? @d.__send__(m, :myserver, PORT, proto: proto, bind: "::1", **kwargs){|x| x } assert_equal "::1", @d._servers.first.bind assert_equal "::1", @d._servers.first.server.instance_eval{ instance_variable_defined?(:@listen_socket) ? @listen_socket : @_io }.addr[3] end data( 'server_create tcp' => [:server_create, :tcp, {}], 'server_create udp' => [:server_create, :udp, {max_bytes: 128}], 'server_create tls' => [:server_create, :tls, {tls_options: {insecure: true}}], # 'server_create unix' => [:server_create, :unix, {}], 'server_create_connection tcp' => [:server_create, :tcp, {}], 'server_create_connection tls' => [:server_create, :tls, {tls_options: {insecure: true}}], # 'server_create_connection unix' => [:server_create, :unix, {}], ) test 'can create 2 or more servers which share same bind address and port if shared option is true' do |(m, proto, kwargs)| begin d2 = Dummy.new; d2.start; d2.after_start assert_nothing_raised do @d.__send__(m, :myserver, PORT, proto: proto, **kwargs){|x| x } d2.__send__(m, :myserver, PORT, proto: proto, **kwargs){|x| x } end ensure d2.stop; d2.before_shutdown; d2.shutdown; d2.after_shutdown; d2.close; d2.terminate end end data( 'server_create tcp' => [:server_create, :tcp, {}], 'server_create udp' => [:server_create, :udp, {max_bytes: 128}], 'server_create tls' => [:server_create, :tls, {tls_options: {insecure: true}}], # 'server_create unix' => [:server_create, :unix, {}], 'server_create_connection tcp' => [:server_create, :tcp, {}], 'server_create_connection tls' => [:server_create, :tls, {tls_options: {insecure: true}}], # 'server_create_connection unix' => [:server_create, :unix, {}], ) test 'cannot create 2 or more servers using same bind address and port if shared option is false' do |(m, proto, kwargs)| begin d2 = Dummy.new; d2.start; d2.after_start assert_nothing_raised do @d.__send__(m, :myserver, PORT, proto: proto, shared: false, **kwargs){|x| x } end assert_raise(Errno::EADDRINUSE, Errno::EACCES) do d2.__send__(m, :myserver, PORT, proto: proto, **kwargs){|x| x } end ensure d2.stop; d2.before_shutdown; d2.shutdown; d2.after_shutdown; d2.close; d2.terminate end end end sub_test_case '#server_create' do data( 'tcp' => [:tcp, {}], 'udp' => [:udp, {max_bytes: 128}], 'tls' => [:tls, {tls_options: {insecure: true}}], # 'unix' => [:unix, {}], ) test 'raise error if block argument is not specified or too many' do |(proto, kwargs)| assert_raise(ArgumentError.new("BUG: block must have 1 or 2 arguments")) do @d.server_create(:myserver, PORT, proto: proto, **kwargs){ 1 } end assert_raise(ArgumentError.new("BUG: block must have 1 or 2 arguments")) do @d.server_create(:myserver, PORT, proto: proto, **kwargs){|sock, conn, what_is_this| 1 } end end test 'creates udp server if specified in proto' do @d.server_create(:myserver, PORT, proto: :udp, max_bytes: 512){|x| x } created_server_info = @d._servers.first assert_equal :udp, created_server_info.proto created_server = created_server_info.server assert created_server.is_a?(Fluent::PluginHelper::Server::EventHandler::UDPServer) end end sub_test_case '#server_create_tcp' do test 'can accept all keyword arguments valid for tcp server' do assert_nothing_raised do @d.server_create_tcp(:s, PORT, bind: '127.0.0.1', shared: false, resolve_name: true, linger_timeout: 10, backlog: 500) do |data, conn| # ... end end end test 'creates a tcp server just to read data' do received = "" @d.server_create_tcp(:s, PORT) do |data| received << data end 3.times do sock = TCPSocket.new("127.0.0.1", PORT) sock.puts "yay" sock.puts "foo" sock.close end waiting(10){ sleep 0.1 until received.bytesize == 24 } assert_equal "yay\nfoo\nyay\nfoo\nyay\nfoo\n", received end test 'creates a tcp server to read and write data' do received = "" responses = [] @d.server_create_tcp(:s, PORT) do |data, conn| received << data conn.write "ack\n" end 3.times do TCPSocket.open("127.0.0.1", PORT) do |sock| sock.puts "yay" sock.puts "foo" responses << sock.readline end end waiting(10){ sleep 0.1 until received.bytesize == 24 } assert_equal "yay\nfoo\nyay\nfoo\nyay\nfoo\n", received assert_equal ["ack\n","ack\n","ack\n"], responses end test 'creates a tcp server to read and write data using IPv6' do omit "IPv6 unavailable here" unless ipv6_enabled? received = "" responses = [] @d.server_create_tcp(:s, PORT, bind: "::1") do |data, conn| received << data conn.write "ack\n" end 3.times do TCPSocket.open("::1", PORT) do |sock| sock.puts "yay" sock.puts "foo" responses << sock.readline end end waiting(10){ sleep 0.1 until received.bytesize == 24 } assert_equal "yay\nfoo\nyay\nfoo\nyay\nfoo\n", received assert_equal ["ack\n","ack\n","ack\n"], responses end test 'does not resolve name of client address in default' do received = "" sources = [] @d.server_create_tcp(:s, PORT) do |data, conn| received << data sources << conn.remote_host end 3.times do TCPSocket.open("127.0.0.1", PORT) do |sock| sock.puts "yay" end end waiting(10){ sleep 0.1 until received.bytesize == 12 } assert_equal "yay\nyay\nyay\n", received assert{ sources.all?{|s| s == "127.0.0.1" } } end test 'does resolve name of client address if resolve_name is true' do hostname = Socket.getnameinfo([nil, nil, nil, "127.0.0.1"])[0] received = "" sources = [] @d.server_create_tcp(:s, PORT, resolve_name: true) do |data, conn| received << data sources << conn.remote_host end 3.times do TCPSocket.open("127.0.0.1", PORT) do |sock| sock.puts "yay" end end waiting(10){ sleep 0.1 until received.bytesize == 12 } assert_equal "yay\nyay\nyay\n", received assert{ sources.all?{|s| s == hostname } } end test 'can keep connections alive for tcp if keepalive specified' do # pend "not implemented yet" end test 'raises error if plugin registers data callback for connection object from #server_create' do received = "" errors = [] @d.server_create_tcp(:s, PORT) do |data, conn| received << data begin conn.data{|d| received << d.upcase } rescue => e errors << e end end TCPSocket.open("127.0.0.1", PORT) do |sock| sock.puts "foo" end waiting(10){ sleep 0.1 until received.bytesize == 4 || errors.size == 1 } assert_equal "foo\n", received assert{ errors.size > 0 } # it might be called twice (or more) when connection was accepted, and then data arrived (or more) assert_equal "data callback can be registered just once, but registered twice", errors.first.message end test 'can call write_complete callback if registered' do buffer = "" lines = [] responses = [] response_completes = [] @d.server_create_tcp(:s, PORT) do |data, conn| conn.on(:write_complete){|c| response_completes << true } buffer << data if idx = buffer.index("\n") lines << buffer.slice!(0,idx+1) conn.write "ack\n" end end 3.times do TCPSocket.open("127.0.0.1", PORT) do |sock| sock.write "yay" sock.write "foo\n" begin responses << sock.readline rescue EOFError, IOError, Errno::ECONNRESET # ignore end sock.close end end waiting(10){ sleep 0.1 until lines.size == 3 && response_completes.size == 3 } assert_equal ["yayfoo\n", "yayfoo\n", "yayfoo\n"], lines assert_equal ["ack\n","ack\n","ack\n"], responses assert_equal [true, true, true], response_completes end test 'can call close callback if registered' do buffer = "" lines = [] callback_results = [] @d.server_create_tcp(:s, PORT) do |data, conn| conn.on(:close){|c| callback_results << "closed" } buffer << data if idx = buffer.index("\n") lines << buffer.slice!(0,idx+1) conn.write "ack\n" end end 3.times do TCPSocket.open("127.0.0.1", PORT) do |sock| sock.write "yay" sock.write "foo\n" begin while line = sock.readline if line == "ack\n" sock.close end end rescue EOFError, IOError, Errno::ECONNRESET # ignore end end end waiting(10){ sleep 0.1 until lines.size == 3 && callback_results.size == 3 } assert_equal ["yayfoo\n", "yayfoo\n", "yayfoo\n"], lines assert_equal ["closed", "closed", "closed"], callback_results end end sub_test_case '#server_create_udp' do test 'can accept all keyword arguments valid for udp server' do assert_nothing_raised do @d.server_create_udp(:s, PORT, bind: '127.0.0.1', shared: false, resolve_name: true, max_bytes: 100, flags: 1) do |data, conn| # ... end end end test 'creates a udp server just to read data' do received = "" @d.server_create_udp(:s, PORT, max_bytes: 128) do |data| received << data end bind_port = unused_port(protocol: :udp, bind: "127.0.0.1") 3.times do sock = UDPSocket.new(Socket::AF_INET) sock.bind("127.0.0.1", bind_port) sock.connect("127.0.0.1", PORT) sock.puts "yay" sock.puts "foo" sock.close end waiting(10){ sleep 0.1 until received.bytesize == 24 } assert_equal "yay\nfoo\nyay\nfoo\nyay\nfoo\n", received end test 'creates a udp server to read and write data' do received = "" responses = [] @d.server_create_udp(:s, PORT, max_bytes: 128) do |data, sock| received << data sock.write "ack\n" end bind_port = unused_port 3.times do begin sock = UDPSocket.new(Socket::AF_INET) sock.bind("127.0.0.1", bind_port) sock.connect("127.0.0.1", PORT) th = Thread.new do while true begin in_data, _addr = sock.recvfrom_nonblock(16) if in_data responses << in_data break end rescue IO::WaitReadable IO.select([sock]) end end true end sock.write "yay\nfoo\n" th.join(5) ensure sock.close end end waiting(10){ sleep 0.1 until received.bytesize == 24 } assert_equal "yay\nfoo\nyay\nfoo\nyay\nfoo\n", received assert_equal ["ack\n","ack\n","ack\n"], responses end test 'creates a udp server to read and write data using IPv6' do omit "IPv6 unavailable here" unless ipv6_enabled? received = "" responses = [] @d.server_create_udp(:s, PORT, bind: "::1", max_bytes: 128) do |data, sock| received << data sock.write "ack\n" end bind_port = unused_port 3.times do begin sock = UDPSocket.new(Socket::AF_INET6) sock.bind("::1", bind_port) th = Thread.new do responses << sock.recv(16) true end sock.connect("::1", PORT) sock.write "yay\nfoo\n" th.join(5) ensure sock.close end end waiting(10){ sleep 0.1 until received.bytesize == 24 } assert_equal "yay\nfoo\nyay\nfoo\nyay\nfoo\n", received assert_equal ["ack\n","ack\n","ack\n"], responses end test 'does not resolve name of client address in default' do received = "" sources = [] @d.server_create_udp(:s, PORT, max_bytes: 128) do |data, sock| received << data sources << sock.remote_host end 3.times do sock = UDPSocket.new(Socket::AF_INET) sock.connect("127.0.0.1", PORT) sock.puts "yay" sock.close end waiting(10){ sleep 0.1 until received.bytesize == 12 } assert_equal "yay\nyay\nyay\n", received assert{ sources.all?{|s| s == "127.0.0.1" } } end test 'does resolve name of client address if resolve_name is true' do hostname = Socket.getnameinfo([nil, nil, nil, "127.0.0.1"])[0] received = "" sources = [] @d.server_create_udp(:s, PORT, resolve_name: true, max_bytes: 128) do |data, sock| received << data sources << sock.remote_host end 3.times do sock = UDPSocket.new(Socket::AF_INET) sock.connect("127.0.0.1", PORT) sock.puts "yay" sock.close end waiting(10){ sleep 0.1 until received.bytesize == 12 } assert_equal "yay\nyay\nyay\n", received assert{ sources.all?{|s| s == hostname } } end test 'raises error if plugin registers data callback for connection object from #server_create' do received = "" errors = [] @d.server_create_udp(:s, PORT, max_bytes: 128) do |data, sock| received << data begin sock.data{|d| received << d.upcase } rescue => e errors << e end end sock = UDPSocket.new(Socket::AF_INET) sock.connect("127.0.0.1", PORT) sock.write "foo\n" sock.close waiting(10){ sleep 0.1 until received.bytesize == 4 && errors.size == 1 } assert_equal "foo\n", received assert_equal 1, errors.size assert_equal "BUG: this event is disabled for udp: data", errors.first.message end test 'raise error if plugin registers write_complete callback for udp' do received = "" errors = [] @d.server_create_udp(:s, PORT, max_bytes: 128) do |data, sock| received << data begin sock.on(:write_complete){|conn| "" } rescue => e errors << e end end sock = UDPSocket.new(Socket::AF_INET) sock.connect("127.0.0.1", PORT) sock.write "foo\n" sock.close waiting(10){ sleep 0.1 until received.bytesize == 4 && errors.size == 1 } assert_equal "foo\n", received assert_equal 1, errors.size assert_equal "BUG: this event is disabled for udp: write_complete", errors.first.message end test 'raises error if plugin registers close callback for udp' do received = "" errors = [] @d.server_create_udp(:s, PORT, max_bytes: 128) do |data, sock| received << data begin sock.on(:close){|d| "" } rescue => e errors << e end end sock = UDPSocket.new(Socket::AF_INET) sock.connect("127.0.0.1", PORT) sock.write "foo\n" sock.close waiting(10){ sleep 0.1 until received.bytesize == 4 && errors.size == 1 } assert_equal "foo\n", received assert_equal 1, errors.size assert_equal "BUG: this event is disabled for udp: close", errors.first.message end end module CertUtil extend Fluent::PluginHelper::CertOption end def create_ca_options { private_key_length: 2048, country: 'US', state: 'CA', locality: 'Mountain View', common_name: 'ca.testing.fluentd.org', expiration: 30 * 86400, digest: :sha256, } end def create_server_options { private_key_length: 2048, country: 'US', state: 'CA', locality: 'Mountain View', common_name: 'server.testing.fluentd.org', expiration: 30 * 86400, digest: :sha256, } end def write_cert_and_key(cert_path, cert, key_path, key, passphrase) File.open(cert_path, "w"){|f| f.write(cert.to_pem) } # Write the secret key (raw or ecnrypted by AES256) in PEM format key_str = passphrase ? key.export(OpenSSL::Cipher.new("AES-256-CBC"), passphrase) : key.export File.open(key_path, "w"){|f| f.write(key_str) } File.chmod(0600, cert_path, key_path) end def create_server_pair_signed_by_self(cert_path, private_key_path, passphrase) cert, key, _ = CertUtil.cert_option_generate_server_pair_self_signed(create_server_options) write_cert_and_key(cert_path, cert, private_key_path, key, passphrase) end def create_ca_pair_signed_by_self(cert_path, private_key_path, passphrase) cert, key, _ = CertUtil.cert_option_generate_ca_pair_self_signed(create_ca_options) write_cert_and_key(cert_path, cert, private_key_path, key, passphrase) end def create_server_pair_signed_by_ca(ca_cert_path, ca_key_path, ca_key_passphrase, cert_path, private_key_path, passphrase) cert, key, _ = CertUtil.cert_option_generate_server_pair_by_ca(ca_cert_path, ca_key_path, ca_key_passphrase, create_server_options) write_cert_and_key(cert_path, cert, private_key_path, key, passphrase) end def create_server_pair_chained_with_root_ca(ca_cert_path, ca_key_path, ca_key_passphrase, cert_path, private_key_path, passphrase) root_cert, root_key, _ = CertUtil.cert_option_generate_ca_pair_self_signed(create_ca_options) write_cert_and_key(ca_cert_path, root_cert, ca_key_path, root_key, ca_key_passphrase) intermediate_ca_options = create_ca_options intermediate_ca_options[:common_name] = 'ca2.testing.fluentd.org' chain_cert, chain_key = CertUtil.cert_option_generate_pair(intermediate_ca_options, root_cert.subject) chain_cert.add_extension OpenSSL::X509::Extension.new('basicConstraints', OpenSSL::ASN1.Sequence([OpenSSL::ASN1::Boolean(true)])) chain_cert.sign(root_key, "sha256") server_cert, server_key, _ = CertUtil.cert_option_generate_pair(create_server_options, chain_cert.subject) server_cert.add_extension OpenSSL::X509::Extension.new('basicConstraints', OpenSSL::ASN1.Sequence([OpenSSL::ASN1::Boolean(false)])) server_cert.add_extension OpenSSL::X509::Extension.new('nsCertType', 'server') server_cert.sign(chain_key, "sha256") # write chained cert File.open(cert_path, "w") do |f| f.write server_cert.to_pem f.write chain_cert.to_pem end key_str = passphrase ? server_key.export(OpenSSL::Cipher.new("AES-256-CBC"), passphrase) : server_key.export File.open(private_key_path, "w"){|f| f.write(key_str) } File.chmod(0600, cert_path, private_key_path) end def open_tls_session(addr, port, verify: true, cert_path: nil, selfsigned: true, hostname: nil) context = OpenSSL::SSL::SSLContext.new context.set_params({}) if verify cert_store = OpenSSL::X509::Store.new cert_store.set_default_paths if selfsigned && OpenSSL::X509.const_defined?('V_FLAG_CHECK_SS_SIGNATURE') cert_store.flags = OpenSSL::X509::V_FLAG_CHECK_SS_SIGNATURE end if cert_path cert_store.add_file(cert_path) end context.verify_mode = OpenSSL::SSL::VERIFY_PEER context.cert_store = cert_store if !hostname && context.respond_to?(:verify_hostname=) context.verify_hostname = false # In test code, using hostname to be connected is very difficult end else context.verify_mode = OpenSSL::SSL::VERIFY_NONE end sock = OpenSSL::SSL::SSLSocket.new(TCPSocket.new(addr, port), context) sock.hostname = hostname if hostname && sock.respond_to?(:hostname) sock.connect yield sock ensure sock.close rescue nil end sub_test_case '#server_create_tls with various certificate options' do setup do @d = Dummy.new # to get plugin not configured/started yet @certs_dir = File.join(TMP_DIR, "tls_certs") @server_cert_dir = File.join(@certs_dir, "server") FileUtils.rm_rf @certs_dir FileUtils.mkdir_p @server_cert_dir end sub_test_case 'using tls_options arguments to specify cert options' do setup do @d.configure(config_element()); @d.start; @d.after_start end test 'create dynamic self-signed cert/key pair (without any verification from clients)' do # insecure tls_options = { protocol: :tls, version: 'TLSv1_2', ciphers: 'ALL:!aNULL:!eNULL:!SSLv2', insecure: true, generate_private_key_length: 2048, generate_cert_country: 'US', generate_cert_state: 'CA', generate_cert_locality: 'Mountain View', generate_cert_common_name: 'myserver.testing.fluentd.org', generate_cert_expiration: 10 * 365 * 86400, generate_cert_digest: :sha256, } received = "" @d.server_create_tls(:s, PORT, tls_options: tls_options) do |data, conn| received << data end assert_raise "" do open_tls_session('127.0.0.1', PORT) do |sock| sock.post_connection_check('myserver.testing.fluentd.org') # cannot connect .... end end open_tls_session('127.0.0.1', PORT, verify: false) do |sock| sock.puts "yay" sock.puts "foo" end waiting(10){ sleep 0.1 until received.bytesize == 8 } assert_equal "yay\nfoo\n", received end data('with passphrase' => 'yaaaaaaaaaaaaaaaaaaay', 'without passphrase' => nil) test 'load self-signed cert/key pair (files), verified from clients using cert files' do |private_key_passphrase| cert_path = File.join(@server_cert_dir, "cert.pem") private_key_path = File.join(@certs_dir, "server.key.pem") create_server_pair_signed_by_self(cert_path, private_key_path, private_key_passphrase) tls_options = { protocol: :tls, version: 'TLSv1_2', ciphers: 'ALL:!aNULL:!eNULL:!SSLv2', insecure: false, cert_path: cert_path, private_key_path: private_key_path, } tls_options[:private_key_passphrase] = private_key_passphrase if private_key_passphrase received = "" @d.server_create_tls(:s, PORT, tls_options: tls_options) do |data, conn| received << data end assert_raise "" do open_tls_session('127.0.0.1', PORT) do |sock| sock.post_connection_check('server.testing.fluentd.org') # cannot connect by failing verification without server cert end end open_tls_session('127.0.0.1', PORT, cert_path: cert_path) do |sock| sock.puts "yay" sock.puts "foo" end waiting(10){ sleep 0.1 until received.bytesize == 8 } assert_equal "yay\nfoo\n", received end data('with passphrase' => "fooooooooooooooooooooooooo", 'without passphrase' => nil) test 'create dynamic server cert by private CA cert file, verified from clients using CA cert file' do |ca_key_passphrase| ca_cert_path = File.join(@certs_dir, "ca_cert.pem") ca_key_path = File.join(@certs_dir, "ca.key.pem") create_ca_pair_signed_by_self(ca_cert_path, ca_key_path, ca_key_passphrase) tls_options = { protocol: :tls, version: 'TLSv1_2', ciphers: 'ALL:!aNULL:!eNULL:!SSLv2', insecure: false, ca_cert_path: ca_cert_path, ca_private_key_path: ca_key_path, generate_private_key_length: 2048, } tls_options[:ca_private_key_passphrase] = ca_key_passphrase if ca_key_passphrase received = "" @d.server_create_tls(:s, PORT, tls_options: tls_options) do |data, conn| received << data end open_tls_session('127.0.0.1', PORT, cert_path: ca_cert_path) do |sock| sock.puts "yay" sock.puts "foo" end waiting(10){ sleep 0.1 until received.bytesize == 8 } assert_equal "yay\nfoo\n", received end data('with passphrase' => ["foooooooo", "yaaaaaaaaaaaaaaaaaaay"], 'without passphrase' => [nil, nil]) test 'load static server cert by private CA cert file, verified from clients using CA cert file' do |(ca_key_passphrase, private_key_passphrase)| ca_cert_path = File.join(@certs_dir, "ca_cert.pem") ca_key_path = File.join(@certs_dir, "ca.key.pem") create_ca_pair_signed_by_self(ca_cert_path, ca_key_path, ca_key_passphrase) cert_path = File.join(@server_cert_dir, "cert.pem") private_key_path = File.join(@certs_dir, "server.key.pem") create_server_pair_signed_by_ca(ca_cert_path, ca_key_path, ca_key_passphrase, cert_path, private_key_path, private_key_passphrase) tls_options = { protocol: :tls, version: 'TLSv1_2', ciphers: 'ALL:!aNULL:!eNULL:!SSLv2', insecure: false, cert_path: cert_path, private_key_path: private_key_path, } tls_options[:private_key_passphrase] = private_key_passphrase if private_key_passphrase received = "" @d.server_create_tls(:s, PORT, tls_options: tls_options) do |data, conn| received << data end open_tls_session('127.0.0.1', PORT, cert_path: ca_cert_path) do |sock| sock.puts "yay" sock.puts "foo" end waiting(10){ sleep 0.1 until received.bytesize == 8 } assert_equal "yay\nfoo\n", received end data('with passphrase' => ["foooooooo", "yaaaaaaaaaaaaaaaaaaay"], 'without passphrase' => [nil, nil]) test 'load chained server cert by private CA cert file, verified from clients using CA cert file as root' do |(ca_key_passphrase, private_key_passphrase)| ca_cert_path = File.join(@certs_dir, "ca_cert.pem") ca_key_path = File.join(@certs_dir, "ca.key.pem") cert_path = File.join(@server_cert_dir, "cert.pem") private_key_path = File.join(@certs_dir, "server.key.pem") create_server_pair_chained_with_root_ca(ca_cert_path, ca_key_path, ca_key_passphrase, cert_path, private_key_path, private_key_passphrase) tls_options = { protocol: :tls, version: 'TLSv1_2', ciphers: 'ALL:!aNULL:!eNULL:!SSLv2', insecure: false, cert_path: cert_path, private_key_path: private_key_path, } tls_options[:private_key_passphrase] = private_key_passphrase if private_key_passphrase received = "" @d.server_create_tls(:s, PORT, tls_options: tls_options) do |data, conn| received << data end open_tls_session('127.0.0.1', PORT, cert_path: ca_cert_path) do |sock| sock.puts "yay" sock.puts "foo" end waiting(10){ sleep 0.1 until received.bytesize == 8 } assert_equal "yay\nfoo\n", received end end sub_test_case 'using configurations to specify cert options' do test 'create dynamic self-signed cert/key pair (without any verification from clients)' do # insecure transport_opts = { 'insecure' => 'true', } transport_conf = config_element('transport', 'tls', transport_opts) conf = config_element('match', 'tag.*', {}, [transport_conf]) @d.configure(conf); @d.start; @d.after_start received = "" @d.server_create_tls(:s, PORT) do |data, conn| received << data end assert_raise "" do open_tls_session('127.0.0.1', PORT) do |sock| sock.post_connection_check('myserver.testing.fluentd.org') # cannot connect .... end end open_tls_session('127.0.0.1', PORT, verify: false) do |sock| sock.puts "yay" sock.puts "foo" end waiting(10){ sleep 0.1 until received.bytesize == 8 } assert_equal "yay\nfoo\n", received end data('with passphrase' => "yaaaaaaaaaaaaaaaaaaay", 'without passphrase' => nil) test 'load self-signed cert/key pair (files), verified from clients using cert files' do |private_key_passphrase| cert_path = File.join(@server_cert_dir, "cert.pem") private_key_path = File.join(@certs_dir, "server.key.pem") create_server_pair_signed_by_self(cert_path, private_key_path, private_key_passphrase) transport_opts = { 'cert_path' => cert_path, 'private_key_path' => private_key_path, } transport_opts['private_key_passphrase'] = private_key_passphrase if private_key_passphrase transport_conf = config_element('transport', 'tls', transport_opts) conf = config_element('match', 'tag.*', {}, [transport_conf]) @d.configure(conf); @d.start; @d.after_start received = "" @d.server_create_tls(:s, PORT) do |data, conn| received << data end assert_raise "" do open_tls_session('127.0.0.1', PORT) do |sock| sock.post_connection_check('server.testing.fluentd.org') # cannot connect by failing verification without server cert end end open_tls_session('127.0.0.1', PORT, cert_path: cert_path) do |sock| sock.puts "yay" sock.puts "foo" end waiting(10){ sleep 0.1 until received.bytesize == 8 } assert_equal "yay\nfoo\n", received end data('with passphrase' => "fooooooooooooooooooooooooo", 'without passphrase' => nil) test 'create dynamic server cert by private CA cert file, verified from clients using CA cert file' do |ca_key_passphrase| ca_cert_path = File.join(@certs_dir, "ca_cert.pem") ca_key_path = File.join(@certs_dir, "ca.key.pem") create_ca_pair_signed_by_self(ca_cert_path, ca_key_path, ca_key_passphrase) transport_opts = { 'ca_cert_path' => ca_cert_path, 'ca_private_key_path' => ca_key_path, } transport_opts['ca_private_key_passphrase'] = ca_key_passphrase if ca_key_passphrase transport_conf = config_element('transport', 'tls', transport_opts) conf = config_element('match', 'tag.*', {}, [transport_conf]) @d.configure(conf); @d.start; @d.after_start received = "" @d.server_create_tls(:s, PORT) do |data, conn| received << data end open_tls_session('127.0.0.1', PORT, cert_path: ca_cert_path) do |sock| sock.puts "yay" sock.puts "foo" end waiting(10){ sleep 0.1 until received.bytesize == 8 } assert_equal "yay\nfoo\n", received end data('with passphrase' => ["foooooooo", "yaaaaaaaaaaaaaaaaaaay"], 'without passphrase' => [nil, nil]) test 'load static server cert by private CA cert file, verified from clients using CA cert file' do |(ca_key_passphrase, private_key_passphrase)| ca_cert_path = File.join(@certs_dir, "ca_cert.pem") ca_key_path = File.join(@certs_dir, "ca.key.pem") create_ca_pair_signed_by_self(ca_cert_path, ca_key_path, ca_key_passphrase) cert_path = File.join(@server_cert_dir, "cert.pem") private_key_path = File.join(@certs_dir, "server.key.pem") create_server_pair_signed_by_ca(ca_cert_path, ca_key_path, ca_key_passphrase, cert_path, private_key_path, private_key_passphrase) transport_opts = { 'cert_path' => cert_path, 'private_key_path' => private_key_path, } transport_opts['private_key_passphrase'] = private_key_passphrase if private_key_passphrase transport_conf = config_element('transport', 'tls', transport_opts) conf = config_element('match', 'tag.*', {}, [transport_conf]) @d.configure(conf); @d.start; @d.after_start received = "" @d.server_create_tls(:s, PORT) do |data, conn| received << data end open_tls_session('127.0.0.1', PORT, cert_path: ca_cert_path) do |sock| sock.puts "yay" sock.puts "foo" end waiting(10){ sleep 0.1 until received.bytesize == 8 } assert_equal "yay\nfoo\n", received end data('with passphrase' => ["foooooooo", "yaaaaaaaaaaaaaaaaaaay"], 'without passphrase' => [nil, nil]) test 'load chained server cert by private CA cert file, verified from clients using CA cert file as root' do |(ca_key_passphrase, private_key_passphrase)| ca_cert_path = File.join(@certs_dir, "ca_cert.pem") ca_key_path = File.join(@certs_dir, "ca.key.pem") cert_path = File.join(@server_cert_dir, "cert.pem") private_key_path = File.join(@certs_dir, "server.key.pem") create_server_pair_chained_with_root_ca(ca_cert_path, ca_key_path, ca_key_passphrase, cert_path, private_key_path, private_key_passphrase) transport_opts = { 'cert_path' => cert_path, 'private_key_path' => private_key_path, } transport_opts['private_key_passphrase'] = private_key_passphrase if private_key_passphrase transport_conf = config_element('transport', 'tls', transport_opts) conf = config_element('match', 'tag.*', {}, [transport_conf]) @d.configure(conf); @d.start; @d.after_start received = "" @d.server_create_tls(:s, PORT) do |data, conn| received << data end open_tls_session('127.0.0.1', PORT, cert_path: ca_cert_path) do |sock| sock.puts "yay" sock.puts "foo" end waiting(10){ sleep 0.1 until received.bytesize == 8 } assert_equal "yay\nfoo\n", received end end end sub_test_case '#server_create_tls' do setup do @certs_dir = File.join(TMP_DIR, "tls_certs") FileUtils.rm_rf @certs_dir FileUtils.mkdir_p @certs_dir @server_cert_dir = File.join(@certs_dir, "server") FileUtils.mkdir_p @server_cert_dir @cert_path = File.join(@server_cert_dir, "cert.pem") private_key_path = File.join(@certs_dir, "server.key.pem") private_key_passphrase = "yaaaaaaaaaaaaaaaaaaay" create_server_pair_signed_by_self(@cert_path, private_key_path, private_key_passphrase) @default_hostname = ::Socket.gethostname @tls_options = { protocol: :tls, version: 'TLSv1_2', ciphers: 'ALL:!aNULL:!eNULL:!SSLv2', insecure: false, cert_path: @cert_path, private_key_path: private_key_path, private_key_passphrase: private_key_passphrase, } end test 'can accept all keyword arguments valid for tcp/tls server' do assert_nothing_raised do @d.server_create_tls(:s, PORT, bind: '127.0.0.1', shared: false, resolve_name: true, linger_timeout: 10, backlog: 500, tls_options: @tls_options) do |data, conn| # ... end end end test 'creates a tls server just to read data' do received = "" @d.server_create_tls(:s, PORT, tls_options: @tls_options) do |data, conn| received << data end 3.times do open_tls_session('127.0.0.1', PORT, cert_path: @cert_path) do |sock| sock.puts "yay" sock.puts "foo" end end waiting(10){ sleep 0.1 until received.bytesize == 24 } assert_equal 3, received.scan("yay\n").size assert_equal 3, received.scan("foo\n").size end test 'creates a tls server to read and write data' do received = "" responses = [] @d.server_create_tls(:s, PORT, tls_options: @tls_options) do |data, conn| received << data conn.write "ack\n" end 3.times do # open_tls_session('127.0.0.1', PORT, cert_path: @cert_path, hostname: @default_hostname) do |sock| open_tls_session('127.0.0.1', PORT, cert_path: @cert_path) do |sock| sock.puts "yay" sock.puts "foo" responses << sock.readline end end waiting(10){ sleep 0.1 until received.bytesize == 24 } assert_equal 3, received.scan("yay\n").size assert_equal 3, received.scan("foo\n").size assert_equal ["ack\n","ack\n","ack\n"], responses end test 'creates a tls server to read and write data using IPv6' do omit "IPv6 unavailable here" unless ipv6_enabled? received = "" responses = [] @d.server_create_tls(:s, PORT, bind: "::1", tls_options: @tls_options) do |data, conn| received << data conn.write "ack\n" end 3.times do # open_tls_session('::1', PORT, cert_path: @cert_path, hostname: @default_hostname) do |sock| open_tls_session('::1', PORT, cert_path: @cert_path) do |sock| sock.puts "yay" sock.puts "foo" responses << sock.readline end end waiting(10){ sleep 0.1 until received.bytesize == 24 } assert_equal 3, received.scan("yay\n").size assert_equal 3, received.scan("foo\n").size assert_equal ["ack\n","ack\n","ack\n"], responses end test 'does not resolve name of client address in default' do received = "" sources = [] @d.server_create_tls(:s, PORT, tls_options: @tls_options) do |data, conn| received << data sources << conn.remote_host end 3.times do # open_tls_session('127.0.0.1', PORT, cert_path: @cert_path, hostname: @default_hostname) do |sock| open_tls_session('127.0.0.1', PORT, cert_path: @cert_path) do |sock| sock.puts "yay" end end waiting(10){ sleep 0.1 until received.bytesize == 12 } assert_equal 3, received.scan("yay\n").size assert{ sources.all?{|s| s == "127.0.0.1" } } end test 'does resolve name of client address if resolve_name is true' do hostname = Socket.getnameinfo([nil, nil, nil, "127.0.0.1"])[0] received = "" sources = [] @d.server_create_tls(:s, PORT, resolve_name: true, tls_options: @tls_options) do |data, conn| received << data sources << conn.remote_host end 3.times do # open_tls_session('127.0.0.1', PORT, cert_path: @cert_path, hostname: @default_hostname) do |sock| open_tls_session('127.0.0.1', PORT, cert_path: @cert_path) do |sock| sock.puts "yay" end end waiting(10){ sleep 0.1 until received.bytesize == 12 } assert_equal 3, received.scan("yay\n").size assert{ sources.all?{|s| s == hostname } } end test 'can keep connections alive for tls if keepalive specified' do # pend "not implemented yet" end test 'raises error if plugin registers data callback for connection object from #server_create' do received = "" errors = [] @d.server_create_tls(:s, PORT, tls_options: @tls_options) do |data, conn| received << data begin conn.data{|d| received << d.upcase } rescue => e errors << e end end open_tls_session('127.0.0.1', PORT, cert_path: @cert_path) do |sock| sock.puts "foo" end waiting(10){ sleep 0.1 until received.bytesize == 4 || errors.size == 1 } assert_equal "foo\n", received assert_equal 1, errors.size assert_equal "data callback can be registered just once, but registered twice", errors.first.message end test 'can call write_complete callback if registered' do buffer = "" lines = [] responses = [] response_completes = [] @d.server_create_tls(:s, PORT, tls_options: @tls_options) do |data, conn| conn.on(:write_complete){|c| response_completes << true } buffer << data if idx = buffer.index("\n") lines << buffer.slice!(0,idx+1) conn.write "ack\n" end end 3.times do open_tls_session('127.0.0.1', PORT, cert_path: @cert_path) do |sock| sock.write "yay" sock.write "foo\n" begin responses << sock.readline rescue EOFError, IOError, Errno::ECONNRESET # ignore end sock.close end end waiting(10){ sleep 0.1 until lines.size == 3 && response_completes.size == 3 } assert_equal ["yayfoo\n", "yayfoo\n", "yayfoo\n"], lines assert_equal ["ack\n","ack\n","ack\n"], responses assert_equal [true, true, true], response_completes end test 'can call close callback if registered' do buffer = "" lines = [] callback_results = [] @d.server_create_tls(:s, PORT, tls_options: @tls_options) do |data, conn| conn.on(:close){|c| callback_results << "closed" } buffer << data if idx = buffer.index("\n") lines << buffer.slice!(0,idx+1) conn.write "ack\n" end end 3.times do open_tls_session('127.0.0.1', PORT, cert_path: @cert_path) do |sock| sock.write "yay" sock.write "foo\n" begin while line = sock.readline if line == "ack\n" sock.close end end rescue EOFError, IOError, Errno::ECONNRESET # ignore end end end waiting(10){ sleep 0.1 until lines.size == 3 && callback_results.size == 3 } assert_equal ["yayfoo\n", "yayfoo\n", "yayfoo\n"], lines assert_equal ["closed", "closed", "closed"], callback_results end end sub_test_case '#server_create_unix' do # not implemented yet # test 'can accept all keyword arguments valid for unix server' # test 'creates a unix server just to read data' # test 'creates a unix server to read and write data' # test 'raises error if plugin registers data callback for connection object from #server_create' # test 'can call write_complete callback if registered' # test 'can call close callback if registered' end def open_client(proto, addr, port) client = case proto when :tcp TCPSocket.open(addr, port) when :tls c = OpenSSL::SSL::SSLSocket.new(TCPSocket.open(addr, port)) c.sync_close = true c.connect else raise ArgumentError, "unknown proto:#{proto}" end yield client ensure client.close rescue nil end # run tests for tcp, tls and unix sub_test_case '#server_create_connection' do test 'raise error if udp is specified in proto' do assert_raise(ArgumentError.new("BUG: cannot create connection for UDP")) do @d.server_create_connection(:myserver, PORT, proto: :udp){|c| c } end end # def server_create_connection(title, port, proto: :tcp, bind: '0.0.0.0', shared: true, tls_options: nil, resolve_name: false, linger_timeout: 0, backlog: nil, &block) protocols = { 'tcp' => [:tcp, {}], 'tls' => [:tls, {tls_options: {insecure: true}}], # 'unix' => [:unix, {path: ""}], } data(protocols) test 'raise error if block argument is not specified or too many' do |(proto, kwargs)| empty_block = ->(){} assert_raise(ArgumentError.new("BUG: block must have just one argument")) do @d.server_create_connection(:myserver, PORT, proto: proto, **kwargs, &empty_block) end assert_raise(ArgumentError.new("BUG: block must have just one argument")) do @d.server_create_connection(:myserver, PORT, proto: proto, **kwargs){|conn, what_is_this| [conn, what_is_this] } end end data(protocols) test 'does not resolve name of client address in default' do |(proto, kwargs)| received = "" sources = [] @d.server_create_connection(:s, PORT, proto: proto, **kwargs) do |conn| sources << conn.remote_host conn.data do |d| received << d end end 3.times do open_client(proto, "127.0.0.1", PORT) do |sock| sock.puts "yay" end end waiting(10){ sleep 0.1 until received.bytesize == 12 } assert_equal "yay\nyay\nyay\n", received assert{ sources.all?{|s| s == "127.0.0.1" } } end data(protocols) test 'does resolve name of client address if resolve_name is true' do |(proto, kwargs)| hostname = Socket.getnameinfo([nil, nil, nil, "127.0.0.1"])[0] received = "" sources = [] @d.server_create_connection(:s, PORT, proto: proto, resolve_name: true, **kwargs) do |conn| sources << conn.remote_host conn.data do |d| received << d end end 3.times do open_client(proto, "127.0.0.1", PORT) do |sock| sock.puts "yay" end end waiting(10){ sleep 0.1 until received.bytesize == 12 } assert_equal "yay\nyay\nyay\n", received assert{ sources.all?{|s| s == hostname } } end data(protocols) test 'creates a server to provide connection, which can read, write and close' do |(proto, kwargs)| lines = [] buffer = "" @d.server_create_connection(:s, PORT, proto: proto, **kwargs) do |conn| conn.data do |d| buffer << d if buffer == "x" buffer.slice!(0, 1) conn.close end if idx = buffer.index("\n") lines << buffer.slice!(0, idx + 1) conn.write "foo!\n" end end end replied = [] disconnecteds = [] 3.times do |i| open_client(proto, "127.0.0.1", PORT) do |sock| sock.puts "yay" while line = sock.readline replied << line break end sock.write "x" connection_closed = false begin data = sock.read if data.empty? connection_closed = true end rescue => e if e.is_a?(Errno::ECONNRESET) connection_closed = true end ensure disconnecteds << connection_closed end end end waiting(10){ sleep 0.1 until lines.size == 3 } waiting(10){ sleep 0.1 until replied.size == 3 } waiting(10){ sleep 0.1 until disconnecteds.size == 3 } assert_equal ["yay\n", "yay\n", "yay\n"], lines assert_equal ["foo!\n", "foo!\n", "foo!\n"], replied assert_equal [true, true, true], disconnecteds end data(protocols) test 'creates a server to provide connection, which accepts callbacks for data, write_complete, and close' do |(proto, kwargs)| lines = [] buffer = "" written = 0 closed = 0 @d.server_create_connection(:s, PORT, proto: proto, **kwargs) do |conn| conn.on(:write_complete){|_conn| written += 1 } conn.on(:close){|_conn| closed += 1 } conn.on(:data) do |d| buffer << d if idx = buffer.index("\n") lines << buffer.slice!(0, idx + 1) conn.write "foo!\n" end end end replied = [] 3.times do open_client(proto, "127.0.0.1", PORT) do |sock| sock.puts "yay" while line = sock.readline replied << line break end end # TCP socket is closed here end waiting(10){ sleep 0.1 until lines.size == 3 } waiting(10){ sleep 0.1 until replied.size == 3 } waiting(10){ sleep 0.1 until closed == 3 } assert_equal ["yay\n", "yay\n", "yay\n"], lines assert_equal 3, written assert_equal 3, closed assert_equal ["foo!\n", "foo!\n", "foo!\n"], replied end data(protocols) test 'creates a server, and does not leak connections' do |(proto, kwargs)| buffer = "" closed = 0 @d.server_create_connection(:s, PORT, proto: proto, **kwargs) do |conn| conn.on(:close){|_c| closed += 1 } conn.on(:data) do |d| buffer << d end end 3.times do open_client(proto, "127.0.0.1", PORT) do |sock| sock.puts "yay" end end waiting(10){ sleep 0.1 until buffer.bytesize == 12 } waiting(10){ sleep 0.1 until closed == 3 } assert_equal 0, @d.instance_eval{ @_server_connections.size } end data(protocols) test 'will refuse more connect requests after stop, but read data from sockets already connected, in non-shared server' do |(proto, kwargs)| connected = false begin open_client(proto, "127.0.0.1", PORT) do |sock| # expected behavior is connection refused... connected = true end rescue end assert_false connected received = "" @d.server_create_connection(:s, PORT, proto: proto, shared: false, **kwargs) do |conn| conn.on(:data) do |data| received << data conn.write "ack\n" end end th0 = Thread.new do open_client(proto, "127.0.0.1", PORT) do |sock| sock.puts "yay" sock.readline end end value0 = waiting(5){ th0.value } assert_equal "ack\n", value0 stopped = false sleeping = false ending = false th1 = Thread.new do open_client(proto, "127.0.0.1", PORT) do |sock| sleeping = true sleep 0.1 until stopped sock.puts "yay" res = sock.readline ending = true res end end sleep 0.1 until sleeping @d.stop assert @d.stopped? stopped = true sleep 0.1 until ending @d.before_shutdown @d.shutdown th2 = Thread.new do begin open_client(proto, "127.0.0.1", PORT) do |sock| sock.puts "foo" end false # failed rescue true # success end end value1 = waiting(5){ th1.value } value2 = waiting(5){ th2.value } assert_equal "yay\nyay\n", received assert_equal "ack\n", value1 assert value2, "should be truthy value to show connection was correctly refused" end test 'can keep connections alive for tcp/tls if keepalive specified' do # pend "not implemented yet" end end end