From cbe2861162e4bd5f8658a008566d98b3f01c6cd9 Mon Sep 17 00:00:00 2001 From: Aurelien Derouineau Date: Tue, 9 Jul 2013 20:31:34 +0200 Subject: Adding support for ephemeral ports --- THANKS.txt | 1 + lib/net/ssh/service/forward.rb | 25 +++++++++++++++++++------ test/manual/test_forward.rb | 41 +++++++++++++++++++++++++++-------------- 3 files changed, 47 insertions(+), 20 deletions(-) diff --git a/THANKS.txt b/THANKS.txt index dc5b0ad..dbe8a7f 100644 --- a/THANKS.txt +++ b/THANKS.txt @@ -83,3 +83,4 @@ watsonian Grant Hutchins Michael Schubert mtrudel +Aurélien Derouineau diff --git a/lib/net/ssh/service/forward.rb b/lib/net/ssh/service/forward.rb index 5e23e36..267ec6d 100644 --- a/lib/net/ssh/service/forward.rb +++ b/lib/net/ssh/service/forward.rb @@ -47,8 +47,12 @@ module Net; module SSH; module Service # If three arguments are given, it is as if the local bind address is # "127.0.0.1", and the rest are applied as above. # + # To request an ephemeral port on the remote server, provide 0 (zero) for + # the port number. In all cases, this method will return the port that + # has been assigned. + # # ssh.forward.local(1234, "www.capify.org", 80) - # ssh.forward.local("0.0.0.0", 1234, "www.capify.org", 80) + # assigned_port = ssh.forward.local("0.0.0.0", 0, "www.capify.org", 80) def local(*args) if args.length < 3 || args.length > 4 raise ArgumentError, "expected 3 or 4 parameters, got #{args.length}" @@ -69,6 +73,7 @@ module Net; module SSH; module Service end end + local_port = socket.addr[1] if local_port == 0 # ephemeral port was requested remote_host = args.shift remote_port = args.shift.to_i @@ -89,6 +94,8 @@ module Net; module SSH; module Service channel[:socket].close end end + + local_port end # Terminates an active local forwarded port. If no such forwarded port @@ -120,15 +127,21 @@ module Net; module SSH; module Service # forwarded immediately. If the remote server is not able to begin the # listener for this request, an exception will be raised asynchronously. # - # If you want to know when the connection is active, it will show up in the - # #active_remotes list. If you want to block until the port is active, you - # could do something like this: + # To request an ephemeral port on the remote server, provide 0 (zero) for + # the port number. The assigned port will show up in the # #active_remotes + # list. + # + # If you want to block until the port is active, you could do something + # like this: # - # ssh.forward.remote(80, "www.google.com", 1234, "0.0.0.0") - # ssh.loop { !ssh.forward.active_remotes.include?([1234, "0.0.0.0"]) } + # old_active_remotes = ssh.forward.active_remotes + # ssh.forward.remote(80, "www.google.com", 0, "0.0.0.0") + # ssh.loop { !(ssh.forward.active_remotes.length > old_active_remotes.length) } + # assigned_port = (ssh.forward.active_remotes - old_active_remotes).first[0] def remote(port, host, remote_port, remote_host="127.0.0.1") session.send_global_request("tcpip-forward", :string, remote_host, :long, remote_port) do |success, response| if success + remote_port = response.read_long if remote_port == 0 debug { "remote forward from remote #{remote_host}:#{remote_port} to #{host}:#{port} established" } @remote_forwarded_ports[[remote_port, remote_host]] = Remote.new(host, port) else diff --git a/test/manual/test_forward.rb b/test/manual/test_forward.rb index 8e27818..5794077 100644 --- a/test/manual/test_forward.rb +++ b/test/manual/test_forward.rb @@ -1,4 +1,4 @@ -# $ ruby -Ilib -Itest -rrubygems test/test_forward.rb +# $ ruby -Ilib -Itest -rrubygems test/manual/test_forward.rb # Tests for the following patch: # @@ -30,14 +30,6 @@ class TestForward < Test::Unit::TestCase [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 @@ -77,12 +69,34 @@ class TestForward < Test::Unit::TestCase return server end + def test_local_ephemeral_port_should_work_correctly + session = Net::SSH.start(*ssh_start_params) + + assert_nothing_raised do + assigned_port = session.forward.local(0, localhost, 22) + assert_not_nil assigned_port + assert_operator assigned_port, :>, 0 + end + end + + def test_remote_ephemeral_port_should_work_correctly + session = Net::SSH.start(*ssh_start_params) + + assert_nothing_raised do + session.forward.remote(22, localhost, 0, localhost) + session.loop { !(session.forward.active_remotes.length > 0) } + assigned_port = session.forward.active_remotes.first[0] + assert_not_nil assigned_port + assert_operator assigned_port, :>, 0 + end + 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 + local_port = 0 # request ephemeral port session.forward.local(local_port, localhost, remote_port) client_done = Queue.new Thread.start do @@ -104,7 +118,7 @@ class TestForward < Test::Unit::TestCase server_exc = Queue.new server = start_server_sending_lot_of_data(server_exc) remote_port = server.addr[1] - local_port = find_free_port + local_port = 0 # request ephemeral port session.forward.local(local_port, localhost, remote_port) client_done = Queue.new Thread.start do @@ -163,7 +177,7 @@ class TestForward < Test::Unit::TestCase session = Net::SSH.start(*ssh_start_params) server = start_server_closing_soon remote_port = server.addr[1] - local_port = find_free_port + local_port = 0 # request ephemeral port session.forward.local(local_port, localhost, remote_port) client_done = Queue.new Thread.start do @@ -203,8 +217,7 @@ class TestForward < Test::Unit::TestCase 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) + local_port = session.forward.local(0, localhost, remote_port) Thread.start do begin client = TCPSocket.new(localhost, local_port) -- cgit v1.2.1