# $ ruby -Ilib -Itest -rrubygems test/test_forward.rb # Tests for the following patch: # # http://github.com/net-ssh/net-ssh/tree/portfwfix # # It fixes 3 issues, regarding closing forwarded ports: # # 1.) if client closes a forwarded connection, but the server is reading, net-ssh terminates with IOError socket closed. # 2.) if client force closes (RST) a forwarded connection, but server is reading, net-ssh terminates with # 3.) if server closes the sending side, the on_eof is not handled. # # More info: # # http://net-ssh.lighthouseapp.com/projects/36253/tickets/7 require 'common' require 'net/ssh/buffer' require 'net/ssh' require 'timeout' require 'tempfile' class TestForward < Test::Unit::TestCase def localhost 'localhost' end def ssh_start_params [localhost ,ENV['USER'], {:keys => "~/.ssh/id_rsa", :verbose => :debug}] end def find_free_port server = TCPServer.open(0) server.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR,true) port = server.addr[1] server.close port end def start_server_sending_lot_of_data(exceptions) server = TCPServer.open(0) Thread.start do loop do Thread.start(server.accept) do |client| begin 10000.times do |i| client.puts "item#{i}" end client.close rescue exceptions << $! raise end end end end return server end def start_server_closing_soon(exceptions=nil) server = TCPServer.open(0) Thread.start do loop do Thread.start(server.accept) do |client| begin client.recv(1024) client.setsockopt(Socket::SOL_SOCKET, Socket::SO_LINGER, [1, 0].pack("ii")) client.close rescue exceptions << $! raise end end end end return server end def test_loop_should_not_abort_when_local_side_of_forward_is_closed session = Net::SSH.start(*ssh_start_params) server_exc = Queue.new server = start_server_sending_lot_of_data(server_exc) remote_port = server.addr[1] local_port = find_free_port session.forward.local(local_port, localhost, remote_port) client_done = Queue.new Thread.start do begin client = TCPSocket.new(localhost, local_port) client.recv(1024) client.close sleep(0.2) ensure client_done << true end end session.loop(0.1) { client_done.empty? } assert_equal "Broken pipe", "#{server_exc.pop}" unless server_exc.empty? end def test_loop_should_not_abort_when_local_side_of_forward_is_reset session = Net::SSH.start(*ssh_start_params) server_exc = Queue.new server = start_server_sending_lot_of_data(server_exc) remote_port = server.addr[1] local_port = find_free_port session.forward.local(local_port, localhost, remote_port) client_done = Queue.new Thread.start do begin client = TCPSocket.new(localhost, local_port) client.recv(1024) client.setsockopt(Socket::SOL_SOCKET, Socket::SO_LINGER, [1, 0].pack("ii")) client.close sleep(0.1) ensure client_done << true end end session.loop(0.1) { client_done.empty? } assert_equal "Broken pipe", "#{server_exc.pop}" unless server_exc.empty? end def create_local_socket(&blk) tempfile = Tempfile.new("net_ssh_forward_test") path = tempfile.path tempfile.delete yield UNIXServer.open(path) File.delete(path) end def test_forward_local_unix_socket_to_remote_port session = Net::SSH.start(*ssh_start_params) server_exc = Queue.new server = start_server_sending_lot_of_data(server_exc) remote_port = server.addr[1] client_data = nil create_local_socket do |local_socket| session.forward.local(local_socket, localhost, remote_port) client_done = Queue.new Thread.start do begin client = UNIXSocket.new(local_socket.path) client_data = client.recv(1024) client.close sleep(0.2) ensure client_done << true end end session.loop(0.1) { client_done.empty? } end assert_not_nil(client_data, "client should have received data") assert(client_data.match(/item\d/), 'client should have received the string item') end def test_loop_should_not_abort_when_server_side_of_forward_is_closed session = Net::SSH.start(*ssh_start_params) server = start_server_closing_soon remote_port = server.addr[1] local_port = find_free_port session.forward.local(local_port, localhost, remote_port) client_done = Queue.new Thread.start do begin client = TCPSocket.new(localhost, local_port) 1.times do |i| client.puts "item#{i}" end client.close sleep(0.1) ensure client_done << true end end session.loop(0.1) { client_done.empty? } end def start_server server = TCPServer.open(0) Thread.start do loop do Thread.start(server.accept) do |client| yield(client) end end end return server end def test_server_eof_should_be_handled session = Net::SSH.start(*ssh_start_params) server = start_server do |client| client.write "This is a small message!" client.close end client_done = Queue.new client_exception = Queue.new client_data = Queue.new remote_port = server.addr[1] local_port = find_free_port session.forward.local(local_port, localhost, remote_port) Thread.start do begin client = TCPSocket.new(localhost, local_port) data = client.read(4096) client.close client_done << data rescue client_done << $! end end timeout(5) do session.loop(0.1) { client_done.empty? } assert_equal "This is a small message!", client_done.pop end end end