em-ssh 0.1.0 → 0.2.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.
data/README.md CHANGED
@@ -53,7 +53,7 @@ See [http://net-ssh.github.com/ssh/v2/api/index.html](http://net-ssh.github.com/
53
53
 
54
54
  ##Shell
55
55
 
56
- Em-ssh provides an exepct-like shell abstraction layer on top of net-ssh in EM::Ssh::Shell
56
+ Em-ssh provides an expect-like shell abstraction layer on top of net-ssh in EM::Ssh::Shell
57
57
 
58
58
  ### Example
59
59
  require 'em-ssh/shell'
data/lib/em-ssh.rb CHANGED
@@ -15,7 +15,7 @@ module EventMachine
15
15
  # end
16
16
  class Ssh
17
17
  DEFAULT_PORT = 22
18
-
18
+
19
19
  # Generic error tag
20
20
  module Error; end
21
21
  # Any class that inherits from SshError will be an Exception and include a Ssh::Error tag
@@ -25,8 +25,8 @@ module EventMachine
25
25
  class Disconnected < SshError; end
26
26
  class ConnectionFailed < SshError; end
27
27
  class ConnectionTimeout < ConnectionFailed; end
28
-
29
-
28
+
29
+
30
30
  class << self
31
31
  attr_writer :logger
32
32
  # Creates a logger when necessary
@@ -34,7 +34,7 @@ module EventMachine
34
34
  def logger(level = Logger::WARN)
35
35
  @logger ||= ::Logger.new(STDERR).tap{ |l| l.level = level }
36
36
  end
37
-
37
+
38
38
  # Connect to an ssh server
39
39
  # @param [String] host
40
40
  # @param [String] user
@@ -42,7 +42,7 @@ module EventMachine
42
42
  # @yield [Session] an EventMachine compatible Net::SSH::Session
43
43
  # @see http://net-ssh.github.com/ssh/v2/api/index.html
44
44
  # @return [Session]
45
- # @example
45
+ # @example
46
46
  # EM::Ssh.start(host, user, options) do |connection|
47
47
  # log.debug "**** connected: #{connection}"
48
48
  # connection.open_channel do |channel|
@@ -55,7 +55,7 @@ module EventMachine
55
55
  end
56
56
  alias :start :connect
57
57
  end # << self
58
-
58
+
59
59
  # Pull in the constants from Net::SSH::[Transport, Connection and Authentication]
60
60
  # and define them locally.
61
61
  [:Transport, :Connection, :Authentication]
@@ -65,10 +65,12 @@ module EventMachine
65
65
  const_set(name, mod.const_get(name))
66
66
  end # |name|
67
67
  end # |module|
68
-
68
+
69
69
  end # class::Ssh
70
70
  end # module::EventMachine
71
71
 
72
+ EM::P::Ssh = EventMachine::Ssh
73
+
72
74
 
73
75
  require 'em-ssh/callbacks'
74
76
  require 'em-ssh/connection'
@@ -2,36 +2,36 @@ module EventMachine
2
2
  class Ssh
3
3
  class AuthenticationSession < Net::SSH::Authentication::Session
4
4
  include Log
5
-
5
+
6
6
  def authenticate(*args)
7
7
  debug { "authenticate(#{args.join(", ")})" }
8
8
  super(*args)
9
9
  end # authenticate(*args)
10
-
10
+
11
11
  # Returns once an acceptable auth packet is received.
12
12
  def next_message
13
- packet = transport.next_message
13
+ packet = transport.next_message
14
14
 
15
- case packet.type
16
- when USERAUTH_BANNER
17
- info { packet[:message] }
18
- transport.fire(:auth_banner, packet[:message])
19
- return next_message
20
- when USERAUTH_FAILURE
21
- @allowed_auth_methods = packet[:authentications].split(/,/)
22
- debug { "allowed methods: #{packet[:authentications]}" }
23
- return packet
15
+ case packet.type
16
+ when USERAUTH_BANNER
17
+ info { packet[:message] }
18
+ transport.fire(:auth_banner, packet[:message])
19
+ return next_message
20
+ when USERAUTH_FAILURE
21
+ @allowed_auth_methods = packet[:authentications].split(/,/)
22
+ debug { "allowed methods: #{packet[:authentications]}" }
23
+ return packet
24
24
 
25
- when USERAUTH_METHOD_RANGE, SERVICE_ACCEPT
26
- return packet
25
+ when USERAUTH_METHOD_RANGE, SERVICE_ACCEPT
26
+ return packet
27
27
 
28
- when USERAUTH_SUCCESS
29
- transport.hint :authenticated
30
- return packet
28
+ when USERAUTH_SUCCESS
29
+ transport.hint :authenticated
30
+ return packet
31
31
 
32
- else
33
- raise SshError, "unexpected message #{packet.type} (#{packet})"
34
- end
32
+ else
33
+ raise SshError, "unexpected message #{packet.type} (#{packet})"
34
+ end
35
35
  end # next_message
36
36
  end # class::AuthenticationSession
37
37
  end # module::Ssh
@@ -19,13 +19,13 @@ module EventMachine
19
19
  # end # @header[-1] == "\n"
20
20
  # end # |data|
21
21
  module Callbacks
22
-
22
+
23
23
  # @return [Hash] The registered callbacks
24
24
  def callbacks
25
25
  @clbks ||= {}
26
26
  end # callbacks
27
-
28
-
27
+
28
+
29
29
  # Signal that an event has occured.
30
30
  # Each callback will receive whatever args are passed to fire, or the object that the event was fired upon.
31
31
  # @param [Symbol] event
@@ -36,22 +36,22 @@ module EventMachine
36
36
  args = self if args.empty?
37
37
  (callbacks[event] ||= []).clone.map { |cb| cb.call(*args) }
38
38
  end # fire(event)
39
-
39
+
40
40
  # Register a callback to be fired when a matching event occurs.
41
41
  # The callback will be fired when the event occurs until it returns true.
42
42
  # @param [Symbol] event
43
43
  def on(event, &blk)
44
- #log.debug("#{self}.on(#{event.inspect}, #{blk})")
44
+ #log.debug("#{self}.on(#{event.inspect}, #{blk})")
45
45
  if block_given?
46
46
  raise "event (#{event.inspect}) must be a symbol when a block is given" unless event.is_a?(Symbol)
47
47
  return Callback.new(self, event, &blk).tap{|cb| (callbacks[event] ||= []).push(cb) }
48
48
  end # block_given?
49
-
49
+
50
50
  raise "event (#{event.inspect}) must be a Callback when a block is not given" unless event.is_a?(Callback)
51
51
  (callbacks[event] ||= []).push(event)
52
52
  return event
53
53
  end # on(event, &blk)
54
-
54
+
55
55
  # Registers a callback that will be canceled after the first time it is called.
56
56
  def on_next(event, &blk)
57
57
  cb = on(event) do |*args|
@@ -59,8 +59,8 @@ module EventMachine
59
59
  blk.call(*args)
60
60
  end # |*args|
61
61
  end # on_next(event, &blk)
62
-
63
-
62
+
63
+
64
64
  class Callback
65
65
  # The object that keeps this callback
66
66
  attr_reader :obj
@@ -68,32 +68,31 @@ module EventMachine
68
68
  attr_reader :event
69
69
  # The block to call when the event is fired
70
70
  attr_reader :block
71
-
71
+
72
72
  def initialize(obj, event, &blk)
73
73
  raise ArgumentError.new("a block is required") unless block_given?
74
74
  @obj = obj
75
75
  @event = event
76
76
  @block = blk
77
77
  end # initialize(obj, event, &blk)
78
-
78
+
79
79
  # Call the callback with optional arguments
80
80
  def call(*args)
81
81
  block.call(*args)
82
82
  end # call(*args)
83
-
84
- # Registers the callback with the object.
83
+
84
+ # Registers the callback with the object.
85
85
  # This is useful if you cancel the callback at one point and want to re-enable it later on.
86
86
  def register
87
87
  @obj.on(self)
88
88
  end # register
89
-
89
+
90
90
  def cancel
91
91
  raise "#{@obj} does not have any callbacks for #{@event.inspect}" unless @obj.respond_to?(:callbacks) && @obj.callbacks.respond_to?(:[]) && @obj.callbacks[@event].respond_to?(:delete)
92
92
  @obj.callbacks[@event].delete(self)
93
93
  self
94
94
  end # cancel
95
95
  end # class::Callback
96
-
97
96
  end # module::Callbacks
98
97
  end # class::Ssh
99
98
  end # module::EventMachine
@@ -5,283 +5,277 @@ module EventMachine
5
5
  # itself into Net::SSH so that the EventMachine reactor loop can take the place of the Net::SSH event loop.
6
6
  # Most of the methods here are only for compatibility with Net::SSH
7
7
  class Connection < EventMachine::Connection
8
- include Log
9
-
10
- # Allows other objects to register callbacks with events that occur on a Ssh instance
11
- include Callbacks
12
-
13
- # maximum number of seconds to wait for a connection
14
- TIMEOUT = 20
15
- # @return [String] The host to connect to, as given to the constructor.
16
- attr_reader :host
17
-
18
- # @return [Fixnum] the port number (DEFAULT_PORT) to connect to, as given in the options to the constructor.
19
- attr_reader :port
20
-
21
- # @return [ServerVersion] The ServerVersion instance that encapsulates the negotiated protocol version.
22
- attr_reader :server_version
23
-
24
- # The Algorithms instance used to perform key exchanges.
25
- attr_reader :algorithms
26
-
27
- # The host-key verifier object used to verify host keys, to ensure that the connection is not being spoofed.
28
- attr_reader :host_key_verifier
29
-
30
- # The hash of options that were given to the object at initialization.
31
- attr_reader :options
32
-
33
- # @return [PacketStream] emulates a socket and ssh packetstream
34
- attr_reader :socket
35
-
36
- # @return [Boolean] true if the connection has been closed
37
- def closed?
38
- @closed == true
39
- end
40
-
41
- # Close the connection
42
- def close
43
- # #unbind will update @closed
44
- close_connection
45
- end
46
-
47
- # Send a packet to the server
48
- def send_message(message)
49
- @socket.send_packet(message)
50
- end
51
- alias :enqueue_message :send_message
52
-
53
- def next_message
54
- return @queue.shift if @queue.any? && algorithms.allow?(@queue.first)
8
+ include Log
9
+
10
+ # Allows other objects to register callbacks with events that occur on a Ssh instance
11
+ include Callbacks
12
+
13
+ # maximum number of seconds to wait for a connection
14
+ TIMEOUT = 20
15
+ # @return [String] The host to connect to, as given to the constructor.
16
+ attr_reader :host
17
+
18
+ # @return [Fixnum] the port number (DEFAULT_PORT) to connect to, as given in the options to the constructor.
19
+ attr_reader :port
20
+
21
+ # @return [ServerVersion] The ServerVersion instance that encapsulates the negotiated protocol version.
22
+ attr_reader :server_version
23
+
24
+ # The Algorithms instance used to perform key exchanges.
25
+ attr_reader :algorithms
26
+
27
+ # The host-key verifier object used to verify host keys, to ensure that the connection is not being spoofed.
28
+ attr_reader :host_key_verifier
29
+
30
+ # The hash of options that were given to the object at initialization.
31
+ attr_reader :options
32
+
33
+ # @return [PacketStream] emulates a socket and ssh packetstream
34
+ attr_reader :socket
35
+
36
+ # @return [Boolean] true if the connection has been closed
37
+ def closed?
38
+ @closed == true
39
+ end
40
+
41
+ # Close the connection
42
+ def close
43
+ # #unbind will update @closed
44
+ close_connection
45
+ end
46
+
47
+ # Send a packet to the server
48
+ def send_message(message)
49
+ @socket.send_packet(message)
50
+ end
51
+ alias :enqueue_message :send_message
52
+
53
+ def next_message
54
+ return @queue.shift if @queue.any? && algorithms.allow?(@queue.first)
55
+ f = Fiber.current
56
+ cb = on(:packet) do |packet|
57
+ if @queue.any? && algorithms.allow?(@queue.first)
58
+ cb.cancel
59
+ f.resume(@queue.shift)
60
+ end
61
+ end # :packet
62
+ return Fiber.yield
63
+ end # next_message
64
+
65
+ # Returns a new service_request packet for the given service name, ready
66
+ # for sending to the server.
67
+ def service_request(service)
68
+ Net::SSH::Buffer.from(:byte, SERVICE_REQUEST, :string, service)
69
+ end
70
+
71
+ # Requests a rekey operation, and simulates a block until the operation completes.
72
+ # If a rekey is already pending, this returns immediately, having no effect.
73
+ def rekey!
74
+ if !algorithms.pending?
55
75
  f = Fiber.current
56
- cb = on(:packet) do |packet|
57
- if @queue.any? && algorithms.allow?(@queue.first)
58
- cb.cancel
59
- f.resume(@queue.shift)
60
- end
61
- end # :packet
76
+ on_next(:algo_init) do
77
+ f.resume
78
+ end # :algo_init
79
+ algorithms.rekey!
62
80
  return Fiber.yield
63
- end # next_message
64
-
65
- # Returns a new service_request packet for the given service name, ready
66
- # for sending to the server.
67
- def service_request(service)
68
- Net::SSH::Buffer.from(:byte, SERVICE_REQUEST, :string, service)
69
81
  end
82
+ end
83
+
84
+ # Returns immediately if a rekey is already in process. Otherwise, if a
85
+ # rekey is needed (as indicated by the socket, see PacketStream#if_needs_rekey?)
86
+ # one is performed, causing this method to block until it completes.
87
+ def rekey_as_needed
88
+ return if algorithms.pending?
89
+ socket.if_needs_rekey? { rekey! }
90
+ end
91
+
92
+
93
+
94
+ ##
95
+ # EventMachine callbacks
96
+ ###
97
+ def post_init
98
+ @socket = PacketStream.new(self)
99
+ @data = @socket.input
100
+ end # post_init
101
+
102
+ # @return
103
+ def unbind
104
+ debug("#{self} is unbound")
105
+ fire(:closed)
106
+ @closed = true
107
+ end
108
+
109
+ def receive_data(data)
110
+ debug("read #{data.length} bytes")
111
+ @data.append(data)
112
+ fire(:data, data)
113
+ end
114
+
115
+ def connection_completed
116
+ @contimeout.cancel
117
+ @nocon.cancel
118
+ end # connection_completed
119
+
120
+ def initialize(options = {})
121
+ debug("#{self.class}.new(#{options})")
122
+ @host = options[:host]
123
+ @port = options[:port]
124
+ @password = options[:password]
125
+ @queue = []
126
+ @options = options
127
+ @timeout = options[:timeout] || TIMEOUT
128
+
129
+ begin
130
+ on(:connected, &options[:callback]) if options[:callback]
131
+ @nocon = on(:closed) { raise ConnectionFailed, @host }
132
+ @contimeout = EM::Timer.new(@timeout) { raise ConnectionTimeout, @host }
133
+
134
+ @error_callback = lambda { |code| raise SshError.new(code) }
135
+
136
+ @host_key_verifier = select_host_key_verifier(options[:paranoid])
137
+ @server_version = ServerVersion.new(self)
138
+ on(:version_negotiated) do
139
+ @data.consume!(@server_version.header.length)
140
+ @algorithms = Net::SSH::Transport::Algorithms.new(self, options)
141
+
142
+ register_data_handler
70
143
 
71
- # Requests a rekey operation, and simulates a block until the operation completes.
72
- # If a rekey is already pending, this returns immediately, having no effect.
73
- def rekey!
74
- if !algorithms.pending?
75
- f = Fiber.current
76
144
  on_next(:algo_init) do
77
- f.resume
145
+ auth = AuthenticationSession.new(self, options)
146
+ user = options.fetch(:user, user)
147
+ Fiber.new do
148
+ if auth.authenticate("ssh-connection", user, options[:password])
149
+ fire(:connected, Session.new(self, options))
150
+ else
151
+ fire(:error, Net::SSH::AuthenticationFailed.new(user))
152
+ close_connection
153
+ end # auth.authenticate("ssh-connection", user, options[:password])
154
+ end.resume # Fiber
78
155
  end # :algo_init
79
- algorithms.rekey!
80
- return Fiber.yield
81
- end
82
- end
83
-
84
- # Returns immediately if a rekey is already in process. Otherwise, if a
85
- # rekey is needed (as indicated by the socket, see PacketStream#if_needs_rekey?)
86
- # one is performed, causing this method to block until it completes.
87
- def rekey_as_needed
88
- return if algorithms.pending?
89
- socket.if_needs_rekey? { rekey! }
90
- end
91
-
92
-
93
-
94
- ##
95
- # EventMachine callbacks
96
- ###
97
- def post_init
98
- @socket = PacketStream.new(self)
99
- @data = @socket.input
100
- end # post_init
101
-
102
- # @return
103
- def unbind
104
- debug("#{self} is unbound")
105
- fire(:closed)
106
- @closed = true
107
- end
108
-
109
- def receive_data(data)
110
- debug("read #{data.length} bytes")
111
- @data.append(data)
112
- fire(:data, data)
113
- end
114
-
115
- def connection_completed
116
- @contimeout.cancel
117
- @nocon.cancel
118
- end # connection_completed
119
-
120
- def initialize(options = {})
121
- debug("#{self.class}.new(#{options})")
122
- @host = options[:host]
123
- @port = options[:port]
124
- @password = options[:password]
125
- @queue = []
126
- @options = options
127
- @timeout = options[:timeout] || TIMEOUT
128
-
129
- begin
130
- on(:connected, &options[:callback]) if options[:callback]
131
- @nocon = on(:closed) { raise ConnectionFailed, @host }
132
- @contimeout = EM::Timer.new(@timeout) { raise ConnectionTimeout, @host }
133
-
134
- @error_callback = lambda { |code| raise SshError.new(code) }
135
-
136
- @host_key_verifier = select_host_key_verifier(options[:paranoid])
137
- @server_version = ServerVersion.new(self)
138
- on(:version_negotiated) do
139
- @data.consume!(@server_version.header.length)
140
- @algorithms = Net::SSH::Transport::Algorithms.new(self, options)
141
-
142
- register_data_handler
143
-
144
- on_next(:algo_init) do
145
- auth = AuthenticationSession.new(self, options)
146
- user = options.fetch(:user, user)
147
- Fiber.new do
148
- if auth.authenticate("ssh-connection", user, options[:password])
149
- fire(:connected, Session.new(self, options))
150
- else
151
- fire(:error, Net::SSH::AuthenticationFailed.new(user))
152
- close_connection
153
- end # auth.authenticate("ssh-connection", user, options[:password])
154
- end.resume # Fiber
155
- end # :algo_init
156
- end # :version_negotiated
157
-
158
- rescue Exception => e
159
- log.fatal("caught an error during initialization: #{e}\n #{e.backtrace.join("\n ")}")
160
- Process.exit
161
- end # begin
162
- self
163
- end # initialize(options = {})
164
-
165
-
166
- ##
167
- # Helpers required for compatibility with Net::SSH
168
- ##
169
-
170
- # Returns the host (and possibly IP address) in a format compatible with
171
- # SSH known-host files.
172
- def host_as_string
173
- @host_as_string ||= "#{host}".tap do |string|
174
- string = "[#{string}]:#{port}" if port != DEFAULT_PORT
175
- _, ip = Socket.unpack_sockaddr_in(get_peername)
176
- if ip != host
177
- string << "," << (port != DEFAULT_PORT ? "[#{ip}]:#{port}" : ip)
178
- end # ip != host
179
- end # |string|
180
- end # host_as_string
181
-
182
- alias :logger :log
183
-
184
-
185
- # Configure's the packet stream's client state with the given set of
186
- # options. This is typically used to define the cipher, compression, and
187
- # hmac algorithms to use when sending packets to the server.
188
- def configure_client(options={})
189
- @socket.client.set(options)
190
- end
191
-
192
- # Configure's the packet stream's server state with the given set of
193
- # options. This is typically used to define the cipher, compression, and
194
- # hmac algorithms to use when reading packets from the server.
195
- def configure_server(options={})
196
- @socket.server.set(options)
197
- end
198
-
199
- # Sets a new hint for the packet stream, which the packet stream may use
200
- # to change its behavior. (See PacketStream#hints).
201
- def hint(which, value=true)
202
- @socket.hints[which] = value
203
- end
204
-
205
- # Returns a new service_request packet for the given service name, ready
206
- # for sending to the server.
207
- def service_request(service)
208
- Net::SSH::Buffer.from(:byte, SERVICE_REQUEST, :string, service)
209
- end
210
-
211
- # Returns a hash of information about the peer (remote) side of the socket,
212
- # including :ip, :port, :host, and :canonized (see #host_as_string).
213
- def peer
214
- @peer ||= {}.tap do |p|
215
- _, ip = Socket.unpack_sockaddr_in(get_peername)
216
- p[:ip] = ip
217
- p[:port] = @port.to_i
218
- p[:host] = @host
219
- p[:canonized] = host_as_string
220
- end
156
+ end # :version_negotiated
157
+
158
+ rescue Exception => e
159
+ log.fatal("caught an error during initialization: #{e}\n #{e.backtrace.join("\n ")}")
160
+ Process.exit
161
+ end # begin
162
+ self
163
+ end # initialize(options = {})
164
+
165
+
166
+ ##
167
+ # Helpers required for compatibility with Net::SSH
168
+ ##
169
+
170
+ # Returns the host (and possibly IP address) in a format compatible with
171
+ # SSH known-host files.
172
+ def host_as_string
173
+ @host_as_string ||= "#{host}".tap do |string|
174
+ string = "[#{string}]:#{port}" if port != DEFAULT_PORT
175
+ _, ip = Socket.unpack_sockaddr_in(get_peername)
176
+ if ip != host
177
+ string << "," << (port != DEFAULT_PORT ? "[#{ip}]:#{port}" : ip)
178
+ end # ip != host
179
+ end # |string|
180
+ end # host_as_string
181
+
182
+ alias :logger :log
183
+
184
+
185
+ # Configure's the packet stream's client state with the given set of
186
+ # options. This is typically used to define the cipher, compression, and
187
+ # hmac algorithms to use when sending packets to the server.
188
+ def configure_client(options={})
189
+ @socket.client.set(options)
190
+ end
191
+
192
+ # Configure's the packet stream's server state with the given set of
193
+ # options. This is typically used to define the cipher, compression, and
194
+ # hmac algorithms to use when reading packets from the server.
195
+ def configure_server(options={})
196
+ @socket.server.set(options)
197
+ end
198
+
199
+ # Sets a new hint for the packet stream, which the packet stream may use
200
+ # to change its behavior. (See PacketStream#hints).
201
+ def hint(which, value=true)
202
+ @socket.hints[which] = value
203
+ end
204
+
205
+ # Returns a new service_request packet for the given service name, ready
206
+ # for sending to the server.
207
+ def service_request(service)
208
+ Net::SSH::Buffer.from(:byte, SERVICE_REQUEST, :string, service)
209
+ end
210
+
211
+ # Returns a hash of information about the peer (remote) side of the socket,
212
+ # including :ip, :port, :host, and :canonized (see #host_as_string).
213
+ def peer
214
+ @peer ||= {}.tap do |p|
215
+ _, ip = Socket.unpack_sockaddr_in(get_peername)
216
+ p[:ip] = ip
217
+ p[:port] = @port.to_i
218
+ p[:host] = @host
219
+ p[:canonized] = host_as_string
221
220
  end
222
-
223
-
224
-
225
-
226
- private
227
-
228
- # Register the primary :data callback
229
- # @return [Callback] the callback that was registered
230
- def register_data_handler
231
- on(:data) do |data|
232
- while (packet = @socket.poll_next_packet)
233
- case packet.type
234
- when DISCONNECT
235
- close_connection
236
- when IGNORE
237
- debug("IGNORE packet received: #{packet[:data].inspect}")
238
- when UNIMPLEMENTED
239
- log.warn("UNIMPLEMENTED: #{packet[:number]}")
240
- when DEBUG
241
- log.send((packet[:always_display] ? :fatal : :debug), packet[:message])
242
- when KEXINIT
243
- Fiber.new do
244
- begin
245
- algorithms.accept_kexinit(packet)
246
- fire(:algo_init) if algorithms.initialized?
247
- rescue Exception => e
248
- fire(:error, e)
249
- end # begin
250
- end.resume
251
- else
252
- @queue.push(packet)
253
- if algorithms.allow?(packet)
254
- fire(:packet, packet)
255
- fire(:session_packet, packet) if packet.type >= GLOBAL_REQUEST
256
- end # algorithms.allow?(packet)
257
- socket.consume!
258
- end # packet.type
259
- end # (packet = @socket.poll_next_packet)
260
- end # |data|
261
- end # register_data_handler
262
-
263
- # Instantiates a new host-key verification class, based on the value of
264
- # the parameter. When true or nil, the default Lenient verifier is
265
- # returned. If it is false, the Null verifier is returned, and if it is
266
- # :very, the Strict verifier is returned. If the argument happens to
267
- # respond to :verify, it is returned directly. Otherwise, an exception
268
- # is raised.
269
- # Taken from Net::SSH::Session
270
- def select_host_key_verifier(paranoid)
271
- case paranoid
272
- when true, nil then
273
- Net::SSH::Verifiers::Lenient.new
274
- when false then
275
- Net::SSH::Verifiers::Null.new
276
- when :very then
277
- Net::SSH::Verifiers::Strict.new
278
- else
279
- paranoid.respond_to?(:verify) ? paranoid : (raise ArgumentError.new("argument to :paranoid is not valid: #{paranoid.inspect}"))
280
- end # paranoid
281
- end # select_host_key_verifier(paranoid)
282
-
221
+ end
222
+
223
+ private
224
+
225
+ # Register the primary :data callback
226
+ # @return [Callback] the callback that was registered
227
+ def register_data_handler
228
+ on(:data) do |data|
229
+ while (packet = @socket.poll_next_packet)
230
+ case packet.type
231
+ when DISCONNECT
232
+ close_connection
233
+ when IGNORE
234
+ debug("IGNORE packet received: #{packet[:data].inspect}")
235
+ when UNIMPLEMENTED
236
+ log.warn("UNIMPLEMENTED: #{packet[:number]}")
237
+ when DEBUG
238
+ log.send((packet[:always_display] ? :fatal : :debug), packet[:message])
239
+ when KEXINIT
240
+ Fiber.new do
241
+ begin
242
+ algorithms.accept_kexinit(packet)
243
+ fire(:algo_init) if algorithms.initialized?
244
+ rescue Exception => e
245
+ fire(:error, e)
246
+ end # begin
247
+ end.resume
248
+ else
249
+ @queue.push(packet)
250
+ if algorithms.allow?(packet)
251
+ fire(:packet, packet)
252
+ fire(:session_packet, packet) if packet.type >= GLOBAL_REQUEST
253
+ end # algorithms.allow?(packet)
254
+ socket.consume!
255
+ end # packet.type
256
+ end # (packet = @socket.poll_next_packet)
257
+ end # |data|
258
+ end # register_data_handler
259
+
260
+ # Instantiates a new host-key verification class, based on the value of
261
+ # the parameter. When true or nil, the default Lenient verifier is
262
+ # returned. If it is false, the Null verifier is returned, and if it is
263
+ # :very, the Strict verifier is returned. If the argument happens to
264
+ # respond to :verify, it is returned directly. Otherwise, an exception
265
+ # is raised.
266
+ # Taken from Net::SSH::Session
267
+ def select_host_key_verifier(paranoid)
268
+ case paranoid
269
+ when true, nil then
270
+ Net::SSH::Verifiers::Lenient.new
271
+ when false then
272
+ Net::SSH::Verifiers::Null.new
273
+ when :very then
274
+ Net::SSH::Verifiers::Strict.new
275
+ else
276
+ paranoid.respond_to?(:verify) ? paranoid : (raise ArgumentError.new("argument to :paranoid is not valid: #{paranoid.inspect}"))
277
+ end # paranoid
278
+ end # select_host_key_verifier(paranoid)
283
279
  end # class::Connection < EventMachine::Connection
284
280
  end # module::Ssh
285
281
  end # module::EventMachine
286
-
287
-
data/lib/em-ssh/log.rb CHANGED
@@ -5,23 +5,23 @@ module EventMachine
5
5
  def log
6
6
  EventMachine::Ssh.logger
7
7
  end
8
-
8
+
9
9
  def debug(msg = nil, &blk)
10
10
  log.debug("#{self.class}".downcase.gsub("::",".") + " #{msg}", &blk)
11
11
  end
12
-
12
+
13
13
  def info(msg = nil, &blk)
14
14
  log.info("#{self.class}".downcase.gsub("::",".") + " #{msg}", &blk)
15
15
  end
16
-
16
+
17
17
  def fatal(msg = nil, &blk)
18
18
  log.fatal("#{self.class}".downcase.gsub("::",".") + " #{msg}", &blk)
19
19
  end
20
-
20
+
21
21
  def warn(msg = nil, &blk)
22
22
  log.warn("#{self.class}".downcase.gsub("::",".") + " #{msg}", &blk)
23
23
  end
24
-
24
+
25
25
  def error(msg = nil, &blk)
26
26
  log.error("#{self.class}".downcase.gsub("::",".") + " #{msg}", &blk)
27
27
  end
@@ -3,8 +3,8 @@ module EventMachine
3
3
  class PacketStream
4
4
  include Net::SSH::BufferedIo
5
5
  include Log
6
-
7
-
6
+
7
+
8
8
  # The map of "hints" that can be used to modify the behavior of the packet
9
9
  # stream. For instance, when authentication succeeds, an "authenticated"
10
10
  # hint is set, which is used to determine whether or not to compress the
@@ -18,12 +18,12 @@ module EventMachine
18
18
  # The client state object, which encapsulates the algorithms used to build
19
19
  # packets to send to the server.
20
20
  attr_reader :client
21
-
21
+
22
22
  # The input stream
23
23
  attr_reader :input
24
24
  # The output stream
25
25
  attr_reader :output
26
-
26
+
27
27
  def initialize(connection)
28
28
  @connection = connection
29
29
  @input = Net::SSH::Buffer.new
@@ -33,7 +33,7 @@ module EventMachine
33
33
  @client = Net::SSH::Transport::State.new(self, :client)
34
34
  @packet = nil
35
35
  end # initialize(content="")
36
-
36
+
37
37
  # Consumes n bytes from the buffer, where n is the current position
38
38
  # unless otherwise specified. This is useful for removing data from the
39
39
  # buffer that has previously been read, when you are expecting more data
@@ -44,7 +44,7 @@ module EventMachine
44
44
  def consume!(*args)
45
45
  input.consume!(*args)
46
46
  end # consume!(*args)
47
-
47
+
48
48
  # Tries to read the next packet. If there is insufficient data to read
49
49
  # an entire packet, this returns immediately, otherwise the packet is
50
50
  # read, post-processed according to the cipher, hmac, and compression
@@ -94,7 +94,7 @@ module EventMachine
94
94
 
95
95
  return Net::SSH::Packet.new(payload)
96
96
  end # poll_next_packet
97
-
97
+
98
98
  # Copyright (c) 2008 Jamis Buck
99
99
  def send_packet(payload)
100
100
  # try to compress the packet
@@ -129,7 +129,7 @@ module EventMachine
129
129
 
130
130
  self
131
131
  end # send_packet(payload)
132
-
132
+
133
133
  # Performs any pending cleanup necessary on the IO and its associated
134
134
  # state objects. (See State#cleanup).
135
135
  def cleanup
@@ -148,7 +148,6 @@ module EventMachine
148
148
  server.reset! if server.needs_rekey?
149
149
  end
150
150
  end
151
-
152
151
  end # class::PacketStream
153
152
  end # class::Ssh
154
153
  end # module::EventMachine
@@ -2,18 +2,18 @@ module EventMachine
2
2
  class Ssh
3
3
  class ServerVersion
4
4
  include Log
5
-
5
+
6
6
  attr_reader :header
7
7
  attr_reader :version
8
-
8
+
9
9
  def initialize(connection)
10
10
  debug("#{self}.new(#{connection})")
11
11
  negotiate!(connection)
12
12
  end
13
-
14
-
15
- private
16
-
13
+
14
+
15
+ private
16
+
17
17
  def negotiate!(connection)
18
18
  @version = ''
19
19
  cb = connection.on(:data) do |data|
@@ -31,7 +31,6 @@ module EventMachine
31
31
  end # @header[-1] == "\n"
32
32
  end # |data|
33
33
  end
34
-
35
34
  end # class::ServerVersion
36
35
  end # module::Ssh
37
36
  end # module::EventMachine
@@ -2,23 +2,23 @@ module EventMachine
2
2
  class Ssh
3
3
  class Session < Net::SSH::Connection::Session
4
4
  include Log
5
-
5
+
6
6
  def initialize(transport, options = {})
7
7
  super(transport, options)
8
8
  register_callbacks
9
9
  end
10
-
10
+
11
11
  # Override the default, blocking behavior of Net::SSH.
12
12
  # Callers to loop will still wait, but not block the loop.
13
13
  def loop(wait=nil, &block)
14
14
  f = Fiber.current
15
- l = proc do
15
+ l = proc do
16
16
  block.call ? EM.next_tick(&l) : f.resume
17
17
  end
18
18
  EM.next_tick(&l)
19
19
  return Fiber.yield
20
20
  end
21
-
21
+
22
22
  # Override the default, blocking behavior of Net::SSH
23
23
  def process(wait=nil, &block)
24
24
  return true
@@ -44,7 +44,6 @@ module EventMachine
44
44
  channels.each { |id, channel| channel.process unless channel.closing? }
45
45
  end
46
46
  end # register_callbacks
47
-
48
47
  end # class::Session
49
48
  end # class::Ssh
50
49
  end # module::EventMachine
data/lib/em-ssh/shell.rb CHANGED
@@ -22,10 +22,10 @@ module EventMachine
22
22
  # admin_shell = shell.split
23
23
  # admin_shell.on(:closed) { warn("admin shell has closed") }
24
24
  # admin_shell.send_and_wait('sudo su -', ']$')
25
- class Shell
25
+ class Shell
26
26
  include Log
27
27
  include Callbacks
28
-
28
+
29
29
  # Global timeout for wait operations; can be overriden by :timeout option to new
30
30
  TIMEOUT = 15
31
31
  # @return [Net::SSH::Connection::Channel] The shell to which we can send_data
@@ -36,7 +36,7 @@ module EventMachine
36
36
  attr_reader :options
37
37
  # @return [Hash] the options to pass to connect automatically. They will be extracted from the opptions[:net_ssh] on initialization
38
38
  attr_reader :connect_opts
39
-
39
+
40
40
  # @return [String] the host to login to
41
41
  attr_reader :host
42
42
  # @return [String] The user to authenticate as
@@ -53,7 +53,7 @@ module EventMachine
53
53
  end
54
54
  # [String]
55
55
  attr_writer :line_terminator
56
-
56
+
57
57
  # Connect to an ssh server then start a user shell.
58
58
  # @param [String] address
59
59
  # @param [String] user
@@ -73,27 +73,31 @@ module EventMachine
73
73
  @parent = opts[:parent]
74
74
  @children = []
75
75
  @reconnect = opts[:reconnect]
76
-
77
- block_given? ? Fiber.new { open(&blk) }.resume : open
76
+
77
+ if block_given?
78
+ Fiber.new { open(&blk).tap{|r| raise r if r.is_a?(Exception) } }.resume
79
+ else
80
+ open.tap{|r| raise r if r.is_a?(Exception) }
81
+ end # block_given?
78
82
  end
79
-
83
+
80
84
  # @return [Boolean] true if the connection should be automatically re-established; default: false
81
85
  def reconnect?
82
86
  @reconnect == true
83
- end # auto_connect?
84
-
87
+ end
88
+
85
89
  # Close the connection to the server and all child shells.
86
90
  # Disconnected shells cannot be split.
87
91
  def disconnect
88
92
  close
89
93
  connection.close
90
94
  end
91
-
95
+
92
96
  # @return [Boolean] true if the connection is still alive
93
97
  def connected?
94
98
  connection && !connection.closed?
95
99
  end
96
-
100
+
97
101
  # Close this shell and all children.
98
102
  # Even when a shell is closed it is still connected to the server.
99
103
  # Fires :closed event.
@@ -104,170 +108,178 @@ module EventMachine
104
108
  children.each { |c| c.close }
105
109
  fire(:closed)
106
110
  end
107
-
111
+
108
112
  # @return [Boolean] Has this shell been closed.
109
113
  def closed?
110
114
  @closed == true
111
115
  end
112
-
116
+
113
117
  # Send a string to the server and wait for a response containing a specified String or Regex.
114
118
  # @param [String] send_str
115
119
  # @return [String] all data in the buffer including the wait_str if it was found
116
120
  def send_and_wait(send_str, wait_str = nil, opts = {})
117
- reconnect? ? connect : raise(Disconnected) if !connected?
121
+ reconnect? ? open : raise(Disconnected) if !connected?
118
122
  raise ClosedChannel if closed?
119
123
  debug("send_and_wait(#{send_str.inspect}, #{wait_str.inspect}, #{opts})")
120
124
  send_data(send_str)
121
125
  return wait_for(wait_str, opts)
122
- end # send_and_wait(send_str, wait_str = nil, opts = {})
123
-
126
+ end
127
+
124
128
  # Wait for the shell to send data containing the given string.
125
129
  # @param [String, Regexp] strregex a string or regex to match the console output against.
126
130
  # @param [Hash] opts
127
131
  # @option opts [Fixnum] :timeout (Session::TIMEOUT) the maximum number of seconds to wait
128
- # @return [String] the contents of the buffer
132
+ # @return [String] the contents of the buffer or a TimeoutError
133
+ # @raise Disconnected
134
+ # @raise ClosedChannel
135
+ # @raise TimeoutError
129
136
  def wait_for(strregex, opts = { })
130
- reconnect? ? connect : raise(Disconnected) unless connected?
137
+ reconnect? ? open : raise(Disconnected) unless connected?
131
138
  raise ClosedChannel if closed?
132
139
  debug("wait_for(#{strregex.inspect}, #{opts})")
133
140
  opts = { :timeout => @timeout }.merge(opts)
134
141
  buffer = ''
135
142
  found = nil
136
143
  f = Fiber.current
137
-
138
- timer = nil
144
+ trace = caller
145
+ timer = nil
146
+
139
147
  timeout = proc do
140
- debug("timeout #{timer} fired")
141
148
  shell.on_data {|c,d| }
142
- begin
143
- raise TimeoutError.new("#{host}: timeout while waiting for #{strregex.inspect}; received: #{buffer.inspect}")
144
- rescue Exception => e
145
- error(e)
146
- debug(e.backtrace)
147
- fire(:error, e)
148
- end # begin
149
- f.resume(nil)
150
- shell.on_data {|c,d| }
151
- end # timeout
152
-
149
+ f.resume(TimeoutError.new("#{host}: timeout while waiting for #{strregex.inspect}; received: #{buffer.inspect}"))
150
+ end
153
151
  shell.on_data do |ch,data|
154
152
  buffer = "#{buffer}#{data}"
155
153
  debug("data: #{buffer.dump}")
156
154
  if strregex.is_a?(Regexp) ? buffer.match(strregex) : buffer.include?(strregex)
157
155
  debug("data matched")
158
- debug("canceling timer #{timer}")
159
156
  timer.respond_to?(:cancel) && timer.cancel
160
157
  shell.on_data {|c,d| }
161
158
  f.resume(buffer)
162
159
  end
163
- end # |ch,data|
164
-
160
+ end
161
+
165
162
  timer = EM::Timer.new(opts[:timeout], &timeout)
166
163
  debug("set timer: #{timer} for #{opts[:timeout]}")
167
- return Fiber.yield
168
- end # wait_for(strregex, opts = { })
169
-
164
+ res = Fiber.yield
165
+ raise(res, res.message, Array(res.backtrace) + trace) if res.is_a?(Exception)
166
+ yield(res) if block_given?
167
+ res
168
+ end
169
+
170
+
170
171
  # Open a shell on the server.
171
172
  # You generally don't need to call this.
172
- # @return [self]
173
+ # @return [self, Exception]
173
174
  def open(&blk)
174
175
  debug("open(#{blk})")
175
176
  f = Fiber.current
177
+ trace = caller
176
178
 
177
179
  conerr = nil
178
180
  unless connected?
179
181
  conerr = on(:error) do |e|
180
182
  error("#{e} (#{e.class})")
183
+ e.set_backtrace(trace + Array(e.backtrace))
181
184
  debug(e.backtrace)
182
185
  conerr = e
183
186
  f.resume(e)
184
187
  end # |e|
185
188
  connect
186
189
  end # connected?
187
-
190
+
188
191
  connection || raise(ConnectionError, "failed to create shell for #{host}: #{conerr} (#{conerr.class})")
189
-
192
+
190
193
  connection.open_channel do |channel|
191
194
  debug "**** channel open: #{channel}"
192
195
  channel.request_pty(options[:pty] || {}) do |pty,suc|
193
196
  debug "***** pty open: #{pty}; suc: #{suc}"
194
197
  pty.send_channel_request("shell") do |shell,success|
195
- raise ConnectionError, "Failed to create shell." unless success
198
+ unless success
199
+ f.resume(ConnectionError.new("Failed to create shell").tap{|e| e.set_backtrace(caller) })
200
+ end
196
201
  conerr && conerr.cancel
197
202
  debug "***** shell open: #{shell}"
198
- @shell = shell
203
+ @closed = false
204
+ @shell = shell
199
205
  Fiber.new { yield(self) if block_given? }.resume
200
206
  f.resume(self)
201
207
  end # |shell,success|
202
208
  end # |pty,suc|
203
209
  end # |channel|
204
-
210
+
205
211
  return Fiber.yield
206
- end # start
207
-
212
+ end
213
+
208
214
  # Create a new shell using the same ssh connection.
209
- # A connection will be established if this shell is not connected.
215
+ # A connection will be established if this shell is not connected.
210
216
  # If a block is provided the child will be closed after yielding.
211
217
  # @yield [Shell] child
212
218
  # @return [Shell] child
213
219
  def split
214
220
  connect unless connected?
215
221
  child = self.class.new(host, user, pass, {:connection => connection, :parent => self}.merge(options))
216
- child.line_terminator = line_terminator
222
+ child.line_terminator = line_terminator
217
223
  children.push(child)
218
224
  child.on(:closed) do
219
- children.delete(child)
225
+ children.delete(child)
220
226
  fire(:childless).tap{ info("fired :childless") } if children.empty?
221
227
  end
222
228
  fire(:split, child)
223
229
  block_given? ? yield(child).tap { child.close } : child
224
- end # split
225
-
230
+ end
231
+
226
232
  # Connect to the server.
227
233
  # Does not open the shell; use #open or #split
228
234
  # You generally won't need to call this on your own.
229
235
  def connect
230
236
  return if connected?
237
+ trace = caller
231
238
  f = Fiber.current
232
- con = ::EM::Ssh.start(host, user, connect_opts) do |connection|
233
- @connection = connection
234
- f.resume
235
- end # |connection|
236
- con.on(:error) { |e| fire(:error, e) }
239
+ con = ::EM::Ssh.start(host, user, connect_opts) { |connection| f.resume(@connection = connection) }
240
+ con.on(:error) do |e|
241
+ e.set_backtrace(trace + Array(e.backtrace))
242
+ fire(:error, e)
243
+ f.resume(e)
244
+ end
237
245
  return Fiber.yield
238
- end # connect
239
-
240
-
246
+ end
247
+
248
+
241
249
  # Send data to the ssh server shell.
242
250
  # You generally don't need to call this.
243
251
  # @see #send_and_wait
244
252
  # @param [String] d the data to send encoded as a string
245
253
  def send_data(d)
246
- #debug("send_data: #{d.dump}#{line_terminator.dump}")
247
254
  shell.send_data("#{d}#{line_terminator}")
248
255
  end
249
-
250
-
256
+
257
+
251
258
  def debug(msg = nil, &blk)
252
259
  super("#{host} #{msg}", &blk)
253
260
  end
254
-
261
+
255
262
  def info(msg = nil, &blk)
256
263
  super("#{host} #{msg}", &blk)
257
264
  end
258
-
265
+
259
266
  def fatal(msg = nil, &blk)
260
267
  super("#{host} #{msg}", &blk)
261
268
  end
262
-
269
+
263
270
  def warn(msg = nil, &blk)
264
271
  super("#{host} #{msg}", &blk)
265
272
  end
266
-
273
+
267
274
  def error(msg = nil, &blk)
268
275
  super("#{host} #{msg}", &blk)
269
276
  end
270
-
277
+
278
+
279
+ private
280
+ # TODO move private stuff down to private
281
+ # e.g., #open, #connect,
282
+
271
283
  end # class::Shell
272
284
  end # class::Ssh
273
285
  end # module::EventMachine
@@ -1,5 +1,5 @@
1
1
  module EventMachine
2
2
  class Ssh
3
- VERSION='0.1.0'
3
+ VERSION='0.2.1'
4
4
  end # class::Ssh
5
- end # module::EventMachine
5
+ end # module::EventMachine
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: em-ssh
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-10-30 00:00:00.000000000Z
12
+ date: 2012-02-02 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: eventmachine
16
- requirement: &70168320140920 !ruby/object:Gem::Requirement
16
+ requirement: &70209705582020 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70168320140920
24
+ version_requirements: *70209705582020
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: net-ssh
27
- requirement: &70168320140460 !ruby/object:Gem::Requirement
27
+ requirement: &70209705581560 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70168320140460
35
+ version_requirements: *70209705581560
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: ruby-termios
38
- requirement: &70168320140040 !ruby/object:Gem::Requirement
38
+ requirement: &70209705581140 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *70168320140040
46
+ version_requirements: *70209705581140
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: highline
49
- requirement: &70168320139620 !ruby/object:Gem::Requirement
49
+ requirement: &70209705580720 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,7 +54,7 @@ dependencies:
54
54
  version: '0'
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *70168320139620
57
+ version_requirements: *70209705580720
58
58
  description: ''
59
59
  email:
60
60
  - em-ssh@simulacre.org