sonixlabs-net-ssh 2.3.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.
- data/CHANGELOG.rdoc +262 -0
- data/Manifest +121 -0
- data/README.rdoc +184 -0
- data/Rakefile +86 -0
- data/Rudyfile +96 -0
- data/THANKS.rdoc +19 -0
- data/lib/net/ssh.rb +223 -0
- data/lib/net/ssh/authentication/agent.rb +179 -0
- data/lib/net/ssh/authentication/constants.rb +18 -0
- data/lib/net/ssh/authentication/key_manager.rb +253 -0
- data/lib/net/ssh/authentication/methods/abstract.rb +60 -0
- data/lib/net/ssh/authentication/methods/hostbased.rb +75 -0
- data/lib/net/ssh/authentication/methods/keyboard_interactive.rb +70 -0
- data/lib/net/ssh/authentication/methods/password.rb +43 -0
- data/lib/net/ssh/authentication/methods/publickey.rb +96 -0
- data/lib/net/ssh/authentication/pageant.rb +264 -0
- data/lib/net/ssh/authentication/session.rb +146 -0
- data/lib/net/ssh/buffer.rb +340 -0
- data/lib/net/ssh/buffered_io.rb +198 -0
- data/lib/net/ssh/config.rb +207 -0
- data/lib/net/ssh/connection/channel.rb +630 -0
- data/lib/net/ssh/connection/constants.rb +33 -0
- data/lib/net/ssh/connection/session.rb +597 -0
- data/lib/net/ssh/connection/term.rb +178 -0
- data/lib/net/ssh/errors.rb +88 -0
- data/lib/net/ssh/key_factory.rb +102 -0
- data/lib/net/ssh/known_hosts.rb +129 -0
- data/lib/net/ssh/loggable.rb +61 -0
- data/lib/net/ssh/packet.rb +102 -0
- data/lib/net/ssh/prompt.rb +93 -0
- data/lib/net/ssh/proxy/command.rb +75 -0
- data/lib/net/ssh/proxy/errors.rb +14 -0
- data/lib/net/ssh/proxy/http.rb +94 -0
- data/lib/net/ssh/proxy/socks4.rb +70 -0
- data/lib/net/ssh/proxy/socks5.rb +142 -0
- data/lib/net/ssh/ruby_compat.rb +43 -0
- data/lib/net/ssh/service/forward.rb +298 -0
- data/lib/net/ssh/test.rb +89 -0
- data/lib/net/ssh/test/channel.rb +129 -0
- data/lib/net/ssh/test/extensions.rb +152 -0
- data/lib/net/ssh/test/kex.rb +44 -0
- data/lib/net/ssh/test/local_packet.rb +51 -0
- data/lib/net/ssh/test/packet.rb +81 -0
- data/lib/net/ssh/test/remote_packet.rb +38 -0
- data/lib/net/ssh/test/script.rb +157 -0
- data/lib/net/ssh/test/socket.rb +64 -0
- data/lib/net/ssh/transport/algorithms.rb +386 -0
- data/lib/net/ssh/transport/cipher_factory.rb +79 -0
- data/lib/net/ssh/transport/constants.rb +30 -0
- data/lib/net/ssh/transport/hmac.rb +42 -0
- data/lib/net/ssh/transport/hmac/abstract.rb +79 -0
- data/lib/net/ssh/transport/hmac/md5.rb +12 -0
- data/lib/net/ssh/transport/hmac/md5_96.rb +11 -0
- data/lib/net/ssh/transport/hmac/none.rb +15 -0
- data/lib/net/ssh/transport/hmac/sha1.rb +13 -0
- data/lib/net/ssh/transport/hmac/sha1_96.rb +11 -0
- data/lib/net/ssh/transport/hmac/sha2_256.rb +15 -0
- data/lib/net/ssh/transport/hmac/sha2_256_96.rb +13 -0
- data/lib/net/ssh/transport/hmac/sha2_512.rb +14 -0
- data/lib/net/ssh/transport/hmac/sha2_512_96.rb +13 -0
- data/lib/net/ssh/transport/identity_cipher.rb +55 -0
- data/lib/net/ssh/transport/kex.rb +17 -0
- data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +208 -0
- data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +80 -0
- data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha256.rb +15 -0
- data/lib/net/ssh/transport/key_expander.rb +26 -0
- data/lib/net/ssh/transport/openssl.rb +127 -0
- data/lib/net/ssh/transport/packet_stream.rb +235 -0
- data/lib/net/ssh/transport/server_version.rb +71 -0
- data/lib/net/ssh/transport/session.rb +278 -0
- data/lib/net/ssh/transport/state.rb +206 -0
- data/lib/net/ssh/verifiers/lenient.rb +30 -0
- data/lib/net/ssh/verifiers/null.rb +12 -0
- data/lib/net/ssh/verifiers/strict.rb +53 -0
- data/lib/net/ssh/version.rb +62 -0
- data/lib/sonixlabs-net-ssh.rb +1 -0
- data/net-ssh.gemspec +145 -0
- data/setup.rb +1585 -0
- data/support/arcfour_check.rb +20 -0
- data/support/ssh_tunnel_bug.rb +65 -0
- data/test/authentication/methods/common.rb +28 -0
- data/test/authentication/methods/test_abstract.rb +51 -0
- data/test/authentication/methods/test_hostbased.rb +114 -0
- data/test/authentication/methods/test_keyboard_interactive.rb +100 -0
- data/test/authentication/methods/test_password.rb +52 -0
- data/test/authentication/methods/test_publickey.rb +148 -0
- data/test/authentication/test_agent.rb +205 -0
- data/test/authentication/test_key_manager.rb +171 -0
- data/test/authentication/test_session.rb +106 -0
- data/test/common.rb +107 -0
- data/test/configs/eqsign +3 -0
- data/test/configs/exact_match +8 -0
- data/test/configs/host_plus +10 -0
- data/test/configs/multihost +4 -0
- data/test/configs/wild_cards +14 -0
- data/test/connection/test_channel.rb +467 -0
- data/test/connection/test_session.rb +488 -0
- data/test/test_all.rb +9 -0
- data/test/test_buffer.rb +336 -0
- data/test/test_buffered_io.rb +63 -0
- data/test/test_config.rb +120 -0
- data/test/test_key_factory.rb +79 -0
- data/test/transport/hmac/test_md5.rb +39 -0
- data/test/transport/hmac/test_md5_96.rb +25 -0
- data/test/transport/hmac/test_none.rb +34 -0
- data/test/transport/hmac/test_sha1.rb +34 -0
- data/test/transport/hmac/test_sha1_96.rb +25 -0
- data/test/transport/hmac/test_sha2_256.rb +35 -0
- data/test/transport/hmac/test_sha2_256_96.rb +25 -0
- data/test/transport/hmac/test_sha2_512.rb +35 -0
- data/test/transport/hmac/test_sha2_512_96.rb +25 -0
- data/test/transport/kex/test_diffie_hellman_group1_sha1.rb +146 -0
- data/test/transport/kex/test_diffie_hellman_group_exchange_sha1.rb +92 -0
- data/test/transport/kex/test_diffie_hellman_group_exchange_sha256.rb +33 -0
- data/test/transport/test_algorithms.rb +308 -0
- data/test/transport/test_cipher_factory.rb +213 -0
- data/test/transport/test_hmac.rb +34 -0
- data/test/transport/test_identity_cipher.rb +40 -0
- data/test/transport/test_packet_stream.rb +736 -0
- data/test/transport/test_server_version.rb +78 -0
- data/test/transport/test_session.rb +315 -0
- data/test/transport/test_state.rb +179 -0
- metadata +178 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
require 'net/ssh/buffered_io'
|
|
2
|
+
require 'net/ssh/errors'
|
|
3
|
+
require 'net/ssh/packet'
|
|
4
|
+
require 'net/ssh/ruby_compat'
|
|
5
|
+
require 'net/ssh/transport/cipher_factory'
|
|
6
|
+
require 'net/ssh/transport/hmac'
|
|
7
|
+
require 'net/ssh/transport/state'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
module Net; module SSH; module Transport
|
|
11
|
+
|
|
12
|
+
# A module that builds additional functionality onto the Net::SSH::BufferedIo
|
|
13
|
+
# module. It adds SSH encryption, compression, and packet validation, as
|
|
14
|
+
# per the SSH2 protocol. It also adds an abstraction for polling packets,
|
|
15
|
+
# to allow for both blocking and non-blocking reads.
|
|
16
|
+
module PacketStream
|
|
17
|
+
include BufferedIo
|
|
18
|
+
|
|
19
|
+
def self.extended(object)
|
|
20
|
+
object.__send__(:initialize_ssh)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The map of "hints" that can be used to modify the behavior of the packet
|
|
24
|
+
# stream. For instance, when authentication succeeds, an "authenticated"
|
|
25
|
+
# hint is set, which is used to determine whether or not to compress the
|
|
26
|
+
# data when using the "delayed" compression algorithm.
|
|
27
|
+
attr_reader :hints
|
|
28
|
+
|
|
29
|
+
# The server state object, which encapsulates the algorithms used to interpret
|
|
30
|
+
# packets coming from the server.
|
|
31
|
+
attr_reader :server
|
|
32
|
+
|
|
33
|
+
# The client state object, which encapsulates the algorithms used to build
|
|
34
|
+
# packets to send to the server.
|
|
35
|
+
attr_reader :client
|
|
36
|
+
|
|
37
|
+
# The name of the client (local) end of the socket, as reported by the
|
|
38
|
+
# socket.
|
|
39
|
+
def client_name
|
|
40
|
+
@client_name ||= begin
|
|
41
|
+
sockaddr = getsockname
|
|
42
|
+
begin
|
|
43
|
+
Socket.getnameinfo(sockaddr, Socket::NI_NAMEREQD).first
|
|
44
|
+
rescue
|
|
45
|
+
begin
|
|
46
|
+
Socket.getnameinfo(sockaddr).first
|
|
47
|
+
rescue
|
|
48
|
+
begin
|
|
49
|
+
Socket.gethostbyname(Socket.gethostname).first
|
|
50
|
+
rescue
|
|
51
|
+
lwarn { "the client ipaddr/name could not be determined" }
|
|
52
|
+
"unknown"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The IP address of the peer (remote) end of the socket, as reported by
|
|
60
|
+
# the socket.
|
|
61
|
+
def peer_ip
|
|
62
|
+
@peer_ip ||=
|
|
63
|
+
if respond_to?(:getpeername)
|
|
64
|
+
addr = getpeername
|
|
65
|
+
Socket.getnameinfo(addr, Socket::NI_NUMERICHOST | Socket::NI_NUMERICSERV).first
|
|
66
|
+
else
|
|
67
|
+
"<no hostip for proxy command>"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Returns true if the IO is available for reading, and false otherwise.
|
|
72
|
+
def available_for_read?
|
|
73
|
+
result = Net::SSH::Compat.io_select([self], nil, nil, 0)
|
|
74
|
+
result && result.first.any?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Returns the next full packet. If the mode parameter is :nonblock (the
|
|
78
|
+
# default), then this will return immediately, whether a packet is
|
|
79
|
+
# available or not, and will return nil if there is no packet ready to be
|
|
80
|
+
# returned. If the mode parameter is :block, then this method will block
|
|
81
|
+
# until a packet is available.
|
|
82
|
+
def next_packet(mode=:nonblock)
|
|
83
|
+
case mode
|
|
84
|
+
when :nonblock then
|
|
85
|
+
fill if available_for_read?
|
|
86
|
+
poll_next_packet
|
|
87
|
+
|
|
88
|
+
when :block then
|
|
89
|
+
loop do
|
|
90
|
+
packet = poll_next_packet
|
|
91
|
+
return packet if packet
|
|
92
|
+
|
|
93
|
+
loop do
|
|
94
|
+
result = Net::SSH::Compat.io_select([self]) or next
|
|
95
|
+
break if result.first.any?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if fill <= 0
|
|
99
|
+
raise Net::SSH::Disconnect, "connection closed by remote host"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
else
|
|
104
|
+
raise ArgumentError, "expected :block or :nonblock, got #{mode.inspect}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Enqueues a packet to be sent, and blocks until the entire packet is
|
|
109
|
+
# sent.
|
|
110
|
+
def send_packet(payload)
|
|
111
|
+
enqueue_packet(payload)
|
|
112
|
+
wait_for_pending_sends
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Enqueues a packet to be sent, but does not immediately send the packet.
|
|
116
|
+
# The given payload is pre-processed according to the algorithms specified
|
|
117
|
+
# in the client state (compression, cipher, and hmac).
|
|
118
|
+
def enqueue_packet(payload)
|
|
119
|
+
# try to compress the packet
|
|
120
|
+
payload = client.compress(payload)
|
|
121
|
+
|
|
122
|
+
# the length of the packet, minus the padding
|
|
123
|
+
actual_length = 4 + payload.length + 1
|
|
124
|
+
|
|
125
|
+
# compute the padding length
|
|
126
|
+
padding_length = client.block_size - (actual_length % client.block_size)
|
|
127
|
+
padding_length += client.block_size if padding_length < 4
|
|
128
|
+
|
|
129
|
+
# compute the packet length (sans the length field itself)
|
|
130
|
+
packet_length = payload.length + padding_length + 1
|
|
131
|
+
|
|
132
|
+
if packet_length < 16
|
|
133
|
+
padding_length += client.block_size
|
|
134
|
+
packet_length = payload.length + padding_length + 1
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
padding = Array.new(padding_length) { rand(256) }.pack("C*")
|
|
138
|
+
|
|
139
|
+
unencrypted_data = [packet_length, padding_length, payload, padding].pack("NCA*A*")
|
|
140
|
+
mac = client.hmac.digest([client.sequence_number, unencrypted_data].pack("NA*"))
|
|
141
|
+
|
|
142
|
+
encrypted_data = client.update_cipher(unencrypted_data) << client.final_cipher
|
|
143
|
+
message = encrypted_data + mac
|
|
144
|
+
|
|
145
|
+
debug { "queueing packet nr #{client.sequence_number} type #{payload.getbyte(0)} len #{packet_length}" }
|
|
146
|
+
enqueue(message)
|
|
147
|
+
|
|
148
|
+
client.increment(packet_length)
|
|
149
|
+
|
|
150
|
+
self
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Performs any pending cleanup necessary on the IO and its associated
|
|
154
|
+
# state objects. (See State#cleanup).
|
|
155
|
+
def cleanup
|
|
156
|
+
client.cleanup
|
|
157
|
+
server.cleanup
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# If the IO object requires a rekey operation (as indicated by either its
|
|
161
|
+
# client or server state objects, see State#needs_rekey?), this will
|
|
162
|
+
# yield. Otherwise, this does nothing.
|
|
163
|
+
def if_needs_rekey?
|
|
164
|
+
if client.needs_rekey? || server.needs_rekey?
|
|
165
|
+
yield
|
|
166
|
+
client.reset! if client.needs_rekey?
|
|
167
|
+
server.reset! if server.needs_rekey?
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
protected
|
|
172
|
+
|
|
173
|
+
# Called when this module is used to extend an object. It initializes
|
|
174
|
+
# the states and generally prepares the object for use as a packet stream.
|
|
175
|
+
def initialize_ssh
|
|
176
|
+
@hints = {}
|
|
177
|
+
@server = State.new(self, :server)
|
|
178
|
+
@client = State.new(self, :client)
|
|
179
|
+
@packet = nil
|
|
180
|
+
initialize_buffered_io
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Tries to read the next packet. If there is insufficient data to read
|
|
184
|
+
# an entire packet, this returns immediately, otherwise the packet is
|
|
185
|
+
# read, post-processed according to the cipher, hmac, and compression
|
|
186
|
+
# algorithms specified in the server state object, and returned as a
|
|
187
|
+
# new Packet object.
|
|
188
|
+
def poll_next_packet
|
|
189
|
+
if @packet.nil?
|
|
190
|
+
minimum = server.block_size < 4 ? 4 : server.block_size
|
|
191
|
+
return nil if available < minimum
|
|
192
|
+
data = read_available(minimum)
|
|
193
|
+
|
|
194
|
+
# decipher it
|
|
195
|
+
@packet = Net::SSH::Buffer.new(server.update_cipher(data))
|
|
196
|
+
@packet_length = @packet.read_long
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
need = @packet_length + 4 - server.block_size
|
|
200
|
+
raise Net::SSH::Exception, "padding error, need #{need} block #{server.block_size}" if need % server.block_size != 0
|
|
201
|
+
|
|
202
|
+
return nil if available < need + server.hmac.mac_length
|
|
203
|
+
|
|
204
|
+
if need > 0
|
|
205
|
+
# read the remainder of the packet and decrypt it.
|
|
206
|
+
data = read_available(need)
|
|
207
|
+
@packet.append(server.update_cipher(data))
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# get the hmac from the tail of the packet (if one exists), and
|
|
211
|
+
# then validate it.
|
|
212
|
+
real_hmac = read_available(server.hmac.mac_length) || ""
|
|
213
|
+
|
|
214
|
+
@packet.append(server.final_cipher)
|
|
215
|
+
padding_length = @packet.read_byte
|
|
216
|
+
|
|
217
|
+
payload = @packet.read(@packet_length - padding_length - 1)
|
|
218
|
+
padding = @packet.read(padding_length) if padding_length > 0
|
|
219
|
+
|
|
220
|
+
my_computed_hmac = server.hmac.digest([server.sequence_number, @packet.content].pack("NA*"))
|
|
221
|
+
raise Net::SSH::Exception, "corrupted mac detected" if real_hmac != my_computed_hmac
|
|
222
|
+
|
|
223
|
+
# try to decompress the payload, in case compression is active
|
|
224
|
+
payload = server.decompress(payload)
|
|
225
|
+
|
|
226
|
+
debug { "received packet nr #{server.sequence_number} type #{payload.getbyte(0)} len #{@packet_length}" }
|
|
227
|
+
|
|
228
|
+
server.increment(@packet_length)
|
|
229
|
+
@packet = nil
|
|
230
|
+
|
|
231
|
+
return Packet.new(payload)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
end; end; end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
require 'net/ssh/errors'
|
|
2
|
+
require 'net/ssh/loggable'
|
|
3
|
+
require 'net/ssh/version'
|
|
4
|
+
|
|
5
|
+
module Net; module SSH; module Transport
|
|
6
|
+
|
|
7
|
+
# Negotiates the SSH protocol version and trades information about server
|
|
8
|
+
# and client. This is never used directly--it is always called by the
|
|
9
|
+
# transport layer as part of the initialization process of the transport
|
|
10
|
+
# layer.
|
|
11
|
+
#
|
|
12
|
+
# Note that this class also encapsulates the negotiated version, and acts as
|
|
13
|
+
# the authoritative reference for any queries regarding the version in effect.
|
|
14
|
+
class ServerVersion
|
|
15
|
+
include Loggable
|
|
16
|
+
|
|
17
|
+
# The SSH version string as reported by Net::SSH
|
|
18
|
+
PROTO_VERSION = "SSH-2.0-Ruby/Net::SSH_#{Net::SSH::Version::CURRENT} #{RUBY_PLATFORM}"
|
|
19
|
+
|
|
20
|
+
# Any header text sent by the server prior to sending the version.
|
|
21
|
+
attr_reader :header
|
|
22
|
+
|
|
23
|
+
# The version string reported by the server.
|
|
24
|
+
attr_reader :version
|
|
25
|
+
|
|
26
|
+
# Instantiates a new ServerVersion and immediately (and synchronously)
|
|
27
|
+
# negotiates the SSH protocol in effect, using the given socket.
|
|
28
|
+
def initialize(socket, logger)
|
|
29
|
+
@header = ""
|
|
30
|
+
@version = nil
|
|
31
|
+
@logger = logger
|
|
32
|
+
negotiate!(socket)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Negotiates the SSH protocol to use, via the given socket. If the server
|
|
38
|
+
# reports an incompatible SSH version (e.g., SSH1), this will raise an
|
|
39
|
+
# exception.
|
|
40
|
+
def negotiate!(socket)
|
|
41
|
+
info { "negotiating protocol version" }
|
|
42
|
+
|
|
43
|
+
loop do
|
|
44
|
+
@version = ""
|
|
45
|
+
loop do
|
|
46
|
+
begin
|
|
47
|
+
b = socket.readpartial(1)
|
|
48
|
+
raise Net::SSH::Disconnect, "connection closed by remote host" if b.nil?
|
|
49
|
+
rescue EOFError => e
|
|
50
|
+
raise Net::SSH::Disconnect, "connection closed by remote host"
|
|
51
|
+
end
|
|
52
|
+
@version << b
|
|
53
|
+
break if b == "\n"
|
|
54
|
+
end
|
|
55
|
+
break if @version.match(/^SSH-/)
|
|
56
|
+
@header << @version
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
@version.chomp!
|
|
60
|
+
debug { "remote is `#{@version}'" }
|
|
61
|
+
|
|
62
|
+
unless @version.match(/^SSH-(1\.99|2\.0)-/)
|
|
63
|
+
raise Net::SSH::Exception, "incompatible SSH version `#{@version}'"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
debug { "local is `#{PROTO_VERSION}'" }
|
|
67
|
+
socket.write "#{PROTO_VERSION}\r\n"
|
|
68
|
+
socket.flush
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end; end; end
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
require 'socket'
|
|
2
|
+
require 'timeout'
|
|
3
|
+
|
|
4
|
+
require 'net/ssh/errors'
|
|
5
|
+
require 'net/ssh/loggable'
|
|
6
|
+
require 'net/ssh/version'
|
|
7
|
+
require 'net/ssh/transport/algorithms'
|
|
8
|
+
require 'net/ssh/transport/constants'
|
|
9
|
+
require 'net/ssh/transport/packet_stream'
|
|
10
|
+
require 'net/ssh/transport/server_version'
|
|
11
|
+
require 'net/ssh/verifiers/null'
|
|
12
|
+
require 'net/ssh/verifiers/strict'
|
|
13
|
+
require 'net/ssh/verifiers/lenient'
|
|
14
|
+
|
|
15
|
+
module Net; module SSH; module Transport
|
|
16
|
+
|
|
17
|
+
# The transport layer represents the lowest level of the SSH protocol, and
|
|
18
|
+
# implements basic message exchanging and protocol initialization. It will
|
|
19
|
+
# never be instantiated directly (unless you really know what you're about),
|
|
20
|
+
# but will instead be created for you automatically when you create a new
|
|
21
|
+
# SSH session via Net::SSH.start.
|
|
22
|
+
class Session
|
|
23
|
+
include Constants, Loggable
|
|
24
|
+
|
|
25
|
+
# The standard port for the SSH protocol.
|
|
26
|
+
DEFAULT_PORT = 22
|
|
27
|
+
|
|
28
|
+
# The host to connect to, as given to the constructor.
|
|
29
|
+
attr_reader :host
|
|
30
|
+
|
|
31
|
+
# The port number to connect to, as given in the options to the constructor.
|
|
32
|
+
# If no port number was given, this will default to DEFAULT_PORT.
|
|
33
|
+
attr_reader :port
|
|
34
|
+
|
|
35
|
+
# The underlying socket object being used to communicate with the remote
|
|
36
|
+
# host.
|
|
37
|
+
attr_reader :socket
|
|
38
|
+
|
|
39
|
+
# The ServerVersion instance that encapsulates the negotiated protocol
|
|
40
|
+
# version.
|
|
41
|
+
attr_reader :server_version
|
|
42
|
+
|
|
43
|
+
# The Algorithms instance used to perform key exchanges.
|
|
44
|
+
attr_reader :algorithms
|
|
45
|
+
|
|
46
|
+
# The host-key verifier object used to verify host keys, to ensure that
|
|
47
|
+
# the connection is not being spoofed.
|
|
48
|
+
attr_reader :host_key_verifier
|
|
49
|
+
|
|
50
|
+
# The hash of options that were given to the object at initialization.
|
|
51
|
+
attr_reader :options
|
|
52
|
+
|
|
53
|
+
# Instantiates a new transport layer abstraction. This will block until
|
|
54
|
+
# the initial key exchange completes, leaving you with a ready-to-use
|
|
55
|
+
# transport session.
|
|
56
|
+
def initialize(host, options={})
|
|
57
|
+
self.logger = options[:logger]
|
|
58
|
+
|
|
59
|
+
@host = host
|
|
60
|
+
@port = options[:port] || DEFAULT_PORT
|
|
61
|
+
@bind_address = options[:bind_address] || nil
|
|
62
|
+
@options = options
|
|
63
|
+
|
|
64
|
+
debug { "establishing connection to #{@host}:#{@port}" }
|
|
65
|
+
factory = options[:proxy] || TCPSocket
|
|
66
|
+
@socket = timeout(options[:timeout] || 0) { @bind_address.nil? || options[:proxy] ? factory.open(@host, @port) : factory.open(@host,@port,@bind_address) }
|
|
67
|
+
@socket.extend(PacketStream)
|
|
68
|
+
@socket.logger = @logger
|
|
69
|
+
|
|
70
|
+
debug { "connection established" }
|
|
71
|
+
|
|
72
|
+
@queue = []
|
|
73
|
+
|
|
74
|
+
@host_key_verifier = select_host_key_verifier(options[:paranoid])
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@server_version = timeout(options[:timeout] || 0) { ServerVersion.new(socket, logger) }
|
|
78
|
+
|
|
79
|
+
@algorithms = Algorithms.new(self, options)
|
|
80
|
+
wait { algorithms.initialized? }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Returns the host (and possibly IP address) in a format compatible with
|
|
84
|
+
# SSH known-host files.
|
|
85
|
+
def host_as_string
|
|
86
|
+
@host_as_string ||= begin
|
|
87
|
+
string = "#{host}"
|
|
88
|
+
string = "[#{string}]:#{port}" if port != DEFAULT_PORT
|
|
89
|
+
if socket.peer_ip != host
|
|
90
|
+
string2 = socket.peer_ip
|
|
91
|
+
string2 = "[#{string2}]:#{port}" if port != DEFAULT_PORT
|
|
92
|
+
string << "," << string2
|
|
93
|
+
end
|
|
94
|
+
string
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Returns true if the underlying socket has been closed.
|
|
99
|
+
def closed?
|
|
100
|
+
socket.closed?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Cleans up (see PacketStream#cleanup) and closes the underlying socket.
|
|
104
|
+
def close
|
|
105
|
+
socket.cleanup
|
|
106
|
+
socket.close
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Performs a "hard" shutdown of the connection. In general, this should
|
|
110
|
+
# never be done, but it might be necessary (in a rescue clause, for instance,
|
|
111
|
+
# when the connection needs to close but you don't know the status of the
|
|
112
|
+
# underlying protocol's state).
|
|
113
|
+
def shutdown!
|
|
114
|
+
error { "forcing connection closed" }
|
|
115
|
+
socket.close
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Returns a new service_request packet for the given service name, ready
|
|
119
|
+
# for sending to the server.
|
|
120
|
+
def service_request(service)
|
|
121
|
+
Net::SSH::Buffer.from(:byte, SERVICE_REQUEST, :string, service)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Requests a rekey operation, and blocks until the operation completes.
|
|
125
|
+
# If a rekey is already pending, this returns immediately, having no
|
|
126
|
+
# effect.
|
|
127
|
+
def rekey!
|
|
128
|
+
if !algorithms.pending?
|
|
129
|
+
algorithms.rekey!
|
|
130
|
+
wait { algorithms.initialized? }
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Returns immediately if a rekey is already in process. Otherwise, if a
|
|
135
|
+
# rekey is needed (as indicated by the socket, see PacketStream#if_needs_rekey?)
|
|
136
|
+
# one is performed, causing this method to block until it completes.
|
|
137
|
+
def rekey_as_needed
|
|
138
|
+
return if algorithms.pending?
|
|
139
|
+
socket.if_needs_rekey? { rekey! }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Returns a hash of information about the peer (remote) side of the socket,
|
|
143
|
+
# including :ip, :port, :host, and :canonized (see #host_as_string).
|
|
144
|
+
def peer
|
|
145
|
+
@peer ||= { :ip => socket.peer_ip, :port => @port.to_i, :host => @host, :canonized => host_as_string }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Blocks until a new packet is available to be read, and returns that
|
|
149
|
+
# packet. See #poll_message.
|
|
150
|
+
def next_message
|
|
151
|
+
poll_message(:block)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Tries to read the next packet from the socket. If mode is :nonblock (the
|
|
155
|
+
# default), this will not block and will return nil if there are no packets
|
|
156
|
+
# waiting to be read. Otherwise, this will block until a packet is
|
|
157
|
+
# available. Note that some packet types (DISCONNECT, IGNORE, UNIMPLEMENTED,
|
|
158
|
+
# DEBUG, and KEXINIT) are handled silently by this method, and will never
|
|
159
|
+
# be returned.
|
|
160
|
+
#
|
|
161
|
+
# If a key-exchange is in process and a disallowed packet type is
|
|
162
|
+
# received, it will be enqueued and otherwise ignored. When a key-exchange
|
|
163
|
+
# is not in process, and consume_queue is true, packets will be first
|
|
164
|
+
# read from the queue before the socket is queried.
|
|
165
|
+
def poll_message(mode=:nonblock, consume_queue=true)
|
|
166
|
+
loop do
|
|
167
|
+
if consume_queue && @queue.any? && algorithms.allow?(@queue.first)
|
|
168
|
+
return @queue.shift
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
packet = socket.next_packet(mode)
|
|
172
|
+
return nil if packet.nil?
|
|
173
|
+
|
|
174
|
+
case packet.type
|
|
175
|
+
when DISCONNECT
|
|
176
|
+
raise Net::SSH::Disconnect, "disconnected: #{packet[:description]} (#{packet[:reason_code]})"
|
|
177
|
+
|
|
178
|
+
when IGNORE
|
|
179
|
+
debug { "IGNORE packet recieved: #{packet[:data].inspect}" }
|
|
180
|
+
|
|
181
|
+
when UNIMPLEMENTED
|
|
182
|
+
lwarn { "UNIMPLEMENTED: #{packet[:number]}" }
|
|
183
|
+
|
|
184
|
+
when DEBUG
|
|
185
|
+
__send__(packet[:always_display] ? :fatal : :debug) { packet[:message] }
|
|
186
|
+
|
|
187
|
+
when KEXINIT
|
|
188
|
+
algorithms.accept_kexinit(packet)
|
|
189
|
+
|
|
190
|
+
else
|
|
191
|
+
return packet if algorithms.allow?(packet)
|
|
192
|
+
push(packet)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Waits (blocks) until the given block returns true. If no block is given,
|
|
198
|
+
# this just waits long enough to see if there are any pending packets. Any
|
|
199
|
+
# packets read are enqueued (see #push).
|
|
200
|
+
def wait
|
|
201
|
+
loop do
|
|
202
|
+
break if block_given? && yield
|
|
203
|
+
message = poll_message(:nonblock, false)
|
|
204
|
+
push(message) if message
|
|
205
|
+
break if !block_given?
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Adds the given packet to the packet queue. If the queue is non-empty,
|
|
210
|
+
# #poll_message will return packets from the queue in the order they
|
|
211
|
+
# were received.
|
|
212
|
+
def push(packet)
|
|
213
|
+
@queue.push(packet)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Sends the given message via the packet stream, blocking until the
|
|
217
|
+
# entire message has been sent.
|
|
218
|
+
def send_message(message)
|
|
219
|
+
socket.send_packet(message)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Enqueues the given message, such that it will be sent at the earliest
|
|
223
|
+
# opportunity. This does not block, but returns immediately.
|
|
224
|
+
def enqueue_message(message)
|
|
225
|
+
socket.enqueue_packet(message)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Configure's the packet stream's client state with the given set of
|
|
229
|
+
# options. This is typically used to define the cipher, compression, and
|
|
230
|
+
# hmac algorithms to use when sending packets to the server.
|
|
231
|
+
def configure_client(options={})
|
|
232
|
+
socket.client.set(options)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Configure's the packet stream's server state with the given set of
|
|
236
|
+
# options. This is typically used to define the cipher, compression, and
|
|
237
|
+
# hmac algorithms to use when reading packets from the server.
|
|
238
|
+
def configure_server(options={})
|
|
239
|
+
socket.server.set(options)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Sets a new hint for the packet stream, which the packet stream may use
|
|
243
|
+
# to change its behavior. (See PacketStream#hints).
|
|
244
|
+
def hint(which, value=true)
|
|
245
|
+
socket.hints[which] = value
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
public
|
|
249
|
+
|
|
250
|
+
# this method is primarily for use in tests
|
|
251
|
+
attr_reader :queue #:nodoc:
|
|
252
|
+
|
|
253
|
+
private
|
|
254
|
+
|
|
255
|
+
# Instantiates a new host-key verification class, based on the value of
|
|
256
|
+
# the parameter. When true or nil, the default Lenient verifier is
|
|
257
|
+
# returned. If it is false, the Null verifier is returned, and if it is
|
|
258
|
+
# :very, the Strict verifier is returned. If the argument happens to
|
|
259
|
+
# respond to :verify, it is returned directly. Otherwise, an exception
|
|
260
|
+
# is raised.
|
|
261
|
+
def select_host_key_verifier(paranoid)
|
|
262
|
+
case paranoid
|
|
263
|
+
when true, nil then
|
|
264
|
+
Net::SSH::Verifiers::Lenient.new
|
|
265
|
+
when false then
|
|
266
|
+
Net::SSH::Verifiers::Null.new
|
|
267
|
+
when :very then
|
|
268
|
+
Net::SSH::Verifiers::Strict.new
|
|
269
|
+
else
|
|
270
|
+
if paranoid.respond_to?(:verify)
|
|
271
|
+
paranoid
|
|
272
|
+
else
|
|
273
|
+
raise ArgumentError, "argument to :paranoid is not valid: #{paranoid.inspect}"
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end; end; end
|