Class: Protocols::SimpleTelnet

Inherits:
Connection
  • Object
show all
Defined in:
lib/em-simple_telnet.rb

Overview

Provides the facility to connect to telnet servers using EventMachine. The asynchronity is hidden so you can use this library just like Net::Telnet in a seemingly synchronous manner. See README for an example.

EventMachine.run do

  opts = {
    host: "localhost",
    username: "user",
    password: "secret",
  }

  EM::P::SimpleTelnet.new(opts) do |host|
    # already logged in
    puts host.cmd("ls -la")
  end
end

Because of being event-driven, it performs quite well and can handle a lot of connections concurrently.

Defined Under Namespace

Classes: ConnectionFailed, LoginFailed, TimeoutError

Constant Summary collapse

IAC =

:stopdoc:

255.chr
DONT =

“377” # “xff” # interpret as command

254.chr
DO =

“376” # “xfe” # you are not to use option

253.chr
WONT =

“375” # “xfd” # please, you use option

252.chr
WILL =

“374” # “xfc” # I won't use option

251.chr
SB =

“373” # “xfb” # I will use option

250.chr
GA =

“372” # “xfa” # interpret as subnegotiation

249.chr
EL =

“371” # “xf9” # you may reverse the line

248.chr
EC =

“370” # “xf8” # erase the current line

247.chr
AYT =

“367” # “xf7” # erase the current character

246.chr
AO =

“366” # “xf6” # are you there

245.chr
IP =

“365” # “xf5” # abort output–but let prog finish

244.chr
BREAK =

“364” # “xf4” # interrupt process–permanently

243.chr
DM =

“363” # “xf3” # break

242.chr
NOP =

“362” # “xf2” # data mark–for connect. cleaning

241.chr
SE =

“361” # “xf1” # nop

240.chr
EOR =

“360” # “xf0” # end sub negotiation

239.chr
ABORT =

“357” # “xef” # end of record (transparent mode)

238.chr
SUSP =

“356” # “xee” # Abort process

237.chr
EOF =

“355” # “xed” # Suspend process

236.chr
SYNCH =

“354” # “xec” # End of file

242.chr
OPT_BINARY =

“362” # “xf2” # for telfunc calls

0.chr
OPT_ECHO =

“000” # “x00” # Binary Transmission

1.chr
OPT_RCP =

“001” # “x01” # Echo

2.chr
OPT_SGA =

“002” # “x02” # Reconnection

3.chr
OPT_NAMS =

“003” # “x03” # Suppress Go Ahead

4.chr
OPT_STATUS =

“004” # “x04” # Approx Message Size Negotiation

5.chr
OPT_TM =

“005” # “x05” # Status

6.chr
OPT_RCTE =

“006” # “x06” # Timing Mark

7.chr
OPT_NAOL =

“a” # “x07” # Remote Controlled Trans and Echo

8.chr
OPT_NAOP =

“010” # “x08” # Output Line Width

9.chr
OPT_NAOCRD =

“t” # “x09” # Output Page Size

10.chr
OPT_NAOHTS =

“n” # “x0a” # Output Carriage-Return Disposition

11.chr
OPT_NAOHTD =

“v” # “x0b” # Output Horizontal Tab Stops

12.chr
OPT_NAOFFD =

“f” # “x0c” # Output Horizontal Tab Disposition

13.chr
OPT_NAOVTS =

“r” # “x0d” # Output Formfeed Disposition

14.chr
OPT_NAOVTD =

“016” # “x0e” # Output Vertical Tabstops

15.chr
OPT_NAOLFD =

“017” # “x0f” # Output Vertical Tab Disposition

16.chr
OPT_XASCII =

“020” # “x10” # Output Linefeed Disposition

17.chr
OPT_LOGOUT =

“021” # “x11” # Extended ASCII

18.chr
OPT_BM =

“022” # “x12” # Logout

19.chr
OPT_DET =

“023” # “x13” # Byte Macro

20.chr
OPT_SUPDUP =

“024” # “x14” # Data Entry Terminal

21.chr
OPT_SUPDUPOUTPUT =

“025” # “x15” # SUPDUP

