evented-ssh 0.0.1

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