--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",
#
assword=>"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
ort => @options["Port"], # ignored
:username => @options["Username"],
assword => @options["Password"],
:timeout => @options["Timeout"],
roxy => 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"],
ort => @options["Port"],
:username => @options["Username"],
assword => @options["Password"],
:timeout => @options["Timeout"],
roxy => @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
ty
state = :shell
channel.send_request "shell", nil, true
when :shell
state =
rompt
@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 =
ty
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--