net-ssh 2.9.2 → 4.0.0

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 (138) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.gitignore +6 -0
  4. data/.rubocop.yml +5 -0
  5. data/.rubocop_todo.yml +1129 -0
  6. data/.travis.yml +41 -5
  7. data/CHANGES.txt +133 -1
  8. data/Gemfile +13 -0
  9. data/Gemfile.norbnacl +10 -0
  10. data/Gemfile.norbnacl.lock +41 -0
  11. data/ISSUE_TEMPLATE.md +30 -0
  12. data/README.rdoc +26 -81
  13. data/Rakefile +63 -45
  14. data/appveyor.yml +51 -0
  15. data/lib/net/ssh/authentication/agent.rb +174 -14
  16. data/lib/net/ssh/authentication/ed25519.rb +137 -0
  17. data/lib/net/ssh/authentication/ed25519_loader.rb +21 -0
  18. data/lib/net/ssh/authentication/key_manager.rb +36 -30
  19. data/lib/net/ssh/authentication/methods/abstract.rb +4 -0
  20. data/lib/net/ssh/authentication/methods/keyboard_interactive.rb +16 -9
  21. data/lib/net/ssh/authentication/methods/password.rb +17 -4
  22. data/lib/net/ssh/authentication/pageant.rb +166 -45
  23. data/lib/net/ssh/authentication/session.rb +3 -2
  24. data/lib/net/ssh/buffer.rb +49 -10
  25. data/lib/net/ssh/buffered_io.rb +17 -12
  26. data/lib/net/ssh/config.rb +39 -8
  27. data/lib/net/ssh/connection/channel.rb +42 -20
  28. data/lib/net/ssh/connection/event_loop.rb +114 -0
  29. data/lib/net/ssh/connection/keepalive.rb +2 -2
  30. data/lib/net/ssh/connection/session.rb +120 -34
  31. data/lib/net/ssh/errors.rb +6 -6
  32. data/lib/net/ssh/key_factory.rb +49 -43
  33. data/lib/net/ssh/known_hosts.rb +49 -3
  34. data/lib/net/ssh/prompt.rb +47 -78
  35. data/lib/net/ssh/proxy/command.rb +31 -5
  36. data/lib/net/ssh/proxy/http.rb +15 -11
  37. data/lib/net/ssh/proxy/https.rb +49 -0
  38. data/lib/net/ssh/proxy/socks4.rb +2 -1
  39. data/lib/net/ssh/proxy/socks5.rb +3 -2
  40. data/lib/net/ssh/ruby_compat.rb +2 -29
  41. data/lib/net/ssh/service/forward.rb +2 -2
  42. data/lib/net/ssh/test/channel.rb +7 -0
  43. data/lib/net/ssh/test/extensions.rb +17 -0
  44. data/lib/net/ssh/test/kex.rb +4 -4
  45. data/lib/net/ssh/test/packet.rb +18 -2
  46. data/lib/net/ssh/test/script.rb +16 -2
  47. data/lib/net/ssh/test/socket.rb +1 -1
  48. data/lib/net/ssh/test.rb +5 -5
  49. data/lib/net/ssh/transport/algorithms.rb +92 -75
  50. data/lib/net/ssh/transport/cipher_factory.rb +19 -26
  51. data/lib/net/ssh/transport/ctr.rb +7 -9
  52. data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +20 -9
  53. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +5 -3
  54. data/lib/net/ssh/transport/kex/ecdh_sha2_nistp256.rb +1 -1
  55. data/lib/net/ssh/transport/key_expander.rb +1 -0
  56. data/lib/net/ssh/transport/openssl.rb +1 -1
  57. data/lib/net/ssh/transport/packet_stream.rb +11 -3
  58. data/lib/net/ssh/transport/server_version.rb +13 -6
  59. data/lib/net/ssh/transport/session.rb +20 -10
  60. data/lib/net/ssh/transport/state.rb +1 -1
  61. data/lib/net/ssh/verifiers/secure.rb +8 -10
  62. data/lib/net/ssh/version.rb +4 -4
  63. data/lib/net/ssh.rb +62 -14
  64. data/net-ssh-public_cert.pem +19 -18
  65. data/net-ssh.gemspec +34 -194
  66. data/support/arcfour_check.rb +1 -1
  67. data/support/ssh_tunnel_bug.rb +1 -1
  68. data.tar.gz.sig +0 -0
  69. metadata +125 -109
  70. metadata.gz.sig +0 -0
  71. data/Rudyfile +0 -96
  72. data/lib/net/ssh/authentication/agent/java_pageant.rb +0 -85
  73. data/lib/net/ssh/authentication/agent/socket.rb +0 -178
  74. data/setup.rb +0 -1585
  75. data/test/README.txt +0 -47
  76. data/test/authentication/methods/common.rb +0 -28
  77. data/test/authentication/methods/test_abstract.rb +0 -51
  78. data/test/authentication/methods/test_hostbased.rb +0 -114
  79. data/test/authentication/methods/test_keyboard_interactive.rb +0 -100
  80. data/test/authentication/methods/test_none.rb +0 -41
  81. data/test/authentication/methods/test_password.rb +0 -95
  82. data/test/authentication/methods/test_publickey.rb +0 -148
  83. data/test/authentication/test_agent.rb +0 -224
  84. data/test/authentication/test_key_manager.rb +0 -227
  85. data/test/authentication/test_session.rb +0 -107
  86. data/test/common.rb +0 -108
  87. data/test/configs/auth_off +0 -5
  88. data/test/configs/auth_on +0 -4
  89. data/test/configs/empty +0 -0
  90. data/test/configs/eqsign +0 -3
  91. data/test/configs/exact_match +0 -8
  92. data/test/configs/host_plus +0 -10
  93. data/test/configs/multihost +0 -4
  94. data/test/configs/negative_match +0 -6
  95. data/test/configs/nohost +0 -19
  96. data/test/configs/numeric_host +0 -4
  97. data/test/configs/send_env +0 -2
  98. data/test/configs/substitutes +0 -8
  99. data/test/configs/wild_cards +0 -14
  100. data/test/connection/test_channel.rb +0 -467
  101. data/test/connection/test_session.rb +0 -543
  102. data/test/known_hosts/github +0 -1
  103. data/test/manual/test_forward.rb +0 -285
  104. data/test/manual/test_pageant.rb +0 -37
  105. data/test/start/test_connection.rb +0 -53
  106. data/test/start/test_options.rb +0 -43
  107. data/test/start/test_transport.rb +0 -28
  108. data/test/test_all.rb +0 -11
  109. data/test/test_buffer.rb +0 -433
  110. data/test/test_buffered_io.rb +0 -63
  111. data/test/test_config.rb +0 -221
  112. data/test/test_key_factory.rb +0 -191
  113. data/test/test_known_hosts.rb +0 -13
  114. data/test/transport/hmac/test_md5.rb +0 -41
  115. data/test/transport/hmac/test_md5_96.rb +0 -27
  116. data/test/transport/hmac/test_none.rb +0 -34
  117. data/test/transport/hmac/test_ripemd160.rb +0 -36
  118. data/test/transport/hmac/test_sha1.rb +0 -36
  119. data/test/transport/hmac/test_sha1_96.rb +0 -27
  120. data/test/transport/hmac/test_sha2_256.rb +0 -37
  121. data/test/transport/hmac/test_sha2_256_96.rb +0 -27
  122. data/test/transport/hmac/test_sha2_512.rb +0 -37
  123. data/test/transport/hmac/test_sha2_512_96.rb +0 -27
  124. data/test/transport/kex/test_diffie_hellman_group14_sha1.rb +0 -13
  125. data/test/transport/kex/test_diffie_hellman_group1_sha1.rb +0 -146
  126. data/test/transport/kex/test_diffie_hellman_group_exchange_sha1.rb +0 -92
  127. data/test/transport/kex/test_diffie_hellman_group_exchange_sha256.rb +0 -34
  128. data/test/transport/kex/test_ecdh_sha2_nistp256.rb +0 -161
  129. data/test/transport/kex/test_ecdh_sha2_nistp384.rb +0 -38
  130. data/test/transport/kex/test_ecdh_sha2_nistp521.rb +0 -38
  131. data/test/transport/test_algorithms.rb +0 -324
  132. data/test/transport/test_cipher_factory.rb +0 -443
  133. data/test/transport/test_hmac.rb +0 -34
  134. data/test/transport/test_identity_cipher.rb +0 -40
  135. data/test/transport/test_packet_stream.rb +0 -1761
  136. data/test/transport/test_server_version.rb +0 -78
  137. data/test/transport/test_session.rb +0 -331
  138. data/test/transport/test_state.rb +0 -181
