em-simple_telnet 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. data/lib/em-simple_telnet.rb +1027 -0
  2. metadata +48 -0
@@ -0,0 +1,1027 @@
1
+ require "fiber"
2
+ require 'timeout' # for Timeout::Error
3
+ require "socket" # for SocketError
4
+ require "eventmachine"
5
+
6
+ ##
7
+ # This defines EventMachine::defers_finished? which is used by StopWhenEMDone
8
+ # to stop EventMachine safely when everything is done. The method returns
9
+ # +true+ if the @threadqueue and @resultqueue are undefined/nil/empty *and*
10
+ # none of the threads in the threadpool isn't working anymore.
11
+ #
12
+ # To do this, the method ::spawn_threadpool is redefined to start threads that
13
+ # provide a thread-local variable :working (like
14
+ # <tt>thread_obj[:working]</tt>). This variable tells whether the thread is
15
+ # still working on a deferred action or not.
16
+ #
17
+ module EventMachine # :nodoc:
18
+ def self.defers_finished?
19
+ (not defined? @threadqueue or (tq=@threadqueue).nil? or tq.empty? ) and
20
+ (not defined? @resultqueue or (rq=@resultqueue).nil? or rq.empty? ) and
21
+ (not defined? @threadpool or (tp=@threadpool).nil? or tp.none? {|t|t[:working]})
22
+ end
23
+
24
+ def self.spawn_threadpool
25
+ until @threadpool.size == @threadpool_size.to_i
26
+ thread = Thread.new do
27
+ Thread.current.abort_on_exception = true
28
+ while true
29
+ Thread.current[:working] = false
30
+ op, cback = *@threadqueue.pop
31
+ Thread.current[:working] = true
32
+ result = op.call
33
+ @resultqueue << [result, cback]
34
+ EventMachine.signal_loopbreak
35
+ end
36
+ end
37
+ @threadpool << thread
38
+ end
39
+ end
40
+ end
41
+
42
+ ##
43
+ # Provides the facility to connect to telnet servers using EventMachine. The
44
+ # asynchronity is hidden so you can use this library just like Net::Telnet in
45
+ # a seemingly synchronous manner. See README for an example.
46
+ #
47
+ # EventMachine.run do
48
+ #
49
+ # opts = {
50
+ # host: "localhost",
51
+ # username: "user",
52
+ # password: "secret",
53
+ # }
54
+ #
55
+ # EM::P::SimpleTelnet.new(opts) do |host|
56
+ # # already logged in
57
+ # puts host.cmd("ls -la")
58
+ # end
59
+ # end
60
+ #
61
+ # Because of being event-driven, it performs quite well and can handle a lot
62
+ # of connections concurrently.
63
+ #
64
+ class EventMachine::Protocols::SimpleTelnet < EventMachine::Connection
65
+
66
+ # :stopdoc:
67
+ IAC = 255.chr # "\377" # "\xff" # interpret as command
68
+ DONT = 254.chr # "\376" # "\xfe" # you are not to use option
69
+ DO = 253.chr # "\375" # "\xfd" # please, you use option
70
+ WONT = 252.chr # "\374" # "\xfc" # I won't use option
71
+ WILL = 251.chr # "\373" # "\xfb" # I will use option
72
+ SB = 250.chr # "\372" # "\xfa" # interpret as subnegotiation
73
+ GA = 249.chr # "\371" # "\xf9" # you may reverse the line
74
+ EL = 248.chr # "\370" # "\xf8" # erase the current line
75
+ EC = 247.chr # "\367" # "\xf7" # erase the current character
76
+ AYT = 246.chr # "\366" # "\xf6" # are you there
77
+ AO = 245.chr # "\365" # "\xf5" # abort output--but let prog finish
78
+ IP = 244.chr # "\364" # "\xf4" # interrupt process--permanently
79
+ BREAK = 243.chr # "\363" # "\xf3" # break
80
+ DM = 242.chr # "\362" # "\xf2" # data mark--for connect. cleaning
81
+ NOP = 241.chr # "\361" # "\xf1" # nop
82
+ SE = 240.chr # "\360" # "\xf0" # end sub negotiation
83
+ EOR = 239.chr # "\357" # "\xef" # end of record (transparent mode)
84
+ ABORT = 238.chr # "\356" # "\xee" # Abort process
85
+ SUSP = 237.chr # "\355" # "\xed" # Suspend process
86
+ EOF = 236.chr # "\354" # "\xec" # End of file
87
+ SYNCH = 242.chr # "\362" # "\xf2" # for telfunc calls
88
+
89
+ OPT_BINARY = 0.chr # "\000" # "\x00" # Binary Transmission
90
+ OPT_ECHO = 1.chr # "\001" # "\x01" # Echo
91
+ OPT_RCP = 2.chr # "\002" # "\x02" # Reconnection
92
+ OPT_SGA = 3.chr # "\003" # "\x03" # Suppress Go Ahead
93
+ OPT_NAMS = 4.chr # "\004" # "\x04" # Approx Message Size Negotiation
94
+ OPT_STATUS = 5.chr # "\005" # "\x05" # Status
95
+ OPT_TM = 6.chr # "\006" # "\x06" # Timing Mark
96
+ OPT_RCTE = 7.chr # "\a" # "\x07" # Remote Controlled Trans and Echo
97
+ OPT_NAOL = 8.chr # "\010" # "\x08" # Output Line Width
98
+ OPT_NAOP = 9.chr # "\t" # "\x09" # Output Page Size
99
+ OPT_NAOCRD = 10.chr # "\n" # "\x0a" # Output Carriage-Return Disposition
100
+ OPT_NAOHTS = 11.chr # "\v" # "\x0b" # Output Horizontal Tab Stops
101
+ OPT_NAOHTD = 12.chr # "\f" # "\x0c" # Output Horizontal Tab Disposition
102
+ OPT_NAOFFD = 13.chr # "\r" # "\x0d" # Output Formfeed Disposition
103
+ OPT_NAOVTS = 14.chr # "\016" # "\x0e" # Output Vertical Tabstops
104
+ OPT_NAOVTD = 15.chr # "\017" # "\x0f" # Output Vertical Tab Disposition
105
+ OPT_NAOLFD = 16.chr # "\020" # "\x10" # Output Linefeed Disposition
106
+ OPT_XASCII = 17.chr # "\021" # "\x11" # Extended ASCII
107
+ OPT_LOGOUT = 18.chr # "\022" # "\x12" # Logout
108
+ OPT_BM = 19.chr # "\023" # "\x13" # Byte Macro
109
+ OPT_DET = 20.chr # "\024" # "\x14" # Data Entry Terminal
110
+ OPT_SUPDUP = 21.chr # "\025" # "\x15" # SUPDUP
111
+ OPT_SUPDUPOUTPUT = 22.chr # "\026" # "\x16" # SUPDUP Output
112
+ OPT_SNDLOC = 23.chr # "\027" # "\x17" # Send Location
113
+ OPT_TTYPE = 24.chr # "\030" # "\x18" # Terminal Type
114
+ OPT_EOR = 25.chr # "\031" # "\x19" # End of Record
115
+ OPT_TUID = 26.chr # "\032" # "\x1a" # TACACS User Identification
116
+ OPT_OUTMRK = 27.chr # "\e" # "\x1b" # Output Marking
117
+ OPT_TTYLOC = 28.chr # "\034" # "\x1c" # Terminal Location Number
118
+ OPT_3270REGIME = 29.chr # "\035" # "\x1d" # Telnet 3270 Regime
119
+ OPT_X3PAD = 30.chr # "\036" # "\x1e" # X.3 PAD
120
+ OPT_NAWS = 31.chr # "\037" # "\x1f" # Negotiate About Window Size
121
+ OPT_TSPEED = 32.chr # " " # "\x20" # Terminal Speed
122
+ OPT_LFLOW = 33.chr # "!" # "\x21" # Remote Flow Control
123
+ OPT_LINEMODE = 34.chr # "\"" # "\x22" # Linemode
124
+ OPT_XDISPLOC = 35.chr # "#" # "\x23" # X Display Location
125
+ OPT_OLD_ENVIRON = 36.chr # "$" # "\x24" # Environment Option
126
+ OPT_AUTHENTICATION = 37.chr # "%" # "\x25" # Authentication Option
127
+ OPT_ENCRYPT = 38.chr # "&" # "\x26" # Encryption Option
128
+ OPT_NEW_ENVIRON = 39.chr # "'" # "\x27" # New Environment Option
129
+ OPT_EXOPL = 255.chr # "\377" # "\xff" # Extended-Options-List
130
+
131
+ NULL = "\000"
132
+ CR = "\015"
133
+ LF = "\012"
134
+ EOL = CR + LF
135
+ # :startdoc:
136
+
137
+ # raised when establishing the TCP connection fails
138
+ class ConnectionFailed < SocketError; end
139
+
140
+ # raised when the login procedure fails
141
+ class LoginFailed < Timeout::Error; end
142
+
143
+ ##
144
+ # Extens Timeout::Error by the attributes _hostname_ and _command_ so one
145
+ # knows where the exception comes from and why.
146
+ #
147
+ class TimeoutError < Timeout::Error
148
+ # hostname this timeout comes from
149
+ attr_accessor :hostname
150
+
151
+ # command that caused this timeout
152
+ attr_accessor :command
153
+ end
154
+
155
+ # default options for new connections (used for merging)
156
+ DefaultOptions = {
157
+ host: "localhost",
158
+ port: 23,
159
+ prompt: %r{[$%#>] \z}n,
160
+ connect_timeout: 3,
161
+ timeout: 10,
162
+ wait_time: 0,
163
+ bin_mode: false,
164
+ telnet_mode: true,
165
+ output_log: nil,
166
+ command_log: nil,
167
+ login_prompt: %r{[Ll]ogin[: ]*\z}n,
168
+ password_prompt: %r{[Pp]ass(?:word|phrase)[: ]*\z}n,
169
+ username: nil,
170
+ password: nil,
171
+
172
+ # telnet protocol stuff
173
+ SGA: false,
174
+ BINARY: false,
175
+ }.freeze
176
+
177
+ # used to terminate the reactor when everything is done
178
+ stop_ticks = 0
179
+ StopWhenEMDone = lambda do
180
+ stop_ticks += 1
181
+ if stop_ticks >= 100
182
+ stop_ticks = 0
183
+ # stop when everything is done
184
+ if self.connection_count.zero? and EventMachine.defers_finished?
185
+ EventMachine.stop
186
+ else
187
+ EventMachine.next_tick(&StopWhenEMDone)
188
+ end
189
+ else
190
+ EventMachine.next_tick(&StopWhenEMDone)
191
+ end
192
+ end
193
+
194
+ # number of active connections
195
+ @@_telnet_connection_count = 0
196
+
197
+ class << self
198
+
199
+ ##
200
+ # Recognizes whether this call was issued by the user program or by
201
+ # EventMachine. If the call was not issued by EventMachine, merges the
202
+ # options provided with the DefaultOptions and creates a Fiber (not
203
+ # started yet). Inside the Fiber SimpleTelnet.connect would be called.
204
+ #
205
+ # If EventMachine's reactor is already running, just starts the Fiber.
206
+ #
207
+ # If it's not running yet, starts a new EventMachine reactor and starts the
208
+ # Fiber. The EventMachine block is stopped using the StopWhenEMDone proc
209
+ # (lambda).
210
+ #
211
+ # The (closed) connection is returned.
212
+ #
213
+ def new *args, &blk
214
+ # call super if first argument is a connection signature of
215
+ # EventMachine
216
+ return super(*args, &blk) if args.first.is_a? Integer
217
+
218
+ # This method was probably called with a Hash of connection options.
219
+
220
+ # create new fiber to connect and execute block
221
+ opts = args[0] || {}
222
+ connection = nil
223
+ fiber = Fiber.new do | callback |
224
+ connection = connect(opts, &blk)
225
+ callback.call if callback
226
+ end
227
+
228
+ if EventMachine.reactor_running?
229
+ # Transfer control to the "inner" Fiber and stop the current one.
230
+ # The block will be called after connect() returned to transfer control
231
+ # back to the "outer" Fiber.
232
+ outer_fiber = Fiber.current
233
+ fiber.transfer ->{ outer_fiber.transfer }
234
+
235
+ else
236
+ # start EventMachine and stop it when connection is done
237
+ EventMachine.run do
238
+ fiber.resume
239
+ EventMachine.next_tick(&StopWhenEMDone)
240
+ end
241
+ end
242
+ return connection
243
+ end
244
+
245
+ ##
246
+ # Merges DefaultOptions with _opts_. Establishes the connection to the
247
+ # <tt>:host</tt> key using EventMachine.connect, logs in using #login and
248
+ # passes the connection to the block provided. Closes the connection using
249
+ # #close after the block terminates. The connection is then returned.
250
+ #
251
+ def connect opts
252
+ opts = DefaultOptions.merge opts
253
+
254
+ params = [
255
+ # for EventMachine.connect
256
+ opts[:host],
257
+ opts[:port],
258
+ self,
259
+
260
+ # pass the *merged* options to SimpleTelnet#initialize
261
+ opts
262
+ ]
263
+
264
+ # start establishing the connection
265
+ connection = EventMachine.connect(*params)
266
+
267
+ # set callback to be executed when connection establishing
268
+ # fails/succeeds
269
+ f = Fiber.current
270
+ connection.connection_state_callback = lambda do |obj=nil|
271
+ @connection_state_callback = nil
272
+ f.resume obj
273
+ end
274
+
275
+ # block here and get result from establishing connection
276
+ state = Fiber.yield
277
+
278
+ # raise if exception (e.g. Telnet::ConnectionFailed)
279
+ raise state if state.is_a? Exception
280
+
281
+ # login
282
+ connection.instance_eval { login }
283
+
284
+ begin
285
+ yield connection
286
+ ensure
287
+ # Use #close so a subclass can execute some kind of logout command
288
+ # before the connection is closed.
289
+ connection.close
290
+ end
291
+
292
+ return connection
293
+ end
294
+
295
+ ##
296
+ # Returns the number of active connections
297
+ # (<tt>@@_telnet_connection_count</tt>).
298
+ #
299
+ def connection_count
300
+ @@_telnet_connection_count
301
+ end
302
+ end
303
+
304
+ ##
305
+ # Initializes the current instance. _opts_ is a Hash of options. The default
306
+ # values are in the constant DefaultOptions. The following keys are
307
+ # recognized:
308
+ #
309
+ # +:host+::
310
+ # the hostname or IP address of the host to connect to, as a String.
311
+ # Defaults to "localhost".
312
+ #
313
+ # +:port+::
314
+ # the port to connect to. Defaults to 23.
315
+ #
316
+ # +:bin_mode+::
317
+ # if +false+ (the default), newline substitution is performed. Outgoing LF
318
+ # is converted to CRLF, and incoming CRLF is converted to LF. If +true+,
319
+ # this substitution is not performed. This value can also be set with the
320
+ # #bin_mode= method. The outgoing conversion only applies to the #puts
321
+ # and #print methods, not the #write method. The precise nature of the
322
+ # newline conversion is also affected by the telnet options SGA and BIN.
323
+ #
324
+ # +:output_log+::
325
+ # the name of the file to write connection status messages and all
326
+ # received traffic to. In the case of a proper Telnet session, this will
327
+ # include the client input as echoed by the host; otherwise, it only
328
+ # includes server responses. Output is appended verbatim to this file.
329
+ # By default, no output log is kept.
330
+ #
331
+ # +:command_log+::
332
+ # the name of the file to write the commands executed in this Telnet
333
+ # session. Commands are appended to this file. By default, no command
334
+ # log is kept.
335
+ #
336
+ # +:prompt+::
337
+ # a regular expression matching the host's command-line prompt sequence.
338
+ # This is needed by the Telnet class to determine when the output from a
339
+ # command has finished and the host is ready to receive a new command. By
340
+ # default, this regular expression is <tt>%r{[$%#>] \z}n</tt>.
341
+ #
342
+ # +:login_prompt+::
343
+ # a regular expression (or String, see #waitfor) used to wait for the
344
+ # login prompt.
345
+ #
346
+ # +:password_prompt+::
347
+ # a regular expression (or String, see #waitfor) used to wait for the
348
+ # password prompt.
349
+ #
350
+ # +:username+::
351
+ # the String that is sent to the telnet server after seeing the login
352
+ # prompt. Just leave this value as +nil+ which is the default value if you
353
+ # don't have to log in.
354
+ #
355
+ # +:password+::
356
+ # the String that is sent to the telnet server after seeing the password
357
+ # prompt. Just leave this value as +nil+ which is the default value if you
358
+ # don't have to print a password after printing the username.
359
+ #
360
+ # +:telnet_mode+::
361
+ # a boolean value, +true+ by default. In telnet mode, traffic received
362
+ # from the host is parsed for special command sequences, and these
363
+ # sequences are escaped in outgoing traffic sent using #puts or #print
364
+ # (but not #write). If you are connecting to a non-telnet service (such
365
+ # as SMTP or POP), this should be set to "false" to prevent undesired data
366
+ # corruption. This value can also be set by the #telnetmode method.
367
+ #
368
+ # +:timeout+::
369
+ # the number of seconds (default: +10+) to wait before timing out while
370
+ # waiting for the prompt (in #waitfor). Exceeding this timeout causes a
371
+ # TimeoutError to be raised. You can disable the timeout by setting
372
+ # this value to +nil+.
373
+ #
374
+ # +:connect_timeout+::
375
+ # the number of seconds (default: +3+) to wait before timing out the
376
+ # initial attempt to connect. You can disable the timeout by setting this
377
+ # value to +nil+.
378
+ #
379
+ # +:wait_time+::
380
+ # the amount of time to wait after seeing what looks like a prompt (that
381
+ # is, received data that matches the Prompt option regular expression) to
382
+ # see if more data arrives. If more data does arrive in this time, it
383
+ # assumes that what it saw was not really a prompt. This is to try to
384
+ # avoid false matches, but it can also lead to missing real prompts (if,
385
+ # for instance, a background process writes to the terminal soon after the
386
+ # prompt is displayed). By default, set to 0, meaning not to wait for
387
+ # more data.
388
+ #
389
+ # The options are actually merged in connect().
390
+ #
391
+ def initialize opts
392
+ @telnet_options = opts
393
+ @last_command = nil
394
+
395
+ @logged_in = nil
396
+ @connection_state = :connecting
397
+ @connection_state_callback = nil
398
+ @input_buffer = ""
399
+ @input_rest = ""
400
+ @wait_time_timer = nil
401
+ @check_input_buffer_timer = nil
402
+
403
+ setup_logging
404
+ end
405
+
406
+ # Last command that was executed in this telnet session
407
+ attr_reader :last_command
408
+
409
+ # Logger used to log output
410
+ attr_reader :output_logger
411
+
412
+ # Logger used to log commands
413
+ attr_reader :command_logger
414
+
415
+ # used telnet options Hash
416
+ attr_reader :telnet_options
417
+
418
+ # the callback executed after connection established or failed
419
+ attr_accessor :connection_state_callback
420
+
421
+ # last prompt matched
422
+ attr_reader :last_prompt
423
+
424
+ ##
425
+ # Return current telnet mode option of this connection.
426
+ #
427
+ def telnet_mode?
428
+ @telnet_options[:telnet_mode]
429
+ end
430
+
431
+ ##
432
+ # Turn telnet command interpretation on or off for this connection. It
433
+ # should be on for true telnet sessions, off if used to connect to a
434
+ # non-telnet service such as SMTP.
435
+ #
436
+ def telnet_mode=(bool)
437
+ @telnet_options[:telnet_mode] = bool
438
+ end
439
+
440
+ ##
441
+ # Return current bin mode option of this connection.
442
+ #
443
+ def bin_mode?
444
+ @telnet_options[:bin_mode]
445
+ end
446
+
447
+ ##
448
+ # Turn newline conversion on or off for this connection.
449
+ #
450
+ def bin_mode=(bool)
451
+ @telnet_options[:bin_mode] = bool
452
+ end
453
+
454
+ ##
455
+ # Set the activity timeout to _seconds_ for this connection. To disable it,
456
+ # set it to +0+ or +nil+.
457
+ #
458
+ def timeout= seconds
459
+ @telnet_options[:timeout] = seconds
460
+ set_comm_inactivity_timeout( seconds )
461
+ end
462
+
463
+ ##
464
+ # If a block is given, sets the timeout to _seconds_ (see #timeout=),
465
+ # executes the block and restores the previous timeout. The block value is
466
+ # returned. This is useful if you want to execute one or more commands with
467
+ # a special timeout.
468
+ #
469
+ # If no block is given, the current timeout is returned.
470
+ #
471
+ # Example:
472
+ #
473
+ # current_timeout = host.timeout
474
+ #
475
+ # host.timeout 200 do
476
+ # host.cmd "command 1"
477
+ # host.cmd "command 2"
478
+ # end
479
+ #
480
+ def timeout seconds=nil
481
+ if block_given?
482
+ before = @telnet_options[:timeout]
483
+ self.timeout = seconds
484
+ begin
485
+ yield
486
+ ensure
487
+ self.timeout = before
488
+ end
489
+ else
490
+ if seconds
491
+ warn "Warning: Use EM::P::SimpleTelnet#timeout= to set the timeout."
492
+ end
493
+ @telnet_options[:timeout]
494
+ end
495
+ end
496
+
497
+ ##
498
+ # When the login succeeded for this connection.
499
+ #
500
+ attr_reader :logged_in
501
+
502
+ ##
503
+ # Returns +true+ if the login already succeeded for this connection.
504
+ # Returns +false+ otherwise.
505
+ #
506
+ def logged_in?
507
+ @logged_in ? true : false
508
+ end
509
+
510
+ ##
511
+ # Returns +true+ if the connection is closed.
512
+ #
513
+ def closed?
514
+ @connection_state == :closed
515
+ end
516
+
517
+ ##
518
+ # Called by EventMachine when data is received.
519
+ #
520
+ # The data is processed using #preprocess_telnet and appended to the
521
+ # <tt>@input_buffer</tt>. The appended data is also logged using
522
+ # #log_output. Then #check_input_buffer is called which checks the input
523
+ # buffer for the prompt.
524
+ #
525
+ def receive_data data
526
+ if @telnet_options[:telnet_mode]
527
+ c = @input_rest + data
528
+ se_pos = c.rindex(/#{IAC}#{SE}/no) || 0
529
+ sb_pos = c.rindex(/#{IAC}#{SB}/no) || 0
530
+ if se_pos < sb_pos
531
+ buf = preprocess_telnet(c[0 ... sb_pos])
532
+ @input_rest = c[sb_pos .. -1]
533
+
534
+ elsif pt_pos = c.rindex(
535
+ /#{IAC}[^#{IAC}#{AO}#{AYT}#{DM}#{IP}#{NOP}]?\z/no) ||
536
+ c.rindex(/\r\z/no)
537
+
538
+ buf = preprocess_telnet(c[0 ... pt_pos])
539
+ @input_rest = c[pt_pos .. -1]
540
+
541
+ else
542
+ buf = preprocess_telnet(c)
543
+ @input_rest.clear
544
+ end
545
+ else
546
+ # Not Telnetmode.
547
+ #
548
+ # We cannot use #preprocess_telnet on this data, because that
549
+ # method makes some Telnetmode-specific assumptions.
550
+ buf = @input_rest + data
551
+ @input_rest.clear
552
+ unless @telnet_options[:bin_mode]
553
+ if pt_pos = buf.rindex(/\r\z/no)
554
+ buf = buf[0 ... pt_pos]
555
+ @input_rest = buf[pt_pos .. -1]
556
+ end
557
+ buf.gsub!(/#{EOL}/no, "\n")
558
+ end
559
+ end
560
+
561
+ # in case only telnet sequences were received
562
+ return if buf.empty?
563
+
564
+ # append output from server to input buffer and log it
565
+ @input_buffer << buf
566
+ log_output buf, true
567
+
568
+ # cancel the timer for wait_time value because we received more data
569
+ if @wait_time_timer
570
+ @wait_time_timer.cancel
571
+ @wait_time_timer = nil
572
+ end
573
+
574
+ # we only need to do something if there's a connection state callback
575
+ return unless @connection_state_callback
576
+
577
+ # we ensure there's no timer running to check the input buffer
578
+ if @check_input_buffer_timer
579
+ @check_input_buffer_timer.cancel
580
+ @check_input_buffer_timer = nil
581
+ end
582
+
583
+ if @input_buffer.size >= 100_000
584
+ ##
585
+ # if the input buffer is really big
586
+ #
587
+
588
+ # We postpone checking the input buffer by one second because the regular
589
+ # expression matches can get quite slow.
590
+ #
591
+ # So as long as data is received (continuously), the input buffer is not
592
+ # checked. It's only checked one second after the whole output has been
593
+ # received.
594
+ @check_input_buffer_timer = EventMachine::Timer.new(1) do
595
+ @check_input_buffer_timer = nil
596
+ check_input_buffer
597
+ end
598
+ else
599
+ ##
600
+ # as long as the input buffer is small
601
+ #
602
+
603
+ # check the input buffer now
604
+ check_input_buffer
605
+ end
606
+ end
607
+
608
+ ##
609
+ # Checks the input buffer (<tt>@input_buffer</tt>) for the prompt we're
610
+ # waiting for. Calls the proc in <tt>@connection_state_callback</tt> if the
611
+ # prompt has been found. Thus, call this method *only* if
612
+ # <tt>@connection_state_callback</tt> is set!
613
+ #
614
+ # If <tt>@telnet_options[:wait_time]</tt> is set, this amount of seconds is
615
+ # waited (call to <tt>@connection_state_callback</tt> is scheduled) after
616
+ # seeing what looks like the prompt before firing the
617
+ # <tt>@connection_state_callback</tt> is fired, so more data can come until
618
+ # the real prompt is reached. This is useful for commands which will cause
619
+ # multiple prompts to be sent.
620
+ #
621
+ def check_input_buffer
622
+ if md = @input_buffer.match(@telnet_options[:prompt])
623
+ blk = lambda do
624
+ @last_prompt = md.to_s # remember last prompt
625
+ output = md.pre_match + @last_prompt
626
+ @input_buffer = md.post_match
627
+ @connection_state_callback.call(output)
628
+ end
629
+
630
+ if s = @telnet_options[:wait_time]
631
+ # fire @connection_state_callback after s seconds
632
+ @wait_time_timer = EventMachine::Timer.new(s, &blk)
633
+ else
634
+ # fire @connection_state_callback now
635
+ blk.call
636
+ end
637
+ end
638
+ end
639
+
640
+ ##
641
+ # Read data from the host until a certain sequence is matched.
642
+ #
643
+ # All data read will be returned in a single string. Note that the received
644
+ # data includes the matched sequence we were looking for.
645
+ #
646
+ # _prompt_ can be a Regexp or String. If it's not a Regexp, it's converted
647
+ # to a Regexp (all special characters escaped) assuming it's a String.
648
+ #
649
+ # _opts_ can be a hash of options. The following options are used and thus
650
+ # can be overridden:
651
+ #
652
+ # * +:timeout+
653
+ # * +:wait_time+ (actually used by #check_input_buffer)
654
+ #
655
+ def waitfor prompt=nil, opts={}
656
+ options_were = @telnet_options
657
+ timeout_was = self.timeout if opts.key?(:timeout)
658
+ opts[:prompt] = prompt if prompt
659
+ @telnet_options = @telnet_options.merge opts
660
+
661
+ # convert String prompt into a Regexp
662
+ unless @telnet_options[:prompt].is_a? Regexp
663
+ regex = Regexp.new(Regexp.quote(@telnet_options[:prompt]))
664
+ @telnet_options[:prompt] = regex
665
+ end
666
+
667
+ # set custom inactivity timeout, if wanted
668
+ self.timeout = @telnet_options[:timeout] if opts.key?(:timeout)
669
+
670
+ # so #unbind knows we were waiting for a prompt (in case that inactivity
671
+ # timeout fires)
672
+ @connection_state = :waiting_for_prompt
673
+
674
+ # for the block in @connection_state_callback
675
+ f = Fiber.current
676
+
677
+ # will be called by #receive_data to resume at "Fiber.yield" below
678
+ @connection_state_callback = lambda do |output|
679
+ @connection_state_callback = nil
680
+ f.resume(output)
681
+ end
682
+
683
+ result = Fiber.yield
684
+
685
+ raise result if result.is_a? Exception
686
+ return result
687
+ ensure
688
+ @telnet_options = options_were
689
+ self.timeout = timeout_was if opts.key?(:timeout)
690
+ @connection_state = :connected
691
+ end
692
+
693
+ alias :write :send_data
694
+
695
+ ##
696
+ # Sends a string to the host.
697
+ #
698
+ # This does _not_ automatically append a newline to the string. Embedded
699
+ # newlines may be converted and telnet command sequences escaped depending
700
+ # upon the values of #telnet_mode, #bin_mode, and telnet options set by the
701
+ # host.
702
+ #
703
+ def print(string)
704
+ string = string.gsub(/#{IAC}/no, IAC + IAC) if telnet_mode?
705
+
706
+ unless bin_mode?
707
+ string = if @telnet_options[:BINARY] and @telnet_options[:SGA]
708
+ # IAC WILL SGA IAC DO BIN send EOL --> CR
709
+ string.gsub(/\n/n, CR)
710
+
711
+ elsif @telnet_options[:SGA]
712
+ # IAC WILL SGA send EOL --> CR+NULL
713
+ string.gsub(/\n/n, CR + NULL)
714
+
715
+ else
716
+ # NONE send EOL --> CR+LF
717
+ string.gsub(/\n/n, EOL)
718
+ end
719
+ end
720
+
721
+ send_data string
722
+ end
723
+
724
+ ##
725
+ # Sends a string to the host.
726
+ #
727
+ # Same as #print, but appends a newline to the string unless there's
728
+ # already one.
729
+ #
730
+ def puts(string)
731
+ string += "\n" unless string.end_with? "\n"
732
+ print string
733
+ end
734
+
735
+ ##
736
+ # Sends a command to the host.
737
+ #
738
+ # More exactly, the following things are done:
739
+ #
740
+ # * stores the command in @last_command
741
+ # * logs it using #log_command
742
+ # * sends a string to the host (#print or #puts)
743
+ # * reads in all received data (using #waitfor)
744
+ # * returns the received data as String
745
+ #
746
+ # _opts_ can be a Hash of options. It is passed to #waitfor as the second
747
+ # parameter. The element in _opts_ with the key <tt>:prompt</tt> is used as
748
+ # the first parameter in the call to #waitfor. Example usage:
749
+ #
750
+ # host.cmd "delete user john", prompt: /Are you sure?/
751
+ # host.cmd "yes"
752
+ #
753
+ # Note that the received data includes the prompt and in most cases the
754
+ # host's echo of our command.
755
+ #
756
+ # If _opts_ has the key <tt>:hide</tt> which evaluates to +true+, calls
757
+ # #log_command with <tt>"<hidden command>"</tt> instead of the command
758
+ # itself. This is useful for passwords, so they don't get logged to the
759
+ # command log.
760
+ #
761
+ # If _opts_ has the key <tt>:raw_command</tt> which evaluates to +true+,
762
+ # #print is used to send the command to the host instead of #puts.
763
+ #
764
+ def cmd command, opts={}
765
+ command = command.to_s
766
+ @last_command = command
767
+
768
+ # log the command
769
+ log_command(opts[:hide] ? "<hidden command>" : command)
770
+
771
+ # send the command
772
+ sendcmd = opts[:raw_command] ? :print : :puts
773
+ self.__send__(sendcmd, command)
774
+
775
+ # wait for the output
776
+ waitfor(opts[:prompt], opts)
777
+ end
778
+
779
+ ##
780
+ # Login to the host with a given username and password.
781
+ #
782
+ # host.login username: "myuser", password: "mypass"
783
+ #
784
+ # This method looks for the login and password prompt (see implementation)
785
+ # from the host to determine when to send the username and password. If the
786
+ # login sequence does not follow this pattern (for instance, you are
787
+ # connecting to a service other than telnet), you will need to handle login
788
+ # yourself.
789
+ #
790
+ # If the key <tt>:password</tt> is omitted (and not set on connection
791
+ # level), the method will not look for a prompt.
792
+ #
793
+ # The method returns all data received during the login process from the
794
+ # host, including the echoed username but not the password (which the host
795
+ # should not echo anyway).
796
+ #
797
+ # Don't forget to set <tt>@logged_in</tt> after the login succeeds when you
798
+ # redefine this method!
799
+ #
800
+ def login opts={}
801
+ opts = @telnet_options.merge opts
802
+
803
+ # don't log in if username is not set
804
+ if opts[:username].nil?
805
+ @logged_in = Time.now
806
+ return
807
+ end
808
+
809
+ begin
810
+ output = waitfor opts[:login_prompt]
811
+
812
+ if opts[:password]
813
+ # login with username and password
814
+ output << cmd(opts[:username], prompt: opts[:password_prompt])
815
+ output << cmd(opts[:password], hide: true)
816
+ else
817
+ # login with username only
818
+ output << cmd(opts[:username])
819
+ end
820
+ rescue Timeout::Error
821
+ e = LoginFailed.new("Timed out while expecting some kind of prompt.")
822
+ e.set_backtrace $!.backtrace
823
+ raise e
824
+ end
825
+
826
+ @logged_in = Time.now
827
+ output
828
+ end
829
+
830
+ ##
831
+ # Called by EventMachine when the connection is being established (not after
832
+ # the connection is established! see #connection_completed). This occurs
833
+ # directly after the call to #initialize.
834
+ #
835
+ # Sets the +pending_connect_timeout+ to
836
+ # <tt>@telnet_options[:connect_timeout]</tt> seconds. This is the duration
837
+ # after which a TCP connection in the connecting state will fail (abort and
838
+ # run #unbind). Increases <tt>@@_telnet_connection_count</tt> by one after
839
+ # that.
840
+ #
841
+ # Sets also the +comm_inactivity_timeout+ to
842
+ # <tt>@telnet_options[:timeout]</tt> seconds. This is the duration after
843
+ # which a TCP connection is automatically closed if no data was sent or
844
+ # received.
845
+ #
846
+ def post_init
847
+ self.pending_connect_timeout = @telnet_options[:connect_timeout]
848
+ self.comm_inactivity_timeout = @telnet_options[:timeout]
849
+ @@_telnet_connection_count += 1
850
+ end
851
+
852
+ ##
853
+ # Called by EventMachine after this connection is closed.
854
+ #
855
+ # Decreases <tt>@@_telnet_connection_count</tt> by one and calls #close_logs.
856
+ #
857
+ # After that and if <tt>@connection_state_callback</tt> is set, it takes a
858
+ # look on <tt>@connection_state</tt>. If it was <tt>:connecting</tt>, calls
859
+ # <tt>@connection_state_callback</tt> with a new instance of
860
+ # ConnectionFailed. If it was <tt>:waiting_for_prompt</tt>, calls the
861
+ # callback with a new instance of TimeoutError.
862
+ #
863
+ # Finally, the <tt>@connection_state</tt> is set to +closed+.
864
+ #
865
+ def unbind
866
+ @@_telnet_connection_count -= 1
867
+ close_logs
868
+
869
+ if @connection_state_callback
870
+ # if we were connecting or waiting for a prompt, return an exception to
871
+ # #waitfor
872
+ case @connection_state
873
+ when :connecting
874
+ @connection_state_callback.call(ConnectionFailed.new)
875
+ when :waiting_for_prompt
876
+ error = TimeoutError.new
877
+
878
+ # set hostname and command
879
+ if hostname = @telnet_options[:host]
880
+ error.hostname = hostname
881
+ end
882
+ error.command = @last_command if @last_command
883
+
884
+ @connection_state_callback.call(error)
885
+ end
886
+ end
887
+
888
+ @connection_state = :closed
889
+ end
890
+
891
+ ##
892
+ # Called by EventMachine after the connection is successfully established.
893
+ #
894
+ def connection_completed
895
+ @connection_state = :connected
896
+ @connection_state_callback.call if @connection_state_callback
897
+ end
898
+
899
+ ##
900
+ # Tells EventMachine to close the connection after sending what's in the
901
+ # output buffer. Redefine this method to execute some logout command like
902
+ # +exit+ or +logout+ before the connection is closed. Don't forget: The
903
+ # command will probably not return a prompt, so use #puts, which doesn't
904
+ # wait for a prompt.
905
+ #
906
+ def close
907
+ close_connection_after_writing
908
+ end
909
+
910
+ ##
911
+ # Close output and command logs if they're set. IOError is rescued because
912
+ # they could already be closed. #closed? can't be used, because the method
913
+ # is not implemented by Logger, for example.
914
+ #
915
+ def close_logs
916
+ begin @output_logger.close
917
+ rescue IOError
918
+ end if @telnet_options[:output_log]
919
+ begin @command_logger.close
920
+ rescue IOError
921
+ end if @telnet_options[:command_log]
922
+ end
923
+
924
+ private
925
+
926
+ ##
927
+ # Sets up output and command logging.
928
+ #
929
+ def setup_logging
930
+ require 'logger'
931
+ if @telnet_options[:output_log]
932
+ @output_logger = Logger.new @telnet_options[:output_log]
933
+ log_output "\n# Starting telnet output log at #{Time.now}"
934
+ end
935
+
936
+ if @telnet_options[:command_log]
937
+ @command_logger = Logger.new @telnet_options[:command_log]
938
+ end
939
+ end
940
+
941
+ ##
942
+ # Logs _output_ to output log. If _exact_ is +true+, it will use #print
943
+ # instead of #puts.
944
+ #
945
+ def log_output output, exact=false
946
+ return unless @telnet_options[:output_log]
947
+ if exact
948
+ @output_logger.print output
949
+ else
950
+ @output_logger.puts output
951
+ end
952
+ end
953
+
954
+ ##
955
+ # Logs _command_ to command log.
956
+ #
957
+ def log_command command
958
+ return unless @telnet_options[:command_log]
959
+ @command_logger.info command
960
+ end
961
+
962
+ ##
963
+ # Preprocess received data from the host.
964
+ #
965
+ # Performs newline conversion and detects telnet command sequences.
966
+ # Called automatically by #receive_data.
967
+ #
968
+ def preprocess_telnet string
969
+ # combine CR+NULL into CR
970
+ string = string.gsub(/#{CR}#{NULL}/no, CR) if telnet_mode?
971
+
972
+ # combine EOL into "\n"
973
+ string = string.gsub(/#{EOL}/no, "\n") unless bin_mode?
974
+
975
+ # remove NULL
976
+ string = string.gsub(/#{NULL}/no, '') unless bin_mode?
977
+
978
+ string.gsub(/#{IAC}(
979
+ [#{IAC}#{AO}#{AYT}#{DM}#{IP}#{NOP}]|
980
+ [#{DO}#{DONT}#{WILL}#{WONT}]
981
+ [#{OPT_BINARY}-#{OPT_NEW_ENVIRON}#{OPT_EXOPL}]|
982
+ #{SB}[^#{IAC}]*#{IAC}#{SE}
983
+ )/xno) do
984
+ if IAC == $1 # handle escaped IAC characters
985
+ IAC
986
+ elsif AYT == $1 # respond to "IAC AYT" (are you there)
987
+ send_data("nobody here but us pigeons" + EOL)
988
+ ''
989
+ elsif DO[0] == $1[0] # respond to "IAC DO x"
990
+ if OPT_BINARY[0] == $1[1]
991
+ @telnet_options[:BINARY] = true
992
+ send_data(IAC + WILL + OPT_BINARY)
993
+ else
994
+ send_data(IAC + WONT + $1[1..1])
995
+ end
996
+ ''
997
+ elsif DONT[0] == $1[0] # respond to "IAC DON'T x" with "IAC WON'T x"
998
+ send_data(IAC + WONT + $1[1..1])
999
+ ''
1000
+ elsif WILL[0] == $1[0] # respond to "IAC WILL x"
1001
+ if OPT_BINARY[0] == $1[1]
1002
+ send_data(IAC + DO + OPT_BINARY)
1003
+ elsif OPT_ECHO[0] == $1[1]
1004
+ send_data(IAC + DO + OPT_ECHO)
1005
+ elsif OPT_SGA[0] == $1[1]
1006
+ @telnet_options[:SGA] = true
1007
+ send_data(IAC + DO + OPT_SGA)
1008
+ else
1009
+ send_data(IAC + DONT + $1[1..1])
1010
+ end
1011
+ ''
1012
+ elsif WONT[0] == $1[0] # respond to "IAC WON'T x"
1013
+ if OPT_ECHO[0] == $1[1]
1014
+ send_data(IAC + DONT + OPT_ECHO)
1015
+ elsif OPT_SGA[0] == $1[1]
1016
+ @telnet_options[:SGA] = false
1017
+ send_data(IAC + DONT + OPT_SGA)
1018
+ else
1019
+ send_data(IAC + DONT + $1[1..1])
1020
+ end
1021
+ ''
1022
+ else
1023
+ ''
1024
+ end
1025
+ end
1026
+ end
1027
+ end
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: em-simple_telnet
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Patrik Wenger
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-11-21 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: This library provides a very simple way to connect to telnet servers
15
+ using EventMachine in a seemingly synchronous manner.
16
+ email:
17
+ - paddor@gmail.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - lib/em-simple_telnet.rb
23
+ homepage: http://github.com/paddor/em-simple_telnet
24
+ licenses: []
25
+ post_install_message:
26
+ rdoc_options: []
27
+ require_paths:
28
+ - lib
29
+ required_ruby_version: !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ! '>='
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ required_rubygems_version: !ruby/object:Gem::Requirement
36
+ none: false
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ requirements: []
42
+ rubyforge_project:
43
+ rubygems_version: 1.8.11
44
+ signing_key:
45
+ specification_version: 3
46
+ summary: Simple telnet client on EventMachine
47
+ test_files: []
48
+ has_rdoc: