Class: Vagrant::Util::SSH

Inherits:
Object
  • Object
show all
Extended by:
SafePuts
Defined in:
lib/vagrant/util/ssh.rb

Overview

This is a class that has helpers on it for dealing with SSH. These helpers don't depend on any part of Vagrant except what is given via the parameters.

Constant Summary collapse

LOGGER =
Log4r::Logger.new("vagrant::util::ssh")

Class Method Summary collapse

Methods included from SafePuts

safe_puts

Class Method Details

.check_key_permissions(key_path) ⇒ Object

Checks that the permissions for a private key are valid, and fixes them if possible. SSH requires that permissions on the private key are 0600 on POSIX based systems. This will make a best effort to fix these permissions if they are not properly set.

Parameters:

  • key_path (Pathname)

    The path to the private key.


28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/vagrant/util/ssh.rb', line 28

def self.check_key_permissions(key_path)
  # Don't do anything if we're on Windows, since Windows doesn't worry
  # about key permissions.
  return if Platform.windows? || Platform.wsl_windows_access_bypass?(key_path)

  LOGGER.debug("Checking key permissions: #{key_path}")
  stat = key_path.stat

  if !stat.owned? && Process.uid != 0
    # The SSH key must be owned by ourselves, unless we're root
    raise Errors::SSHKeyBadOwner, key_path: key_path
  end

  if FileMode.from_octal(stat.mode) != "600"
    LOGGER.info("Attempting to correct key permissions to 0600")
    key_path.chmod(0600)

    # Re-stat the file to get the new mode, and verify it worked
    stat = key_path.stat
    if FileMode.from_octal(stat.mode) != "600"
      raise Errors::SSHKeyBadPermissions, key_path: key_path
    end
  end
rescue Errno::EPERM
  # This shouldn't happen since we verify we own the file, but
  # it is possible in theory, so we raise an error.
  raise Errors::SSHKeyBadPermissions, key_path: key_path
end

.exec(ssh_info, opts = {}) ⇒ Object

Halts the running of this process and replaces it with a full-fledged SSH shell into a remote machine.

Note: This method NEVER returns. The process ends after this.

Parameters:

  • ssh_info (Hash)

    This is the SSH information. For the keys required please see the documentation of Machine#ssh_info.

  • opts (Hash) (defaults to: {})

    These are additional options that are supported by exec.


66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/vagrant/util/ssh.rb', line 66