@@ -126,7 +126,7 @@ module Net; module SSH; module Connection
126
126
  @pending_requests = []
127
127
  @on_open_failed = @on_data = @on_extended_data = @on_process = @on_close = @on_eof = nil
128
128
  @on_request = {}
129
- @closing = @eof = @sent_eof = false
129
+ @closing = @eof = @sent_eof = @local_closed = @remote_closed = false
130
130
  end
131
131
 
132
132
  # A shortcut for accessing properties of the channel (see #properties).
@@ -189,12 +189,12 @@ module Net; module SSH; module Connection
189
189
  end
190
190
 
191
191
  # A hash of the valid PTY options (see #request_pty).
192
- VALID_PTY_OPTIONS = { :term => "xterm",
193
- :chars_wide => 80,
194
- :chars_high => 24,
195
- :pixels_wide => 640,
196
- :pixels_high => 480,
197
- :modes => {} }
192
+ VALID_PTY_OPTIONS = { term: "xterm",
193
+ chars_wide: 80,
194
+ chars_high: 24,
195
+ pixels_wide: 640,
196
+ pixels_high: 480,
197
+ modes: {} }
198
198
 
199
199
  # Requests that a pseudo-tty (or "pty") be made available for this channel.
200
200
  # This is useful when you want to invoke and interact with some kind of
