em-ssh 0.1.0 → 0.2.1

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