From: Brian Candler Date: 2007-09-25T18:11:11+09:00 Subject: Re: Net::SSH expect like interface --jI8keyz6grp/JLjh Content-Type: text/plain; charset=us-ascii Content-Disposition: inline On Mon, Sep 24, 2007 at 11:55:41AM +0100, Brian Candler wrote: > 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 # Modified to work with Net::SSH by Brian Candler 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--