net-ssh 4.0.0.alpha3 → 4.0.0.alpha4

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.
@@ -70,7 +70,8 @@ module Net; module SSH; module Authentication
70
70
 
71
71
  debug { "trying #{name}" }
72
72
  begin
73
- method = Methods.const_get(name.split(/\W+/).map { |p| p.capitalize }.join).new(self, :key_manager => key_manager)
73
+ auth_class = Methods.const_get(name.split(/\W+/).map { |p| p.capitalize }.join)
74
+ method = auth_class.new(self, key_manager: key_manager, password_prompt: options[:password_prompt])
74
75
  rescue NameError
75
76
  debug{"Mechanism #{name} was requested, but isn't a known type. Ignoring it."}
76
77
  next
@@ -59,7 +59,7 @@ module Net; module SSH
59
59
  # given +files+ (defaulting to the list of files returned by
60
60
  # #default_files), translates the resulting hash into the options
61
61
  # recognized by Net::SSH, and returns them.
62
- def for(host, files=default_files)
62
+ def for(host, files=expandable_default_files)
63
63
  translate(files.inject({}) { |settings, file|
64
64
  load(file, host, settings)
65
65
  })
@@ -239,6 +239,17 @@ module Net; module SSH
239
239
 
240
240
  private
241
241
 
242
+ def expandable_default_files
243
+ default_files.keep_if do |path|
244
+ begin
245
+ File.expand_path(path)
246
+ true
247
+ rescue ArgumentError
248
+ false
249
+ end
250
+ end
251
+ end
252
+
242
253
  # Converts an ssh_config pattern into a regex for matching against
243
254
  # host names.
244
255
  def pattern2regex(pattern)
@@ -0,0 +1,110 @@
1
+ require 'net/ssh/loggable'
2
+ require 'net/ssh/ruby_compat'
3
+
4
+ module Net; module SSH; module Connection
5
+ # EventLoop can be shared across multiple sessions
6
+ #
7
+ # one issue is with blocks passed to loop, etc.
8
+ # they should get current session as parameter, but in
9
+ # case you're using multiple sessions in an event loop it doesnt makes sense
10
+ # and we don't pass session.
11
+ class EventLoop
12
+ include Loggable
13
+
14
+ def initialize(logger=nil)
15
+ self.logger = logger
16
+ @sessions = []
17
+ end
18
+
19
+ def register(session)
20
+ @sessions << session
21
+ end
22
+
23
+ # process until timeout
24
+ # if a block is given a session will be removed from loop
25
+ # if block returns false for that session
26
+ def process(wait = nil, &block)
27
+ return false unless ev_preprocess(&block)
28
+
29
+ ev_select_and_postprocess(wait)
30
+ end
31
+
32
+ # process the event loop but only for the sepcified session
33
+ def process_only(session, wait = nil)
34
+ orig_sessions = @sessions
35
+ begin
36
+ @sessions = [session]
37
+ return false unless ev_preprocess
38
+ ev_select_and_postprocess(wait)
39
+ ensure
40
+ @sessions = orig_sessions
41
+ end
42
+ end
43
+
44
+ # Call preprocess on each session. If block given and that
45
+ # block retuns false then we exit the processing
46
+ def ev_preprocess(&block)
47
+ return false if block_given? && !yield(self)
48
+ @sessions.each(&:ev_preprocess)
49
+ return false if block_given? && !yield(self)
50
+ return true
51
+ end
52
+
53
+ def ev_select_and_postprocess(wait)
54
+ owners = {}
55
+ r = []
56
+ w = []
57
+ minwait = nil
58
+ @sessions.each do |session|
59
+ sr,sw,actwait = session.ev_do_calculate_rw_wait(wait)
60
+ minwait = actwait if actwait && (minwait.nil? || actwait < minwait)
61
+ r.push(*sr)
62
+ w.push(*sw)
63
+ sr.each { |ri| owners[ri] = session }
64
+ sw.each { |wi| owners[wi] = session }
65
+ end
66
+
67
+ readers, writers, = Net::SSH::Compat.io_select(r, w, nil, minwait)
68
+
69
+ fired_sessions = {}
70
+
71
+ readers.each do |reader|
72
+ session = owners[reader]
73
+ (fired_sessions[session] ||= {r: [],w: []})[:r] << reader
74
+ end if readers
75
+ writers.each do |writer|
76
+ session = owners[writer]
77
+ (fired_sessions[session] ||= {r: [],w: []})[:w] << writer
78
+ end if writers
79
+
80
+ fired_sessions.each do |s,rw|
81
+ s.ev_do_handle_events(rw[:r],rw[:w])
82
+ end
83
+
84
+ @sessions.each { |s| s.ev_do_postprocess(fired_sessions.key?(s)) }
85
+ true
86
+ end
87
+ end
88
+
89
+ # optimized version for a single session
90
+ class SingleSessionEventLoop < EventLoop
91
+ # Compatibility for original single session event loops:
92
+ # we call block with session as argument
93
+ def ev_preprocess(&block)
94
+ return false if block_given? && !yield(@sessions.first)
95
+ @sessions.each(&:ev_preprocess)
96
+ return false if block_given? && !yield(@sessions.first)
97
+ return true
98
+ end
99
+
100
+ def ev_select_and_postprocess(wait)
101
+ raise "Only one session expected" unless @sessions.count == 1
102
+ session = @sessions.first
103
+ sr,sw,actwait = session.ev_do_calculate_rw_wait(wait)
104
+ readers, writers, = Net::SSH::Compat.io_select(sr, sw, nil, actwait)
105
+
106
+ session.ev_do_handle_events(readers,writers)
107
+ session.ev_do_postprocess(!((readers.nil? || readers.empty?) && (writers.nil? || writers.empty?)))
108
+ end
109
+ end
110
+ end; end; end
@@ -33,8 +33,8 @@ class Keepalive
33
33
  (options[:keepalive_maxcount] || 3).to_i