@@ -269,24 +269,33 @@ module Net; module SSH; module Connection
269
269
  connection.loop { active? }
270
270
  end
271
271
 
272
- # Returns true if the channel is currently closing, but not actually
273
- # closed. A channel is closing when, for instance, #close has been
274
- # invoked, but the server has not yet responded with a CHANNEL_CLOSE
275
- # packet of its own.
272
+ # True if close() has been called; NOTE: if the channel has data waiting to
273
+ # be sent then the channel will close after all the data is sent. See
274
+ # closed?() to determine if we have actually sent CHANNEL_CLOSE to server.
275
+ # This may be true for awhile before closed? returns true if we are still
276
+ # sending buffered output to server.
276
277
  def closing?
277
278
  @closing
278
279
  end
279
280
 
280
- # Requests that the channel be closed. If the channel is already closing,
281
- # this does nothing, nor does it do anything if the channel has not yet
282
- # been confirmed open (see #do_open_confirmation). Otherwise, it sends a
283
- # CHANNEL_CLOSE message and marks the channel as closing.
281
+ # True if we have sent CHANNEL_CLOSE to the remote server.
282
+ def local_closed?
283
+ @local_closed
284
+ end
285
+
286
+ def remote_closed?
287
+ @remote_closed
288
+ end
289
+
290
+ def remote_closed!
291
+ @remote_closed = true
292
+ end
293
+
294
+ # Requests that the channel be closed. It only marks the channel to be closed
295
+ # the CHANNEL_CLOSE message will be sent from event loop
284
296
  def close
285
297
  return if @closing
286
- if remote_id
287
- @closing = true
288
- connection.send_message(Buffer.from(:byte, CHANNEL_CLOSE, :long, remote_id))
289
- end
298
+ @closing = true
290
299
  end
291
300
 
292
301
  # Returns true if the local end of the channel has declared that no more
@@ -311,10 +320,16 @@ module Net; module SSH; module Connection
311
320
  @on_process.call(self) if @on_process
312
321
  enqueue_pending_output
313
322
 
