Net::SSH expect like interface

M

Manish Sapariya

Hi All,
I have been looking for expect like interface for Net::SSH lib,
but apparently there is none.

Net::SSH is working fine for me except one place I need to check
the scp command prompt and send the password. (Invoking scp on
the machine to which I have ssh'ed).

Can somebody on list, provide some guidelines as to how can this
be achieved.

- I tried sending the password blindly to the popen session once
i get on_success or on_stderr callback. However this seems to be
not working. The log says..

--------snippet of the log......
[DEBUG] Thu Apr 26 14:05:48 +0530 2007 -- connection.driver:
CHANNEL_SUCCESS recieved (1)
[DEBUG] Thu Apr 26 14:05:48 +0530 2007 -- transport.session: sending
message >>"^\000\000\000\000\000\000\000\tpassword\n"<<
[DEBUG] Thu Apr 26 14:05:48 +0530 2007 -- transport.session: waiting for
packet from server...
[DEBUG] Thu Apr 26 14:05:48 +0530 2007 --
transport.incoming_packet_stream: reading 8 bytes from socket...
[DEBUG] Thu Apr 26 14:05:48 +0530 2007 --
transport.incoming_packet_stream: packet length(60) remaining(56)
[DEBUG] Thu Apr 26 14:05:48 +0530 2007 --
transport.incoming_packet_stream: received:
"_\000\000\000\001\000\000\000\001\000\000\000&Permission denied, please
try again.\r\n"
[DEBUG] Thu Apr 26 14:05:48 +0530 2007 -- transport.session: got packet
of type 95
[DEBUG] Thu Apr 26 14:05:48 +0530 2007 -- connection.driver:
CHANNEL_EXTENDED_DATA recieved (1:1:"Permission denied, please try
again.\r\n")
[DEBUGrequesting result of password
--> Permission denied, please try again.

requesting result of password
--> Permission denied, please try again.

requesting result of password
--> Permission denied (publickey,gssapi-with-mic,password).

requesting result of password
process finished with exit status: 1
] Thu Apr 26 14:05:48 +0530 2007 -- transport.session: sending message[DEBUG] Thu Apr 26 14:05:48 +0530 2007 -- transport.session: waiting for
packet from server...
--------

What I think is that there are these extra keysequence that are being
passed to the sever.
Do I need to emulate any kind of terminal for this to work.

What will be the best approach to implement expect like interface to
Net::SSH if at all
I can't get this working with existing functinoality.

Any pointer will be greatly appreciated.
Thanks and Regards,
Manish
 
B

Brian Candler

--G4iJoqBmSsgzjUCe
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline

I have been looking for expect like interface for Net::SSH lib,
but apparently there is none.

This is something I've been wanting for a long time too, ideally to use ssh
as a drop-in replacement for Net::Telnet.

I've made a first step by monkey-patching Net::SSH so that the
process.popen3 interface is also available for shell sessions, just by
passing in nil as the command name (see attached). Once you've done this,
you can drive Net::SSH like this:

------------------------------------------------------------------------
require 'rubygems'
require 'net/ssh'

class ShellSession
def initialize(*args)
@session = Net::SSH.start(*args)
@inp, @out, @err = @session.process.popen3 # nil means shell
end

def cmd(command, prompt = /[>\#\$?] ?\z/)
puts "(#{command})"
@inp.puts command
res = ""

while true
@out.channel.connection.process # block for incoming data
res << @out.read if @out.data_available?
res << @err.read if @err.data_available?
break if res =~ prompt
end

res
end
end

s = ShellSession.new("x.x.x.x", "cisco", "cisco")
puts s.cmd("term len 0")
puts s.cmd("show ver")
puts s.cmd("show run")
puts s.cmd("enable", /assword:/)
puts s.cmd("cisco")
puts s.cmd("show run")
------------------------------------------------------------------------

However this is only part of the story. Both IO#expect and Net::Telnet
expect a real IO object that you can use as an argument to Kernel#select.

One solution may be to make a Socketpair:

s1, s2 = Socket.pair(Socket::AF_LOCAL, Socket::SOCK_STREAM, 0)

However, then the code which copies the other end of the pair to/from the
ssh session needs to run as a thread. It's probably also not portable to
Windows.

Alternatively, a modified version of Net::Telnet can be made. And in that
case, it could use the ssh channel interface directly and so not rely on a
patched Net::SSH.

Regards,

Brian.

--G4iJoqBmSsgzjUCe
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename="ssh-process-shell.diff"

--- lib/net/ssh/service/process/open.rb.orig 2007-09-24 11:14:25.000000000 +0100
+++ lib/net/ssh/service/process/open.rb 2007-09-24 11:16:12.000000000 +0100
@@ -38,7 +38,7 @@
# attempt to execute the given command. If a block is given, the
# manager will be yielded to the block, and the constructor will not
# return until all channels are closed.
- def initialize( connection, log, command )
+ def initialize( connection, log, command=nil )
@log = log
@command = command
@channel = connection.open_channel(
@@ -129,7 +129,11 @@
def do_confirm( channel )
channel.on_success(&method:)do_exec_success))
channel.on_failure(&method:)do_exec_failure))
- channel.exec @command, true
+ if @command
+ channel.exec @command, true
+ else
+ channel.send_request "shell", nil, true
+ end
end

# Invoked when the invocation of the command has been successful.
@@ -151,7 +155,7 @@
@on_failure.call( self, nil )
else
raise Net::SSH::Exception,
- "could not execute process (#{@command})"
+ @command ? "could not execute process (#{@command})" : "could not start shell"
end
end

--- lib/net/ssh/service/process/popen3.rb.orig 2007-09-24 11:14:29.000000000 +0100
+++ lib/net/ssh/service/process/popen3.rb 2007-09-24 11:15:02.000000000 +0100
@@ -40,7 +40,7 @@
# is not given, the input, output, and error channels are returned
# and the process *might* not terminate until the session itself
# terminates.
- def popen3( command )
+ def popen3( command=nil )
@connection.open_channel( "session" ) do |chan|

chan.on_success do |ch|
@@ -60,7 +60,11 @@
chan.close
end

- chan.exec command, true
+ if command
+ chan.exec command, true
+ else
+ chan.send_request "shell", nil, true
+ end
end

@connection.loop
--- lib/net/ssh/service/process/driver.rb.orig 2007-09-24 11:21:35.000000000 +0100
+++ lib/net/ssh/service/process/driver.rb 2007-09-24 11:22:04.000000000 +0100
@@ -126,7 +126,7 @@
@handlers = handlers
end

- def open( command )
+ def open( command=nil )
@log.debug "opening '#{command}'" if @log.debug?
process = @handlers[ :eek:pen ].call( command )

@@ -139,7 +139,7 @@
process
end

- def popen3( command, &block )
+ def popen3( command=nil, &block )
@log.debug "popen3 '#{command}'" if @log.debug?
mgr = @handlers[ :popen3 ]
mgr.popen3( command, &block )

--G4iJoqBmSsgzjUCe--
 
B

Brian Candler

--jI8keyz6grp/JLjh
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline

Alternatively, a modified version of Net::Telnet can be made. And in that
case, it could use the ssh channel interface directly and so not rely on a
patched Net::SSH.

Here's my attempt at this. It provides Net::SSH::Telnet which has an almost
identical API to Net::Telnet, since it uses mostly the same code.

Anyone want to give this a try and see if it works for them?

Regards,

Brian.

P.S. Unlike Net::Telnet, it doesn't delegate. I'm not sure if it would be
useful to delegate to the underlying socket, the ssh session, or the shell
channel.

--jI8keyz6grp/JLjh
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename="sshtelnet.rb"

# Based on code in net/telnet.rb by Wakou Aoyama <[email protected]>
# Modified to work with Net::SSH by Brian Candler <[email protected]>

require "net/ssh"

module Net
module SSH

# == Net::SSH::Telnet
#
# Provides a simple send/expect interface with an API almost
# identical to Net::Telnet. Please see Net::Telnet for main documentation.
# Only the differences are documented here.

class Telnet

CR = "\015"
LF = "\012"
EOL = CR + LF
REVISION = '$Id$'

# Wrapper to emulate the behaviour of Net::Telnet "Proxy" option, where
# the user passes in an already-open socket

class TinyFactory
def initialize(sock)
@sock = sock
end
def open(host, port)
s = @sock
@sock = nil
s
end
end

# Creates a new Net::SSH::Telnet object.
#
# The API is similar to Net::Telnet, although you will need to pass in
# either an existing Net::SSH::Session object or a Username and Password,
# as shown below.
#
# Note that "Binmode" only affects translation of CRLF->LF on incoming
# data. Outbound lines are always sent with LF only. This is because SSH
# servers seem to treat CRLF as two line-ends.
#
# A new option is added to correct a misfeature of Net::Telnet. If you
# pass "FailEOF" => true, then if the remote end disconnects while you
# are still waiting for your match pattern then an EOFError is raised.
# Otherwise, it reverts to the same behaviour as Net::Telnet, which is
# just to return whatever data was sent so far, or nil if no data was
# returned so far. (This is a poor design because you can't tell whether
# the expected pattern was successfully matched or the remote end
# disconnected unexpectedly, unless you perform a second match on the
# return string). See
# http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/11373
# http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/11380
#
# Example 1 - pass existing Net::SSH::Session object
#
# ssh = Net::SSH.start("127.0.0.1",
# :username=>"test123",
# :password=>"pass456"
# )
# s = Net::SSH::Telnet.new(
# "Dump_log" => "/dev/stdout",
# "Session" => ssh
# )
# puts "Logged in"
# p s.cmd("echo hello")
#
# This is the most flexible way as it allows you to set up the SSH
# session using whatever authentication system you like. When done this
# way, calling Net::SSH::Telnet.new multiple times will create
# multiple channels, and #close will only close one channel.
#
# In all later examples, calling #close will close the entire
# Net::SSH::Session object (and therefore drop the TCP connection)
#
# Example 2 - pass host, username and password
#
# s = Net::SSH::Telnet.new(
# "Dump_log" => "/dev/stdout",
# "Host" => "127.0.0.1",
# "Username" => "test123",
# "Password" => "pass456"
# )
# puts "Logged in"
# puts s.cmd("echo hello")
#
# Example 3 - pass open IO object, username and password (this is really
# just for compatibility with Net::Telnet Proxy feature)
#
# require 'socket'
# sock = TCPSocket.open("127.0.0.1",22)
# s = Net::SSH::Telnet.new(
# "Dump_log" => "/dev/stdout",
# "Proxy" => sock,
# "Username" => "test123",
# "Password" => "pass456"
# )
# puts "Logged in"
# puts s.cmd("echo hello")
#
# Example 4 - pass a connection factory, host, username and password;
# Net::SSH will call #open(host,port) on this object. Included just
# because it was easy :)
#
# require 'socket'
# s = Net::SSH::Telnet.new(
# "Dump_log" => "/dev/stdout",
# "Factory" => TCPSocket,
# "Host" => "127.0.0.1",
# "Username" => "test123",
# "Password" => "pass456"
# )
# puts "Logged in"
# puts s.cmd("echo hello")

def initialize(options, &blk) # :yield: mesg
@options = options
@options["Host"] = "localhost" unless @options.has_key?("Host")
@options["Port"] = 22 unless @options.has_key?("Port")
@options["Prompt"] = /[$%#>] \z/n unless @options.has_key?("Prompt")
@options["Timeout"] = 10 unless @options.has_key?("Timeout")
@options["Waittime"] = 0 unless @options.has_key?("Waittime")

unless @options.has_key?("Binmode")
@options["Binmode"] = false
else
unless (true == @options["Binmode"] or false == @options["Binmode"])
raise ArgumentError, "Binmode option must be true or false"
end
end

if @options.has_key?("Output_log")
@log = File.open(@options["Output_log"], 'a+')
@log.sync = true
@log.binmode
end

if @options.has_key?("Dump_log")
@dumplog = File.open(@options["Dump_log"], 'a+')
@dumplog.sync = true
@dumplog.binmode
def @dumplog.log_dump(dir, x) # :nodoc:
len = x.length
addr = 0
offset = 0
while 0 < len
if len < 16
line = x[offset, len]
else
line = x[offset, 16]
end
hexvals = line.unpack('H*')[0]
hexvals += ' ' * (32 - hexvals.length)
hexvals = format("%s %s %s %s " * 4, *hexvals.unpack('a2' * 16))
line = line.gsub(/[\000-\037\177-\377]/n, '.')
printf "%s 0x%5.5x: %s%s\n", dir, addr, hexvals, line
addr += 16
offset += 16
len -= 16
end
print "\n"
end
end

if @options.has_key?("Session")
@ssh = @options["Session"]
@close_all = false
elsif @options.has_key?("Proxy")
@ssh = Net::SSH.start(@options["Host"], # ignored
:port => @options["Port"], # ignored
:username => @options["Username"],
:password => @options["Password"],
:timeout => @options["Timeout"],
:proxy => TinyFactory.new(@options["Proxy"]),
:log => @log
)
@close_all = true
else
message = "Trying " + @options["Host"] + "...\n"
yield(message) if block_given?
@log.write(message) if @options.has_key?("Output_log")
@dumplog.log_dump('#', message) if @options.has_key?("Dump_log")

begin
@ssh = Net::SSH.start(@options["Host"],
:port => @options["Port"],
:username => @options["Username"],
:password => @options["Password"],
:timeout => @options["Timeout"],
:proxy => @options["Factory"],
:log => @log
)
@close_all = true
rescue TimeoutError
raise TimeoutError, "timed out while opening a connection to the host"
rescue
@log.write($ERROR_INFO.to_s + "\n") if @options.has_key?("Output_log")
@dumplog.log_dump('#', $ERROR_INFO.to_s + "\n") if @options.has_key?("Dump_log")
raise
end

message = "Connected to " + @options["Host"] + ".\n"
yield(message) if block_given?
@log.write(message) if @options.has_key?("Output_log")
@dumplog.log_dump('#', message) if @options.has_key?("Dump_log")
end

@buf = ""
@eof = false
@channel = nil
state = nil
@ssh.open_channel do |channel|
channel.on_success { |ch|
case state
when :pty
state = :shell
channel.send_request "shell", nil, true
when :shell
state = :prompt
@channel = ch
waitfor(@options['Prompt'], &blk)
return
end
}
channel.on_failure { |ch|
ch.close
raise "Failed to open #{state}"
}
channel.on_data { |ch,data| @buf << data }
channel.on_extended_data { |ch,type,data| @buf << data if type == 1 }
channel.on_close { @eof = true }
state = :pty
channel.request_pty:)want_reply => true)
end
@ssh.loop
end # initialize

