net-ssh 7.3.6 → 8.0.0
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/.github/pull_request_template.md +1 -0
- data/.github/workflows/changelog.yml +43 -0
- data/.github/workflows/ci-with-docker.yml +0 -3
- data/.github/workflows/ci.yml +2 -17
- data/.github/workflows/rubocop.yml +3 -1
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +1 -0
- data/CHANGES.txt +73 -7
- data/CONTRIBUTING.md +35 -0
- data/Dockerfile +3 -3
- data/Dockerfile.openssl3 +1 -1
- data/Dockerfile.zlib_ng +1 -1
- data/README.md +71 -57
- data/Rakefile +66 -12
- data/appveyor.yml +4 -16
- data/changelog.d/README.md +1 -0
- data/docker-compose.yml +1 -7
- data/lib/net/ssh/authentication/agent.rb +35 -11
- data/lib/net/ssh/authentication/ed25519.rb +137 -44
- data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
- data/lib/net/ssh/authentication/key_manager.rb +22 -11
- 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 +28 -12
- data/lib/net/ssh/connection/channel.rb +49 -2
- data/lib/net/ssh/connection/session.rb +58 -8
- data/lib/net/ssh/errors.rb +24 -6
- data/lib/net/ssh/key_factory.rb +7 -1
- data/lib/net/ssh/known_hosts.rb +18 -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 +130 -0
- data/lib/net/ssh/prompt.rb +3 -3
- data/lib/net/ssh/proxy/jump.rb +40 -2
- data/lib/net/ssh/service/forward.rb +29 -24
- data/lib/net/ssh/test.rb +2 -1
- data/lib/net/ssh/transport/algorithms.rb +88 -33
- 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 +23 -1
- data/lib/net/ssh/transport/constants.rb +1 -0
- data/lib/net/ssh/transport/gcm_cipher.rb +27 -6
- 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 +12 -0
- data/lib/net/ssh/transport/session.rb +41 -44
- data/lib/net/ssh/verifiers/always.rb +0 -3
- data/lib/net/ssh/version.rb +3 -3
- data/lib/net/ssh.rb +139 -57
- data/net-ssh.gemspec +5 -8
- data/rakelib/changelog.rb +229 -0
- data.tar.gz.sig +0 -0
- metadata +37 -36
- metadata.gz.sig +0 -0
- data/Gemfile.noed25519 +0 -12
- data/Gemfile.norbnacl +0 -12
|
@@ -69,27 +69,22 @@ module Net
|
|
|
69
69
|
|
|
70
70
|
attempted = []
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
pubkey_algorithms: options[:pubkey_algorithms] || nil)
|
|
83
|
-
rescue NameError
|
|
84
|
-
debug {"Mechanism #{name} was requested, but isn't a known type. Ignoring it."}
|
|
85
|
-
next
|
|
72
|
+
# After a partial success (sshd AuthenticationMethods) restart from the most preferred method the server now allows, as OpenSSH does.
|
|
73
|
+
loop do
|
|
74
|
+
@partial_success = false
|
|
75
|
+
|
|
76
|
+
@auth_methods.each do |name|
|
|
77
|
+
next unless @allowed_auth_methods.include?(name)
|
|
78
|
+
|
|
79
|
+
attempted << name
|
|
80
|
+
return true if try_authenticate(name, next_service, username, password, key_manager)
|
|
81
|
+
break if @partial_success
|
|
86
82
|
end
|
|
87
83
|
|
|
88
|
-
|
|
89
|
-
rescue Net::SSH::Authentication::DisallowedMethod
|
|
84
|
+
break unless @partial_success
|
|
90
85
|
end
|
|
91
86
|
|
|
92
|
-
error { "all authorization methods failed (tried #{attempted.join(', ')})" }
|
|
87
|
+
error { "all authorization methods failed (tried #{attempted.uniq.join(', ')})" }
|
|
93
88
|
return false
|
|
94
89
|
ensure
|
|
95
90
|
key_manager.finish if key_manager
|
|
@@ -109,6 +104,7 @@ module Net
|
|
|
109
104
|
|
|
110
105
|
when USERAUTH_FAILURE
|
|
111
106
|
@allowed_auth_methods = packet[:authentications].split(/,/)
|
|
107
|
+
@partial_success ||= packet[:partial_success]
|
|
112
108
|
debug { "allowed methods: #{packet[:authentications]}" }
|
|
113
109
|
return packet
|
|
114
110
|
|
|
@@ -136,6 +132,22 @@ module Net
|
|
|
136
132
|
|
|
137
133
|
private
|
|
138
134
|
|
|
135
|
+
def try_authenticate(method_name, next_service, username, password, key_manager)
|
|
136
|
+
debug { "trying #{method_name}" }
|
|
137
|
+
begin
|
|
138
|
+
auth_class = Methods.const_get(method_name.split(/\W+/).map { |p| p.capitalize }.join)
|
|
139
|
+
method = auth_class.new(self,
|
|
140
|
+
key_manager: key_manager, password_prompt: options[:password_prompt])
|
|
141
|
+
rescue NameError
|
|
142
|
+
debug {"Mechanism #{method_name} was requested, but isn't a known type. Ignoring it."}
|
|
143
|
+
return false
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
method.authenticate(next_service, username, password)
|
|
147
|
+
rescue Net::SSH::Authentication::DisallowedMethod
|
|
148
|
+
false
|
|
149
|
+
end
|
|
150
|
+
|
|
139
151
|
# Returns an array of paths to the key files usually defined
|
|
140
152
|
# by system default.
|
|
141
153
|
def default_keys
|
data/lib/net/ssh/buffer.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
require 'net/ssh/errors'
|
|
1
2
|
require 'net/ssh/transport/openssl'
|
|
2
3
|
|
|
3
4
|
require 'net/ssh/authentication/certificate'
|
|
4
5
|
require 'net/ssh/authentication/ed25519_loader'
|
|
6
|
+
require 'net/ssh/authentication/security_key'
|
|
5
7
|
|
|
6
8
|
module Net
|
|
7
9
|
module SSH
|
|
@@ -338,8 +340,11 @@ module Net
|
|
|
338
340
|
key = Net::SSH::Authentication::ED25519::PubKey.read_keyblob(self)
|
|
339
341
|
when /^ecdsa\-sha2\-(\w*)$/
|
|
340
342
|
key = OpenSSL::PKey::EC.read_keyblob($1, self)
|
|
343
|
+
when Net::SSH::Authentication::SecurityKey::SK_ECDSA_SHA2_NISTP256,
|
|
344
|
+
Net::SSH::Authentication::SecurityKey::SK_SSH_ED25519
|
|
345
|
+
key = Net::SSH::Authentication::SecurityKey::PubKey.read_keyblob(type, self)
|
|
341
346
|
else
|
|
342
|
-
raise
|
|
347
|
+
raise Net::SSH::UnsupportedKeyType, "unsupported key type `#{type}'"
|
|
343
348
|
end
|
|
344
349
|
|
|
345
350
|
return key
|
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,
|
|
@@ -231,7 +236,7 @@ module Net
|
|
|
231
236
|
user: :user,
|
|
232
237
|
checkhostip: :check_host_ip
|
|
233
238
|
}.freeze
|
|
234
|
-
def translate_config_key(hash, key, value, settings)
|
|
239
|
+
def translate_config_key(hash, key, value, settings) # rubocop:disable Metrics/AbcSize
|
|
235
240
|
case key
|
|
236
241
|
when :stricthostkeychecking
|
|
237
242
|
hash[:verify_host_key] = translate_verify_host_key(value)
|
|
@@ -247,6 +252,8 @@ module Net
|
|
|
247
252
|
hash[:host_key] = value.split(/,/)
|
|
248
253
|
when :hostname
|
|
249
254
|
hash[:host_name] = value.gsub(/%h/, settings['host'])
|
|
255
|
+
when :kexalgorithms
|
|
256
|
+
hash[:kex] = value.split(/,/)
|
|
250
257
|
when :macs
|
|
251
258
|
hash[:hmac] = value.split(/,/)
|
|
252
259
|
when :serveralivecountmax
|
|
@@ -277,6 +284,8 @@ module Net
|
|
|
277
284
|
if (proxy = setup_proxy(*value))
|
|
278
285
|
hash[:proxy] = proxy
|
|
279
286
|
end
|
|
287
|
+
when :pubkeyacceptedalgorithms, :pubkeyacceptedkeytypes
|
|
288
|
+
hash[:pubkey_algorithms] = value.split(/,/)
|
|
280
289
|
when :pubkeyauthentication
|
|
281
290
|
if value
|
|
282
291
|
(hash[:auth_methods] << 'publickey').uniq!
|
|
@@ -302,13 +311,15 @@ module Net
|
|
|
302
311
|
def setup_proxy(type, value)
|
|
303
312
|
case type
|
|
304
313
|
when 'proxycommand'
|
|
305
|
-
if value !~ /^none$/
|
|
314
|
+
if value !~ /^none$/i
|
|
306
315
|
require 'net/ssh/proxy/command'
|
|
307
316
|
Net::SSH::Proxy::Command.new(value)
|
|
308
317
|
end
|
|
309
318
|
when 'proxyjump'
|
|
310
|
-
|
|
311
|
-
|
|
319
|
+
if value !~ /^none$/i
|
|
320
|
+
require 'net/ssh/proxy/jump'
|
|
321
|
+
Net::SSH::Proxy::Jump.new(value)
|
|
322
|
+
end
|
|
312
323
|
end
|
|
313
324
|
end
|
|
314
325
|
|
|
@@ -398,6 +409,11 @@ module Net
|
|
|
398
409
|
!condition_matches.empty? && condition_matches.all?
|
|
399
410
|
end
|
|
400
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
|
+
|
|
401
417
|
def unquote(string)
|
|
402
418
|
string =~ /^"(.*)"$/ ? Regexp.last_match(1) : string
|
|
403
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
|
|
|
@@ -74,13 +74,15 @@ module Net
|
|
|
74
74
|
@channel_id_counter = -1
|
|
75
75
|
@channels = Hash.new(NilChannel.new(self))
|
|
76
76
|
@listeners = { transport.socket => nil }
|
|
77
|
+
@read_paused = {}
|
|
78
|
+
@draining = {}
|
|
77
79
|
@pending_requests = []
|
|
78
80
|
@channel_open_handlers = {}
|
|
79
81
|
@on_global_request = {}
|
|
80
82
|
@properties = (options[:properties] || {}).dup
|
|
81
83
|
|
|
82
84
|
@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] :
|
|
85
|
+
@max_win_size = (options.key?(:max_win_size) ? options[:max_win_size] : Channel::DEFAULT_MAX_WIN_SIZE)
|
|
84
86
|
|
|
85
87
|
@keepalive = Keepalive.new(self)
|
|
86
88
|
|
|
@@ -120,12 +122,19 @@ module Net
|
|
|
120
122
|
# connection.
|
|
121
123
|
def close
|
|
122
124
|
info { "closing remaining channels (#{channels.length} open)" }
|
|
123
|
-
|
|
125
|
+
|
|
126
|
+
channels.each do |id, channel|
|
|
127
|
+
channel.eof!
|
|
128
|
+
channel.close
|
|
129
|
+
end
|
|
130
|
+
|
|
124
131
|
begin
|
|
125
132
|
loop(0.1) { channels.any? }
|
|
126
133
|
rescue Net::SSH::Disconnect
|
|
127
134
|
raise unless channels.empty?
|
|
128
135
|
end
|
|
136
|
+
@draining.each_key(&:close)
|
|
137
|
+
@draining.clear
|
|
129
138
|
transport.close
|
|
130
139
|
end
|
|
131
140
|
|
|
@@ -146,10 +155,13 @@ module Net
|
|
|
146
155
|
# a +true+ value for +include_invisible+, then those will be counted.
|
|
147
156
|
#
|
|
148
157
|
# This can be useful for determining whether the event loop should continue
|
|
149
|
-
# to be run.
|
|
158
|
+
# to be run. A forwarded socket still writing out data that arrived before
|
|
159
|
+
# its channel closed also counts as busy.
|
|
150
160
|
#
|
|
151
161
|
# ssh.loop { ssh.busy? }
|
|
152
162
|
def busy?(include_invisible = false)
|
|
163
|
+
return true if @draining.any?
|
|
164
|
+
|
|
153
165
|
if include_invisible
|
|
154
166
|
channels.any?
|
|
155
167
|
else
|
|
@@ -255,7 +267,7 @@ module Net
|
|
|
255
267
|
def ev_do_calculate_rw_wait(wait)
|
|
256
268
|
r = listeners.keys
|
|
257
269
|
w = r.select { |w2| w2.respond_to?(:pending_write?) && w2.pending_write? }
|
|
258
|
-
[r, w, io_select_wait(wait)]
|
|
270
|
+
[r.reject { |io| @read_paused.key?(io) }, w, io_select_wait(wait)]
|
|
259
271
|
end
|
|
260
272
|
|
|
261
273
|
# This is called internally as part of #process.
|
|
@@ -281,6 +293,7 @@ module Net
|
|
|
281
293
|
|
|
282
294
|
Array(writers).each do |writer|
|
|
283
295
|
writer.send_pending
|
|
296
|
+
close_if_drained(writer) if @draining.key?(writer)
|
|
284
297
|
end
|
|
285
298
|
end
|
|
286
299
|
|
|
@@ -416,7 +429,10 @@ module Net
|
|
|
416
429
|
#
|
|
417
430
|
# matches = ssh.exec!("grep something /some/files")
|
|
418
431
|
#
|
|
419
|
-
#
|
|
432
|
+
# With a block, output goes to the block and an empty string is returned,
|
|
433
|
+
# unless the block sets <tt>ch[:result]</tt>, which is returned instead.
|
|
434
|
+
# Either way the returned string has an exitstatus method to query the
|
|
435
|
+
# command's exit status.
|
|
420
436
|
def exec!(command, status: nil, &block)
|
|
421
437
|
block_or_concat = block || Proc.new do |ch, type, data|
|
|
422
438
|
ch[:result] ||= String.new
|
|
@@ -427,10 +443,10 @@ module Net
|
|
|
427
443
|
channel = exec(command, status: status, &block_or_concat)
|
|
428
444
|
channel.wait
|
|
429
445
|
|
|
430
|
-
channel[:result] ||= String.new unless block
|
|
431
446
|
channel[:result] &&= channel[:result].force_encoding("UTF-8") unless block
|
|
447
|
+
channel[:result] ||= String.new(encoding: Encoding::UTF_8)
|
|
432
448
|
|
|
433
|
-
StringWithExitstatus.new(channel[:result], status[:exit_code])
|
|
449
|
+
StringWithExitstatus.new(channel[:result], status[:exit_code])
|
|
434
450
|
end
|
|
435
451
|
|
|
436
452
|
# Enqueues a message to be sent to the server as soon as the socket is
|
|
@@ -485,9 +501,36 @@ module Net
|
|
|
485
501
|
# Removes the given io object from the listeners collection, so that the
|
|
486
502
|
# event loop will no longer monitor it.
|
|
487
503
|
def stop_listening_to(io)
|
|
504
|
+
@read_paused.delete(io)
|
|
505
|
+
@draining.delete(io)
|
|
488
506
|
listeners.delete(io)
|
|
489
507
|
end
|
|
490
508
|
|
|
509
|
+
# Internal and experimental, used by port forwarding: stops the event
|
|
510
|
+
# loop from reading +io+ while it keeps flushing its pending output.
|
|
511
|
+
def pause_reading(io) # :nodoc:
|
|
512
|
+
raise ArgumentError, "the transport socket can't be paused" if io.equal?(transport.socket)
|
|
513
|
+
|
|
514
|
+
@read_paused[io] = true if listeners.key?(io)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def resume_reading(io) # :nodoc:
|
|
518
|
+
@read_paused.delete(io)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def reading_paused?(io) # :nodoc:
|
|
522
|
+
@read_paused.key?(io)
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Internal and experimental, used by port forwarding: stops reading +io+
|
|
526
|
+
# but keeps flushing its pending output, then closes it and stops
|
|
527
|
+
# listening, as OpenSSH's chan_rcvd_oclose does.
|
|
528
|
+
def close_when_drained(io) # :nodoc:
|
|
529
|
+
pause_reading(io)
|
|
530
|
+
@draining[io] = true
|
|
531
|
+
close_if_drained(io)
|
|
532
|
+
end
|
|
533
|
+
|
|
491
534
|
# Returns a reference to the Net::SSH::Service::Forward service, which can
|
|
492
535
|
# be used for forwarding ports over SSH.
|
|
493
536
|
def forward
|
|
@@ -565,6 +608,13 @@ module Net
|
|
|
565
608
|
end
|
|
566
609
|
end
|
|
567
610
|
|
|
611
|
+
def close_if_drained(io)
|
|
612
|
+
return if io.pending_write? && !io.closed? && !closed? && listeners.key?(io)
|
|
613
|
+
|
|
614
|
+
io.close unless io.closed?
|
|
615
|
+
stop_listening_to(io)
|
|
616
|
+
end
|
|
617
|
+
|
|
568
618
|
def channel_closed(channel)
|
|
569
619
|
channel.remote_closed!
|
|
570
620
|
channel.close
|
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
|
data/lib/net/ssh/key_factory.rb
CHANGED
|
@@ -2,6 +2,7 @@ require 'net/ssh/transport/openssl'
|
|
|
2
2
|
require 'net/ssh/prompt'
|
|
3
3
|
|
|
4
4
|
require 'net/ssh/authentication/ed25519_loader'
|
|
5
|
+
require 'net/ssh/authentication/security_key'
|
|
5
6
|
|
|
6
7
|
module Net
|
|
7
8
|
module SSH
|
|
@@ -92,7 +93,7 @@ module Net
|
|
|
92
93
|
blob = nil
|
|
93
94
|
begin
|
|
94
95
|
blob = fields.shift
|
|
95
|
-
end while !blob.nil? &&
|
|
96
|
+
end while !blob.nil? && !public_key_type?(blob)
|
|
96
97
|
blob = fields.shift
|
|
97
98
|
|
|
98
99
|
raise Net::SSH::Exception, "public key at #{filename} is not valid" if blob.nil?
|
|
@@ -104,6 +105,11 @@ module Net
|
|
|
104
105
|
|
|
105
106
|
private
|
|
106
107
|
|
|
108
|
+
def public_key_type?(type)
|
|
109
|
+
/^(ssh-(rsa|dss|ed25519)|ecdsa-sha2-nistp\d+)(-cert-v01@openssh\.com)?$/.match?(type) ||
|
|
110
|
+
Net::SSH::Authentication::SecurityKey.understands?(type)
|
|
111
|
+
end
|
|
112
|
+
|
|
107
113
|
# rubocop:disable Style/Documentation, Lint/DuplicateMethods
|
|
108
114
|
class KeyType
|
|
109
115
|
def self.read(key_data, passphrase)
|
data/lib/net/ssh/known_hosts.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
require 'strscan'
|
|
2
2
|
require 'openssl'
|
|
3
|
-
require 'delegate'
|
|
4
3
|
require 'net/ssh/buffer'
|
|
5
4
|
require 'net/ssh/authentication/ed25519_loader'
|
|
6
5
|
|
|
@@ -8,8 +7,11 @@ module Net
|
|
|
8
7
|
module SSH
|
|
9
8
|
module HostKeyEntries
|
|
10
9
|
# regular public key entry
|
|
11
|
-
class PubKey
|
|
12
|
-
|
|
10
|
+
class PubKey
|
|
11
|
+
# The public key itself, e.g. an OpenSSL::PKey::PKey.
|
|
12
|
+
attr_reader :key
|
|
13
|
+
|
|
14
|
+
def initialize(key, comment: nil)
|
|
13
15
|
@key = key
|
|
14
16
|
@comment = comment
|
|
15
17
|
end
|
|
@@ -26,14 +28,22 @@ module Net
|
|
|
26
28
|
@key.to_blob
|
|
27
29
|
end
|
|
28
30
|
|
|
29
|
-
def __getobj__
|
|
30
|
-
Kernel.warn("Calling Net::SSH::Buffer methods on HostKeyEntries PubKey is deprecated")
|
|
31
|
-
@key
|
|
32
|
-
end
|
|
33
|
-
|
|
34
31
|
def matches_key?(server_key)
|
|
35
32
|
@key.ssh_type == server_key.ssh_type && @key.to_blob == server_key.to_blob
|
|
36
33
|
end
|
|
34
|
+
|
|
35
|
+
def method_missing(name, *args, &block)
|
|
36
|
+
return super unless @key.respond_to?(name)
|
|
37
|
+
|
|
38
|
+
raise NoMethodError.new(
|
|
39
|
+
"undefined method `#{name}' for #{self.class}; call it on #key, the wrapped public key",
|
|
40
|
+
name
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def respond_to_missing?(_name, _include_private = false)
|
|
45
|
+
false
|
|
46
|
+
end
|
|
37
47
|
end
|
|
38
48
|
|
|
39
49
|
# @cert-authority entry
|