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 +7 -0
- data/README.md +11 -0
- data/lib/evented-ssh/connection/channel.rb +33 -0
- data/lib/evented-ssh/connection/event_loop.rb +16 -0
- data/lib/evented-ssh/connection/session.rb +49 -0
- data/lib/evented-ssh/transport/algorithms.rb +443 -0
- data/lib/evented-ssh/transport/packet_stream.rb +322 -0
- data/lib/evented-ssh/transport/session.rb +251 -0
- data/lib/evented-ssh/version.rb +3 -0
- data/lib/evented-ssh.rb +124 -0
- metadata +95 -0
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
|
data/lib/evented-ssh.rb
ADDED
@@ -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: []
|