diff options
-rw-r--r-- | lib/net/ssh.rb | 8 | ||||
-rw-r--r-- | lib/net/ssh/authentication/key_manager.rb | 4 | ||||
-rw-r--r-- | lib/net/ssh/authentication/methods/abstract.rb | 4 | ||||
-rw-r--r-- | lib/net/ssh/authentication/methods/keyboard_interactive.rb | 20 | ||||
-rw-r--r-- | lib/net/ssh/authentication/methods/password.rb | 17 | ||||
-rw-r--r-- | lib/net/ssh/authentication/session.rb | 3 | ||||
-rw-r--r-- | lib/net/ssh/key_factory.rb | 45 | ||||
-rw-r--r-- | lib/net/ssh/prompt.rb | 125 | ||||
-rw-r--r-- | test/authentication/methods/common.rb | 8 | ||||
-rw-r--r-- | test/authentication/methods/test_keyboard_interactive.rb | 27 | ||||
-rw-r--r-- | test/authentication/methods/test_password.rb | 10 | ||||
-rw-r--r-- | test/authentication/test_key_manager.rb | 18 | ||||
-rw-r--r-- | test/common.rb | 14 | ||||
-rw-r--r-- | test/integration/playbook.yml | 4 | ||||
-rw-r--r-- | test/integration/test_forward.rb | 2 | ||||
-rw-r--r-- | test/integration/test_id_rsa_keys.rb | 9 | ||||
-rw-r--r-- | test/integration/test_password.rb | 50 | ||||
-rw-r--r-- | test/integration/test_proxy.rb | 2 | ||||
-rw-r--r-- | test/test_key_factory.rb | 21 |
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 |