34
34
  end
35
35
 
36
- def send_as_needed(readers, writers)
37
- return unless readers.nil? && writers.nil?
36
+ def send_as_needed(was_events)
37
+ return if was_events
38
38
  return unless should_send?
39
39
  info { "sending keepalive #{@unresponded_keepalive_count}" }
40
40
 
@@ -4,6 +4,7 @@ require 'net/ssh/connection/channel'
4
4
  require 'net/ssh/connection/constants'
5
5
  require 'net/ssh/service/forward'
6
6
  require 'net/ssh/connection/keepalive'
7
+ require 'net/ssh/connection/event_loop'
7
8
 
8
9
  module Net; module SSH; module Connection
9
10
 
@@ -81,6 +82,9 @@ module Net; module SSH; module Connection
81
82
  @max_win_size = (options.has_key?(:max_win_size) ? options[:max_win_size] : 0x20000)
82
83
 
83
84
  @keepalive = Keepalive.new(self)
85
+
86
+ @event_loop = options[:event_loop] || SingleSessionEventLoop.new
87
+ @event_loop.register(self)
84
88
  end
85
89
 
86
90
  # Retrieves a custom property from this instance. This can be used to
@@ -186,6 +190,8 @@ module Net; module SSH; module Connection
186
190
  # This will also cause all active channels to be processed once each (see
187
191
  # Net::SSH::Connection::Channel#on_process).
188
192
  #
193
+ # TODO revise example
194
+ #
189
195
  # # process multiple Net::SSH connections in parallel
190
196
  # connections = [
191
197
  # Net::SSH.start("host1", ...),
@@ -203,13 +209,10 @@ module Net; module SSH; module Connection
203
209
  # break if connections.empty?
204
210
  # end
205
211
  def process(wait=nil, &block)
206
- return false unless preprocess(&block)
207
-
208
- r = listeners.keys
209
- w = r.select { |w2| w2.respond_to?(:pending_write?) && w2.pending_write? }
210
- readers, writers, = Net::SSH::Compat.io_select(r, w, nil, io_select_wait(wait))
211
-
212
- postprocess(readers, writers)
212
+ @event_loop.process(wait, &block)
213
+ rescue
214
+ force_channel_cleanup_on_close if closed?
215
+ raise
213
216
  end
