net-ssh-backports 6.3.0.backports
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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +93 -0
- data/.gitignore +13 -0
- data/.rubocop.yml +21 -0
- data/.rubocop_todo.yml +1074 -0
- data/.travis.yml +51 -0
- data/CHANGES.txt +698 -0
- data/Gemfile +13 -0
- data/Gemfile.noed25519 +12 -0
- data/ISSUE_TEMPLATE.md +30 -0
- data/LICENSE.txt +19 -0
- data/Manifest +132 -0
- data/README.md +287 -0
- data/Rakefile +105 -0
- data/THANKS.txt +110 -0
- data/appveyor.yml +58 -0
- data/lib/net/ssh/authentication/agent.rb +284 -0
- data/lib/net/ssh/authentication/certificate.rb +183 -0
- data/lib/net/ssh/authentication/constants.rb +20 -0
- data/lib/net/ssh/authentication/ed25519.rb +185 -0
- data/lib/net/ssh/authentication/ed25519_loader.rb +31 -0
- data/lib/net/ssh/authentication/key_manager.rb +297 -0
- data/lib/net/ssh/authentication/methods/abstract.rb +69 -0
- data/lib/net/ssh/authentication/methods/hostbased.rb +72 -0
- data/lib/net/ssh/authentication/methods/keyboard_interactive.rb +77 -0
- data/lib/net/ssh/authentication/methods/none.rb +34 -0
- data/lib/net/ssh/authentication/methods/password.rb +80 -0
- data/lib/net/ssh/authentication/methods/publickey.rb +95 -0
- data/lib/net/ssh/authentication/pageant.rb +497 -0
- data/lib/net/ssh/authentication/pub_key_fingerprint.rb +43 -0
- data/lib/net/ssh/authentication/session.rb +163 -0
- data/lib/net/ssh/buffer.rb +434 -0
- data/lib/net/ssh/buffered_io.rb +202 -0
- data/lib/net/ssh/config.rb +406 -0
- data/lib/net/ssh/connection/channel.rb +695 -0
- data/lib/net/ssh/connection/constants.rb +33 -0
- data/lib/net/ssh/connection/event_loop.rb +123 -0
- data/lib/net/ssh/connection/keepalive.rb +59 -0
- data/lib/net/ssh/connection/session.rb +712 -0
- data/lib/net/ssh/connection/term.rb +180 -0
- data/lib/net/ssh/errors.rb +106 -0
- data/lib/net/ssh/key_factory.rb +218 -0
- data/lib/net/ssh/known_hosts.rb +264 -0
- data/lib/net/ssh/loggable.rb +62 -0
- data/lib/net/ssh/packet.rb +106 -0
- data/lib/net/ssh/prompt.rb +62 -0
- data/lib/net/ssh/proxy/command.rb +123 -0
- data/lib/net/ssh/proxy/errors.rb +16 -0
- data/lib/net/ssh/proxy/http.rb +98 -0
- data/lib/net/ssh/proxy/https.rb +50 -0
- data/lib/net/ssh/proxy/jump.rb +54 -0
- data/lib/net/ssh/proxy/socks4.rb +67 -0
- data/lib/net/ssh/proxy/socks5.rb +140 -0
- data/lib/net/ssh/service/forward.rb +426 -0
- data/lib/net/ssh/test/channel.rb +147 -0
- data/lib/net/ssh/test/extensions.rb +173 -0
- data/lib/net/ssh/test/kex.rb +46 -0
- data/lib/net/ssh/test/local_packet.rb +53 -0
- data/lib/net/ssh/test/packet.rb +101 -0
- data/lib/net/ssh/test/remote_packet.rb +40 -0
- data/lib/net/ssh/test/script.rb +180 -0
- data/lib/net/ssh/test/socket.rb +65 -0
- data/lib/net/ssh/test.rb +94 -0
- data/lib/net/ssh/transport/algorithms.rb +502 -0
- data/lib/net/ssh/transport/cipher_factory.rb +103 -0
- data/lib/net/ssh/transport/constants.rb +40 -0
- data/lib/net/ssh/transport/ctr.rb +115 -0
- data/lib/net/ssh/transport/hmac/abstract.rb +97 -0
- data/lib/net/ssh/transport/hmac/md5.rb +10 -0
- data/lib/net/ssh/transport/hmac/md5_96.rb +9 -0
- data/lib/net/ssh/transport/hmac/none.rb +13 -0
- data/lib/net/ssh/transport/hmac/ripemd160.rb +11 -0
- data/lib/net/ssh/transport/hmac/sha1.rb +11 -0
- data/lib/net/ssh/transport/hmac/sha1_96.rb +9 -0
- data/lib/net/ssh/transport/hmac/sha2_256.rb +11 -0
- data/lib/net/ssh/transport/hmac/sha2_256_96.rb +9 -0
- data/lib/net/ssh/transport/hmac/sha2_256_etm.rb +12 -0
- data/lib/net/ssh/transport/hmac/sha2_512.rb +11 -0
- data/lib/net/ssh/transport/hmac/sha2_512_96.rb +9 -0
- data/lib/net/ssh/transport/hmac/sha2_512_etm.rb +12 -0
- data/lib/net/ssh/transport/hmac.rb +47 -0
- data/lib/net/ssh/transport/identity_cipher.rb +57 -0
- data/lib/net/ssh/transport/kex/abstract.rb +130 -0
- data/lib/net/ssh/transport/kex/abstract5656.rb +72 -0
- data/lib/net/ssh/transport/kex/curve25519_sha256.rb +39 -0
- data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +30 -0
- data/lib/net/ssh/transport/kex/diffie_hellman_group14_sha1.rb +37 -0
- data/lib/net/ssh/transport/kex/diffie_hellman_group14_sha256.rb +11 -0
- data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +122 -0
- data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +72 -0
- data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha256.rb +11 -0
- data/lib/net/ssh/transport/kex/ecdh_sha2_nistp256.rb +39 -0
- data/lib/net/ssh/transport/kex/ecdh_sha2_nistp384.rb +21 -0
- data/lib/net/ssh/transport/kex/ecdh_sha2_nistp521.rb +21 -0
- data/lib/net/ssh/transport/kex.rb +31 -0
- data/lib/net/ssh/transport/key_expander.rb +30 -0
- data/lib/net/ssh/transport/openssl.rb +253 -0
- data/lib/net/ssh/transport/packet_stream.rb +280 -0
- data/lib/net/ssh/transport/server_version.rb +77 -0
- data/lib/net/ssh/transport/session.rb +354 -0
- data/lib/net/ssh/transport/state.rb +208 -0
- data/lib/net/ssh/verifiers/accept_new.rb +33 -0
- data/lib/net/ssh/verifiers/accept_new_or_local_tunnel.rb +33 -0
- data/lib/net/ssh/verifiers/always.rb +58 -0
- data/lib/net/ssh/verifiers/never.rb +19 -0
- data/lib/net/ssh/version.rb +68 -0
- data/lib/net/ssh.rb +330 -0
- data/net-ssh-public_cert.pem +20 -0
- data/net-ssh.gemspec +44 -0
- data/support/ssh_tunnel_bug.rb +65 -0
- metadata +271 -0
@@ -0,0 +1,712 @@
|
|
1
|
+
require 'net/ssh/loggable'
|
2
|
+
require 'net/ssh/connection/channel'
|
3
|
+
require 'net/ssh/connection/constants'
|
4
|
+
require 'net/ssh/service/forward'
|
5
|
+
require 'net/ssh/connection/keepalive'
|
6
|
+
require 'net/ssh/connection/event_loop'
|
7
|
+
|
8
|
+
module Net
|
9
|
+
module SSH
|
10
|
+
module Connection
|
11
|
+
# A session class representing the connection service running on top of
|
12
|
+
# the SSH transport layer. It manages the creation of channels (see
|
13
|
+
# #open_channel), and the dispatching of messages to the various channels.
|
14
|
+
# It also encapsulates the SSH event loop (via #loop and #process),
|
15
|
+
# and serves as a central point-of-reference for all SSH-related services (e.g.
|
16
|
+
# port forwarding, SFTP, SCP, etc.).
|
17
|
+
#
|
18
|
+
# You will rarely (if ever) need to instantiate this class directly; rather,
|
19
|
+
# you'll almost always use Net::SSH.start to initialize a new network
|
20
|
+
# connection, authenticate a user, and return a new connection session,
|
21
|
+
# all in one call.
|
22
|
+
#
|
23
|
+
# Net::SSH.start("localhost", "user") do |ssh|
|
24
|
+
# # 'ssh' is an instance of Net::SSH::Connection::Session
|
25
|
+
# ssh.exec! "/etc/init.d/some_process start"
|
26
|
+
# end
|
27
|
+
class Session
|
28
|
+
include Loggable
|
29
|
+
include Constants
|
30
|
+
|
31
|
+
# Default IO.select timeout threshold
|
32
|
+
DEFAULT_IO_SELECT_TIMEOUT = 300
|
33
|
+
|
34
|
+
# The underlying transport layer abstraction (see Net::SSH::Transport::Session).
|
35
|
+
attr_reader :transport
|
36
|
+
|
37
|
+
# The map of options that were used to initialize this instance.
|
38
|
+
attr_reader :options
|
39
|
+
|
40
|
+
# The collection of custom properties for this instance. (See #[] and #[]=).
|
41
|
+
attr_reader :properties
|
42
|
+
|
43
|
+
# The map of channels, each key being the local-id for the channel.
|
44
|
+
attr_reader :channels #:nodoc:
|
45
|
+
|
46
|
+
# The map of listeners that the event loop knows about. See #listen_to.
|
47
|
+
attr_reader :listeners #:nodoc:
|
48
|
+
|
49
|
+
# The map of specialized handlers for opening specific channel types. See
|
50
|
+
# #on_open_channel.
|
51
|
+
attr_reader :channel_open_handlers #:nodoc:
|
52
|
+
|
53
|
+
# The list of callbacks for pending requests. See #send_global_request.
|
54
|
+
attr_reader :pending_requests #:nodoc:
|
55
|
+
|
56
|
+
class NilChannel
|
57
|
+
def initialize(session)
|
58
|
+
@session = session
|
59
|
+
end
|
60
|
+
|
61
|
+
def method_missing(sym, *args)
|
62
|
+
@session.lwarn { "ignoring request #{sym.inspect} for non-existent (closed?) channel; probably ssh server bug" }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Create a new connection service instance atop the given transport
|
67
|
+
# layer. Initializes the listeners to be only the underlying socket object.
|
68
|
+
def initialize(transport, options={})
|
69
|
+
self.logger = transport.logger
|
70
|
+
|
71
|
+
@transport = transport
|
72
|
+
@options = options
|
73
|
+
|
74
|
+
@channel_id_counter = -1
|
75
|
+
@channels = Hash.new(NilChannel.new(self))
|
76
|
+
@listeners = { transport.socket => nil }
|
77
|
+
@pending_requests = []
|
78
|
+
@channel_open_handlers = {}
|
79
|
+
@on_global_request = {}
|
80
|
+
@properties = (options[:properties] || {}).dup
|
81
|
+
|
82
|
+
@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)
|
84
|
+
|
85
|
+
@keepalive = Keepalive.new(self)
|
86
|
+
|
87
|
+
@event_loop = options[:event_loop] || SingleSessionEventLoop.new
|
88
|
+
@event_loop.register(self)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Retrieves a custom property from this instance. This can be used to
|
92
|
+
# store additional state in applications that must manage multiple
|
93
|
+
# SSH connections.
|
94
|
+
def [](key)
|
95
|
+
@properties[key]
|
96
|
+
end
|
97
|
+
|
98
|
+
# Sets a custom property for this instance.
|
99
|
+
def []=(key, value)
|
100
|
+
@properties[key] = value
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns the name of the host that was given to the transport layer to
|
104
|
+
# connect to.
|
105
|
+
def host
|
106
|
+
transport.host
|
107
|
+
end
|
108
|
+
|
109
|
+
# Returns true if the underlying transport has been closed. Note that
|
110
|
+
# this can be a little misleading, since if the remote server has
|
111
|
+
# closed the connection, the local end will still think it is open
|
112
|
+
# until the next operation on the socket. Nevertheless, this method can
|
113
|
+
# be useful if you just want to know if _you_ have closed the connection.
|
114
|
+
def closed?
|
115
|
+
transport.closed?
|
116
|
+
end
|
117
|
+
|
118
|
+
# Closes the session gracefully, blocking until all channels have
|
119
|
+
# successfully closed, and then closes the underlying transport layer
|
120
|
+
# connection.
|
121
|
+
def close
|
122
|
+
info { "closing remaining channels (#{channels.length} open)" }
|
123
|
+
channels.each { |id, channel| channel.close }
|
124
|
+
begin
|
125
|
+
loop(0.1) { channels.any? }
|
126
|
+
rescue Net::SSH::Disconnect
|
127
|
+
raise unless channels.empty?
|
128
|
+
end
|
129
|
+
transport.close
|
130
|
+
end
|
131
|
+
|
132
|
+
# Performs a "hard" shutdown of the connection. In general, this should
|
133
|
+
# never be done, but it might be necessary (in a rescue clause, for instance,
|
134
|
+
# when the connection needs to close but you don't know the status of the
|
135
|
+
# underlying protocol's state).
|
136
|
+
def shutdown!
|
137
|
+
transport.shutdown!
|
138
|
+
end
|
139
|
+
|
140
|
+
# preserve a reference to Kernel#loop
|
141
|
+
alias :loop_forever :loop
|
142
|
+
|
143
|
+
# Returns +true+ if there are any channels currently active on this
|
144
|
+
# session. By default, this will not include "invisible" channels
|
145
|
+
# (such as those created by forwarding ports and such), but if you pass
|
146
|
+
# a +true+ value for +include_invisible+, then those will be counted.
|
147
|
+
#
|
148
|
+
# This can be useful for determining whether the event loop should continue
|
149
|
+
# to be run.
|
150
|
+
#
|
151
|
+
# ssh.loop { ssh.busy? }
|
152
|
+
def busy?(include_invisible=false)
|
153
|
+
if include_invisible
|
154
|
+
channels.any?
|
155
|
+
else
|
156
|
+
channels.any? { |id, ch| !ch[:invisible] }
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# The main event loop. Calls #process until #process returns false. If a
|
161
|
+
# block is given, it is passed to #process, otherwise a default proc is
|
162
|
+
# used that just returns true if there are any channels active (see #busy?).
|
163
|
+
# The # +wait+ parameter is also passed through to #process (where it is
|
164
|
+
# interpreted as the maximum number of seconds to wait for IO.select to return).
|
165
|
+
#
|
166
|
+
# # loop for as long as there are any channels active
|
167
|
+
# ssh.loop
|
168
|
+
#
|
169
|
+
# # loop for as long as there are any channels active, but make sure
|
170
|
+
# # the event loop runs at least once per 0.1 second
|
171
|
+
# ssh.loop(0.1)
|
172
|
+
#
|
173
|
+
# # loop until ctrl-C is pressed
|
174
|
+
# int_pressed = false
|
175
|
+
# trap("INT") { int_pressed = true }
|
176
|
+
# ssh.loop(0.1) { not int_pressed }
|
177
|
+
def loop(wait=nil, &block)
|
178
|
+
running = block || Proc.new { busy? }
|
179
|
+
loop_forever { break unless process(wait, &running) }
|
180
|
+
begin
|
181
|
+
process(0)
|
182
|
+
rescue IOError => e
|
183
|
+
if e.message =~ /closed/
|
184
|
+
debug { "stream was closed after loop => shallowing exception so it will be re-raised in next loop" }
|
185
|
+
else
|
186
|
+
raise
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# The core of the event loop. It processes a single iteration of the event
|
192
|
+
# loop. If a block is given, it should return false when the processing
|
193
|
+
# should abort, which causes #process to return false. Otherwise,
|
194
|
+
# #process returns true. The session itself is yielded to the block as its
|
195
|
+
# only argument.
|
196
|
+
#
|
197
|
+
# If +wait+ is nil (the default), this method will block until any of the
|
198
|
+
# monitored IO objects are ready to be read from or written to. If you want
|
199
|
+
# it to not block, you can pass 0, or you can pass any other numeric value
|
200
|
+
# to indicate that it should block for no more than that many seconds.
|
201
|
+
# Passing 0 is a good way to poll the connection, but if you do it too
|
202
|
+
# frequently it can make your CPU quite busy!
|
203
|
+
#
|
204
|
+
# This will also cause all active channels to be processed once each (see
|
205
|
+
# Net::SSH::Connection::Channel#on_process).
|
206
|
+
#
|
207
|
+
# TODO revise example
|
208
|
+
#
|
209
|
+
# # process multiple Net::SSH connections in parallel
|
210
|
+
# connections = [
|
211
|
+
# Net::SSH.start("host1", ...),
|
212
|
+
# Net::SSH.start("host2", ...)
|
213
|
+
# ]
|
214
|
+
#
|
215
|
+
# connections.each do |ssh|
|
216
|
+
# ssh.exec "grep something /in/some/files"
|
217
|
+
# end
|
218
|
+
#
|
219
|
+
# condition = Proc.new { |s| s.busy? }
|
220
|
+
#
|
221
|
+
# loop do
|
222
|
+
# connections.delete_if { |ssh| !ssh.process(0.1, &condition) }
|
223
|
+
# break if connections.empty?
|
224
|
+
# end
|
225
|
+
def process(wait=nil, &block)
|
226
|
+
@event_loop.process(wait, &block)
|
227
|
+
rescue StandardError
|
228
|
+
force_channel_cleanup_on_close if closed?
|
229
|
+
raise
|
230
|
+
end
|
231
|
+
|
232
|
+
# This is called internally as part of #process. It dispatches any
|
233
|
+
# available incoming packets, and then runs Net::SSH::Connection::Channel#process
|
234
|
+
# for any active channels. If a block is given, it is invoked at the
|
235
|
+
# start of the method and again at the end, and if the block ever returns
|
236
|
+
# false, this method returns false. Otherwise, it returns true.
|
237
|
+
def preprocess(&block)
|
238
|
+
return false if block_given? && !yield(self)
|
239
|
+
|
240
|
+
ev_preprocess(&block)
|
241
|
+
return false if block_given? && !yield(self)
|
242
|
+
|
243
|
+
return true
|
244
|
+
end
|
245
|
+
|
246
|
+
# Called by event loop to process available data before going to
|
247
|
+
# event multiplexing
|
248
|
+
def ev_preprocess(&block)
|
249
|
+
dispatch_incoming_packets(raise_disconnect_errors: false)
|
250
|
+
each_channel { |id, channel| channel.process unless channel.local_closed? }
|
251
|
+
end
|
252
|
+
|
253
|
+
# Returns the file descriptors the event loop should wait for read/write events,
|
254
|
+
# we also return the max wait
|
255
|
+
def ev_do_calculate_rw_wait(wait)
|
256
|
+
r = listeners.keys
|
257
|
+
w = r.select { |w2| w2.respond_to?(:pending_write?) && w2.pending_write? }
|
258
|
+
[r,w,io_select_wait(wait)]
|
259
|
+
end
|
260
|
+
|
261
|
+
# This is called internally as part of #process.
|
262
|
+
def postprocess(readers, writers)
|
263
|
+
ev_do_handle_events(readers, writers)
|
264
|
+
end
|
265
|
+
|
266
|
+
# It loops over the given arrays of reader IO's and writer IO's,
|
267
|
+
# processing them as needed, and
|
268
|
+
# then calls Net::SSH::Transport::Session#rekey_as_needed to allow the
|
269
|
+
# transport layer to rekey. Then returns true.
|
270
|
+
def ev_do_handle_events(readers, writers)
|
271
|
+
Array(readers).each do |reader|
|
272
|
+
if listeners[reader]
|
273
|
+
listeners[reader].call(reader)
|
274
|
+
else
|
275
|
+
if reader.fill.zero?
|
276
|
+
reader.close
|
277
|
+
stop_listening_to(reader)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
Array(writers).each do |writer|
|
283
|
+
writer.send_pending
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
# calls Net::SSH::Transport::Session#rekey_as_needed to allow the
|
288
|
+
# transport layer to rekey
|
289
|
+
def ev_do_postprocess(was_events)
|
290
|
+
@keepalive.send_as_needed(was_events)
|
291
|
+
transport.rekey_as_needed
|
292
|
+
true
|
293
|
+
end
|
294
|
+
|
295
|
+
# Send a global request of the given type. The +extra+ parameters must
|
296
|
+
# be even in number, and conform to the same format as described for
|
297
|
+
# Net::SSH::Buffer.from. If a callback is not specified, the request will
|
298
|
+
# not require a response from the server, otherwise the server is required
|
299
|
+
# to respond and indicate whether the request was successful or not. This
|
300
|
+
# success or failure is indicated by the callback being invoked, with the
|
301
|
+
# first parameter being true or false (success, or failure), and the second
|
302
|
+
# being the packet itself.
|
303
|
+
#
|
304
|
+
# Generally, Net::SSH will manage global requests that need to be sent
|
305
|
+
# (e.g. port forward requests and such are handled in the Net::SSH::Service::Forward
|
306
|
+
# class, for instance). However, there may be times when you need to
|
307
|
+
# send a global request that isn't explicitly handled by Net::SSH, and so
|
308
|
+
# this method is available to you.
|
309
|
+
#
|
310
|
+
# ssh.send_global_request("keep-alive@openssh.com")
|
311
|
+
def send_global_request(type, *extra, &callback)
|
312
|
+
info { "sending global request #{type}" }
|
313
|
+
msg = Buffer.from(:byte, GLOBAL_REQUEST, :string, type.to_s, :bool, !callback.nil?, *extra)
|
314
|
+
send_message(msg)
|
315
|
+
pending_requests << callback if callback
|
316
|
+
self
|
317
|
+
end
|
318
|
+
|
319
|
+
# Requests that a new channel be opened. By default, the channel will be
|
320
|
+
# of type "session", but if you know what you're doing you can select any
|
321
|
+
# of the channel types supported by the SSH protocol. The +extra+ parameters
|
322
|
+
# must be even in number and conform to the same format as described for
|
323
|
+
# Net::SSH::Buffer.from. If a callback is given, it will be invoked when
|
324
|
+
# the server confirms that the channel opened successfully. The sole parameter
|
325
|
+
# for the callback is the channel object itself.
|
326
|
+
#
|
327
|
+
# In general, you'll use #open_channel without any arguments; the only
|
328
|
+
# time you'd want to set the channel type or pass additional initialization
|
329
|
+
# data is if you were implementing an SSH extension.
|
330
|
+
#
|
331
|
+
# channel = ssh.open_channel do |ch|
|
332
|
+
# ch.exec "grep something /some/files" do |ch, success|
|
333
|
+
# ...
|
334
|
+
# end
|
335
|
+
# end
|
336
|
+
#
|
337
|
+
# channel.wait
|
338
|
+
def open_channel(type="session", *extra, &on_confirm)
|
339
|
+
local_id = get_next_channel_id
|
340
|
+
|
341
|
+
channel = Channel.new(self, type, local_id, @max_pkt_size, @max_win_size, &on_confirm)
|
342
|
+
msg = Buffer.from(:byte, CHANNEL_OPEN, :string, type, :long, local_id,
|
343
|
+
:long, channel.local_maximum_window_size,
|
344
|
+
:long, channel.local_maximum_packet_size, *extra)
|
345
|
+
send_message(msg)
|
346
|
+
|
347
|
+
channels[local_id] = channel
|
348
|
+
end
|
349
|
+
|
350
|
+
class StringWithExitstatus < String
|
351
|
+
def initialize(str, exitstatus)
|
352
|
+
super(str)
|
353
|
+
@exitstatus = exitstatus
|
354
|
+
end
|
355
|
+
|
356
|
+
attr_reader :exitstatus
|
357
|
+
end
|
358
|
+
|
359
|
+
# A convenience method for executing a command and interacting with it. If
|
360
|
+
# no block is given, all output is printed via $stdout and $stderr. Otherwise,
|
361
|
+
# the block is called for each data and extended data packet, with three
|
362
|
+
# arguments: the channel object, a symbol indicating the data type
|
363
|
+
# (:stdout or :stderr), and the data (as a string).
|
364
|
+
#
|
365
|
+
# Note that this method returns immediately, and requires an event loop
|
366
|
+
# (see Session#loop) in order for the command to actually execute.
|
367
|
+
#
|
368
|
+
# This is effectively identical to calling #open_channel, and then
|
369
|
+
# Net::SSH::Connection::Channel#exec, and then setting up the channel
|
370
|
+
# callbacks. However, for most uses, this will be sufficient.
|
371
|
+
#
|
372
|
+
# ssh.exec "grep something /some/files" do |ch, stream, data|
|
373
|
+
# if stream == :stderr
|
374
|
+
# puts "ERROR: #{data}"
|
375
|
+
# else
|
376
|
+
# puts data
|
377
|
+
# end
|
378
|
+
# end
|
379
|
+
def exec(command, status: nil, &block)
|
380
|
+
open_channel do |channel|
|
381
|
+
channel.exec(command) do |ch, success|
|
382
|
+
raise "could not execute command: #{command.inspect}" unless success
|
383
|
+
|
384
|
+
if status
|
385
|
+
channel.on_request("exit-status") do |ch2,data|
|
386
|
+
status[:exit_code] = data.read_long
|
387
|
+
end
|
388
|
+
|
389
|
+
channel.on_request("exit-signal") do |ch2, data|
|
390
|
+
status[:exit_signal] = data.read_long
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
channel.on_data do |ch2, data|
|
395
|
+
if block
|
396
|
+
block.call(ch2, :stdout, data)
|
397
|
+
else
|
398
|
+
$stdout.print(data)
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
channel.on_extended_data do |ch2, type, data|
|
403
|
+
if block
|
404
|
+
block.call(ch2, :stderr, data)
|
405
|
+
else
|
406
|
+
$stderr.print(data)
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
# Same as #exec, except this will block until the command finishes. Also,
|
414
|
+
# if no block is given, this will return all output (stdout and stderr)
|
415
|
+
# as a single string.
|
416
|
+
#
|
417
|
+
# matches = ssh.exec!("grep something /some/files")
|
418
|
+
#
|
419
|
+
# the returned string has an exitstatus method to query it's exit satus
|
420
|
+
def exec!(command, status: nil, &block)
|
421
|
+
block_or_concat = block || Proc.new do |ch, type, data|
|
422
|
+
ch[:result] ||= String.new
|
423
|
+
ch[:result] << data
|
424
|
+
end
|
425
|
+
|
426
|
+
status ||= {}
|
427
|
+
channel = exec(command, status: status, &block_or_concat)
|
428
|
+
channel.wait
|
429
|
+
|
430
|
+
channel[:result] ||= String.new unless block
|
431
|
+
channel[:result] &&= channel[:result].force_encoding("UTF-8") unless block
|
432
|
+
|
433
|
+
StringWithExitstatus.new(channel[:result], status[:exit_code]) if channel[:result]
|
434
|
+
end
|
435
|
+
|
436
|
+
# Enqueues a message to be sent to the server as soon as the socket is
|
437
|
+
# available for writing. Most programs will never need to call this, but
|
438
|
+
# if you are implementing an extension to the SSH protocol, or if you
|
439
|
+
# need to send a packet that Net::SSH does not directly support, you can
|
440
|
+
# use this to send it.
|
441
|
+
#
|
442
|
+
# ssh.send_message(Buffer.from(:byte, REQUEST_SUCCESS).to_s)
|
443
|
+
def send_message(message)
|
444
|
+
transport.enqueue_message(message)
|
445
|
+
end
|
446
|
+
|
447
|
+
# Adds an IO object for the event loop to listen to. If a callback
|
448
|
+
# is given, it will be invoked when the io is ready to be read, otherwise,
|
449
|
+
# the io will merely have its #fill method invoked.
|
450
|
+
#
|
451
|
+
# Any +io+ value passed to this method _must_ have mixed into it the
|
452
|
+
# Net::SSH::BufferedIo functionality, typically by calling #extend on the
|
453
|
+
# object.
|
454
|
+
#
|
455
|
+
# The following example executes a process on the remote server, opens
|
456
|
+
# a socket to somewhere, and then pipes data from that socket to the
|
457
|
+
# remote process' stdin stream:
|
458
|
+
#
|
459
|
+
# channel = ssh.open_channel do |ch|
|
460
|
+
# ch.exec "/some/process/that/wants/input" do |ch, success|
|
461
|
+
# abort "can't execute!" unless success
|
462
|
+
#
|
463
|
+
# io = TCPSocket.new(somewhere, port)
|
464
|
+
# io.extend(Net::SSH::BufferedIo)
|
465
|
+
# ssh.listen_to(io)
|
466
|
+
#
|
467
|
+
# ch.on_process do
|
468
|
+
# if io.available > 0
|
469
|
+
# ch.send_data(io.read_available)
|
470
|
+
# end
|
471
|
+
# end
|
472
|
+
#
|
473
|
+
# ch.on_close do
|
474
|
+
# ssh.stop_listening_to(io)
|
475
|
+
# io.close
|
476
|
+
# end
|
477
|
+
# end
|
478
|
+
# end
|
479
|
+
#
|
480
|
+
# channel.wait
|
481
|
+
def listen_to(io, &callback)
|
482
|
+
listeners[io] = callback
|
483
|
+
end
|
484
|
+
|
485
|
+
# Removes the given io object from the listeners collection, so that the
|
486
|
+
# event loop will no longer monitor it.
|
487
|
+
def stop_listening_to(io)
|
488
|
+
listeners.delete(io)
|
489
|
+
end
|
490
|
+
|
491
|
+
# Returns a reference to the Net::SSH::Service::Forward service, which can
|
492
|
+
# be used for forwarding ports over SSH.
|
493
|
+
def forward
|
494
|
+
@forward ||= Service::Forward.new(self)
|
495
|
+
end
|
496
|
+
|
497
|
+
# Registers a handler to be invoked when the server wants to open a
|
498
|
+
# channel on the client. The callback receives the connection object,
|
499
|
+
# the new channel object, and the packet itself as arguments, and should
|
500
|
+
# raise ChannelOpenFailed if it is unable to open the channel for some
|
501
|
+
# reason. Otherwise, the channel will be opened and a confirmation message
|
502
|
+
# sent to the server.
|
503
|
+
#
|
504
|
+
# This is used by the Net::SSH::Service::Forward service to open a channel
|
505
|
+
# when a remote forwarded port receives a connection. However, you are
|
506
|
+
# welcome to register handlers for other channel types, as needed.
|
507
|
+
def on_open_channel(type, &block)
|
508
|
+
channel_open_handlers[type] = block
|
509
|
+
end
|
510
|
+
|
511
|
+
# Registers a handler to be invoked when the server sends a global request
|
512
|
+
# of the given type. The callback receives the request data as the first
|
513
|
+
# parameter, and true/false as the second (indicating whether a response
|
514
|
+
# is required). If the callback sends the response, it should return
|
515
|
+
# :sent. Otherwise, if it returns true, REQUEST_SUCCESS will be sent, and
|
516
|
+
# if it returns false, REQUEST_FAILURE will be sent.
|
517
|
+
def on_global_request(type, &block)
|
518
|
+
old, @on_global_request[type] = @on_global_request[type], block
|
519
|
+
old
|
520
|
+
end
|
521
|
+
|
522
|
+
def cleanup_channel(channel)
|
523
|
+
if channel.local_closed? and channel.remote_closed?
|
524
|
+
info { "#{host} delete channel #{channel.local_id} which closed locally and remotely" }
|
525
|
+
channels.delete(channel.local_id)
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
# If the #preprocess and #postprocess callbacks for this session need to run
|
530
|
+
# periodically, this method returns the maximum number of seconds which may
|
531
|
+
# pass between callbacks.
|
532
|
+
def max_select_wait_time
|
533
|
+
@keepalive.interval if @keepalive.enabled?
|
534
|
+
end
|
535
|
+
|
536
|
+
private
|
537
|
+
|
538
|
+
# iterate channels with the posibility of callbacks opening new channels during the iteration
|
539
|
+
def each_channel(&block)
|
540
|
+
channels.dup.each(&block)
|
541
|
+
end
|
542
|
+
|
543
|
+
# Read all pending packets from the connection and dispatch them as
|
544
|
+
# appropriate. Returns as soon as there are no more pending packets.
|
545
|
+
def dispatch_incoming_packets(raise_disconnect_errors: true)
|
546
|
+
while packet = transport.poll_message
|
547
|
+
raise Net::SSH::Exception, "unexpected response #{packet.type} (#{packet.inspect})" unless MAP.key?(packet.type)
|
548
|
+
|
549
|
+
send(MAP[packet.type], packet)
|
550
|
+
end
|
551
|
+
rescue StandardError
|
552
|
+
force_channel_cleanup_on_close if closed?
|
553
|
+
raise if raise_disconnect_errors || !$!.is_a?(Net::SSH::Disconnect)
|
554
|
+
end
|
555
|
+
|
556
|
+
# Returns the next available channel id to be assigned, and increments
|
557
|
+
# the counter.
|
558
|
+
def get_next_channel_id
|
559
|
+
@channel_id_counter += 1
|
560
|
+
end
|
561
|
+
|
562
|
+
def force_channel_cleanup_on_close
|
563
|
+
channels.each do |id, channel|
|
564
|
+
channel_closed(channel)
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
def channel_closed(channel)
|
569
|
+
channel.remote_closed!
|
570
|
+
channel.close
|
571
|
+
|
572
|
+
cleanup_channel(channel)
|
573
|
+
channel.do_close
|
574
|
+
end
|
575
|
+
|
576
|
+
# Invoked when a global request is received. The registered global
|
577
|
+
# request callback will be invoked, if one exists, and the necessary
|
578
|
+
# reply returned.
|
579
|
+
def global_request(packet)
|
580
|
+
info { "global request received: #{packet[:request_type]} #{packet[:want_reply]}" }
|
581
|
+
callback = @on_global_request[packet[:request_type]]
|
582
|
+
result = callback ? callback.call(packet[:request_data], packet[:want_reply]) : false
|
583
|
+
|
584
|
+
if result != :sent && result != true && result != false
|
585
|
+
raise "expected global request handler for `#{packet[:request_type]}' to return true, false, or :sent, but got #{result.inspect}"
|
586
|
+
end
|
587
|
+
|
588
|
+
if packet[:want_reply] && result != :sent
|
589
|
+
msg = Buffer.from(:byte, result ? REQUEST_SUCCESS : REQUEST_FAILURE)
|
590
|
+
send_message(msg)
|
591
|
+
end
|
592
|
+
end
|
593
|
+
|
594
|
+
# Invokes the next pending request callback with +true+.
|
595
|
+
def request_success(packet)
|
596
|
+
info { "global request success" }
|
597
|
+
callback = pending_requests.shift
|
598
|
+
callback.call(true, packet) if callback
|
599
|
+
end
|
600
|
+
|
601
|
+
# Invokes the next pending request callback with +false+.
|
602
|
+
def request_failure(packet)
|
603
|
+
info { "global request failure" }
|
604
|
+
callback = pending_requests.shift
|
605
|
+
callback.call(false, packet) if callback
|
606
|
+
end
|
607
|
+
|
608
|
+
# Called when the server wants to open a channel. If no registered
|
609
|
+
# channel handler exists for the given channel type, CHANNEL_OPEN_FAILURE
|
610
|
+
# is returned, otherwise the callback is invoked and everything proceeds
|
611
|
+
# accordingly.
|
612
|
+
def channel_open(packet)
|
613
|
+
info { "channel open #{packet[:channel_type]}" }
|
614
|
+
|
615
|
+
local_id = get_next_channel_id
|
616
|
+
|
617
|
+
channel = Channel.new(self, packet[:channel_type], local_id, @max_pkt_size, @max_win_size)
|
618
|
+
channel.do_open_confirmation(packet[:remote_id], packet[:window_size], packet[:packet_size])
|
619
|
+
|
620
|
+
callback = channel_open_handlers[packet[:channel_type]]
|
621
|
+
|
622
|
+
if callback
|
623
|
+
begin
|
624
|
+
callback[self, channel, packet]
|
625
|
+
rescue ChannelOpenFailed => err
|
626
|
+
failure = [err.code, err.reason]
|
627
|
+
else
|
628
|
+
channels[local_id] = channel
|
629
|
+
msg = Buffer.from(:byte, CHANNEL_OPEN_CONFIRMATION, :long, channel.remote_id, :long,
|
630
|
+
channel.local_id, :long, channel.local_maximum_window_size, :long,
|
631
|
+
channel.local_maximum_packet_size)
|
632
|
+
end
|
633
|
+
else
|
634
|
+
failure = [3, "unknown channel type #{channel.type}"]
|
635
|
+
end
|
636
|
+
|
637
|
+
if failure
|
638
|
+
error { failure.inspect }
|
639
|
+
msg = Buffer.from(:byte, CHANNEL_OPEN_FAILURE, :long, channel.remote_id, :long, failure[0], :string, failure[1], :string, "")
|
640
|
+
end
|
641
|
+
|
642
|
+
send_message(msg)
|
643
|
+
end
|
644
|
+
|
645
|
+
def channel_open_confirmation(packet)
|
646
|
+
info { "channel_open_confirmation: #{packet[:local_id]} #{packet[:remote_id]} #{packet[:window_size]} #{packet[:packet_size]}" }
|
647
|
+
channel = channels[packet[:local_id]]
|
648
|
+
channel.do_open_confirmation(packet[:remote_id], packet[:window_size], packet[:packet_size])
|
649
|
+
end
|
650
|
+
|
651
|
+
def channel_open_failure(packet)
|
652
|
+
error { "channel_open_failed: #{packet[:local_id]} #{packet[:reason_code]} #{packet[:description]}" }
|
653
|
+
channel = channels.delete(packet[:local_id])
|
654
|
+
channel.do_open_failed(packet[:reason_code], packet[:description])
|
655
|
+
end
|
656
|
+
|
657
|
+
def channel_window_adjust(packet)
|
658
|
+
info { "channel_window_adjust: #{packet[:local_id]} +#{packet[:extra_bytes]}" }
|
659
|
+
channels[packet[:local_id]].do_window_adjust(packet[:extra_bytes])
|
660
|
+
end
|
661
|
+
|
662
|
+
def channel_request(packet)
|
663
|
+
info { "channel_request: #{packet[:local_id]} #{packet[:request]} #{packet[:want_reply]}" }
|
664
|
+
channels[packet[:local_id]].do_request(packet[:request], packet[:want_reply], packet[:request_data])
|
665
|
+
end
|
666
|
+
|
667
|
+
def channel_data(packet)
|
668
|
+
info { "channel_data: #{packet[:local_id]} #{packet[:data].length}b" }
|
669
|
+
channels[packet[:local_id]].do_data(packet[:data])
|
670
|
+
end
|
671
|
+
|
672
|
+
def channel_extended_data(packet)
|
673
|
+
info { "channel_extended_data: #{packet[:local_id]} #{packet[:data_type]} #{packet[:data].length}b" }
|
674
|
+
channels[packet[:local_id]].do_extended_data(packet[:data_type], packet[:data])
|
675
|
+
end
|
676
|
+
|
677
|
+
def channel_eof(packet)
|
678
|
+
info { "channel_eof: #{packet[:local_id]}" }
|
679
|
+
channels[packet[:local_id]].do_eof
|
680
|
+
end
|
681
|
+
|
682
|
+
def channel_close(packet)
|
683
|
+
info { "channel_close: #{packet[:local_id]}" }
|
684
|
+
|
685
|
+
channel = channels[packet[:local_id]]
|
686
|
+
channel_closed(channel)
|
687
|
+
end
|
688
|
+
|
689
|
+
def channel_success(packet)
|
690
|
+
info { "channel_success: #{packet[:local_id]}" }
|
691
|
+
channels[packet[:local_id]].do_success
|
692
|
+
end
|
693
|
+
|
694
|
+
def channel_failure(packet)
|
695
|
+
info { "channel_failure: #{packet[:local_id]}" }
|
696
|
+
channels[packet[:local_id]].do_failure
|
697
|
+
end
|
698
|
+
|
699
|
+
def io_select_wait(wait)
|
700
|
+
[wait, max_select_wait_time].compact.min
|
701
|
+
end
|
702
|
+
|
703
|
+
MAP = Constants.constants.each_with_object({}) do |name, memo|
|
704
|
+
value = const_get(name)
|
705
|
+
next unless Integer === value
|
706
|
+
|
707
|
+
memo[value] = name.downcase.to_sym
|
708
|
+
end
|
709
|
+
end
|
710
|
+
end
|
711
|
+
end
|
712
|
+
end
|