From bd06bea3e5a4a17e91395ef47a811da11d68bb8a Mon Sep 17 00:00:00 2001 From: Patrick Wyatt Date: Wed, 16 May 2012 22:28:54 -0700 Subject: [PATCH] Enable Windows users with SSH installed to use 'vagrant ssh' --- lib/vagrant/ssh.rb | 132 +++++++++++++++++++++++ lib/vagrant/util/file_util.rb | 36 +++++++ lib/vagrant/util/ssh.rb | 26 ++--- templates/locales/en.yml | 7 +- test/unit/vagrant/util/file_util_test.rb | 46 ++++++++ 5 files changed, 231 insertions(+), 16 deletions(-) create mode 100644 lib/vagrant/ssh.rb create mode 100644 lib/vagrant/util/file_util.rb create mode 100644 test/unit/vagrant/util/file_util_test.rb diff --git a/lib/vagrant/ssh.rb b/lib/vagrant/ssh.rb new file mode 100644 index 000000000..e01941b84 --- /dev/null +++ b/lib/vagrant/ssh.rb @@ -0,0 +1,132 @@ +require 'log4r' + +require 'vagrant/util/file_util' +require 'vagrant/util/file_mode' +require 'vagrant/util/platform' +require 'vagrant/util/safe_exec' + +module Vagrant + # Manages SSH connection information as well as allows opening an + # SSH connection. + class SSH + include Util::SafeExec + + def initialize(vm) + @vm = vm + @logger = Log4r::Logger.new("vagrant::ssh") + end + + # Returns a hash of information necessary for accessing this + # virtual machine via SSH. + # + # @return [Hash] + def info + results = { + :host => @vm.config.ssh.host, + :port => @vm.config.ssh.port || @vm.driver.ssh_port(@vm.config.ssh.guest_port), + :username => @vm.config.ssh.username, + :forward_agent => @vm.config.ssh.forward_agent, + :forward_x11 => @vm.config.ssh.forward_x11 + } + + # This can happen if no port is set and for some reason Vagrant + # can't detect an SSH port. + raise Errors::SSHPortNotDetected if !results[:port] + + # Determine the private key path, which is either set by the + # configuration or uses just the built-in insecure key. + pk_path = @vm.config.ssh.private_key_path || @vm.env.default_private_key_path + results[:private_key_path] = File.expand_path(pk_path, @vm.env.root_path) + + # We need to check and fix the private key permissions + # to make sure that SSH gets a key with 0600 perms. + check_key_permissions(results[:private_key_path]) + + # Return the results + return results + end + + # Connects to the environment's virtual machine, replacing the ruby + # process with an SSH process. + # + # @param [Hash] opts Options hash + # @options opts [Boolean] :plain_mode If True, doesn't authenticate with + # the machine, only connects, allowing the user to connect. + def exec(opts={}) + # Get the SSH information and cache it here + ssh_info = info + + # Ensure the platform supports ssh. On Windows there are several programs which + # include ssh, notably git, mingw and cygwin, but make sure ssh is in the path! + if !Util::FileUtil.which("ssh") + if Util::Platform.windows? + raise Errors::SSHUnavailableWindows, :host => ssh_info[:host], + :port => ssh_info[:port], + :username => ssh_info[:username], + :key_path => ssh_info[:private_key_path] + end + raise Errors::SSHUnavailable + end + + # If plain mode is enabled then we don't do any authentication (we don't + # set a user or an identity file) + plain_mode = opts[:plain_mode] + + options = {} + options[:host] = ssh_info[:host] + options[:port] = ssh_info[:port] + options[:username] = ssh_info[:username] + options[:private_key_path] = ssh_info[:private_key_path] + + # Command line options + command_options = ["-p", options[:port].to_s, "-o", "UserKnownHostsFile=/dev/null", + "-o", "StrictHostKeyChecking=no", "-o", "LogLevel=QUIET"] + + # Solaris/OpenSolaris/Illumos uses SunSSH which doesn't support the IdentitiesOnly option + # (Also don't use it in plain mode, it'll skip user agents.) + command_options += ["-o", "IdentitiesOnly=yes"] if !(Util::Platform.solaris? || plain_mode) + + command_options += ["-i", options[:private_key_path]] if !plain_mode + command_options += ["-o", "ForwardAgent=yes"] if ssh_info[:forward_agent] + + # If there are extra options, then we append those + command_options.concat(opts[:extra_args]) if opts[:extra_args] + + if ssh_info[:forward_x11] + # Both are required so that no warnings are shown regarding X11 + command_options += ["-o", "ForwardX11=yes"] + command_options += ["-o", "ForwardX11Trusted=yes"] + end + + host_string = options[:host] + host_string = "#{options[:username]}@#{host_string}" if !plain_mode + command_options << host_string + @logger.info("Invoking SSH: #{command_options.inspect}") + safe_exec("ssh", *command_options) + end + + # Checks the file permissions for a private key, resetting them + # if needed. + def check_key_permissions(key_path) + # Windows systems don't have this issue + return if Util::Platform.windows? + + @logger.debug("Checking key permissions: #{key_path}") + stat = File.stat(key_path) + + if stat.owned? && Util::FileMode.from_octal(stat.mode) != "600" + @logger.info("Attempting to correct key permissions to 0600") + File.chmod(0600, key_path) + + stat = File.stat(key_path) + if Util::FileMode.from_octal(stat.mode) != "600" + raise Errors::SSHKeyBadPermissions, :key_path => key_path + end + end + rescue Errno::EPERM + # This shouldn't happen since we verified we own the file, but + # it is possible in theory, so we raise an error. + raise Errors::SSHKeyBadPermissions, :key_path => key_path + end + end +end diff --git a/lib/vagrant/util/file_util.rb b/lib/vagrant/util/file_util.rb new file mode 100644 index 000000000..4bada368b --- /dev/null +++ b/lib/vagrant/util/file_util.rb @@ -0,0 +1,36 @@ +module Vagrant + module Util + class FileUtil + # Cross-platform way of finding an executable in the $PATH. + # + # which('ruby') #=> /usr/bin/ruby + # by http://stackoverflow.com/users/11687/mislav + # + # This code is adapted from the following post by mislav: + # http://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby + def self.which(cmd) + + # If the PATHEXT variable is empty, we're on *nix and need to find the exact filename + exts = nil + if !Util::Platform.windows? || ENV['PATHEXT'].nil? + exts = [''] + # On Windows: if filename contains an extension, we must match that exact filename + elsif File.extname(cmd).length != 0 + exts = [''] + # On Windows: otherwise try to match all possible executable file extensions (.EXE .COM .BAT etc.) + else + exts = ENV['PATHEXT'].split(';') + end + + ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| + exts.each do |ext| + exe = "#{path}#{File::SEPARATOR}#{cmd}#{ext}" + return exe if File.executable? exe + end + end + + return nil + end + end + end +end diff --git a/lib/vagrant/util/ssh.rb b/lib/vagrant/util/ssh.rb index 7b73308c4..fa211e3f3 100644 --- a/lib/vagrant/util/ssh.rb +++ b/lib/vagrant/util/ssh.rb @@ -1,5 +1,6 @@ require "log4r" +require "vagrant/util/file_util" require "vagrant/util/file_mode" require "vagrant/util/platform" require "vagrant/util/safe_exec" @@ -52,20 +53,19 @@ module Vagrant # @param [Hash] opts These are additional options that are supported # by exec. def self.exec(ssh_info, opts={}) - # If we're running Windows, raise an exception since we currently - # still don't support exec-ing into SSH. In the future this should - # certainly be possible if we can detect we're in an environment that - # supports it. - if Platform.windows? - raise Errors::SSHUnavailableWindows, - :host => ssh_info[:host], - :port => ssh_info[:port], - :username => ssh_info[:username], - :key_path => ssh_info[:private_key_path] - end + # Ensure the platform supports ssh. On Windows there are several programs which + # include ssh, notably git, mingw and cygwin, but make sure ssh is in the path! + if !FileUtil.which("ssh") + if Platform.windows? + raise Errors::SSHUnavailableWindows, + :host => ssh_info[:host], + :port => ssh_info[:port], + :username => ssh_info[:username], + :key_path => ssh_info[:private_key_path] + end - # Verify that we have SSH available on the system. - raise Errors::SSHUnavailable if !Kernel.system("which ssh > /dev/null 2>&1") + raise Errors::SSHUnavailable + end # If plain mode is enabled then we don't do any authentication (we don't # set a user or an identity file) diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 1950215b2..03c1af035 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -298,9 +298,10 @@ en: guest port value, or specify an explicit SSH port with `config.ssh.port`. ssh_unavailable: "`ssh` binary could not be found. Is an SSH client installed?" ssh_unavailable_windows: |- - `vagrant ssh` isn't available on the Windows platform. You are still able - to SSH into the virtual machine if you get a Windows SSH client (such as - PuTTY). The authentication information is shown below: + `ssh` executable not found in any directories in the %PATH% variable. Is an + SSH client installed? Try installing Cygwin, MinGW or Git, all of which + contain an SSH client. Or use the PuTTY SSH client with the following + authentication information shown below: Host: %{host} Port: %{port} diff --git a/test/unit/vagrant/util/file_util_test.rb b/test/unit/vagrant/util/file_util_test.rb new file mode 100644 index 000000000..61d7897af --- /dev/null +++ b/test/unit/vagrant/util/file_util_test.rb @@ -0,0 +1,46 @@ +require File.expand_path("../../../base", __FILE__) + +require 'vagrant/util/file_util' +require 'vagrant/util/platform' + +describe Vagrant::Util::FileUtil do + + def tester (file_extension, test_extension, mode, &block) + # create file in temp directory + filename = '__vagrant_unit_test__' + dir = Dir.tmpdir + file = Pathname(dir) + (filename + file_extension) + file.open("w") { |f| f.write("#") } + file.chmod(mode) + + # set the path to the directory where the file is located + savepath = ENV['PATH'] + ENV['PATH'] = dir.to_s + block.call filename + test_extension + ENV['PATH'] = savepath + + file.unlink + end + + it "should return a path for an executable file" do + tester '.bat', '.bat', 0755 do |name| + Vagrant::Util::FileUtil.which(name).should_not be_nil + end + end + + if Vagrant::Util::Platform.windows? + it "should return a path for a Windows executable file" do + tester '.bat', '', 0755 do |name| + Vagrant::Util::FileUtil.which(name).should_not be_nil + end + end + end + + it "should return nil for a non-executable file" do + tester '.txt', '.txt', 0644 do |name| + Vagrant::Util::FileUtil.which(name).should be_nil + end + end + +end +