# Close the ssh channel, and also the entire ssh session if we
# opened it.

def close
@channel.close if @channel
@channel = nil
@ssh.close if @close_all and @ssh
end

# The ssh session and channel we are using.
attr_reader :ssh, :channel

# Turn newline conversion on (+mode+ == false) or off (+mode+ == true),
# or return the current value (+mode+ is not specified).
def binmode(mode = nil)
case mode
when nil
@options["Binmode"]
when true, false
@options["Binmode"] = mode
else
raise ArgumentError, "argument must be true or false"
end
end

# Turn newline conversion on (false) or off (true).
def binmode=(mode)
if (true == mode or false == mode)
@options["Binmode"] = mode
else
raise ArgumentError, "argument must be true or false"
end
end

# Read data from the host until a certain sequence is matched.

def waitfor(options) # :yield: recvdata
time_out = @options["Timeout"]
waittime = @options["Waittime"]
fail_eof = @options["FailEOF"]

if options.kind_of?(Hash)
prompt = if options.has_key?("Match")
options["Match"]
elsif options.has_key?("Prompt")
options["Prompt"]
elsif options.has_key?("String")
Regexp.new( Regexp.quote(options["String"]) )
end
time_out = options["Timeout"] if options.has_key?("Timeout")
waittime = options["Waittime"] if options.has_key?("Waittime")
fail_eof = options["FailEOF"] if options.has_key?("FailEOF")
else
prompt = options
end