214
217
 
215
218
  # This is called internally as part of #process. It dispatches any
@@ -217,19 +220,38 @@ module Net; module SSH; module Connection
217
220
  # for any active channels. If a block is given, it is invoked at the
218
221
  # start of the method and again at the end, and if the block ever returns
219
222
  # false, this method returns false. Otherwise, it returns true.
220
- def preprocess
223
+ def preprocess(&block)
221
224
  return false if block_given? && !yield(self)
222
- dispatch_incoming_packets
223
- channels.each { |id, channel| channel.process unless channel.local_closed? }
225
+ ev_preprocess(&block)
224
226
  return false if block_given? && !yield(self)
225
227
  return true
226
228
  end
227
229
 
228
- # This is called internally as part of #process. It loops over the given
229
- # arrays of reader IO's and writer IO's, processing them as needed, and
230
+ # Called by event loop to process available data before going to
231
+ # event multiplexing
232
+ def ev_preprocess(&block)
233
+ dispatch_incoming_packets
234
+ each_channel { |id, channel| channel.process unless channel.local_closed? }
235
+ end
236
+
237
+ # Returns the file descriptors the event loop should wait for read/write events,
238
+ # we also return the max wait
239
+ def ev_do_calculate_rw_wait(wait)
240
+ r = listeners.keys
241
+ w = r.select { |w2| w2.respond_to?(:pending_write?) && w2.pending_write? }
242
+ [r,w,io_select_wait(wait)]
243
+ end
244
+
245
+ # This is called internally as part of #process.
246
+ def postprocess(readers, writers)
247
+ ev_do_handle_events(readers, writers)
248
+ end
249
+
250
+ # It loops over the given arrays of reader IO's and writer IO's,
251
+ # processing them as needed, and
230
252
  # then calls Net::SSH::Transport::Session#rekey_as_needed to allow the
231
253
  # transport layer to rekey. Then returns true.
232
- def postprocess(readers, writers)
254
+ def ev_do_handle_events(readers, writers)
233
255
  Array(readers).each do |reader|
234
256
  if listeners[reader]
235
257
  listeners[reader].call(reader)
@@ -244,11 +266,14 @@ module Net; module SSH; module Connection
244
266
  Array(writers).each do |writer|
245
267
  writer.send_pending
246
268
  end
269
+ end
247
270
 
248
- @keepalive.send_as_needed(readers, writers)
271
+ # calls Net::SSH::Transport::Session#rekey_as_needed to allow the
272
+ # transport layer to rekey
273
+ def ev_do_postprocess(was_events)
274
+ @keepalive.send_as_needed(was_events)
249
275
  transport.rekey_as_needed
250
-
251
- return true
276
+ true
252
277
  end
253
278
 
254
279
  # Send a global request of the given type. The +extra+ parameters must
@@ -330,7 +355,7 @@ module Net; module SSH; module Connection
330
355
  open_channel do |channel|
331
356
  channel.exec(command) do |ch, success|
332
357
  raise "could not execute command: #{command.inspect}" unless success
333
-
358
+
334
359
  channel.on_data do |ch2, data|
335
360
  if block
336
361
  block.call(ch2, :stdout, data)
@@ -472,6 +497,11 @@ module Net; module SSH; module Connection
472
497
 
473
498
  private
474
499
 
500
+ # iterate channels with the posibility of callbacks opening new channels during the iteration
501
+ def each_channel(&block)
502
+ channels.dup.each(&block)
503
+ end
504
+
475
505
  # Read all pending packets from the connection and dispatch them as
476
506
  # appropriate. Returns as soon as there are no more pending packets.
477
507
  def dispatch_incoming_packets
@@ -495,14 +525,18 @@ module Net; module SSH; module Connection
495
525
 
496
526
  def force_channel_cleanup_on_close
497
527
  channels.each do |id, channel|
498
- channel.remote_closed!
499
- channel.close
500
-
501
- cleanup_channel(channel)
502
- channel.do_close
528
+ channel_closed(channel)
503
529
  end
