Class: Aspera::Ssh

Inherits:
Object
  • Object
show all
Defined in:
lib/aspera/ssh.rb

Overview

A simple wrapper around Net::SSH executes one command and get its result from stdout

Defined Under Namespace

Classes: Error

Constant Summary collapse

EXCLUDE_ECDSHA2 =

Regexp matching ecdsa/ecdh-sha2 algorithm names (JRuby workaround)

/^ecd(sa|h)-sha2/

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(host, username, ssh_options) ⇒ Ssh

Returns a new instance of Ssh.

Parameters:

  • host (String) —

    remote server address

  • username (String) —

    SSH user name

  • ssh_options (Hash{Symbol => Object}) —

    options forwarded to Net::SSH.start (see Net::SSH.start). Defaults are injected for :logger, :verbose, and :use_agent if absent.



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/aspera/ssh.rb', line 61

def initialize(host, username, ssh_options)
  Aspera.assert_type(host, String)
  Aspera.assert_type(username, String)
  Aspera.assert_type(ssh_options, Hash)
  Aspera.assert_hash_all(ssh_options, Symbol, nil)
  @host = host
  @username = username
  @ssh_options = ssh_options.dup
  @ssh_options[:logger] = Log.log unless @ssh_options.key?(:logger)
  @ssh_options[:verbose] = :warn unless @ssh_options.key?(:verbose)
  # @ssh_options[:verbose] = :debug
  @ssh_options[:use_agent] = false unless @ssh_options.key?(:use_agent)
  Log.log.debug { "ssh:#{@username}@#{@host}" }
  Log.dump(:ssh_options, @ssh_options)
end

Class Method Details

.disable_ecd_sha2_algorithms ⇒ void

This method returns an undefined value.

Mutates Net::SSH internal algorithm lists to remove ecdsa/ecdh-sha2 entries globally. Kept for backwards compatibility and JRuby usage; prefer no_ecd_sha2_options for new code.



50
51
52
53
54
# File 'lib/aspera/ssh.rb', line 50

def disable_ecd_sha2_algorithms
  Log.log.debug('Disabling SSH ecdsa (global)')
  Net::SSH::Transport::Algorithms::ALGORITHMS.each_value { |a| a.reject! { |a| a.match?(EXCLUDE_ECDSHA2) } }
  Net::SSH::KnownHosts::SUPPORTED_TYPE.reject! { |t| t.match?(EXCLUDE_ECDSHA2) }
end

.disable_ed25519_keys ⇒ void

This method returns an undefined value.

Removes ed25519 keys from the default identity list used by Net::SSH. Called when the ed25519 gem is absent or explicitly disabled via ASCLI_ENABLE_ED25519=false.



22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/aspera/ssh.rb', line 22

def disable_ed25519_keys
  Log.log.debug('Disabling SSH ed25519 user keys')
  old_verbose = $VERBOSE
  $VERBOSE = nil
  Net::SSH::Authentication::Session.class_eval do
    define_method(:default_keys) do
      %w[.ssh .ssh2].product(%w[rsa dsa ecdsa]).map { "~/#{_1}/id_#{_2}" }.freeze
    end
    private(:default_keys)
  end
  $VERBOSE = old_verbose
end

.no_ecd_sha2_options ⇒ Hash{Symbol => Array<String>}

Returns Net::SSH option overrides that exclude all ecdsa/ecdh-sha2 algorithms. Merge the result into ssh_options passed to #initialize to avoid mutating Net::SSH internal constants.

Returns:

  • (Hash{Symbol => Array<String>}) —

    :host_key and :kex filtered lists



39
40
41
42
43
44
45
# File 'lib/aspera/ssh.rb', line 39

def no_ecd_sha2_options
  Log.log.debug('Building SSH options without ecdsa/ecdh-sha2')
  {
    host_key: Net::SSH::Transport::Algorithms::ALGORITHMS[:host_key].reject { |a| a.match?(EXCLUDE_ECDSHA2) },
    kex:      Net::SSH::Transport::Algorithms::ALGORITHMS[:kex].reject {      |a| a.match?(EXCLUDE_ECDSHA2) }
  }
end

Instance Method Details

#execute(cmd, input: nil) ⇒ String

Executes a single command on the remote host over a new SSH session.

Parameters:

  • cmd (String) —

    shell command to execute remotely

  • input (String, nil) (defaults to: nil) —

    data written to the command's stdin, or nil

Returns:

  • (String) —

    concatenated stdout of the remote command

Raises:

  • (Error) —

    if the channel cannot be opened or the remote command exits with a non-zero status



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
# File 'lib/aspera/ssh.rb', line 82

def execute(cmd, input: nil)
  Aspera.assert_type(cmd, String)
  Log.log.debug { "cmd=#{cmd}" }
  # @type response [Array<String>]
  response = []
  # @type error [Array<String>]
  error = []
  exit_code = nil
  # @param session [Net::SSH::Connection::Session]
  Net::SSH.start(@host, @username, @ssh_options) do |session|
    # @param channel [Net::SSH::Connection::Channel]
    session.open_channel do |channel|
      # Register stdout/stderr before exec so no data is missed (e.g. ForceCommand errors)
      channel.on_data { |_ch, data| response.push(data) }
      channel.on_extended_data { |_ch, type, data| error.push(data) if type.eql?(1) }
      # @param data [Net::SSH::Buffer]
      channel.on_request('exit-status') do |_channel, data|
        Log.dump(:data, data, level: :trace1)
        exit_code = data.read_long
      end
      # send command to SSH channel (execute) cspell: disable-next-line
      channel.send('cexe'.reverse, cmd) do |_ch, success|
        raise Error, "could not execute command: #{cmd}" unless success
        channel.send_data(input) unless input.nil?
      end
    end
    # wait for channel to finish and flush the session
    session.loop
  end
  error_text = error.join
  hint = error_text.include?('Could not chdir to home directory') ? "\nHint: home not created in Windows?" : ''
  raise Error, "#{cmd}: exit #{exit_code}, #{error_text.chomp}#{hint}" if exit_code&.nonzero?
  Log.log.error { "#{error_text}#{hint}" } unless error_text.empty?
  # response as single string
  return response.join
end