em-simple_telnet 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: