net-ssh 2.9.2 → 4.0.0

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