22.chr
OPT_SNDLOC =

“026” # “x16” # SUPDUP Output

23.chr
OPT_TTYPE =

“027” # “x17” # Send Location

24.chr
OPT_EOR =

“030” # “x18” # Terminal Type

25.chr
OPT_TUID =

“031” # “x19” # End of Record

26.chr
OPT_OUTMRK =

“032” # “x1a” # TACACS User Identification

27.chr
OPT_TTYLOC =

“e” # “x1b” # Output Marking

28.chr
OPT_3270REGIME =

“034” # “x1c” # Terminal Location Number

29.chr
OPT_X3PAD =

“035” # “x1d” # Telnet 3270 Regime

30.chr
OPT_NAWS =

“036” # “x1e” # X.3 PAD

31.chr
OPT_TSPEED =

“037” # “x1f” # Negotiate About Window Size

32.chr
OPT_LFLOW =

“ ” # “x20” # Terminal Speed

33.chr
OPT_LINEMODE =

“!” # “x21” # Remote Flow Control

34.chr
OPT_XDISPLOC =

“"” # “x22” # Linemode

35.chr
OPT_OLD_ENVIRON =

“#” # “x23” # X Display Location

36.chr
OPT_AUTHENTICATION =

“$” # “x24” # Environment Option

37.chr
OPT_ENCRYPT =

“%” # “x25” # Authentication Option

38.chr
OPT_NEW_ENVIRON =

“&” # “x26” # Encryption Option

39.chr
OPT_EXOPL =

“'” # “x27” # New Environment Option

255.chr
NULL =

“377” # “xff” # Extended-Options-List

"\000"
CR =
"\015"
LF =
"\012"
EOL =
CR + LF
DefaultOptions =

default options for new connections (used for merging)

