summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiklos Fazekas <mfazekas@szemafor.com>2016-04-08 06:27:07 +0200
committerMiklos Fazekas <mfazekas@szemafor.com>2016-04-08 18:58:15 +0200
commit2b6cd7a5a139ce2c1379546b1e8d09c9013ebbd2 (patch)
treeca9dd835964e5d81edd6a5669c887f6627f741d0
parent81170de0b08a47f1f86001ea5ab7f519da29c00f (diff)
downloadnet-ssh-2b6cd7a5a139ce2c1379546b1e8d09c9013ebbd2.tar.gz
Refactor prompting to a class that can be customized
Fixes: #91, and fixes #254, and fixes #293
-rw-r--r--lib/net/ssh.rb8
-rw-r--r--lib/net/ssh/authentication/key_manager.rb4
-rw-r--r--lib/net/ssh/authentication/methods/abstract.rb4
-rw-r--r--lib/net/ssh/authentication/methods/keyboard_interactive.rb20
-rw-r--r--lib/net/ssh/authentication/methods/password.rb17
-rw-r--r--lib/net/ssh/authentication/session.rb3
-rw-r--r--lib/net/ssh/key_factory.rb45
-rw-r--r--lib/net/ssh/prompt.rb125
-rw-r--r--test/authentication/methods/common.rb8
-rw-r--r--test/authentication/methods/test_keyboard_interactive.rb27
-rw-r--r--test/authentication/methods/test_password.rb10
-rw-r--r--test/authentication/test_key_manager.rb18
-rw-r--r--test/common.rb14
-rw-r--r--test/integration/playbook.yml4
-rw-r--r--test/integration/test_forward.rb2
-rw-r--r--test/integration/test_id_rsa_keys.rb9
-rw-r--r--test/integration/test_password.rb50
-rw-r--r--test/integration/test_proxy.rb2
-rw-r--r--test/test_key_factory.rb21
19 files changed, 240 insertions, 151 deletions
diff --git a/lib/net/ssh.rb b/lib/net/ssh.rb
index e690911..4e97b06 100644
--- a/lib/net/ssh.rb
+++ b/lib/net/ssh.rb
@@ -3,6 +3,7 @@
ENV['HOME'] ||= ENV['HOMEPATH'] ? "#{ENV['HOMEDRIVE']}#{ENV['HOMEPATH']}" : Dir.pwd
require 'logger'
+require 'etc'
require 'net/ssh/config'
require 'net/ssh/errors'
@@ -10,7 +11,7 @@ require 'net/ssh/loggable'
require 'net/ssh/transport/session'
require 'net/ssh/authentication/session'
require 'net/ssh/connection/session'
-require 'etc'
+require 'net/ssh/prompt'
module Net
@@ -70,7 +71,7 @@ module Net
:known_hosts, :global_known_hosts_file, :user_known_hosts_file, :host_key_alias,
:host_name, :user, :properties, :passphrase, :keys_only, :max_pkt_size,
:max_win_size, :send_env, :use_agent, :number_of_password_prompts,
- :append_supported_algorithms, :non_interactive
+ :append_supported_algorithms, :non_interactive, :password_prompt
]
# The standard means of starting a new SSH connection. When used with a
@@ -192,6 +193,7 @@ module Net
# password auth method
# * :non_interactive => non interactive applications should set it to true
# to prefer failing a password/etc auth methods vs asking for password
+ # * :password_prompt => a custom prompt object with ask method. See Net::SSH::Prompt
#
# If +user+ parameter is nil it defaults to USER from ssh_config, or
# local username
@@ -214,6 +216,8 @@ module Net
options[:number_of_password_prompts] = 0
end
+ options[:password_prompt] ||= Prompt.default(options)
+
if options[:verbose]
options[:logger].level = case options[:verbose]
when Fixnum then options[:verbose]
diff --git a/lib/net/ssh/authentication/key_manager.rb b/lib/net/ssh/authentication/key_manager.rb
index e0ba7ff..0309a5e 100644
--- a/lib/net/ssh/authentication/key_manager.rb
+++ b/lib/net/ssh/authentication/key_manager.rb
@@ -221,11 +221,11 @@ module Net
key = KeyFactory.load_public_key(identity[:pubkey_file])
{ :public_key => key, :from => :file, :file => identity[:privkey_file] }
when :privkey_file
- private_key = KeyFactory.load_private_key(identity[:privkey_file], options[:passphrase], ask_passphrase)
+ private_key = KeyFactory.load_private_key(identity[:privkey_file], options[:passphrase], ask_passphrase, options[:password_prompt])
key = private_key.send(:public_key)
{ :public_key => key, :from => :file, :file => identity[:privkey_file], :key => private_key }
when :data
- private_key = KeyFactory.load_data_private_key(identity[:data], options[:passphrase], ask_passphrase)
+ private_key = KeyFactory.load_data_private_key(identity[:data], options[:passphrase], ask_passphrase, "<key in memory>", options[:password_prompt])
key = private_key.send(:public_key)
{ :public_key => key, :from => :key_data, :data => identity[:data], :key => private_key }
else
diff --git a/lib/net/ssh/authentication/methods/abstract.rb b/lib/net/ssh/authentication/methods/abstract.rb
index 339c53c..eb830aa 100644
--- a/lib/net/ssh/authentication/methods/abstract.rb
+++ b/lib/net/ssh/authentication/methods/abstract.rb
@@ -22,6 +22,7 @@ module Net; module SSH; module Authentication; module Methods
@session = session
@key_manager = options[:key_manager]
@options = options
+ @prompt = options[:password_prompt]
self.logger = session.logger
end
@@ -55,6 +56,9 @@ module Net; module SSH; module Authentication; module Methods
buffer
end
+ private
+
+ attr_reader :prompt
end
end; end; end; end \ No newline at end of file
diff --git a/lib/net/ssh/authentication/methods/keyboard_interactive.rb b/lib/net/ssh/authentication/methods/keyboard_interactive.rb
index e68b825..70c3c07 100644
--- a/lib/net/ssh/authentication/methods/keyboard_interactive.rb
+++ b/lib/net/ssh/authentication/methods/keyboard_interactive.rb
@@ -8,8 +8,6 @@ module Net
# Implements the "keyboard-interactive" SSH authentication method.
class KeyboardInteractive < Abstract
- include Prompt
-
USERAUTH_INFO_REQUEST = 60
USERAUTH_INFO_RESPONSE = 61
@@ -18,12 +16,14 @@ module Net
debug { "trying keyboard-interactive" }
send_message(userauth_request(username, next_service, "keyboard-interactive", "", ""))
+ prompter = nil
loop do
message = session.next_message
case message.type
when USERAUTH_SUCCESS
debug { "keyboard-interactive succeeded" }
+ prompter.success if prompter
return true
when USERAUTH_FAILURE
debug { "keyboard-interactive failed" }
@@ -31,26 +31,26 @@ module Net
raise Net::SSH::Authentication::DisallowedMethod unless
message[:authentications].split(/,/).include? 'keyboard-interactive'
- return false
+ return false unless interactive?
+ password = nil
+ debug { "retrying keyboard-interactive" }
+ send_message(userauth_request(username, next_service, "keyboard-interactive", "", ""))
when USERAUTH_INFO_REQUEST
name = message.read_string
instruction = message.read_string
debug { "keyboard-interactive info request" }
- unless password
- if interactive?
- puts(name) unless name.empty?
- puts(instruction) unless instruction.empty?
- end
+ if password.nil? && interactive? && prompter.nil?
+ prompter = prompt.start(type: 'keyboard-interactive', name: name, instruction: instruction)
end
_ = message.read_string # lang_tag
responses =[]
-
+
message.read_long.times do
text = message.read_string
echo = message.read_bool
- password_to_send = password || (interactive? ? prompt(text, echo) : nil)
+ password_to_send = password || (prompter && prompter.ask(text, echo))
responses << password_to_send
end
diff --git a/lib/net/ssh/authentication/methods/password.rb b/lib/net/ssh/authentication/methods/password.rb
index 535b327..7707eb8 100644
--- a/lib/net/ssh/authentication/methods/password.rb
+++ b/lib/net/ssh/authentication/methods/password.rb
@@ -9,11 +9,10 @@ module Net
# Implements the "password" SSH authentication method.
class Password < Abstract
- include Prompt
-
# Attempt to authenticate the given user for the given service. If
# the password parameter is nil, this will ask for password
def authenticate(next_service, username, password=nil)
+ clear_prompter!
retries = 0
max_retries = get_max_retries
return false if !password && max_retries == 0
@@ -37,6 +36,7 @@ module Net
case message.type
when USERAUTH_SUCCESS
debug { "password succeeded" }
+ @prompter.success if @prompter
return true
when USERAUTH_FAILURE
return false
@@ -52,9 +52,20 @@ module Net
NUMBER_OF_PASSWORD_PROMPTS = 3
+ def clear_prompter!
+ @prompt_info = nil
+ @prompter = nil
+ end
+
def ask_password(username)
+ host = session.transport.host
+ prompt_info = {type: 'password', user: username, host: host}
+ if @prompt_info != prompt_info
+ @prompt_info = prompt_info
+ @prompter = prompt.start(prompt_info)
+ end
echo = false
- prompt("#{username}@#{session.transport.host}'s password:", echo)
+ @prompter.ask("#{username}@#{host}'s password:", echo)
end
def get_max_retries
diff --git a/lib/net/ssh/authentication/session.rb b/lib/net/ssh/authentication/session.rb
index 67a0be9..e87f669 100644
--- a/lib/net/ssh/authentication/session.rb
+++ b/lib/net/ssh/authentication/session.rb
@@ -70,7 +70,8 @@ module Net; module SSH; module Authentication
debug { "trying #{name}" }
begin
- method = Methods.const_get(name.split(/\W+/).map { |p| p.capitalize }.join).new(self, :key_manager => key_manager)
+ auth_class = Methods.const_get(name.split(/\W+/).map { |p| p.capitalize }.join)
+ method = auth_class.new(self, key_manager: key_manager, password_prompt: options[:password_prompt])
rescue NameError
debug{"Mechanism #{name} was requested, but isn't a known type. Ignoring it."}
next
diff --git a/lib/net/ssh/key_factory.rb b/lib/net/ssh/key_factory.rb
index 56a5faf..021cc96 100644
--- a/lib/net/ssh/key_factory.rb
+++ b/lib/net/ssh/key_factory.rb
@@ -26,8 +26,6 @@ module Net; module SSH
end
class <<self
- include Prompt
-
# Fetch an OpenSSL key instance by its SSH name. It will be a new,
# empty key of the given type.
def get(name)
@@ -39,9 +37,9 @@ module Net; module SSH
# appropriately. The new key is returned. If the key itself is
# encrypted (requiring a passphrase to use), the user will be
# prompted to enter their password unless passphrase works.
- def load_private_key(filename, passphrase=nil, ask_passphrase=true)
+ def load_private_key(filename, passphrase=nil, ask_passphrase=true, prompt=Prompt.default)
data = File.read(File.expand_path(filename))
- load_data_private_key(data, passphrase, ask_passphrase, filename)
+ load_data_private_key(data, passphrase, ask_passphrase, filename, prompt)
end
# Loads a private key. It will correctly determine
@@ -49,7 +47,7 @@ module Net; module SSH
# appropriately. The new key is returned. If the key itself is
# encrypted (requiring a passphrase to use), the user will be
# prompted to enter their password unless passphrase works.
- def load_data_private_key(data, passphrase=nil, ask_passphrase=true, filename="")
+ def load_data_private_key(data, passphrase=nil, ask_passphrase=true, filename="", prompt=Prompt.default)
if OpenSSL::PKey.respond_to?(:read)
pkey_read = true
error_class = ArgumentError
@@ -78,29 +76,32 @@ module Net; module SSH
openssh_key = data.match(/-----BEGIN OPENSSH PRIVATE KEY-----/)
tries = 0
- begin
- if openssh_key
- ED25519::PrivKey.read(data, passphrase || 'invalid')
- else
- if pkey_read
- return OpenSSL::PKey.read(data, passphrase || 'invalid')
+ prompter = nil
+ result =
+ begin
+ if openssh_key
+ ED25519::PrivKey.read(data, passphrase || 'invalid')
+ elsif pkey_read
+ OpenSSL::PKey.read(data, passphrase || 'invalid')
else
- return key_type.new(data, passphrase || 'invalid')
+ key_type.new(data, passphrase || 'invalid')
end
- end
- rescue error_class
- if encrypted_key && ask_passphrase
- tries += 1
- if tries <= 3
- passphrase = prompt("Enter passphrase for #{filename}:", false)
- retry
+ rescue error_class
+ if encrypted_key && ask_passphrase
+ tries += 1
+ if tries <= 3
+ prompter ||= prompt.start(type: 'private_key', filename: filename, sha: Digest::SHA256.digest(data))
+ passphrase = prompter.ask("Enter passphrase for #{filename}:", false)
+ retry
+ else
+ raise
+ end
else
raise
end
- else
- raise
end
- end
+ prompter.success if prompter
+ result
end
# Loads a public key from a file. It will correctly determine whether
diff --git a/lib/net/ssh/prompt.rb b/lib/net/ssh/prompt.rb
index 505e0b3..54b4d85 100644
--- a/lib/net/ssh/prompt.rb
+++ b/lib/net/ssh/prompt.rb
@@ -1,93 +1,64 @@
-module Net; module SSH
-
- # A basic prompt module that can be mixed into other objects. If HighLine is
- # installed, it will be used to display prompts and read input from the
- # user. Otherwise, the termios library will be used. If neither HighLine
- # nor termios is installed, a simple prompt that echos text in the clear
- # will be used.
+require 'io/console'
- module PromptMethods
+module Net; module SSH
- # Defines the prompt method to use if the Highline library is installed.
- module Highline
- # Uses Highline#ask to present a prompt and accept input. If +echo+ is
- # +false+, the characters entered by the user will not be echoed to the
- # screen.
- def prompt(prompt, echo=true)
- @highline ||= ::HighLine.new
- @highline.ask(prompt + " ") { |q| q.echo = echo }
- end
+ # Default prompt implementation, called for asking password from user.
+ # It will never be instantiated directly, but will instead be created for
+ # you automatically.
+ #
+ # A custom prompt objects can implement caching, or different UI. The prompt
+ # object should implemnted a start method, which should return something implementing
+ # ask and success. Net::SSH uses it like:
+ #
+ # prompter = options[:password_prompt].start({type:'password'})
+ # while !ok && max_retries < 3
+ # user = prompter.ask("user: ", {}, true)
+ # password = prompter.ask("password: ", {}, false)
+ # ok = send(user, password)
+ # prompter.sucess if ok
+ # end
+ #
+ class Prompt
+ # factory
+ def self.default(options = {})
+ @default ||= new(options)
end
- # Defines the prompt method to use if the Termios library is installed.
- module Termios
- # Displays the prompt to $stdout. If +echo+ is false, the Termios
- # library will be used to disable keystroke echoing for the duration of
- # this method.
- def prompt(prompt, echo=true)
- $stdout.print(prompt)
- $stdout.flush
-
- set_echo(false) unless echo
- $stdin.gets.chomp
- ensure
- if !echo
- set_echo(true)
- $stdout.puts
- end
- end
-
- private
-
- # Enables or disables keystroke echoing using the Termios library.
- def set_echo(enable)
- term = ::Termios.getattr($stdin)
-
- if enable
- term.c_lflag |= (::Termios::ECHO | ::Termios::ICANON)
- else
- term.c_lflag &= ~::Termios::ECHO
- end
-
- ::Termios.setattr($stdin, ::Termios::TCSANOW, term)
- end
+ def initialize(options = {})
end
- # Defines the prompt method to use when neither Highline nor Termios are
- # installed.
- module Clear
- # Displays the prompt to $stdout and pulls the response from $stdin.
- # Text is always echoed in the clear, regardless of the +echo+ setting.
- # The first time a prompt is given and +echo+ is false, a warning will
- # be written to $stderr recommending that either Highline or Termios
- # be installed.
- def prompt(prompt, echo=true)
- @seen_warning ||= false
- if !echo && !@seen_warning
- $stderr.puts "Text will be echoed in the clear. Please install the HighLine or Termios libraries to suppress echoed text."
- @seen_warning = true
+ # default prompt object implementation. More sophisticated implemenetations
+ # might implement caching.
+ class Prompter
+ def initialize(info)
+ if info[:type] == 'keyboard-interactive' # rubocop:disable Style/GuardClause
+ $stdout.puts(info[:name]) unless info[:name].empty?
+ $stdout.puts(info[:instruction]) unless info[:instruction].empty?
end
+ end
+ # ask input from user, a prompter might ask for multiple inputs
+ # (like user and password) in a single session.
+ def ask(prompt, echo=true)
$stdout.print(prompt)
$stdout.flush
- $stdin.gets.chomp
+ ret = $stdin.noecho(&:gets).chomp
+ $stdout.print("\n")
+ ret
end
- end
- end
- # Try to load Highline and Termios in turn, selecting the corresponding
- # PromptMethods module to use. If neither are available, choose PromptMethods::Clear.
- Prompt = begin
- require 'highline'
- HighLine.track_eof = false
- PromptMethods::Highline
- rescue LoadError
- begin
- require 'termios'
- PromptMethods::Termios
- rescue LoadError
- PromptMethods::Clear
+ # success method will be called when the password was accepted
+ # It's a good time to save password asked to a cache.
+ def success
end
end
+ # start password session. Multiple questions might be asked multiple times
+ # on the returned object. Info hash tries to uniquely identify the password
+ # session, so caching implementations can save passwords properly.
+ def start(info)
+ Prompter.new(info)
+ end
+ end
+
end; end \ No newline at end of file
diff --git a/test/authentication/methods/common.rb b/test/authentication/methods/common.rb
index 735836d..0bfba99 100644
--- a/test/authentication/methods/common.rb
+++ b/test/authentication/methods/common.rb
@@ -10,7 +10,7 @@ module Authentication; module Methods
end
def transport(options={})
- @transport ||= MockTransport.new(options.merge(:socket => socket))
+ @transport ||= MockTransport.new(options.merge(socket: socket))
end
def session(options={})
@@ -23,6 +23,12 @@ module Authentication; module Methods
end
end
+ def reset_session(options = {})
+ @transport = nil
+ @session = nil
+ session(options)
+ end
+
end
end; end \ No newline at end of file
diff --git a/test/authentication/methods/test_keyboard_interactive.rb b/test/authentication/methods/test_keyboard_interactive.rb
index 8e02767..27ed290 100644
--- a/test/authentication/methods/test_keyboard_interactive.rb
+++ b/test/authentication/methods/test_keyboard_interactive.rb
@@ -1,6 +1,6 @@
-require 'common'
+require_relative '../../common'
require 'net/ssh/authentication/methods/keyboard_interactive'
-require 'authentication/methods/common'
+require_relative 'common'
module Authentication; module Methods
@@ -10,6 +10,10 @@ module Authentication; module Methods
USERAUTH_INFO_REQUEST = 60
USERAUTH_INFO_RESPONSE = 61
+ def setup
+ reset_subject({}) if defined? @subject && !@subject.options.empty?
+ end
+
def test_authenticate_should_raise_if_keyboard_interactive_disallowed
transport.expect do |t,packet|
assert_equal USERAUTH_REQUEST, packet.type
@@ -28,6 +32,8 @@ module Authentication; module Methods
end
def test_authenticate_should_be_false_if_given_password_is_not_accepted
+ reset_subject(non_interactive: true)
+
transport.expect do |t,packet|
assert_equal USERAUTH_REQUEST, packet.type
t.return(USERAUTH_INFO_REQUEST, :string, "", :string, "", :string, "", :long, 1, :string, "Password:", :bool, false)
@@ -72,10 +78,7 @@ module Authentication; module Methods
end
def test_authenticate_should_not_prompt_for_input_when_in_non_interactive_mode
-
- def transport.options
- {non_interactive: true}
- end
+ reset_subject(non_interactive: true)
transport.expect do |t,packet|
assert_equal USERAUTH_REQUEST, packet.type
t.return(USERAUTH_INFO_REQUEST, :string, "", :string, "", :string, "", :long, 2, :string, "Name:", :bool, true, :string, "Password:", :bool, false)
@@ -93,8 +96,10 @@ module Authentication; module Methods
def test_authenticate_should_prompt_for_input_when_password_is_not_given
- subject.expects(:prompt).with("Name:", true).returns("name")
- subject.expects(:prompt).with("Password:", false).returns("password")
+ prompt = MockPrompt.new
+ prompt.expects(:_ask).with("Name:", anything, true).returns("name")
+ prompt.expects(:_ask).with("Password:", anything, false).returns("password")
+ reset_subject(password_prompt: prompt)
transport.expect do |t,packet|
assert_equal USERAUTH_REQUEST, packet.type
@@ -116,6 +121,12 @@ module Authentication; module Methods
def subject(options={})
@subject ||= Net::SSH::Authentication::Methods::KeyboardInteractive.new(session(options), options)
end
+
+ def reset_subject(options)
+ @subject = nil
+ reset_session(options)
+ subject(options)
+ end
end
end; end
diff --git a/test/authentication/methods/test_password.rb b/test/authentication/methods/test_password.rb
index 60a5c3b..48f6060 100644
--- a/test/authentication/methods/test_password.rb
+++ b/test/authentication/methods/test_password.rb
@@ -47,8 +47,9 @@ module Authentication; module Methods
end
end
- subject.expects(:prompt).with("jamis@'s password:", false).returns("the-password-2")
- subject.authenticate("ssh-connection", "jamis", "the-password")
+ prompt = MockPrompt.new
+ prompt.expects(:_ask).with("jamis@'s password:", {type: 'password', user: 'jamis', host: nil}, false).returns("the-password-2")
+ subject(password_prompt: prompt).authenticate("ssh-connection", "jamis", "the-password")
end
def test_authenticate_ask_for_password_if_not_given
@@ -63,8 +64,9 @@ module Authentication; module Methods
end
transport.instance_eval { @host='testhost' }
- subject.expects(:prompt).with("bill@testhost's password:", false).returns("good-password")
- subject.authenticate("ssh-connection", "bill", nil)
+ prompt = MockPrompt.new
+ prompt.expects(:_ask).with("bill@testhost's password:", {type: 'password', user: 'bill', host: 'testhost'}, false).returns("good-password")
+ subject(password_prompt: prompt).authenticate("ssh-connection", "bill", nil)
end
def test_authenticate_when_password_is_acceptible_should_return_true
diff --git a/test/authentication/test_key_manager.rb b/test/authentication/test_key_manager.rb
index caf72d1..080e1ec 100644
--- a/test/authentication/test_key_manager.rb
+++ b/test/authentication/test_key_manager.rb
@@ -1,4 +1,4 @@
-require 'common'
+require_relative '../common'
require 'net/ssh/authentication/key_manager'
module Authentication
@@ -172,13 +172,13 @@ module Authentication
case options.fetch(:passphrase, :indifferently)
when :should_be_asked
- Net::SSH::KeyFactory.expects(:load_private_key).with(name, nil, false).raises(OpenSSL::PKey::RSAError).at_least_once
- Net::SSH::KeyFactory.expects(:load_private_key).with(name, nil, true).returns(key).at_least_once
+ Net::SSH::KeyFactory.expects(:load_private_key).with(name, nil, false, prompt).raises(OpenSSL::PKey::RSAError).at_least_once
+ Net::SSH::KeyFactory.expects(:load_private_key).with(name, nil, true, prompt).returns(key).at_least_once
when :should_not_be_asked
- Net::SSH::KeyFactory.expects(:load_private_key).with(name, nil, false).raises(OpenSSL::PKey::RSAError).at_least_once
- Net::SSH::KeyFactory.expects(:load_private_key).with(name, nil, true).never
+ Net::SSH::KeyFactory.expects(:load_private_key).with(name, nil, false, prompt).raises(OpenSSL::PKey::RSAError).at_least_once
+ Net::SSH::KeyFactory.expects(:load_private_key).with(name, nil, true, prompt).never
else # :indifferently
- Net::SSH::KeyFactory.expects(:load_private_key).with(name, nil, any_of(true, false)).returns(key).at_least_once
+ Net::SSH::KeyFactory.expects(:load_private_key).with(name, nil, any_of(true, false), prompt).returns(key).at_least_once
end
# do not override OpenSSL::PKey::EC#public_key
@@ -231,8 +231,12 @@ module Authentication
ecdsa_sha2_nistp521])
end
+ def prompt
+ @promp ||= MockPrompt.new
+ end
+
def manager(options = {})
- @manager ||= Net::SSH::Authentication::KeyManager.new(nil, options)
+ @manager ||= Net::SSH::Authentication::KeyManager.new(nil, {password_prompt: prompt}.merge(options))
end
end
diff --git a/test/common.rb b/test/common.rb
index 5089a38..1aa0f12 100644
--- a/test/common.rb
+++ b/test/common.rb
@@ -40,6 +40,20 @@ class NetSSHTest < Minitest::Test
end
end
+class MockPrompt
+ def start(info)
+ @info = info
+ self
+ end
+
+ def ask(message, echo)
+ _ask(message, @info, echo)
+ end
+
+ def success
+ end
+end
+
class MockTransport < Net::SSH::Transport::Session
class BlockVerifier
def initialize(block)
diff --git a/test/integration/playbook.yml b/test/integration/playbook.yml
index 5ff3c83..226ed00 100644
--- a/test/integration/playbook.yml
+++ b/test/integration/playbook.yml
@@ -2,6 +2,7 @@
- hosts: all
sudo: yes
vars:
+ no_rvm: no
myuser: vagrant
mygroup: vagrant
homedir: /home/vagrant
@@ -43,6 +44,9 @@
- name: sshd debug
lineinfile: dest='/etc/ssh/sshd_config' line='LogLevel DEBUG' regexp=LogLevel
notify: restart sshd
+ - name: sshd allow interactive
+ lineinfile: dest='/etc/ssh/sshd_config' line='ChallengeResponseAuthentication yes' regexp='^ChallengeResponseAuthentication.+'
+ notify: restart sshd
- name: sshd allow forward
lineinfile: dest='/etc/ssh/sshd_config' line='AllowTcpForwarding all' regexp=LogLevel
notify: restart sshd
diff --git a/test/integration/test_forward.rb b/test/integration/test_forward.rb
index fc0032e..e07fe16 100644
--- a/test/integration/test_forward.rb
+++ b/test/integration/test_forward.rb
@@ -14,7 +14,7 @@
#
# http://net-ssh.lighthouseapp.com/projects/36253/tickets/7
-require_relative './common'
+require_relative 'common'
require 'net/ssh/buffer'
require 'net/ssh'
require 'net/ssh/proxy/command'
diff --git a/test/integration/test_id_rsa_keys.rb b/test/integration/test_id_rsa_keys.rb
index 43ffc89..3df4e93 100644
--- a/test/integration/test_id_rsa_keys.rb
+++ b/test/integration/test_id_rsa_keys.rb
@@ -1,4 +1,4 @@
-require 'common'
+require_relative 'common'
require 'fileutils'
require 'tmpdir'
@@ -86,9 +86,10 @@ class TestIDRSAPKeys < NetSSHTest
options = {keys: [], key_data: [private_key]}
#key_manager = Net::SSH::Authentication::KeyManager.new(nil, options)
-
- Net::SSH::KeyFactory.expects(:prompt).with('Enter passphrase for :', false).returns('pwd12')
- Net::SSH.start("localhost", "net_ssh_1", options) do |ssh|
+ prompt = MockPrompt.new
+ sha = Digest::SHA256.digest(private_key)
+ prompt.expects(:_ask).with('Enter passphrase for <key in memory>:', {type: 'private_key', filename: '<key in memory>', sha: sha}, false).returns('pwd12')
+ Net::SSH.start("localhost", "net_ssh_1", options.merge(password_prompt: prompt)) do |ssh|
ssh.exec! 'whoami'
end
end
diff --git a/test/integration/test_password.rb b/test/integration/test_password.rb
new file mode 100644
index 0000000..36c800c
--- /dev/null
+++ b/test/integration/test_password.rb
@@ -0,0 +1,50 @@
+require_relative 'common'
+require 'net/ssh'
+
+class TestPassword < NetSSHTest
+ include IntegrationTestHelpers
+
+ def test_with_password_parameter
+ ret = Net::SSH.start("localhost", "net_ssh_1", password: 'foopwd') do |ssh|
+ ssh.exec! 'echo "hello from:$USER"'
+ end
+ assert_equal ret, "hello from:net_ssh_1\n"
+ end
+
+ def test_keyboard_interactive_with_good_password
+ ps = Object.new
+ pt = Object.new
+ pt.expects(:start).with(type: 'keyboard-interactive', name: '', instruction: '').returns(ps)
+ ps.expects(:ask).with('Password: ', false).returns("foopwd")
+ ps.expects(:success)
+ ret = Net::SSH.start("localhost", "net_ssh_1", auth_methods: ['keyboard-interactive'], password_prompt: pt) do |ssh|
+ ssh.exec! 'echo "hello from:$USER"'
+ end
+ assert_equal ret, "hello from:net_ssh_1\n"
+ end
+
+ def test_keyboard_interactive_with_one_failed_attempt
+ ps = Object.new
+ pt = Object.new
+ pt.expects(:start).with(type: 'keyboard-interactive', name: '', instruction: '').returns(ps)
+ ps.expects(:ask).twice.with('Password: ', false).returns("badpwd").then.with('Password: ', false).returns("foopwd")
+ ps.expects(:success)
+ ret = Net::SSH.start("localhost", "net_ssh_1", auth_methods: ['keyboard-interactive'], password_prompt: pt) do |ssh|
+ ssh.exec! 'echo "hello from:$USER"'
+ end
+ assert_equal ret, "hello from:net_ssh_1\n"
+ end
+
+ def test_password_with_good_password
+ ps = Object.new
+ pt = Object.new
+ pt.expects(:start).with(type: 'password', user: 'net_ssh_1', host: 'localhost').returns(ps)
+ ps.expects(:ask).with("net_ssh_1@localhost's password:", false).returns("foopwd")
+ ps.expects(:success)
+
+ ret = Net::SSH.start("localhost", "net_ssh_1", auth_methods: ['password'], password_prompt: pt) do |ssh|
+ ssh.exec! 'echo "hello from:$USER"'
+ end
+ assert_equal ret, "hello from:net_ssh_1\n"
+ end
+end \ No newline at end of file
diff --git a/test/integration/test_proxy.rb b/test/integration/test_proxy.rb
index 689b517..3286f34 100644
--- a/test/integration/test_proxy.rb
+++ b/test/integration/test_proxy.rb
@@ -1,4 +1,4 @@
-require_relative './common'
+require_relative 'common'
require 'net/ssh/buffer'
require 'net/ssh'
require 'timeout'
diff --git a/test/test_key_factory.rb b/test/test_key_factory.rb
index da61c5b..336405b 100644
--- a/test/test_key_factory.rb
+++ b/test/test_key_factory.rb
@@ -1,4 +1,4 @@
-require 'common'
+require_relative 'common'
require 'net/ssh/key_factory'
class TestKeyFactory < NetSSHTest
@@ -17,9 +17,10 @@ class TestKeyFactory < NetSSHTest
end
def test_load_encrypted_private_RSA_key_should_prompt_for_password_and_return_key
+ prompt = MockPrompt.new
File.expects(:read).with(@key_file).returns(encrypted(rsa_key, "password"))
- Net::SSH::KeyFactory.expects(:prompt).with("Enter passphrase for #{@key_file}:", false).returns("password")
- assert_equal rsa_key.to_der, Net::SSH::KeyFactory.load_private_key(@key_file).to_der
+ prompt.expects(:_ask).with("Enter passphrase for #{@key_file}:", has_entries(type: 'private_key', filename: @key_file), false).returns("password")
+ assert_equal rsa_key.to_der, Net::SSH::KeyFactory.load_private_key(@key_file, nil, true, prompt).to_der
end
def test_load_encrypted_private_RSA_key_with_password_should_not_prompt_and_return_key
@@ -28,9 +29,12 @@ class TestKeyFactory < NetSSHTest
end
def test_load_encrypted_private_DSA_key_should_prompt_for_password_and_return_key
- File.expects(:read).with(@key_file).returns(encrypted(dsa_key, "password"))
- Net::SSH::KeyFactory.expects(:prompt).with("Enter passphrase for #{@key_file}:", false).returns("password")
- assert_equal dsa_key.to_der, Net::SSH::KeyFactory.load_private_key(@key_file).to_der
+ prompt = MockPrompt.new
+ data = encrypted(dsa_key, "password")
+ File.expects(:read).with(@key_file).returns(data)
+ sha = Digest::SHA256.digest(data)
+ prompt.expects(:_ask).with("Enter passphrase for #{@key_file}:", {type: 'private_key', filename: '/key-file', sha: sha}, false).returns("password")
+ assert_equal dsa_key.to_der, Net::SSH::KeyFactory.load_private_key(@key_file, nil, true, prompt).to_der
end
def test_load_encrypted_private_DSA_key_with_password_should_not_prompt_and_return_key
@@ -39,14 +43,15 @@ class TestKeyFactory < NetSSHTest
end
def test_load_encrypted_private_key_should_give_three_tries_for_the_password_and_then_raise_exception
+ prompt = MockPrompt.new
File.expects(:read).with(@key_file).returns(encrypted(rsa_key, "password"))
- Net::SSH::KeyFactory.expects(:prompt).times(3).with("Enter passphrase for #{@key_file}:", false).returns("passwod","passphrase","passwd")
+ prompt.expects(:_ask).times(3).with("Enter passphrase for #{@key_file}:", has_entries(type: 'private_key', filename: '/key-file'), false).returns("passwod","passphrase","passwd")
if OpenSSL::PKey.respond_to?(:read)
error_class = ArgumentError
else
error_class = OpenSSL::PKey::RSAError
end
- assert_raises(error_class) { Net::SSH::KeyFactory.load_private_key(@key_file) }
+ assert_raises(error_class) { Net::SSH::KeyFactory.load_private_key(@key_file, nil, true, prompt) }
end
def test_load_encrypted_private_key_should_raise_exception_without_asking_passphrase