net-ssh 7.2.3 → 8.0.4
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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.dockerignore +3 -0
- data/.github/pull_request_template.md +1 -0
- data/.github/workflows/changelog.yml +18 -0
- data/.github/workflows/ci-with-docker.yml +20 -9
- data/.github/workflows/ci.yml +10 -20
- data/.github/workflows/rubocop.yml +3 -1
- data/.rubocop.yml +5 -1
- data/.rubocop_todo.yml +4 -8
- data/CHANGES.txt +122 -0
- data/CONTRIBUTING.md +34 -0
- data/Dockerfile +4 -1
- data/Dockerfile.openssl3 +1 -1
- data/Dockerfile.zlib_ng +29 -0
- data/Gemfile +2 -0
- data/README.md +74 -57
- data/Rakefile +30 -12
- data/appveyor.yml +4 -16
- data/changelog.d/README.md +1 -0
- data/docker-compose.yml +1 -9
- data/lib/net/ssh/authentication/agent.rb +35 -11
- data/lib/net/ssh/authentication/ed25519.rb +140 -37
- data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
- data/lib/net/ssh/authentication/key_manager.rb +42 -16
- data/lib/net/ssh/authentication/methods/abstract.rb +2 -8
- data/lib/net/ssh/authentication/methods/publickey.rb +3 -1
- data/lib/net/ssh/authentication/security_key.rb +71 -0
- data/lib/net/ssh/authentication/session.rb +29 -17
- data/lib/net/ssh/buffer.rb +6 -1
- data/lib/net/ssh/buffered_io.rb +40 -3
- data/lib/net/ssh/config.rb +30 -13
- data/lib/net/ssh/connection/channel.rb +49 -2
- data/lib/net/ssh/connection/session.rb +151 -13
- data/lib/net/ssh/errors.rb +24 -6
- data/lib/net/ssh/key_factory.rb +11 -4
- data/lib/net/ssh/known_hosts.rb +62 -8
- data/lib/net/ssh/multiplex/client.rb +162 -0
- data/lib/net/ssh/multiplex/control_path.rb +120 -0
- data/lib/net/ssh/multiplex/proxy_transport.rb +218 -0
- data/lib/net/ssh/multiplex.rb +126 -0
- data/lib/net/ssh/prompt.rb +3 -3
- data/lib/net/ssh/proxy/command.rb +28 -3
- data/lib/net/ssh/proxy/jump.rb +71 -8
- data/lib/net/ssh/service/forward.rb +44 -27
- data/lib/net/ssh/test/extensions.rb +3 -2
- data/lib/net/ssh/test/socket.rb +1 -1
- data/lib/net/ssh/test.rb +2 -1
- data/lib/net/ssh/transport/aes128_gcm.rb +40 -0
- data/lib/net/ssh/transport/aes256_gcm.rb +40 -0
- data/lib/net/ssh/transport/algorithms.rb +96 -34
- data/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +89 -16
- data/lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb +20 -1
- data/lib/net/ssh/transport/cipher_factory.rb +32 -8
- data/lib/net/ssh/transport/constants.rb +1 -0
- data/lib/net/ssh/transport/gcm_cipher.rb +228 -0
- data/lib/net/ssh/transport/hmac/abstract.rb +16 -0
- data/lib/net/ssh/transport/hmac/sha1_etm.rb +12 -0
- data/lib/net/ssh/transport/hmac.rb +2 -0
- data/lib/net/ssh/transport/kex/curve25519_sha256.rb +41 -9
- data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +14 -6
- data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +8 -0
- data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +45 -15
- data/lib/net/ssh/transport/openssl.rb +2 -1
- data/lib/net/ssh/transport/packet_stream.rb +19 -3
- data/lib/net/ssh/transport/server_version.rb +33 -13
- data/lib/net/ssh/transport/session.rb +61 -46
- data/lib/net/ssh/transport/state.rb +1 -1
- data/lib/net/ssh/verifiers/always.rb +43 -8
- data/lib/net/ssh/version.rb +3 -3
- data/lib/net/ssh.rb +148 -63
- data/net-ssh-public_cert.pem +7 -7
- data/net-ssh.gemspec +6 -8
- data.tar.gz.sig +0 -0
- metadata +64 -46
- metadata.gz.sig +0 -0
- data/Gemfile.noed25519 +0 -12
- data/Gemfile.norbnacl +0 -12
data/lib/net/ssh/buffered_io.rb
CHANGED
|
@@ -166,6 +166,11 @@ module Net
|
|
|
166
166
|
# http://github.com/net-ssh/net-ssh/tree/portfwfix
|
|
167
167
|
#
|
|
168
168
|
module ForwardedBufferedIo
|
|
169
|
+
def self.extended(object) # :nodoc:
|
|
170
|
+
object.instance_variable_set(:@on_sent, nil)
|
|
171
|
+
object.instance_variable_set(:@shutdown_write_when_drained, false)
|
|
172
|
+
end
|
|
173
|
+
|
|
169
174
|
def fill(n = 8192)
|
|
170
175
|
begin
|
|
171
176
|
super(n)
|
|
@@ -182,11 +187,25 @@ module Net
|
|
|
182
187
|
end
|
|
183
188
|
end
|
|
184
189
|
|
|
190
|
+
# Internal and experimental: registers a block called with the number of
|
|
191
|
+
# bytes each #send_pending wrote to the socket.
|
|
192
|
+
def on_sent(&block) # :nodoc:
|
|
193
|
+
@on_sent = block
|
|
194
|
+
end
|
|
195
|
+
|
|
185
196
|
def send_pending
|
|
186
197
|
begin
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
198
|
+
pending = output.length
|
|
199
|
+
result = super
|
|
200
|
+
@on_sent.call(pending - output.length) if @on_sent && output.length < pending
|
|
201
|
+
shutdown_write_if_drained
|
|
202
|
+
result
|
|
203
|
+
rescue Errno::EPIPE, Errno::ECONNRESET => e
|
|
204
|
+
# Like OpenSSH's chan_write_failed: the peer is gone, so drop what it
|
|
205
|
+
# will never read and close, letting the forwarded channel close too.
|
|
206
|
+
debug { "write failed => discarding #{output.length} bytes and closing:#{e}" }
|
|
207
|
+
output.clear!
|
|
208
|
+
close
|
|
190
209
|
return 0
|
|
191
210
|
rescue IOError => e
|
|
192
211
|
if e.message =~ /closed/ then
|
|
@@ -197,6 +216,24 @@ module Net
|
|
|
197
216
|
end
|
|
198
217
|
end
|
|
199
218
|
end
|
|
219
|
+
|
|
220
|
+
# Internal: shuts down the write side once pending output has been sent,
|
|
221
|
+
# as OpenSSH does on CHANNEL_EOF (chan_rcvd_ieof, chan_obuf_empty).
|
|
222
|
+
def shutdown_write_when_drained # :nodoc:
|
|
223
|
+
@shutdown_write_when_drained = true
|
|
224
|
+
shutdown_write_if_drained
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
private
|
|
228
|
+
|
|
229
|
+
def shutdown_write_if_drained
|
|
230
|
+
return unless @shutdown_write_when_drained && output.empty? && !closed?
|
|
231
|
+
|
|
232
|
+
@shutdown_write_when_drained = false
|
|
233
|
+
shutdown(Socket::SHUT_WR)
|
|
234
|
+
rescue Errno::EPIPE, Errno::ENOTCONN => e
|
|
235
|
+
debug { "shutdown failed => shallowing exception:#{e}" }
|
|
236
|
+
end
|
|
200
237
|
end
|
|
201
238
|
end
|
|
202
239
|
end
|
data/lib/net/ssh/config.rb
CHANGED
|
@@ -15,6 +15,8 @@ module Net
|
|
|
15
15
|
# * Compression => :compression
|
|
16
16
|
# * CompressionLevel => :compression_level
|
|
17
17
|
# * ConnectTimeout => maps to the :timeout option
|
|
18
|
+
# * ControlPath => :control_path, used only when Net::SSH.start is given
|
|
19
|
+
# control_master: :auto; ControlMaster itself is not read
|
|
18
20
|
# * ForwardAgent => :forward_agent
|
|
19
21
|
# * GlobalKnownHostsFile => :global_known_hosts_file
|
|
20
22
|
# * HostBasedAuthentication => maps to the :auth_methods option
|
|
@@ -24,6 +26,7 @@ module Net
|
|
|
24
26
|
# * IdentityFile => maps to the :keys option
|
|
25
27
|
# * IdentityAgent => :identity_agent
|
|
26
28
|
# * IdentitiesOnly => :keys_only
|
|
29
|
+
# * KexAlgorithms => maps to the :kex option
|
|
27
30
|
# * CheckHostIP => :check_host_ip
|
|
28
31
|
# * Macs => maps to the :hmac option
|
|
29
32
|
# * PasswordAuthentication => maps to the :auth_methods option password
|
|
@@ -31,6 +34,7 @@ module Net
|
|
|
31
34
|
# * PreferredAuthentications => maps to the :auth_methods option
|
|
32
35
|
# * ProxyCommand => maps to the :proxy option
|
|
33
36
|
# * ProxyJump => maps to the :proxy option
|
|
37
|
+
# * PubkeyAcceptedAlgorithms (or PubkeyAcceptedKeyTypes) => maps to :pubkey_algorithms
|
|
34
38
|
# * PubKeyAuthentication => maps to the :auth_methods option
|
|
35
39
|
# * RekeyLimit => :rekey_limit
|
|
36
40
|
# * StrictHostKeyChecking => :verify_host_key
|
|
@@ -88,16 +92,14 @@ module Net
|
|
|
88
92
|
IO.foreach(file) do |line|
|
|
89
93
|
next if line =~ /^\s*(?:#.*)?$/
|
|
90
94
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
else
|
|
94
|
-
key, value = line.strip.split(/\s+/, 2)
|
|
95
|
-
end
|
|
95
|
+
# As in OpenSSH, the keyword ends at the first whitespace or "=".
|
|
96
|
+
key, value = line.strip.split(/\s*=\s*|\s+/, 2)
|
|
96
97
|
|
|
97
98
|
# silently ignore malformed entries
|
|
98
99
|
next if value.nil?
|
|
99
100
|
|
|
100
101
|
key.downcase!
|
|
102
|
+
value = uncomment(value)
|
|
101
103
|
value = unquote(value)
|
|
102
104
|
|
|
103
105
|
value = case value.strip
|
|
@@ -152,9 +154,11 @@ module Net
|
|
|
152
154
|
end
|
|
153
155
|
|
|
154
156
|
# ProxyCommand and ProxyJump override each other so they need to be tracked togeather
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
[globals, settings].each do |hash|
|
|
158
|
+
%w[proxyjump proxycommand].each do |proxy_key|
|
|
159
|
+
if (proxy_value = hash.delete(proxy_key))
|
|
160
|
+
hash['proxy'] ||= [proxy_key, proxy_value]
|
|
161
|
+
end
|
|
158
162
|
end
|
|
159
163
|
end
|
|
160
164
|
end
|
|
@@ -218,6 +222,7 @@ module Net
|
|
|
218
222
|
bindaddress: :bind_address,
|
|
219
223
|
compression: :compression,
|
|
220
224
|
compressionlevel: :compression_level,
|
|
225
|
+
controlpath: :control_path,
|
|
221
226
|
certificatefile: :keycerts,
|
|
222
227
|
connecttimeout: :timeout,
|
|
223
228
|
forwardagent: :forward_agent,
|
|
@@ -229,10 +234,9 @@ module Net
|
|
|
229
234
|
fingerprinthash: :fingerprint_hash,
|
|
230
235
|
port: :port,
|
|
231
236
|
user: :user,
|
|
232
|
-
userknownhostsfile: :user_known_hosts_file,
|
|
233
237
|
checkhostip: :check_host_ip
|
|
234
238
|
}.freeze
|
|
235
|
-
def translate_config_key(hash, key, value, settings)
|
|
239
|
+
def translate_config_key(hash, key, value, settings) # rubocop:disable Metrics/AbcSize
|
|
236
240
|
case key
|
|
237
241
|
when :stricthostkeychecking
|
|
238
242
|
hash[:verify_host_key] = translate_verify_host_key(value)
|
|
@@ -248,6 +252,8 @@ module Net
|
|
|
248
252
|
hash[:host_key] = value.split(/,/)
|
|
249
253
|
when :hostname
|
|
250
254
|
hash[:host_name] = value.gsub(/%h/, settings['host'])
|
|
255
|
+
when :kexalgorithms
|
|
256
|
+
hash[:kex] = value.split(/,/)
|
|
251
257
|
when :macs
|
|
252
258
|
hash[:hmac] = value.split(/,/)
|
|
253
259
|
when :serveralivecountmax
|
|
@@ -278,6 +284,8 @@ module Net
|
|
|
278
284
|
if (proxy = setup_proxy(*value))
|
|
279
285
|
hash[:proxy] = proxy
|
|
280
286
|
end
|
|
287
|
+
when :pubkeyacceptedalgorithms, :pubkeyacceptedkeytypes
|
|
288
|
+
hash[:pubkey_algorithms] = value.split(/,/)
|
|
281
289
|
when :pubkeyauthentication
|
|
282
290
|
if value
|
|
283
291
|
(hash[:auth_methods] << 'publickey').uniq!
|
|
@@ -293,6 +301,8 @@ module Net
|
|
|
293
301
|
hash[:set_env] = Shellwords.split(value.to_s).map { |e| e.split '=', 2 }.to_h
|
|
294
302
|
when :numberofpasswordprompts
|
|
295
303
|
hash[:number_of_password_prompts] = value.to_i
|
|
304
|
+
when :userknownhostsfile
|
|
305
|
+
hash[:user_known_hosts_file] = value.split(/\s+/)
|
|
296
306
|
when *TRANSLATE_CONFIG_KEY_RENAME_MAP.keys
|
|
297
307
|
hash[TRANSLATE_CONFIG_KEY_RENAME_MAP[key]] = value
|
|
298
308
|
end
|
|
@@ -301,13 +311,15 @@ module Net
|
|
|
301
311
|
def setup_proxy(type, value)
|
|
302
312
|
case type
|
|
303
313
|
when 'proxycommand'
|
|
304
|
-
if value !~ /^none$/
|
|
314
|
+
if value !~ /^none$/i
|
|
305
315
|
require 'net/ssh/proxy/command'
|
|
306
316
|
Net::SSH::Proxy::Command.new(value)
|
|
307
317
|
end
|
|
308
318
|
when 'proxyjump'
|
|
309
|
-
|
|
310
|
-
|
|
319
|
+
if value !~ /^none$/i
|
|
320
|
+
require 'net/ssh/proxy/jump'
|
|
321
|
+
Net::SSH::Proxy::Jump.new(value)
|
|
322
|
+
end
|
|
311
323
|
end
|
|
312
324
|
end
|
|
313
325
|
|
|
@@ -397,6 +409,11 @@ module Net
|
|
|
397
409
|
!condition_matches.empty? && condition_matches.all?
|
|
398
410
|
end
|
|
399
411
|
|
|
412
|
+
# As in OpenSSH's argv_split, "#" starts a comment only at the start of a token and outside quotes.
|
|
413
|
+
def uncomment(string)
|
|
414
|
+
string[/\A(?:\s*(?!#)(?:\\.|"[^"]*"?|'[^']*'?|[^\s"'])+)*/]
|
|
415
|
+
end
|
|
416
|
+
|
|
400
417
|
def unquote(string)
|
|
401
418
|
string =~ /^"(.*)"$/ ? Regexp.last_match(1) : string
|
|
402
419
|
end
|
|
@@ -55,6 +55,9 @@ module Net
|
|
|
55
55
|
include Loggable
|
|
56
56
|
include Constants
|
|
57
57
|
|
|
58
|
+
# OpenSSH's CHAN_SES_WINDOW_DEFAULT; some servers (e.g. Mikrotik) truncate output with smaller windows (#726).
|
|
59
|
+
DEFAULT_MAX_WIN_SIZE = 0x200000
|
|
60
|
+
|
|
58
61
|
# The local id for this channel, assigned by the Net::SSH::Connection::Session instance.
|
|
59
62
|
attr_reader :local_id
|
|
60
63
|
|
|
@@ -109,7 +112,7 @@ module Net
|
|
|
109
112
|
# that time (see #do_open_confirmation).
|
|
110
113
|
#
|
|
111
114
|
# This also sets the default maximum packet size and maximum window size.
|
|
112
|
-
def initialize(connection, type, local_id, max_pkt_size = 0x8000, max_win_size =
|
|
115
|
+
def initialize(connection, type, local_id, max_pkt_size = 0x8000, max_win_size = DEFAULT_MAX_WIN_SIZE, &on_confirm_open)
|
|
113
116
|
self.logger = connection.logger
|
|
114
117
|
|
|
115
118
|
@connection = connection
|
|
@@ -118,6 +121,8 @@ module Net
|
|
|
118
121
|
|
|
119
122
|
@local_maximum_packet_size = max_pkt_size
|
|
120
123
|
@local_window_size = @local_maximum_window_size = max_win_size
|
|
124
|
+
@local_consumed = 0
|
|
125
|
+
@grant_window_on_consume = false
|
|
121
126
|
|
|
122
127
|
@on_confirm_open = on_confirm_open
|
|
123
128
|
|
|
@@ -254,6 +259,26 @@ module Net
|
|
|
254
259
|
output.append(data.to_s)
|
|
255
260
|
end
|
|
256
261
|
|
|
262
|
+
# Internal and experimental, used by port forwarding: from now on window
|
|
263
|
+
# is granted only for bytes reported via #consumed, so a slow consumer
|
|
264
|
+
# stalls the remote sender instead of buffering everything it sends.
|
|
265
|
+
# Must be called before any data is received.
|
|
266
|
+
def grant_window_on_consume! # :nodoc:
|
|
267
|
+
raise ArgumentError, "grant_window_on_consume! must be called before any data is received" if local_window_size != local_maximum_window_size
|
|
268
|
+
|
|
269
|
+
@grant_window_on_consume = true
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Internal and experimental: reports that +bytes+ of received data have
|
|
273
|
+
# been processed, letting the remote end send that much more.
|
|
274
|
+
def consumed(bytes) # :nodoc:
|
|
275
|
+
raise ArgumentError, "consumed requires grant_window_on_consume!" unless @grant_window_on_consume
|
|
276
|
+
raise ArgumentError, "consumed bytes must not be negative" if bytes.negative?
|
|
277
|
+
|
|
278
|
+
@local_consumed += bytes
|
|
279
|
+
adjust_window_for_consumed
|
|
280
|
+
end
|
|
281
|
+
|
|
257
282
|
# Returns true if the channel exists in the channel list of the session,
|
|
258
283
|
# and false otherwise. This can be used to determine whether a channel has
|
|
259
284
|
# been closed or not.
|
|
@@ -598,7 +623,12 @@ module Net
|
|
|
598
623
|
# type, and data.
|
|
599
624
|
def do_extended_data(type, data)
|
|
600
625
|
update_local_window_size(data.length)
|
|
601
|
-
|
|
626
|
+
if @on_extended_data
|
|
627
|
+
@on_extended_data.call(self, type, data)
|
|
628
|
+
elsif @grant_window_on_consume
|
|
629
|
+
# Nothing will ever consume it, so don't let it shrink the window.
|
|
630
|
+
consumed(data.length)
|
|
631
|
+
end
|
|
602
632
|
end
|
|
603
633
|
|
|
604
634
|
# Invokes the #on_eof callback when the server indicates that no
|
|
@@ -651,6 +681,8 @@ module Net
|
|
|
651
681
|
# server telling it that the window size has grown.
|
|
652
682
|
def update_local_window_size(size)
|
|
653
683
|
@local_window_size -= size
|
|
684
|
+
return adjust_window_for_consumed if @grant_window_on_consume
|
|
685
|
+
|
|
654
686
|
if local_window_size < local_maximum_window_size / 2
|
|
655
687
|
connection.send_message(
|
|
656
688
|
Buffer.from(:byte, CHANNEL_WINDOW_ADJUST, :long, remote_id, :long, LOCAL_WINDOW_SIZE_INCREMENT)
|
|
@@ -663,6 +695,21 @@ module Net
|
|
|
663
695
|
end
|
|
664
696
|
end
|
|
665
697
|
|
|
698
|
+
# Mirrors OpenSSH's channel_check_window: once the window is below half,
|
|
699
|
+
# give back what was consumed, growing the maximum window the same way
|
|
700
|
+
# #update_local_window_size does.
|
|
701
|
+
def adjust_window_for_consumed
|
|
702
|
+
return if @local_consumed.zero? || local_window_size >= local_maximum_window_size / 2
|
|
703
|
+
return if remote_id.nil? || local_closed? || remote_closed?
|
|
704
|
+
|
|
705
|
+
growth = (GOOD_LOCAL_MAXIUMUM_WINDOW_SIZE - local_maximum_window_size).clamp(0, LOCAL_WINDOW_SIZE_INCREMENT)
|
|
706
|
+
adjust = @local_consumed + growth
|
|
707
|
+
connection.send_message(Buffer.from(:byte, CHANNEL_WINDOW_ADJUST, :long, remote_id, :long, adjust))
|
|
708
|
+
@local_window_size += adjust
|
|
709
|
+
@local_maximum_window_size += growth
|
|
710
|
+
@local_consumed = 0
|
|
711
|
+
end
|
|
712
|
+
|
|
666
713
|
# Gets an +Array+ of local environment variables in the remote process'
|
|
667
714
|
# environment.
|
|
668
715
|
# A variable name can either be described by a +Regexp+ or +String+.
|
|
@@ -24,7 +24,7 @@ module Net
|
|
|
24
24
|
# # 'ssh' is an instance of Net::SSH::Connection::Session
|
|
25
25
|
# ssh.exec! "/etc/init.d/some_process start"
|
|
26
26
|
# end
|
|
27
|
-
class Session
|
|
27
|
+
class Session # rubocop:disable Metrics/ClassLength
|
|
28
28
|
include Loggable
|
|
29
29
|
include Constants
|
|
30
30
|
|
|
@@ -53,6 +53,10 @@ module Net
|
|
|
53
53
|
# The list of callbacks for pending requests. See #send_global_request.
|
|
54
54
|
attr_reader :pending_requests # :nodoc:
|
|
55
55
|
|
|
56
|
+
# A "session" channel opened ahead of time by :preopen_channels, and
|
|
57
|
+
# the server's confirmation for it once that has arrived.
|
|
58
|
+
SpareChannel = Struct.new(:local_id, :remote_id, :window_size, :packet_size) # :nodoc:
|
|
59
|
+
|
|
56
60
|
class NilChannel
|
|
57
61
|
def initialize(session)
|
|
58
62
|
@session = session
|
|
@@ -74,13 +78,20 @@ module Net
|
|
|
74
78
|
@channel_id_counter = -1
|
|
75
79
|
@channels = Hash.new(NilChannel.new(self))
|
|
76
80
|
@listeners = { transport.socket => nil }
|
|
81
|
+
@read_paused = {}
|
|
82
|
+
@draining = {}
|
|
77
83
|
@pending_requests = []
|
|
78
84
|
@channel_open_handlers = {}
|
|
79
85
|
@on_global_request = {}
|
|
80
86
|
@properties = (options[:properties] || {}).dup
|
|
81
87
|
|
|
82
88
|
@max_pkt_size = (options.key?(:max_pkt_size) ? options[:max_pkt_size] : 0x8000)
|
|
83
|
-
@max_win_size = (options.key?(:max_win_size) ? options[:max_win_size] :
|
|
89
|
+
@max_win_size = (options.key?(:max_win_size) ? options[:max_win_size] : Channel::DEFAULT_MAX_WIN_SIZE)
|
|
90
|
+
|
|
91
|
+
@preopen_channels = options.fetch(:preopen_channels, false)
|
|
92
|
+
@spare_channel = nil
|
|
93
|
+
@spare_wanted = false
|
|
94
|
+
@preopened = []
|
|
84
95
|
|
|
85
96
|
@keepalive = Keepalive.new(self)
|
|
86
97
|
|
|
@@ -119,13 +130,21 @@ module Net
|
|
|
119
130
|
# successfully closed, and then closes the underlying transport layer
|
|
120
131
|
# connection.
|
|
121
132
|
def close
|
|
133
|
+
stop_preopening
|
|
122
134
|
info { "closing remaining channels (#{channels.length} open)" }
|
|
123
|
-
|
|
135
|
+
|
|
136
|
+
channels.each do |id, channel|
|
|
137
|
+
channel.eof!
|
|
138
|
+
channel.close
|
|
139
|
+
end
|
|
140
|
+
|
|
124
141
|
begin
|
|
125
142
|
loop(0.1) { channels.any? }
|
|
126
143
|
rescue Net::SSH::Disconnect
|
|
127
144
|
raise unless channels.empty?
|
|
128
145
|
end
|
|
146
|
+
@draining.each_key(&:close)
|
|
147
|
+
@draining.clear
|
|
129
148
|
transport.close
|
|
130
149
|
end
|
|
131
150
|
|
|
@@ -134,6 +153,7 @@ module Net
|
|
|
134
153
|
# when the connection needs to close but you don't know the status of the
|
|
135
154
|
# underlying protocol's state).
|
|
136
155
|
def shutdown!
|
|
156
|
+
stop_preopening
|
|
137
157
|
transport.shutdown!
|
|
138
158
|
end
|
|
139
159
|
|
|
@@ -146,10 +166,13 @@ module Net
|
|
|
146
166
|
# a +true+ value for +include_invisible+, then those will be counted.
|
|
147
167
|
#
|
|
148
168
|
# This can be useful for determining whether the event loop should continue
|
|
149
|
-
# to be run.
|
|
169
|
+
# to be run. A forwarded socket still writing out data that arrived before
|
|
170
|
+
# its channel closed also counts as busy.
|
|
150
171
|
#
|
|
151
172
|
# ssh.loop { ssh.busy? }
|
|
152
173
|
def busy?(include_invisible = false)
|
|
174
|
+
return true if @draining.any?
|
|
175
|
+
|
|
153
176
|
if include_invisible
|
|
154
177
|
channels.any?
|
|
155
178
|
else
|
|
@@ -246,8 +269,10 @@ module Net
|
|
|
246
269
|
# Called by event loop to process available data before going to
|
|
247
270
|
# event multiplexing
|
|
248
271
|
def ev_preprocess(&block)
|
|
272
|
+
confirm_preopened_channels
|
|
249
273
|
dispatch_incoming_packets(raise_disconnect_errors: false)
|
|
250
274
|
each_channel { |id, channel| channel.process unless channel.local_closed? }
|
|
275
|
+
open_spare_channel
|
|
251
276
|
end
|
|
252
277
|
|
|
253
278
|
# Returns the file descriptors the event loop should wait for read/write events,
|
|
@@ -255,7 +280,7 @@ module Net
|
|
|
255
280
|
def ev_do_calculate_rw_wait(wait)
|
|
256
281
|
r = listeners.keys
|
|
257
282
|
w = r.select { |w2| w2.respond_to?(:pending_write?) && w2.pending_write? }
|
|
258
|
-
[r, w, io_select_wait(wait)]
|
|
283
|
+
[r.reject { |io| @read_paused.key?(io) }, w, io_select_wait(wait)]
|
|
259
284
|
end
|
|
260
285
|
|
|
261
286
|
# This is called internally as part of #process.
|
|
@@ -281,6 +306,7 @@ module Net
|
|
|
281
306
|
|
|
282
307
|
Array(writers).each do |writer|
|
|
283
308
|
writer.send_pending
|
|
309
|
+
close_if_drained(writer) if @draining.key?(writer)
|
|
284
310
|
end
|
|
285
311
|
end
|
|
286
312
|
|
|
@@ -335,14 +361,20 @@ module Net
|
|
|
335
361
|
# end
|
|
336
362
|
#
|
|
337
363
|
# channel.wait
|
|
364
|
+
#
|
|
365
|
+
# With the :preopen_channels option a plain "session" channel reuses
|
|
366
|
+
# the spare opened ahead of time, if there is one. Its callback still
|
|
367
|
+
# runs from the event loop, as if the server had just confirmed it.
|
|
338
368
|
def open_channel(type = "session", *extra, &on_confirm)
|
|
339
|
-
|
|
369
|
+
spare = take_spare_channel if type == "session" && extra.empty?
|
|
370
|
+
local_id = spare ? spare.local_id : get_next_channel_id
|
|
340
371
|
|
|
341
372
|
channel = Channel.new(self, type, local_id, @max_pkt_size, @max_win_size, &on_confirm)
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
373
|
+
if spare
|
|
374
|
+
@preopened << [channel, spare] if spare.remote_id
|
|
375
|
+
else
|
|
376
|
+
send_channel_open(type, channel.local_id, channel.local_maximum_window_size, channel.local_maximum_packet_size, *extra)
|
|
377
|
+
end
|
|
346
378
|
|
|
347
379
|
channels[local_id] = channel
|
|
348
380
|
end
|
|
@@ -416,7 +448,10 @@ module Net
|
|
|
416
448
|
#
|
|
417
449
|
# matches = ssh.exec!("grep something /some/files")
|
|
418
450
|
#
|
|
419
|
-
#
|
|
451
|
+
# With a block, output goes to the block and an empty string is returned,
|
|
452
|
+
# unless the block sets <tt>ch[:result]</tt>, which is returned instead.
|
|
453
|
+
# Either way the returned string has an exitstatus method to query the
|
|
454
|
+
# command's exit status.
|
|
420
455
|
def exec!(command, status: nil, &block)
|
|
421
456
|
block_or_concat = block || Proc.new do |ch, type, data|
|
|
422
457
|
ch[:result] ||= String.new
|
|
@@ -427,10 +462,10 @@ module Net
|
|
|
427
462
|
channel = exec(command, status: status, &block_or_concat)
|
|
428
463
|
channel.wait
|
|
429
464
|
|
|
430
|
-
channel[:result] ||= String.new unless block
|
|
431
465
|
channel[:result] &&= channel[:result].force_encoding("UTF-8") unless block
|
|
466
|
+
channel[:result] ||= String.new(encoding: Encoding::UTF_8)
|
|
432
467
|
|
|
433
|
-
StringWithExitstatus.new(channel[:result], status[:exit_code])
|
|
468
|
+
StringWithExitstatus.new(channel[:result], status[:exit_code])
|
|
434
469
|
end
|
|
435
470
|
|
|
436
471
|
# Enqueues a message to be sent to the server as soon as the socket is
|
|
@@ -485,9 +520,36 @@ module Net
|
|
|
485
520
|
# Removes the given io object from the listeners collection, so that the
|
|
486
521
|
# event loop will no longer monitor it.
|
|
487
522
|
def stop_listening_to(io)
|
|
523
|
+
@read_paused.delete(io)
|
|
524
|
+
@draining.delete(io)
|
|
488
525
|
listeners.delete(io)
|
|
489
526
|
end
|
|
490
527
|
|
|
528
|
+
# Internal and experimental, used by port forwarding: stops the event
|
|
529
|
+
# loop from reading +io+ while it keeps flushing its pending output.
|
|
530
|
+
def pause_reading(io) # :nodoc:
|
|
531
|
+
raise ArgumentError, "the transport socket can't be paused" if io.equal?(transport.socket)
|
|
532
|
+
|
|
533
|
+
@read_paused[io] = true if listeners.key?(io)
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def resume_reading(io) # :nodoc:
|
|
537
|
+
@read_paused.delete(io)
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def reading_paused?(io) # :nodoc:
|
|
541
|
+
@read_paused.key?(io)
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# Internal and experimental, used by port forwarding: stops reading +io+
|
|
545
|
+
# but keeps flushing its pending output, then closes it and stops
|
|
546
|
+
# listening, as OpenSSH's chan_rcvd_oclose does.
|
|
547
|
+
def close_when_drained(io) # :nodoc:
|
|
548
|
+
pause_reading(io)
|
|
549
|
+
@draining[io] = true
|
|
550
|
+
close_if_drained(io)
|
|
551
|
+
end
|
|
552
|
+
|
|
491
553
|
# Returns a reference to the Net::SSH::Service::Forward service, which can
|
|
492
554
|
# be used for forwarding ports over SSH.
|
|
493
555
|
def forward
|
|
@@ -560,11 +622,20 @@ module Net
|
|
|
560
622
|
end
|
|
561
623
|
|
|
562
624
|
def force_channel_cleanup_on_close
|
|
625
|
+
stop_preopening
|
|
626
|
+
@preopened.clear
|
|
563
627
|
channels.each do |id, channel|
|
|
564
628
|
channel_closed(channel)
|
|
565
629
|
end
|
|
566
630
|
end
|
|
567
631
|
|
|
632
|
+
def close_if_drained(io)
|
|
633
|
+
return if io.pending_write? && !io.closed? && !closed? && listeners.key?(io)
|
|
634
|
+
|
|
635
|
+
io.close unless io.closed?
|
|
636
|
+
stop_listening_to(io)
|
|
637
|
+
end
|
|
638
|
+
|
|
568
639
|
def channel_closed(channel)
|
|
569
640
|
channel.remote_closed!
|
|
570
641
|
channel.close
|
|
@@ -644,18 +715,37 @@ module Net
|
|
|
644
715
|
|
|
645
716
|
def channel_open_confirmation(packet)
|
|
646
717
|
info { "channel_open_confirmation: #{packet[:local_id]} #{packet[:remote_id]} #{packet[:window_size]} #{packet[:packet_size]}" }
|
|
718
|
+
if (spare = spare_channel(packet))
|
|
719
|
+
spare.remote_id = packet[:remote_id]
|
|
720
|
+
spare.window_size = packet[:window_size]
|
|
721
|
+
spare.packet_size = packet[:packet_size]
|
|
722
|
+
return
|
|
723
|
+
end
|
|
724
|
+
|
|
647
725
|
channel = channels[packet[:local_id]]
|
|
648
726
|
channel.do_open_confirmation(packet[:remote_id], packet[:window_size], packet[:packet_size])
|
|
649
727
|
end
|
|
650
728
|
|
|
651
729
|
def channel_open_failure(packet)
|
|
652
730
|
error { "channel_open_failed: #{packet[:local_id]} #{packet[:reason_code]} #{packet[:description]}" }
|
|
731
|
+
if spare_channel(packet)
|
|
732
|
+
info { "preopen_channels: server refused the spare channel, no longer pre-opening" }
|
|
733
|
+
stop_preopening
|
|
734
|
+
@spare_channel = nil
|
|
735
|
+
return
|
|
736
|
+
end
|
|
737
|
+
|
|
653
738
|
channel = channels.delete(packet[:local_id])
|
|
654
739
|
channel.do_open_failed(packet[:reason_code], packet[:description])
|
|
655
740
|
end
|
|
656
741
|
|
|
657
742
|
def channel_window_adjust(packet)
|
|
658
743
|
info { "channel_window_adjust: #{packet[:local_id]} +#{packet[:extra_bytes]}" }
|
|
744
|
+
if (spare = spare_channel(packet))
|
|
745
|
+
spare.window_size += packet[:extra_bytes]
|
|
746
|
+
return
|
|
747
|
+
end
|
|
748
|
+
|
|
659
749
|
channels[packet[:local_id]].do_window_adjust(packet[:extra_bytes])
|
|
660
750
|
end
|
|
661
751
|
|
|
@@ -676,11 +766,20 @@ module Net
|
|
|
676
766
|
|
|
677
767
|
def channel_eof(packet)
|
|
678
768
|
info { "channel_eof: #{packet[:local_id]}" }
|
|
769
|
+
return if spare_channel(packet)
|
|
770
|
+
|
|
679
771
|
channels[packet[:local_id]].do_eof
|
|
680
772
|
end
|
|
681
773
|
|
|
682
774
|
def channel_close(packet)
|
|
683
775
|
info { "channel_close: #{packet[:local_id]}" }
|
|
776
|
+
if (spare = spare_channel(packet))
|
|
777
|
+
send_message(Buffer.from(:byte, CHANNEL_CLOSE, :long, spare.remote_id))
|
|
778
|
+
@spare_channel = nil
|
|
779
|
+
# Wait for the next plain open_channel before opening another, so a server closing spares can't make us loop.
|
|
780
|
+
@spare_wanted = false
|
|
781
|
+
return
|
|
782
|
+
end
|
|
684
783
|
|
|
685
784
|
channel = channels[packet[:local_id]]
|
|
686
785
|
channel_closed(channel)
|
|
@@ -696,6 +795,45 @@ module Net
|
|
|
696
795
|
channels[packet[:local_id]].do_failure
|
|
697
796
|
end
|
|
698
797
|
|
|
798
|
+
def send_channel_open(type, local_id, window_size, packet_size, *extra)
|
|
799
|
+
send_message(Buffer.from(:byte, CHANNEL_OPEN, :string, type, :long, local_id,
|
|
800
|
+
:long, window_size, :long, packet_size, *extra))
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
def take_spare_channel
|
|
804
|
+
return unless @preopen_channels
|
|
805
|
+
|
|
806
|
+
@spare_wanted = true
|
|
807
|
+
spare = @spare_channel
|
|
808
|
+
@spare_channel = nil
|
|
809
|
+
spare
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
def open_spare_channel
|
|
813
|
+
return unless @spare_wanted && @preopen_channels && @spare_channel.nil? && !closed?
|
|
814
|
+
|
|
815
|
+
@spare_channel = SpareChannel.new(get_next_channel_id)
|
|
816
|
+
info { "preopen_channels: opening spare channel #{@spare_channel.local_id}" }
|
|
817
|
+
send_channel_open("session", @spare_channel.local_id, @max_win_size, @max_pkt_size)
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
# An unused spare is never closed, as that would cost round trips just before the transport goes away;
|
|
821
|
+
# it stays in @spare_channel to absorb the server's late replies for it.
|
|
822
|
+
def stop_preopening
|
|
823
|
+
@preopen_channels = false
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
def confirm_preopened_channels
|
|
827
|
+
until @preopened.empty?
|
|
828
|
+
channel, spare = @preopened.shift
|
|
829
|
+
channel.do_open_confirmation(spare.remote_id, spare.window_size, spare.packet_size)
|
|
830
|
+
end
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
def spare_channel(packet)
|
|
834
|
+
@spare_channel if @spare_channel && @spare_channel.local_id == packet[:local_id]
|
|
835
|
+
end
|
|
836
|
+
|
|
699
837
|
def io_select_wait(wait)
|
|
700
838
|
[wait, max_select_wait_time].compact.min
|
|
701
839
|
end
|
data/lib/net/ssh/errors.rb
CHANGED
|
@@ -19,6 +19,16 @@ module Net
|
|
|
19
19
|
# timeouted unexpectedly.
|
|
20
20
|
class Timeout < Disconnect; end
|
|
21
21
|
|
|
22
|
+
# This exception is raised when a key blob has a key type that Net::SSH
|
|
23
|
+
# cannot parse.
|
|
24
|
+
class UnsupportedKeyType < Net::SSH::Exception; end
|
|
25
|
+
|
|
26
|
+
# Raised when a session connected through an OpenSSH ControlMaster (see the
|
|
27
|
+
# :control_master option of Net::SSH.start) is asked for something the
|
|
28
|
+
# master's proxy mode cannot carry, such as remote port forwarding, agent
|
|
29
|
+
# or X11 forwarding, or other global requests.
|
|
30
|
+
class NotSupportedOverControlMaster < Net::SSH::Exception; end
|
|
31
|
+
|
|
22
32
|
# This exception is primarily used internally, but if you have a channel
|
|
23
33
|
# request handler (see Net::SSH::Connection::Channel#on_request) that you
|
|
24
34
|
# want to fail in such a way that the server knows it failed, you can
|
|
@@ -62,19 +72,21 @@ module Net
|
|
|
62
72
|
@data && @data[:fingerprint]
|
|
63
73
|
end
|
|
64
74
|
|
|
65
|
-
# Returns the host name
|
|
75
|
+
# Returns the host name that was connected to.
|
|
66
76
|
def host
|
|
67
|
-
|
|
77
|
+
session&.host
|
|
68
78
|
end
|
|
69
79
|
|
|
70
|
-
# Returns the port number
|
|
80
|
+
# Returns the port number that was connected to.
|
|
71
81
|
def port
|
|
72
|
-
|
|
82
|
+
session&.port
|
|
73
83
|
end
|
|
74
84
|
|
|
75
|
-
# Returns the IP address of the remote host, as reported by the socket
|
|
85
|
+
# Returns the IP address of the remote host, as reported by the socket,
|
|
86
|
+
# or nil when connected through a ProxyCommand.
|
|
76
87
|
def ip
|
|
77
|
-
|
|
88
|
+
peer_ip = session&.socket&.peer_ip
|
|
89
|
+
peer_ip unless peer_ip == Net::SSH::Transport::PacketStream::PROXY_COMMAND_HOST_IP
|
|
78
90
|
end
|
|
79
91
|
|
|
80
92
|
# Returns the key itself, as reported by the remote host.
|
|
@@ -87,6 +99,12 @@ module Net
|
|
|
87
99
|
def remember_host!
|
|
88
100
|
@callback.call
|
|
89
101
|
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def session
|
|
106
|
+
@data && @data[:session]
|
|
107
|
+
end
|
|
90
108
|
end
|
|
91
109
|
|
|
92
110
|
# Raised when the cached key for a particular host does not match the
|