aboutsummaryrefslogtreecommitdiff
path: root/vendor/rsync_command/lib/rsync_command/ssh_options.rb
blob: 3cc908fe98cf0f543c2d3cb09d4e81f5b373a803 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
56
57
58
59
60
61
62
63
64
65
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
#
# Converts capistrano-style ssh configuration (which uses Net::SSH) into a OpenSSH command line flags suitable for rsync.
#
# For a list of the options normally support by Net::SSH (and thus Capistrano), see
# http://net-ssh.github.com/net-ssh/classes/Net/SSH.html#method-c-start
#
# Also, to see how Net::SSH does the opposite of the conversion we are doing here, check out:
# https://github.com/net-ssh/net-ssh/blob/master/lib/net/ssh/config.rb
#
# API mismatch:
#
# * many OpenSSH options not supported
# * some options only make sense for Net::SSH
# * compression: for Net::SSH, this option is supposed to accept true, false, or algorithm. OpenSSH accepts 'yes' or 'no'
#
class RsyncCommand
  class SshOptions

    def initialize(options={})
      @options = parse_options(options)
    end

    def to_flags
      if @options.empty?
        nil
      else
        %[-e "ssh #{@options.join(' ')}"]
      end
    end

    private

    def parse_options(options)
      options.map do |key, value|
        next if value.nil?
        # Convert Net::SSH options into OpenSSH options.
        case key
        when :auth_methods            then opt_auth_methods(value)
        when :bind_address            then opt('BindAddress', value)
        when :compression             then opt('Compression', value ? 'yes' : 'no')
        when :compression_level       then opt('CompressionLevel', value.to_i)
        when :config                  then value ? "-F '#{value}'" : nil
        when :encryption              then opt('Ciphers', [value].flatten.join(','))
        when :forward_agent           then opt('ForwardAgent', value)
        when :global_known_hosts_file then opt('GlobalKnownHostsFile', value)
        when :hmac                    then opt('MACs', [value].flatten.join(','))
        when :host_key                then opt('HostKeyAlgorithms', [value].flatten.join(','))
        when :host_key_alias          then opt('HostKeyAlias', value)
        when :host_name               then opt('HostName', value)
        when :kex                     then opt('KexAlgorithms', [value].flatten.join(','))
        when :key_data                then nil # not supported
        when :keys                    then [value].flatten.select { |k| File.exist?(k) }.map { |k| "-i '#{k}'" }
        when :keys_only               then opt('IdentitiesOnly', value ? 'yes' : 'no')
        when :languages               then nil # not applicable
        when :logger                  then nil # not applicable
        when :paranoid                then opt('StrictHostKeyChecking', value ? 'yes' : 'no')
        when :passphrase              then nil # not supported
        when :password                then nil # not supported
        when :port                    then "-p #{value.to_i}"
        when :properties              then nil # not applicable
        when :proxy                   then nil # not applicable
        when :rekey_blocks_limit      then nil # not supported
        when :rekey_limit             then opt('RekeyLimit', reverse_interpret_size(value))
        when :rekey_packet_limit      then nil # not supported
        when :timeout                 then opt('ConnectTimeout', value.to_i)
        when :user                    then "-l #{value}"
        when :user_known_hosts_file   then multi_opt('UserKnownHostsFile', value)
        when :verbose                 then opt('LogLevel', interpret_log_level(value))
        end
      end.compact
    end

    private

    def opt(option_name, option_value)
      "-o #{option_name}='#{option_value}'"
    end

    def multi_opt(option_name, option_values)
      [option_values].flatten.map do |value|
        opt(option_name, value)
      end.join(' ')
    end

    #
    # In OpenSSH, password and pubkey default to 'yes', hostbased defaults to 'no'.
    # Regardless, if :auth_method is configured, then we explicitly set the auth method.
    #
    def opt_auth_methods(value)
      value = [value].flatten
      opts = []
      if value.any?
        if value.include? 'password'
          opts << opt('PasswordAuthentication', 'yes')
        else
          opts << opt('PasswordAuthentication', 'no')
        end
        if value.include? 'publickey'
          opts << opt('PubkeyAuthentication', 'yes')
        else
          opts << opt('PubkeyAuthentication', 'no')
        end
        if value.include? 'hostbased'
          opts << opt('HostbasedAuthentication', 'yes')
        else
          opts << opt('HostbasedAuthentication', 'no')
        end
      end
      if opts.any?
        return opts.join(' ')
      else
        nil
      end
    end

    #
    # Converts the given integer size in bytes into a string with 'K', 'M', 'G' suffix, as appropriate.
    #
    # reverse of interpret_size in https://github.com/net-ssh/net-ssh/blob/master/lib/net/ssh/config.rb
    #
    def reverse_interpret_size(size)
      size = size.to_i
      if size < 1024
        "#{size}"
      elsif size < 1024 * 1024
        "#{size/1024}K"
      elsif size < 1024 * 1024 * 1024
        "#{size/(1024*1024)}M"
      else
        "#{size/(1024*1024*1024)}G"
      end
    end

    def interpret_log_level(level)
      if level.is_a? Symbol
        case level
          when :debug then "DEBUG"
          when :info then "INFO"
          when :warn then "ERROR"
          when :error then "ERROR"
          when :fatal then "FATAL"
          else "INFO"
        end
      elsif level.is_a?(Integer) && defined?(Logger)
        case level
          when Logger::DEBUG then "DEBUG"
          when Logger::INFO then "INFO"
          when Logger::WARN then "ERROR"
          when Logger::ERROR then "ERROR"
          when Logger::FATAL then "FATAL"
          else "INFO"
        end
      else
        "INFO"
      end
    end

  end
end