314
- if @eof and not @sent_eof and output.empty? and remote_id
323
+ if @eof and not @sent_eof and output.empty? and remote_id and not @local_closed
315
324
  connection.send_message(Buffer.from(:byte, CHANNEL_EOF, :long, remote_id))
316
325
  @sent_eof = true
317
326
  end
327
+
328
+ if @closing and not @local_closed and output.empty? and remote_id
329
+ connection.send_message(Buffer.from(:byte, CHANNEL_CLOSE, :long, remote_id))
330
+ @local_closed = true
331
+ connection.cleanup_channel(self)
332
+ end
318
333
  end
319
334
 
320
335
  # Registers a callback to be invoked when data packets are received by the
@@ -467,6 +482,7 @@ module Net; module SSH; module Connection
467
482
  # convenient helper methods (see #exec and #subsystem).
468
483
  def send_channel_request(request_name, *data, &callback)
469
484
  info { "sending channel request #{request_name.inspect}" }
485
+ fail "Channel open not yet confirmed, please call send_channel_request(or exec) from block of open_channel" unless remote_id
470
486
  msg = Buffer.from(:byte, CHANNEL_REQUEST,
471
487
  :long, remote_id, :string, request_name,
472
488
  :bool, !callback.nil?, *data)
@@ -613,6 +629,12 @@ module Net; module SSH; module Connection
613
629
 
614
630
  private
615
631
 
632
+ # Runs the SSH event loop until the remote confirmed channel open
633
+ # experimental api
634
+ def wait_until_open_confirmed
635
+ connection.loop { !remote_id }
636
+ end
637
+
616
638
  # Updates the local window size by the given amount. If the window
617
639
  # size drops to less than half of the local maximum (an arbitrary
618
640
  # threshold), a CHANNEL_WINDOW_ADJUST message will be sent to the
@@ -0,0 +1,114 @@
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
+ if readers
72
+ readers.each do |reader|
73
+ session = owners[reader]
74
+ (fired_sessions[session] ||= {r: [],w: []})[:r] << reader
75
+ end
76
+ end
77
+ if writers
78
+ writers.each do |writer|
79
+ session = owners[writer]
80
+ (fired_sessions[session] ||= {r: [],w: []})[:w] << writer
81
+ end
82
+ end
83
+
84
+ fired_sessions.each do |s,rw|
85
+ s.ev_do_handle_events(rw[:r],rw[:w])
86
+ end
87
+
88
+ @sessions.each { |s| s.ev_do_postprocess(fired_sessions.key?(s)) }
89
+ true
90
+ end
91
+ end
92
+
93
+ # optimized version for a single session
94
+ class SingleSessionEventLoop < EventLoop
95
+ # Compatibility for original single session event loops:
96
+ # we call block with session as argument
97
+ def ev_preprocess(&block)
98
+ return false if block_given? && !yield(@sessions.first)
99
+ @sessions.each(&:ev_preprocess)
100
+ return false if block_given? && !yield(@sessions.first)
101
+ return true
102
+ end
103
+
104
+ def ev_select_and_postprocess(wait)
105
+ raise "Only one session expected" unless @sessions.count == 1
106
+ session = @sessions.first
107
+ sr,sw,actwait = session.ev_do_calculate_rw_wait(wait)
108
+ readers, writers, = Net::SSH::Compat.io_select(sr, sw, nil, actwait)
109
+
110
+ session.ev_do_handle_events(readers,writers)
111
+ session.ev_do_postprocess(!((readers.nil? || readers.empty?) && (writers.nil? || writers.empty?)))
112
+ end
113
+ end
114
+ 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
 
@@ -77,10 +78,13 @@ module Net; module SSH; module Connection
77
78
  @on_global_request = {}
78
79
  @properties = (options[:properties] || {}).dup
79
80
 
