rubynet-ssh

Interactive SSH session using Net::SSH or creating a STDIN Socket


This is not a duplicate of How to SSH interactive Session or Net::SSH interactive terminal scripting. How? or Ruby net-ssh interactive response/reply

I'm trying to write an interactive SSH client using Net:SSH, because I'm already using it for run non-interactive commands on the target hosts.

I could just shell out to system "ssh" but it would require converting the connection settings, proxying, etc to ssh params.

The problem is streaming the data from STDIN to the shell channel. The Net::SSH documentation for listen_to shows how to do it when the input is from a socket instead of STDIN. $stdin or IO.console are not sockets and thus not compatible with Net::SSH::BufferedIo.

Is there a way to create a socket from STDIN that can be used for this? Or is there a better way to send everything from the STDIN to the Net::SSH::Channel until the channel closes?

Here's code that works, but is way too slow to be usable:

require 'net/ssh'
require 'io/console'
require 'io/wait'

Net::SSH.start('localhost', 'root') do |session|
  session.open_channel do |channel|
    channel.on_data do |_, data|
      $stdout.write(data)
    end

    channel.on_extended_data do |_, data|
      $stdout.write(data)
    end

    channel.request_pty do
      channel.send_channel_request "shell"
    end

    channel.connection.loop do
      $stdin.raw do |io|
        input = io.readpartial(1024)
        channel.send_data(input) unless input.empty?
      end
      channel.active?
    end
  end.wait
end

Solution

  • Sockets are really nothing more than file descriptors, and since STDIN is a file descriptor too, it doesn't hurt to try.

    What you want however, is to put the TTY into raw mode first to get interactivity.
    This code seems to work fine:

    begin
      system('stty cbreak -echo')
    
      Net::SSH.start(...) do |session|
        session.open_channel do |...|
           ...
           session.listen_to(STDIN) { |stdin|
             input = stdin.readpartial(1024)
             channel.send_data(input) unless input.empty?
           }
         end.wait
       end
    ensure
      system('stty sane')
    end