net-ssh 7.3.6 → 8.0.2
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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.github/pull_request_template.md +1 -0
- data/.github/workflows/changelog.yml +43 -0
- data/.github/workflows/ci-with-docker.yml +0 -3
- data/.github/workflows/ci.yml +2 -17
- data/.github/workflows/rubocop.yml +3 -1
- data/.rubocop.yml +5 -1
- data/.rubocop_todo.yml +1 -7
- data/CHANGES.txt +82 -7
- data/CONTRIBUTING.md +35 -0
- data/Dockerfile +3 -3
- data/Dockerfile.openssl3 +1 -1
- data/Dockerfile.zlib_ng +1 -1
- data/README.md +71 -57
- data/Rakefile +66 -12
- data/appveyor.yml +4 -16
- data/changelog.d/README.md +1 -0
- data/docker-compose.yml +1 -7
- data/lib/net/ssh/authentication/agent.rb +35 -11
- data/lib/net/ssh/authentication/ed25519.rb +137 -44
- data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
- data/lib/net/ssh/authentication/key_manager.rb +22 -11
- data/lib/net/ssh/authentication/methods/abstract.rb +2 -8
- data/lib/net/ssh/authentication/methods/publickey.rb +3 -1
- data/lib/net/ssh/authentication/security_key.rb +71 -0
- data/lib/net/ssh/authentication/session.rb +29 -17
- data/lib/net/ssh/buffer.rb +6 -1
- data/lib/net/ssh/buffered_io.rb +40 -3
- data/lib/net/ssh/config.rb +28 -12
- data/lib/net/ssh/connection/channel.rb +49 -2
- data/lib/net/ssh/connection/session.rb +58 -8
- data/lib/net/ssh/errors.rb +24 -6
- data/lib/net/ssh/key_factory.rb +7 -1
- data/lib/net/ssh/known_hosts.rb +18 -8
- data/lib/net/ssh/multiplex/client.rb +162 -0
- data/lib/net/ssh/multiplex/control_path.rb +120 -0
- data/lib/net/ssh/multiplex/proxy_transport.rb +218 -0
- data/lib/net/ssh/multiplex.rb +126 -0
- data/lib/net/ssh/prompt.rb +3 -3
- data/lib/net/ssh/proxy/jump.rb +40 -2
- data/lib/net/ssh/service/forward.rb +35 -24
- data/lib/net/ssh/test.rb +2 -1
- data/lib/net/ssh/transport/algorithms.rb +88 -33
- data/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +89 -16
- data/lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb +20 -1
- data/lib/net/ssh/transport/cipher_factory.rb +23 -1
- data/lib/net/ssh/transport/constants.rb +1 -0
- data/lib/net/ssh/transport/gcm_cipher.rb +27 -6
- data/lib/net/ssh/transport/hmac/sha1_etm.rb +12 -0
- data/lib/net/ssh/transport/hmac.rb +2 -0
- data/lib/net/ssh/transport/kex/curve25519_sha256.rb +41 -9
- data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +14 -6
- data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +8 -0
- data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +45 -15
- data/lib/net/ssh/transport/openssl.rb +2 -1
- data/lib/net/ssh/transport/packet_stream.rb +12 -0
- data/lib/net/ssh/transport/session.rb +43 -45
- data/lib/net/ssh/verifiers/always.rb +0 -3
- data/lib/net/ssh/version.rb +3 -3
- data/lib/net/ssh.rb +141 -58
- data/net-ssh.gemspec +5 -8
- data/rakelib/changelog.rb +229 -0
- data.tar.gz.sig +0 -0
- metadata +37 -36
- metadata.gz.sig +0 -0
- data/Gemfile.noed25519 +0 -12
- data/Gemfile.norbnacl +0 -12
data/lib/net/ssh/key_factory.rb
CHANGED
|
@@ -2,6 +2,7 @@ require 'net/ssh/transport/openssl'
|
|
|
2
2
|
require 'net/ssh/prompt'
|
|
3
3
|
|
|
4
4
|
require 'net/ssh/authentication/ed25519_loader'
|
|
5
|
+
require 'net/ssh/authentication/security_key'
|
|
5
6
|
|
|
6
7
|
module Net
|
|
7
8
|
module SSH
|
|
@@ -92,7 +93,7 @@ module Net
|
|
|
92
93
|
blob = nil
|
|
93
94
|
begin
|
|
94
95
|
blob = fields.shift
|
|
95
|
-
end while !blob.nil? &&
|
|
96
|
+
end while !blob.nil? && !public_key_type?(blob)
|
|
96
97
|
blob = fields.shift
|
|
97
98
|
|
|
98
99
|
raise Net::SSH::Exception, "public key at #{filename} is not valid" if blob.nil?
|
|
@@ -104,6 +105,11 @@ module Net
|
|
|
104
105
|
|
|
105
106
|
private
|
|
106
107
|
|
|
108
|
+
def public_key_type?(type)
|
|
109
|
+
/^(ssh-(rsa|dss|ed25519)|ecdsa-sha2-nistp\d+)(-cert-v01@openssh\.com)?$/.match?(type) ||
|
|
110
|
+
Net::SSH::Authentication::SecurityKey.understands?(type)
|
|
111
|
+
end
|
|
112
|
+
|
|
107
113
|
# rubocop:disable Style/Documentation, Lint/DuplicateMethods
|
|
108
114
|
class KeyType
|
|
109
115
|
def self.read(key_data, passphrase)
|
data/lib/net/ssh/known_hosts.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
require 'strscan'
|
|
2
2
|
require 'openssl'
|
|
3
|
-
require 'delegate'
|
|
4
3
|
require 'net/ssh/buffer'
|
|
5
4
|
require 'net/ssh/authentication/ed25519_loader'
|
|
6
5
|
|
|
@@ -8,8 +7,11 @@ module Net
|
|
|
8
7
|
module SSH
|
|
9
8
|
module HostKeyEntries
|
|
10
9
|
# regular public key entry
|
|
11
|
-
class PubKey
|
|
12
|
-
|
|
10
|
+
class PubKey
|
|
11
|
+
# The public key itself, e.g. an OpenSSL::PKey::PKey.
|
|
12
|
+
attr_reader :key
|
|
13
|
+
|
|
14
|
+
def initialize(key, comment: nil)
|
|
13
15
|
@key = key
|
|
14
16
|
@comment = comment
|
|
15
17
|
end
|
|
@@ -26,14 +28,22 @@ module Net
|
|
|
26
28
|
@key.to_blob
|
|
27
29
|
end
|
|
28
30
|
|
|
29
|
-
def __getobj__
|
|
30
|
-
Kernel.warn("Calling Net::SSH::Buffer methods on HostKeyEntries PubKey is deprecated")
|
|
31
|
-
@key
|
|
32
|
-
end
|
|
33
|
-
|
|
34
31
|
def matches_key?(server_key)
|
|
35
32
|
@key.ssh_type == server_key.ssh_type && @key.to_blob == server_key.to_blob
|
|
36
33
|
end
|
|
34
|
+
|
|
35
|
+
def method_missing(name, *args, &block)
|
|
36
|
+
return super unless @key.respond_to?(name)
|
|
37
|
+
|
|
38
|
+
raise NoMethodError.new(
|
|
39
|
+
"undefined method `#{name}' for #{self.class}; call it on #key, the wrapped public key",
|
|
40
|
+
name
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def respond_to_missing?(_name, _include_private = false)
|
|
45
|
+
false
|
|
46
|
+
end
|
|
37
47
|
end
|
|
38
48
|
|
|
39
49
|
# @cert-authority entry
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
require 'io/wait'
|
|
2
|
+
require 'socket'
|
|
3
|
+
|
|
4
|
+
require 'net/ssh/buffer'
|
|
5
|
+
require 'net/ssh/errors'
|
|
6
|
+
require 'net/ssh/loggable'
|
|
7
|
+
|
|
8
|
+
module Net
|
|
9
|
+
module SSH
|
|
10
|
+
module Multiplex
|
|
11
|
+
# Raised when the ControlMaster socket can't be used, e.g. the master
|
|
12
|
+
# refused the request or spoke an unexpected protocol.
|
|
13
|
+
class Error < Net::SSH::Exception; end
|
|
14
|
+
|
|
15
|
+
# Connects to an OpenSSH ControlMaster's UNIX socket and switches it to
|
|
16
|
+
# proxy mode (MUX_C_PROXY, OpenSSH 7.4 and later), after which the socket
|
|
17
|
+
# carries plain SSH connection-protocol packets. See PROTOCOL.mux and
|
|
18
|
+
# mux.c in OpenSSH.
|
|
19
|
+
class Client
|
|
20
|
+
include Loggable
|
|
21
|
+
|
|
22
|
+
MUX_MSG_HELLO = 0x00000001
|
|
23
|
+
MUX_C_PROXY = 0x1000000f
|
|
24
|
+
MUX_S_PERMISSION_DENIED = 0x80000002
|
|
25
|
+
MUX_S_FAILURE = 0x80000003
|
|
26
|
+
MUX_S_PROXY = 0x8000000f
|
|
27
|
+
|
|
28
|
+
PROTOCOL_VERSION = 4
|
|
29
|
+
|
|
30
|
+
# Mux control messages are small; this only guards against reading a
|
|
31
|
+
# bogus length from something that isn't a ControlMaster.
|
|
32
|
+
MAX_MESSAGE_SIZE = 256 * 1024
|
|
33
|
+
|
|
34
|
+
attr_reader :path
|
|
35
|
+
|
|
36
|
+
def initialize(path, timeout:, logger: nil)
|
|
37
|
+
self.logger = logger
|
|
38
|
+
@path = path
|
|
39
|
+
@timeout = timeout
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Performs the HELLO and PROXY exchange and returns the connected
|
|
43
|
+
# socket, positioned at the first proxied packet. Raises
|
|
44
|
+
# Multiplex::Error, SystemCallError or IOError if the master can't be
|
|
45
|
+
# used; the socket is closed in that case.
|
|
46
|
+
def connect_proxy
|
|
47
|
+
@deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @timeout
|
|
48
|
+
@socket = open_socket
|
|
49
|
+
verify_peer
|
|
50
|
+
hello
|
|
51
|
+
request_proxy
|
|
52
|
+
@socket
|
|
53
|
+
rescue StandardError
|
|
54
|
+
@socket&.close
|
|
55
|
+
raise
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def open_socket
|
|
61
|
+
socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_STREAM)
|
|
62
|
+
address = Socket.sockaddr_un(path)
|
|
63
|
+
loop do
|
|
64
|
+
result = socket.connect_nonblock(address, exception: false)
|
|
65
|
+
break if result == 0
|
|
66
|
+
|
|
67
|
+
wait_for(:writable, socket)
|
|
68
|
+
rescue Errno::EISCONN
|
|
69
|
+
break
|
|
70
|
+
end
|
|
71
|
+
socket
|
|
72
|
+
rescue StandardError
|
|
73
|
+
socket&.close
|
|
74
|
+
raise
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The master itself rejects clients with a different uid (channels.c,
|
|
78
|
+
# getpeereid); checking here also keeps us off a socket someone else
|
|
79
|
+
# planted at the ControlPath.
|
|
80
|
+
def verify_peer
|
|
81
|
+
euid = begin
|
|
82
|
+
@socket.getpeereid.first
|
|
83
|
+
rescue NotImplementedError
|
|
84
|
+
File.stat(path).uid
|
|
85
|
+
end
|
|
86
|
+
raise Error, "#{path} is owned by uid #{euid}, not #{Process.euid}" unless euid == Process.euid
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def hello
|
|
90
|
+
type, body = read_message
|
|
91
|
+
raise Error, "expected MUX_MSG_HELLO, got 0x#{type.to_s(16)}" unless type == MUX_MSG_HELLO
|
|
92
|
+
|
|
93
|
+
version = body.read_long
|
|
94
|
+
raise Error, "unsupported mux protocol version #{version}" unless version == PROTOCOL_VERSION
|
|
95
|
+
|
|
96
|
+
# Any extensions the master advertises (OpenSSH 10.3 sends "info")
|
|
97
|
+
# are optional, so the rest of the body is ignored.
|
|
98
|
+
write_message(MUX_MSG_HELLO, :long, PROTOCOL_VERSION)
|
|
99
|
+
debug { "mux hello exchanged with #{path}" }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def request_proxy
|
|
103
|
+
request_id = 1
|
|
104
|
+
write_message(MUX_C_PROXY, :long, request_id)
|
|
105
|
+
type, body = read_message
|
|
106
|
+
case type
|
|
107
|
+
when MUX_S_PROXY
|
|
108
|
+
reply_id = body.read_long
|
|
109
|
+
raise Error, "MUX_S_PROXY for request #{reply_id}, expected #{request_id}" unless reply_id == request_id
|
|
110
|
+
when MUX_S_PERMISSION_DENIED, MUX_S_FAILURE
|
|
111
|
+
body.read_long
|
|
112
|
+
raise Error, "master refused proxy request: #{body.read_string}"
|
|
113
|
+
else
|
|
114
|
+
raise Error, "unexpected reply 0x#{type.to_s(16)} to MUX_C_PROXY"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def write_message(type, *args)
|
|
119
|
+
body = Net::SSH::Buffer.from(:long, type, *args).to_s
|
|
120
|
+
data = [body.bytesize].pack('N') + body
|
|
121
|
+
until data.empty?
|
|
122
|
+
written = @socket.write_nonblock(data, exception: false)
|
|
123
|
+
if written == :wait_writable
|
|
124
|
+
wait_for(:writable, @socket)
|
|
125
|
+
else
|
|
126
|
+
data = data.byteslice(written..-1)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def read_message
|
|
132
|
+
length = read_exactly(4).unpack1('N')
|
|
133
|
+
raise Error, "mux message length #{length} out of range" unless length.between?(4, MAX_MESSAGE_SIZE)
|
|
134
|
+
|
|
135
|
+
body = Net::SSH::Buffer.new(read_exactly(length))
|
|
136
|
+
[body.read_long, body]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Reads exactly +count+ bytes so nothing after the PROXY reply, which
|
|
140
|
+
# already belongs to the packet stream, is consumed here.
|
|
141
|
+
def read_exactly(count)
|
|
142
|
+
data = String.new(encoding: Encoding::BINARY)
|
|
143
|
+
while data.bytesize < count
|
|
144
|
+
chunk = @socket.read_nonblock(count - data.bytesize, exception: false)
|
|
145
|
+
case chunk
|
|
146
|
+
when :wait_readable then wait_for(:readable, @socket)
|
|
147
|
+
when nil then raise Error, "#{path} closed the connection during the mux handshake"
|
|
148
|
+
else data << chunk
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
data
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def wait_for(direction, socket)
|
|
155
|
+
remaining = @deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
156
|
+
ready = remaining > 0 && (direction == :readable ? socket.wait_readable(remaining) : socket.wait_writable(remaining))
|
|
157
|
+
raise Error, "timed out talking to #{path}" unless ready
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
require 'digest/sha1'
|
|
2
|
+
require 'ipaddr'
|
|
3
|
+
require 'etc'
|
|
4
|
+
require 'socket'
|
|
5
|
+
|
|
6
|
+
module Net
|
|
7
|
+
module SSH
|
|
8
|
+
module Multiplex
|
|
9
|
+
# Expands a ControlPath the way OpenSSH's ssh does: a leading ~ first,
|
|
10
|
+
# then ${ENV} and the % tokens below in a single pass (ssh.c,
|
|
11
|
+
# DEFAULT_CLIENT_PERCENT_EXPAND_ARGS in sshconnect.h, misc.c
|
|
12
|
+
# percent_dollar_expand).
|
|
13
|
+
#
|
|
14
|
+
# %% a literal %
|
|
15
|
+
# %C SHA1 hex of %l%h%p%r%j
|
|
16
|
+
# %d local home directory
|
|
17
|
+
# %h remote host name, after HostName
|
|
18
|
+
# %i local uid
|
|
19
|
+
# %j host of the last ProxyJump hop, or empty
|
|
20
|
+
# %k the host key alias, or the host name as given
|
|
21
|
+
# %L local host name up to the first dot
|
|
22
|
+
# %l local host name
|
|
23
|
+
# %n the host name as given
|
|
24
|
+
# %p remote port
|
|
25
|
+
# %r remote user
|
|
26
|
+
# %u local user
|
|
27
|
+
class ControlPath
|
|
28
|
+
# +host_arg+ is the host as given, +host+ the one connected to after
|
|
29
|
+
# HostName.
|
|
30
|
+
def initialize(host_arg:, host:, port:, remote_user:, jump_host: '', host_key_alias: nil) # rubocop:disable Metrics/ParameterLists
|
|
31
|
+
@host_arg = host_arg.to_s
|
|
32
|
+
# ssh lowercases host names, but not addresses, before expanding.
|
|
33
|
+
@host = address?(host.to_s) ? host.to_s : host.to_s.downcase
|
|
34
|
+
@port = port.to_s
|
|
35
|
+
@remote_user = remote_user.to_s
|
|
36
|
+
@jump_host = jump_host.to_s
|
|
37
|
+
@host_key_alias = host_key_alias
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Raises ArgumentError for an unknown % token, an unset ${ENV}
|
|
41
|
+
# variable or an unknown ~user, which make ssh refuse the ControlPath
|
|
42
|
+
# too.
|
|
43
|
+
def expand(path)
|
|
44
|
+
expand_tokens(expand_tilde(path))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def tokens
|
|
48
|
+
@tokens ||= begin
|
|
49
|
+
pw = Etc.getpwuid(Process.uid)
|
|
50
|
+
local_host = Socket.gethostname
|
|
51
|
+
{
|
|
52
|
+
'C' => Digest::SHA1.hexdigest("#{local_host}#{@host}#{@port}#{@remote_user}#{@jump_host}"),
|
|
53
|
+
'd' => pw.dir,
|
|
54
|
+
'h' => @host,
|
|
55
|
+
'i' => Process.uid.to_s,
|
|
56
|
+
'j' => @jump_host,
|
|
57
|
+
'k' => @host_key_alias || @host_arg,
|
|
58
|
+
'L' => local_host.split('.', 2).first,
|
|
59
|
+
'l' => local_host,
|
|
60
|
+
'n' => @host_arg,
|
|
61
|
+
'p' => @port,
|
|
62
|
+
'r' => @remote_user,
|
|
63
|
+
'u' => pw.name
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The host ssh uses for %j: the last hop of a ProxyJump list, without
|
|
69
|
+
# user or port (readconf.c, parse_jump).
|
|
70
|
+
def self.jump_host(proxy_jump)
|
|
71
|
+
last = proxy_jump.to_s.split(',').last.to_s.strip.delete_prefix('ssh://')
|
|
72
|
+
last = last.split('@').last.to_s
|
|
73
|
+
if last.start_with?('[')
|
|
74
|
+
last[1...last.index(']').to_i]
|
|
75
|
+
else
|
|
76
|
+
last.split(':').first.to_s
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# ssh uses the home directory from the password database, not $HOME
|
|
83
|
+
# (misc.c, tilde_expand).
|
|
84
|
+
def expand_tilde(path)
|
|
85
|
+
return path unless path.start_with?('~')
|
|
86
|
+
|
|
87
|
+
user, rest = path[1..].split('/', 2)
|
|
88
|
+
home = user.nil? || user.empty? ? Etc.getpwuid(Process.uid).dir : Etc.getpwnam(user).dir
|
|
89
|
+
|
|
90
|
+
rest.nil? ? home : File.join(home, rest)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def expand_tokens(path)
|
|
94
|
+
path.gsub(/\$\{([^}]*)\}|%(.?)|\$\{.*/m) do
|
|
95
|
+
if Regexp.last_match(1)
|
|
96
|
+
name = Regexp.last_match(1)
|
|
97
|
+
raise ArgumentError, "empty ${} variable" if name.empty?
|
|
98
|
+
|
|
99
|
+
ENV.fetch(name) { raise ArgumentError, "variable ${#{name}} is not set" }
|
|
100
|
+
elsif Regexp.last_match(2)
|
|
101
|
+
token = Regexp.last_match(2)
|
|
102
|
+
next '%' if token == '%'
|
|
103
|
+
|
|
104
|
+
tokens.fetch(token) { raise ArgumentError, "unknown token %#{token}" }
|
|
105
|
+
else
|
|
106
|
+
raise ArgumentError, "unterminated ${"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def address?(host)
|
|
112
|
+
IPAddr.new(host)
|
|
113
|
+
true
|
|
114
|
+
rescue IPAddr::Error
|
|
115
|
+
false
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
require 'io/wait'
|
|
2
|
+
|
|
3
|
+
require 'net/ssh/buffer'
|
|
4
|
+
require 'net/ssh/buffered_io'
|
|
5
|
+
require 'net/ssh/errors'
|
|
6
|
+
require 'net/ssh/loggable'
|
|
7
|
+
require 'net/ssh/packet'
|
|
8
|
+
require 'net/ssh/connection/constants'
|
|
9
|
+
|
|
10
|
+
module Net
|
|
11
|
+
module SSH
|
|
12
|
+
module Multiplex
|
|
13
|
+
# Packet framing on a ControlMaster socket in proxy mode: the usual
|
|
14
|
+
# uint32 packet_length and byte padding_length, but no padding, MAC,
|
|
15
|
+
# encryption or compression. The master parses everything after the
|
|
16
|
+
# padding_length byte as payload (channel_proxy_downstream in OpenSSH's
|
|
17
|
+
# channels.c), so padding must not be sent.
|
|
18
|
+
module PacketSocket
|
|
19
|
+
include BufferedIo
|
|
20
|
+
|
|
21
|
+
# OpenSSH's PACKET_MAX_SIZE.
|
|
22
|
+
MAX_PACKET_SIZE = 256 * 1024
|
|
23
|
+
|
|
24
|
+
def self.extended(object) # :nodoc:
|
|
25
|
+
object.__send__(:initialize_buffered_io)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def available_for_read?
|
|
29
|
+
!wait_readable(0).nil?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Same contract as Net::SSH::Transport::PacketStream#next_packet.
|
|
33
|
+
def next_packet(mode = :nonblock, timeout = nil)
|
|
34
|
+
case mode
|
|
35
|
+
when :nonblock
|
|
36
|
+
packet = poll_next_packet
|
|
37
|
+
return packet if packet
|
|
38
|
+
|
|
39
|
+
if available_for_read? && fill <= 0
|
|
40
|
+
packet = poll_next_packet
|
|
41
|
+
raise Net::SSH::Disconnect, "connection closed by ControlMaster" if packet.nil?
|
|
42
|
+
|
|
43
|
+
return packet
|
|
44
|
+
end
|
|
45
|
+
poll_next_packet
|
|
46
|
+
when :block
|
|
47
|
+
loop do
|
|
48
|
+
packet = poll_next_packet
|
|
49
|
+
return packet if packet
|
|
50
|
+
|
|
51
|
+
raise Net::SSH::ConnectionTimeout, "timeout waiting for next packet" unless wait_readable(timeout)
|
|
52
|
+
raise Net::SSH::Disconnect, "connection closed by ControlMaster" if fill <= 0
|
|
53
|
+
end
|
|
54
|
+
else
|
|
55
|
+
raise ArgumentError, "expected :block or :nonblock, got #{mode.inspect}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def enqueue_packet(payload)
|
|
60
|
+
data = payload.to_s
|
|
61
|
+
enqueue([data.bytesize + 1, 0].pack('NC') + data.b)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def send_packet(payload)
|
|
65
|
+
enqueue_packet(payload)
|
|
66
|
+
wait_for_pending_sends
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def poll_next_packet
|
|
72
|
+
return nil if available < 4
|
|
73
|
+
|
|
74
|
+
start = input.position
|
|
75
|
+
length = input.read_long
|
|
76
|
+
raise Net::SSH::Exception, "invalid packet length #{length} from ControlMaster" unless length.between?(1, MAX_PACKET_SIZE)
|
|
77
|
+
|
|
78
|
+
if available < length
|
|
79
|
+
input.position = start
|
|
80
|
+
return nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
data = input.read(length)
|
|
84
|
+
input.consume!
|
|
85
|
+
padding = data.getbyte(0)
|
|
86
|
+
raise Net::SSH::Exception, "invalid padding length #{padding} from ControlMaster" if padding > length - 2
|
|
87
|
+
|
|
88
|
+
Net::SSH::Packet.new(data.byteslice(1, length - 1 - padding))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Stands in for Net::SSH::Transport::Session underneath a
|
|
93
|
+
# Net::SSH::Connection::Session when connected through an OpenSSH
|
|
94
|
+
# ControlMaster in proxy mode. Key exchange, rekeying, host key
|
|
95
|
+
# verification and authentication all belong to the master, so the
|
|
96
|
+
# matching transport methods are no-ops or raise
|
|
97
|
+
# NotSupportedOverControlMaster.
|
|
98
|
+
class ProxyTransport
|
|
99
|
+
include Loggable
|
|
100
|
+
include Net::SSH::Connection::Constants
|
|
101
|
+
|
|
102
|
+
DEFAULT_PORT = 22
|
|
103
|
+
|
|
104
|
+
# Channel requests whose follow-up channel the server opens towards
|
|
105
|
+
# the master, which serves it from its own agent or X display only if
|
|
106
|
+
# it has ForwardAgent or ForwardX11 enabled itself, and rejects it
|
|
107
|
+
# otherwise (clientloop.c, client_input_channel_open).
|
|
108
|
+
UNSUPPORTED_CHANNEL_REQUESTS = %w[auth-agent-req@openssh.com auth-agent-req x11-req].freeze
|
|
109
|
+
|
|
110
|
+
attr_reader :host, :port, :socket, :options
|
|
111
|
+
|
|
112
|
+
# The ControlMaster socket path this transport is connected through.
|
|
113
|
+
attr_reader :control_path
|
|
114
|
+
|
|
115
|
+
def initialize(socket, host:, control_path:, options: {})
|
|
116
|
+
self.logger = options[:logger]
|
|
117
|
+
@host = host
|
|
118
|
+
@port = options[:port] || DEFAULT_PORT
|
|
119
|
+
@options = options
|
|
120
|
+
@control_path = control_path
|
|
121
|
+
@socket = socket
|
|
122
|
+
@socket.extend(PacketSocket)
|
|
123
|
+
@socket.logger = logger
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def host_as_string
|
|
127
|
+
@host_as_string ||= port == DEFAULT_PORT ? host.to_s : "[#{host}]:#{port}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def peer
|
|
131
|
+
@peer ||= { ip: nil, port: port.to_i, host: host, canonized: host_as_string }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def closed?
|
|
135
|
+
socket.closed?
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def close
|
|
139
|
+
socket.close unless closed?
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def shutdown!
|
|
143
|
+
error { "forcing connection closed" }
|
|
144
|
+
close
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def next_message
|
|
148
|
+
poll_message(:block)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def poll_message(mode = :nonblock, *)
|
|
152
|
+
socket.next_packet(mode, options[:timeout])
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def send_message(message)
|
|
156
|
+
socket.send_packet(check_supported(message.to_s))
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def enqueue_message(message)
|
|
160
|
+
socket.enqueue_packet(check_supported(message.to_s))
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# The master rekeys its own connection.
|
|
164
|
+
def rekey!; end
|
|
165
|
+
|
|
166
|
+
def rekey_as_needed; end
|
|
167
|
+
|
|
168
|
+
def hint(*); end
|
|
169
|
+
|
|
170
|
+
def server_version
|
|
171
|
+
not_supported("the server version")
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def algorithms
|
|
175
|
+
not_supported("the negotiated algorithms")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def host_keys
|
|
179
|
+
not_supported("the host keys")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def host_key_verifier
|
|
183
|
+
not_supported("host key verification")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
private
|
|
187
|
+
|
|
188
|
+
def not_supported(what)
|
|
189
|
+
raise NotSupportedOverControlMaster, "#{what} belong(s) to the ControlMaster at #{control_path}"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# The master kills the proxy connection on any global request other
|
|
193
|
+
# than tcpip-forward, and consumes the reply to that one itself, so
|
|
194
|
+
# its success or failure and an allocated port 0 can't be observed
|
|
195
|
+
# (channels.c channel_proxy_downstream, clientloop.c
|
|
196
|
+
# client_global_request_reply).
|
|
197
|
+
def check_supported(data)
|
|
198
|
+
case data.getbyte(0)
|
|
199
|
+
when GLOBAL_REQUEST
|
|
200
|
+
request = Net::SSH::Buffer.new(data.byteslice(1..-1)).read_string
|
|
201
|
+
if request == 'tcpip-forward'
|
|
202
|
+
raise NotSupportedOverControlMaster, "remote forwarding (tcpip-forward) is not supported over a ControlMaster: " \
|
|
203
|
+
"the master consumes the reply, so success and the allocated port can't be observed"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
raise NotSupportedOverControlMaster, "global request #{request} is not supported over a ControlMaster"
|
|
207
|
+
when CHANNEL_REQUEST
|
|
208
|
+
request = Net::SSH::Buffer.new(data.byteslice(5..-1)).read_string
|
|
209
|
+
if UNSUPPORTED_CHANNEL_REQUESTS.include?(request)
|
|
210
|
+
raise NotSupportedOverControlMaster, "channel request #{request} is not supported over a ControlMaster"
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
data
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|