diff options
author | Delano Mandelbaum <delano.mandelbaum@gmail.com> | 2012-05-17 04:25:23 -0700 |
---|---|---|
committer | Delano Mandelbaum <delano.mandelbaum@gmail.com> | 2012-05-17 04:25:23 -0700 |
commit | f10c563115ecf3a5dd00b6907d11a7220facd2e7 (patch) | |
tree | 052be4866eb8e3acf8a3e4c4ce661de607b8ac15 | |
parent | a54775848a33aa42f97618a6c05e3121962c4e15 (diff) | |
parent | 75c2743a89678b18ba7b443a3495c555537abead (diff) | |
download | net-ssh-f10c563115ecf3a5dd00b6907d11a7220facd2e7.tar.gz |
Merge pull request #35 from arturaz/master
Support for JRuby + Pageant + Windows
-rw-r--r-- | lib/net/ssh/authentication/agent.rb | 178 | ||||
-rw-r--r-- | lib/net/ssh/authentication/agent/java_pageant.rb | 85 | ||||
-rw-r--r-- | lib/net/ssh/authentication/agent/socket.rb | 170 | ||||
-rw-r--r-- | net-ssh.gemspec | 31 |
4 files changed, 285 insertions, 179 deletions
diff --git a/lib/net/ssh/authentication/agent.rb b/lib/net/ssh/authentication/agent.rb index 2208872..dbc7e3d 100644 --- a/lib/net/ssh/authentication/agent.rb +++ b/lib/net/ssh/authentication/agent.rb @@ -1,179 +1,23 @@ require 'net/ssh/buffer' require 'net/ssh/errors' require 'net/ssh/loggable' -require 'net/ssh/transport/server_version' - -# Only load pageant on Windows -if File::ALT_SEPARATOR && !(RUBY_PLATFORM =~ /java/) - require 'net/ssh/authentication/pageant' -end module Net; module SSH; module Authentication + PLATFORM = File::ALT_SEPARATOR \ + ? RUBY_PLATFORM =~ /java/ ? :java_win32 : :win32 \ + : RUBY_PLATFORM =~ /java/ ? :java : :unix # A trivial exception class for representing agent-specific errors. class AgentError < Net::SSH::Exception; end # An exception for indicating that the SSH agent is not available. class AgentNotAvailable < AgentError; end - - # This class implements a simple client for the ssh-agent protocol. It - # does not implement any specific protocol, but instead copies the - # behavior of the ssh-agent functions in the OpenSSH library (3.8). - # - # This means that although it behaves like a SSH1 client, it also has - # some SSH2 functionality (like signing data). - class Agent - include Loggable - - # A simple module for extending keys, to allow comments to be specified - # for them. - module Comment - attr_accessor :comment - end - - SSH2_AGENT_REQUEST_VERSION = 1 - SSH2_AGENT_REQUEST_IDENTITIES = 11 - SSH2_AGENT_IDENTITIES_ANSWER = 12 - SSH2_AGENT_SIGN_REQUEST = 13 - SSH2_AGENT_SIGN_RESPONSE = 14 - SSH2_AGENT_FAILURE = 30 - SSH2_AGENT_VERSION_RESPONSE = 103 - - SSH_COM_AGENT2_FAILURE = 102 - - SSH_AGENT_REQUEST_RSA_IDENTITIES = 1 - SSH_AGENT_RSA_IDENTITIES_ANSWER1 = 2 - SSH_AGENT_RSA_IDENTITIES_ANSWER2 = 5 - SSH_AGENT_FAILURE = 5 - - # The underlying socket being used to communicate with the SSH agent. - attr_reader :socket - - # Instantiates a new agent object, connects to a running SSH agent, - # negotiates the agent protocol version, and returns the agent object. - def self.connect(logger=nil) - agent = new(logger) - agent.connect! - agent.negotiate! - agent - end - - # Creates a new Agent object, using the optional logger instance to - # report status. - def initialize(logger=nil) - self.logger = logger - end - - # Connect to the agent process using the socket factory and socket name - # given by the attribute writers. If the agent on the other end of the - # socket reports that it is an SSH2-compatible agent, this will fail - # (it only supports the ssh-agent distributed by OpenSSH). - def connect! - begin - debug { "connecting to ssh-agent" } - @socket = agent_socket_factory.open(ENV['SSH_AUTH_SOCK']) - rescue - error { "could not connect to ssh-agent" } - raise AgentNotAvailable, $!.message - end - end - - # Attempts to negotiate the SSH agent protocol version. Raises an error - # if the version could not be negotiated successfully. - def negotiate! - # determine what type of agent we're communicating with - type, body = send_and_wait(SSH2_AGENT_REQUEST_VERSION, :string, Transport::ServerVersion::PROTO_VERSION) - - if type == SSH2_AGENT_VERSION_RESPONSE - raise NotImplementedError, "SSH2 agents are not yet supported" - elsif type != SSH_AGENT_RSA_IDENTITIES_ANSWER1 && type != SSH_AGENT_RSA_IDENTITIES_ANSWER2 - raise AgentError, "unknown response from agent: #{type}, #{body.to_s.inspect}" - end - end - - # Return an array of all identities (public keys) known to the agent. - # Each key returned is augmented with a +comment+ property which is set - # to the comment returned by the agent for that key. - def identities - type, body = send_and_wait(SSH2_AGENT_REQUEST_IDENTITIES) - raise AgentError, "could not get identity count" if agent_failed(type) - raise AgentError, "bad authentication reply: #{type}" if type != SSH2_AGENT_IDENTITIES_ANSWER - - identities = [] - body.read_long.times do - key = Buffer.new(body.read_string).read_key - key.extend(Comment) - key.comment = body.read_string - identities.push key - end - - return identities - end - - # Closes this socket. This agent reference is no longer able to - # query the agent. - def close - @socket.close - end - - # Using the agent and the given public key, sign the given data. The - # signature is returned in SSH2 format. - def sign(key, data) - type, reply = send_and_wait(SSH2_AGENT_SIGN_REQUEST, :string, Buffer.from(:key, key), :string, data, :long, 0) - - if agent_failed(type) - raise AgentError, "agent could not sign data with requested identity" - elsif type != SSH2_AGENT_SIGN_RESPONSE - raise AgentError, "bad authentication response #{type}" - end - - return reply.read_string - end - - private - - # Returns the agent socket factory to use. - def agent_socket_factory - if File::ALT_SEPARATOR - Pageant::socket_factory - else - UNIXSocket - end - end - - # Send a new packet of the given type, with the associated data. - def send_packet(type, *args) - buffer = Buffer.from(*args) - data = [buffer.length + 1, type.to_i, buffer.to_s].pack("NCA*") - debug { "sending agent request #{type} len #{buffer.length}" } - @socket.send data, 0 - end - - # Read the next packet from the agent. This will return a two-part - # tuple consisting of the packet type, and the packet's body (which - # is returned as a Net::SSH::Buffer). - def read_packet - buffer = Net::SSH::Buffer.new(@socket.read(4)) - buffer.append(@socket.read(buffer.read_long)) - type = buffer.read_byte - debug { "received agent packet #{type} len #{buffer.length-4}" } - return type, buffer - end - - # Send the given packet and return the subsequent reply from the agent. - # (See #send_packet and #read_packet). - def send_and_wait(type, *args) - send_packet(type, *args) - read_packet - end - - # Returns +true+ if the parameter indicates a "failure" response from - # the agent, and +false+ otherwise. - def agent_failed(type) - type == SSH_AGENT_FAILURE || - type == SSH2_AGENT_FAILURE || - type == SSH_COM_AGENT2_FAILURE - end - end - end; end; end + +case Net::SSH::Authentication::PLATFORM +when :java_win32 + # Java pageant requires whole different agent. + require 'net/ssh/authentication/agent/java_pageant' +else + require 'net/ssh/authentication/agent/socket' +end diff --git a/lib/net/ssh/authentication/agent/java_pageant.rb b/lib/net/ssh/authentication/agent/java_pageant.rb new file mode 100644 index 0000000..ec3f635 --- /dev/null +++ b/lib/net/ssh/authentication/agent/java_pageant.rb @@ -0,0 +1,85 @@ +require 'jruby_pageant' + +module Net; module SSH; module Authentication + + # This class implements an agent for JRuby + Pageant. + # + # Written by Artūras Šlajus <arturas.slajus@gmail.com> + class Agent + include Loggable + include JRubyPageant + + # A simple module for extending keys, to allow blobs and comments to be + # specified for them. + module Key + # :blob is used by OpenSSL::PKey::RSA#to_blob + attr_accessor :java_blob + attr_accessor :comment + end + + # Instantiates a new agent object, connects to a running SSH agent, + # negotiates the agent protocol version, and returns the agent object. + def self.connect(logger=nil) + agent = new(logger) + agent.connect! + agent + end + + # Creates a new Agent object, using the optional logger instance to + # report status. + def initialize(logger=nil) + self.logger = logger + end + + # Connect to the agent process using the socket factory and socket name + # given by the attribute writers. If the agent on the other end of the + # socket reports that it is an SSH2-compatible agent, this will fail + # (it only supports the ssh-agent distributed by OpenSSH). + def connect! + debug { "connecting to Pageant ssh-agent (via java connector)" } + @agent_proxy = JRubyPageant.create + unless @agent_proxy.is_running + raise AgentNotAvailable, "Pageant is not running!" + end + debug { "connection to Pageant ssh-agent (via java connector) succeeded" } + rescue AgentProxyException => e + error { "could not connect to Pageant ssh-agent (via java connector)" } + raise AgentNotAvailable, e.message, e.backtrace + end + + # Return an array of all identities (public keys) known to the agent. + # Each key returned is augmented with a +comment+ property which is set + # to the comment returned by the agent for that key. + def identities + debug { "getting identities from Pageant" } + @agent_proxy.get_identities.map do |identity| + blob = identity.get_blob + key = Buffer.new(String.from_java_bytes(blob)).read_key + key.extend(Key) + key.java_blob = blob + key.comment = String.from_java_bytes(identity.get_comment) + key + end + rescue AgentProxyException => e + raise AgentError, "Cannot get identities: #{e.message}", e.backtrace + end + + # Simulate agent close. This agent reference is no longer able to + # query the agent. + def close + @agent_proxy = nil + end + + # Using the agent and the given public key, sign the given data. The + # signature is returned in SSH2 format. + def sign(key, data) + signed = @agent_proxy.sign(key.java_blob, data.to_java_bytes) + String.from_java_bytes(signed) + rescue AgentProxyException => e + raise AgentError, + "agent could not sign data with requested identity: #{e.message}", + e.backtrace + end + end + +end; end; end diff --git a/lib/net/ssh/authentication/agent/socket.rb b/lib/net/ssh/authentication/agent/socket.rb new file mode 100644 index 0000000..78b6382 --- /dev/null +++ b/lib/net/ssh/authentication/agent/socket.rb @@ -0,0 +1,170 @@ +require 'net/ssh/transport/server_version' + +# Only load pageant on Windows +if Net::SSH::Authentication::PLATFORM == :win32 + require 'net/ssh/authentication/pageant' +end + +module Net; module SSH; module Authentication + + # This class implements a simple client for the ssh-agent protocol. It + # does not implement any specific protocol, but instead copies the + # behavior of the ssh-agent functions in the OpenSSH library (3.8). + # + # This means that although it behaves like a SSH1 client, it also has + # some SSH2 functionality (like signing data). + class Agent + include Loggable + + # A simple module for extending keys, to allow comments to be specified + # for them. + module Comment + attr_accessor :comment + end + + SSH2_AGENT_REQUEST_VERSION = 1 + SSH2_AGENT_REQUEST_IDENTITIES = 11 + SSH2_AGENT_IDENTITIES_ANSWER = 12 + SSH2_AGENT_SIGN_REQUEST = 13 + SSH2_AGENT_SIGN_RESPONSE = 14 + SSH2_AGENT_FAILURE = 30 + SSH2_AGENT_VERSION_RESPONSE = 103 + + SSH_COM_AGENT2_FAILURE = 102 + + SSH_AGENT_REQUEST_RSA_IDENTITIES = 1 + SSH_AGENT_RSA_IDENTITIES_ANSWER1 = 2 + SSH_AGENT_RSA_IDENTITIES_ANSWER2 = 5 + SSH_AGENT_FAILURE = 5 + + # The underlying socket being used to communicate with the SSH agent. + attr_reader :socket + + # Instantiates a new agent object, connects to a running SSH agent, + # negotiates the agent protocol version, and returns the agent object. + def self.connect(logger=nil) + agent = new(logger) + agent.connect! + agent.negotiate! + agent + end + + # Creates a new Agent object, using the optional logger instance to + # report status. + def initialize(logger=nil) + self.logger = logger + end + + # Connect to the agent process using the socket factory and socket name + # given by the attribute writers. If the agent on the other end of the + # socket reports that it is an SSH2-compatible agent, this will fail + # (it only supports the ssh-agent distributed by OpenSSH). + def connect! + begin + debug { "connecting to ssh-agent" } + @socket = agent_socket_factory.open(ENV['SSH_AUTH_SOCK']) + rescue + error { "could not connect to ssh-agent" } + raise AgentNotAvailable, $!.message + end + end + + # Attempts to negotiate the SSH agent protocol version. Raises an error + # if the version could not be negotiated successfully. + def negotiate! + # determine what type of agent we're communicating with + type, body = send_and_wait(SSH2_AGENT_REQUEST_VERSION, :string, Transport::ServerVersion::PROTO_VERSION) + + if type == SSH2_AGENT_VERSION_RESPONSE + raise NotImplementedError, "SSH2 agents are not yet supported" + elsif type != SSH_AGENT_RSA_IDENTITIES_ANSWER1 && type != SSH_AGENT_RSA_IDENTITIES_ANSWER2 + raise AgentError, "unknown response from agent: #{type}, #{body.to_s.inspect}" + end + end + + # Return an array of all identities (public keys) known to the agent. + # Each key returned is augmented with a +comment+ property which is set + # to the comment returned by the agent for that key. + def identities + type, body = send_and_wait(SSH2_AGENT_REQUEST_IDENTITIES) + raise AgentError, "could not get identity count" if agent_failed(type) + raise AgentError, "bad authentication reply: #{type}" if type != SSH2_AGENT_IDENTITIES_ANSWER + + identities = [] + body.read_long.times do + key = Buffer.new(body.read_string).read_key + key.extend(Comment) + key.comment = body.read_string + identities.push key + end + + return identities + end + + # Closes this socket. This agent reference is no longer able to + # query the agent. + def close + @socket.close + end + + # Using the agent and the given public key, sign the given data. The + # signature is returned in SSH2 format. + def sign(key, data) + type, reply = send_and_wait(SSH2_AGENT_SIGN_REQUEST, :string, Buffer.from(:key, key), :string, data, :long, 0) + + if agent_failed(type) + raise AgentError, "agent could not sign data with requested identity" + elsif type != SSH2_AGENT_SIGN_RESPONSE + raise AgentError, "bad authentication response #{type}" + end + + return reply.read_string + end + + private + + # Returns the agent socket factory to use. + def agent_socket_factory + if Net::SSH::Authentication::PLATFORM == :win32 + Pageant::socket_factory + else + UNIXSocket + end + end + + # Send a new packet of the given type, with the associated data. + def send_packet(type, *args) + buffer = Buffer.from(*args) + data = [buffer.length + 1, type.to_i, buffer.to_s].pack("NCA*") + debug { "sending agent request #{type} len #{buffer.length}" } + @socket.send data, 0 + end + + # Read the next packet from the agent. This will return a two-part + # tuple consisting of the packet type, and the packet's body (which + # is returned as a Net::SSH::Buffer). + def read_packet + buffer = Net::SSH::Buffer.new(@socket.read(4)) + buffer.append(@socket.read(buffer.read_long)) + type = buffer.read_byte + debug { "received agent packet #{type} len #{buffer.length-4}" } + return type, buffer + end + + # Send the given packet and return the subsequent reply from the agent. + # (See #send_packet and #read_packet). + def send_and_wait(type, *args) + send_packet(type, *args) + read_packet + end + + # Returns +true+ if the parameter indicates a "failure" response from + # the agent, and +false+ otherwise. + def agent_failed(type) + type == SSH_AGENT_FAILURE || + type == SSH2_AGENT_FAILURE || + type == SSH_COM_AGENT2_FAILURE + end + end + +end; end; end diff --git a/net-ssh.gemspec b/net-ssh.gemspec index 959f8a5..1caecce 100644 --- a/net-ssh.gemspec +++ b/net-ssh.gemspec @@ -1,21 +1,26 @@ @spec = Gem::Specification.new do |s| - s.name = "net-ssh" - s.rubyforge_project = 'net-ssh' - s.version = "2.3.0" - s.summary = "Net::SSH: a pure-Ruby implementation of the SSH2 client protocol." - s.description = s.summary + " It allows you to write programs that invoke and interact with processes on remote servers, via SSH2." - s.authors = ["Jamis Buck", "Delano Mandelbaum"] - s.email = ["net-ssh@solutious.com"] - s.homepage = "http://github.com/net-ssh/net-ssh" - + s.name = "net-ssh" + s.rubyforge_project = 'net-ssh' + s.version = "2.3.1" + s.summary = "Net::SSH: a pure-Ruby implementation of the SSH2 client protocol." + s.description = s.summary + " It allows you to write programs that invoke and interact with processes on remote servers, via SSH2." + s.authors = ["Jamis Buck", "Delano Mandelbaum"] + s.email = ["net-ssh@solutious.com"] + s.homepage = "http://github.com/net-ssh/net-ssh" + s.extra_rdoc_files = %w[README.rdoc THANKS.rdoc CHANGELOG.rdoc] s.has_rdoc = true s.rdoc_options = ["--line-numbers", "--title", s.summary, "--main", "README.rdoc"] s.require_paths = %w[lib] s.rubygems_version = '1.3.2' - + + # This has two flavours with java one actually doing something and other + # one just raising error. This is a workaround for no ability to specify + # platform specific dependencies in gemspecs. + s.add_dependency 'jruby-pageant', ">=1.0.2" + s.executables = %w[] - + # = MANIFEST = s.files = %w( CHANGELOG.rdoc @@ -26,6 +31,8 @@ THANKS.rdoc lib/net/ssh.rb lib/net/ssh/authentication/agent.rb + lib/net/ssh/authentication/agent/java_pageant.rb + lib/net/ssh/authentication/agent/socket.rb lib/net/ssh/authentication/constants.rb lib/net/ssh/authentication/key_manager.rb lib/net/ssh/authentication/methods/abstract.rb @@ -141,5 +148,5 @@ test/transport/test_state.rb ) - + end |