{
  host: "localhost",
  port: 23,
  prompt: %r{[$%#>] \z}n,
  connect_timeout: 3,
  timeout: 10,
  wait_time: 0,
  bin_mode: false,
  telnet_mode: true,
  output_log: nil,
  command_log: nil,
  login_prompt: %r{[Ll]ogin[: ]*\z}n,
  password_prompt: %r{[Pp]ass(?:word|phrase)[: ]*\z}n,
  username: nil,
  password: nil,

  # telnet protocol stuff
  SGA: false,
  BINARY: false,
}.freeze
StopWhenEMDone =
lambda do
  stop_ticks += 1
  if stop_ticks >= 100
    stop_ticks = 0
    # stop when everything is done
    if self.connection_count.zero? and EventMachine.defers_finished?
      EventMachine.stop
    else
      EventMachine.next_tick(&StopWhenEMDone)
    end
  else
    EventMachine.next_tick(&StopWhenEMDone)
  end
end
RootFiber =

the root fiber

Fiber.current
@@_telnet_connection_count =

number of active connections

0

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(opts) ⇒ SimpleTelnet

Initializes the current instance. opts is a Hash of options. The default values are in the constant DefaultOptions. The following keys are recognized:

:host

the hostname or IP address of the host to connect to, as a String. Defaults to “localhost”.

:port

the port to connect to. Defaults to 23.

:bin_mode

if false (the default), newline substitution is performed. Outgoing LF is converted to CRLF, and incoming CRLF is converted to LF. If true, this substitution is not performed. This value can also be set with the #bin_mode= method. The outgoing conversion only applies to the #puts and #print methods, not the #write method. The precise nature of the newline conversion is also affected by the telnet options SGA and BIN.

:output_log

the name of the file to write connection status messages and all received traffic to. In the case of a proper Telnet session, this will include the client input as echoed by the host; otherwise, it only includes server responses. Output is appended verbatim to this file. By default, no output log is kept.

:command_log

the name of the file to write the commands executed in this Telnet session. Commands are appended to this file. By default, no command log is kept.

:prompt

a regular expression matching the host's command-line prompt sequence. This is needed by the Telnet class to determine when the output from a command has finished and the host is ready to receive a new command. By default, this regular expression is %r{[$%#>] \z}n.

:login_prompt

a regular expression (or String, see #waitfor) used to wait for the login prompt.

:password_prompt

a regular expression (or String, see #waitfor) used to wait for the password prompt.

:username

the String that is sent to the telnet server after seeing the login prompt. Just leave this value as nil which is the default value if you don't have to log in.

:password

the String that is sent to the telnet server after seeing the password prompt. Just leave this value as nil which is the default value if you don't have to print a password after printing the username.

:telnet_mode

a boolean value, true by default. In telnet mode, traffic received from the host is parsed for special command sequences, and these sequences are escaped in outgoing traffic sent using #puts or #print (but not #write). If you are connecting to a non-telnet service (such as SMTP or POP), this should be set to “false” to prevent undesired data corruption. This value can also be set by the #telnetmode method.

:timeout

the number of seconds (default: 10) to wait before timing out while waiting for the prompt (in #waitfor). Exceeding this timeout causes a TimeoutError to be raised. You can disable the timeout by setting this value to nil.

:connect_timeout

the number of seconds (default: 3) to wait before timing out the initial attempt to connect. You can disable the timeout by setting this value to nil.

:wait_time

the amount of time to wait after seeing what looks like a prompt (that is, received data that matches the Prompt option regular expression) to see if more data arrives. If more data does arrive in this time, it assumes that what it saw was not really a prompt. This is to try to avoid false matches, but it can also lead to missing real prompts (if, for instance, a background process writes to the terminal soon after the prompt is displayed). By default, set to 0, meaning not to wait for more data.

The options are actually merged in connect().


380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/em-simple_telnet.rb', line 380

def initialize opts
  @telnet_options = opts
  @last_command = nil

  @logged_in = nil
  @connection_state = :connecting
  f = Fiber.current
  @fiber_resumer = ->(result = nil){ f.resume(result) }
  @input_buffer = ""
  @input_rest = ""
  @wait_time_timer = nil
  @check_input_buffer_timer = nil
  @recently_received_data = ""
  @logger = opts[:logger] || EventMachine::Protocols::SimpleTelnet.logger

  setup_logging
end

Instance Attribute Details

#command_loggerObject (readonly)

connection specific logger used to log commands


405
406
407
# File 'lib/em-simple_telnet.rb', line 405

def command_logger
  @command_logger
end

#fiber_resumerObject

the callback executed again and again to resume this connection's Fiber


411
412
413
# File 'lib/em-simple_telnet.rb', line 411

def fiber_resumer
  @fiber_resumer
end

#last_commandObject (readonly)

Last command that was executed in this telnet session


399
400
401
# File 'lib/em-simple_telnet.rb', line 399

def last_command
  @last_command
end

#last_promptObject (readonly)

last prompt matched


422
423
424
# File 'lib/em-simple_telnet.rb', line 422

def last_prompt
  @last_prompt
end

#logged_inObject (readonly)

When the login succeeded for this connection.


500
501
502
# File 'lib/em-simple_telnet.rb', line 500

def logged_in
  @logged_in
end

#loggerObject

logger for connection activity (messages from SimpleTelnet)


414
415
416
# File 'lib/em-simple_telnet.rb', line 414

def logger
  @logger
end

#output_loggerObject (readonly)

connection specific logger used to log output


402
403
404
# File 'lib/em-simple_telnet.rb', line 402

def output_logger
  @output_logger
end

#telnet_optionsObject (readonly)

used telnet options Hash


408
409
410
# File 'lib/em-simple_telnet.rb', line 408

def telnet_options
  @telnet_options
end

Class Method Details

.connect(opts) ⇒ Object

Merges DefaultOptions with opts. Establishes the connection to the :host key using EventMachine.connect, logs in using #login and passes the connection to the block provided. Closes the connection using #close after the block terminates, unless it's already #closed?. The connection is then returned.


241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/em-simple_telnet.rb', line 241

def connect opts
  opts = DefaultOptions.merge opts

  params = [
    # for EventMachine.connect
    opts[:host],
    opts[:port],
    self,

    # pass the *merged* options to SimpleTelnet#initialize
    opts
  ]

  begin
    # start establishing the connection
    connection = EventMachine.connect(*params)

    # will be resumed by #connection_completed or #unbind
    connection.pause_and_wait_for_result

    # login
    connection.__send__(:login)

    begin
      yield connection
    ensure
      # Use #close so a subclass can execute some kind of logout command
      # before the connection is closed.
      connection.close unless connection.closed?
    end
  ensure
    # close the connection in any case
    if connection
      connection.close_connection_after_writing

      # give some time to send the remaining data, which should be nothing
      EventMachine.add_timer(2) { connection.close_connection }
    end
  end

  return connection
end

.connection_countObject

Returns the number of active connections (@@_telnet_connection_count).


288
289
290
# File 'lib/em-simple_telnet.rb', line 288

def connection_count
  @@_telnet_connection_count
end

.loggerObject


175
# File 'lib/em-simple_telnet.rb', line 175

def self.logger() @logger end

.new(*args, &blk) ⇒ Object

Recognizes whether this call was issued by the user program or by EventMachine. If the call was not issued by EventMachine, merges the options provided with the DefaultOptions and creates a Fiber (not started yet). Inside the Fiber SimpleTelnet.connect would be called.

If EventMachine's reactor is already running, just starts the Fiber.

If it's not running yet, starts a new EventMachine reactor and starts the Fiber. The EventMachine block is stopped using the StopWhenEMDone proc (lambda).

The (closed) connection is returned.


193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/em-simple_telnet.rb', line 193

def new *args, &blk
  # call super if first argument is a connection signature of
  # EventMachine
  return super(*args, &blk) if args.first.is_a? Integer

  # This method was probably called with a Hash of connection options.

  # create new fiber to connect and execute block
  opts = args[0] || {}
  connection = nil
  fiber = Fiber.new do | callback |
    connection = connect(opts, &blk)
    callback.call if callback
  end

  if EventMachine.reactor_running? and Fiber.current == RootFiber
    logger.debug "EventMachine reactor had been started " +
      "independently. Won't stop it automatically."
    fiber.resume

  elsif EventMachine.reactor_running?
    # NOTE: Seems like the EventMachine reactor is already running, but we're
    # not in the root Fiber. That means we're probably in the process of
    # establishing a nested connection (from inside a Fiber created by SimpleTelnet).

    # Transfer control to the "inner" Fiber and stop the current one.
    # The block will be called after connect() returned to transfer control
    # back to the "outer" Fiber.
    outer_fiber = Fiber.current
    fiber.transfer ->{ outer_fiber.transfer }

  else
    # start EventMachine and stop it when connection is done
    EventMachine.run do
      fiber.resume
      EventMachine.next_tick(&StopWhenEMDone)
    end
  end
  return connection
end

Instance Method Details

#bin_mode=(bool) ⇒ Object

Turn newline conversion on or off for this connection.


450
451
452
# File 'lib/em-simple_telnet.rb', line 450

def bin_mode=(bool)
  @telnet_options[:bin_mode] = bool
end

#bin_mode?Boolean

Return current bin mode option of this connection.

Returns:

  • (Boolean)

443
444
445
# File 'lib/em-simple_telnet.rb', line 443

def bin_mode?
  @telnet_options[:bin_mode]
end

#check_input_bufferObject

Checks the input buffer (@input_buffer) for the prompt we're waiting for. Calls @fiber_resumer with the output if the prompt has been found.

If @telnet_options[:wait_time] is set, it will wait this amount of seconds after seeing what looks like the prompt before calling until the real prompt is received. This is useful for commands that send multiple prompts.


637
638
639
640
641
642
643
644
645
646
647
# File 'lib/em-simple_telnet.rb', line 637

def check_input_buffer
  return unless md = @input_buffer.match(@telnet_options[:prompt])

  if s = @telnet_options[:wait_time] and s > 0
    # resume Fiber after s seconds
    @wait_time_timer = EventMachine::Timer.new(s) { process_match_data(md) }
  else
    # resume Fiber now
    process_match_data(md)
  end
end

#closeObject

Redefine this method to execute some logout command like exit or logout before the connection is closed. Don't forget: The command will probably not return a prompt, so use #puts, which doesn't wait for a prompt.


983
984
# File 'lib/em-simple_telnet.rb', line 983

def close
end

#close_logsObject

Close output and command logs if they're set. IOError is rescued because they could already be closed. #closed? can't be used, because the method is not implemented by Logger, for example.


991
992
993
994
995
996
997
998
# File 'lib/em-simple_telnet.rb', line 991

def close_logs
  begin @output_logger.close
  rescue IOError
  end if @telnet_options[:output_log]
  begin @command_logger.close
  rescue IOError
  end if @telnet_options[:command_log]
end

#closed?Boolean

Returns true if the connection is closed.

Returns:

  • (Boolean)

513
514
515
# File 'lib/em-simple_telnet.rb', line 513

def closed?
  @connection_state == :closed
end

#cmd(command, opts = {}) ⇒ Object

Sends a command to the host.

More exactly, the following things are done:

  • stores the command in @last_command

  • logs it using #log_command

  • sends a string to the host (#print or #puts)

  • reads in all received data (using #waitfor)

  • returns the received data as String

opts can be a Hash of options. It is passed to #waitfor as the second parameter. The element in opts with the key :prompt is used as the first parameter in the call to #waitfor. Example usage:

host.cmd "delete user john", prompt: /Are you sure?/
host.cmd "yes"

Note that the received data includes the prompt and in most cases the host's echo of our command.

If opts has the key :hide which evaluates to true, calls #log_command with "<hidden command>" instead of the command itself. This is useful for passwords, so they don't get logged to the command log.

If opts has the key :raw_command which evaluates to true, #print is used to send the command to the host instead of #puts.


834
835
836
837
838
839
840
841
842
843
844
845
# File 'lib/em-simple_telnet.rb', line 834

def cmd command, opts={}
  @last_command = command = command.to_s

  # log the command
  log_command(opts[:hide] ? "<hidden command>" : command)

  # send the command
  opts[:raw_command] ? print(command) : puts(command)

  # wait for the output
  waitfor(opts[:prompt], opts)
end

#connection_completedObject

Called by EventMachine after the connection is successfully established.


967
968
969
970
971
972
973
974
975
# File 'lib/em-simple_telnet.rb', line 967

def connection_completed
  self.connection_state = :connected
  @fiber_resumer.(:connection_completed)

  # log received data in a more readable way
  if logger.debug?
    EventMachine.add_periodic_timer(0.5) { log_recently_received_data }
  end
end

#connection_state_callbackObject

Deprecated.

use #fiber_resumer instead


417
418
419
# File 'lib/em-simple_telnet.rb', line 417

def connection_state_callback
  fiber_resumer
end

#listen(opts = {}, &blk) ⇒ Object

Listen for anything that's received from the node. Each received chunk will be yielded to the block passed. To make it stop listening, the block should return or raise something.

The default timeout during listening is 90 seconds. Use the option :timeout to change this.


740
741
742
743
744
745
746
747
# File 'lib/em-simple_telnet.rb', line 740

def listen(opts = {}, &blk)
  self.connection_state = :listening
  timeout(opts.fetch(:timeout, 90)) do
    yield pause_and_wait_for_result while true
  end
ensure
  self.connection_state = :connected if !closed?
end

#logged_in?Boolean

Returns true if the login already succeeded for this connection. Returns false otherwise.

Returns:

  • (Boolean)

506
507
508
# File 'lib/em-simple_telnet.rb', line 506

def logged_in?
  @logged_in ? true : false
end

#login(opts = {}) ⇒ Object

Login to the host with a given username and password.

host. username: "myuser", password: "mypass"

This method looks for the login and password prompt (see implementation) from the host to determine when to send the username and password. If the login sequence does not follow this pattern (for instance, you are connecting to a service other than telnet), you will need to handle login yourself.

If the key :password is omitted (and not set on connection level), the method will not look for a prompt.

The method returns all data received during the login process from the host, including the echoed username but not the password (which the host should not echo anyway).

Don't forget to set @logged_in after the login succeeds when you redefine this method!


868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
# File 'lib/em-simple_telnet.rb', line 868

def  opts={}
  opts = @telnet_options.merge opts

  # don't log in if username is not set
  if opts[:username].nil?
    @logged_in = Time.now
    return
  end

  begin
    output = waitfor opts[:login_prompt]

    if opts[:password]
      # login with username and password
      output << cmd(opts[:username], prompt: opts[:password_prompt])
      output << cmd(opts[:password], hide: true)
    else
      # login with username only
      output << cmd(opts[:username])
    end
  rescue Timeout::Error
    e = LoginFailed.new("Timed out while expecting some kind of prompt.")
    e.set_backtrace $!.backtrace
    raise e
  end

  @logged_in = Time.now
  output
end

#nodeObject

Identifier for this connection. Like an IP address or hostname. In this case, it's @telnet_options[:host].


730
731
732
# File 'lib/em-simple_telnet.rb', line 730

def node
  @telnet_options[:host]
end

#pause_and_wait_for_resultObject

Pauses the current Fiber. When resumed, returns the value passed. If the value passed is an Exeption, it's raised.


708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
# File 'lib/em-simple_telnet.rb', line 708

def pause_and_wait_for_result
  result = nil
  while result == nil
    # measure how long Fiber is paused
    if logger.debug?
      before_pause = Time.now
      result = Fiber.yield
      pause_duration = Time.now - before_pause

      logger.debug "#{node}: Fiber was paused for " +
        "#{pause_duration * 1000}ms and is resumed with: #{result.inspect}"
    else
      result = Fiber.yield
    end
  end

  raise result if result.is_a? Exception
  return result
end

#post_initObject

Called by EventMachine when the connection is being established (not after the connection is established! see #connection_completed). This occurs directly after the call to #initialize.

Sets the pending_connect_timeout to @telnet_options[:connect_timeout] seconds. This is the duration after which a TCP connection in the connecting state will fail (abort and run #unbind). Increases @@_telnet_connection_count by one after that.

Sets also the comm_inactivity_timeout to @telnet_options[:timeout] seconds. This is the duration after which a TCP connection is automatically closed if no data was sent or received.


914
915
916
917
918
# File 'lib/em-simple_telnet.rb', line 914

def post_init
  self.pending_connect_timeout = @telnet_options[:connect_timeout]
  self.comm_inactivity_timeout = @telnet_options[:timeout]
  @@_telnet_connection_count += 1
end

Sends a string to the host.

This does not automatically append a newline to the string. Embedded newlines may be converted and telnet command sequences escaped depending upon the values of #telnet_mode, #bin_mode, and telnet options set by the host.


773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
# File 'lib/em-simple_telnet.rb', line 773

def print(string)
  string = string.gsub(/#{IAC}/no, IAC + IAC) if telnet_mode?

  unless bin_mode?
    string = if @telnet_options[:BINARY] and @telnet_options[:SGA]
      # IAC WILL SGA IAC DO BIN send EOL --> CR
      string.gsub(/\n/n, CR)

    elsif @telnet_options[:SGA]
      # IAC WILL SGA send EOL --> CR+NULL
      string.gsub(/\n/n, CR + NULL)

    else
      # NONE send EOL --> CR+LF
      string.gsub(/\n/n, EOL)
    end
  end

  send_data string
end

#process_match_data(md) ⇒ Object

Takes out the @last_prompt from md (MatchData) and remembers it. Resumes the fiber (using @fiber_resumer) with the output (which includes the prompt and everything before).


652
653
654
655
656
657
# File 'lib/em-simple_telnet.rb', line 652

def process_match_data(md)
  @last_prompt = md.to_s # remember the prompt matched
  output = md.pre_match + @last_prompt
  @input_buffer = md.post_match
  @fiber_resumer.(output)
end

#process_payload(buf) ⇒ Object

Appends buf to the @input_buffer. Then cancels the @wait_time_timer and @check_input_buffer_timer if they're set.

Does some performance optimizations in case the input buffer is becoming huge and finally calls #check_input_buffer.


572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
# File 'lib/em-simple_telnet.rb', line 572

def process_payload(buf)
  # append output from server to input buffer and log it
  @input_buffer << buf

  case @connection_state
  when :waiting_for_prompt

    # cancel the timer for wait_time value because we received more data
    if @wait_time_timer
      @wait_time_timer.cancel
      @wait_time_timer = nil
    end

    # we ensure there's no timer running for checking the input buffer
    if @check_input_buffer_timer
      @check_input_buffer_timer.cancel
      @check_input_buffer_timer = nil
    end

    if @input_buffer.size >= 100_000
      ##
      # if the input buffer is really big
      #

      # We postpone checking the input buffer by one second because the regular
      # expression matches can get quite slow.
      #
      # So as long as data is being received (continuously), the input buffer
      # is not checked. It's only checked one second after the whole output
      # has been received.
      @check_input_buffer_timer = EventMachine::Timer.new(1) do
        @check_input_buffer_timer = nil
        check_input_buffer
      end
    else
      ##
      # as long as the input buffer is small
      #

      # check the input buffer now
      check_input_buffer
    end
  when :listening
    @fiber_resumer.(buf)
  when :connected, :sleeping
    logger.debug "#{node}: Discarding data that was received " +
      "while not waiting " +
      "for data (state = #{@connection_state.inspect}): #{buf.inspect}"
  else
    raise "Don't know what to do with received data while being in " +
      "connection state #{@connection_state.inspect}"
  end
end

#puts(string) ⇒ Object

Sends a string to the host.

Same as #print, but appends a newline to the string unless there's already one.


800
801
802
803
# File 'lib/em-simple_telnet.rb', line 800

def puts(string)
  string += "\n" unless string.end_with? "\n"
  print string
end

#receive_data(data) ⇒ Object

Called by EventMachine when data is received.

The data is processed using #preprocess_telnet. The data then logged using #log_output. Then calls #process_payload.


523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
# File 'lib/em-simple_telnet.rb', line 523

def receive_data data
  @recently_received_data << data if log_recently_received_data?
  if @telnet_options[:telnet_mode]
    c = @input_rest + data
    se_pos = c.rindex(/#{IAC}#{SE}/no) || 0
    sb_pos = c.rindex(/#{IAC}#{SB}/no) || 0
    if se_pos < sb_pos
      buf = preprocess_telnet(c[0 ... sb_pos])
      @input_rest = c[sb_pos .. -1]

    elsif pt_pos = c.rindex(
      /#{IAC}[^#{IAC}#{AO}#{AYT}#{DM}#{IP}#{NOP}]?\z/no) ||
      c.rindex(/\r\z/no)

      buf = preprocess_telnet(c[0 ... pt_pos])
      @input_rest = c[pt_pos .. -1]

    else
      buf = preprocess_telnet(c)
      @input_rest.clear
    end
  else
    # Not Telnetmode.
    #
    # We cannot use #preprocess_telnet on this data, because that
    # method makes some Telnetmode-specific assumptions.
    buf = @input_rest + data
    @input_rest.clear
    unless @telnet_options[:bin_mode]
      buf.chop! if buf =~ /\r\z/no
      buf.gsub!(/#{EOL}/no, "\n")
    end
  end

  # in case only telnet sequences were received
  return if buf.empty?

  log_output(buf)
  process_payload(buf)
end

#send_data(s) ⇒ Object

Raises Errno::ENOTCONN in case the connection is closed (#unbind has been called before).

Raises:

  • (Errno::ENOTCONN)

756
757
758
759
760
761
762
763
# File 'lib/em-simple_telnet.rb', line 756

def send_data(s)
  raise Errno::ENOTCONN,
    "Can't send data: Connection is already closed." if closed?
  @last_sent_data = Time.now
  log_recently_received_data
  logger.debug "#{node}: Sending #{s.inspect}"
  super
end

#telnet_mode=(bool) ⇒ Object

Turn telnet command interpretation on or off for this connection. It should be on for true telnet sessions, off if used to connect to a non-telnet service such as SMTP.


436
437
438
# File 'lib/em-simple_telnet.rb', line 436

def telnet_mode=(bool)
  @telnet_options[:telnet_mode] = bool
end

#telnet_mode?Boolean

Return current telnet mode option of this connection.

Returns:

  • (Boolean)

427
428
429
# File 'lib/em-simple_telnet.rb', line 427

def telnet_mode?
  @telnet_options[:telnet_mode]
end

#timeout(seconds = nil) ⇒ Object

If a block is given, sets the timeout to seconds (see #timeout=), executes the block and restores the previous timeout. The block value is returned. This is useful if you want to execute one or more commands with a special timeout.

If no block is given, the current timeout is returned.

Example:

current_timeout = host.timeout

 host.timeout 200 do
   host.cmd "command 1"
   host.cmd "command 2"
 end

480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
# File 'lib/em-simple_telnet.rb', line 480

def timeout seconds=nil
  if block_given?
    before = @telnet_options[:timeout]
    self.timeout = seconds
    begin
      yield
    ensure
      self.timeout = before
    end
  else
    if seconds
      logger.warn "Use #timeout= to set the timeout."
    end
    @telnet_options[:timeout]
  end
end

#timeout=(seconds) ⇒ Object

Set the activity timeout to seconds for this connection. To disable it, set it to 0 or nil.


458
459
460
461
# File 'lib/em-simple_telnet.rb', line 458

def timeout= seconds
  @telnet_options[:timeout] = seconds
  set_comm_inactivity_timeout( seconds )
end

#unbind(reason) ⇒ Object

Called by EventMachine after this connection is closed.

Decreases @@_telnet_connection_count by one and calls #close_logs.

After that is set, it takes a look at @connection_state. If it was :connecting, calls If it was :waiting_for_prompt, calls the same method with a new instance of TimeoutError.

Finally, the @connection_state is set to closed.


933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
# File 'lib/em-simple_telnet.rb', line 933

def unbind(reason)
  prev_conn_state = @connection_state
  self.connection_state = :closed
  logger.debug "#{node}: Unbind reason: " + reason.inspect
  @@_telnet_connection_count -= 1
  close_logs

  # if we were connecting or waiting for a prompt, return an exception to
  # #waitfor
  case prev_conn_state
  when :waiting_for_prompt, :listening
    # NOTE: reason should be Errno::ETIMEDOUT in these cases.
    error = TimeoutError.new

    # set hostname and command
    if hostname = @telnet_options[:host]
      error.hostname = hostname
    end
    error.command = @last_command if @last_command

    @fiber_resumer.(error)
  when :sleeping, :connected

  when :connecting
    @fiber_resumer.(ConnectionFailed.new)
  else
    logger.error "#{node}: bad connection state #{prev_conn_state.inspect} " +
      "while unbinding"
  end
end

#waitfor(prompt = nil, opts = {}) ⇒ Object

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

All data read will be returned in a single string. Note that the received data includes the matched sequence we were looking for.

prompt can be a Regexp or String. If it's not a Regexp, it's converted to a Regexp (all special characters escaped) assuming it's a String.

opts can be a hash of options. The following options are used and thus can be overridden:

  • :timeout

  • :wait_time (actually used by #check_input_buffer)


674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
# File 'lib/em-simple_telnet.rb', line 674

def waitfor prompt=nil, opts={}
  if closed?
    raise Errno::ENOTCONN,
      "Can't wait for anything when connection is already closed!"
  end
  options_were = @telnet_options
  timeout_was = self.timeout if opts.key?(:timeout)
  opts[:prompt] = prompt if prompt
  @telnet_options = @telnet_options.merge opts

  # convert String prompt into a Regexp
  unless @telnet_options[:prompt].is_a? Regexp
    regex = Regexp.new(Regexp.quote(@telnet_options[:prompt]))
    @telnet_options[:prompt] = regex
  end

  # set custom inactivity timeout, if wanted
  self.timeout = @telnet_options[:timeout] if opts.key?(:timeout)

  # so #unbind knows we were waiting for a prompt (in case that inactivity
  # timeout fires)
  self.connection_state = :waiting_for_prompt

  pause_and_wait_for_result
ensure
  @telnet_options = options_were
  self.timeout = timeout_was if opts.key?(:timeout)

  # #unbind could have been called in the meantime
  self.connection_state = :connected if !closed?
end

#write(s) ⇒ Object

Passes argument to #send_data.


750
751
752
# File 'lib/em-simple_telnet.rb', line 750

def write(s)
  send_data(s)
end