if time_out == false
time_out = nil
end

line = ''
buf = ''
rest = ''
# We want to use #read_ready?(maxwait) but it's not available
sock = @ssh.connection.instance_variable_get:)@session).instance_variable_get:)@socket)

until prompt === line and ((@eof and @buf == "") or not IO::select([sock], nil, nil, waittime))
while @buf == "" and !@eof
unless IO::select([sock], nil, nil, time_out)
raise TimeoutError, "timed out while waiting for more data"
end
# Note: this could hang if partial message received. Should we
# wrap with timeout { ... } instead?
@channel.connection.process
end
if @buf != ""
c = @buf; @buf = ""
@dumplog.log_dump('<', c) if @options.has_key?("Dump_log")
buf = c
buf.gsub!(/#{EOL}/no, "\n") unless @options["Binmode"]
rest = ''
@log.print(buf) if @options.has_key?("Output_log")
line += buf
yield buf if block_given?
elsif @eof # End of file reached
break if prompt === line
raise EOFError, "EOF while waiting for more data\n#{prompt}\n#{line[-100,100] || line}" if fail_eof
if line == ''
line = nil
yield nil if block_given?
end
break
end
end
line
end

# Write +string+ to the host.
#
# Does not perform any conversions on +string+. Will log +string+ to the
# dumplog, if the Dump_log option is set.
def write(string)
@dumplog.log_dump('>', string) if @options.has_key?("Dump_log")
@channel.send_data string
@channel.connection.process true
end

# Sends a string to the host.
#
# This does _not_ automatically append a newline to the string.
def print(string)
self.write(string)
end

# Sends a string to the host.
#
# Same as #print(), but appends a newline to the string.
def puts(string)
self.print(string + "\n")
end

# Send a command to the host.
#
# More exactly, sends a string to the host, and reads in all received
# data until is sees the prompt or other matched sequence.
#
# The command or other string will have the newline sequence appended
# to it.

def cmd(options) # :yield: recvdata
match = @options["Prompt"]
time_out = @options["Timeout"]

if options.kind_of?(Hash)
string = options["String"]
match = options["Match"] if options.has_key?("Match")
time_out = options["Timeout"] if options.has_key?("Timeout")
else
string = options
end

self.puts(string)
if block_given?
waitfor({"Prompt" => match, "Timeout" => time_out}){|c| yield c }
else
waitfor({"Prompt" => match, "Timeout" => time_out})
end
end

end # class Telnet
end # module SSH
end # module Net

--jI8keyz6grp/JLjh--
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,579
Members
45,053
Latest member
BrodieSola

Latest Threads

Top