504
530
  end
505
531
 
532
+ def channel_closed(channel)
533
+ channel.remote_closed!
534
+ channel.close
535
+
536
+ cleanup_channel(channel)
537
+ channel.do_close
538
+ end
539
+
506
540
  # Invoked when a global request is received. The registered global
507
541
  # request callback will be invoked, if one exists, and the necessary
508
542
  # reply returned.
@@ -611,11 +645,7 @@ module Net; module SSH; module Connection
611
645
  info { "channel_close: #{packet[:local_id]}" }
612
646
 
613
647
  channel = channels[packet[:local_id]]
614
- channel.remote_closed!
615
- channel.close
616
-
617
- cleanup_channel(channel)
618
- channel.do_close
648
+ channel_closed(channel)
619
649
  end
620
650
 
621
651
  def channel_success(packet)
@@ -1,6 +1,10 @@
1
1
  require 'net/ssh/transport/openssl'
2
2
  require 'net/ssh/prompt'
3
- require 'net/ssh/authentication/ed25519'
3
+
4
+ begin
5
+ require 'net/ssh/authentication/ed25519'
6
+ rescue Gem::LoadError => e # rubocop:disable Lint/HandleExceptions
7
+ end
4
8
 
5
9
  module Net; module SSH
6
10
 
@@ -22,12 +26,10 @@ module Net; module SSH
22
26
  }
23
27
  if defined?(OpenSSL::PKey::EC)
24
28
  MAP["ecdsa"] = OpenSSL::PKey::EC
25
- MAP["ed25519"] = ED25519::PrivKey
29
+ MAP["ed25519"] = ED25519::PrivKey if defined? ED25519
26
30
  end
27
31
 
28
32
  class <<self
29
- include Prompt
30
-
31
33
  # Fetch an OpenSSL key instance by its SSH name. It will be a new,
32
34
  # empty key of the given type.
33
35
  def get(name)
@@ -39,9 +41,9 @@ module Net; module SSH
39
41
  # appropriately. The new key is returned. If the key itself is
40
42
  # encrypted (requiring a passphrase to use), the user will be
41
43
  # prompted to enter their password unless passphrase works.
42
- def load_private_key(filename, passphrase=nil, ask_passphrase=true)
44
+ def load_private_key(filename, passphrase=nil, ask_passphrase=true, prompt=Prompt.default)
43
45
  data = File.read(File.expand_path(filename))
44
- load_data_private_key(data, passphrase, ask_passphrase, filename)
46
+ load_data_private_key(data, passphrase, ask_passphrase, filename, prompt)
45
47
  end
46
48
 
47
49
  # Loads a private key. It will correctly determine
@@ -49,58 +51,32 @@ module Net; module SSH
49
51
  # appropriately. The new key is returned. If the key itself is
50
52
  # encrypted (requiring a passphrase to use), the user will be
51
53
  # prompted to enter their password unless passphrase works.
52
- def load_data_private_key(data, passphrase=nil, ask_passphrase=true, filename="")
53
- if OpenSSL::PKey.respond_to?(:read)
54
- pkey_read = true
55
- error_class = ArgumentError
56
- else
57
- pkey_read = false
58
- if data.match(/-----BEGIN DSA PRIVATE KEY-----/)
59
- key_type = OpenSSL::PKey::DSA
60
- error_class = OpenSSL::PKey::DSAError
61
- elsif data.match(/-----BEGIN RSA PRIVATE KEY-----/)
62
- key_type = OpenSSL::PKey::RSA
63
- error_class = OpenSSL::PKey::RSAError
64
- elsif data.match(/-----BEGIN EC PRIVATE KEY-----/) && defined?(OpenSSL::PKey::EC)
65
- key_type = OpenSSL::PKey::EC
66
- error_class = OpenSSL::PKey::ECError
67
- elsif data.match(/-----BEGIN OPENSSH PRIVATE KEY-----/)
68
- openssh_key = true
69
- key_type = ED25519::PrivKey
70
- elsif data.match(/-----BEGIN (.+) PRIVATE KEY-----/)
71
- raise OpenSSL::PKey::PKeyError, "not a supported key type '#{$1}'"
72
- else
73
- raise OpenSSL::PKey::PKeyError, "not a private key (#{filename})"
74
- end
75
- end
54
+ def load_data_private_key(data, passphrase=nil, ask_passphrase=true, filename="", prompt=Prompt.default)
55
+ key_read, error_class = classify_key(data, filename)
76
56
 