def self.exec(ssh_info, opts={})
  # 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!

  # First try using the original path provided
  if ENV["VAGRANT_PREFER_SYSTEM_BIN"] != "0"
    ssh_path = Which.which("ssh", original_path: true)
  end

  # If we didn't find an ssh executable, see if we shipped one
  if !ssh_path
    ssh_path = Which.which("ssh")
    if ssh_path && Platform.windows? && (Platform.cygwin? || Platform.msys?)
      LOGGER.warn("Failed to locate native SSH executable. Using vendored version.")
      LOGGER.warn("If display issues are encountered, install the ssh package for your environment.")
    end
  end

  if !ssh_path
    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].join(", ")
    end

    raise Errors::SSHUnavailable
  end

  if Platform.windows?
    # On Windows, we need to detect whether SSH is actually "plink"
    # underneath the covers. In this case, we tell the user.
    r = Subprocess.execute(ssh_path)
    if r.stdout.include?("PuTTY Link") || r.stdout.include?("Plink: command-line connection utility")
      raise Errors::SSHIsPuttyLink,
        host: ssh_info[:host],
        port: ssh_info[:port],
        username: ssh_info[:username],
        key_path: ssh_info[:private_key_path].join(", ")
    end
  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]

  log_level = ssh_info[:log_level] || "FATAL"

  # Command line options
  command_options = [
    "-p", options[:port].to_s,
    "-o", "LogLevel=#{log_level}"]

  if ssh_info[:compression]
    command_options += ["-o", "Compression=yes"]
  end

  if ssh_info[:dsa_authentication]
    command_options += ["-o", "DSAAuthentication=yes"]
  end

  # Solaris/OpenSolaris/Illumos uses SunSSH which doesn't support the
  # IdentitiesOnly option. Also, we don't enable it in plain mode or if
  # if keys_only is false so that SSH and Net::SSH properly search our identities
  # and tries to do it itself.
  if !Platform.solaris? && !plain_mode && ssh_info[:keys_only]
    command_options += ["-o", "IdentitiesOnly=yes"]
  end

  # no strict hostkey checking unless paranoid
  if ssh_info[:verify_host_key] == :never || !ssh_info[:verify_host_key]
    command_options += [
      "-o", "StrictHostKeyChecking=no",
      "-o", "UserKnownHostsFile=/dev/null"]
  end

  # If we're not in plain mode and :private_key_path is set attach the private key path(s).
  if !plain_mode && options[:private_key_path]
    options[:private_key_path].each do |path|

      private_key_arr = []

      if path.include?('%')
        if path.include?(' ') && Platform.windows?
          LOGGER.warn("Paths with spaces and % on windows is not supported and will fail to read the file")
        end
        # Use '-o' instead of '-i' because '-i' does not call
        # percent_expand in misc.c, but '-o' does. when passing the path,
        # replace '%' in the path with '%%' to escape the '%'
        path = path.to_s.gsub('%', '%%')
        private_key_arr = ["-o", "IdentityFile=\"#{path}\""]
      else
        # Pass private key file directly with '-i', which properly supports
        # paths with spaces on Windows guests
        private_key_arr = ["-i", path]
      end

      command_options += private_key_arr
    end
  end

  if ssh_info[:forward_x11]
    # Both are required so that no warnings are shown regarding X11
    command_options += [
      "-o", "ForwardX11=yes",
      "-o", "ForwardX11Trusted=yes"]
  end

  if ssh_info[:config]
    command_options += ["-F", ssh_info[:config]]
  end

  if ssh_info[:proxy_command]
    command_options += ["-o", "ProxyCommand=#{ssh_info[:proxy_command]}"]
  end

  if ssh_info[:forward_env]
    command_options += ["-o", "SendEnv=#{ssh_info[:forward_env].join(" ")}"]
  end

  # Configurables -- extra_args should always be last due to the way the
  # ssh args parser works. e.g. if the user wants to use the -t option,
  # any shell command(s) she'd like to run on the remote server would
  # have to be the last part of the 'ssh' command:
  #
  #   $ ssh localhost -t -p 2222 "cd mydirectory; bash"
  #
  # Without having extra_args be last, the user loses this ability
  command_options += ["-o", "ForwardAgent=yes"] if ssh_info[:forward_agent]

  # Note about :extra_args
  #   ssh_info[:extra_args] comes from a machines ssh config in a Vagrantfile,
  #   where as opts[:extra_args] comes from running the ssh command
  command_options += Array(ssh_info[:extra_args]) if ssh_info[:extra_args]

  command_options.concat(opts[:extra_args]) if opts[:extra_args]

  # Build up the host string for connecting
  host_string = options[:host]
  host_string = "#{options[:username]}@#{host_string}" if !plain_mode
  command_options.unshift(host_string)

  # On Cygwin we want to get rid of any DOS file warnings because
  # we really don't care since both work.
  ENV["nodosfilewarning"] = "1" if Platform.cygwin?

  # If an ssh command is defined, use that. If an ssh binary was
  # discovered on the path, use that. Otherwise fail to just trying `ssh`
  ssh = ssh_info[:ssh_command] || ssh_path || 'ssh'

  # Invoke SSH with all our options
  if !opts[:subprocess]
    LOGGER.info("Invoking SSH: #{ssh} #{command_options.inspect}")
    SafeExec.exec(ssh, *command_options)
    return
  end

  # If we're still here, it means we're supposed to subprocess
  # out to ssh rather than exec it.
  LOGGER.info("Executing SSH in subprocess: #{ssh} #{command_options.inspect}")
  process = ChildProcess.build(ssh, *command_options)
  process.io.inherit!

  # Forward configured environment variables.
  if ssh_info[:forward_env]
    ssh_info[:forward_env].each do |key|
      process.environment[key] = ENV[key]
    end
  end

  process.start
  process.wait
  return process.exit_code
end