Interactive SSH session using Net::SSH or creating a STDIN Socket - ruby
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
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
Related
Make Net:SSH update returned data packets/chunks in exec block more often
I have a ruby script on a remote server that I'm running via Net:SSH on my local pc. The remote script takes a few minutes to run and outputs it's progress to stdout. The problem I have is the block in my exec command only gets called when the packet/chunk is full. So I get the progress all in one hit about each minute. Here is some cut down examples that illustrate my problem: Server Script: (0.999).each do |i| puts i sleep 1 end puts 1000 Local Script: Net::SSH.start('ip.v.4.addr', 'user', :keys => ['my_key']) do |ssh| ssh.exec("ruby count_to_1000.rb") do |ch, stream, data| puts data if stream == :stdout end ssh.loop(1) end Is there any way from the remote script to force the sending of the packet/chunk? Or is there a way to set a limit of say a second (or n bits) before it's flushed? (within Net:SSH) Thanks for all your help!
Try flush: http://www.ruby-doc.org/core-2.1.5/IO.html#method-i-flush (0.999).each do |i| puts i STDOUT.flush sleep 1 end Or sync: http://www.ruby-doc.org/core-2.1.5/IO.html#method-i-sync STDOUT.sync = true (0.999).each do |i| puts i sleep 1 end (Untested, btw. Maybe they need to be used on the client-side instead, or on some other IO stream. But those are the two methods that immediately come to mind.)
In my test setup this works as expected (tested with localhost). However, there might be some issues with the STDOUT flush. You can try to to write to STDOUT in stead of using puts (I have heard that there is some difference that I don't really understand). Thus, you can on your server use: (0.999).each do |i| STDOUT.puts i sleep 1 end STDOUT.puts 1000 #You could possibly also use "STDOUT.write 1000", but it will not append a newline, like puts does. If that does not work, then you can try to force-flush the STDOUT by using STDOUT.flush(). I believe the same can be achieved by writing an empty string to STDOUT, but I am not 1000% sure. It might also happen that the exec command actually waits for the entire process to terminate for some reason(I was not able to figure out from the docs). In which case, you won't be able to achieve what you want. Then you can consider setting up websockets, use DRB, or some other means to pass the data.
Ruby, run linux commands one by one, by SSH and LOG everything
I want to write code in Ruby witch net::ssh that run commands one by one on remote linux machine and log everything (called command, stdout and stderr on linux machine). So I write function: def rs(ssh,cmds) cmds.each do |cmd| log.debug "[SSH>] #{cmd}" ssh.exec!(cmd) do |ch, stream, data| log.debug "[SSH:#{stream}>] #{data}" end end end For example if I want to create on remote linux new folders and file: "./verylongdirname/anotherlongdirname/a.txt", and list files in that direcotry, and find firefox there (which is stupid a little :P) so i call above procedure like that: Net::SSH.start(host, user, :password => pass) do |ssh| cmds=["mkdir verylongdirname", \ #1 "cd verylongdirname; mkdir anotherlongdirname, \ #2 "cd verylongdirname/anotherlongdirname; touch a.txt", \ #3 "cd verylongdirname/anotherlongdirname; ls -la", \ #4 "cd verylongdirname/anotherlongdirname; find ./ firefox" #5 that command send error to stderr. ] rs(ssh,cmds) # HERE we call our function ssh.loop end After run code above i will have full LOG witch informations about executions commands in line #1,#2,#3,#4,#5. The problem is that state on linux, between execude commands from cmds array, is not saved (so I must repeat "cd" statement before run proper command). And I'm not satisfy with that. My purpose is to have cmds tables like that: cmds=["mkdir verylongdirname", \ #1 "cd verylongdirname", \ "mkdir anotherlongdirname", \ #2 "cd anotherlongdirname", \ "touch a.txt", \ #3 "ls -la", \ #4 "find ./ firefox"] #5 As you see, te state between run each command is save on the linux machine (and we don't need repeat apropriate "cd" statement before run proper command). How to change "rs(ssh,cmds)" procedure to do it and LOG EVERYTHING (comand,stdout,stdin) like before?
Perhaps try it with an ssh channel instead to open a remote shell. That should preserve state between your commands as the connection will be kept open: http://net-ssh.github.com/ssh/v1/chapter-5.html Here's also an article of doing something similar with a little bit different approach: http://drnicwilliams.com/2006/09/22/remote-shell-with-ruby/ Edit 1: Ok. I see what you are saying. SyncShell was removed from Net::SSH 2.0. However I found this, which looks like it does pretty much what SyncShell did: http://net-ssh-telnet.rubyforge.org/ Example: s = Net::SSH.start(host, user) t = Net::SSH::Telnet.new("Session" => s, "Prompt" => %r{^myprompt :}) puts t.cmd("cd /tmp") puts t.cmd("ls") # <- Lists contents of /tmp I.e. Net::SSH::Telnet is synchronous, and preserves state, because it runs in a pty with your remote shell environment. Remember to set the correct prompt detection, otherwise Net::SSH::Telnet will appear to hang once you call it (it's trying to find the prompt).
You can use pipe instead: require "open3" SERVER = "..." BASH_PATH = "/bin/bash" BASH_REMOTE = lambda do |command| Open3.popen3("ssh #{SERVER} #{BASH_PATH}") do |stdin, stdout, stderr| stdin.puts command stdin.close_write puts "STDOUT:", stdout.read puts "STDERR:", stderr.read end end BASH_REMOTE["ls /"] BASH_REMOTE["ls /no_such_file"]
Ok, finally with the help of #Casper i get the procedure (maby someone use it): # Remote command execution # t=net::ssh:telnet, c="command_string" def cmd(t,c) first=true d='' # We send command via SSH and read output piece by piece (in 'cm' variable) t.cmd(c) do |cm| # below we cleaning up output piece (becouse it have strange chars) d << cm.gsub(/\e\].*?\a/,"").gsub(/\e\[.*?m/,"").gsub(/\r/,"") # when we read entire line(composed of many pieces) we write it to log if d =~ /(^.*?)\n(.*)$/m if first ; # instead of the first line (which has repeated commands) we log commands 'c' #log.info "[SSH]>"+c; first=false else #log.info "[SSH] "+$1; end d=$2 end end # We print lines that were at the end (in last piece) d.each_line do |l| #log.info "[SSH] "+l.chomp end end And we call it in code: #!/usr/bin/env ruby require 'rubygems' require 'net/ssh' require 'net/ssh/telnet' require 'log4r' ... ... ... Net::SSH.start(host, user, :password => pass) do |ssh| t = Net::SSH::Telnet.new("Session" => ssh) cmd(t,"cd /") cmd(t,"ls -la") cmd(t,"find ./ firefox") end Thanks, bye.
Here's wrapper around Net/ssh here's article http://ruby-lang.info/blog/virtual-file-system-b3g source https://github.com/alexeypetrushin/vfs to log all commands just overwrite the Box.bash method and add logging there
Reading / Writing from a Unix Socket in Ruby
I'm trying to connect, read and write from a UNIX socket in Ruby. It is a stats socket used by haproxy. My code is the following: require 'socket' socket = UNIXSocket.new("/tmp/haproxy.stats.socket") # First attempt: works socket.puts("show stat") while(line = socket.gets) do puts line end # Second attemp: fails socket.puts("show stat") while(line = socket.gets) do puts line end It succeeds the first time, but on the second attempt fails. I'm not sure why. # pxname,svname,qcur,qmax,scur,smax,slim,stot,bin,bout,dreq,dresp,ereq,econ,eresp,wretr,wredis,status,weight,act,bck,chkfail,chkdown,lastchg,downtime,qlimit,pid,iid,sid,throttle,lbtot,tracked,type,rate,rate_lim,rate_max,check_status,check_code,check_duration,hrsp_1xx,hrsp_2xx,hrsp_3xx,hrsp_4xx,hrsp_5xx,hrsp_other,hanafail,req_rate,req_rate_max,req_tot,cli_abrt,srv_abrt, stats,FRONTEND,,,0,0,2000,0,0,0,0,0,0,,,,,OPEN,,,,,,,,,1,1,0,,,,0,0,0,0,,,,0,0,0,0,0,0,,0,0,0,,, stats,BACKEND,0,0,0,0,2000,0,0,0,0,0,,0,0,0,0,UP,0,0,0,,0,22,0,,1,1,0,,0,,1,0,,0,,,,0,0,0,0,0,0,,,,,0,0, legacy_socket,FRONTEND,,,0,0,1000,0,0,0,0,0,0,,,,,OPEN,,,,,,,,,1,2,0,,,,0,0,0,0,,,,0,0,0,0,0,0,,0,0,0,,, all,FRONTEND,,,0,0,10000,0,0,0,0,0,0,,,,,OPEN,,,,,,,,,1,3,0,,,,0,0,0,0,,,,0,0,0,0,0,0,,0,0,0,,, socket_backend,socket,0,0,0,0,200,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,0,1,22,22,,1,4,1,,0,,2,0,,0,L4CON,,0,0,0,0,0,0,0,0,,,,0,0, socket_backend,socket,0,0,0,0,200,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,0,1,22,22,,1,4,2,,0,,2,0,,0,L4CON,,0,0,0,0,0,0,0,0,,,,0,0, socket_backend,socket,0,0,0,0,200,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,0,1,22,22,,1,4,3,,0,,2,0,,0,L4CON,,0,0,0,0,0,0,0,0,,,,0,0, socket_backend,socket,0,0,0,0,200,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,0,1,22,22,,1,4,4,,0,,2,0,,0,L4CON,,0,0,0,0,0,0,0,0,,,,0,0, socket_backend,socket,0,0,0,0,200,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,0,1,22,22,,1,4,5,,0,,2,0,,0,L4CON,,0,0,0,0,0,0,0,0,,,,0,0, socket_backend,socket,0,0,0,0,200,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,0,1,22,22,,1,4,6,,0,,2,0,,0,L4CON,,0,0,0,0,0,0,0,0,,,,0,0, socket_backend,socket,0,0,0,0,200,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,0,1,22,22,,1,4,7,,0,,2,0,,0,L4CON,,0,0,0,0,0,0,0,0,,,,0,0, socket_backend,socket,0,0,0,0,200,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,0,1,21,21,,1,4,8,,0,,2,0,,0,L4CON,,0,0,0,0,0,0,0,0,,,,0,0, socket_backend,socket,0,0,0,0,200,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,0,1,21,21,,1,4,9,,0,,2,0,,0,L4CON,,0,0,0,0,0,0,0,0,,,,0,0, socket_backend,socket,0,0,0,0,200,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,0,1,21,21,,1,4,10,,0,,2,0,,0,L4CON,,0,0,0,0,0,0,0,0,,,,0,0, socket_backend,BACKEND,0,0,0,0,0,0,0,0,0,0,,0,0,0,0,DOWN,0,0,0,,1,21,21,,1,4,0,,0,,1,0,,0,,,,0,0,0,0,0,0,,,,,0,0, api_backend,api,0,0,0,0,200,0,0,0,,0,,0,0,0,0,UP,1,1,0,0,0,22,0,,1,5,1,,0,,2,0,,0,L4OK,,0,0,0,0,0,0,0,0,,,,0,0, api_backend,api,0,0,0,0,1,0,0,0,,0,,0,0,0,0,UP,1,1,0,0,0,22,0,,1,5,2,,0,,2,0,,0,L4OK,,0,0,0,0,0,0,0,0,,,,0,0, api_backend,api,0,0,0,0,1,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,0,1,21,21,,1,5,3,,0,,2,0,,0,L4CON,,0,0,0,0,0,0,0,0,,,,0,0, api_backend,BACKEND,0,0,0,0,0,0,0,0,0,0,,0,0,0,0,UP,2,2,0,,0,22,0,,1,5,0,,0,,1,0,,0,,,,0,0,0,0,0,0,,,,,0,0, www_backend,ruby-www,0,0,0,0,10000,0,0,0,,0,,0,0,0,0,UP,1,1,0,0,0,22,0,,1,6,1,,0,,2,0,,0,L4OK,,0,0,0,0,0,0,0,0,,,,0,0, www_backend,BACKEND,0,0,0,0,0,0,0,0,0,0,,0,0,0,0,UP,1,1,0,,0,22,0,,1,6,0,,0,,1,0,,0,,,,0,0,0,0,0,0,,,,,0,0, /Users/Olly/Desktop/haproxy_stats.rb:14:in `write': Broken pipe (Errno::EPIPE) from /Users/Olly/Desktop/haproxy_stats.rb:14:in `puts' from /Users/Olly/Desktop/haproxy_stats.rb:14 What is the problem? Is there a good reference to using UNIX sockets and Ruby?
Olly, HAproxy closes connection after the first request unless you use the "prompt" command (see http://haproxy.1wt.eu/download/1.4/doc/configuration.txt section 9.2) : #!/usr/bin/env ruby require 'socket' socket = UNIXSocket.new("/tmp/haproxy.stats.socket") # Goes interactive mode socket.puts("prompt") # Ask statistics every second while true socket.puts("show stat") socket.each_char do |c| # We had the prompt, break out break if c == '>' print c end sleep 1 end
Looks like the connection has been closed after the first request. I don't think you are doing anything wrong. The HAProxy stats socket is probably designed so that it responds to a single command and then closes the connection. I think you need to reconnect for each request. If you look at this blog post which is about using HAProxy stats socket with socat then this makes sense because you pipe the show stat command into socat and socat reads from the socket until it closes.
I also encountered the same problem when use socket.puts, you can use socket.write instead of socket.puts to fix it. #!/usr/bin/evn ruby # -*- coding: UTF-8 -*- require 'rubygems' require 'uri' require 'socket' require 'yaml' SOCKET = URI.parse("/var/run/haproxy/haproxy.sock") def get_info UNIXSocket.open(SOCKET.path) do |socket| socket.write("show info;") info = YAML::load socket #info.each {|key, value| puts "#{key} ➤ #{value}"} end end puts get_info["Uptime_sec"] Look this gem for more information, source code is here.
You can use man socket. You can use the socket-class just like you would if it was a C-function. I found man-pages to be very useful.
Exposing console apps to the web with Ruby
I'm looking to expose an interactive command line program via JSON or another RPC style service using Ruby. I've found a couple tricks to do this, but im missing something when redirecting the output and input. One method at least on linux is to redirect the stdin and stdout to a file then read and write to that file asynchronously with file reads and writes. Another method ive been trying after googling around was to use open4. Here is the code I wrote so far, but its getting stuck after reading a few lines from standard output. require "open4" include Open4 status = popen4("./srcds_run -console -game tf +map ctf_2fort -maxplayers 6") do |pid, stdin, stdout, stderr| puts "PID #{pid}" lines="" while (line=stdout.gets) lines+=line puts line end while (line=stderr.gets) lines+=line puts line end end Any help on this or some insight would be appreciated!
What I would recommend is using Xinetd (or similar) to run the command on some socket and then using the ruby network code. One of the problems you've already run into in your code here is that your two while loops are sequential, which can cause problems.
Another trick you might try is to re-direct stderr to stdout in your command, so that your program only has to read the stdout. Something like this: popen4("./srcds_run -console -game tf +map ctf_2fort -maxplayers 6 2>&1") The other benefit of this is that you get all the messages/errors in the order they happen during the program run.
EDIT Your should consider integrating with AnyTerm. You can then either expose AnyTerm directly e.g. via Apache mod_proxy, or have your Rails controller act as the reverse proxy (handling authentication/session validation, then playing back controller.request minus any cookies to localhost:<AnyTerm-daemon-port>, and sending back as a response whatever AnyTerm replies with.) class ConsoleController < ApplicationController # AnyTerm speaks via HTTP POST only def update # validate session ... # forward request to AnyTerm response = Net::HTTP.post_form(URI.parse('http://localhost:#{AnyTermPort}/', request.params)) headers['Content-Type'] = response['Content-Type'] render_text response.body, response.status end Otherwise, you'd need to use IO::Select or IO::read_noblock to know when data is available to be read (from either network or subprocess) so you don't deadlock. See this too. Also check that either your Rails is used in a multi-threaded environment or that your Ruby version is not affected by this IO::Select bug. You can start with something along these lines: status = POpen4::popen4("ping localhost") do |stdout, stderr, stdin, pid| puts "PID #{pid}" # our buffers stdout_lines="" stderr_lines="" begin loop do # check whether stdout, stderr or both are # ready to be read from without blocking IO.select([stdout,stderr]).flatten.compact.each { |io| # stdout, if ready, goes to stdout_lines stdout_lines += io.readpartial(1024) if io.fileno == stdout.fileno # stderr, if ready, goes to stdout_lines stderr_lines += io.readpartial(1024) if io.fileno == stderr.fileno } break if stdout.closed? && stderr.closed? # if we acumulated any complete lines (\n-terminated) # in either stdout/err_lines, output them now stdout_lines.sub!(/.*\n/m) { puts $& ; '' } stderr_lines.sub!(/.*\n/m) { puts $& ; '' } end rescue EOFError puts "Done" end end To also handle stdin, change to: IO.select([stdout,stderr],[stdin]).flatten.compact.each { |io| # program ready to get stdin? do we have anything for it? if io.fileno == stdin.fileno && <got data from client?> <write a small chunk from client to stdin> end # stdout, if ready, goes to stdout_lines
Respond to a SSH prompt before first command with ruby and Net::SSH
I'm trying to connect, using Net::SSH, to a server that immediately after login executes a script that requires input from user. The user has to enter "1" or "2" and will receive some data via in the terminal afterwards. My problem is that, although I am able to connect, I can not figure out a way to send "1\n" to the server and to receive the output. The following code stops at "INFO -- net.ssh.connection.session[80906b74]: channel_open_confirmation: 0 0 0 32768". Using channel.exec( "1\n" ) instead of channel.send_data unsurprisingly does not work either. Net::SSH.start('host', 'user', :password => "pass", :auth_methods => ["password"], :verbose => :debug) do |session| session.open_channel do |channel| channel.on_data do |ch, data| STDOUT.print data end channel.send_data( "1\n") end session.loop end Any ideas, anyone? Thanks in advance
Can you verify that your send_data call is happening after you get the prompt from the remote server? Try constructing a channel.on_data block around your send_data call so that you can verify that you get the expected prompt from the server before you send a response. You might not want to be using exec here. From the docs for Net::SSH::Connection::Channel: Sends a channel request asking that the given command be invoked. You are wanting to send a text string to reply to a prompt, not invoke a command. The docs show exec being used to send full CLI commands like "ls -l /home". Instead, send_data is probably what you want. The docs show it used to send arbitrary text such as channel.send_data("the password\n"). Note, however, this sentence in the docs: Note that it does not immediately send the data across the channel, but instead merely appends the given data to the channel‘s output buffer, preparatory to being packaged up and sent out the next time the connection is accepting data. You might want to take a look at channel.request_pty. It appears to be designed for interaction with a console-based application. If you are trying to (in essence) script an SSH session that you would normally do manually, you may find it easier to use an expect-like interface (for example, a gem like sshExpect might be worth a try).
Thank you all for the pointers. I have been able to put my finger on the problem – besides using channel.request_pty it was also necessary to request a shell. The following finally works as expected: Net::SSH.start('host', 'user', :password => "pass", :auth_methods => ["password"]) do |session| session.open_channel do |channel| channel.request_pty do |ch, success| raise "Error requesting pty" unless success ch.send_channel_request("shell") do |ch, success| raise "Error opening shell" unless success end end channel.on_data do |ch, data| STDOUT.print data end channel.on_extended_data do |ch, type, data| STDOUT.print "Error: #{data}\n" end channel.send_data( "1\n" ) session.loop end end
I'm not terribly familiar with the Net::SSH libs so I can't help with that per-se but it sounds like you could achieve what you want using Capistrano. For example I have a capistrano task which connects to a remote server, runs a command which expects input and then continues. Capistrano takes care of the remote i/o. Maybe that could be a solution for you? Hope it helps!
If I execute "1\n" in a shell the reply I get is: bash: 1: command not found If I execute echo "1" I get: 1 Are you sure you want to try to execute the text you are sending? Maybe you were looking for something like: output = "" Net::SSH.start('host', 'user', :password => "pass") do |ssh| output = ssh.exec! "echo 1" end puts output
I'm not proficient with that lib, but on SSH you can open multiple channels. Maybe the server only responds to the first default channel and if you open another one you get a fresh shell.