lib/io_streams/paths/sftp.rb in iostreams-1.6.2 vs lib/io_streams/paths/sftp.rb in iostreams-1.7.0

- old
+ new

@@ -45,13 +45,27 @@ # Name of user to login with. # # password: [String] # Password for the user. # - # **ssh_options - # Any other options supported by ssh_config. - # `man ssh_config` to see all available options. + # ssh_options: [Hash] + # - IdentityKey [String] + # The identity key that this client should use to talk to this host. + # Under the covers this value is written to a file and then the file name is passed as `IdentityFile` + # - HostKey [String] + # The expected SSH Host key that is presented by the remote host. + # Instead of storing the host key in the `known_hosts` file, it can be supplied explicity + # using this option. + # Under the covers this value is written to a file and then the file name is passed as `UserKnownHostsFile` + # Notes: + # - It must contain the entire line that would be stored in `known_hosts`, + # including the hostname, ip address, key type and key value. This value is written as-is into a + # "known_hosts" like file and then passed into sftp using the `UserKnownHostsFile` option. + # - The easiest way to generate the required is to use `ssh-keyscan` and then supply that value in this field. + # For example: `ssh-keyscan hostname` + # - Any other options supported by ssh_config. + # `man ssh_config` to see all available options. # # Examples: # # # Display the contents of a remote file # IOStreams.path("sftp://test.com/path/file_name.csv", username: "jack", password: "OpenSesame").reader do |io| @@ -166,91 +180,105 @@ # Use sftp and sshpass executables to download to a local file def sftp_download(remote_file_name, local_file_name) with_sftp_args do |args| Open3.popen2e(*args) do |writer, reader, waith_thr| - begin - # Give time for remote sftp server to get ready to accept the password. - sleep self.class.before_password_wait_seconds + # Give time for remote sftp server to get ready to accept the password. + sleep self.class.before_password_wait_seconds - writer.puts password + writer.puts password - # Give time for password to be processed and stdin to be passed to sftp process. - sleep self.class.sshpass_wait_seconds + # Give time for password to be processed and stdin to be passed to sftp process. + sleep self.class.sshpass_wait_seconds - writer.puts "get #{remote_file_name} #{local_file_name}" - writer.puts "bye" - writer.close - out = reader.read.chomp - unless waith_thr.value.success? - raise( - Errors::CommunicationsFailure, - "Download failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}" - ) - end - - out - rescue Errno::EPIPE - out = begin - reader.read.chomp - rescue StandardError - nil - end + writer.puts "get #{remote_file_name} #{local_file_name}" + writer.puts "bye" + writer.close + out = reader.read.chomp + unless waith_thr.value.success? raise( Errors::CommunicationsFailure, "Download failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}" ) end + + out + rescue Errno::EPIPE + out = begin + reader.read.chomp + rescue StandardError + nil + end + raise( + Errors::CommunicationsFailure, + "Download failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}" + ) end end end def sftp_upload(local_file_name, remote_file_name) with_sftp_args do |args| Open3.popen2e(*args) do |writer, reader, waith_thr| - begin - writer.puts(password) if password - # Give time for password to be processed and stdin to be passed to sftp process. - sleep self.class.sshpass_wait_seconds - writer.puts "put #{local_file_name.inspect} #{remote_file_name.inspect}" - writer.puts "bye" - writer.close - out = reader.read.chomp - unless waith_thr.value.success? - raise( - Errors::CommunicationsFailure, - "Upload failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}" - ) - end - - out - rescue Errno::EPIPE - out = begin - reader.read.chomp - rescue StandardError - nil - end + writer.puts(password) if password + # Give time for password to be processed and stdin to be passed to sftp process. + sleep self.class.sshpass_wait_seconds + writer.puts "put #{local_file_name.inspect} #{remote_file_name.inspect}" + writer.puts "bye" + writer.close + out = reader.read.chomp + unless waith_thr.value.success? raise( Errors::CommunicationsFailure, "Upload failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}" ) end + + out + rescue Errno::EPIPE + out = begin + reader.read.chomp + rescue StandardError + nil + end + raise( + Errors::CommunicationsFailure, + "Upload failed calling #{self.class.sftp_bin} via #{self.class.sshpass_bin}: #{out}" + ) end end end def with_sftp_args - return yield sftp_args(ssh_options) unless ssh_options.key?("IdentityKey") + return yield sftp_args(ssh_options) if !ssh_options.key?("IdentityKey") && !ssh_options.key?("HostKey") + with_identity_key(ssh_options.dup) do |options| + with_host_key(options) do |options2| + yield sftp_args(options2) + end + end + end + + def with_identity_key(options) + return yield options unless ssh_options.key?("IdentityKey") + + with_temp_file(options, "IdentityFile", options.delete("IdentityKey")) { yield options } + end + + def with_host_key(options) + return yield options unless ssh_options.key?("HostKey") + + with_temp_file(options, "UserKnownHostsFile", options.delete("HostKey")) { yield options } + end + + def with_temp_file(options, option, value) Utils.temp_file_name("iostreams-sftp-args", "key") do |file_name| - options = ssh_options.dup - key = options.delete("IdentityKey") # sftp requires that private key is only readable by the current user - ::File.open(file_name, "wb", 0o600) { |io| io.write(key) } + ::File.open(file_name, "wb", 0o600) { |io| io.write(value) } - options["IdentityFile"] = file_name - yield sftp_args(options) + options[option] = file_name + yield options end end def sftp_args(ssh_options) args = [self.class.sshpass_bin, self.class.sftp_bin] @@ -275,10 +303,10 @@ args << "#{username}@#{hostname}" args end def build_ssh_options - options = ssh_options.dup + options = ssh_options.dup options[:logger] ||= logger if defined?(SemanticLogger) options[:port] ||= port options[:max_pkt_size] ||= 65_536 options[:password] ||= @password options