em-ssh 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,102 @@
1
+ #EM-SSH
2
+ Em-ssh is a net-ssh adapter for EventMachine. For the most part you can take any net-ssh code you have and run it in the EventMachine reactor.
3
+
4
+ Em-ssh is not associated with the Jamis Buck's [net-ssh](http://net-ssh.github.com/) library. Please report any bugs with em-ssh to [https://github.com/simulacre/em-ssh/issues](https://github.com/simulacre/em-ssh/issues)
5
+ ##Installation
6
+ gem install em-ssh
7
+
8
+ ##Synopsis
9
+ EM.run do
10
+ EM::Ssh.start(host, user, :password => password) do |ssh|
11
+ # capture all stderr and stdout output from a remote process
12
+ ssh.exec!('uname -a').tap {|r| puts "\nuname: #{r}"}
13
+
14
+ # capture only stdout matching a particular pattern
15
+ stdout = ""
16
+ ssh.exec!("ls -l /home") do |channel, stream, data|
17
+ stdout << data if stream == :stdout
18
+ end
19
+ puts "\n#{stdout}"
20
+
21
+ # run multiple processes in parallel to completion
22
+ ssh.exec('ping -c 1 www.google.com')
23
+ ssh.exec('ping -c 1 www.yahoo.com')
24
+ ssh.exec('ping -c 1 www.rakuten.co.jp')
25
+
26
+ #open a new channel and configure a minimal set of callbacks, then wait for the channel to finishes (closees).
27
+ channel = ssh.open_channel do |ch|
28
+ ch.exec "/usr/local/bin/ruby /path/to/file.rb" do |ch, success|
29
+ raise "could not execute command" unless success
30
+
31
+ # "on_data" is called when the process writes something to stdout
32
+ ch.on_data do |c, data|
33
+ $stdout.print data
34
+ end
35
+
36
+ # "on_extended_data" is called when the process writes something to stderr
37
+ ch.on_extended_data do |c, type, data|
38
+ $stderr.print data
39
+ end
40
+
41
+ ch.on_close { puts "done!" }
42
+ end
43
+ end
44
+
45
+ channel.wait
46
+
47
+ ssh.close
48
+ EM.stop
49
+ end
50
+ end
51
+
52
+ See [http://net-ssh.github.com/ssh/v2/api/index.html](http://net-ssh.github.com/ssh/v2/api/index.html)
53
+
54
+ ##Shell
55
+
56
+ Em-ssh provides an exepct-like shell abstraction layer on top of net-ssh in EM::Ssh::Shell
57
+
58
+ ### Example
59
+ EM.run {
60
+ EM::Ssh::Shell.new(host, 'caleb', 'password') do |shell|
61
+ shell.wait_for(']$')
62
+ shell.send_and_wait('sudo su -', 'password for caleb: ')
63
+ shell.send_and_wait('password', ']$')
64
+ output = shell.send_and_wait('/etc/init.d/openvpn restart', ']$')
65
+ # ...
66
+ shell.send_and_wait('exit', ']$')
67
+ shell.send_data('exit')
68
+ end
69
+ }
70
+
71
+
72
+
73
+ ## Other Examples
74
+ See bin/em-ssh for an example of a basic replacement for system ssh.
75
+
76
+ See bin/em-ssh-shell for a more complex example usage of Shell.
77
+
78
+
79
+ ##Copyright
80
+ Copyright (c) 2011 Caleb Crane
81
+
82
+ Permission is hereby granted, free of charge, to any person obtaining
83
+ a copy of this software and associated documentation files (the
84
+ "Software"), to deal in the Software without restriction, including
85
+ without limitation the rights to use, copy, modify, merge, publish,
86
+ distribute, sublicense, and/or sell copies of the Software, and to
87
+ permit persons to whom the Software is furnished to do so, subject to
88
+ the following conditions:
89
+
90
+ The above copyright notice and this permission notice shall be
91
+ included in all copies or substantial portions of the Software.
92
+
93
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
94
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
95
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
96
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
97
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
98
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
99
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
100
+
101
+
102
+ Portions of this software are Copyright (c) 2008 Jamis Buck
data/bin/em-ssh ADDED
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env ruby
2
+ # This is really nothing more than a utility to test the em-ssh adapter.
3
+ # It's not meant to be used for anything else.
4
+ # It probably requires ruby 1.9.2-p180 as p190 tends to segfault when using Fibers.
5
+ require 'bundler/setup'
6
+ require 'termios'
7
+ require 'highline'
8
+ require 'em-ssh'
9
+
10
+ include EM::Ssh::Log
11
+
12
+
13
+ def bufferio( enable, io = $stdin )
14
+ raise "Termios library not found" unless defined?(::Termios)
15
+ attr = Termios::getattr( io )
16
+ enable ? (attr.c_lflag |= Termios::ICANON | Termios::ECHO) : (attr.c_lflag &= ~(Termios::ICANON|Termios::ECHO))
17
+ Termios::setattr( io, Termios::TCSANOW, attr )
18
+ end # def bufferio( enable, io = $stdin )
19
+
20
+ def abort(msg)
21
+ puts msg
22
+ Process.exit
23
+ end # abort(msg)
24
+
25
+ options = {}
26
+ opts = OptionParser.new
27
+ opts.banner += " [user:[password]@]host[:port]"
28
+ options[:port] = 22
29
+ opts.on('-u', '--user String', String) { |u| options[:user] = u }
30
+ opts.on('-p', '--password [String]', String) do |p|
31
+ options[:password] = p.nil? ? HighLine.new.ask("password: "){|q| q.echo = "*" } : p
32
+ end
33
+ opts.on('-v', '--verbose') do
34
+ EM::Ssh.logger.level = EM::Ssh.logger.level - 1 unless EM::Ssh.logger.level == 0
35
+ options[:verbose] = EM::Ssh.logger.level
36
+ end
37
+ opts.parse!
38
+
39
+ host = ARGV.shift
40
+ if host.nil?
41
+ host,options[:password] = options[:password], HighLine.new.ask("#{options[:password]}'s password: "){|q| q.echo = "*" }
42
+ end # host.nil?
43
+ abort("a host is required") if host.nil?
44
+
45
+ options[:user], host = *host.split('@') if host.include?('@')
46
+ options[:user], options[:password] = *options[:user].split(':') if options[:user].include?(':')
47
+ host, options[:port] = *host.split(':') if host.include?(':')
48
+ connected = false
49
+
50
+
51
+ module CInput
52
+ def shell=(shell)
53
+ @shell = shell
54
+ end # shell=(shell)
55
+ def initialize
56
+ bufferio(false, $stdin)
57
+ end # initialize
58
+ def unbind
59
+ bufferio(true, $stdin)
60
+ end # unbind
61
+ def notify_readable
62
+ @shell.send_data($stdin.read(1))
63
+ end
64
+ end
65
+
66
+
67
+
68
+ EM.run do
69
+ EM::Ssh.start(host, options[:user], options) do |connection|
70
+ debug "**** connected: #{connection}"
71
+ connection.open_channel do |channel|
72
+ debug "**** channel: #{channel}"
73
+ channel.request_pty(options[:pty] || {}) do |pty,suc|
74
+ debug "***** pty: #{pty}; suc: #{suc}"
75
+ pty.send_channel_request("shell") do |shell,success|
76
+ raise ConnectionError, "Failed to create shell." unless success
77
+ debug "***** shell: #{shell}"
78
+ connected = true
79
+
80
+ shell.on_data { |c,d| $stdout.print d }
81
+ shell.on_extended_data { |c,data| $STDERR.print data }
82
+ shell.on_eof do
83
+ shell.close
84
+ EM.stop
85
+ end #
86
+
87
+ trap("SIGINT") { shell.send_data("\C-c") }
88
+ trap("SIGEXIT") do
89
+ shell.close
90
+ trap("SIGINT", "SIG_DFL")
91
+ end #
92
+
93
+ conn = EM.watch($stdin, CInput)
94
+ conn.shell = shell
95
+ conn.notify_readable = true
96
+
97
+ end # |shell,success|
98
+ end # |pty,suc|
99
+ end # |channel|
100
+ end # |connection|
101
+ end # EM.start
102
+
data/bin/em-ssh-shell ADDED
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env ruby
2
+ # This is really nothing more than a utility to test the em-ssh adapter.
3
+ # It's not meant to be used for anything else.
4
+ # It probably requires ruby 1.9.2-p180 as p190 tends to segfault when using Fibers.
5
+ require 'bundler/setup'
6
+ require 'termios'
7
+ require 'highline'
8
+ require 'em-ssh'
9
+ require 'em-ssh/shell'
10
+
11
+ include EM::Ssh::Log
12
+
13
+ def abort(msg)
14
+ puts msg
15
+ Process.exit
16
+ end # abort(msg)
17
+
18
+ options = {}
19
+ opts = OptionParser.new
20
+ opts.banner += " [user:[password]@]host[:port] wait_string command [command command ...]"
21
+ options[:port] = 22
22
+ opts.on('-u', '--user String', String) { |u| options[:user] = u }
23
+ opts.on('-p', '--password [String]', String) do |p|
24
+ options[:password] = p.nil? ? HighLine.new.ask("password: "){|q| q.echo = "*" } : p
25
+ end
26
+ opts.on('-v', '--verbose') do
27
+ EM::Ssh.logger.level = EM::Ssh.logger.level - 1 unless EM::Ssh.logger.level == 0
28
+ options[:verbose] = EM::Ssh.logger.level
29
+ end
30
+ opts.parse!
31
+
32
+ host = ARGV.shift
33
+ if host.nil?
34
+ host,options[:password] = options[:password], HighLine.new.ask("#{options[:password]}'s password: "){|q| q.echo = "*" }
35
+ end # host.nil?
36
+ abort("a host is required") if host.nil?
37
+
38
+ options[:user], host = *host.split('@') if host.include?('@')
39
+ options[:user], options[:password] = *options[:user].split(':') if options[:user].include?(':')
40
+ host, options[:port] = *host.split(':') if host.include?(':')
41
+
42
+
43
+ waitstr = ARGV.shift
44
+ commands = ARGV
45
+ abort("wait_string is required") if waitstr.nil?
46
+ abort("command is required") if commands.empty?
47
+
48
+
49
+ EM.run do
50
+ Fiber.new {
51
+ shell = EM::Ssh::Shell.new(host, options[:user], options[:password])
52
+ commands.each do |command|
53
+ mys = shell.split
54
+ mys.on(:closed) { info("#{mys} has closed") }
55
+
56
+ EM.next_tick do
57
+ Fiber.new {
58
+ debug("#{mys} waited for: '#{mys.wait_for(waitstr)}'")
59
+ mys.line_terminator = "\n"
60
+ debug("#{mys} send: #{command.inspect}")
61
+ puts "#{mys} result: '#{mys.send_and_wait(command, waitstr)}'"
62
+ mys.close
63
+ }.resume
64
+ end #
65
+ end # |command|
66
+
67
+ shell.on(:childless) do
68
+ info("#{shell}'s children all closed")
69
+ shell.close
70
+ EM.stop
71
+ end # :childless
72
+ }.resume
73
+ end # EM.run
@@ -0,0 +1,38 @@
1
+ module EventMachine
2
+ class Ssh
3
+ class AuthenticationSession < Net::SSH::Authentication::Session
4
+ include Log
5
+
6
+ def authenticate(*args)
7
+ debug { "authenticate(#{args.join(", ")})" }
8
+ super(*args)
9
+ end # authenticate(*args)
10
+
11
+ # Returns once an acceptable auth packet is received.
12
+ def next_message
13
+ packet = transport.next_message
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
24
+
25
+ when USERAUTH_METHOD_RANGE, SERVICE_ACCEPT
26
+ return packet
27
+
28
+ when USERAUTH_SUCCESS
29
+ transport.hint :authenticated
30
+ return packet
31
+
32
+ else
33
+ raise SshError, "unexpected message #{packet.type} (#{packet})"
34
+ end
35
+ end # next_message
36
+ end # class::AuthenticationSession
37
+ end # module::Ssh
38
+ end # module::EventMachine
@@ -0,0 +1,99 @@
1
+ module EventMachine
2
+ class Ssh
3
+ # A simple mixin enabling your objects to allow other objects to register callbacks and fire events.
4
+ # @example
5
+ # class Connection
6
+ # include Callbacks
7
+ # # ...
8
+ # end
9
+ #
10
+ # connection = Connection.new(...)
11
+ # cb = connection.on(:data) do |data|
12
+ # @version << data
13
+ # if @version[-1] == "\n"
14
+ # @version.chomp!
15
+ # raise SshError.new("incompatible SSH version `#{@version}'") unless @version.match(/^SSH-(1\.99|2\.0)-/)
16
+ # connection.send_data("#{PROTO_VERSION}\r\n")
17
+ # cb.cancel
18
+ # connection.fire(:version_negotiated)
19
+ # end # @header[-1] == "\n"
20
+ # end # |data|
21
+ module Callbacks
22
+
23
+ # @return [Hash] The registered callbacks
24
+ def callbacks
25
+ @clbks ||= {}
26
+ end # callbacks
27
+
28
+
29
+ # Signal that an event has occured.
30
+ # Each callback will receive whatever args are passed to fire, or the object that the event was fired upon.
31
+ # @param [Symbol] event
32
+ # @param [Objects] arguments to pass to all callbacks registered for the given event
33
+ # @param [Array] the results of each callback that was executed
34
+ def fire(event, *args)
35
+ #log.debug("#{self}.fire(#{event.inspect}, #{args})")
36
+ args = self if args.empty?
37
+ (callbacks[event] ||= []).clone.map { |cb| cb.call(*args) }
38
+ end # fire(event)
39
+
40
+ # Register a callback to be fired when a matching event occurs.
41
+ # The callback will be fired when the event occurs until it returns true.
42
+ # @param [Symbol] event
43
+ def on(event, &blk)
44
+ #log.debug("#{self}.on(#{event.inspect}, #{blk})")
45
+ if block_given?
46
+ raise "event (#{event.inspect}) must be a symbol when a block is given" unless event.is_a?(Symbol)
47
+ return Callback.new(self, event, &blk).tap{|cb| (callbacks[event] ||= []).push(cb) }
48
+ end # block_given?
49
+
50
+ raise "event (#{event.inspect}) must be a Callback when a block is not given" unless event.is_a?(Callback)
51
+ (callbacks[event] ||= []).push(event)
52
+ return event
53
+ end # on(event, &blk)
54
+
55
+ # Registers a callback that will be canceled after the first time it is called.
56
+ def on_next(event, &blk)
57
+ cb = on(event) do |*args|
58
+ cb.cancel
59
+ blk.call(*args)
60
+ end # |*args|
61
+ end # on_next(event, &blk)
62
+
63
+
64
+ class Callback
65
+ # The object that keeps this callback
66
+ attr_reader :obj
67
+ # [Sybmol] the name of the event
68
+ attr_reader :event
69
+ # The block to call when the event is fired
70
+ attr_reader :block
71
+
72
+ def initialize(obj, event, &blk)
73
+ raise ArgumentError.new("a block is required") unless block_given?
74
+ @obj = obj
75
+ @event = event
76
+ @block = blk
77
+ end # initialize(obj, event, &blk)
78
+
79
+ # Call the callback with optional arguments
80
+ def call(*args)
81
+ block.call(*args)
82
+ end # call(*args)
83
+
84
+ # Registers the callback with the object.
85
+ # This is useful if you cancel the callback at one point and want to re-enable it later on.
86
+ def register
87
+ @obj.on(self)
88
+ end # register
89
+
90
+ def cancel
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
+ @obj.callbacks[@event].delete(self)
93
+ self
94
+ end # cancel
95
+ end # class::Callback
96
+
97
+ end # module::Callbacks
98
+ end # class::Ssh
99
+ end # module::EventMachine
@@ -0,0 +1,277 @@
1
+ require 'em-ssh/callbacks'
2
+ module EventMachine
3
+ class Ssh
4
+ # EventMachine::Ssh::Connection is a EventMachine::Connection that emulates the Net::SSH transport layer. It ties
5
+ # itself into Net::SSH so that the EventMachine reactor loop can take the place of the Net::SSH event loop.
6
+ # Most of the methods here are only for compatibility with Net::SSH
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
+ ##
14
+ # Transport related
15
+
16
+ # @return [String] The host to connect to, as given to the constructor.
17
+ attr_reader :host
18
+
19
+ # @return [Fixnum] the port number (DEFAULT_PORT) to connect to, as given in the options to the constructor.
20
+ attr_reader :port
21
+
22
+ # @return [ServerVersion] The ServerVersion instance that encapsulates the negotiated protocol version.
23
+ attr_reader :server_version
24
+
25
+ # The Algorithms instance used to perform key exchanges.
26
+ attr_reader :algorithms
27
+
28
+ # The host-key verifier object used to verify host keys, to ensure that the connection is not being spoofed.
29
+ attr_reader :host_key_verifier
30
+
31
+ # The hash of options that were given to the object at initialization.
32
+ attr_reader :options
33
+
34
+ # @return [PacketStream] emulates a socket and ssh packetstream
35
+ attr_reader :socket
36
+
37
+ # @return [Boolean] true if the connection has been closed
38
+ def closed?
39
+ @closed == true
40
+ end
41
+
42
+ # Close the connection
43
+ def close
44
+ # #unbind will update @closed
45
+ close_connection
46
+ end
47
+
48
+ # Send a packet to the server
49
+ def send_message(message)
50
+ @socket.send_packet(message)
51
+ end
52
+ alias :enqueue_message :send_message
53
+
54
+ def next_message
55
+ return @queue.shift if @queue.any? && algorithms.allow?(@queue.first)
56
+ f = Fiber.current
57
+ cb = on(:packet) do |packet|
58
+ if @queue.any? && algorithms.allow?(@queue.first)
59
+ cb.cancel
60
+ f.resume(@queue.shift)
61
+ end
62
+ end # :packet
63
+ return Fiber.yield
64
+ end # next_message
65
+
66
+ # Returns a new service_request packet for the given service name, ready
67
+ # for sending to the server.
68
+ def service_request(service)
69
+ Net::SSH::Buffer.from(:byte, SERVICE_REQUEST, :string, service)
70
+ end
71
+
72
+ # Requests a rekey operation, and simulates a block until the operation completes.
73
+ # If a rekey is already pending, this returns immediately, having no effect.
74
+ def rekey!
75
+ if !algorithms.pending?
76
+ f = Fiber.current
77
+ on_next(:algo_init) do
78
+ f.resume
79
+ end # :algo_init
80
+ algorithms.rekey!
81
+ return Fiber.yield
82
+ end
83
+ end
84
+
85
+ # Returns immediately if a rekey is already in process. Otherwise, if a
86
+ # rekey is needed (as indicated by the socket, see PacketStream#if_needs_rekey?)
87
+ # one is performed, causing this method to block until it completes.
88
+ def rekey_as_needed
89
+ return if algorithms.pending?
90
+ socket.if_needs_rekey? { rekey! }
91
+ end
92
+
93
+
94
+
95
+ ##
96
+ # EventMachine callbacks
97
+ ###
98
+ def post_init
99
+ @socket = PacketStream.new(self)
100
+ @data = @socket.input
101
+ end # post_init
102
+
103
+ # @return
104
+ def unbind
105
+ debug("#{self} is unbound")
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
+
116
+
117
+ def initialize(options = {})
118
+ debug("#{self.class}.new(#{options})")
119
+ @host = options[:host]
120
+ @port = options[:port]
121
+ @password = options[:password]
122
+ @queue = []
123
+ @options = options
124
+
125
+ begin
126
+ on(:connected, &options[:callback]) if options[:callback]
127
+ @error_callback = lambda { |code| raise SshError.new(code) }
128
+
129
+ @host_key_verifier = select_host_key_verifier(options[:paranoid])
130
+ @server_version = ServerVersion.new(self)
131
+ on(:version_negotiated) do
132
+ @data.consume!(@server_version.header.length)
133
+ @algorithms = Net::SSH::Transport::Algorithms.new(self, options)
134
+
135
+ register_data_handler
136
+
137
+ on_next(:algo_init) do
138
+ auth = AuthenticationSession.new(self, options)
139
+ user = options.fetch(:user, user)
140
+ Fiber.new do
141
+ if auth.authenticate("ssh-connection", user, options[:password])
142
+ fire(:connected, Session.new(self, options))
143
+ else
144
+ close_connection
145
+ raise Net::SSH::AuthenticationFailed, user
146
+ end # auth.authenticate("ssh-connection", user, options[:password])
147
+ end.resume # Fiber
148
+ end # :algo_init
149
+ end # :version_negotiated
150
+
151
+ rescue Exception => e
152
+ log.fatal("caught an error during initialization: #{e}\n #{e.backtrace.join("\n ")}")
153
+ Process.exit
154
+ end # begin
155
+ self
156
+ end # initialize(options = {})
157
+
158
+
159
+ ##
160
+ # Helpers required for compatibility with Net::SSH
161
+ ##
162
+
163
+ # Returns the host (and possibly IP address) in a format compatible with
164
+ # SSH known-host files.
165
+ def host_as_string
166
+ @host_as_string ||= "#{host}".tap do |string|
167
+ string = "[#{string}]:#{port}" if port != DEFAULT_PORT
168
+ _, ip = Socket.unpack_sockaddr_in(get_peername)
169
+ if ip != host
170
+ string << "," << (port != DEFAULT_PORT ? "[#{ip}]:#{port}" : ip)
171
+ end # ip != host
172
+ end # |string|
173
+ end # host_as_string
174
+
175
+ alias :logger :log
176
+
177
+
178
+ # Configure's the packet stream's client state with the given set of
179
+ # options. This is typically used to define the cipher, compression, and
180
+ # hmac algorithms to use when sending packets to the server.
181
+ def configure_client(options={})
182
+ @socket.client.set(options)
183
+ end
184
+
185
+ # Configure's the packet stream's server state with the given set of
186
+ # options. This is typically used to define the cipher, compression, and
187
+ # hmac algorithms to use when reading packets from the server.
188
+ def configure_server(options={})
189
+ @socket.server.set(options)
190
+ end
191
+
192
+ # Sets a new hint for the packet stream, which the packet stream may use
193
+ # to change its behavior. (See PacketStream#hints).
194
+ def hint(which, value=true)
195
+ @socket.hints[which] = value
196
+ end
197
+
198
+ # Returns a new service_request packet for the given service name, ready
199
+ # for sending to the server.
200
+ def service_request(service)
201
+ Net::SSH::Buffer.from(:byte, SERVICE_REQUEST, :string, service)
202
+ end
203
+
204
+ # Returns a hash of information about the peer (remote) side of the socket,
205
+ # including :ip, :port, :host, and :canonized (see #host_as_string).
206
+ def peer
207
+ @peer ||= {}.tap do |p|
208
+ _, ip = Socket.unpack_sockaddr_in(get_peername)
209
+ p[:ip] = ip
210
+ p[:port] = @port.to_i
211
+ p[:host] = @host
212
+ p[:canonized] = host_as_string
213
+ end
214
+ end
215
+
216
+
217
+
218
+
219
+ private
220
+
221
+ # Register the primary :data callback
222
+ # @return [Callback] the callback that was registered
223
+ def register_data_handler
224
+ on(:data) do |data|
225
+ # @todo instead of while use a EM.next_tick
226
+ while (packet = @socket.poll_next_packet)
227
+ case packet.type
228
+ when DISCONNECT
229
+ raise Net::SSH::Disconnect, "disconnected: #{packet[:description]} (#{packet[:reason_code]})"
230
+ when IGNORE
231
+ debug("IGNORE packet received: #{packet[:data].inspect}")
232
+ when UNIMPLEMENTED
233
+ log.warn("UNIMPLEMENTED: #{packet[:number]}")
234
+ when DEBUG
235
+ log.send((packet[:always_display] ? :fatal : :debug), packet[:message])
236
+ when KEXINIT
237
+ Fiber.new do
238
+ algorithms.accept_kexinit(packet)
239
+ fire(:algo_init) if algorithms.initialized?
240
+ end.resume
241
+ else
242
+ @queue.push(packet)
243
+ if algorithms.allow?(packet)
244
+ fire(:packet, packet)
245
+ fire(:session_packet, packet) if packet.type >= GLOBAL_REQUEST
246
+ end # algorithms.allow?(packet)
247
+ socket.consume!
248
+ end # packet.type
249
+ end # (packet = @socket.poll_next_packet)
250
+ end # |data|
251
+ end # register_data_handler
252
+
253
+ # Instantiates a new host-key verification class, based on the value of
254
+ # the parameter. When true or nil, the default Lenient verifier is
255
+ # returned. If it is false, the Null verifier is returned, and if it is
256
+ # :very, the Strict verifier is returned. If the argument happens to
257
+ # respond to :verify, it is returned directly. Otherwise, an exception
258
+ # is raised.
259
+ # Taken from Net::SSH::Session
260
+ def select_host_key_verifier(paranoid)
261
+ case paranoid
262
+ when true, nil then
263
+ Net::SSH::Verifiers::Lenient.new
264
+ when false then
265
+ Net::SSH::Verifiers::Null.new
266
+ when :very then
267
+ Net::SSH::Verifiers::Strict.new
268
+ else
269
+ paranoid.respond_to?(:verify) ? paranoid : (raise ArgumentError.new("argument to :paranoid is not valid: #{paranoid.inspect}"))
270
+ end # paranoid
271
+ end # select_host_key_verifier(paranoid)
272
+
273
+ end # class::Connection < EventMachine::Connection
274
+ end # module::Ssh
275
+ end # module::EventMachine
276
+
277
+
data/lib/em-ssh/log.rb ADDED
@@ -0,0 +1,30 @@
1
+ module EventMachine
2
+ class Ssh
3
+ module Log
4
+ # @return [Logger] the default logger
5
+ def log
6
+ EventMachine::Ssh.logger
7
+ end
8
+
9
+ def debug(msg = nil, &blk)
10
+ log.debug("#{self.class}".downcase.gsub("::",".") + " #{msg}", &blk)
11
+ end
12
+
13
+ def info(msg = nil, &blk)
14
+ log.info("#{self.class}".downcase.gsub("::",".") + " #{msg}", &blk)
15
+ end
16
+
17
+ def fatal(msg = nil, &blk)
18
+ log.fatal("#{self.class}".downcase.gsub("::",".") + " #{msg}", &blk)
19
+ end
20
+
21
+ def warn(msg = nil, &blk)
22
+ log.warn("#{self.class}".downcase.gsub("::",".") + " #{msg}", &blk)
23
+ end
24
+
25
+ def error(msg = nil, &blk)
26
+ log.error("#{self.class}".downcase.gsub("::",".") + " #{msg}", &blk)
27
+ end
28
+ end # module::Log
29
+ end # class::Ssh
30
+ end # module::EventMachine
@@ -0,0 +1,154 @@
1
+ module EventMachine
2
+ class Ssh
3
+ class PacketStream
4
+ include Net::SSH::BufferedIo
5
+ include Log
6
+
7
+
8
+ # The map of "hints" that can be used to modify the behavior of the packet
9
+ # stream. For instance, when authentication succeeds, an "authenticated"
10
+ # hint is set, which is used to determine whether or not to compress the
11
+ # data when using the "delayed" compression algorithm.
12
+ attr_reader :hints
13
+
14
+ # The server state object, which encapsulates the algorithms used to interpret
15
+ # packets coming from the server.
16
+ attr_reader :server
17
+
18
+ # The client state object, which encapsulates the algorithms used to build
19
+ # packets to send to the server.
20
+ attr_reader :client
21
+
22
+ # The input stream
23
+ attr_reader :input
24
+ # The output stream
25
+ attr_reader :output
26
+
27
+ def initialize(connection)
28
+ @connection = connection
29
+ @input = Net::SSH::Buffer.new
30
+ @output = Net::SSH::Buffer.new
31
+ @hints = {}
32
+ @server = Net::SSH::Transport::State.new(self, :server)
33
+ @client = Net::SSH::Transport::State.new(self, :client)
34
+ @packet = nil
35
+ end # initialize(content="")
36
+
37
+ # Consumes n bytes from the buffer, where n is the current position
38
+ # unless otherwise specified. This is useful for removing data from the
39
+ # buffer that has previously been read, when you are expecting more data
40
+ # to be appended. It helps to keep the size of buffers down when they
41
+ # would otherwise tend to grow without bound.
42
+ #
43
+ # Returns the buffer object itself.
44
+ def consume!(*args)
45
+ input.consume!(*args)
46
+ end # consume!(*args)
47
+
48
+ # Tries to read the next packet. If there is insufficient data to read
49
+ # an entire packet, this returns immediately, otherwise the packet is
50
+ # read, post-processed according to the cipher, hmac, and compression
51
+ # algorithms specified in the server state object, and returned as a
52
+ # new Packet object.
53
+ # Copyright (c) 2008 Jamis Buck
54
+ def poll_next_packet
55
+ if @packet.nil?
56
+ minimum = server.block_size < 4 ? 4 : server.block_size
57
+ return nil if available < minimum
58
+ data = read_available(minimum)
59
+ # decipher it
60
+ @packet = Net::SSH::Buffer.new(server.update_cipher(data))
61
+ @packet_length = @packet.read_long
62
+ end
63
+ need = @packet_length + 4 - server.block_size
64
+ raise SshError, "padding error, need #{need} block #{server.block_size}" if need % server.block_size != 0
65
+
66
+ return nil if available < need + server.hmac.mac_length
67
+
68
+ if need > 0
69
+ # read the remainder of the packet and decrypt it.
70
+ data = read_available(need)
71
+ @packet.append(server.update_cipher(data))
72
+ end
73
+
74
+ # get the hmac from the tail of the packet (if one exists), and
75
+ # then validate it.
76
+ real_hmac = read_available(server.hmac.mac_length) || ""
77
+
78
+ @packet.append(server.final_cipher)
79
+ padding_length = @packet.read_byte
80
+
81
+ payload = @packet.read(@packet_length - padding_length - 1)
82
+ padding = @packet.read(padding_length) if padding_length > 0
83
+
84
+ my_computed_hmac = server.hmac.digest([server.sequence_number, @packet.content].pack("NA*"))
85
+ raise Net::SSH::Exception, "corrupted mac detected" if real_hmac != my_computed_hmac
86
+
87
+ # try to decompress the payload, in case compression is active
88
+ payload = server.decompress(payload)
89
+
90
+ log.debug("received packet nr #{server.sequence_number} type #{payload.getbyte(0)} len #{@packet_length}")
91
+
92
+ server.increment(@packet_length)
93
+ @packet = nil
94
+
95
+ return Net::SSH::Packet.new(payload)
96
+ end # poll_next_packet
97
+
98
+ # Copyright (c) 2008 Jamis Buck
99
+ def send_packet(payload)
100
+ # try to compress the packet
101
+ payload = client.compress(payload)
102
+
103
+ # the length of the packet, minus the padding
104
+ actual_length = 4 + payload.length + 1
105
+
106
+ # compute the padding length
107
+ padding_length = client.block_size - (actual_length % client.block_size)
108
+ padding_length += client.block_size if padding_length < 4
109
+
110
+ # compute the packet length (sans the length field itself)
111
+ packet_length = payload.length + padding_length + 1
112
+ if packet_length < 16
113
+ padding_length += client.block_size
114
+ packet_length = payload.length + padding_length + 1
115
+ end
116
+
117
+ padding = Array.new(padding_length) { rand(256) }.pack("C*")
118
+
119
+ unencrypted_data = [packet_length, padding_length, payload, padding].pack("NCA*A*")
120
+ mac = client.hmac.digest([client.sequence_number, unencrypted_data].pack("NA*"))
121
+
122
+ encrypted_data = client.update_cipher(unencrypted_data) << client.final_cipher
123
+ message = encrypted_data + mac
124
+
125
+ log.debug("queueing packet nr #{client.sequence_number} type #{payload.getbyte(0)} len #{packet_length}")
126
+ @connection.send_data(message)
127
+ log.debug("sent #{message.length} bytes")
128
+ client.increment(packet_length)
129
+
130
+ self
131
+ end # send_packet(payload)
132
+
133
+ # Performs any pending cleanup necessary on the IO and its associated
134
+ # state objects. (See State#cleanup).
135
+ def cleanup
136
+ client.cleanup
137
+ server.cleanup
138
+ end
139
+
140
+ # If the IO object requires a rekey operation (as indicated by either its
141
+ # client or server state objects, see State#needs_rekey?), this will
142
+ # yield. Otherwise, this does nothing.
143
+ # Copyright (c) 2008 Jamis Buck
144
+ def if_needs_rekey?
145
+ if client.needs_rekey? || server.needs_rekey?
146
+ yield
147
+ client.reset! if client.needs_rekey?
148
+ server.reset! if server.needs_rekey?
149
+ end
150
+ end
151
+
152
+ end # class::PacketStream
153
+ end # class::Ssh
154
+ end # module::EventMachine
@@ -0,0 +1,37 @@
1
+ module EventMachine
2
+ class Ssh
3
+ class ServerVersion
4
+ include Log
5
+
6
+ attr_reader :header
7
+ attr_reader :version
8
+
9
+ def initialize(connection)
10
+ debug("#{self}.new(#{connection})")
11
+ negotiate!(connection)
12
+ end
13
+
14
+
15
+ private
16
+
17
+ def negotiate!(connection)
18
+ @version = ''
19
+ cb = connection.on(:data) do |data|
20
+ log.debug("#{self.class}.on(:data, #{data.inspect})")
21
+ @version << data
22
+ @header = @version.clone
23
+ if @version[-1] == "\n"
24
+ @version.chomp!
25
+ log.debug("server version: #{@version}")
26
+ raise SshError.new("incompatible SSH version `#{@version}'") unless @version.match(/^SSH-(1\.99|2\.0)-/)
27
+ log.debug("local version: #{Net::SSH::Transport::ServerVersion::PROTO_VERSION}")
28
+ connection.send_data("#{Net::SSH::Transport::ServerVersion::PROTO_VERSION}\r\n")
29
+ cb.cancel
30
+ connection.fire(:version_negotiated)
31
+ end # @header[-1] == "\n"
32
+ end # |data|
33
+ end
34
+
35
+ end # class::ServerVersion
36
+ end # module::Ssh
37
+ end # module::EventMachine
@@ -0,0 +1,51 @@
1
+
2
+ module EventMachine
3
+ class Ssh
4
+ class Session < Net::SSH::Connection::Session
5
+ include Log
6
+
7
+ def initialize(transport, options = {})
8
+ super(transport, options)
9
+ register_callbacks
10
+ end
11
+
12
+ # Override the default, blocking behavior of Net::SSH.
13
+ # Callers to loop will still wait, but not block the loop.
14
+ def loop(wait=nil, &block)
15
+ f = Fiber.current
16
+ l = proc do
17
+ block.call ? EM.next_tick(&l) : f.resume
18
+ end
19
+ EM.next_tick(&l)
20
+ return Fiber.yield
21
+ end
22
+
23
+ # Override the default, blocking behavior of Net::SSH
24
+ def process(wait=nil, &block)
25
+ return true
26
+ end
27
+
28
+ def send_message(msg)
29
+ transport.send_message(msg)
30
+ end
31
+
32
+
33
+ private
34
+
35
+
36
+ def register_callbacks
37
+ transport.on(:packet) do |packet|
38
+ raise SshError, "unexpected response #{packet.type} (#{packet.inspect})" unless MAP.key?(packet.type)
39
+ send(MAP[packet.type], packet)
40
+ end # |packet|
41
+
42
+ chann_proc = proc do
43
+ channels.each { |id, channel| channel.process unless channel.closing? }
44
+ EM.next_tick(&chann_proc)
45
+ end
46
+ EM.next_tick(&chann_proc)
47
+ end # register_callbacks
48
+
49
+ end # class::Session
50
+ end # class::Ssh
51
+ end # module::EventMachine
@@ -0,0 +1,225 @@
1
+ require 'em-ssh'
2
+
3
+ module EventMachine
4
+ class Ssh
5
+ # EM::Ssh::Shell encapsulates interaction with a user shell on an SSH server.
6
+ # @example Retrieve the output of ifconfig -a on a server
7
+ # EM.run{
8
+ # shell = EM::Ssh::Shell.new(host, user, password)
9
+ # shell.wait_for('@icaleb ~]$')
10
+ # interfaces = send_and_wait('/sbin/ifconfig -a', '@icaleb ~]$')
11
+ #
12
+ # Shells can be easily and quickly duplicated (#split) without the need to establish another connection.
13
+ # Shells provide :closed, :childless, and :split callbacks.
14
+ #
15
+ # @example Start another shell using the same connection
16
+ # shell.on(:childless) do
17
+ # info("#{shell}'s children all closed")
18
+ # shell.disconnect
19
+ # EM.stop
20
+ # end
21
+ #
22
+ # admin_shell = shell.split
23
+ # admin_shell.on(:closed) { warn("admin shell has closed") }
24
+ # admin_shell.send_and_wait('sudo su -', ']$')
25
+ class Shell
26
+ include Log
27
+ include Callbacks
28
+
29
+ # Global timeout for wait operations; can be overriden by :timeout option to new
30
+ TIMEOUT = 15
31
+ # @return [Net::SSH::Connection::Channel] The shell to which we can send_data
32
+ attr_reader :shell
33
+ # @return [Net::SSH::Connection]
34
+ attr_reader :connection
35
+ # @return [Boolean] Default (false) halt on timeout value - when true any timeouts will be halt the shell by raising an exception
36
+ attr_reader :halt_on_timeout
37
+ # @return [Hash] the options passed to initialize
38
+ attr_reader :options
39
+ # @return [Hash] the options to pass to connect automatically. They will be extracted from the opptions[:net_ssh] on initialization
40
+ attr_reader :connect_opts
41
+
42
+ # @return [String] the host to login to
43
+ attr_reader :host
44
+ # @return [String] The user to authenticate as
45
+ attr_reader :user
46
+ # @return [String] the password to authenticate with - can be nil
47
+ attr_reader :pass
48
+ # @return [Array] all shells that have been split off from this one.
49
+ attr_reader :children
50
+ # @return [Shell] the parent of this shell
51
+ attr_reader :parent
52
+ # @return [String] a string (\r\n) to append to every command
53
+ def line_terminator
54
+ @line_terminator ||= "\r\n"
55
+ end
56
+ # [String]
57
+ attr_writer :line_terminator
58
+
59
+ # Connect to an ssh server then start a user shell.
60
+ # @param [String] address
61
+ # @param [String] user
62
+ # @param [String, nil] pass by default publickey and password auth will be attempted
63
+ # @param [Hash] opts
64
+ # @option opts [Hash] :net_ssh options to pass to Net::SSH; see Net::SSH.start
65
+ # @option opts [Boolean] :halt_on_timeout (false)
66
+ # @option opts [Fixnum] :timeout (TIMEOUT) default timeout for all #wait_for and #send_wait calls.
67
+ def initialize(address, user, pass, opts = {}, &blk)
68
+ @halt_on_timeout = opts[:halt_on_timeout] || false
69
+ @timeout = opts[:timeout].is_a?(Fixnum) ? opts[:timeout] : TIMEOUT
70
+ @host = address
71
+ @user = user
72
+ @pass = pass
73
+ @options = opts
74
+ @connect_opts = {:password => pass, :port => 22, :auth_methods => ['publickey', 'password']}.merge(opts[:net_ssh] || {})
75
+ @connection = opts[:connection]
76
+ @parent = opts[:parent]
77
+ @children = []
78
+
79
+ block_given? ? open(&blk) : open
80
+ end
81
+
82
+ # Close the connection to the server and all child shells.
83
+ # Disconnected shells cannot be split.
84
+ def disconnect
85
+ close
86
+ connection.close
87
+ end
88
+
89
+ # @return [Boolean] true if the connection is still alive
90
+ def connected?
91
+ connection && !connection.closed?
92
+ end
93
+
94
+ # Close this shell and all children.
95
+ # Even when a shell is closed it is still connected to the server.
96
+ # Fires :closed event.
97
+ # @see Callbacks
98
+ def close
99
+ shell.close.tap{ debug("closing") } if shell.active?
100
+ @closed = true
101
+ children.each { |c| c.close }
102
+ fire(:closed)
103
+ end
104
+
105
+ # @return [Boolean] Has this shell been closed.
106
+ def closed?
107
+ @closed == true
108
+ end
109
+
110
+ # Send a string to the server and wait for a response containing a specified String or Regex.
111
+ # @param [String] send_str
112
+ # @return [String] all data in the buffer including the wait_str if it was found
113
+ def send_and_wait(send_str, wait_str = nil, opts = {})
114
+ raise ClosedChannel if closed?
115
+ debug("send_and_wait(#{send_str.inspect}, #{wait_str.inspect}, #{opts})")
116
+ send_data(send_str)
117
+ return wait_for(wait_str, opts)
118
+ end # send_and_wait(send_str, wait_str = nil, opts = {})
119
+
120
+ # Wait for the shell to send data containing the given string.
121
+ # @param [String, Regexp] strregex a string or regex to match the console output against.
122
+ # @param [Hash] opts
123
+ # @option opts [Fixnum] :timeout (Session::TIMEOUT) the maximum number of seconds to wait
124
+ # @option opts [Boolean] (false) :halt_on_timeout
125
+ # @return [String] the contents of the buffer
126
+ def wait_for(strregex, opts = { })
127
+ raise ClosedChannel if closed?
128
+ debug("wait_for(#{strregex}, #{opts})")
129
+ opts = { :timeout => @timeout, :halt_on_timeout => @halt_on_timeout }.merge(opts)
130
+ buffer = ''
131
+ found = nil
132
+ f = Fiber.current
133
+
134
+ timer = nil
135
+ timeout = proc do
136
+ shell.on_data {|c,d| }
137
+ # @todo fire an em errback
138
+ if opts[:halt_on_timeout]
139
+ raise TimeoutError("timeout while waiting for #{strregex.inspect}; received: #{buffer.inspect}")
140
+ else
141
+ warn("timeout while waiting for #{strregex.inspect}; received: #{buffer.inspect}")
142
+ end # opts[:halt_on_timeout]
143
+ end # timeout
144
+
145
+ shell.on_data do |ch,data|
146
+ buffer = "#{buffer}#{data}"
147
+ if strregex.is_a?(Regexp) ? buffer.match(strregex) : buffer.include?(strregex)
148
+ timer.respond_to?(:cancel) && timer.cancel
149
+ result = buffer.clone
150
+ shell.on_data {|c,d| }
151
+ f.resume(result)
152
+ end
153
+ end # |ch,data|
154
+
155
+ timer = EM::Timer.new(opts[:timeout], &timeout)
156
+ return Fiber.yield
157
+ end # wait_for(strregex, opts = { })
158
+
159
+ # Open a shell on the server.
160
+ # You generally don't need to call this.
161
+ # @return [self]
162
+ def open
163
+ f = Fiber.current
164
+ connect unless connected?
165
+
166
+ connection.open_channel do |channel|
167
+ debug "**** channel open: #{channel}"
168
+ channel.request_pty(options[:pty] || {}) do |pty,suc|
169
+ debug "***** pty open: #{pty}; suc: #{suc}"
170
+ pty.send_channel_request("shell") do |shell,success|
171
+ raise ConnectionError, "Failed to create shell." unless success
172
+ debug "***** shell open: #{shell}"
173
+ @shell = shell
174
+ f.resume(self)
175
+ end # |shell,success|
176
+ end # |pty,suc|
177
+ end # |channel|
178
+
179
+ return Fiber.yield
180
+ end # start
181
+
182
+ # Create a new shell using the same ssh connection.
183
+ # A connection will be established if this shell is not connected.
184
+ # If a block is provided the child will be closed after yielding.
185
+ # @yield [Shell] child
186
+ # @return [Shell] child
187
+ def split
188
+ connect unless connected?
189
+ child = self.class.new(host, user, pass, {:connection => connection, :parent => self}.merge(options))
190
+ child.line_terminator = line_terminator
191
+ children.push(child)
192
+ child.on(:closed) do
193
+ children.delete(child)
194
+ fire(:childless).tap{ info("fired :childless") } if children.empty?
195
+ end
196
+ fire(:split, child)
197
+ block_given? ? yield(child).tap { child.close } : child
198
+ end # split
199
+
200
+ # Connect to the server.
201
+ # Does not open the shell; use #open or #split
202
+ # You generally won't need to call this on your own.
203
+ def connect
204
+ f = Fiber.current
205
+ ::EM::Ssh.start(host, user, connect_opts) do |connection|
206
+ @connection = connection
207
+ f.resume
208
+ end # |connection|
209
+ return Fiber.yield
210
+ end # connect
211
+
212
+
213
+ # Send data to the ssh server shell.
214
+ # You generally don't need to call this.
215
+ # @see #send_and_wait
216
+ # @param [String] d the data to send encoded as a string
217
+ def send_data(d)
218
+ #debug("send_data: #{d.inspect}#{line_terminator}")
219
+ shell.send_data("#{d}#{line_terminator}")
220
+ end
221
+
222
+
223
+ end # class::Shell
224
+ end # class::Ssh
225
+ end # module::EventMachine
@@ -0,0 +1,5 @@
1
+ module EventMachine
2
+ class Ssh
3
+ VERSION='0.0.1'
4
+ end # class::Ssh
5
+ end # module::EventMachine
data/lib/em-ssh.rb ADDED
@@ -0,0 +1,74 @@
1
+ require 'eventmachine'
2
+
3
+ require 'fiber'
4
+ require 'timeout'
5
+
6
+ require 'net/ssh'
7
+ require 'em-ssh/log'
8
+
9
+ module EventMachine
10
+ # @example
11
+ # EM::Ssh.start(host, user, :password => password) do |ssh|
12
+ # ssh.exec("hostname") do |ch,stream,data|
13
+ # puts "data: #{data}"
14
+ # end
15
+ # end
16
+ class Ssh
17
+ DEFAULT_PORT = 22
18
+
19
+ # Generic error tag
20
+ module Error; end
21
+ # Any class that inherits from SshError will be an Exception and include a Ssh::Error tag
22
+ class SshError < Net::SSH::Exception; include Error; end
23
+ class TimeoutError < Timeout::Error; include Error; end
24
+ class ClosedChannel < SshError; end
25
+
26
+ class << self
27
+ attr_writer :logger
28
+ # Creates a logger when necessary
29
+ # @return [Logger]
30
+ def logger(level = Logger::WARN)
31
+ @logger ||= ::Logger.new(STDERR).tap{ |l| l.level = level }
32
+ end
33
+
34
+ # Connect to an ssh server
35
+ # @param [String] host
36
+ # @param [String] user
37
+ # @param [Hash] opts all options accepted by Net::SSH.start
38
+ # @yield [Session] an EventMachine compatible Net::SSH::Session
39
+ # @see http://net-ssh.github.com/ssh/v2/api/index.html
40
+ # @return [Session]
41
+ # @example
42
+ # EM::Ssh.start(host, user, options) do |connection|
43
+ # log.debug "**** connected: #{connection}"
44
+ # connection.open_channel do |channel|
45
+ # log.debug "**** channel: #{channel}"
46
+ # channel.request_pty(options[:pty] || {}) do |pty,suc|
47
+ def connect(host, user, opts = {}, &blk)
48
+ logger.debug("#{self}.connect(#{host}, #{user}, #{opts})")
49
+ options = { :host => host, :user => user, :port => DEFAULT_PORT, :callback => blk }.merge(opts)
50
+ EM.connect(options[:host], options[:port], Connection, options)
51
+ end
52
+ alias :start :connect
53
+ end # << self
54
+
55
+ # Pull in the constants from Net::SSH::[Transport, Connection and Authentication]
56
+ # and define them locally.
57
+ [:Transport, :Connection, :Authentication]
58
+ .map{ |sym| Net::SSH.const_get(sym).const_get(:Constants) }
59
+ .each do |mod|
60
+ mod.constants.each do |name|
61
+ const_set(name, mod.const_get(name))
62
+ end # |name|
63
+ end # |module|
64
+
65
+ end # class::Ssh
66
+ end # module::EventMachine
67
+
68
+
69
+ require 'em-ssh/callbacks'
70
+ require 'em-ssh/connection'
71
+ require 'em-ssh/server-version'
72
+ require 'em-ssh/packet-stream'
73
+ require 'em-ssh/authentication-session'
74
+ require 'em-ssh/session'
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: em-ssh
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Caleb Crane
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-10-22 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: eventmachine
16
+ requirement: &70157793111680 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70157793111680
25
+ - !ruby/object:Gem::Dependency
26
+ name: net-ssh
27
+ requirement: &70157793111020 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70157793111020
36
+ - !ruby/object:Gem::Dependency
37
+ name: ruby-termios
38
+ requirement: &70157793110100 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70157793110100
47
+ - !ruby/object:Gem::Dependency
48
+ name: highline
49
+ requirement: &70157793109680 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70157793109680
58
+ description: ''
59
+ email:
60
+ - em-ssh@simulacre.org
61
+ executables:
62
+ - em-ssh
63
+ - em-ssh-shell
64
+ extensions: []
65
+ extra_rdoc_files: []
66
+ files:
67
+ - lib/em-ssh/authentication-session.rb
68
+ - lib/em-ssh/callbacks.rb
69
+ - lib/em-ssh/connection.rb
70
+ - lib/em-ssh/log.rb
71
+ - lib/em-ssh/packet-stream.rb
72
+ - lib/em-ssh/server-version.rb
73
+ - lib/em-ssh/session.rb
74
+ - lib/em-ssh/shell.rb
75
+ - lib/em-ssh/version.rb
76
+ - lib/em-ssh.rb
77
+ - bin/em-ssh
78
+ - bin/em-ssh-shell
79
+ - README.md
80
+ homepage: http://github.com/simulacre/em-ssh
81
+ licenses:
82
+ - MIT
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ! '>='
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: 1.3.6
99
+ requirements: []
100
+ rubyforge_project:
101
+ rubygems_version: 1.8.10
102
+ signing_key:
103
+ specification_version: 3
104
+ summary: An EventMachine compatible net-ssh
105
+ test_files: []
106
+ has_rdoc: