evented-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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1b74dc6a3af01c7c24fe72960ce9a64928f850d9
4
+ data.tar.gz: dccac2ad2cfbbcdfbbd9639deb6df27b210656f1
5
+ SHA512:
6
+ metadata.gz: 0f2bb02a4a7c4731b883d4155c66175bc3c77485294b6d06993be6d14110faf917b3a2fe5502f3a49f3dde209aa1127bbc744e98fb1b5647c35b6a693295def1
7
+ data.tar.gz: 3c22dced0445134e2f2e2001ce71827094ce2b7fb2fce52df74dc994f3bd046eed1cc0c43c652972be3d7d11c1ba3519533e69993c777705b09fab09ccb44c7f
data/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # Evented SSH
2
+
3
+ evented-ssh is a net-ssh adapter for Libuv. For the most part you can take any net-ssh code you have and run it in the Libuv reactor.
4
+
5
+ It runs almost entirely on net-ssh code, replacing parts of the transport and using futures in place of IO select calls.
6
+
7
+
8
+ ## Installation
9
+
10
+ gem install evented-ssh
11
+
@@ -0,0 +1,33 @@
1
+
2
+ require 'net/ssh/connection/channel'
3
+
4
+ module Net; module SSH; module Connection
5
+ class Channel
6
+ alias_method :original_initialize, :initialize
7
+ def initialize(connection, *args, &block)
8
+ original_initialize(connection, *args, &block)
9
+ @defer = connection.transport.reactor.defer
10
+ end
11
+
12
+ attr_reader :defer
13
+
14
+ # Use promise resolution instead of a loop
15
+ def wait
16
+ @defer.promise.value
17
+ end
18
+
19
+ # Allow direct access to the promise.
20
+ # Means we can do parallel tasks and then grab
21
+ # the results of multiple executions.
22
+ def promise
23
+ @defer.promise
24
+ end
25
+
26
+ alias_method :original_do_close, :do_close
27
+ def do_close
28
+ # Resolve the promise and anything waiting
29
+ @defer.resolve(nil)
30
+ original_do_close
31
+ end
32
+ end
33
+ end; end; end
@@ -0,0 +1,16 @@
1
+
2
+ require 'net/ssh/connection/event_loop'
3
+
4
+ module Net; module SSH; module Connection
5
+ class EventLoop
6
+
7
+ # Same as Net::SSH except it never tries to wait on IO. This
8
+ # basically always blocks the current fiber now until a packet
9
+ # is available. Connection#loop is called in a dedicated fiber
10
+ # who's purpose is to distribute the packets as they come in.
11
+ def process(wait = nil, &block)
12
+ return false unless ev_preprocess(&block)
13
+ #ev_select_and_postprocess(wait)
14
+ end
15
+ end
16
+ end; end; end
@@ -0,0 +1,49 @@
1
+
2
+ require 'net/ssh/connection/session'
3
+
4
+ module Net; module SSH; module Connection
5
+ class Session
6
+ alias_method :original_initialize, :initialize
7
+ def initialize(transport, options = {})
8
+ original_initialize(transport, options)
9
+
10
+ # This processes the incoming packets
11
+ # Replacing the IO select calls
12
+ # Next tick so we don't block the current fiber
13
+ transport.reactor.next_tick {
14
+ loop { true }
15
+ }
16
+ end
17
+
18
+ def close
19
+ info { "closing remaining channels (#{channels.length} open)" }
20
+ waiting = channels.collect { |id, channel|
21
+ channel.close
22
+ channel.defer.promise
23
+ }
24
+ begin
25
+ # We use promise resolution here instead of a loop
26
+ ::Libuv.all(waiting).value if channels.any?
27
+ rescue Net::SSH::Disconnect
28
+ raise unless channels.empty?
29
+ end
30
+ transport.close
31
+ end
32
+
33
+ # similar to exec! however it returns a promise instead of
34
+ # blocking the flow of execution.
35
+ def p_exec!(command, status: nil)
36
+ status ||= {}
37
+ channel = exec(command, status: status) do |ch, type, data|
38
+ ch[:result] ||= String.new
39
+ ch[:result] << data
40
+ end
41
+ channel.promise.then do
42
+ channel[:result] ||= ""
43
+ channel[:result] &&= channel[:result].force_encoding("UTF-8")
44
+
45
+ StringWithExitstatus.new(channel[:result], status[:exit_code])
46
+ end
47
+ end
48
+ end
49
+ end; end; end
@@ -0,0 +1,443 @@
1
+
2
+ module ESSH; module Transport
3
+ # Implements the higher-level logic behind an SSH key-exchange. It handles
4
+ # both the initial exchange, as well as subsequent re-exchanges (as needed).
5
+ # It also encapsulates the negotiation of the algorithms, and provides a
6
+ # single point of access to the negotiated algorithms.
7
+ #
8
+ # You will never instantiate or reference this directly. It is used
9
+ # internally by the transport layer.
10
+ class Algorithms
11
+ include ::Net::SSH::Transport::Constants
12
+ include ::Net::SSH::Loggable
13
+
14
+ # Define the default algorithms, in order of preference, supported by
15
+ # Net::SSH.
16
+ ALGORITHMS = {
17
+ host_key: %w(ssh-rsa ssh-dss
18
+ ssh-rsa-cert-v01@openssh.com
19
+ ssh-rsa-cert-v00@openssh.com),
20
+ kex: %w(diffie-hellman-group-exchange-sha1
21
+ diffie-hellman-group1-sha1
22
+ diffie-hellman-group14-sha1
23
+ diffie-hellman-group-exchange-sha256),
24
+ encryption: %w(aes128-cbc 3des-cbc blowfish-cbc cast128-cbc
25
+ aes192-cbc aes256-cbc rijndael-cbc@lysator.liu.se
26
+ idea-cbc arcfour128 arcfour256 arcfour
27
+ aes128-ctr aes192-ctr aes256-ctr
28
+ cast128-ctr blowfish-ctr 3des-ctr none),
29
+
30
+ hmac: %w(hmac-sha1 hmac-md5 hmac-sha1-96 hmac-md5-96
31
+ hmac-ripemd160 hmac-ripemd160@openssh.com
32
+ hmac-sha2-256 hmac-sha2-512 hmac-sha2-256-96
33
+ hmac-sha2-512-96 none),
34
+
35
+ compression: %w(none zlib@openssh.com zlib),
36
+ language: %w()
37
+ }
38
+ if defined?(::OpenSSL::PKey::EC)
39
+ ALGORITHMS[:host_key] += %w(ecdsa-sha2-nistp256
40
+ ecdsa-sha2-nistp384
41
+ ecdsa-sha2-nistp521)
42
+
43
+ if ::Net::SSH::Authentication::ED25519Loader::LOADED
44
+ ALGORITHMS[:host_key] += %w(ssh-ed25519)
45
+ end
46
+
47
+ ALGORITHMS[:kex] += %w(ecdh-sha2-nistp256
48
+ ecdh-sha2-nistp384
49
+ ecdh-sha2-nistp521)
50
+ end
51
+
52
+ # The underlying transport layer session that supports this object
53
+ attr_reader :session
54
+
55
+ # The hash of options used to initialize this object
56
+ attr_reader :options
57
+
58
+ # The kex algorithm to use settled on between the client and server.
59
+ attr_reader :kex
60
+
61
+ # The type of host key that will be used for this session.
62
+ attr_reader :host_key
63
+
64
+ # The type of the cipher to use to encrypt packets sent from the client to
65
+ # the server.
66
+ attr_reader :encryption_client
67
+
68
+ # The type of the cipher to use to decrypt packets arriving from the server.
69
+ attr_reader :encryption_server
70
+
71
+ # The type of HMAC to use to sign packets sent by the client.
72
+ attr_reader :hmac_client
73
+
74
+ # The type of HMAC to use to validate packets arriving from the server.
75
+ attr_reader :hmac_server
76
+
77
+ # The type of compression to use to compress packets being sent by the client.
78
+ attr_reader :compression_client
79
+
80
+ # The type of compression to use to decompress packets arriving from the server.
81
+ attr_reader :compression_server
82
+
83
+ # The language that will be used in messages sent by the client.
84
+ attr_reader :language_client
85
+
86
+ # The language that will be used in messages sent from the server.
87
+ attr_reader :language_server
88
+
89
+ # The hash of algorithms preferred by the client, which will be told to
90
+ # the server during algorithm negotiation.
91
+ attr_reader :algorithms
92
+
93
+ # The session-id for this session, as decided during the initial key exchange.
94
+ attr_reader :session_id
95
+
96
+ # Returns true if the given packet can be processed during a key-exchange.
97
+ def self.allowed_packet?(packet)
98
+ ( 1.. 4).include?(packet.type) ||
99
+ ( 6..19).include?(packet.type) ||
100
+ (21..49).include?(packet.type)
101
+ end
102
+
103
+ # Instantiates a new Algorithms object, and prepares the hash of preferred
104
+ # algorithms based on the options parameter and the ALGORITHMS constant.
105
+ def initialize(session, options={})
106
+ @session = session
107
+ @logger = session.logger
108
+ @options = options
109
+ @algorithms = {}
110
+ @ready = @session.reactor.defer
111
+ @pending = nil
112
+ @initialized = false
113
+ @client_packet = @server_packet = nil
114
+ prepare_preferred_algorithms!
115
+ end
116
+
117
+ # Start the algorithm negotation
118
+ def start
119
+ raise ArgumentError, "Cannot call start if it's negotiation started or done" if @pending || @initialized
120
+ send_kexinit unless @server_data
121
+ end
122
+
123
+ def ready
124
+ @ready.promise.value
125
+ end
126
+
127
+ def reject(error)
128
+ @ready.reject(error)
129
+ @pending.reject(error) if @pending
130
+ end
131
+
132
+ # Request a rekey operation. This will return immediately, and does not
133
+ # actually perform the rekey operation. It does cause the session to change
134
+ # state, however--until the key exchange finishes, no new packets will be
135
+ # processed.
136
+ def rekey!
137
+ @client_packet = @server_packet = nil
138
+ @initialized = false
139
+ send_kexinit
140
+ end
141
+
142
+ # Called by the transport layer when a KEXINIT packet is received, indicating
143
+ # that the server wants to exchange keys. This can be spontaneous, or it
144
+ # can be in response to a client-initiated rekey request (see #rekey!). Either
145
+ # way, this will block until the key exchange completes.
146
+ def accept_kexinit(packet)
147
+ info { "got KEXINIT from server" }
148
+ @server_data = parse_server_algorithm_packet(packet)
149
+ @server_packet = @server_data[:raw]
150
+ if @pending.nil?
151
+ send_kexinit
152
+ else
153
+ proceed!
154
+ end
155
+ end
156
+
157
+ # A convenience method for accessing the list of preferred types for a
158
+ # specific algorithm (see #algorithms).
159
+ def [](key)
160
+ algorithms[key]
161
+ end
162
+
163
+ # Returns +true+ if a key-exchange is pending. This will be true from the
164
+ # moment either the client or server requests the key exchange, until the
165
+ # exchange completes. While an exchange is pending, only a limited number
166
+ # of packets are allowed, so event processing essentially stops during this
167
+ # period.
168
+ def pending?
169
+ @pending
170
+ end
171
+
172
+ # Returns true if no exchange is pending, and otherwise returns true or
173
+ # false depending on whether the given packet is of a type that is allowed
174
+ # during a key exchange.
175
+ def allow?(packet)
176
+ !pending? || Algorithms.allowed_packet?(packet)
177
+ end
178
+
179
+ # Returns true if the algorithms have been negotiated at all.
180
+ def initialized?
181
+ @initialized
182
+ end
183
+
184
+ private
185
+
186
+ # Sends a KEXINIT packet to the server. If a server KEXINIT has already
187
+ # been received, this will then invoke #proceed! to proceed with the key
188
+ # exchange, otherwise it returns immediately (but sets the object to the
189
+ # pending state).
190
+ def send_kexinit
191
+ @pending = @session.reactor.defer
192
+ @pending.promise.finally do
193
+ @pending = nil
194
+ end
195
+ @ready.resolve(@pending.promise)
196
+
197
+ info { "sending KEXINIT" }
198
+
199
+ packet = build_client_algorithm_packet
200
+ @client_packet = packet.to_s
201
+ session.enqueue_message(packet)
202
+ proceed! if @server_packet
203
+ end
204
+
205
+ # After both client and server have sent their KEXINIT packets, this
206
+ # will do the algorithm negotiation and key exchange. Once both finish,
207
+ # the object leaves the pending state and the method returns.
208
+ def proceed!
209
+ info { "negotiating algorithms" }
210
+ negotiate_algorithms
211
+ exchange_keys
212
+ rescue Exception => e
213
+ @pending.reject(e)
214
+ end
215
+
216
+ # Prepares the list of preferred algorithms, based on the options hash
217
+ # that was given when the object was constructed, and the ALGORITHMS
218
+ # constant. Also, when determining the host_key type to use, the known
219
+ # hosts files are examined to see if the host has ever sent a host_key
220
+ # before, and if so, that key type is used as the preferred type for
221
+ # communicating with this server.
222
+ def prepare_preferred_algorithms!
223
+ options[:compression] = %w(zlib@openssh.com zlib) if options[:compression] == true
224
+
225
+ ALGORITHMS.each do |algorithm, supported|
226
+ algorithms[algorithm] = compose_algorithm_list(supported, options[algorithm], options[:append_all_supported_algorithms])
227
+ end
228
+
229
+ # for convention, make sure our list has the same keys as the server
230
+ # list
231
+
232
+ algorithms[:encryption_client ] = algorithms[:encryption_server ] = algorithms[:encryption]
233
+ algorithms[:hmac_client ] = algorithms[:hmac_server ] = algorithms[:hmac]
234
+ algorithms[:compression_client] = algorithms[:compression_server] = algorithms[:compression]
235
+ algorithms[:language_client ] = algorithms[:language_server ] = algorithms[:language]
236
+
237
+ if !options.key?(:host_key)
238
+ # make sure the host keys are specified in preference order, where any
239
+ # existing known key for the host has preference.
240
+
241
+ existing_keys = session.host_keys
242
+ host_keys = existing_keys.map { |key| key.ssh_type }.uniq
243
+ algorithms[:host_key].each do |name|
244
+ host_keys << name unless host_keys.include?(name)
245
+ end
246
+ algorithms[:host_key] = host_keys
247
+ end
248
+ end
249
+
250
+ # Composes the list of algorithms by taking supported algorithms and matching with supplied options.
251
+ def compose_algorithm_list(supported, option, append_all_supported_algorithms = false)
252
+ return supported.dup unless option
253
+
254
+ list = []
255
+ option = Array(option).compact.uniq
256
+
257
+ if option.first && option.first.start_with?('+')
258
+ list = supported.dup
259
+ list << option.first[1..-1]
260
+ list.concat(option[1..-1])
261
+ list.uniq!
262
+ else
263
+ list = option
264
+
265
+ if append_all_supported_algorithms
266
+ supported.each { |name| list << name unless list.include?(name) }
267
+ end
268
+ end
269
+
270
+ unsupported = []
271
+ list.select! do |name|
272
+ is_supported = supported.include?(name)
273
+ unsupported << name unless is_supported
274
+ is_supported
275
+ end
276
+
277
+ lwarn { %(unsupported algorithm: `#{unsupported}') } unless unsupported.empty?
278
+
279
+ list
280
+ end
281
+
282
+ # Parses a KEXINIT packet from the server.
283
+ def parse_server_algorithm_packet(packet)
284
+ data = { raw: packet.content }
285
+
286
+ packet.read(16) # skip the cookie value
287
+
288
+ data[:kex] = packet.read_string.split(/,/)
289
+ data[:host_key] = packet.read_string.split(/,/)
290
+ data[:encryption_client] = packet.read_string.split(/,/)
291
+ data[:encryption_server] = packet.read_string.split(/,/)
292
+ data[:hmac_client] = packet.read_string.split(/,/)
293
+ data[:hmac_server] = packet.read_string.split(/,/)
294
+ data[:compression_client] = packet.read_string.split(/,/)
295
+ data[:compression_server] = packet.read_string.split(/,/)
296
+ data[:language_client] = packet.read_string.split(/,/)
297
+ data[:language_server] = packet.read_string.split(/,/)
298
+
299
+ # TODO: if first_kex_packet_follows, we need to try to skip the
300
+ # actual kexinit stuff and try to guess what the server is doing...
301
+ # need to read more about this scenario.
302
+ # first_kex_packet_follows = packet.read_bool
303
+
304
+ return data
305
+ end
306
+
307
+ # Given the #algorithms map of preferred algorithm types, this constructs
308
+ # a KEXINIT packet to send to the server. It does not actually send it,
309
+ # it simply builds the packet and returns it.
310
+ def build_client_algorithm_packet
311
+ kex = algorithms[:kex ].join(",")
312
+ host_key = algorithms[:host_key ].join(",")
313
+ encryption = algorithms[:encryption ].join(",")
314
+ hmac = algorithms[:hmac ].join(",")
315
+ compression = algorithms[:compression].join(",")
316
+ language = algorithms[:language ].join(",")
317
+
318
+ ::Net::SSH::Buffer.from(:byte, KEXINIT,
319
+ :long, [rand(0xFFFFFFFF), rand(0xFFFFFFFF), rand(0xFFFFFFFF), rand(0xFFFFFFFF)],
320
+ :mstring, [kex, host_key, encryption, encryption, hmac, hmac],
321
+ :mstring, [compression, compression, language, language],
322
+ :bool, false, :long, 0)
323
+ end
324
+
325
+ # Given the parsed server KEX packet, and the client's preferred algorithm
326
+ # lists in #algorithms, determine which preferred algorithms each has
327
+ # in common and set those as the selected algorithms. If, for any algorithm,
328
+ # no type can be settled on, an exception is raised.
329
+ def negotiate_algorithms
330
+ @kex = negotiate(:kex)
331
+ @host_key = negotiate(:host_key)
332
+ @encryption_client = negotiate(:encryption_client)
333
+ @encryption_server = negotiate(:encryption_server)
334
+ @hmac_client = negotiate(:hmac_client)
335
+ @hmac_server = negotiate(:hmac_server)
336
+ @compression_client = negotiate(:compression_client)
337
+ @compression_server = negotiate(:compression_server)
338
+ @language_client = negotiate(:language_client) rescue ""
339
+ @language_server = negotiate(:language_server) rescue ""
340
+
341
+ debug do
342
+ "negotiated:\n" +
343
+ [:kex, :host_key, :encryption_server, :encryption_client, :hmac_client, :hmac_server, :compression_client, :compression_server, :language_client, :language_server].map do |key|
344
+ "* #{key}: #{instance_variable_get("@#{key}")}"
345
+ end.join("\n")
346
+ end
347
+ end
348
+
349
+ # Negotiates a single algorithm based on the preferences reported by the
350
+ # server and those set by the client. This is called by
351
+ # #negotiate_algorithms.
352
+ def negotiate(algorithm)
353
+ match = self[algorithm].find { |item| @server_data[algorithm].include?(item) }
354
+ if match.nil?
355
+ raise ::Net::SSH::Exception, "could not settle on #{algorithm} algorithm"
356
+ end
357
+ return match
358
+ end
359
+
360
+ # Considers the sizes of the keys and block-sizes for the selected ciphers,
361
+ # and the lengths of the hmacs, and returns the largest as the byte requirement
362
+ # for the key-exchange algorithm.
363
+ def kex_byte_requirement
364
+ sizes = [8] # require at least 8 bytes
365
+
366
+ sizes.concat(::Net::SSH::Transport::CipherFactory.get_lengths(encryption_client))
367
+ sizes.concat(::Net::SSH::Transport::CipherFactory.get_lengths(encryption_server))
368
+
369
+ sizes << ::Net::SSH::Transport::HMAC.key_length(hmac_client)
370
+ sizes << ::Net::SSH::Transport::HMAC.key_length(hmac_server)
371
+
372
+ sizes.max
373
+ end
374
+
375
+ # Instantiates one of the Transport::Kex classes (based on the negotiated
376
+ # kex algorithm), and uses it to exchange keys. Then, the ciphers and
377
+ # HMACs are initialized and fed to the transport layer, to be used in
378
+ # further communication with the server.
379
+ def exchange_keys
380
+ debug { "exchanging keys" }
381
+
382
+ algorithm = ::Net::SSH::Transport::Kex::MAP[kex].new(self, session,
383
+ client_version_string: ::Net::SSH::Transport::ServerVersion::PROTO_VERSION,
384
+ server_version_string: session.server_version.version,
385
+ server_algorithm_packet: @server_packet,
386
+ client_algorithm_packet: @client_packet,
387
+ need_bytes: kex_byte_requirement,
388
+ minimum_dh_bits: options[:minimum_dh_bits],
389
+ logger: logger)
390
+ result = algorithm.exchange_keys
391
+
392
+ secret = result[:shared_secret].to_ssh
393
+ hash = result[:session_id]
394
+ digester = result[:hashing_algorithm]
395
+
396
+ @session_id ||= hash
397
+
398
+ key = Proc.new { |salt| digester.digest(secret + hash + salt + @session_id) }
399
+
400
+ iv_client = key["A"]
401
+ iv_server = key["B"]
402
+ key_client = key["C"]
403
+ key_server = key["D"]
404
+ mac_key_client = key["E"]
405
+ mac_key_server = key["F"]
406
+
407
+ parameters = { shared: secret, hash: hash, digester: digester }
408
+
409
+ cipher_client = ::Net::SSH::Transport::CipherFactory.get(encryption_client, parameters.merge(iv: iv_client, key: key_client, encrypt: true))
410
+ cipher_server = ::Net::SSH::Transport::CipherFactory.get(encryption_server, parameters.merge(iv: iv_server, key: key_server, decrypt: true))
411
+
412
+ mac_client = ::Net::SSH::Transport::HMAC.get(hmac_client, mac_key_client, parameters)
413
+ mac_server = ::Net::SSH::Transport::HMAC.get(hmac_server, mac_key_server, parameters)
414
+
415
+ session.configure_client cipher: cipher_client, hmac: mac_client,
416
+ compression: normalize_compression_name(compression_client),
417
+ compression_level: options[:compression_level],
418
+ rekey_limit: options[:rekey_limit],
419
+ max_packets: options[:rekey_packet_limit],
420
+ max_blocks: options[:rekey_blocks_limit]
421
+
422
+ session.configure_server cipher: cipher_server, hmac: mac_server,
423
+ compression: normalize_compression_name(compression_server),
424
+ rekey_limit: options[:rekey_limit],
425
+ max_packets: options[:rekey_packet_limit],
426
+ max_blocks: options[:rekey_blocks_limit]
427
+
428
+ @pending.resolve(self)
429
+ @initialized = true
430
+ end
431
+
432
+ # Given the SSH name for some compression algorithm, return a normalized
433
+ # name as a symbol.
434
+ def normalize_compression_name(name)
435
+ case name
436
+ when "none" then false
437
+ when "zlib" then :standard
438
+ when "zlib@openssh.com" then :delayed
439
+ else raise ArgumentError, "unknown compression type `#{name}'"
440
+ end
441
+ end
442
+ end
443
+ end; end
@@ -0,0 +1,322 @@
1
+ require 'libuv'
2
+
3
+ require 'net/ssh/errors'
4
+ require 'net/ssh/loggable'
5
+ require 'net/ssh/transport/constants'
6
+ require 'net/ssh/transport/state'
7
+ require 'net/ssh/buffer'
8
+ require 'net/ssh/packet'
9
+
10
+ module ESSH; module Transport
11
+ class PacketStream < ::Libuv::TCP
12
+ include ::Net::SSH::Transport::Constants
13
+ include ::Net::SSH::Loggable
14
+
15
+ def initialize(session, **options)
16
+ @hints = {}
17
+ @server = ::Net::SSH::Transport::State.new(self, :server)
18
+ @client = ::Net::SSH::Transport::State.new(self, :client)
19
+ @packet = nil
20
+ @packets = []
21
+ @packets = []
22
+ @pending_packets = []
23
+ @process_pending = nil
24
+ @awaiting = []
25
+ @input = ::Net::SSH::Buffer.new
26
+
27
+ @session = session
28
+ @have_header = false
29
+
30
+ self.logger = options[:logger]
31
+ super(session.reactor, **options)
32
+
33
+ progress method(:check_packet)
34
+ end
35
+
36
+ def prepare(buff)
37
+ progress method(:check_packet)
38
+ check_packet(buff, self) unless buff.empty?
39
+ #start_read
40
+ end
41
+
42
+ attr_accessor :algorithms
43
+
44
+ # The map of "hints" that can be used to modify the behavior of the packet
45
+ # stream. For instance, when authentication succeeds, an "authenticated"
46
+ # hint is set, which is used to determine whether or not to compress the
47
+ # data when using the "delayed" compression algorithm.
48
+ attr_reader :hints
49
+
50
+ # The server state object, which encapsulates the algorithms used to interpret
51
+ # packets coming from the server.
52
+ attr_reader :server
53
+
54
+ # The client state object, which encapsulates the algorithms used to build
55
+ # packets to send to the server.
56
+ attr_reader :client
57
+
58
+ # The name of the client (local) end of the socket, as reported by the
59
+ # socket.
60
+ def client_name
61
+ sockname[0]
62
+ end
63
+
64
+ # The IP address of the peer (remote) end of the socket, as reported by
65
+ # the socket.
66
+ def peer_ip
67
+ peername[0]
68
+ end
69
+
70
+ # Enqueues a packet to be sent, but does not immediately send the packet.
71
+ # The given payload is pre-processed according to the algorithms specified
72
+ # in the client state (compression, cipher, and hmac).
73
+ def enqueue_packet(payload)
74
+ # try to compress the packet
75
+ payload = client.compress(payload)
76
+
77
+ # the length of the packet, minus the padding
78
+ actual_length = 4 + payload.bytesize + 1
79
+
80
+ # compute the padding length
81
+ padding_length = client.block_size - (actual_length % client.block_size)
82
+ padding_length += client.block_size if padding_length < 4
83
+
84
+ # compute the packet length (sans the length field itself)
85
+ packet_length = payload.bytesize + padding_length + 1
86
+
87
+ if packet_length < 16
88
+ padding_length += client.block_size
89
+ packet_length = payload.bytesize + padding_length + 1
90
+ end
91
+
92
+ padding = Array.new(padding_length) { rand(256) }.pack("C*")
93
+
94
+ unencrypted_data = [packet_length, padding_length, payload, padding].pack("NCA*A*")
95
+ mac = client.hmac.digest([client.sequence_number, unencrypted_data].pack("NA*"))
96
+
97
+ encrypted_data = client.update_cipher(unencrypted_data) << client.final_cipher
98
+ message = "#{encrypted_data}#{mac}"
99
+
100
+ debug { "queueing packet nr #{client.sequence_number} type #{payload.getbyte(0)} len #{packet_length}" }
101
+
102
+ client.increment(packet_length)
103
+ direct_write(message)
104
+
105
+ self
106
+ end
107
+
108
+ def get_packet(mode = :block)
109
+ case mode
110
+ when :nonblock
111
+ return @packets.shift
112
+ when :block
113
+ packet = @packets.shift
114
+ return packet unless packet.nil?
115
+ defer = @reactor.defer
116
+ @awaiting << defer
117
+ return defer.promise.value
118
+ else
119
+ raise ArgumentError, "expected :block or :nonblock, got #{mode.inspect}"
120
+ end
121
+ rescue
122
+ nil
123
+ end
124
+
125
+ def queue_packet(packet)
126
+ pending = @algorithms.pending?
127
+ if not pending
128
+ @packets << packet
129
+ elsif Algorithms.allowed_packet?(packet)
130
+ @packets << packet
131
+ else
132
+ @pending_packets << packet
133
+ if @process_pending.nil?
134
+ @process_pending = pending
135
+ @process_pending.promise.finally do
136
+ @process_pending = nil
137
+ @packets.concat(@pending_packets)
138
+ @pending_packets.clear
139
+ process_waiting
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ def process_waiting
146
+ loop do
147
+ break if @packets.empty?
148
+ waiting = @awaiting.shift
149
+ break unless waiting
150
+ waiting.resolve(@packets.shift)
151
+ end
152
+ end
153
+
154
+ # If the IO object requires a rekey operation (as indicated by either its
155
+ # client or server state objects, see State#needs_rekey?), this will
156
+ # yield. Otherwise, this does nothing.
157
+ def if_needs_rekey?
158
+ if client.needs_rekey? || server.needs_rekey?
159
+ yield
160
+ client.reset! if client.needs_rekey?
161
+ server.reset! if server.needs_rekey?
162
+ end
163
+ end
164
+
165
+ # Read up to +length+ bytes from the input buffer. If +length+ is nil,
166
+ # all available data is read from the buffer. (See #available.)
167
+ def read_available(length = nil)
168
+ @input.read(length || available)
169
+ end
170
+
171
+ # Returns the number of bytes available to be read from the input buffer.
172
+ # (See #read_available.)
173
+ def available
174
+ @input.available
175
+ end
176
+
177
+ def read_buffer #:nodoc:
178
+ @input.to_s
179
+ end
180
+
181
+
182
+ private
183
+
184
+
185
+ def check_packet(data, _)
186
+ data.force_encoding(Encoding::BINARY)
187
+ @input.append(data)
188
+
189
+ if @have_header
190
+ process_buffer
191
+ else
192
+ version = @input.read_to(/SSH-.+\n/)
193
+ return unless version
194
+
195
+ if version.match(/SSH-(1\.99|2\.0)-/)
196
+ @input = @input.remainder_as_buffer
197
+
198
+ # Grab just the version string (some older implementation don't send the \r char)
199
+ # This is then used as part of the Diffie-Hellman Key Exchange
200
+ parts = version.split("\n")
201
+ @session.server_version.header = parts[0..-2].map { |part| part.chomp }.join("\n")
202
+ @session.server_version.version = parts[-1].chomp
203
+
204
+ @have_header = true
205
+ @algorithms.start
206
+ process_buffer if @input.length > 0
207
+ else
208
+ reject_and_raise(::Net::SSH::Exception, "incompatible SSH version: #{version}")
209
+ end
210
+ end
211
+ end
212
+
213
+ def process_buffer
214
+ packets = []
215
+
216
+ # Extract packets from the input stream
217
+ loop do
218
+ packet = next_packet
219
+ break if packet.nil?
220
+ packets << packet
221
+ end
222
+
223
+ # Pre-process packets
224
+ packets.each do |packet|
225
+ case packet.type
226
+ when DISCONNECT
227
+ reject_and_raise(::Net::SSH::Disconnect, "disconnected: #{packet[:description]} (#{packet[:reason_code]})")
228
+
229
+ when IGNORE
230
+ debug { "IGNORE packet received: #{packet[:data].inspect}" }
231
+
232
+ when UNIMPLEMENTED
233
+ lwarn { "UNIMPLEMENTED: #{packet[:number]}" }
234
+
235
+ when DEBUG
236
+ __send__(packet[:always_display] ? :fatal : :debug) { packet[:message] }
237
+
238
+ when KEXINIT
239
+ @algorithms.accept_kexinit(packet)
240
+
241
+ else
242
+ queue_packet(packet)
243
+ end
244
+ end
245
+
246
+ # Process what we can
247
+ process_waiting
248
+ end
249
+
250
+ def next_packet
251
+ if @packet.nil?
252
+ minimum = server.block_size < 4 ? 4 : server.block_size
253
+ return nil if available < minimum
254
+ data = read_available(minimum)
255
+
256
+ # decipher it
257
+ @packet = ::Net::SSH::Buffer.new(server.update_cipher(data))
258
+ @packet_length = @packet.read_long
259
+ end
260
+
261
+ need = @packet_length + 4 - server.block_size
262
+ if need % server.block_size != 0
263
+ reject_and_raise(::Net::SSH::Exception, "padding error, need #{need} block #{server.block_size}")
264
+ end
265
+
266
+ return nil if available < need + server.hmac.mac_length
267
+
268
+ if need > 0
269
+ # read the remainder of the packet and decrypt it.
270
+ data = read_available(need)
271
+ @packet.append(server.update_cipher(data))
272
+ end
273
+
274
+ # get the hmac from the tail of the packet (if one exists), and
275
+ # then validate it.
276
+ real_hmac = read_available(server.hmac.mac_length) || ""
277
+
278
+ @packet.append(server.final_cipher)
279
+ padding_length = @packet.read_byte
280
+
281
+ payload = @packet.read(@packet_length - padding_length - 1)
282
+
283
+ my_computed_hmac = server.hmac.digest([server.sequence_number, @packet.content].pack("NA*"))
284
+ if real_hmac != my_computed_hmac
285
+ reject_and_raise(::Net::SSH::Exception, "corrupted mac detected")
286
+ end
287
+
288
+ # try to decompress the payload, in case compression is active
289
+ payload = server.decompress(payload)
290
+
291
+ debug { "received packet nr #{server.sequence_number} type #{payload.getbyte(0)} len #{@packet_length}" }
292
+
293
+ server.increment(@packet_length)
294
+ @packet = nil
295
+
296
+ return ::Net::SSH::Packet.new(payload)
297
+ end
298
+
299
+ def on_close(pointer)
300
+ super(pointer)
301
+ client.cleanup
302
+ server.cleanup
303
+
304
+ @reactor.next_tick do
305
+ reject_reason = @close_error || 'connection closed'
306
+ @awaiting.each do |wait|
307
+ wait.reject(reject_reason)
308
+ end
309
+ @awaiting.clear
310
+ @algorithms.reject(reject_reason)
311
+ end
312
+ rescue => e
313
+ error { e }
314
+ end
315
+
316
+ def reject_and_raise(klass, msg)
317
+ error = klass.new(msg)
318
+ reject(error)
319
+ raise error
320
+ end
321
+ end
322
+ end; end
@@ -0,0 +1,251 @@
1
+ require 'ipaddress'
2
+
3
+ require 'net/ssh/errors'
4
+ require 'net/ssh/loggable'
5
+ require 'net/ssh/version'
6
+ require 'net/ssh/transport/constants'
7
+ require 'net/ssh/transport/server_version'
8
+ require 'net/ssh/verifiers/null'
9
+ require 'net/ssh/verifiers/secure'
10
+ require 'net/ssh/verifiers/strict'
11
+ require 'net/ssh/verifiers/lenient'
12
+
13
+ require 'evented-ssh/transport/packet_stream'
14
+ require 'evented-ssh/transport/algorithms'
15
+
16
+ module ESSH; module Transport
17
+
18
+ # The transport layer represents the lowest level of the SSH protocol, and
19
+ # implements basic message exchanging and protocol initialization. It will
20
+ # never be instantiated directly (unless you really know what you're about),
21
+ # but will instead be created for you automatically when you create a new
22
+ # SSH session via Net::SSH.start.
23
+ class Session
24
+ include ::Net::SSH::Transport::Constants
25
+ include ::Net::SSH::Loggable
26
+
27
+ ServerVersion = Struct.new(:header, :version)
28
+
29
+ # The standard port for the SSH protocol.
30
+ DEFAULT_PORT = 22
31
+
32
+ # The host to connect to, as given to the constructor.
33
+ attr_reader :host
34
+
35
+ # The port number to connect to, as given in the options to the constructor.
36
+ # If no port number was given, this will default to DEFAULT_PORT.
37
+ attr_reader :port
38
+
39
+ # The underlying socket object being used to communicate with the remote
40
+ # host.
41
+ attr_reader :socket
42
+
43
+ # The ServerVersion instance that encapsulates the negotiated protocol
44
+ # version.
45
+ attr_reader :server_version
46
+
47
+ # The Algorithms instance used to perform key exchanges.
48
+ attr_reader :algorithms
49
+
50
+ # The host-key verifier object used to verify host keys, to ensure that
51
+ # the connection is not being spoofed.
52
+ attr_reader :host_key_verifier
53
+
54
+ # The hash of options that were given to the object at initialization.
55
+ attr_reader :options
56
+
57
+ # The event loop that this SSH session is running on
58
+ attr_reader :reactor
59
+
60
+ # Instantiates a new transport layer abstraction. This will block until
61
+ # the initial key exchange completes, leaving you with a ready-to-use
62
+ # transport session.
63
+ def initialize(host, **options)
64
+ self.logger = options[:logger]
65
+
66
+ @reactor = ::Libuv.reactor
67
+
68
+ @host = host
69
+ @port = options[:port] || DEFAULT_PORT
70
+ @bind_address = options[:bind_address] || '0.0.0.0'
71
+ @options = options
72
+
73
+ debug { "establishing connection to #{@host}:#{@port}" }
74
+
75
+ actual_host = if IPAddress.valid?(@host)
76
+ @host
77
+ else
78
+ @reactor.lookup(@host)[0][0]
79
+ end
80
+
81
+ @socket = PacketStream.new(self, **options)
82
+ @socket.connect(actual_host, @port)
83
+
84
+ debug { "connection established" }
85
+
86
+ @host_key_verifier = select_host_key_verifier(options[:paranoid])
87
+ @algorithms = Algorithms.new(self, options)
88
+ @server_version = ServerVersion.new
89
+ @socket.algorithms = @algorithms
90
+
91
+ socket.direct_write "#{::Net::SSH::Transport::ServerVersion::PROTO_VERSION}\r\n"
92
+ socket.start_read
93
+
94
+ @algorithms.ready # Wait for this to complete
95
+ end
96
+
97
+ def host_keys
98
+ @host_keys ||= begin
99
+ known_hosts = options.fetch(:known_hosts, ::Net::SSH::KnownHosts)
100
+ known_hosts.search_for(options[:host_key_alias] || host_as_string, options)
101
+ end
102
+ end
103
+
104
+ # Returns the host (and possibly IP address) in a format compatible with
105
+ # SSH known-host files.
106
+ def host_as_string
107
+ @host_as_string ||= begin
108
+ string = "#{host}"
109
+ string = "[#{string}]:#{port}" if port != DEFAULT_PORT
110
+
111
+ peer_ip = socket.peer_ip
112
+
113
+ if peer_ip != host
114
+ string2 = peer_ip
115
+ string2 = "[#{string2}]:#{port}" if port != DEFAULT_PORT
116
+ string << "," << string2
117
+ end
118
+
119
+ string
120
+ end
121
+ end
122
+
123
+ # Returns true if the underlying socket has been closed.
124
+ def closed?
125
+ socket.closed?
126
+ end
127
+
128
+ # Cleans up (see PacketStream#cleanup) and closes the underlying socket.
129
+ def close
130
+ info { "closing connection" }
131
+ socket.shutdown
132
+ end
133
+
134
+ # Performs a "hard" shutdown of the connection. In general, this should
135
+ # never be done, but it might be necessary (in a rescue clause, for instance,
136
+ # when the connection needs to close but you don't know the status of the
137
+ # underlying protocol's state).
138
+ def shutdown!
139
+ error { "forcing connection closed" }
140
+ socket.close
141
+ end
142
+
143
+ # Returns a new service_request packet for the given service name, ready
144
+ # for sending to the server.
145
+ def service_request(service)
146
+ ::Net::SSH::Buffer.from(:byte, SERVICE_REQUEST, :string, service)
147
+ end
148
+
149
+ # Requests a rekey operation, and blocks until the operation completes.
150
+ # If a rekey is already pending, this returns immediately, having no
151
+ # effect.
152
+ def rekey!
153
+ if !algorithms.pending?
154
+ algorithms.rekey!
155
+ @algorithms.pending?&.promise&.value # Wait for this to complete
156
+ end
157
+ end
158
+
159
+ # Returns immediately if a rekey is already in process. Otherwise, if a
160
+ # rekey is needed (as indicated by the socket, see PacketStream#if_needs_rekey?)
161
+ # one is performed, causing this method to block until it completes.
162
+ def rekey_as_needed
163
+ return if algorithms.pending?
164
+ socket.if_needs_rekey? { rekey! }
165
+ end
166
+
167
+ # Returns a hash of information about the peer (remote) side of the socket,
168
+ # including :ip, :port, :host, and :canonized (see #host_as_string).
169
+ def peer
170
+ @peer ||= { ip: socket.peer_ip, port: @port.to_i, host: @host, canonized: host_as_string }
171
+ end
172
+
173
+ # Blocks until a new packet is available to be read, and returns that
174
+ # packet. See #poll_message.
175
+ def next_message
176
+ socket.get_packet
177
+ end
178
+
179
+ def poll_message
180
+ socket.get_packet
181
+ end
182
+
183
+ # Adds the given packet to the packet queue. If the queue is non-empty,
184
+ # #poll_message will return packets from the queue in the order they
185
+ # were received.
186
+ def push(packet)
187
+ socket.queue_packet(packet)
188
+ process_waiting
189
+ end
190
+
191
+ # Sends the given message via the packet stream, blocking until the
192
+ # entire message has been sent.
193
+ def send_message(message)
194
+ socket.enqueue_packet(message)
195
+ end
196
+
197
+ # Enqueues the given message, such that it will be sent at the earliest
198
+ # opportunity. This does not block, but returns immediately.
199
+ def enqueue_message(message)
200
+ socket.enqueue_packet(message)
201
+ end
202
+
203
+ # Configure's the packet stream's client state with the given set of
204
+ # options. This is typically used to define the cipher, compression, and
205
+ # hmac algorithms to use when sending packets to the server.
206
+ def configure_client(options={})
207
+ socket.client.set(options)
208
+ end
209
+
210
+ # Configure's the packet stream's server state with the given set of
211
+ # options. This is typically used to define the cipher, compression, and
212
+ # hmac algorithms to use when reading packets from the server.
213
+ def configure_server(options={})
214
+ socket.server.set(options)
215
+ end
216
+
217
+ # Sets a new hint for the packet stream, which the packet stream may use
218
+ # to change its behavior. (See PacketStream#hints).
219
+ def hint(which, value=true)
220
+ socket.hints[which] = value
221
+ end
222
+
223
+ private
224
+
225
+ # Instantiates a new host-key verification class, based on the value of
226
+ # the parameter. When true or nil, the default Lenient verifier is
227
+ # returned. If it is false, the Null verifier is returned, and if it is
228
+ # :very, the Strict verifier is returned. If it is :secure, the even more
229
+ # strict Secure verifier is returned. If the argument happens to respond
230
+ # to :verify, it is returned directly. Otherwise, an exception
231
+ # is raised.
232
+ def select_host_key_verifier(paranoid)
233
+ case paranoid
234
+ when true, nil
235
+ ::Net::SSH::Verifiers::Lenient.new
236
+ when false
237
+ ::Net::SSH::Verifiers::Null.new
238
+ when :very
239
+ ::Net::SSH::Verifiers::Strict.new
240
+ when :secure
241
+ ::Net::SSH::Verifiers::Secure.new
242
+ else
243
+ if paranoid.respond_to?(:verify)
244
+ paranoid
245
+ else
246
+ raise ArgumentError, "argument to :paranoid is not valid: #{paranoid.inspect}"
247
+ end
248
+ end
249
+ end
250
+ end
251
+ end; end
@@ -0,0 +1,3 @@
1
+ module ESSH
2
+ VERSION='0.0.1'
3
+ end
@@ -0,0 +1,124 @@
1
+ require 'net/ssh'
2
+
3
+ require 'evented-ssh/transport/packet_stream'
4
+ require 'evented-ssh/transport/algorithms'
5
+ require 'evented-ssh/transport/session'
6
+
7
+ require 'evented-ssh/connection/event_loop'
8
+ require 'evented-ssh/connection/channel'
9
+ require 'evented-ssh/connection/session'
10
+
11
+ module ESSH
12
+ VALID_OPTIONS = [
13
+ :auth_methods, :bind_address, :compression, :compression_level, :config,
14
+ :encryption, :forward_agent, :hmac, :host_key, :remote_user,
15
+ :keepalive, :keepalive_interval, :keepalive_maxcount, :kex, :keys, :key_data,
16
+ :languages, :logger, :paranoid, :password, :port, :proxy,
17
+ :rekey_blocks_limit,:rekey_limit, :rekey_packet_limit, :timeout, :verbose,
18
+ :known_hosts, :global_known_hosts_file, :user_known_hosts_file, :host_key_alias,
19
+ :host_name, :user, :properties, :passphrase, :keys_only, :max_pkt_size,
20
+ :max_win_size, :send_env, :use_agent, :number_of_password_prompts,
21
+ :append_all_supported_algorithms, :non_interactive, :password_prompt,
22
+ :agent_socket_factory, :minimum_dh_bits
23
+ ]
24
+
25
+ def self.start(host, user = nil, **options, &block)
26
+ invalid_options = options.keys - VALID_OPTIONS
27
+ if invalid_options.any?
28
+ raise ArgumentError, "invalid option(s): #{invalid_options.join(', ')}"
29
+ end
30
+
31
+ assign_defaults(options)
32
+ _sanitize_options(options)
33
+
34
+ options[:user] = user if user
35
+ options = configuration_for(host, options.fetch(:config, true)).merge(options)
36
+ host = options.fetch(:host_name, host)
37
+
38
+ if options[:non_interactive]
39
+ options[:number_of_password_prompts] = 0
40
+ end
41
+
42
+ if options[:verbose]
43
+ options[:logger].level = case options[:verbose]
44
+ when Integer then options[:verbose]
45
+ when :debug then Logger::DEBUG
46
+ when :info then Logger::INFO
47
+ when :warn then Logger::WARN
48
+ when :error then Logger::ERROR
49
+ when :fatal then Logger::FATAL
50
+ else raise ArgumentError, "can't convert #{options[:verbose].inspect} to any of the Logger level constants"
51
+ end
52
+ end
53
+
54
+ transport = Transport::Session.new(host, options)
55
+ auth = ::Net::SSH::Authentication::Session.new(transport, options)
56
+
57
+ user = options.fetch(:user, user) || Etc.getlogin
58
+ if auth.authenticate("ssh-connection", user, options[:password])
59
+ connection = ::Net::SSH::Connection::Session.new(transport, options)
60
+ if block_given?
61
+ begin
62
+ yield connection
63
+ ensure
64
+ connection.close unless connection.closed?
65
+ end
66
+ else
67
+ return connection
68
+ end
69
+ else
70
+ transport.close
71
+ raise AuthenticationFailed, "Authentication failed for user #{user}@#{host}"
72
+ end
73
+ rescue => e
74
+ transport.socket.__send__(:reject, e) if transport
75
+ raise
76
+ end
77
+
78
+ # Start using a promise
79
+ def self.p_start(host, user = nil, **options)
80
+ thread = ::Libuv.reactor
81
+ defer = thread.defer
82
+ thread.next_tick do
83
+ begin
84
+ connection = start(host, user, **options)
85
+ defer.resolve(connection)
86
+ rescue Exception => e
87
+ defer.reject(e)
88
+ end
89
+ end
90
+ defer.promise
91
+ end
92
+
93
+ def self.configuration_for(host, use_ssh_config)
94
+ files = case use_ssh_config
95
+ when true then ::Net::SSH::Config.expandable_default_files
96
+ when false, nil then return {}
97
+ else Array(use_ssh_config)
98
+ end
99
+
100
+ ::Net::SSH::Config.for(host, files)
101
+ end
102
+
103
+ def self.assign_defaults(options)
104
+ if !options[:logger]
105
+ options[:logger] = Logger.new(STDERR)
106
+ options[:logger].level = Logger::FATAL
107
+ end
108
+
109
+ options[:password_prompt] ||= ::Net::SSH::Prompt.default(options)
110
+
111
+ [:password, :passphrase].each do |key|
112
+ options.delete(key) if options.key?(key) && options[key].nil?
113
+ end
114
+ end
115
+
116
+ def self._sanitize_options(options)
117
+ invalid_option_values = [nil,[nil]]
118
+ unless (options.values & invalid_option_values).empty?
119
+ nil_options = options.select { |_k,v| invalid_option_values.include?(v) }.map(&:first)
120
+ Kernel.warn "#{caller_locations(2, 1)[0]}: Passing nil, or [nil] to Net::SSH.start is deprecated for keys: #{nil_options.join(', ')}"
121
+ end
122
+ end
123
+ private_class_method :_sanitize_options
124
+ end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: evented-ssh
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Stephen von Takach
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-07-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: net-ssh
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: ipaddress
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.8'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.8'
41
+ - !ruby/object:Gem::Dependency
42
+ name: libuv
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.1'
55
+ description: SSH on the Ruby platform using event driven IO
56
+ email:
57
+ - steve@aca.im
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - README.md
63
+ - lib/evented-ssh.rb
64
+ - lib/evented-ssh/connection/channel.rb
65
+ - lib/evented-ssh/connection/event_loop.rb
66
+ - lib/evented-ssh/connection/session.rb
67
+ - lib/evented-ssh/transport/algorithms.rb
68
+ - lib/evented-ssh/transport/packet_stream.rb
69
+ - lib/evented-ssh/transport/session.rb
70
+ - lib/evented-ssh/version.rb
71
+ homepage: http://github.com/acaprojects/evented-ssh
72
+ licenses:
73
+ - MIT
74
+ metadata: {}
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 1.3.6
89
+ requirements: []
90
+ rubyforge_project:
91
+ rubygems_version: 2.6.12
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: SSH on event driven IO
95
+ test_files: []