80
- @max_pkt_size = (options.has_key?(:max_pkt_size) ? options[:max_pkt_size] : 0x8000)
81
- @max_win_size = (options.has_key?(:max_win_size) ? options[:max_win_size] : 0x20000)
81
+ @max_pkt_size = (options.key?(:max_pkt_size) ? options[:max_pkt_size] : 0x8000)
82
+ @max_win_size = (options.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
@@ -116,7 +120,11 @@ module Net; module SSH; module Connection
116
120
  def close
117
121
  info { "closing remaining channels (#{channels.length} open)" }
118
122
  channels.each { |id, channel| channel.close }
119
- loop(0.1) { channels.any? }
123
+ begin
124
+ loop(0.1) { channels.any? }
125
+ rescue Net::SSH::Disconnect
126
+ raise unless channels.empty?
127
+ end
120
128
  transport.close
121
129
  end
122
130
 
@@ -186,6 +194,8 @@ module Net; module SSH; module Connection
186
194
  # This will also cause all active channels to be processed once each (see
187
195
  # Net::SSH::Connection::Channel#on_process).
188
196
  #
197
+ # TODO revise example
198
+ #
189
199
  # # process multiple Net::SSH connections in parallel
190
200
  # connections = [
191
201
  # Net::SSH.start("host1", ...),
@@ -203,13 +213,10 @@ module Net; module SSH; module Connection
203
213
  # break if connections.empty?
204
214
  # end
205
215
  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)
216
+ @event_loop.process(wait, &block)
217
+ rescue
218
+ force_channel_cleanup_on_close if closed?
219
+ raise
213
220
  end
214
221
 
215
222
  # This is called internally as part of #process. It dispatches any
@@ -217,19 +224,38 @@ module Net; module SSH; module Connection
217
224
  # for any active channels. If a block is given, it is invoked at the
218
225
  # start of the method and again at the end, and if the block ever returns
219
226
  # false, this method returns false. Otherwise, it returns true.
220
- def preprocess
227
+ def preprocess(&block)
221
228
  return false if block_given? && !yield(self)
222
- dispatch_incoming_packets
223
- channels.each { |id, channel| channel.process unless channel.closing? }
229
+ ev_preprocess(&block)
224
230
  return false if block_given? && !yield(self)
225
231
  return true
226
232
  end
227
233
 
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
234
+ # Called by event loop to process available data before going to
235
+ # event multiplexing
236
+ def ev_preprocess(&block)
237
+ dispatch_incoming_packets(raise_disconnect_errors: false)
238
+ each_channel { |id, channel| channel.process unless channel.local_closed? }
239
+ end
240
+
241
+ # Returns the file descriptors the event loop should wait for read/write events,
242
+ # we also return the max wait
243
+ def ev_do_calculate_rw_wait(wait)
244
+ r = listeners.keys
245
+ w = r.select { |w2| w2.respond_to?(:pending_write?) && w2.pending_write? }
246
+ [r,w,io_select_wait(wait)]
247
+ end
248
+
249
+ # This is called internally as part of #process.
250
+ def postprocess(readers, writers)
251
+ ev_do_handle_events(readers, writers)
252
+ end
253
+
254
+ # It loops over the given arrays of reader IO's and writer IO's,
255
+ # processing them as needed, and
230
256
  # then calls Net::SSH::Transport::Session#rekey_as_needed to allow the
231
257
  # transport layer to rekey. Then returns true.
232
- def postprocess(readers, writers)
258
+ def ev_do_handle_events(readers, writers)
233
259
  Array(readers).each do |reader|
234
260
  if listeners[reader]
235
261
  listeners[reader].call(reader)
@@ -244,11 +270,14 @@ module Net; module SSH; module Connection
244
270
  Array(writers).each do |writer|
245
271
  writer.send_pending
246
272
  end
273
+ end
247
274
 
248
- @keepalive.send_as_needed(readers, writers)
275
+ # calls Net::SSH::Transport::Session#rekey_as_needed to allow the
276
+ # transport layer to rekey
277
+ def ev_do_postprocess(was_events)
278
+ @keepalive.send_as_needed(was_events)
249
279
  transport.rekey_as_needed
250
-
251
- return true
280
+ true
252
281
  end
253
282
 
254
283
  # Send a global request of the given type. The +extra+ parameters must
@@ -306,6 +335,15 @@ module Net; module SSH; module Connection
306
335
  channels[local_id] = channel
307
336
  end
308
337
 
