net-ssh 4.0.0.alpha3 → 4.0.0.alpha4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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