lib/net/telnet.rb


DEFINITIONS

This source file includes following functions.


   1  =begin
   2  
   3  == NAME
   4  
   5  net/telnet.rb - simple telnet client library
   6  
   7  Wakou Aoyama <wakou@fsinet.or.jp>
   8  
   9  
  10  === MAKE NEW TELNET OBJECT
  11  
  12    host = Net::Telnet::new({
  13             "Binmode"    => false,        # default: false
  14             "Host"       => "localhost",  # default: "localhost"
  15             "Output_log" => "output_log", # default: nil (no output)
  16             "Dump_log"   => "dump_log",   # default: nil (no output)
  17             "Port"       => 23,           # default: 23
  18             "Prompt"     => /[$%#>] \z/n, # default: /[$%#>] \z/n
  19             "Telnetmode" => true,         # default: true
  20             "Timeout"    => 10,           # default: 10
  21               # if ignore timeout then set "Timeout" to false.
  22             "Waittime"   => 0,            # default: 0
  23             "Proxy"      => proxy         # default: nil
  24                             # proxy is Net::Telnet or IO object
  25           })
  26  
  27  Telnet object has socket class methods.
  28  
  29  if set "Telnetmode" option to false. not telnet command interpretation.
  30  "Waittime" is time to confirm "Prompt". There is a possibility that
  31  the same character as "Prompt" is included in the data, and, when
  32  the network or the host is very heavy, the value is enlarged.
  33  
  34  
  35  === STATUS OUTPUT
  36  
  37    host = Net::Telnet::new({"Host" => "localhost"}){|c| print c }
  38  
  39  connection status output.
  40  
  41  example:
  42  
  43    Trying localhost...
  44    Connected to localhost.
  45  
  46  
  47  === WAIT FOR MATCH
  48  
  49    line = host.waitfor(/match/)
  50    line = host.waitfor({"Match"   => /match/,
  51                         "String"  => "string",
  52                         "Timeout" => secs})
  53                           # if ignore timeout then set "Timeout" to false.
  54  
  55  if set "String" option, then Match == Regexp.new(quote("string"))
  56  
  57  
  58  ==== REALTIME OUTPUT
  59  
  60    host.waitfor(/match/){|c| print c }
  61    host.waitfor({"Match"   => /match/,
  62                  "String"  => "string",
  63                  "Timeout" => secs}){|c| print c}
  64  
  65  of cource, set sync=true or flush is necessary.
  66  
  67  
  68  === SEND STRING AND WAIT PROMPT
  69  
  70    line = host.cmd("string")
  71    line = host.cmd({"String" => "string",
  72                     "Match" => /[$%#>] \z/n,
  73                     "Timeout" => 10})
  74  
  75  
  76  ==== REALTIME OUTPUT
  77  
  78    host.cmd("string"){|c| print c }
  79    host.cmd({"String" => "string",
  80              "Match" => /[$%#>] \z/n,
  81              "Timeout" => 10}){|c| print c }
  82  
  83  of cource, set sync=true or flush is necessary.
  84  
  85  
  86  === SEND STRING
  87  
  88    host.print("string")
  89    host.puts("string")
  90  
  91  Telnet#puts() adds "\n" to the last of "string".
  92  
  93  WARNING: Telnet#print() NOT adds "\n" to the last of "string", in the future.
  94  
  95  If "Telnetmode" option is true, then escape IAC code ("\xFF"). If
  96  "Binmode" option is false, then convert "\n" to EOL(end of line) code.
  97  
  98  If "WILL SGA" and "DO BIN", then EOL is CR. If "WILL SGA", then EOL is
  99  CR + NULL. If the other cases, EOL is CR + LF.
 100  
 101  
 102  === TOGGLE TELNET COMMAND INTERPRETATION
 103  
 104    host.telnetmode          # return the current status (true or false)
 105    host.telnetmode = true   # do telnet command interpretation (default)
 106    host.telnetmode = false  # don't telnet command interpretation
 107  
 108  
 109  === TOGGLE NEWLINE TRANSLATION
 110  
 111    host.binmode          # return the current status (true or false)
 112    host.binmode = true   # no translate newline
 113    host.binmode = false  # translate newline (default)
 114  
 115  
 116  === LOGIN
 117  
 118    host.login("username", "password")
 119    host.login({"Name" => "username",
 120                "Password" => "password"})
 121  
 122  if no password prompt:
 123  
 124    host.login("username")
 125    host.login({"Name" => "username"})
 126  
 127  
 128  ==== REALTIME OUTPUT
 129  
 130    host.login("username", "password"){|c| print c }
 131    host.login({"Name" => "username",
 132                "Password" => "password"}){|c| print c }
 133  
 134  of cource, set sync=true or flush is necessary.
 135  
 136  
 137  
 138  == EXAMPLE
 139  
 140  === LOGIN AND SEND COMMAND
 141  
 142    localhost = Net::Telnet::new({"Host" => "localhost",
 143                                  "Timeout" => 10,
 144                                  "Prompt" => /[$%#>] \z/n})
 145    localhost.login("username", "password"){|c| print c }
 146    localhost.cmd("command"){|c| print c }
 147    localhost.close
 148  
 149  
 150  === CHECKS A POP SERVER TO SEE IF YOU HAVE MAIL
 151  
 152    pop = Net::Telnet::new({"Host" => "your_destination_host_here",
 153                            "Port" => 110,
 154                            "Telnetmode" => false,
 155                            "Prompt" => /^\+OK/n})
 156    pop.cmd("user " + "your_username_here"){|c| print c}
 157    pop.cmd("pass " + "your_password_here"){|c| print c}
 158    pop.cmd("list"){|c| print c}
 159  
 160  
 161  =end
 162  
 163  
 164  require "socket"
 165  require "delegate"
 166  require "timeout"
 167  require "English"
 168  
 169  module Net
 170    class Telnet < SimpleDelegator
 171  
 172      IAC   = 255.chr # "\377" # "\xff" # interpret as command:
 173      DONT  = 254.chr # "\376" # "\xfe" # you are not to use option
 174      DO    = 253.chr # "\375" # "\xfd" # please, you use option
 175      WONT  = 252.chr # "\374" # "\xfc" # I won't use option
 176      WILL  = 251.chr # "\373" # "\xfb" # I will use option
 177      SB    = 250.chr # "\372" # "\xfa" # interpret as subnegotiation
 178      GA    = 249.chr # "\371" # "\xf9" # you may reverse the line
 179      EL    = 248.chr # "\370" # "\xf8" # erase the current line
 180      EC    = 247.chr # "\367" # "\xf7" # erase the current character
 181      AYT   = 246.chr # "\366" # "\xf6" # are you there
 182      AO    = 245.chr # "\365" # "\xf5" # abort output--but let prog finish
 183      IP    = 244.chr # "\364" # "\xf4" # interrupt process--permanently
 184      BREAK = 243.chr # "\363" # "\xf3" # break
 185      DM    = 242.chr # "\362" # "\xf2" # data mark--for connect. cleaning
 186      NOP   = 241.chr # "\361" # "\xf1" # nop
 187      SE    = 240.chr # "\360" # "\xf0" # end sub negotiation
 188      EOR   = 239.chr # "\357" # "\xef" # end of record (transparent mode)
 189      ABORT = 238.chr # "\356" # "\xee" # Abort process
 190      SUSP  = 237.chr # "\355" # "\xed" # Suspend process
 191      EOF   = 236.chr # "\354" # "\xec" # End of file
 192      SYNCH = 242.chr # "\362" # "\xf2" # for telfunc calls
 193  
 194      OPT_BINARY         =   0.chr # "\000" # "\x00" # Binary Transmission
 195      OPT_ECHO           =   1.chr # "\001" # "\x01" # Echo
 196      OPT_RCP            =   2.chr # "\002" # "\x02" # Reconnection
 197      OPT_SGA            =   3.chr # "\003" # "\x03" # Suppress Go Ahead
 198      OPT_NAMS           =   4.chr # "\004" # "\x04" # Approx Message Size Negotiation
 199      OPT_STATUS         =   5.chr # "\005" # "\x05" # Status
 200      OPT_TM             =   6.chr # "\006" # "\x06" # Timing Mark
 201      OPT_RCTE           =   7.chr # "\a"   # "\x07" # Remote Controlled Trans and Echo
 202      OPT_NAOL           =   8.chr # "\010" # "\x08" # Output Line Width
 203      OPT_NAOP           =   9.chr # "\t"   # "\x09" # Output Page Size
 204      OPT_NAOCRD         =  10.chr # "\n"   # "\x0a" # Output Carriage-Return Disposition
 205      OPT_NAOHTS         =  11.chr # "\v"   # "\x0b" # Output Horizontal Tab Stops
 206      OPT_NAOHTD         =  12.chr # "\f"   # "\x0c" # Output Horizontal Tab Disposition
 207      OPT_NAOFFD         =  13.chr # "\r"   # "\x0d" # Output Formfeed Disposition
 208      OPT_NAOVTS         =  14.chr # "\016" # "\x0e" # Output Vertical Tabstops
 209      OPT_NAOVTD         =  15.chr # "\017" # "\x0f" # Output Vertical Tab Disposition
 210      OPT_NAOLFD         =  16.chr # "\020" # "\x10" # Output Linefeed Disposition
 211      OPT_XASCII         =  17.chr # "\021" # "\x11" # Extended ASCII
 212      OPT_LOGOUT         =  18.chr # "\022" # "\x12" # Logout
 213      OPT_BM             =  19.chr # "\023" # "\x13" # Byte Macro
 214      OPT_DET            =  20.chr # "\024" # "\x14" # Data Entry Terminal
 215      OPT_SUPDUP         =  21.chr # "\025" # "\x15" # SUPDUP
 216      OPT_SUPDUPOUTPUT   =  22.chr # "\026" # "\x16" # SUPDUP Output
 217      OPT_SNDLOC         =  23.chr # "\027" # "\x17" # Send Location
 218      OPT_TTYPE          =  24.chr # "\030" # "\x18" # Terminal Type
 219      OPT_EOR            =  25.chr # "\031" # "\x19" # End of Record
 220      OPT_TUID           =  26.chr # "\032" # "\x1a" # TACACS User Identification
 221      OPT_OUTMRK         =  27.chr # "\e"   # "\x1b" # Output Marking
 222      OPT_TTYLOC         =  28.chr # "\034" # "\x1c" # Terminal Location Number
 223      OPT_3270REGIME     =  29.chr # "\035" # "\x1d" # Telnet 3270 Regime
 224      OPT_X3PAD          =  30.chr # "\036" # "\x1e" # X.3 PAD
 225      OPT_NAWS           =  31.chr # "\037" # "\x1f" # Negotiate About Window Size
 226      OPT_TSPEED         =  32.chr # " "    # "\x20" # Terminal Speed
 227      OPT_LFLOW          =  33.chr # "!"    # "\x21" # Remote Flow Control
 228      OPT_LINEMODE       =  34.chr # "\""   # "\x22" # Linemode
 229      OPT_XDISPLOC       =  35.chr # "#"    # "\x23" # X Display Location
 230      OPT_OLD_ENVIRON    =  36.chr # "$"    # "\x24" # Environment Option
 231      OPT_AUTHENTICATION =  37.chr # "%"    # "\x25" # Authentication Option
 232      OPT_ENCRYPT        =  38.chr # "&"    # "\x26" # Encryption Option
 233      OPT_NEW_ENVIRON    =  39.chr # "'"    # "\x27" # New Environment Option
 234      OPT_EXOPL          = 255.chr # "\377" # "\xff" # Extended-Options-List
 235  
 236      NULL = "\000"
 237      CR   = "\015"
 238      LF   = "\012"
 239      EOL  = CR + LF
 240      REVISION = '$Id: telnet.rb,v 1.18 2002/07/11 08:22:16 matz Exp $'
 241  
 242      def initialize(options)
 243        @options = options
 244        @options["Host"]       = "localhost"   unless @options.has_key?("Host")
 245        @options["Port"]       = 23            unless @options.has_key?("Port")
 246        @options["Prompt"]     = /[$%#>] \z/n  unless @options.has_key?("Prompt")
 247        @options["Timeout"]    = 10            unless @options.has_key?("Timeout")
 248        @options["Waittime"]   = 0             unless @options.has_key?("Waittime")
 249        unless @options.has_key?("Binmode")
 250          @options["Binmode"]    = false         
 251        else
 252          unless (true == @options["Binmode"] or false == @options["Binmode"])
 253            raise ArgumentError, "Binmode option required true or false"
 254          end
 255        end
 256  
 257        unless @options.has_key?("Telnetmode")
 258          @options["Telnetmode"] = true          
 259        else
 260          unless (true == @options["Telnetmode"] or false == @options["Telnetmode"])
 261            raise ArgumentError, "Telnetmode option required true or false"
 262          end
 263        end
 264  
 265        @telnet_option = { "SGA" => false, "BINARY" => false }
 266  
 267        if @options.has_key?("Output_log")
 268          @log = File.open(@options["Output_log"], 'a+')
 269          @log.sync = true
 270          @log.binmode
 271        end
 272  
 273        if @options.has_key?("Dump_log")
 274          @dumplog = File.open(@options["Dump_log"], 'a+')
 275          @dumplog.sync = true
 276          @dumplog.binmode
 277          def @dumplog.log_dump(dir, x)
 278            len = x.length
 279            addr = 0
 280            offset = 0
 281            while 0 < len
 282              if len < 16
 283                line = x[offset, len]
 284              else
 285                line = x[offset, 16]
 286              end
 287              hexvals = line.unpack('H*')[0]
 288              hexvals += ' ' * (32 - hexvals.length)
 289              hexvals = format "%s %s %s %s  " * 4, *hexvals.unpack('a2' * 16)
 290              line = line.gsub(/[\000-\037\177-\377]/n, '.')
 291              printf "%s 0x%5.5x: %s%s\n", dir, addr, hexvals, line
 292              addr += 16
 293              offset += 16
 294              len -= 16
 295            end
 296            print "\n"
 297          end
 298        end
 299  
 300        if @options.has_key?("Proxy")
 301          if @options["Proxy"].kind_of?(Net::Telnet)
 302            @sock = @options["Proxy"].sock
 303          elsif @options["Proxy"].kind_of?(IO)
 304            @sock = @options["Proxy"]
 305          else
 306            raise "Error; Proxy is Net::Telnet or IO object."
 307          end
 308        else
 309          message = "Trying " + @options["Host"] + "...\n"
 310          yield(message) if block_given?
 311          @log.write(message) if @options.has_key?("Output_log")
 312          @dumplog.log_dump('#', message) if @options.has_key?("Dump_log")
 313  
 314          begin
 315            if @options["Timeout"] == false
 316              @sock = TCPSocket.open(@options["Host"], @options["Port"])
 317            else
 318              timeout(@options["Timeout"]) do
 319                @sock = TCPSocket.open(@options["Host"], @options["Port"])
 320              end
 321            end
 322          rescue TimeoutError
 323            raise TimeoutError, "timed-out; opening of the host"
 324          rescue
 325            @log.write($ERROR_INFO.to_s + "\n") if @options.has_key?("Output_log")
 326            @dumplog.log_dump('#', $ERROR_INFO.to_s + "\n") if @options.has_key?("Dump_log")
 327            raise
 328          end
 329          @sock.sync = true
 330          @sock.binmode
 331  
 332          message = "Connected to " + @options["Host"] + ".\n"
 333          yield(message) if block_given?
 334          @log.write(message) if @options.has_key?("Output_log")
 335          @dumplog.log_dump('#', message) if @options.has_key?("Dump_log")
 336        end
 337  
 338        super(@sock)
 339      end # initialize
 340  
 341      attr :sock
 342  
 343      def telnetmode(mode = nil)
 344        case mode
 345        when nil
 346          @options["Telnetmode"]
 347        when true, false
 348          @options["Telnetmode"] = mode
 349        else
 350          raise ArgumentError, "required true or false"
 351        end
 352      end
 353  
 354      def telnetmode=(mode)
 355        if (true == mode or false == mode)
 356          @options["Telnetmode"] = mode
 357        else
 358          raise ArgumentError, "required true or false"
 359        end
 360      end
 361  
 362      def binmode(mode = nil)
 363        case mode
 364        when nil
 365          @options["Binmode"] 
 366        when true, false
 367          @options["Binmode"] = mode
 368        else
 369          raise ArgumentError, "required true or false"
 370        end
 371      end
 372  
 373      def binmode=(mode)
 374        if (true == mode or false == mode)
 375          @options["Binmode"] = mode
 376        else
 377          raise ArgumentError, "required true or false"
 378        end
 379      end
 380  
 381      def preprocess(string)
 382        # combine CR+NULL into CR
 383        string = string.gsub(/#{CR}#{NULL}/no, CR) if @options["Telnetmode"]
 384  
 385        # combine EOL into "\n"
 386        string = string.gsub(/#{EOL}/no, "\n") unless @options["Binmode"]
 387  
 388        string.gsub(/#{IAC}(
 389                     [#{IAC}#{AO}#{AYT}#{DM}#{IP}#{NOP}]|
 390                     [#{DO}#{DONT}#{WILL}#{WONT}]
 391                       [#{OPT_BINARY}-#{OPT_NEW_ENVIRON}#{OPT_EXOPL}]|
 392                     #{SB}[^#{IAC}]*#{IAC}#{SE}
 393                   )/xno) do
 394          if    IAC == $1  # handle escaped IAC characters
 395            IAC
 396          elsif AYT == $1  # respond to "IAC AYT" (are you there)
 397            self.write("nobody here but us pigeons" + EOL)
 398            ''
 399          elsif DO[0] == $1[0]  # respond to "IAC DO x"
 400            if OPT_BINARY[0] == $1[1]
 401              @telnet_option["BINARY"] = true
 402              self.write(IAC + WILL + OPT_BINARY)
 403            else
 404              self.write(IAC + WONT + $1[1..1])
 405            end
 406            ''
 407          elsif DONT[0] == $1[0]  # respond to "IAC DON'T x" with "IAC WON'T x"
 408            self.write(IAC + WONT + $1[1..1])
 409            ''
 410          elsif WILL[0] == $1[0]  # respond to "IAC WILL x"
 411            if    OPT_BINARY[0] == $1[1]
 412              self.write(IAC + DO + OPT_BINARY)
 413            elsif OPT_ECHO[0] == $1[1]
 414              self.write(IAC + DO + OPT_ECHO)
 415            elsif OPT_SGA[0]  == $1[1]
 416              @telnet_option["SGA"] = true
 417              self.write(IAC + DO + OPT_SGA)
 418            else
 419              self.write(IAC + DONT + $1[1..1])
 420            end
 421            ''
 422          elsif WONT[0] == $1[0]  # respond to "IAC WON'T x"
 423            if    OPT_ECHO[0] == $1[1]
 424              self.write(IAC + DONT + OPT_ECHO)
 425            elsif OPT_SGA[0]  == $1[1]
 426              @telnet_option["SGA"] = false
 427              self.write(IAC + DONT + OPT_SGA)
 428            else
 429              self.write(IAC + DONT + $1[1..1])
 430            end
 431            ''
 432          else
 433            ''
 434          end
 435        end
 436      end # preprocess
 437  
 438      def waitfor(options)
 439        time_out = @options["Timeout"]
 440        waittime = @options["Waittime"]
 441  
 442        if options.kind_of?(Hash)
 443          prompt   = if options.has_key?("Match")
 444                       options["Match"]
 445                     elsif options.has_key?("Prompt")
 446                       options["Prompt"]
 447                     elsif options.has_key?("String")
 448                       Regexp.new( Regexp.quote(options["String"]) )
 449                     end
 450          time_out = options["Timeout"]  if options.has_key?("Timeout")
 451          waittime = options["Waittime"] if options.has_key?("Waittime")
 452        else
 453          prompt = options
 454        end
 455  
 456        if time_out == false
 457          time_out = nil
 458        end
 459  
 460        line = ''
 461        buf = ''
 462        rest = ''
 463        until(prompt === line and not IO::select([@sock], nil, nil, waittime))
 464          unless IO::select([@sock], nil, nil, time_out)
 465            raise TimeoutError, "timed-out; wait for the next data"
 466          end
 467          begin
 468            c = @sock.sysread(1024 * 1024)
 469            @dumplog.log_dump('<', c) if @options.has_key?("Dump_log")
 470            if @options["Telnetmode"]
 471              c = rest + c
 472              if Integer(c.rindex(/#{IAC}#{SE}/no)) <
 473                 Integer(c.rindex(/#{IAC}#{SB}/no))
 474                buf = preprocess(c[0 ... c.rindex(/#{IAC}#{SB}/no)])
 475                rest = c[c.rindex(/#{IAC}#{SB}/no) .. -1]
 476              elsif pt = c.rindex(/#{IAC}[^#{IAC}#{AO}#{AYT}#{DM}#{IP}#{NOP}]?\z/no)
 477                buf = preprocess(c[0 ... pt])
 478                rest = c[pt .. -1]
 479              else
 480                buf = preprocess(c)
 481                rest = ''
 482              end
 483            end
 484            @log.print(buf) if @options.has_key?("Output_log")
 485            line += buf
 486            yield buf if block_given?
 487          rescue EOFError # End of file reached
 488            if line == ''
 489              line = nil
 490              yield nil if block_given?
 491            end
 492            break
 493          end
 494        end
 495        line
 496      end
 497  
 498      def write(string)
 499        length = string.length
 500        while 0 < length
 501          IO::select(nil, [@sock])
 502          @dumplog.log_dump('>', string[-length..-1]) if @options.has_key?("Dump_log")
 503          length -= @sock.syswrite(string[-length..-1])
 504        end
 505      end
 506  
 507      def _print(string)
 508        string = string.gsub(/#{IAC}/no, IAC + IAC) if @options["Telnetmode"]
 509  
 510        if @options["Binmode"]
 511          self.write(string)
 512        else
 513          if @telnet_option["BINARY"] and @telnet_option["SGA"]
 514            # IAC WILL SGA IAC DO BIN send EOL --> CR
 515            self.write(string.gsub(/\n/n, CR))
 516          elsif @telnet_option["SGA"]
 517            # IAC WILL SGA send EOL --> CR+NULL
 518            self.write(string.gsub(/\n/n, CR + NULL))
 519          else
 520            # NONE send EOL --> CR+LF
 521            self.write(string.gsub(/\n/n, EOL))
 522          end
 523        end
 524      end
 525  
 526      def puts(string)
 527        self._print(string + "\n")
 528      end
 529  
 530      def print(string)
 531        if $VERBOSE
 532          $stderr.puts 'WARNING: Telnet#print("string") NOT adds "\n" to the last of "string", in the future.'
 533          $stderr.puts '         cf. Telnet#puts().'
 534        end
 535        self.puts(string)
 536      end
 537  
 538      def cmd(options)
 539        match    = @options["Prompt"]
 540        time_out = @options["Timeout"]
 541  
 542        if options.kind_of?(Hash)
 543          string   = options["String"]
 544          match    = options["Match"]   if options.has_key?("Match")
 545          time_out = options["Timeout"] if options.has_key?("Timeout")
 546        else
 547          string = options
 548        end
 549  
 550        self.puts(string)
 551        if block_given?
 552          waitfor({"Prompt" => match, "Timeout" => time_out}){|c| yield c }
 553        else
 554          waitfor({"Prompt" => match, "Timeout" => time_out})
 555        end
 556      end
 557  
 558      def login(options, password = nil)
 559        if options.kind_of?(Hash)
 560          username = options["Name"]
 561          password = options["Password"]
 562        else
 563          username = options
 564        end
 565  
 566        if block_given?
 567          line = waitfor(/login[: ]*\z/n){|c| yield c }
 568          if password
 569            line += cmd({"String" => username,
 570                         "Match" => /Password[: ]*\z/n}){|c| yield c }
 571            line += cmd(password){|c| yield c }
 572          else
 573            line += cmd(username){|c| yield c }
 574          end
 575        else
 576          line = waitfor(/login[: ]*\z/n)
 577          if password
 578            line += cmd({"String" => username,
 579                         "Match" => /Password[: ]*\z/n})
 580            line += cmd(password)
 581          else
 582            line += cmd(username)
 583          end
 584        end
 585        line
 586      end
 587  
 588    end
 589  end
 590  
 591  
 592  =begin
 593  
 594  == HISTORY
 595  
 596  delete. see cvs log.
 597  
 598  
 599  =end