338
+ class StringWithExitstatus < String
339
+ def initialize(str, exitstatus)
340
+ super(str)
341
+ @exitstatus = exitstatus
342
+ end
343
+
344
+ attr_reader :exitstatus
345
+ end
346
+
309
347
  # A convenience method for executing a command and interacting with it. If
310
348
  # no block is given, all output is printed via $stdout and $stderr. Otherwise,
311
349
  # the block is called for each data and extended data packet, with three
@@ -326,11 +364,21 @@ module Net; module SSH; module Connection
326
364
  # puts data
327
365
  # end
328
366
  # end
329
- def exec(command, &block)
367
+ def exec(command, status: nil, &block)
330
368
  open_channel do |channel|
331
369
  channel.exec(command) do |ch, success|
332
370
  raise "could not execute command: #{command.inspect}" unless success
333
-
371
+
372
+ if status
373
+ channel.on_request("exit-status") do |ch2,data|
374
+ status[:exit_code] = data.read_long
375
+ end
376
+
377
+ channel.on_request("exit-signal") do |ch2, data|
378
+ status[:exit_signal] = data.read_long
379
+ end
380
+ end
381
+
334
382
  channel.on_data do |ch2, data|
335
383
  if block
336
384
  block.call(ch2, :stdout, data)
@@ -351,20 +399,26 @@ module Net; module SSH; module Connection
351
399
  end
352
400
 
353
401
  # Same as #exec, except this will block until the command finishes. Also,
354
- # if a block is not given, this will return all output (stdout and stderr)
402
+ # if no block is given, this will return all output (stdout and stderr)
355
403
  # as a single string.
356
404
  #
357
405
  # matches = ssh.exec!("grep something /some/files")
358
- def exec!(command, &block)
359
- block ||= Proc.new do |ch, type, data|
406
+ #
407
+ # the returned string has an exitstatus method to query it's exit satus
408
+ def exec!(command, status: nil, &block)
409
+ block_or_concat = block || Proc.new do |ch, type, data|
360
410
  ch[:result] ||= ""
361
411
  ch[:result] << data
362
412
  end
363
413
 
364
- channel = exec(command, &block)
414
+ status ||= {}
415
+ channel = exec(command, status: status, &block_or_concat)
365
416
  channel.wait
366
417
 
367
- return channel[:result]
418
+ channel[:result] ||= "" unless block
419
+ channel[:result] &&= channel[:result].force_encoding("UTF-8") unless block
420
+
421
+ StringWithExitstatus.new(channel[:result], status[:exit_code]) if channel[:result]
368
422
  end
369
423
 
370
424
  # Enqueues a message to be sent to the server as soon as the socket is
@@ -453,11 +507,31 @@ module Net; module SSH; module Connection
453
507
  old
454
508
  end
455
509
 
510
+ def cleanup_channel(channel)
511
+ if channel.local_closed? and channel.remote_closed?
512
+ info { "#{host} delete channel #{channel.local_id} which closed locally and remotely" }
513
+ channels.delete(channel.local_id)
514
+ end
515
+ end
516
+
517
+ # If the #preprocess and #postprocess callbacks for this session need to run
518
+ # periodically, this method returns the maximum number of seconds which may
519
+ # pass between callbacks.
520
+ def max_select_wait_time
521
+ @keepalive.interval if @keepalive.enabled?
522
+ end
523
+
524
+
456
525
  private
457
526
 
527
+ # iterate channels with the posibility of callbacks opening new channels during the iteration
528
+ def each_channel(&block)
529
+ channels.dup.each(&block)
530
+ end
531
+
458
532
  # Read all pending packets from the connection and dispatch them as
459
533
  # appropriate. Returns as soon as there are no more pending packets.
460
- def dispatch_incoming_packets
534
+ def dispatch_incoming_packets(raise_disconnect_errors: true)
461
535
  while packet = transport.poll_message
462
536
  unless MAP.key?(packet.type)
463
537
  raise Net::SSH::Exception, "unexpected response #{packet.type} (#{packet.inspect})"
@@ -465,6 +539,9 @@ module Net; module SSH; module Connection
465
539
 
