em-ssh 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: