├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib ├── net-telnet.rb └── net │ ├── telnet.rb │ └── telnet │ └── version.rb ├── net-telnet.gemspec └── spec └── binmode_spec.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 8 | strategy: 9 | matrix: 10 | ruby: [ head, '3.0', '2.7', '2.6' ] 11 | os: [ ubuntu-latest, macos-latest ] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Ruby 16 | uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: ${{ matrix.ruby }} 19 | - name: Install dependencies 20 | run: bundle install 21 | - name: Run spec 22 | run: mspec 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in net-telnet.gemspec 4 | gemspec 5 | 6 | group :development do 7 | gem "bundler" 8 | gem "rake" 9 | gem "mspec" 10 | end 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Ruby is copyrighted free software by Yukihiro Matsumoto . 2 | You can redistribute it and/or modify it under either the terms of the 3 | 2-clause BSDL (see the file BSDL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a) place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b) use the modified software only within your corporation or 18 | organization. 19 | 20 | c) give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d) make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a) distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b) accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c) give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d) make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Net::Telnet 2 | 3 | Provides telnet client functionality. 4 | 5 | This class also has, through delegation, all the methods of a socket object (by default, a **TCPSocket**, but can be set by the **Proxy** option to ```new()```). This provides methods such as ```close()``` to end the session and ```sysread()``` to read data directly from the host, instead of via the ```waitfor()``` mechanism. Note that if you do use ```sysread()``` directly when in telnet mode, you should probably pass the output through ```preprocess()``` to extract telnet command sequences. 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem 'net-telnet' 13 | ``` 14 | 15 | And then execute: 16 | 17 | $ bundle 18 | 19 | Or install it yourself as: 20 | 21 | $ gem install net-telnet 22 | 23 | ## Usage 24 | 25 | ### Log in and send a command, echoing all output to stdout 26 | 27 | ```ruby 28 | localhost = Net::Telnet::new("Host" => "localhost", 29 | "Timeout" => 10, 30 | "Prompt" => /[$%#>] \z/n) 31 | localhost.login("username", "password") { |c| print c } 32 | localhost.cmd("command") { |c| print c } 33 | localhost.close 34 | ``` 35 | 36 | ### Check a POP server to see if you have mail 37 | 38 | ```ruby 39 | pop = Net::Telnet::new("Host" => "your_destination_host_here", 40 | "Port" => 110, 41 | "Telnetmode" => false, 42 | "Prompt" => /^\+OK/n) 43 | pop.cmd("user " + "your_username_here") { |c| print c } 44 | pop.cmd("pass " + "your_password_here") { |c| print c } 45 | pop.cmd("list") { |c| print c } 46 | ``` 47 | 48 | ## Development 49 | 50 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment. 51 | 52 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 53 | 54 | ## Contributing 55 | 56 | 1. Fork it ( https://github.com/ruby/net-telnet/fork ) 57 | 2. Create your feature branch (`git checkout -b my-new-feature`) 58 | 3. Commit your changes (`git commit -am 'Add some feature'`) 59 | 4. Push to the branch (`git push origin my-new-feature`) 60 | 5. Create a new Pull Request 61 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "net/telnet" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /lib/net-telnet.rb: -------------------------------------------------------------------------------- 1 | require 'net/telnet' 2 | -------------------------------------------------------------------------------- /lib/net/telnet.rb: -------------------------------------------------------------------------------- 1 | # = net/telnet.rb - Simple Telnet Client Library 2 | # 3 | # Author:: Wakou Aoyama 4 | # Documentation:: William Webber and Wakou Aoyama 5 | # 6 | # This file holds the class Net::Telnet, which provides client-side 7 | # telnet functionality. 8 | # 9 | # For documentation, see Net::Telnet. 10 | # 11 | 12 | require "net/protocol" 13 | require "English" 14 | 15 | module Net 16 | 17 | # 18 | # == Net::Telnet 19 | # 20 | # Provides telnet client functionality. 21 | # 22 | # This class also has, through delegation, all the methods of a 23 | # socket object (by default, a +TCPSocket+, but can be set by the 24 | # +Proxy+ option to new()). This provides methods such as 25 | # close() to end the session and sysread() to read 26 | # data directly from the host, instead of via the waitfor() 27 | # mechanism. Note that if you do use sysread() directly 28 | # when in telnet mode, you should probably pass the output through 29 | # preprocess() to extract telnet command sequences. 30 | # 31 | # == Overview 32 | # 33 | # The telnet protocol allows a client to login remotely to a user 34 | # account on a server and execute commands via a shell. The equivalent 35 | # is done by creating a Net::Telnet class with the +Host+ option 36 | # set to your host, calling #login() with your user and password, 37 | # issuing one or more #cmd() calls, and then calling #close() 38 | # to end the session. The #waitfor(), #print(), #puts(), and 39 | # #write() methods, which #cmd() is implemented on top of, are 40 | # only needed if you are doing something more complicated. 41 | # 42 | # A Net::Telnet object can also be used to connect to non-telnet 43 | # services, such as SMTP or HTTP. In this case, you normally 44 | # want to provide the +Port+ option to specify the port to 45 | # connect to, and set the +Telnetmode+ option to false to prevent 46 | # the client from attempting to interpret telnet command sequences. 47 | # Generally, #login() will not work with other protocols, and you 48 | # have to handle authentication yourself. 49 | # 50 | # For some protocols, it will be possible to specify the +Prompt+ 51 | # option once when you create the Telnet object and use #cmd() calls; 52 | # for others, you will have to specify the response sequence to 53 | # look for as the Match option to every #cmd() call, or call 54 | # #puts() and #waitfor() directly; for yet others, you will have 55 | # to use #sysread() instead of #waitfor() and parse server 56 | # responses yourself. 57 | # 58 | # It is worth noting that when you create a new Net::Telnet object, 59 | # you can supply a proxy IO channel via the Proxy option. This 60 | # can be used to attach the Telnet object to other Telnet objects, 61 | # to already open sockets, or to any read-write IO object. This 62 | # can be useful, for instance, for setting up a test fixture for 63 | # unit testing. 64 | # 65 | # == Examples 66 | # 67 | # === Log in and send a command, echoing all output to stdout 68 | # 69 | # localhost = Net::Telnet::new("Host" => "localhost", 70 | # "Timeout" => 10, 71 | # "Prompt" => /[$%#>] \z/n) 72 | # localhost.login("username", "password") { |c| print c } 73 | # localhost.cmd("command") { |c| print c } 74 | # localhost.close 75 | # 76 | # 77 | # === Check a POP server to see if you have mail 78 | # 79 | # pop = Net::Telnet::new("Host" => "your_destination_host_here", 80 | # "Port" => 110, 81 | # "Telnetmode" => false, 82 | # "Prompt" => /^\+OK/n) 83 | # pop.cmd("user " + "your_username_here") { |c| print c } 84 | # pop.cmd("pass " + "your_password_here") { |c| print c } 85 | # pop.cmd("list") { |c| print c } 86 | # 87 | # == References 88 | # 89 | # There are a large number of RFCs relevant to the Telnet protocol. 90 | # RFCs 854-861 define the base protocol. For a complete listing 91 | # of relevant RFCs, see 92 | # http://www.omnifarious.org/~hopper/technical/telnet-rfc.html 93 | # 94 | class Telnet 95 | 96 | # :stopdoc: 97 | IAC = 255.chr # "\377" # "\xff" # interpret as command 98 | DONT = 254.chr # "\376" # "\xfe" # you are not to use option 99 | DO = 253.chr # "\375" # "\xfd" # please, you use option 100 | WONT = 252.chr # "\374" # "\xfc" # I won't use option 101 | WILL = 251.chr # "\373" # "\xfb" # I will use option 102 | SB = 250.chr # "\372" # "\xfa" # interpret as subnegotiation 103 | GA = 249.chr # "\371" # "\xf9" # you may reverse the line 104 | EL = 248.chr # "\370" # "\xf8" # erase the current line 105 | EC = 247.chr # "\367" # "\xf7" # erase the current character 106 | AYT = 246.chr # "\366" # "\xf6" # are you there 107 | AO = 245.chr # "\365" # "\xf5" # abort output--but let prog finish 108 | IP = 244.chr # "\364" # "\xf4" # interrupt process--permanently 109 | BREAK = 243.chr # "\363" # "\xf3" # break 110 | DM = 242.chr # "\362" # "\xf2" # data mark--for connect. cleaning 111 | NOP = 241.chr # "\361" # "\xf1" # nop 112 | SE = 240.chr # "\360" # "\xf0" # end sub negotiation 113 | EOR = 239.chr # "\357" # "\xef" # end of record (transparent mode) 114 | ABORT = 238.chr # "\356" # "\xee" # Abort process 115 | SUSP = 237.chr # "\355" # "\xed" # Suspend process 116 | EOF = 236.chr # "\354" # "\xec" # End of file 117 | SYNCH = 242.chr # "\362" # "\xf2" # for telfunc calls 118 | 119 | OPT_BINARY = 0.chr # "\000" # "\x00" # Binary Transmission 120 | OPT_ECHO = 1.chr # "\001" # "\x01" # Echo 121 | OPT_RCP = 2.chr # "\002" # "\x02" # Reconnection 122 | OPT_SGA = 3.chr # "\003" # "\x03" # Suppress Go Ahead 123 | OPT_NAMS = 4.chr # "\004" # "\x04" # Approx Message Size Negotiation 124 | OPT_STATUS = 5.chr # "\005" # "\x05" # Status 125 | OPT_TM = 6.chr # "\006" # "\x06" # Timing Mark 126 | OPT_RCTE = 7.chr # "\a" # "\x07" # Remote Controlled Trans and Echo 127 | OPT_NAOL = 8.chr # "\010" # "\x08" # Output Line Width 128 | OPT_NAOP = 9.chr # "\t" # "\x09" # Output Page Size 129 | OPT_NAOCRD = 10.chr # "\n" # "\x0a" # Output Carriage-Return Disposition 130 | OPT_NAOHTS = 11.chr # "\v" # "\x0b" # Output Horizontal Tab Stops 131 | OPT_NAOHTD = 12.chr # "\f" # "\x0c" # Output Horizontal Tab Disposition 132 | OPT_NAOFFD = 13.chr # "\r" # "\x0d" # Output Formfeed Disposition 133 | OPT_NAOVTS = 14.chr # "\016" # "\x0e" # Output Vertical Tabstops 134 | OPT_NAOVTD = 15.chr # "\017" # "\x0f" # Output Vertical Tab Disposition 135 | OPT_NAOLFD = 16.chr # "\020" # "\x10" # Output Linefeed Disposition 136 | OPT_XASCII = 17.chr # "\021" # "\x11" # Extended ASCII 137 | OPT_LOGOUT = 18.chr # "\022" # "\x12" # Logout 138 | OPT_BM = 19.chr # "\023" # "\x13" # Byte Macro 139 | OPT_DET = 20.chr # "\024" # "\x14" # Data Entry Terminal 140 | OPT_SUPDUP = 21.chr # "\025" # "\x15" # SUPDUP 141 | OPT_SUPDUPOUTPUT = 22.chr # "\026" # "\x16" # SUPDUP Output 142 | OPT_SNDLOC = 23.chr # "\027" # "\x17" # Send Location 143 | OPT_TTYPE = 24.chr # "\030" # "\x18" # Terminal Type 144 | OPT_EOR = 25.chr # "\031" # "\x19" # End of Record 145 | OPT_TUID = 26.chr # "\032" # "\x1a" # TACACS User Identification 146 | OPT_OUTMRK = 27.chr # "\e" # "\x1b" # Output Marking 147 | OPT_TTYLOC = 28.chr # "\034" # "\x1c" # Terminal Location Number 148 | OPT_3270REGIME = 29.chr # "\035" # "\x1d" # Telnet 3270 Regime 149 | OPT_X3PAD = 30.chr # "\036" # "\x1e" # X.3 PAD 150 | OPT_NAWS = 31.chr # "\037" # "\x1f" # Negotiate About Window Size 151 | OPT_TSPEED = 32.chr # " " # "\x20" # Terminal Speed 152 | OPT_LFLOW = 33.chr # "!" # "\x21" # Remote Flow Control 153 | OPT_LINEMODE = 34.chr # "\"" # "\x22" # Linemode 154 | OPT_XDISPLOC = 35.chr # "#" # "\x23" # X Display Location 155 | OPT_OLD_ENVIRON = 36.chr # "$" # "\x24" # Environment Option 156 | OPT_AUTHENTICATION = 37.chr # "%" # "\x25" # Authentication Option 157 | OPT_ENCRYPT = 38.chr # "&" # "\x26" # Encryption Option 158 | OPT_NEW_ENVIRON = 39.chr # "'" # "\x27" # New Environment Option 159 | OPT_EXOPL = 255.chr # "\377" # "\xff" # Extended-Options-List 160 | 161 | NULL = "\000" 162 | CR = "\015" 163 | LF = "\012" 164 | EOL = CR + LF 165 | REVISION = '$Id$' 166 | # :startdoc: 167 | 168 | # 169 | # Creates a new Net::Telnet object. 170 | # 171 | # Attempts to connect to the host (unless the Proxy option is 172 | # provided: see below). If a block is provided, it is yielded 173 | # status messages on the attempt to connect to the server, of 174 | # the form: 175 | # 176 | # Trying localhost... 177 | # Connected to localhost. 178 | # 179 | # +options+ is a hash of options. The following example lists 180 | # all options and their default values. 181 | # 182 | # host = Net::Telnet::new( 183 | # "Host" => "localhost", # default: "localhost" 184 | # "Port" => 23, # default: 23 185 | # "Binmode" => false, # default: false 186 | # "Output_log" => "output_log", # default: nil (no output) 187 | # "Dump_log" => "dump_log", # default: nil (no output) 188 | # "Prompt" => /[$%#>] \z/n, # default: /[$%#>] \z/n 189 | # "Telnetmode" => true, # default: true 190 | # "Timeout" => 10, # default: 10 191 | # # if ignore timeout then set "Timeout" to false. 192 | # "Waittime" => 0, # default: 0 193 | # "Proxy" => proxy # default: nil 194 | # # proxy is Net::Telnet or IO object 195 | # ) 196 | # 197 | # The options have the following meanings: 198 | # 199 | # Host:: the hostname or IP address of the host to connect to, as a String. 200 | # Defaults to "localhost". 201 | # 202 | # Port:: the port to connect to. Defaults to 23. 203 | # 204 | # Binmode:: if false (the default), newline substitution is performed. 205 | # Outgoing LF is 206 | # converted to CRLF, and incoming CRLF is converted to LF. If 207 | # true, this substitution is not performed. This value can 208 | # also be set with the #binmode() method. The 209 | # outgoing conversion only applies to the #puts() and #print() 210 | # methods, not the #write() method. The precise nature of 211 | # the newline conversion is also affected by the telnet options 212 | # SGA and BIN. 213 | # 214 | # Output_log:: the name of the file to write connection status messages 215 | # and all received traffic to. In the case of a proper 216 | # Telnet session, this will include the client input as 217 | # echoed by the host; otherwise, it only includes server 218 | # responses. Output is appended verbatim to this file. 219 | # By default, no output log is kept. 220 | # 221 | # Dump_log:: as for Output_log, except that output is written in hexdump 222 | # format (16 bytes per line as hex pairs, followed by their 223 | # printable equivalent), with connection status messages 224 | # preceded by '#', sent traffic preceded by '>', and 225 | # received traffic preceded by '<'. By default, not dump log 226 | # is kept. 227 | # 228 | # Prompt:: a regular expression matching the host's command-line prompt 229 | # sequence. This is needed by the Telnet class to determine 230 | # when the output from a command has finished and the host is 231 | # ready to receive a new command. By default, this regular 232 | # expression is /[$%#>] \z/n. 233 | # 234 | # Telnetmode:: a boolean value, true by default. In telnet mode, 235 | # traffic received from the host is parsed for special 236 | # command sequences, and these sequences are escaped 237 | # in outgoing traffic sent using #puts() or #print() 238 | # (but not #write()). If you are using the Net::Telnet 239 | # object to connect to a non-telnet service (such as 240 | # SMTP or POP), this should be set to "false" to prevent 241 | # undesired data corruption. This value can also be set 242 | # by the #telnetmode() method. 243 | # 244 | # Timeout:: the number of seconds to wait before timing out both the 245 | # initial attempt to connect to host (in this constructor), 246 | # which raises a Net::OpenTimeout, and all attempts to read data 247 | # from the host, which raises a Net::ReadTimeout (in #waitfor(), 248 | # #cmd(), and #login()). The default value is 10 seconds. 249 | # You can disable the timeout by setting this value to false. 250 | # In this case, the connect attempt will eventually timeout 251 | # on the underlying connect(2) socket call with an 252 | # Errno::ETIMEDOUT error (but generally only after a few 253 | # minutes), but other attempts to read data from the host 254 | # will hang indefinitely if no data is forthcoming. 255 | # 256 | # Waittime:: the amount of time to wait after seeing what looks like a 257 | # prompt (that is, received data that matches the Prompt 258 | # option regular expression) to see if more data arrives. 259 | # If more data does arrive in this time, Net::Telnet assumes 260 | # that what it saw was not really a prompt. This is to try to 261 | # avoid false matches, but it can also lead to missing real 262 | # prompts (if, for instance, a background process writes to 263 | # the terminal soon after the prompt is displayed). By 264 | # default, set to 0, meaning not to wait for more data. 265 | # 266 | # Proxy:: a proxy object to used instead of opening a direct connection 267 | # to the host. Must be either another Net::Telnet object or 268 | # an IO object. If it is another Net::Telnet object, this 269 | # instance will use that one's socket for communication. If an 270 | # IO object, it is used directly for communication. Any other 271 | # kind of object will cause an error to be raised. 272 | # 273 | def initialize(options) # :yield: mesg 274 | @options = options 275 | @options["Host"] = "localhost" unless @options.has_key?("Host") 276 | @options["Port"] = 23 unless @options.has_key?("Port") 277 | @options["Prompt"] = /[$%#>] \z/n unless @options.has_key?("Prompt") 278 | @options["Timeout"] = 10 unless @options.has_key?("Timeout") 279 | @options["Waittime"] = 0 unless @options.has_key?("Waittime") 280 | unless @options.has_key?("Binmode") 281 | @options["Binmode"] = false 282 | else 283 | unless (true == @options["Binmode"] or false == @options["Binmode"]) 284 | raise ArgumentError, "Binmode option must be true or false" 285 | end 286 | end 287 | 288 | unless @options.has_key?("Telnetmode") 289 | @options["Telnetmode"] = true 290 | else 291 | unless (true == @options["Telnetmode"] or false == @options["Telnetmode"]) 292 | raise ArgumentError, "Telnetmode option must be true or false" 293 | end 294 | end 295 | 296 | @telnet_option = { "SGA" => false, "BINARY" => false } 297 | 298 | if @options.has_key?("Output_log") 299 | @log = File.open(@options["Output_log"], 'a+') 300 | @log.sync = true 301 | @log.binmode 302 | end 303 | 304 | if @options.has_key?("Dump_log") 305 | @dumplog = File.open(@options["Dump_log"], 'a+') 306 | @dumplog.sync = true 307 | @dumplog.binmode 308 | def @dumplog.log_dump(dir, x) # :nodoc: 309 | len = x.length 310 | addr = 0 311 | offset = 0 312 | while 0 < len 313 | if len < 16 314 | line = x[offset, len] 315 | else 316 | line = x[offset, 16] 317 | end 318 | hexvals = line.unpack('H*')[0] 319 | hexvals += ' ' * (32 - hexvals.length) 320 | hexvals = format("%s %s %s %s " * 4, *hexvals.unpack('a2' * 16)) 321 | line = line.gsub(/[\000-\037\177-\377]/n, '.') 322 | printf "%s 0x%5.5x: %s%s\n", dir, addr, hexvals, line 323 | addr += 16 324 | offset += 16 325 | len -= 16 326 | end 327 | print "\n" 328 | end 329 | end 330 | 331 | if @options.has_key?("Proxy") 332 | if @options["Proxy"].kind_of?(Net::Telnet) 333 | @sock = @options["Proxy"].sock 334 | elsif @options["Proxy"].kind_of?(IO) 335 | @sock = @options["Proxy"] 336 | else 337 | raise "Error: Proxy must be an instance of Net::Telnet or IO." 338 | end 339 | else 340 | message = "Trying " + @options["Host"] + "...\n" 341 | yield(message) if block_given? 342 | @log.write(message) if @options.has_key?("Output_log") 343 | @dumplog.log_dump('#', message) if @options.has_key?("Dump_log") 344 | 345 | begin 346 | if @options["Timeout"] == false 347 | @sock = TCPSocket.open(@options["Host"], @options["Port"]) 348 | else 349 | Timeout.timeout(@options["Timeout"], Net::OpenTimeout) do 350 | @sock = TCPSocket.open(@options["Host"], @options["Port"]) 351 | end 352 | end 353 | rescue Net::OpenTimeout 354 | raise Net::OpenTimeout, "timed out while opening a connection to the host" 355 | rescue 356 | @log.write($ERROR_INFO.to_s + "\n") if @options.has_key?("Output_log") 357 | @dumplog.log_dump('#', $ERROR_INFO.to_s + "\n") if @options.has_key?("Dump_log") 358 | raise 359 | end 360 | @sock.sync = true 361 | @sock.binmode 362 | 363 | message = "Connected to " + @options["Host"] + ".\n" 364 | yield(message) if block_given? 365 | @log.write(message) if @options.has_key?("Output_log") 366 | @dumplog.log_dump('#', message) if @options.has_key?("Dump_log") 367 | end 368 | 369 | end # initialize 370 | 371 | # The socket the Telnet object is using. Note that this object becomes 372 | # a delegate of the Telnet object, so normally you invoke its methods 373 | # directly on the Telnet object. 374 | attr_reader :sock 375 | 376 | # Set telnet command interpretation on (+mode+ == true) or off 377 | # (+mode+ == false), or return the current value (+mode+ not 378 | # provided). It should be on for true telnet sessions, off if 379 | # using Net::Telnet to connect to a non-telnet service such 380 | # as SMTP. 381 | def telnetmode(mode = nil) 382 | case mode 383 | when nil 384 | @options["Telnetmode"] 385 | when true, false 386 | @options["Telnetmode"] = mode 387 | else 388 | raise ArgumentError, "argument must be true or false, or missing" 389 | end 390 | end 391 | 392 | # Turn telnet command interpretation on (true) or off (false). It 393 | # should be on for true telnet sessions, off if using Net::Telnet 394 | # to connect to a non-telnet service such as SMTP. 395 | def telnetmode=(mode) 396 | if (true == mode or false == mode) 397 | @options["Telnetmode"] = mode 398 | else 399 | raise ArgumentError, "argument must be true or false" 400 | end 401 | end 402 | 403 | # Turn newline conversion on (+mode+ == false) or off (+mode+ == true), 404 | # or return the current value (+mode+ is not specified). 405 | def binmode(mode = nil) 406 | case mode 407 | when nil 408 | @options["Binmode"] 409 | when true, false 410 | @options["Binmode"] = mode 411 | else 412 | raise ArgumentError, "argument must be true or false" 413 | end 414 | end 415 | 416 | # Turn newline conversion on (false) or off (true). 417 | def binmode=(mode) 418 | if (true == mode or false == mode) 419 | @options["Binmode"] = mode 420 | else 421 | raise ArgumentError, "argument must be true or false" 422 | end 423 | end 424 | 425 | # Preprocess received data from the host. 426 | # 427 | # Performs newline conversion and detects telnet command sequences. 428 | # Called automatically by #waitfor(). You should only use this 429 | # method yourself if you have read input directly using sysread() 430 | # or similar, and even then only if in telnet mode. 431 | def preprocess(string) 432 | # combine CR+NULL into CR 433 | string = string.gsub(/#{CR}#{NULL}/no, CR) if @options["Telnetmode"] 434 | 435 | # combine EOL into "\n" 436 | string = string.gsub(/#{EOL}/no, "\n") unless @options["Binmode"] 437 | 438 | # remove NULL 439 | string = string.gsub(/#{NULL}/no, '') unless @options["Binmode"] 440 | 441 | string.gsub(/#{IAC}( 442 | [#{IAC}#{AO}#{AYT}#{DM}#{IP}#{NOP}]| 443 | [#{DO}#{DONT}#{WILL}#{WONT}] 444 | [#{OPT_BINARY}-#{OPT_NEW_ENVIRON}#{OPT_EXOPL}]| 445 | #{SB}[^#{IAC}]*#{IAC}#{SE} 446 | )/xno) do 447 | if IAC == $1 # handle escaped IAC characters 448 | IAC 449 | elsif AYT == $1 # respond to "IAC AYT" (are you there) 450 | self.write("nobody here but us pigeons" + EOL) 451 | '' 452 | elsif DO[0] == $1[0] # respond to "IAC DO x" 453 | if OPT_BINARY[0] == $1[1] 454 | @telnet_option["BINARY"] = true 455 | self.write(IAC + WILL + OPT_BINARY) 456 | else 457 | self.write(IAC + WONT + $1[1..1]) 458 | end 459 | '' 460 | elsif DONT[0] == $1[0] # respond to "IAC DON'T x" with "IAC WON'T x" 461 | self.write(IAC + WONT + $1[1..1]) 462 | '' 463 | elsif WILL[0] == $1[0] # respond to "IAC WILL x" 464 | if OPT_BINARY[0] == $1[1] 465 | self.write(IAC + DO + OPT_BINARY) 466 | elsif OPT_ECHO[0] == $1[1] 467 | self.write(IAC + DO + OPT_ECHO) 468 | elsif OPT_SGA[0] == $1[1] 469 | @telnet_option["SGA"] = true 470 | self.write(IAC + DO + OPT_SGA) 471 | else 472 | self.write(IAC + DONT + $1[1..1]) 473 | end 474 | '' 475 | elsif WONT[0] == $1[0] # respond to "IAC WON'T x" 476 | if OPT_ECHO[0] == $1[1] 477 | self.write(IAC + DONT + OPT_ECHO) 478 | elsif OPT_SGA[0] == $1[1] 479 | @telnet_option["SGA"] = false 480 | self.write(IAC + DONT + OPT_SGA) 481 | else 482 | self.write(IAC + DONT + $1[1..1]) 483 | end 484 | '' 485 | else 486 | '' 487 | end 488 | end 489 | end # preprocess 490 | 491 | # Read data from the host until a certain sequence is matched. 492 | # 493 | # If a block is given, the received data will be yielded as it 494 | # is read in (not necessarily all in one go), or nil if EOF 495 | # occurs before any data is received. Whether a block is given 496 | # or not, all data read will be returned in a single string, or again 497 | # nil if EOF occurs before any data is received. Note that 498 | # received data includes the matched sequence we were looking for. 499 | # 500 | # +options+ can be either a regular expression or a hash of options. 501 | # If a regular expression, this specifies the data to wait for. 502 | # If a hash, this can specify the following options: 503 | # 504 | # Match:: a regular expression, specifying the data to wait for. 505 | # Prompt:: as for Match; used only if Match is not specified. 506 | # String:: as for Match, except a string that will be converted 507 | # into a regular expression. Used only if Match and 508 | # Prompt are not specified. 509 | # Timeout:: the number of seconds to wait for data from the host 510 | # before raising a Timeout::Error. If set to false, 511 | # no timeout will occur. If not specified, the 512 | # Timeout option value specified when this instance 513 | # was created will be used, or, failing that, the 514 | # default value of 10 seconds. 515 | # Waittime:: the number of seconds to wait after matching against 516 | # the input data to see if more data arrives. If more 517 | # data arrives within this time, we will judge ourselves 518 | # not to have matched successfully, and will continue 519 | # trying to match. If not specified, the Waittime option 520 | # value specified when this instance was created will be 521 | # used, or, failing that, the default value of 0 seconds, 522 | # which means not to wait for more input. 523 | # FailEOF:: if true, when the remote end closes the connection then an 524 | # EOFError will be raised. Otherwise, defaults to the old 525 | # behaviour that the function will return whatever data 526 | # has been received already, or nil if nothing was received. 527 | # 528 | def waitfor(options) # :yield: recvdata 529 | time_out = @options["Timeout"] 530 | waittime = @options["Waittime"] 531 | fail_eof = @options["FailEOF"] 532 | 533 | if options.kind_of?(Hash) 534 | prompt = if options.has_key?("Match") 535 | options["Match"] 536 | elsif options.has_key?("Prompt") 537 | options["Prompt"] 538 | elsif options.has_key?("String") 539 | Regexp.new( Regexp.quote(options["String"]) ) 540 | end 541 | time_out = options["Timeout"] if options.has_key?("Timeout") 542 | waittime = options["Waittime"] if options.has_key?("Waittime") 543 | fail_eof = options["FailEOF"] if options.has_key?("FailEOF") 544 | else 545 | prompt = options 546 | end 547 | 548 | if time_out == false 549 | time_out = nil 550 | end 551 | 552 | line = '' 553 | buf = '' 554 | rest = '' 555 | until(prompt === line and not @sock.wait_readable(waittime)) 556 | unless @sock.wait_readable(time_out) 557 | raise Net::ReadTimeout, "timed out while waiting for more data" 558 | end 559 | begin 560 | c = @sock.readpartial(1024 * 1024) 561 | @dumplog.log_dump('<', c) if @options.has_key?("Dump_log") 562 | if @options["Telnetmode"] 563 | c = rest + c 564 | if Integer(c.rindex(/#{IAC}#{SE}/no) || 0) < 565 | Integer(c.rindex(/#{IAC}#{SB}/no) || 0) 566 | buf = preprocess(c[0 ... c.rindex(/#{IAC}#{SB}/no)]) 567 | rest = c[c.rindex(/#{IAC}#{SB}/no) .. -1] 568 | elsif pt = c.rindex(/#{IAC}[^#{IAC}#{AO}#{AYT}#{DM}#{IP}#{NOP}]?\z/no) || 569 | c.rindex(/\r\z/no) 570 | buf = preprocess(c[0 ... pt]) 571 | rest = c[pt .. -1] 572 | else 573 | buf = preprocess(c) 574 | rest = '' 575 | end 576 | else 577 | # Not Telnetmode. 578 | # 579 | # We cannot use preprocess() on this data, because that 580 | # method makes some Telnetmode-specific assumptions. 581 | buf = rest + c 582 | rest = '' 583 | unless @options["Binmode"] 584 | if pt = buf.rindex(/\r\z/no) 585 | buf = buf[0 ... pt] 586 | rest = buf[pt .. -1] 587 | end 588 | buf.gsub!(/#{EOL}/no, "\n") 589 | end 590 | end 591 | @log.print(buf) if @options.has_key?("Output_log") 592 | line += buf 593 | yield buf if block_given? 594 | rescue EOFError # End of file reached 595 | raise if fail_eof 596 | if line == '' 597 | line = nil 598 | yield nil if block_given? 599 | end 600 | break 601 | end 602 | end 603 | line 604 | end 605 | 606 | # Write +string+ to the host. 607 | # 608 | # Does not perform any conversions on +string+. Will log +string+ to the 609 | # dumplog, if the Dump_log option is set. 610 | def write(string) 611 | length = string.length 612 | while 0 < length 613 | @sock.wait_writable 614 | @dumplog.log_dump('>', string[-length..-1]) if @options.has_key?("Dump_log") 615 | length -= @sock.syswrite(string[-length..-1]) 616 | end 617 | end 618 | 619 | # Sends a string to the host. 620 | # 621 | # This does _not_ automatically append a newline to the string. Embedded 622 | # newlines may be converted and telnet command sequences escaped 623 | # depending upon the values of telnetmode, binmode, and telnet options 624 | # set by the host. 625 | def print(string) 626 | string = string.gsub(/#{IAC}/no, IAC + IAC) if @options["Telnetmode"] 627 | 628 | if @options["Binmode"] 629 | self.write(string) 630 | else 631 | if @telnet_option["BINARY"] and @telnet_option["SGA"] 632 | # IAC WILL SGA IAC DO BIN send EOL --> CR 633 | self.write(string.gsub(/\n/n, CR)) 634 | elsif @telnet_option["SGA"] 635 | # IAC WILL SGA send EOL --> CR+NULL 636 | self.write(string.gsub(/\n/n, CR + NULL)) 637 | else 638 | # NONE send EOL --> CR+LF 639 | self.write(string.gsub(/\n/n, EOL)) 640 | end 641 | end 642 | end 643 | 644 | # Sends a string to the host. 645 | # 646 | # Same as #print(), but appends a newline to the string. 647 | def puts(string) 648 | self.print(string + "\n") 649 | end 650 | 651 | # Send a command to the host. 652 | # 653 | # More exactly, sends a string to the host, and reads in all received 654 | # data until is sees the prompt or other matched sequence. 655 | # 656 | # If a block is given, the received data will be yielded to it as 657 | # it is read in. Whether a block is given or not, the received data 658 | # will be return as a string. Note that the received data includes 659 | # the prompt and in most cases the host's echo of our command. 660 | # 661 | # +options+ is either a String, specified the string or command to 662 | # send to the host; or it is a hash of options. If a hash, the 663 | # following options can be specified: 664 | # 665 | # String:: the command or other string to send to the host. 666 | # Match:: a regular expression, the sequence to look for in 667 | # the received data before returning. If not specified, 668 | # the Prompt option value specified when this instance 669 | # was created will be used, or, failing that, the default 670 | # prompt of /[$%#>] \z/n. 671 | # Timeout:: the seconds to wait for data from the host before raising 672 | # a Timeout error. If not specified, the Timeout option 673 | # value specified when this instance was created will be 674 | # used, or, failing that, the default value of 10 seconds. 675 | # 676 | # The command or other string will have the newline sequence appended 677 | # to it. 678 | def cmd(options) # :yield: recvdata 679 | match = @options["Prompt"] 680 | time_out = @options["Timeout"] 681 | fail_eof = @options["FailEOF"] 682 | 683 | if options.kind_of?(Hash) 684 | string = options["String"] 685 | match = options["Match"] if options.has_key?("Match") 686 | time_out = options["Timeout"] if options.has_key?("Timeout") 687 | fail_eof = options["FailEOF"] if options.has_key?("FailEOF") 688 | else 689 | string = options 690 | end 691 | 692 | self.puts(string) 693 | if block_given? 694 | waitfor({"Prompt" => match, "Timeout" => time_out, "FailEOF" => fail_eof}){|c| yield c } 695 | else 696 | waitfor({"Prompt" => match, "Timeout" => time_out, "FailEOF" => fail_eof}) 697 | end 698 | end 699 | 700 | # Login to the host with a given username and password. 701 | # 702 | # The username and password can either be provided as two string 703 | # arguments in that order, or as a hash with keys "Name" and 704 | # "Password". 705 | # 706 | # This method looks for the strings "login" and "Password" from the 707 | # host to determine when to send the username and password. If the 708 | # login sequence does not follow this pattern (for instance, you 709 | # are connecting to a service other than telnet), you will need 710 | # to handle login yourself. 711 | # 712 | # The password can be omitted, either by only 713 | # provided one String argument, which will be used as the username, 714 | # or by providing a has that has no "Password" key. In this case, 715 | # the method will not look for the "Password:" prompt; if it is 716 | # sent, it will have to be dealt with by later calls. 717 | # 718 | # The method returns all data received during the login process from 719 | # the host, including the echoed username but not the password (which 720 | # the host should not echo). If a block is passed in, this received 721 | # data is also yielded to the block as it is received. 722 | def login(options, password = nil) # :yield: recvdata 723 | login_prompt = /[Ll]ogin[: ]*\z/n 724 | password_prompt = /[Pp]ass(?:word|phrase)[: ]*\z/n 725 | if options.kind_of?(Hash) 726 | username = options["Name"] 727 | password = options["Password"] 728 | login_prompt = options["LoginPrompt"] if options["LoginPrompt"] 729 | password_prompt = options["PasswordPrompt"] if options["PasswordPrompt"] 730 | else 731 | username = options 732 | end 733 | 734 | if block_given? 735 | line = waitfor(login_prompt){|c| yield c } 736 | if password 737 | line += cmd({"String" => username, 738 | "Match" => password_prompt}){|c| yield c } 739 | line += cmd(password){|c| yield c } 740 | else 741 | line += cmd(username){|c| yield c } 742 | end 743 | else 744 | line = waitfor(login_prompt) 745 | if password 746 | line += cmd({"String" => username, 747 | "Match" => password_prompt}) 748 | line += cmd(password) 749 | else 750 | line += cmd(username) 751 | end 752 | end 753 | line 754 | end 755 | 756 | # Closes the connection 757 | def close 758 | @sock.close 759 | end 760 | 761 | end # class Telnet 762 | end # module Net 763 | 764 | -------------------------------------------------------------------------------- /lib/net/telnet/version.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | class Telnet 3 | VERSION = "0.2.0" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /net-telnet.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'net/telnet/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "net-telnet" 8 | spec.version = Net::Telnet::VERSION 9 | spec.authors = ["SHIBATA Hiroshi"] 10 | spec.email = ["hsbt@ruby-lang.org"] 11 | 12 | spec.summary = %q{Provides telnet client functionality.} 13 | spec.description = %q{Provides telnet client functionality.} 14 | spec.homepage = "https://github.com/ruby/net-telnet" 15 | spec.license = "ruby" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = "exe" 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ["lib"] 21 | 22 | spec.required_ruby_version = '>= 2.6.0' 23 | end 24 | -------------------------------------------------------------------------------- /spec/binmode_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/net/telnet' 2 | 3 | describe "Net::Telnet#binmode when passed no arguments or nil" do 4 | before(:each) do 5 | @socket = mock("Telnet Socket") 6 | def @socket.kind_of?(klass) 7 | klass == IO 8 | end 9 | @telnet = Net::Telnet.new("Proxy" => @socket) 10 | end 11 | 12 | it "returns the current Binmode value" do 13 | @telnet.binmode.should be_false 14 | @telnet.binmode(nil).should be_false 15 | @telnet.binmode = true 16 | @telnet.binmode.should be_true 17 | end 18 | end 19 | 20 | describe "Net::Telnet#binmode when passed [true]" do 21 | before(:each) do 22 | @socket = mock("Telnet Socket") 23 | def @socket.kind_of?(klass) 24 | klass == IO 25 | end 26 | @telnet = Net::Telnet.new("Proxy" => @socket) 27 | end 28 | 29 | it "returns true" do 30 | @telnet.binmode(true).should be_true 31 | end 32 | 33 | it "sets the Binmode to true" do 34 | @telnet.binmode(true) 35 | @telnet.binmode.should be_true 36 | end 37 | end 38 | 39 | describe "Net::Telnet#binmode when passed [false]" do 40 | before(:each) do 41 | @socket = mock("Telnet Socket") 42 | def @socket.kind_of?(klass) 43 | klass == IO 44 | end 45 | @telnet = Net::Telnet.new("Proxy" => @socket) 46 | end 47 | 48 | it "returns false" do 49 | @telnet.binmode(false).should be_false 50 | end 51 | 52 | it "sets the Binmode to false" do 53 | @telnet.binmode(false) 54 | @telnet.binmode.should be_false 55 | end 56 | end 57 | 58 | 59 | describe "Net::Telnet#binmode when passed [Object]" do 60 | before(:each) do 61 | @socket = mock("Telnet Socket") 62 | def @socket.kind_of?(klass) 63 | klass == IO 64 | end 65 | @telnet = Net::Telnet.new("Proxy" => @socket) 66 | end 67 | 68 | it "raises an ArgumentError" do 69 | lambda { @telnet.binmode(Object.new) }.should raise_error(ArgumentError) 70 | lambda { @telnet.binmode("") }.should raise_error(ArgumentError) 71 | lambda { @telnet.binmode(:sym) }.should raise_error(ArgumentError) 72 | end 73 | 74 | it "does not change the Binmode" do 75 | mode = @telnet.binmode 76 | @telnet.binmode(Object.new) rescue nil 77 | @telnet.binmode.should == mode 78 | end 79 | end 80 | 81 | describe "Net::Telnet#binmode= when passed [true]" do 82 | before(:each) do 83 | @socket = mock("Telnet Socket") 84 | def @socket.kind_of?(klass) 85 | klass == IO 86 | end 87 | @telnet = Net::Telnet.new("Proxy" => @socket) 88 | end 89 | 90 | it "returns true" do 91 | (@telnet.binmode = true).should be_true 92 | end 93 | 94 | it "sets the Binmode to true" do 95 | @telnet.binmode = true 96 | @telnet.binmode.should be_true 97 | end 98 | end 99 | 100 | describe "Net::Telnet#binmode= when passed [false]" do 101 | before(:each) do 102 | @socket = mock("Telnet Socket") 103 | def @socket.kind_of?(klass) 104 | klass == IO 105 | end 106 | @telnet = Net::Telnet.new("Proxy" => @socket) 107 | end 108 | 109 | it "returns false" do 110 | (@telnet.binmode = false).should be_false 111 | end 112 | 113 | it "sets the Binmode to false" do 114 | @telnet.binmode = false 115 | @telnet.binmode.should be_false 116 | end 117 | end 118 | 119 | describe "Net::Telnet#binmode when passed [Object]" do 120 | before(:each) do 121 | @socket = mock("Telnet Socket") 122 | def @socket.kind_of?(klass) 123 | klass == IO 124 | end 125 | @telnet = Net::Telnet.new("Proxy" => @socket) 126 | end 127 | 128 | it "raises an ArgumentError" do 129 | lambda { @telnet.binmode = Object.new }.should raise_error(ArgumentError) 130 | lambda { @telnet.binmode = "" }.should raise_error(ArgumentError) 131 | lambda { @telnet.binmode = nil }.should raise_error(ArgumentError) 132 | lambda { @telnet.binmode = :sym }.should raise_error(ArgumentError) 133 | end 134 | 135 | it "does not change the Binmode" do 136 | @telnet.binmode = true 137 | (@telnet.binmode = Object.new) rescue nil 138 | @telnet.binmode.should be_true 139 | end 140 | end 141 | --------------------------------------------------------------------------------