466
540
  send(MAP[packet.type], packet)
467
541
  end
542
+ rescue
543
+ force_channel_cleanup_on_close if closed?
544
+ raise if raise_disconnect_errors || !$!.is_a?(Net::SSH::Disconnect)
468
545
  end
469
546
 
470
547
  # Returns the next available channel id to be assigned, and increments
@@ -473,6 +550,20 @@ module Net; module SSH; module Connection
473
550
  @channel_id_counter += 1
474
551
  end
475
552
 
553
+ def force_channel_cleanup_on_close
554
+ channels.each do |id, channel|
555
+ channel_closed(channel)
556
+ end
557
+ end
558
+
559
+ def channel_closed(channel)
560
+ channel.remote_closed!
561
+ channel.close
562
+
563
+ cleanup_channel(channel)
564
+ channel.do_close
565
+ end
566
+
476
567
  # Invoked when a global request is received. The registered global
477
568
  # request callback will be invoked, if one exists, and the necessary
478
569
  # reply returned.
@@ -581,10 +672,7 @@ module Net; module SSH; module Connection
581
672
  info { "channel_close: #{packet[:local_id]}" }
582
673
 
583
674
  channel = channels[packet[:local_id]]
584
- channel.close
585
-
586
- channels.delete(packet[:local_id])
587
- channel.do_close
675
+ channel_closed(channel)
588
676
  end
589
677
 
590
678
  def channel_success(packet)
@@ -598,9 +686,7 @@ module Net; module SSH; module Connection
598
686
  end
599
687
 
600
688
  def io_select_wait(wait)
601
- return wait if wait
602
- return wait unless @keepalive.enabled?
603
- @keepalive.interval
689
+ [wait, max_select_wait_time].compact.min
604
690
  end
605
691
 
606
692
  MAP = Constants.constants.inject({}) do |memo, name|
@@ -5,14 +5,14 @@ module Net; module SSH
5
5
 
6
6
  # This exception is raised when authentication fails (whether it be
7
7
  # public key authentication, password authentication, or whatever).
8
- class AuthenticationFailed < Exception; end
8
+ class AuthenticationFailed < Net::SSH::Exception; end
9
9
 
10
10
  # This exception is raised when a connection attempt times out.
11
- class ConnectionTimeout < Exception; end
11
+ class ConnectionTimeout < Net::SSH::Exception; end
12
12
 
13
13
  # This exception is raised when the remote host has disconnected
14
14
  # unexpectedly.
15
- class Disconnect < Exception; end
15
+ class Disconnect < Net::SSH::Exception; end
16
16
 
17
17
  # This exception is raised when the remote host has disconnected/
18
18
  # timeouted unexpectedly.
@@ -23,14 +23,14 @@ module Net; module SSH
23
23
  # want to fail in such a way that the server knows it failed, you can
24
24
  # raise this exception in the handler and Net::SSH will translate that into
25
25
  # a "channel failure" message.
26
- class ChannelRequestFailed < Exception; end
26
+ class ChannelRequestFailed < Net::SSH::Exception; end
27
27
 
28
28
  # This is exception is primarily used internally, but if you have a channel
29
29
  # open handler (see Net::SSH::Connection::Session#on_open_channel) and you
30
30
  # want to fail in such a way that the server knows it failed, you can
31
31
  # raise this exception in the handler and Net::SSH will translate that into
32
32
  # a "channel open failed" message.
33
- class ChannelOpenFailed < Exception
33
+ class ChannelOpenFailed < Net::SSH::Exception
34
34
  attr_reader :code, :reason
35
35
 
36
36
  def initialize(code, reason)
@@ -42,7 +42,7 @@ module Net; module SSH
42
42
  # Base class for host key exceptions. When rescuing this exception, you can
43
43
  # inspect the key fingerprint and, if you want to proceed anyway, simply call
44
44
  # the remember_host! method on the exception, and then retry.
45
- class HostKeyError < Exception
45
+ class HostKeyError < Net::SSH::Exception
46
46
  # the callback to use when #remember_host! is called
47
47
  attr_writer :callback #:nodoc:
48
48