77
57
  encrypted_key = data.match(/ENCRYPTED/)
78
- openssh_key = data.match(/-----BEGIN OPENSSH PRIVATE KEY-----/)
79
58
  tries = 0
80
59
 
81
- begin
82
- if openssh_key
83
- ED25519::PrivKey.read(data, passphrase || 'invalid')
84
- else
85
- if pkey_read
86
- return OpenSSL::PKey.read(data, passphrase || 'invalid')
87
- else
88
- return key_type.new(data, passphrase || 'invalid')
89
- end
90
- end
91
- rescue error_class
92
- if encrypted_key && ask_passphrase
93
- tries += 1
94
- if tries <= 3
95
- passphrase = prompt("Enter passphrase for #{filename}:", false)
96
- retry
60
+ prompter = nil
61
+ result =
62
+ begin
63
+ key_read[data, passphrase || 'invalid']
64
+ rescue error_class
65
+ if encrypted_key && ask_passphrase
66
+ tries += 1
67
+ if tries <= 3
68
+ prompter ||= prompt.start(type: 'private_key', filename: filename, sha: Digest::SHA256.digest(data))
69
+ passphrase = prompter.ask("Enter passphrase for #{filename}:", false)
70
+ retry
71
+ else
72
+ raise
73
+ end
97
74
  else
98
75
  raise
99
76
  end
100
- else
101
- raise
102
77
  end
103
- end
78
+ prompter.success if prompter
79
+ result
104
80
  end
105
81
 
106
82
  # Loads a public key from a file. It will correctly determine whether
@@ -129,6 +105,28 @@ module Net; module SSH
129
105
  reader = Net::SSH::Buffer.new(blob)
130
106
  reader.read_key or raise OpenSSL::PKey::PKeyError, "not a public key #{filename.inspect}"
131
107
  end
108
+
109
+ private
110
+
111
+ # Determine whether the file describes an RSA or DSA key, and return how load it
112
+ # appropriately.
113
+ def classify_key(data, filename)
114
+ if data.match(/-----BEGIN OPENSSH PRIVATE KEY-----/)
115
+ return ->(key_data, passphrase) { ED25519::PrivKey.read(key_data, passphrase) }, ArgumentError
116
+ elsif OpenSSL::PKey.respond_to?(:read)
117
+ return ->(key_data, passphrase) { OpenSSL::PKey.read(key_data, passphrase) }, ArgumentError
118
+ elsif data.match(/-----BEGIN DSA PRIVATE KEY-----/)
119
+ return ->(key_data, passphrase) { OpenSSL::PKey::DSA.new(key_data, passphrase) }, OpenSSL::PKey::DSAError
120
+ elsif data.match(/-----BEGIN RSA PRIVATE KEY-----/)
121
+ return ->(key_data, passphrase) { OpenSSL::PKey::RSA.new(key_data, passphrase) }, OpenSSL::PKey::RSAError
122
+ elsif data.match(/-----BEGIN EC PRIVATE KEY-----/) && defined?(OpenSSL::PKey::EC)
123
+ return ->(key_data, passphrase) { OpenSSL::PKey::EC.new(key_data, passphrase) }, OpenSSL::PKey::ECError
124
+ elsif data.match(/-----BEGIN (.+) PRIVATE KEY-----/)
125
+ raise OpenSSL::PKey::PKeyError, "not a supported key type '#{$1}'"
126
+ else
127
+ raise OpenSSL::PKey::PKeyError, "not a private key (#{filename})"
128
+ end
129
+ end
132
130
  end
133
131
 
134
132
  end