net-ssh 8.0.0.rc3 → 8.0.0.rc5

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 (46) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +4 -1
  3. data/.github/pull_request_template.md +1 -0
  4. data/.github/workflows/changelog.yml +43 -0
  5. data/CHANGES.txt +40 -0
  6. data/CONTRIBUTING.md +35 -0
  7. data/README.md +65 -48
  8. data/Rakefile +48 -0
  9. data/changelog.d/README.md +1 -0
  10. data/lib/net/ssh/authentication/agent.rb +4 -1
  11. data/lib/net/ssh/authentication/ed25519.rb +1 -1
  12. data/lib/net/ssh/authentication/key_manager.rb +22 -12
  13. data/lib/net/ssh/authentication/methods/abstract.rb +2 -8
  14. data/lib/net/ssh/authentication/methods/publickey.rb +3 -1
  15. data/lib/net/ssh/authentication/security_key.rb +71 -0
  16. data/lib/net/ssh/authentication/session.rb +29 -17
  17. data/lib/net/ssh/buffer.rb +6 -1
  18. data/lib/net/ssh/buffered_io.rb +40 -3
  19. data/lib/net/ssh/config.rb +16 -1
  20. data/lib/net/ssh/connection/channel.rb +49 -2
  21. data/lib/net/ssh/connection/session.rb +58 -8
  22. data/lib/net/ssh/errors.rb +24 -6
  23. data/lib/net/ssh/key_factory.rb +7 -1
  24. data/lib/net/ssh/known_hosts.rb +18 -8
  25. data/lib/net/ssh/multiplex/client.rb +162 -0
  26. data/lib/net/ssh/multiplex/control_path.rb +120 -0
  27. data/lib/net/ssh/multiplex/proxy_transport.rb +218 -0
  28. data/lib/net/ssh/multiplex.rb +130 -0
  29. data/lib/net/ssh/proxy/jump.rb +40 -2
  30. data/lib/net/ssh/service/forward.rb +26 -21
  31. data/lib/net/ssh/test.rb +2 -1
  32. data/lib/net/ssh/transport/algorithms.rb +71 -23
  33. data/lib/net/ssh/transport/hmac/sha1_etm.rb +12 -0
  34. data/lib/net/ssh/transport/hmac.rb +2 -0
  35. data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +8 -0
  36. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +45 -15
  37. data/lib/net/ssh/transport/openssl.rb +2 -1
  38. data/lib/net/ssh/transport/session.rb +21 -44
  39. data/lib/net/ssh/verifiers/always.rb +0 -3
  40. data/lib/net/ssh/version.rb +1 -1
  41. data/lib/net/ssh.rb +132 -56
  42. data/net-ssh.gemspec +1 -1
  43. data/rakelib/changelog.rb +229 -0
  44. data.tar.gz.sig +0 -0
  45. metadata +23 -6
  46. metadata.gz.sig +0 -0
@@ -69,27 +69,22 @@ module Net
69
69
 
70
70
  attempted = []
71
71
 
72
- @auth_methods.each do |name|
73
- next unless @allowed_auth_methods.include?(name)
74
-
75
- attempted << name
76
-
77
- debug { "trying #{name}" }
78
- begin
79
- auth_class = Methods.const_get(name.split(/\W+/).map { |p| p.capitalize }.join)
80
- method = auth_class.new(self,
81
- key_manager: key_manager, password_prompt: options[:password_prompt],
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
- return true if method.authenticate(next_service, username, password)
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
@@ -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 NotImplementedError, "unsupported key type `#{type}'"
347
+ raise Net::SSH::UnsupportedKeyType, "unsupported key type `#{type}'"
343
348
  end
344
349
 
345
350
  return key
@@ -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
@@ -95,6 +99,7 @@ module Net
95
99
  next if value.nil?
96
100
 
97
101
  key.downcase!
102
+ value = uncomment(value)
98
103
  value = unquote(value)
99
104
 
100
105
  value = case value.strip
@@ -217,6 +222,7 @@ module Net
217
222
  bindaddress: :bind_address,
218
223
  compression: :compression,
219
224
  compressionlevel: :compression_level,
225
+ controlpath: :control_path,
220
226
  certificatefile: :keycerts,
221
227
  connecttimeout: :timeout,
222
228
  forwardagent: :forward_agent,
@@ -230,7 +236,7 @@ module Net
230
236
  user: :user,
231
237
  checkhostip: :check_host_ip
232
238
  }.freeze
233
- def translate_config_key(hash, key, value, settings)
239
+ def translate_config_key(hash, key, value, settings) # rubocop:disable Metrics/AbcSize
234
240
  case key
235
241
  when :stricthostkeychecking
236
242
  hash[:verify_host_key] = translate_verify_host_key(value)
@@ -246,6 +252,8 @@ module Net
246
252
  hash[:host_key] = value.split(/,/)
247
253
  when :hostname
248
254
  hash[:host_name] = value.gsub(/%h/, settings['host'])
255
+ when :kexalgorithms
256
+ hash[:kex] = value.split(/,/)
249
257
  when :macs
250
258
  hash[:hmac] = value.split(/,/)
251
259
  when :serveralivecountmax
@@ -276,6 +284,8 @@ module Net
276
284
  if (proxy = setup_proxy(*value))
277
285
  hash[:proxy] = proxy
278
286
  end
287
+ when :pubkeyacceptedalgorithms, :pubkeyacceptedkeytypes
288
+ hash[:pubkey_algorithms] = value.split(/,/)
279
289
  when :pubkeyauthentication
280
290
  if value
281
291
  (hash[:auth_methods] << 'publickey').uniq!
@@ -399,6 +409,11 @@ module Net
399
409
  !condition_matches.empty? && condition_matches.all?
400
410
  end
401
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
+
402
417
  def unquote(string)
403
418
  string =~ /^"(.*)"$/ ? Regexp.last_match(1) : string
404
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
 
@@ -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] : 0x20000)
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
- channels.each { |id, channel| channel.close }
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
- # the returned string has an exitstatus method to query its exit status
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]) if channel[:result]
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
@@ -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
@@ -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? && !/^(ssh-(rsa|dss|ed25519)|ecdsa-sha2-nistp\d+)(-cert-v01@openssh\.com)?$/.match(blob)
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)
@@ -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 < Delegator
12
- def initialize(key, comment: nil) # rubocop:disable Lint/MissingSuper
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