net-ssh 7.3.6 → 8.1.2
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/CHANGES.txt +106 -5
- data/README.md +71 -57
- data/lib/net/ssh/authentication/agent.rb +35 -11
- data/lib/net/ssh/authentication/certificate.rb +2 -2
- 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 +6 -8
- data/lib/net/ssh/authentication/methods/hostbased.rb +3 -3
- data/lib/net/ssh/authentication/methods/publickey.rb +52 -38
- data/lib/net/ssh/authentication/security_key.rb +71 -0
- data/lib/net/ssh/authentication/session.rb +30 -17
- data/lib/net/ssh/buffer.rb +8 -3
- data/lib/net/ssh/buffered_io.rb +40 -3
- data/lib/net/ssh/config.rb +123 -50
- data/lib/net/ssh/connection/channel.rb +49 -2
- data/lib/net/ssh/connection/session.rb +151 -13
- data/lib/net/ssh/errors.rb +34 -6
- data/lib/net/ssh/key_factory.rb +7 -1
- data/lib/net/ssh/known_hosts.rb +103 -38
- 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 +127 -0
- data/lib/net/ssh/prompt.rb +43 -5
- data/lib/net/ssh/proxy/https.rb +28 -4
- data/lib/net/ssh/proxy/jump.rb +61 -8
- data/lib/net/ssh/service/forward.rb +35 -24
- data/lib/net/ssh/test.rb +2 -1
- data/lib/net/ssh/transport/algorithms.rb +92 -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 +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 +61 -45
- data/lib/net/ssh/verifiers/accept_new_or_local_tunnel.rb +3 -1
- data/lib/net/ssh/verifiers/always.rb +16 -4
- data/lib/net/ssh/verifiers/never.rb +2 -1
- data/lib/net/ssh/version.rb +3 -3
- data/lib/net/ssh.rb +164 -63
- data.tar.gz.sig +0 -0
- metadata +33 -61
- metadata.gz.sig +0 -0
- data/.dockerignore +0 -9
- data/.github/FUNDING.yml +0 -1
- data/.github/config/rubocop_linter_action.yml +0 -4
- data/.github/workflows/ci-with-docker.yml +0 -58
- data/.github/workflows/ci.yml +0 -99
- data/.github/workflows/rubocop.yml +0 -16
- data/.gitignore +0 -15
- data/.rubocop.yml +0 -22
- data/.rubocop_todo.yml +0 -1083
- data/DEVELOPMENT.md +0 -23
- data/Dockerfile +0 -32
- data/Dockerfile.openssl3 +0 -17
- data/Dockerfile.zlib_ng +0 -29
- data/Gemfile +0 -13
- data/Gemfile.noed25519 +0 -12
- data/Gemfile.norbnacl +0 -12
- data/ISSUE_TEMPLATE.md +0 -30
- data/Manifest +0 -132
- data/Rakefile +0 -196
- data/SECURITY.md +0 -4
- data/THANKS.txt +0 -110
- data/appveyor.yml +0 -58
- data/docker-compose.yml +0 -23
- data/net-ssh-public_cert.pem +0 -21
- data/net-ssh.gemspec +0 -48
- data/support/ssh_tunnel_bug.rb +0 -65
|
@@ -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,23 @@ 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
|
+
fingerprint_hash: options[:fingerprint_hash])
|
|
142
|
+
rescue NameError
|
|
143
|
+
debug {"Mechanism #{method_name} was requested, but isn't a known type. Ignoring it."}
|
|
144
|
+
return false
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
method.authenticate(next_service, username, password)
|
|
148
|
+
rescue Net::SSH::Authentication::DisallowedMethod
|
|
149
|
+
false
|
|
150
|
+
end
|
|
151
|
+
|
|
139
152
|
# Returns an array of paths to the key files usually defined
|
|
140
153
|
# by system default.
|
|
141
154
|
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
|
|
@@ -143,7 +145,7 @@ module Net
|
|
|
143
145
|
# Appends the given text to the end of the buffer. Does not alter the
|
|
144
146
|
# read position. Returns the buffer object itself.
|
|
145
147
|
def append(text)
|
|
146
|
-
@content << text
|
|
148
|
+
@content << text.b
|
|
147
149
|
self
|
|
148
150
|
end
|
|
149
151
|
|
|
@@ -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
|
|
@@ -354,7 +359,7 @@ module Net
|
|
|
354
359
|
# Writes the given data literally into the string. Does not alter the
|
|
355
360
|
# read position. Returns the buffer object.
|
|
356
361
|
def write(*data)
|
|
357
|
-
data.each { |datum| @content << datum.
|
|
362
|
+
data.each { |datum| @content << datum.b }
|
|
358
363
|
self
|
|
359
364
|
end
|
|
360
365
|
|
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
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
require 'etc'
|
|
2
|
+
require 'set'
|
|
3
|
+
|
|
1
4
|
module Net
|
|
2
5
|
module SSH
|
|
3
6
|
# The Net::SSH::Config class is used to parse OpenSSH configuration files,
|
|
@@ -15,6 +18,8 @@ module Net
|
|
|
15
18
|
# * Compression => :compression
|
|
16
19
|
# * CompressionLevel => :compression_level
|
|
17
20
|
# * ConnectTimeout => maps to the :timeout option
|
|
21
|
+
# * ControlPath => :control_path, used only when Net::SSH.start is given
|
|
22
|
+
# control_master: :auto; ControlMaster itself is not read
|
|
18
23
|
# * ForwardAgent => :forward_agent
|
|
19
24
|
# * GlobalKnownHostsFile => :global_known_hosts_file
|
|
20
25
|
# * HostBasedAuthentication => maps to the :auth_methods option
|
|
@@ -24,6 +29,7 @@ module Net
|
|
|
24
29
|
# * IdentityFile => maps to the :keys option
|
|
25
30
|
# * IdentityAgent => :identity_agent
|
|
26
31
|
# * IdentitiesOnly => :keys_only
|
|
32
|
+
# * KexAlgorithms => maps to the :kex option
|
|
27
33
|
# * CheckHostIP => :check_host_ip
|
|
28
34
|
# * Macs => maps to the :hmac option
|
|
29
35
|
# * PasswordAuthentication => maps to the :auth_methods option password
|
|
@@ -31,6 +37,7 @@ module Net
|
|
|
31
37
|
# * PreferredAuthentications => maps to the :auth_methods option
|
|
32
38
|
# * ProxyCommand => maps to the :proxy option
|
|
33
39
|
# * ProxyJump => maps to the :proxy option
|
|
40
|
+
# * PubkeyAcceptedAlgorithms (or PubkeyAcceptedKeyTypes) => maps to :pubkey_algorithms
|
|
34
41
|
# * PubKeyAuthentication => maps to the :auth_methods option
|
|
35
42
|
# * RekeyLimit => :rekey_limit
|
|
36
43
|
# * StrictHostKeyChecking => :verify_host_key
|
|
@@ -65,9 +72,19 @@ module Net
|
|
|
65
72
|
# given +files+ (defaulting to the list of files returned by
|
|
66
73
|
# #default_files), translates the resulting hash into the options
|
|
67
74
|
# recognized by Net::SSH, and returns them.
|
|
68
|
-
|
|
75
|
+
#
|
|
76
|
+
# +user+ is the remote user given explicitly (e.g. to Net::SSH.start),
|
|
77
|
+
# used by <tt>Match User</tt>; when nil, the User set so far by the
|
|
78
|
+
# config, then the local user name, is used, as in OpenSSH.
|
|
79
|
+
#
|
|
80
|
+
# Match supports +all+, +Host+, +OriginalHost+, +User+ and +LocalUser+,
|
|
81
|
+
# each optionally negated with a leading "!". A Match block with any
|
|
82
|
+
# other condition (+exec+, +canonical+, +final+, +localnetwork+,
|
|
83
|
+
# +tagged+, ...) never matches, and a warning naming the condition is
|
|
84
|
+
# printed with Kernel#warn once per config line.
|
|
85
|
+
def for(host, files = expandable_default_files, user: nil)
|
|
69
86
|
translate(files.inject({}) { |settings, file|
|
|
70
|
-
load(file, host, settings)
|
|
87
|
+
load(file, host, settings, user: user)
|
|
71
88
|
})
|
|
72
89
|
end
|
|
73
90
|
|
|
@@ -77,27 +94,26 @@ module Net
|
|
|
77
94
|
# ones. Returns a hash containing the OpenSSH options. (See
|
|
78
95
|
# #translate for how to convert the OpenSSH options into Net::SSH
|
|
79
96
|
# options.)
|
|
80
|
-
def load(path, host, settings = {}, base_dir = nil)
|
|
97
|
+
def load(path, host, settings = {}, base_dir = nil, user: nil)
|
|
81
98
|
file = File.expand_path(path)
|
|
82
99
|
base_dir ||= File.dirname(file)
|
|
83
100
|
return settings unless File.readable?(file)
|
|
84
101
|
|
|
85
102
|
globals = {}
|
|
86
|
-
block_matched = false
|
|
87
|
-
|
|
103
|
+
block_matched = block_seen = false
|
|
104
|
+
lineno = 0
|
|
88
105
|
IO.foreach(file) do |line|
|
|
106
|
+
lineno += 1
|
|
89
107
|
next if line =~ /^\s*(?:#.*)?$/
|
|
90
108
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
else
|
|
94
|
-
key, value = line.strip.split(/\s+/, 2)
|
|
95
|
-
end
|
|
109
|
+
# As in OpenSSH, the keyword ends at the first whitespace or "=".
|
|
110
|
+
key, value = line.strip.split(/\s*=\s*|\s+/, 2)
|
|
96
111
|
|
|
97
112
|
# silently ignore malformed entries
|
|
98
113
|
next if value.nil?
|
|
99
114
|
|
|
100
115
|
key.downcase!
|
|
116
|
+
value = uncomment(value)
|
|
101
117
|
value = unquote(value)
|
|
102
118
|
|
|
103
119
|
value = case value.strip
|
|
@@ -125,7 +141,7 @@ module Net
|
|
|
125
141
|
block_seen = true
|
|
126
142
|
settings[key] = host
|
|
127
143
|
elsif key == 'match'
|
|
128
|
-
block_matched = eval_match_conditions(value, host, settings)
|
|
144
|
+
block_matched = eval_match_conditions(value, host, [user, settings['user'], globals['user']].compact.first, "#{file} line #{lineno}")
|
|
129
145
|
block_seen = true
|
|
130
146
|
elsif !block_seen
|
|
131
147
|
case key
|
|
@@ -133,7 +149,7 @@ module Net
|
|
|
133
149
|
(globals[key] ||= []) << value
|
|
134
150
|
when 'include'
|
|
135
151
|
included_file_paths(base_dir, value).each do |file_path|
|
|
136
|
-
globals = load(file_path, host, globals, base_dir)
|
|
152
|
+
globals = load(file_path, host, globals, base_dir, user: user)
|
|
137
153
|
end
|
|
138
154
|
else
|
|
139
155
|
globals[key] = value unless settings.key?(key)
|
|
@@ -144,7 +160,7 @@ module Net
|
|
|
144
160
|
(settings[key] ||= []) << value
|
|
145
161
|
when 'include'
|
|
146
162
|
included_file_paths(base_dir, value).each do |file_path|
|
|
147
|
-
settings = load(file_path, host, settings, base_dir)
|
|
163
|
+
settings = load(file_path, host, settings, base_dir, user: user)
|
|
148
164
|
end
|
|
149
165
|
else
|
|
150
166
|
settings[key] = value unless settings.key?(key)
|
|
@@ -152,9 +168,11 @@ module Net
|
|
|
152
168
|
end
|
|
153
169
|
|
|
154
170
|
# ProxyCommand and ProxyJump override each other so they need to be tracked togeather
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
171
|
+
[globals, settings].each do |hash|
|
|
172
|
+
%w[proxyjump proxycommand].each do |proxy_key|
|
|
173
|
+
if (proxy_value = hash.delete(proxy_key))
|
|
174
|
+
hash['proxy'] ||= [proxy_key, proxy_value]
|
|
175
|
+
end
|
|
158
176
|
end
|
|
159
177
|
end
|
|
160
178
|
end
|
|
@@ -218,6 +236,7 @@ module Net
|
|
|
218
236
|
bindaddress: :bind_address,
|
|
219
237
|
compression: :compression,
|
|
220
238
|
compressionlevel: :compression_level,
|
|
239
|
+
controlpath: :control_path,
|
|
221
240
|
certificatefile: :keycerts,
|
|
222
241
|
connecttimeout: :timeout,
|
|
223
242
|
forwardagent: :forward_agent,
|
|
@@ -231,7 +250,7 @@ module Net
|
|
|
231
250
|
user: :user,
|
|
232
251
|
checkhostip: :check_host_ip
|
|
233
252
|
}.freeze
|
|
234
|
-
def translate_config_key(hash, key, value, settings)
|
|
253
|
+
def translate_config_key(hash, key, value, settings) # rubocop:disable Metrics/AbcSize
|
|
235
254
|
case key
|
|
236
255
|
when :stricthostkeychecking
|
|
237
256
|
hash[:verify_host_key] = translate_verify_host_key(value)
|
|
@@ -247,6 +266,8 @@ module Net
|
|
|
247
266
|
hash[:host_key] = value.split(/,/)
|
|
248
267
|
when :hostname
|
|
249
268
|
hash[:host_name] = value.gsub(/%h/, settings['host'])
|
|
269
|
+
when :kexalgorithms
|
|
270
|
+
hash[:kex] = value.split(/,/)
|
|
250
271
|
when :macs
|
|
251
272
|
hash[:hmac] = value.split(/,/)
|
|
252
273
|
when :serveralivecountmax
|
|
@@ -277,6 +298,8 @@ module Net
|
|
|
277
298
|
if (proxy = setup_proxy(*value))
|
|
278
299
|
hash[:proxy] = proxy
|
|
279
300
|
end
|
|
301
|
+
when :pubkeyacceptedalgorithms, :pubkeyacceptedkeytypes
|
|
302
|
+
hash[:pubkey_algorithms] = value.split(/,/)
|
|
280
303
|
when :pubkeyauthentication
|
|
281
304
|
if value
|
|
282
305
|
(hash[:auth_methods] << 'publickey').uniq!
|
|
@@ -302,19 +325,21 @@ module Net
|
|
|
302
325
|
def setup_proxy(type, value)
|
|
303
326
|
case type
|
|
304
327
|
when 'proxycommand'
|
|
305
|
-
if value !~ /^none$/
|
|
328
|
+
if value !~ /^none$/i
|
|
306
329
|
require 'net/ssh/proxy/command'
|
|
307
330
|
Net::SSH::Proxy::Command.new(value)
|
|
308
331
|
end
|
|
309
332
|
when 'proxyjump'
|
|
310
|
-
|
|
311
|
-
|
|
333
|
+
if value !~ /^none$/i
|
|
334
|
+
require 'net/ssh/proxy/jump'
|
|
335
|
+
Net::SSH::Proxy::Jump.new(value)
|
|
336
|
+
end
|
|
312
337
|
end
|
|
313
338
|
end
|
|
314
339
|
|
|
315
340
|
# Converts an ssh_config pattern into a regex for matching against
|
|
316
341
|
# host names.
|
|
317
|
-
def pattern2regex(pattern)
|
|
342
|
+
def pattern2regex(pattern, ignore_case: true)
|
|
318
343
|
tail = pattern
|
|
319
344
|
prefix = String.new
|
|
320
345
|
while !tail.empty? do
|
|
@@ -330,7 +355,7 @@ module Net
|
|
|
330
355
|
fail "Unpexpcted sep:#{sep}"
|
|
331
356
|
end
|
|
332
357
|
end
|
|
333
|
-
Regexp.new("^" + prefix + "$",
|
|
358
|
+
Regexp.new("^" + prefix + "$", ignore_case)
|
|
334
359
|
end
|
|
335
360
|
|
|
336
361
|
# Converts the given size into an integer number of bytes.
|
|
@@ -363,39 +388,87 @@ module Net
|
|
|
363
388
|
str.scan(/([^"\s]+)?(?:"([^"]+)")?\s*/).map(&:join)
|
|
364
389
|
end
|
|
365
390
|
|
|
366
|
-
|
|
391
|
+
MATCH_CONDITIONS_WITHOUT_ARGUMENT = %w[all canonical final].freeze
|
|
392
|
+
SUPPORTED_MATCH_CONDITIONS = %w[all host originalhost user localuser].freeze
|
|
393
|
+
|
|
394
|
+
def eval_match_conditions(condition, host, remote_user, location)
|
|
367
395
|
# Not using `\s` for whitespace matching as canonical
|
|
368
396
|
# ssh_config parser implementation (OpenSSH) has specific character set.
|
|
369
397
|
# Ref: https://github.com/openssh/openssh-portable/blob/2581333d564d8697837729b3d07d45738eaf5a54/misc.c#L237-L239
|
|
370
|
-
|
|
371
|
-
return true if
|
|
372
|
-
|
|
373
|
-
conditions =
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
# warn "net-ssh: Unsupported expr in Match block: #{kind}"
|
|
395
|
-
end
|
|
398
|
+
tokens = condition.split(/[ \t\r\n]+|(?<!=)=(?!=)/).reject(&:empty?)
|
|
399
|
+
return true if tokens == ["all"]
|
|
400
|
+
|
|
401
|
+
conditions = parse_match_conditions(tokens)
|
|
402
|
+
unsupported = conditions.find { |_kind, name, exprs|
|
|
403
|
+
!SUPPORTED_MATCH_CONDITIONS.include?(name) || (name != 'all' && exprs.nil?)
|
|
404
|
+
}
|
|
405
|
+
if unsupported
|
|
406
|
+
warn_unsupported_match(unsupported[0], location)
|
|
407
|
+
return false
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
!conditions.empty? && conditions.all? do |kind, name, exprs|
|
|
411
|
+
kind.start_with?('!') ^ match_condition?(name, exprs, host, remote_user || local_user, conditions.size)
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def parse_match_conditions(tokens)
|
|
416
|
+
conditions = []
|
|
417
|
+
until tokens.empty?
|
|
418
|
+
kind = tokens.shift
|
|
419
|
+
name = kind.delete_prefix('!').downcase
|
|
420
|
+
exprs = tokens.shift unless MATCH_CONDITIONS_WITHOUT_ARGUMENT.include?(name)
|
|
421
|
+
conditions << [kind, name, exprs && unquote(exprs)]
|
|
396
422
|
end
|
|
423
|
+
conditions
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def match_condition?(name, exprs, host, remote_user, condition_count)
|
|
427
|
+
case name
|
|
428
|
+
when "all"
|
|
429
|
+
raise "all cannot be mixed with other conditions" if condition_count > 1
|
|
430
|
+
|
|
431
|
+
true
|
|
432
|
+
when "host"
|
|
433
|
+
negated = exprs.start_with?('!')
|
|
434
|
+
exprs = exprs[1..-1] if negated
|
|
435
|
+
negated ^ exprs.split(",").any? { |expr| host =~ pattern2regex(expr) }
|
|
436
|
+
when "originalhost"
|
|
437
|
+
match_pattern_list(host, exprs)
|
|
438
|
+
when "user"
|
|
439
|
+
match_pattern_list(remote_user, exprs, ignore_case: false)
|
|
440
|
+
when "localuser"
|
|
441
|
+
match_pattern_list(local_user, exprs, ignore_case: false)
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# OpenSSH's match_pattern_list: true if any pattern matches and no "!pattern" does.
|
|
446
|
+
def match_pattern_list(string, list, ignore_case: true)
|
|
447
|
+
matched = false
|
|
448
|
+
list.split(",").each do |pattern|
|
|
449
|
+
negated = pattern.start_with?('!')
|
|
450
|
+
next unless string =~ pattern2regex(negated ? pattern[1..-1] : pattern, ignore_case: ignore_case)
|
|
451
|
+
return false if negated
|
|
452
|
+
|
|
453
|
+
matched = true
|
|
454
|
+
end
|
|
455
|
+
matched
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def warn_unsupported_match(kind, location)
|
|
459
|
+
@warned_unsupported_matches ||= Set.new
|
|
460
|
+
return unless @warned_unsupported_matches.add?([location, kind])
|
|
461
|
+
|
|
462
|
+
Kernel.warn("net-ssh: #{location}: unsupported Match condition '#{kind}', the Match block is skipped")
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def local_user
|
|
466
|
+
Etc.getpwuid&.name || Etc.getlogin
|
|
467
|
+
end
|
|
397
468
|
|
|
398
|
-
|
|
469
|
+
# As in OpenSSH's argv_split, "#" starts a comment only at the start of a token and outside quotes.
|
|
470
|
+
def uncomment(string)
|
|
471
|
+
string[/\A(?:\s*(?!#)(?:\\.|"[^"]*"?|'[^']*'?|[^\s"'])+)*/]
|
|
399
472
|
end
|
|
400
473
|
|
|
401
474
|
def unquote(string)
|
|
@@ -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+.
|