net-ssh 2.3.0 → 2.4.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 +4 -0
- data/lib/net/ssh/authentication/agent.rb +11 -167
- data/lib/net/ssh/authentication/agent/java_pageant.rb +85 -0
- data/lib/net/ssh/authentication/agent/socket.rb +170 -0
- data/lib/net/ssh/authentication/pageant.rb +94 -13
- data/lib/net/ssh/version.rb +1 -1
- data/net-ssh.gemspec +19 -12
- data/test/authentication/test_key_manager.rb +18 -11
- data/test/test_config.rb +3 -2
- data/test/test_key_factory.rb +25 -21
- metadata +17 -5
data/CHANGELOG.rdoc
CHANGED
@@ -1,179 +1,23 @@
|
|
1
1
|
require 'net/ssh/buffer'
|
2
2
|
require 'net/ssh/errors'
|
3
3
|
require 'net/ssh/loggable'
|
4
|
-
require 'net/ssh/transport/server_version'
|
5
|
-
|
6
|
-
# Only load pageant on Windows, Ruby 1.8.x
|
7
|
-
if File::ALT_SEPARATOR && !(RUBY_PLATFORM =~ /java/) && RUBY_VERSION < "1.9"
|
8
|
-
require 'net/ssh/authentication/pageant'
|
9
|
-
end
|
10
4
|
|
11
5
|
module Net; module SSH; module Authentication
|
6
|
+
PLATFORM = File::ALT_SEPARATOR \
|
7
|
+
? RUBY_PLATFORM =~ /java/ ? :java_win32 : :win32 \
|
8
|
+
: RUBY_PLATFORM =~ /java/ ? :java : :unix
|
12
9
|
|
13
10
|
# A trivial exception class for representing agent-specific errors.
|
14
11
|
class AgentError < Net::SSH::Exception; end
|
15
12
|
|
16
13
|
# An exception for indicating that the SSH agent is not available.
|
17
14
|
class AgentNotAvailable < AgentError; end
|
18
|
-
|
19
|
-
# This class implements a simple client for the ssh-agent protocol. It
|
20
|
-
# does not implement any specific protocol, but instead copies the
|
21
|
-
# behavior of the ssh-agent functions in the OpenSSH library (3.8).
|
22
|
-
#
|
23
|
-
# This means that although it behaves like a SSH1 client, it also has
|
24
|
-
# some SSH2 functionality (like signing data).
|
25
|
-
class Agent
|
26
|
-
include Loggable
|
27
|
-
|
28
|
-
# A simple module for extending keys, to allow comments to be specified
|
29
|
-
# for them.
|
30
|
-
module Comment
|
31
|
-
attr_accessor :comment
|
32
|
-
end
|
33
|
-
|
34
|
-
SSH2_AGENT_REQUEST_VERSION = 1
|
35
|
-
SSH2_AGENT_REQUEST_IDENTITIES = 11
|
36
|
-
SSH2_AGENT_IDENTITIES_ANSWER = 12
|
37
|
-
SSH2_AGENT_SIGN_REQUEST = 13
|
38
|
-
SSH2_AGENT_SIGN_RESPONSE = 14
|
39
|
-
SSH2_AGENT_FAILURE = 30
|
40
|
-
SSH2_AGENT_VERSION_RESPONSE = 103
|
41
|
-
|
42
|
-
SSH_COM_AGENT2_FAILURE = 102
|
43
|
-
|
44
|
-
SSH_AGENT_REQUEST_RSA_IDENTITIES = 1
|
45
|
-
SSH_AGENT_RSA_IDENTITIES_ANSWER1 = 2
|
46
|
-
SSH_AGENT_RSA_IDENTITIES_ANSWER2 = 5
|
47
|
-
SSH_AGENT_FAILURE = 5
|
48
|
-
|
49
|
-
# The underlying socket being used to communicate with the SSH agent.
|
50
|
-
attr_reader :socket
|
51
|
-
|
52
|
-
# Instantiates a new agent object, connects to a running SSH agent,
|
53
|
-
# negotiates the agent protocol version, and returns the agent object.
|
54
|
-
def self.connect(logger=nil)
|
55
|
-
agent = new(logger)
|
56
|
-
agent.connect!
|
57
|
-
agent.negotiate!
|
58
|
-
agent
|
59
|
-
end
|
60
|
-
|
61
|
-
# Creates a new Agent object, using the optional logger instance to
|
62
|
-
# report status.
|
63
|
-
def initialize(logger=nil)
|
64
|
-
self.logger = logger
|
65
|
-
end
|
66
|
-
|
67
|
-
# Connect to the agent process using the socket factory and socket name
|
68
|
-
# given by the attribute writers. If the agent on the other end of the
|
69
|
-
# socket reports that it is an SSH2-compatible agent, this will fail
|
70
|
-
# (it only supports the ssh-agent distributed by OpenSSH).
|
71
|
-
def connect!
|
72
|
-
begin
|
73
|
-
debug { "connecting to ssh-agent" }
|
74
|
-
@socket = agent_socket_factory.open(ENV['SSH_AUTH_SOCK'])
|
75
|
-
rescue
|
76
|
-
error { "could not connect to ssh-agent" }
|
77
|
-
raise AgentNotAvailable, $!.message
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
# Attempts to negotiate the SSH agent protocol version. Raises an error
|
82
|
-
# if the version could not be negotiated successfully.
|
83
|
-
def negotiate!
|
84
|
-
# determine what type of agent we're communicating with
|
85
|
-
type, body = send_and_wait(SSH2_AGENT_REQUEST_VERSION, :string, Transport::ServerVersion::PROTO_VERSION)
|
86
|
-
|
87
|
-
if type == SSH2_AGENT_VERSION_RESPONSE
|
88
|
-
raise NotImplementedError, "SSH2 agents are not yet supported"
|
89
|
-
elsif type != SSH_AGENT_RSA_IDENTITIES_ANSWER1 && type != SSH_AGENT_RSA_IDENTITIES_ANSWER2
|
90
|
-
raise AgentError, "unknown response from agent: #{type}, #{body.to_s.inspect}"
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
# Return an array of all identities (public keys) known to the agent.
|
95
|
-
# Each key returned is augmented with a +comment+ property which is set
|
96
|
-
# to the comment returned by the agent for that key.
|
97
|
-
def identities
|
98
|
-
type, body = send_and_wait(SSH2_AGENT_REQUEST_IDENTITIES)
|
99
|
-
raise AgentError, "could not get identity count" if agent_failed(type)
|
100
|
-
raise AgentError, "bad authentication reply: #{type}" if type != SSH2_AGENT_IDENTITIES_ANSWER
|
101
|
-
|
102
|
-
identities = []
|
103
|
-
body.read_long.times do
|
104
|
-
key = Buffer.new(body.read_string).read_key
|
105
|
-
key.extend(Comment)
|
106
|
-
key.comment = body.read_string
|
107
|
-
identities.push key
|
108
|
-
end
|
109
|
-
|
110
|
-
return identities
|
111
|
-
end
|
112
|
-
|
113
|
-
# Closes this socket. This agent reference is no longer able to
|
114
|
-
# query the agent.
|
115
|
-
def close
|
116
|
-
@socket.close
|
117
|
-
end
|
118
|
-
|
119
|
-
# Using the agent and the given public key, sign the given data. The
|
120
|
-
# signature is returned in SSH2 format.
|
121
|
-
def sign(key, data)
|
122
|
-
type, reply = send_and_wait(SSH2_AGENT_SIGN_REQUEST, :string, Buffer.from(:key, key), :string, data, :long, 0)
|
123
|
-
|
124
|
-
if agent_failed(type)
|
125
|
-
raise AgentError, "agent could not sign data with requested identity"
|
126
|
-
elsif type != SSH2_AGENT_SIGN_RESPONSE
|
127
|
-
raise AgentError, "bad authentication response #{type}"
|
128
|
-
end
|
129
|
-
|
130
|
-
return reply.read_string
|
131
|
-
end
|
132
|
-
|
133
|
-
private
|
134
|
-
|
135
|
-
# Returns the agent socket factory to use.
|
136
|
-
def agent_socket_factory
|
137
|
-
if File::ALT_SEPARATOR
|
138
|
-
Pageant::Socket
|
139
|
-
else
|
140
|
-
UNIXSocket
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
# Send a new packet of the given type, with the associated data.
|
145
|
-
def send_packet(type, *args)
|
146
|
-
buffer = Buffer.from(*args)
|
147
|
-
data = [buffer.length + 1, type.to_i, buffer.to_s].pack("NCA*")
|
148
|
-
debug { "sending agent request #{type} len #{buffer.length}" }
|
149
|
-
@socket.send data, 0
|
150
|
-
end
|
151
|
-
|
152
|
-
# Read the next packet from the agent. This will return a two-part
|
153
|
-
# tuple consisting of the packet type, and the packet's body (which
|
154
|
-
# is returned as a Net::SSH::Buffer).
|
155
|
-
def read_packet
|
156
|
-
buffer = Net::SSH::Buffer.new(@socket.read(4))
|
157
|
-
buffer.append(@socket.read(buffer.read_long))
|
158
|
-
type = buffer.read_byte
|
159
|
-
debug { "received agent packet #{type} len #{buffer.length-4}" }
|
160
|
-
return type, buffer
|
161
|
-
end
|
162
|
-
|
163
|
-
# Send the given packet and return the subsequent reply from the agent.
|
164
|
-
# (See #send_packet and #read_packet).
|
165
|
-
def send_and_wait(type, *args)
|
166
|
-
send_packet(type, *args)
|
167
|
-
read_packet
|
168
|
-
end
|
169
|
-
|
170
|
-
# Returns +true+ if the parameter indicates a "failure" response from
|
171
|
-
# the agent, and +false+ otherwise.
|
172
|
-
def agent_failed(type)
|
173
|
-
type == SSH_AGENT_FAILURE ||
|
174
|
-
type == SSH2_AGENT_FAILURE ||
|
175
|
-
type == SSH_COM_AGENT2_FAILURE
|
176
|
-
end
|
177
|
-
end
|
178
|
-
|
179
15
|
end; end; end
|
16
|
+
|
17
|
+
case Net::SSH::Authentication::PLATFORM
|
18
|
+
when :java_win32
|
19
|
+
# Java pageant requires whole different agent.
|
20
|
+
require 'net/ssh/authentication/agent/java_pageant'
|
21
|
+
else
|
22
|
+
require 'net/ssh/authentication/agent/socket'
|
23
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'jruby_pageant'
|
2
|
+
|
3
|
+
module Net; module SSH; module Authentication
|
4
|
+
|
5
|
+
# This class implements an agent for JRuby + Pageant.
|
6
|
+
#
|
7
|
+
# Written by Artūras Šlajus <arturas.slajus@gmail.com>
|
8
|
+
class Agent
|
9
|
+
include Loggable
|
10
|
+
include JRubyPageant
|
11
|
+
|
12
|
+
# A simple module for extending keys, to allow blobs and comments to be
|
13
|
+
# specified for them.
|
14
|
+
module Key
|
15
|
+
# :blob is used by OpenSSL::PKey::RSA#to_blob
|
16
|
+
attr_accessor :java_blob
|
17
|
+
attr_accessor :comment
|
18
|
+
end
|
19
|
+
|
20
|
+
# Instantiates a new agent object, connects to a running SSH agent,
|
21
|
+
# negotiates the agent protocol version, and returns the agent object.
|
22
|
+
def self.connect(logger=nil)
|
23
|
+
agent = new(logger)
|
24
|
+
agent.connect!
|
25
|
+
agent
|
26
|
+
end
|
27
|
+
|
28
|
+
# Creates a new Agent object, using the optional logger instance to
|
29
|
+
# report status.
|
30
|
+
def initialize(logger=nil)
|
31
|
+
self.logger = logger
|
32
|
+
end
|
33
|
+
|
34
|
+
# Connect to the agent process using the socket factory and socket name
|
35
|
+
# given by the attribute writers. If the agent on the other end of the
|
36
|
+
# socket reports that it is an SSH2-compatible agent, this will fail
|
37
|
+
# (it only supports the ssh-agent distributed by OpenSSH).
|
38
|
+
def connect!
|
39
|
+
debug { "connecting to Pageant ssh-agent (via java connector)" }
|
40
|
+
@agent_proxy = JRubyPageant.create
|
41
|
+
unless @agent_proxy.is_running
|
42
|
+
raise AgentNotAvailable, "Pageant is not running!"
|
43
|
+
end
|
44
|
+
debug { "connection to Pageant ssh-agent (via java connector) succeeded" }
|
45
|
+
rescue AgentProxyException => e
|
46
|
+
error { "could not connect to Pageant ssh-agent (via java connector)" }
|
47
|
+
raise AgentNotAvailable, e.message, e.backtrace
|
48
|
+
end
|
49
|
+
|
50
|
+
# Return an array of all identities (public keys) known to the agent.
|
51
|
+
# Each key returned is augmented with a +comment+ property which is set
|
52
|
+
# to the comment returned by the agent for that key.
|
53
|
+
def identities
|
54
|
+
debug { "getting identities from Pageant" }
|
55
|
+
@agent_proxy.get_identities.map do |identity|
|
56
|
+
blob = identity.get_blob
|
57
|
+
key = Buffer.new(String.from_java_bytes(blob)).read_key
|
58
|
+
key.extend(Key)
|
59
|
+
key.java_blob = blob
|
60
|
+
key.comment = String.from_java_bytes(identity.get_comment)
|
61
|
+
key
|
62
|
+
end
|
63
|
+
rescue AgentProxyException => e
|
64
|
+
raise AgentError, "Cannot get identities: #{e.message}", e.backtrace
|
65
|
+
end
|
66
|
+
|
67
|
+
# Simulate agent close. This agent reference is no longer able to
|
68
|
+
# query the agent.
|
69
|
+
def close
|
70
|
+
@agent_proxy = nil
|
71
|
+
end
|
72
|
+
|
73
|
+
# Using the agent and the given public key, sign the given data. The
|
74
|
+
# signature is returned in SSH2 format.
|
75
|
+
def sign(key, data)
|
76
|
+
signed = @agent_proxy.sign(key.java_blob, data.to_java_bytes)
|
77
|
+
String.from_java_bytes(signed)
|
78
|
+
rescue AgentProxyException => e
|
79
|
+
raise AgentError,
|
80
|
+
"agent could not sign data with requested identity: #{e.message}",
|
81
|
+
e.backtrace
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end; end; end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
require 'net/ssh/transport/server_version'
|
2
|
+
|
3
|
+
# Only load pageant on Windows
|
4
|
+
if Net::SSH::Authentication::PLATFORM == :win32
|
5
|
+
require 'net/ssh/authentication/pageant'
|
6
|
+
end
|
7
|
+
|
8
|
+
module Net; module SSH; module Authentication
|
9
|
+
|
10
|
+
# This class implements a simple client for the ssh-agent protocol. It
|
11
|
+
# does not implement any specific protocol, but instead copies the
|
12
|
+
# behavior of the ssh-agent functions in the OpenSSH library (3.8).
|
13
|
+
#
|
14
|
+
# This means that although it behaves like a SSH1 client, it also has
|
15
|
+
# some SSH2 functionality (like signing data).
|
16
|
+
class Agent
|
17
|
+
include Loggable
|
18
|
+
|
19
|
+
# A simple module for extending keys, to allow comments to be specified
|
20
|
+
# for them.
|
21
|
+
module Comment
|
22
|
+
attr_accessor :comment
|
23
|
+
end
|
24
|
+
|
25
|
+
SSH2_AGENT_REQUEST_VERSION = 1
|
26
|
+
SSH2_AGENT_REQUEST_IDENTITIES = 11
|
27
|
+
SSH2_AGENT_IDENTITIES_ANSWER = 12
|
28
|
+
SSH2_AGENT_SIGN_REQUEST = 13
|
29
|
+
SSH2_AGENT_SIGN_RESPONSE = 14
|
30
|
+
SSH2_AGENT_FAILURE = 30
|
31
|
+
SSH2_AGENT_VERSION_RESPONSE = 103
|
32
|
+
|
33
|
+
SSH_COM_AGENT2_FAILURE = 102
|
34
|
+
|
35
|
+
SSH_AGENT_REQUEST_RSA_IDENTITIES = 1
|
36
|
+
SSH_AGENT_RSA_IDENTITIES_ANSWER1 = 2
|
37
|
+
SSH_AGENT_RSA_IDENTITIES_ANSWER2 = 5
|
38
|
+
SSH_AGENT_FAILURE = 5
|
39
|
+
|
40
|
+
# The underlying socket being used to communicate with the SSH agent.
|
41
|
+
attr_reader :socket
|
42
|
+
|
43
|
+
# Instantiates a new agent object, connects to a running SSH agent,
|
44
|
+
# negotiates the agent protocol version, and returns the agent object.
|
45
|
+
def self.connect(logger=nil)
|
46
|
+
agent = new(logger)
|
47
|
+
agent.connect!
|
48
|
+
agent.negotiate!
|
49
|
+
agent
|
50
|
+
end
|
51
|
+
|
52
|
+
# Creates a new Agent object, using the optional logger instance to
|
53
|
+
# report status.
|
54
|
+
def initialize(logger=nil)
|
55
|
+
self.logger = logger
|
56
|
+
end
|
57
|
+
|
58
|
+
# Connect to the agent process using the socket factory and socket name
|
59
|
+
# given by the attribute writers. If the agent on the other end of the
|
60
|
+
# socket reports that it is an SSH2-compatible agent, this will fail
|
61
|
+
# (it only supports the ssh-agent distributed by OpenSSH).
|
62
|
+
def connect!
|
63
|
+
begin
|
64
|
+
debug { "connecting to ssh-agent" }
|
65
|
+
@socket = agent_socket_factory.open(ENV['SSH_AUTH_SOCK'])
|
66
|
+
rescue
|
67
|
+
error { "could not connect to ssh-agent" }
|
68
|
+
raise AgentNotAvailable, $!.message
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Attempts to negotiate the SSH agent protocol version. Raises an error
|
73
|
+
# if the version could not be negotiated successfully.
|
74
|
+
def negotiate!
|
75
|
+
# determine what type of agent we're communicating with
|
76
|
+
type, body = send_and_wait(SSH2_AGENT_REQUEST_VERSION, :string, Transport::ServerVersion::PROTO_VERSION)
|
77
|
+
|
78
|
+
if type == SSH2_AGENT_VERSION_RESPONSE
|
79
|
+
raise NotImplementedError, "SSH2 agents are not yet supported"
|
80
|
+
elsif type != SSH_AGENT_RSA_IDENTITIES_ANSWER1 && type != SSH_AGENT_RSA_IDENTITIES_ANSWER2
|
81
|
+
raise AgentError, "unknown response from agent: #{type}, #{body.to_s.inspect}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Return an array of all identities (public keys) known to the agent.
|
86
|
+
# Each key returned is augmented with a +comment+ property which is set
|
87
|
+
# to the comment returned by the agent for that key.
|
88
|
+
def identities
|
89
|
+
type, body = send_and_wait(SSH2_AGENT_REQUEST_IDENTITIES)
|
90
|
+
raise AgentError, "could not get identity count" if agent_failed(type)
|
91
|
+
raise AgentError, "bad authentication reply: #{type}" if type != SSH2_AGENT_IDENTITIES_ANSWER
|
92
|
+
|
93
|
+
identities = []
|
94
|
+
body.read_long.times do
|
95
|
+
key = Buffer.new(body.read_string).read_key
|
96
|
+
key.extend(Comment)
|
97
|
+
key.comment = body.read_string
|
98
|
+
identities.push key
|
99
|
+
end
|
100
|
+
|
101
|
+
return identities
|
102
|
+
end
|
103
|
+
|
104
|
+
# Closes this socket. This agent reference is no longer able to
|
105
|
+
# query the agent.
|
106
|
+
def close
|
107
|
+
@socket.close
|
108
|
+
end
|
109
|
+
|
110
|
+
# Using the agent and the given public key, sign the given data. The
|
111
|
+
# signature is returned in SSH2 format.
|
112
|
+
def sign(key, data)
|
113
|
+
type, reply = send_and_wait(SSH2_AGENT_SIGN_REQUEST, :string, Buffer.from(:key, key), :string, data, :long, 0)
|
114
|
+
|
115
|
+
if agent_failed(type)
|
116
|
+
raise AgentError, "agent could not sign data with requested identity"
|
117
|
+
elsif type != SSH2_AGENT_SIGN_RESPONSE
|
118
|
+
raise AgentError, "bad authentication response #{type}"
|
119
|
+
end
|
120
|
+
|
121
|
+
return reply.read_string
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
# Returns the agent socket factory to use.
|
127
|
+
def agent_socket_factory
|
128
|
+
if Net::SSH::Authentication::PLATFORM == :win32
|
129
|
+
Pageant::socket_factory
|
130
|
+
else
|
131
|
+
UNIXSocket
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Send a new packet of the given type, with the associated data.
|
136
|
+
def send_packet(type, *args)
|
137
|
+
buffer = Buffer.from(*args)
|
138
|
+
data = [buffer.length + 1, type.to_i, buffer.to_s].pack("NCA*")
|
139
|
+
debug { "sending agent request #{type} len #{buffer.length}" }
|
140
|
+
@socket.send data, 0
|
141
|
+
end
|
142
|
+
|
143
|
+
# Read the next packet from the agent. This will return a two-part
|
144
|
+
# tuple consisting of the packet type, and the packet's body (which
|
145
|
+
# is returned as a Net::SSH::Buffer).
|
146
|
+
def read_packet
|
147
|
+
buffer = Net::SSH::Buffer.new(@socket.read(4))
|
148
|
+
buffer.append(@socket.read(buffer.read_long))
|
149
|
+
type = buffer.read_byte
|
150
|
+
debug { "received agent packet #{type} len #{buffer.length-4}" }
|
151
|
+
return type, buffer
|
152
|
+
end
|
153
|
+
|
154
|
+
# Send the given packet and return the subsequent reply from the agent.
|
155
|
+
# (See #send_packet and #read_packet).
|
156
|
+
def send_and_wait(type, *args)
|
157
|
+
send_packet(type, *args)
|
158
|
+
read_packet
|
159
|
+
end
|
160
|
+
|
161
|
+
# Returns +true+ if the parameter indicates a "failure" response from
|
162
|
+
# the agent, and +false+ otherwise.
|
163
|
+
def agent_failed(type)
|
164
|
+
type == SSH_AGENT_FAILURE ||
|
165
|
+
type == SSH2_AGENT_FAILURE ||
|
166
|
+
type == SSH_COM_AGENT2_FAILURE
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
end; end; end
|
@@ -1,5 +1,13 @@
|
|
1
1
|
require 'dl/import'
|
2
|
-
|
2
|
+
|
3
|
+
if RUBY_VERSION < "1.9"
|
4
|
+
require 'dl/struct'
|
5
|
+
end
|
6
|
+
|
7
|
+
if RUBY_VERSION =~ /^1.9/
|
8
|
+
require 'dl/types'
|
9
|
+
require 'dl'
|
10
|
+
end
|
3
11
|
|
4
12
|
require 'net/ssh/errors'
|
5
13
|
|
@@ -17,15 +25,23 @@ module Net; module SSH; module Authentication
|
|
17
25
|
# From Putty pageant.c
|
18
26
|
AGENT_MAX_MSGLEN = 8192
|
19
27
|
AGENT_COPYDATA_ID = 0x804e50ba
|
20
|
-
|
28
|
+
|
21
29
|
# The definition of the Windows methods and data structures used in
|
22
30
|
# communicating with the pageant process.
|
23
31
|
module Win
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
32
|
+
if RUBY_VERSION < "1.9"
|
33
|
+
extend DL::Importable
|
34
|
+
|
35
|
+
dlload 'user32'
|
36
|
+
dlload 'kernel32'
|
37
|
+
end
|
38
|
+
|
39
|
+
if RUBY_VERSION =~ /^1.9/
|
40
|
+
extend DL::Importer
|
41
|
+
dlload 'user32','kernel32'
|
42
|
+
include DL::Win32Types
|
43
|
+
end
|
44
|
+
|
29
45
|
typealias("LPCTSTR", "char *") # From winnt.h
|
30
46
|
typealias("LPVOID", "void *") # From winnt.h
|
31
47
|
typealias("LPCVOID", "const void *") # From windef.h
|
@@ -52,7 +68,7 @@ module Net; module SSH; module Authentication
|
|
52
68
|
# args: hFile, (ignored), flProtect, dwMaximumSizeHigh,
|
53
69
|
# dwMaximumSizeLow, lpName
|
54
70
|
extern 'HANDLE CreateFileMapping(HANDLE, void *, DWORD, DWORD, ' +
|
55
|
-
|
71
|
+
'DWORD, LPCTSTR)'
|
56
72
|
|
57
73
|
# args: hFileMappingObject, dwDesiredAccess, dwFileOffsetHigh,
|
58
74
|
# dwfileOffsetLow, dwNumberOfBytesToMap
|
@@ -66,7 +82,11 @@ module Net; module SSH; module Authentication
|
|
66
82
|
|
67
83
|
# args: hWnd, Msg, wParam, lParam, fuFlags, uTimeout, lpdwResult
|
68
84
|
extern 'LRESULT SendMessageTimeout(HWND, UINT, WPARAM, LPARAM, ' +
|
69
|
-
|
85
|
+
'UINT, UINT, PDWORD_PTR)'
|
86
|
+
if RUBY_VERSION < "1.9"
|
87
|
+
alias_method :FindWindow,:findWindow
|
88
|
+
module_function :FindWindow
|
89
|
+
end
|
70
90
|
end
|
71
91
|
|
72
92
|
# This is the pseudo-socket implementation that mimics the interface of
|
@@ -87,7 +107,7 @@ module Net; module SSH; module Authentication
|
|
87
107
|
# Create a new instance that communicates with the running pageant
|
88
108
|
# instance. If no such instance is running, this will cause an error.
|
89
109
|
def initialize
|
90
|
-
@win = Win.
|
110
|
+
@win = Win.FindWindow("Pageant", "Pageant")
|
91
111
|
|
92
112
|
if @win == 0
|
93
113
|
raise Net::SSH::Exception,
|
@@ -97,7 +117,7 @@ module Net; module SSH; module Authentication
|
|
97
117
|
@res = nil
|
98
118
|
@pos = 0
|
99
119
|
end
|
100
|
-
|
120
|
+
|
101
121
|
# Forwards the data to #send_query, ignoring any arguments after
|
102
122
|
# the first. Returns 0.
|
103
123
|
def send(data, *args)
|
@@ -132,11 +152,11 @@ module Net; module SSH; module Authentication
|
|
132
152
|
end
|
133
153
|
|
134
154
|
ptr[0] = query
|
135
|
-
|
155
|
+
|
136
156
|
cds = [AGENT_COPYDATA_ID, mapname.size + 1, mapname].
|
137
157
|
pack("LLp").to_ptr
|
138
158
|
succ = Win.sendMessageTimeout(@win, Win::WM_COPYDATA, Win::NULL,
|
139
|
-
|
159
|
+
cds, Win::SMTO_NORMAL, 5000, id)
|
140
160
|
|
141
161
|
if succ > 0
|
142
162
|
retlen = 4 + ptr.to_s(4).unpack("N")[0]
|
@@ -178,6 +198,67 @@ module Net; module SSH; module Authentication
|
|
178
198
|
|
179
199
|
end
|
180
200
|
|
201
|
+
# Socket changes for Ruby 1.9
|
202
|
+
# Functionality is the same as Ruby 1.8 but it includes the new calls to
|
203
|
+
# the DL module as well as other pointer transformations
|
204
|
+
class Socket19 < Socket
|
205
|
+
# Packages the given query string and sends it to the pageant
|
206
|
+
# process via the Windows messaging subsystem. The result is
|
207
|
+
# cached, to be returned piece-wise when #read is called.
|
208
|
+
def send_query(query)
|
209
|
+
res = nil
|
210
|
+
filemap = 0
|
211
|
+
ptr = nil
|
212
|
+
id = DL.malloc(DL::SIZEOF_LONG)
|
213
|
+
|
214
|
+
mapname = "PageantRequest%08x\000" % Win.GetCurrentThreadId()
|
215
|
+
|
216
|
+
filemap = Win.CreateFileMapping(Win::INVALID_HANDLE_VALUE,
|
217
|
+
Win::NULL,
|
218
|
+
Win::PAGE_READWRITE, 0,
|
219
|
+
AGENT_MAX_MSGLEN, mapname)
|
220
|
+
|
221
|
+
if filemap == 0 || filemap == Win::INVALID_HANDLE_VALUE
|
222
|
+
raise Net::SSH::Exception,
|
223
|
+
"Creation of file mapping failed"
|
224
|
+
end
|
225
|
+
|
226
|
+
ptr = Win.MapViewOfFile(filemap, Win::FILE_MAP_WRITE, 0, 0,
|
227
|
+
0)
|
228
|
+
|
229
|
+
if ptr.nil? || ptr.null?
|
230
|
+
raise Net::SSH::Exception, "Mapping of file failed"
|
231
|
+
end
|
232
|
+
|
233
|
+
DL::CPtr.new(ptr)[0,query.size]=query
|
234
|
+
|
235
|
+
cds = DL::CPtr.to_ptr [AGENT_COPYDATA_ID, mapname.size + 1, mapname].
|
236
|
+
pack("LLp")
|
237
|
+
succ = Win.SendMessageTimeout(@win, Win::WM_COPYDATA, Win::NULL,
|
238
|
+
cds, Win::SMTO_NORMAL, 5000, id)
|
239
|
+
|
240
|
+
if succ > 0
|
241
|
+
retlen = 4 + ptr.to_s(4).unpack("N")[0]
|
242
|
+
res = ptr.to_s(retlen)
|
243
|
+
end
|
244
|
+
|
245
|
+
return res
|
246
|
+
ensure
|
247
|
+
Win.UnmapViewOfFile(ptr) unless ptr.nil? || ptr.null?
|
248
|
+
Win.CloseHandle(filemap) if filemap != 0
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# Selects which socket to use depending on the ruby version
|
253
|
+
# This is needed due changes in the DL module.
|
254
|
+
def self.socket_factory
|
255
|
+
if RUBY_VERSION < "1.9"
|
256
|
+
Socket
|
257
|
+
else
|
258
|
+
Socket19
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
181
262
|
end
|
182
263
|
|
183
264
|
end; end; end
|
data/lib/net/ssh/version.rb
CHANGED
data/net-ssh.gemspec
CHANGED
@@ -1,21 +1,26 @@
|
|
1
1
|
@spec = Gem::Specification.new do |s|
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
2
|
+
s.name = "net-ssh"
|
3
|
+
s.rubyforge_project = 'net-ssh'
|
4
|
+
s.version = "2.4.0"
|
5
|
+
s.summary = "Net::SSH: a pure-Ruby implementation of the SSH2 client protocol."
|
6
|
+
s.description = s.summary + " It allows you to write programs that invoke and interact with processes on remote servers, via SSH2."
|
7
|
+
s.authors = ["Jamis Buck", "Delano Mandelbaum"]
|
8
|
+
s.email = ["net-ssh@solutious.com"]
|
9
|
+
s.homepage = "http://github.com/net-ssh/net-ssh"
|
10
|
+
|
11
11
|
s.extra_rdoc_files = %w[README.rdoc THANKS.rdoc CHANGELOG.rdoc]
|
12
12
|
s.has_rdoc = true
|
13
13
|
s.rdoc_options = ["--line-numbers", "--title", s.summary, "--main", "README.rdoc"]
|
14
14
|
s.require_paths = %w[lib]
|
15
15
|
s.rubygems_version = '1.3.2'
|
16
|
-
|
16
|
+
|
17
|
+
# This has two flavours with java one actually doing something and other
|
18
|
+
# one just raising error. This is a workaround for no ability to specify
|
19
|
+
# platform specific dependencies in gemspecs.
|
20
|
+
s.add_dependency 'jruby-pageant', ">=1.0.2"
|
21
|
+
|
17
22
|
s.executables = %w[]
|
18
|
-
|
23
|
+
|
19
24
|
# = MANIFEST =
|
20
25
|
s.files = %w(
|
21
26
|
CHANGELOG.rdoc
|
@@ -26,6 +31,8 @@
|
|
26
31
|
THANKS.rdoc
|
27
32
|
lib/net/ssh.rb
|
28
33
|
lib/net/ssh/authentication/agent.rb
|
34
|
+
lib/net/ssh/authentication/agent/java_pageant.rb
|
35
|
+
lib/net/ssh/authentication/agent/socket.rb
|
29
36
|
lib/net/ssh/authentication/constants.rb
|
30
37
|
lib/net/ssh/authentication/key_manager.rb
|
31
38
|
lib/net/ssh/authentication/methods/abstract.rb
|
@@ -141,5 +148,5 @@
|
|
141
148
|
test/transport/test_state.rb
|
142
149
|
)
|
143
150
|
|
144
|
-
|
151
|
+
|
145
152
|
end
|
@@ -18,7 +18,9 @@ module Authentication
|
|
18
18
|
manager.add "/second"
|
19
19
|
manager.add "/third"
|
20
20
|
manager.add "/second"
|
21
|
-
|
21
|
+
assert_true manager.key_files.length == 3
|
22
|
+
final_files = manager.key_files.map {|item| item.split('/').last}
|
23
|
+
assert_equal %w(first second third), final_files
|
22
24
|
end
|
23
25
|
|
24
26
|
def test_use_agent_should_be_set_to_false_if_agent_could_not_be_found
|
@@ -30,9 +32,10 @@ module Authentication
|
|
30
32
|
|
31
33
|
def test_each_identity_should_load_from_key_files
|
32
34
|
manager.stubs(:agent).returns(nil)
|
33
|
-
|
34
|
-
|
35
|
-
stub_file_private_key
|
35
|
+
first = File.expand_path("/first")
|
36
|
+
second = File.expand_path("/second")
|
37
|
+
stub_file_private_key first, rsa
|
38
|
+
stub_file_private_key second, dsa
|
36
39
|
|
37
40
|
identities = []
|
38
41
|
manager.each_identity { |identity| identities << identity }
|
@@ -40,9 +43,9 @@ module Authentication
|
|
40
43
|
assert_equal 2, identities.length
|
41
44
|
assert_equal rsa.to_blob, identities.first.to_blob
|
42
45
|
assert_equal dsa.to_blob, identities.last.to_blob
|
43
|
-
|
44
|
-
assert_equal({:from => :file, :file =>
|
45
|
-
assert_equal({:from => :file, :file =>
|
46
|
+
|
47
|
+
assert_equal({:from => :file, :file => first, :key => rsa}, manager.known_identities[rsa])
|
48
|
+
assert_equal({:from => :file, :file => second, :key => dsa}, manager.known_identities[dsa])
|
46
49
|
end
|
47
50
|
|
48
51
|
def test_identities_should_load_from_agent
|
@@ -62,7 +65,8 @@ module Authentication
|
|
62
65
|
def test_only_identities_with_key_files_should_load_from_agent_of_keys_only_set
|
63
66
|
manager(:keys_only => true).stubs(:agent).returns(agent)
|
64
67
|
|
65
|
-
|
68
|
+
first = File.expand_path("/first")
|
69
|
+
stub_file_private_key first, rsa
|
66
70
|
|
67
71
|
identities = []
|
68
72
|
manager.each_identity { |identity| identities << identity }
|
@@ -76,8 +80,10 @@ module Authentication
|
|
76
80
|
def test_identities_without_public_key_files_should_not_be_touched_if_identity_loaded_from_agent
|
77
81
|
manager.stubs(:agent).returns(agent)
|
78
82
|
|
79
|
-
|
80
|
-
|
83
|
+
first = File.expand_path("/first")
|
84
|
+
stub_file_public_key first, rsa
|
85
|
+
second = File.expand_path("/second")
|
86
|
+
stub_file_private_key second, dsa, :passphrase => :should_not_be_asked
|
81
87
|
|
82
88
|
identities = []
|
83
89
|
manager.each_identity do |identity|
|
@@ -98,7 +104,8 @@ module Authentication
|
|
98
104
|
|
99
105
|
def test_sign_with_file_originated_key_should_load_private_key_and_sign_with_it
|
100
106
|
manager.stubs(:agent).returns(nil)
|
101
|
-
|
107
|
+
first = File.expand_path("/first")
|
108
|
+
stub_file_private_key first, rsa(512)
|
102
109
|
rsa.expects(:ssh_do_sign).with("hello, world").returns("abcxyz123")
|
103
110
|
manager.each_identity { |identity| } # preload the known_identities
|
104
111
|
assert_equal "\0\0\0\assh-rsa\0\0\0\011abcxyz123", manager.sign(rsa, "hello, world")
|
data/test/test_config.rb
CHANGED
@@ -3,8 +3,9 @@ require 'net/ssh/config'
|
|
3
3
|
|
4
4
|
class TestConfig < Test::Unit::TestCase
|
5
5
|
def test_load_for_non_existant_file_should_return_empty_hash
|
6
|
-
File.
|
7
|
-
|
6
|
+
bogus_file = File.expand_path("/bogus/file")
|
7
|
+
File.expects(:readable?).with(bogus_file).returns(false)
|
8
|
+
assert_equal({}, Net::SSH::Config.load(bogus_file, "host.name"))
|
8
9
|
end
|
9
10
|
|
10
11
|
def test_load_should_expand_path
|
data/test/test_key_factory.rb
CHANGED
@@ -2,53 +2,57 @@ require 'common'
|
|
2
2
|
require 'net/ssh/key_factory'
|
3
3
|
|
4
4
|
class TestKeyFactory < Test::Unit::TestCase
|
5
|
+
def setup
|
6
|
+
@key_file = File.expand_path("/key-file")
|
7
|
+
end
|
8
|
+
|
5
9
|
def test_load_unencrypted_private_RSA_key_should_return_key
|
6
|
-
File.expects(:read).with(
|
7
|
-
assert_equal rsa_key.to_der, Net::SSH::KeyFactory.load_private_key(
|
10
|
+
File.expects(:read).with(@key_file).returns(rsa_key.export)
|
11
|
+
assert_equal rsa_key.to_der, Net::SSH::KeyFactory.load_private_key(@key_file).to_der
|
8
12
|
end
|
9
13
|
|
10
14
|
def test_load_unencrypted_private_DSA_key_should_return_key
|
11
|
-
File.expects(:read).with(
|
12
|
-
assert_equal dsa_key.to_der, Net::SSH::KeyFactory.load_private_key(
|
15
|
+
File.expects(:read).with(@key_file).returns(dsa_key.export)
|
16
|
+
assert_equal dsa_key.to_der, Net::SSH::KeyFactory.load_private_key(@key_file).to_der
|
13
17
|
end
|
14
18
|
|
15
19
|
def test_load_encrypted_private_RSA_key_should_prompt_for_password_and_return_key
|
16
|
-
File.expects(:read).with(
|
17
|
-
Net::SSH::KeyFactory.expects(:prompt).with("Enter passphrase for
|
18
|
-
assert_equal rsa_key.to_der, Net::SSH::KeyFactory.load_private_key(
|
20
|
+
File.expects(:read).with(@key_file).returns(encrypted(rsa_key, "password"))
|
21
|
+
Net::SSH::KeyFactory.expects(:prompt).with("Enter passphrase for #{@key_file}:", false).returns("password")
|
22
|
+
assert_equal rsa_key.to_der, Net::SSH::KeyFactory.load_private_key(@key_file).to_der
|
19
23
|
end
|
20
24
|
|
21
25
|
def test_load_encrypted_private_RSA_key_with_password_should_not_prompt_and_return_key
|
22
|
-
File.expects(:read).with(
|
23
|
-
assert_equal rsa_key.to_der, Net::SSH::KeyFactory.load_private_key(
|
26
|
+
File.expects(:read).with(@key_file).returns(encrypted(rsa_key, "password"))
|
27
|
+
assert_equal rsa_key.to_der, Net::SSH::KeyFactory.load_private_key(@key_file, "password").to_der
|
24
28
|
end
|
25
29
|
|
26
30
|
def test_load_encrypted_private_DSA_key_should_prompt_for_password_and_return_key
|
27
|
-
File.expects(:read).with(
|
28
|
-
Net::SSH::KeyFactory.expects(:prompt).with("Enter passphrase for
|
29
|
-
assert_equal dsa_key.to_der, Net::SSH::KeyFactory.load_private_key(
|
31
|
+
File.expects(:read).with(@key_file).returns(encrypted(dsa_key, "password"))
|
32
|
+
Net::SSH::KeyFactory.expects(:prompt).with("Enter passphrase for #{@key_file}:", false).returns("password")
|
33
|
+
assert_equal dsa_key.to_der, Net::SSH::KeyFactory.load_private_key(@key_file).to_der
|
30
34
|
end
|
31
35
|
|
32
36
|
def test_load_encrypted_private_DSA_key_with_password_should_not_prompt_and_return_key
|
33
|
-
File.expects(:read).with(
|
34
|
-
assert_equal dsa_key.to_der, Net::SSH::KeyFactory.load_private_key(
|
37
|
+
File.expects(:read).with(@key_file).returns(encrypted(dsa_key, "password"))
|
38
|
+
assert_equal dsa_key.to_der, Net::SSH::KeyFactory.load_private_key(@key_file, "password").to_der
|
35
39
|
end
|
36
40
|
|
37
41
|
def test_load_encrypted_private_key_should_give_three_tries_for_the_password_and_then_raise_exception
|
38
|
-
File.expects(:read).with(
|
39
|
-
Net::SSH::KeyFactory.expects(:prompt).times(3).with("Enter passphrase for
|
40
|
-
assert_raises(OpenSSL::PKey::RSAError) { Net::SSH::KeyFactory.load_private_key(
|
42
|
+
File.expects(:read).with(@key_file).returns(encrypted(rsa_key, "password"))
|
43
|
+
Net::SSH::KeyFactory.expects(:prompt).times(3).with("Enter passphrase for #{@key_file}:", false).returns("passwod","passphrase","passwd")
|
44
|
+
assert_raises(OpenSSL::PKey::RSAError) { Net::SSH::KeyFactory.load_private_key(@key_file) }
|
41
45
|
end
|
42
46
|
|
43
47
|
def test_load_encrypted_private_key_should_raise_exception_without_asking_passphrase
|
44
|
-
File.expects(:read).with(
|
48
|
+
File.expects(:read).with(@key_file).returns(encrypted(rsa_key, "password"))
|
45
49
|
Net::SSH::KeyFactory.expects(:prompt).never
|
46
|
-
assert_raises(OpenSSL::PKey::RSAError) { Net::SSH::KeyFactory.load_private_key(
|
50
|
+
assert_raises(OpenSSL::PKey::RSAError) { Net::SSH::KeyFactory.load_private_key(@key_file, nil, false) }
|
47
51
|
end
|
48
52
|
|
49
53
|
def test_load_public_rsa_key_should_return_key
|
50
|
-
File.expects(:read).with(
|
51
|
-
assert_equal rsa_key.to_blob, Net::SSH::KeyFactory.load_public_key(
|
54
|
+
File.expects(:read).with(@key_file).returns(public(rsa_key))
|
55
|
+
assert_equal rsa_key.to_blob, Net::SSH::KeyFactory.load_public_key(@key_file).to_blob
|
52
56
|
end
|
53
57
|
|
54
58
|
private
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: net-ssh
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 2.
|
5
|
+
version: 2.4.0
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Jamis Buck
|
@@ -11,10 +11,20 @@ autorequire:
|
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
13
|
|
14
|
-
date: 2012-
|
15
|
-
dependencies:
|
16
|
-
|
17
|
-
|
14
|
+
date: 2012-05-17 00:00:00 Z
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: jruby-pageant
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ">="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 1.0.2
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
description: "Net::SSH: a pure-Ruby implementation of the SSH2 client protocol. It allows you to write programs that invoke and interact with processes on remote servers, via SSH2."
|
18
28
|
email:
|
19
29
|
- net-ssh@solutious.com
|
20
30
|
executables: []
|
@@ -34,6 +44,8 @@ files:
|
|
34
44
|
- THANKS.rdoc
|
35
45
|
- lib/net/ssh.rb
|
36
46
|
- lib/net/ssh/authentication/agent.rb
|
47
|
+
- lib/net/ssh/authentication/agent/java_pageant.rb
|
48
|
+
- lib/net/ssh/authentication/agent/socket.rb
|
37
49
|
- lib/net/ssh/authentication/constants.rb
|
38
50
|
- lib/net/ssh/authentication/key_manager.rb
|
39
51
|
- lib/net/ssh/authentication/methods/abstract.rb
|