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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.dockerignore +3 -0
  4. data/.github/pull_request_template.md +1 -0
  5. data/.github/workflows/changelog.yml +18 -0
  6. data/.github/workflows/ci-with-docker.yml +20 -9
  7. data/.github/workflows/ci.yml +10 -20
  8. data/.github/workflows/rubocop.yml +3 -1
  9. data/.rubocop.yml +5 -1
  10. data/.rubocop_todo.yml +4 -8
  11. data/CHANGES.txt +122 -0
  12. data/CONTRIBUTING.md +34 -0
  13. data/Dockerfile +4 -1
  14. data/Dockerfile.openssl3 +1 -1
  15. data/Dockerfile.zlib_ng +29 -0
  16. data/Gemfile +2 -0
  17. data/README.md +74 -57
  18. data/Rakefile +30 -12
  19. data/appveyor.yml +4 -16
  20. data/changelog.d/README.md +1 -0
  21. data/docker-compose.yml +1 -9
  22. data/lib/net/ssh/authentication/agent.rb +35 -11
  23. data/lib/net/ssh/authentication/ed25519.rb +140 -37
  24. data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
  25. data/lib/net/ssh/authentication/key_manager.rb +42 -16
  26. data/lib/net/ssh/authentication/methods/abstract.rb +2 -8
  27. data/lib/net/ssh/authentication/methods/publickey.rb +3 -1
  28. data/lib/net/ssh/authentication/security_key.rb +71 -0
  29. data/lib/net/ssh/authentication/session.rb +29 -17
  30. data/lib/net/ssh/buffer.rb +6 -1
  31. data/lib/net/ssh/buffered_io.rb +40 -3
  32. data/lib/net/ssh/config.rb +30 -13
  33. data/lib/net/ssh/connection/channel.rb +49 -2
  34. data/lib/net/ssh/connection/session.rb +151 -13
  35. data/lib/net/ssh/errors.rb +24 -6
  36. data/lib/net/ssh/key_factory.rb +11 -4
  37. data/lib/net/ssh/known_hosts.rb +62 -8
  38. data/lib/net/ssh/multiplex/client.rb +162 -0
  39. data/lib/net/ssh/multiplex/control_path.rb +120 -0
  40. data/lib/net/ssh/multiplex/proxy_transport.rb +218 -0
  41. data/lib/net/ssh/multiplex.rb +126 -0
  42. data/lib/net/ssh/prompt.rb +3 -3
  43. data/lib/net/ssh/proxy/command.rb +28 -3
  44. data/lib/net/ssh/proxy/jump.rb +71 -8
  45. data/lib/net/ssh/service/forward.rb +44 -27
  46. data/lib/net/ssh/test/extensions.rb +3 -2
  47. data/lib/net/ssh/test/socket.rb +1 -1
  48. data/lib/net/ssh/test.rb +2 -1
  49. data/lib/net/ssh/transport/aes128_gcm.rb +40 -0
  50. data/lib/net/ssh/transport/aes256_gcm.rb +40 -0
  51. data/lib/net/ssh/transport/algorithms.rb +96 -34
  52. data/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +89 -16
  53. data/lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb +20 -1
  54. data/lib/net/ssh/transport/cipher_factory.rb +32 -8
  55. data/lib/net/ssh/transport/constants.rb +1 -0
  56. data/lib/net/ssh/transport/gcm_cipher.rb +228 -0
  57. data/lib/net/ssh/transport/hmac/abstract.rb +16 -0
  58. data/lib/net/ssh/transport/hmac/sha1_etm.rb +12 -0
  59. data/lib/net/ssh/transport/hmac.rb +2 -0
  60. data/lib/net/ssh/transport/kex/curve25519_sha256.rb +41 -9
  61. data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +14 -6
  62. data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +8 -0
  63. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +45 -15
  64. data/lib/net/ssh/transport/openssl.rb +2 -1
  65. data/lib/net/ssh/transport/packet_stream.rb +19 -3
  66. data/lib/net/ssh/transport/server_version.rb +33 -13
  67. data/lib/net/ssh/transport/session.rb +61 -46
  68. data/lib/net/ssh/transport/state.rb +1 -1
  69. data/lib/net/ssh/verifiers/always.rb +43 -8
  70. data/lib/net/ssh/version.rb +3 -3
  71. data/lib/net/ssh.rb +148 -63
  72. data/net-ssh-public_cert.pem +7 -7
  73. data/net-ssh.gemspec +6 -8
  74. data.tar.gz.sig +0 -0
  75. metadata +64 -46
  76. metadata.gz.sig +0 -0
  77. data/Gemfile.noed25519 +0 -12
  78. data/Gemfile.norbnacl +0 -12
@@ -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
- super
188
- rescue Errno::ECONNRESET => e
189
- debug { "connection was reset => shallowing exception:#{e}" }
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
@@ -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
- if line =~ /^\s*(\S+)\s*=(.*)$/
92
- key, value = $1, $2
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
- %w[proxyjump proxycommand].each do |proxy_key|
156
- if (proxy_value = settings.delete(proxy_key))
157
- settings['proxy'] ||= [proxy_key, proxy_value]
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
- require 'net/ssh/proxy/jump'
310
- Net::SSH::Proxy::Jump.new(value)
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 = 0x20000, &on_confirm_open)
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
- @on_extended_data.call(self, type, data) if @on_extended_data
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] : 0x20000)
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
- channels.each { |id, channel| channel.close }
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
- local_id = get_next_channel_id
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
- msg = Buffer.from(:byte, CHANNEL_OPEN, :string, type, :long, local_id,
343
- :long, channel.local_maximum_window_size,
344
- :long, channel.local_maximum_packet_size, *extra)
345
- send_message(msg)
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
- # the returned string has an exitstatus method to query its exit status
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]) if channel[:result]
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
@@ -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 for the remote host, as reported by the socket.
75
+ # Returns the host name that was connected to.
66
76
  def host
67
- @data && @data[:peer] && @data[:peer][:host]
77
+ session&.host
68
78
  end
69
79
 
70
- # Returns the port number for the remote host, as reported by the socket.
80
+ # Returns the port number that was connected to.
71
81
  def port
72
- @data && @data[:peer] && @data[:peer][:port]
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
- @data && @data[:peer] && @data[:peer][:ip]
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