require 'log4r'

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

      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 if !Kernel.system("which ssh > /dev/null 2>&1")

      # 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=ERROR"]

      # Solaris/OpenSolaris/Illumos uses SunSSH which doesn't support the IdentitiesOnly option
      command_options += ["-o", "IdentitiesOnly=yes"] unless Util::Platform.solaris?

      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)

        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
