net-ssh 3.2.0 → 7.2.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +0 -0
  3. data/.dockerignore +6 -0
  4. data/.github/FUNDING.yml +1 -0
  5. data/.github/config/rubocop_linter_action.yml +4 -0
  6. data/.github/workflows/ci-with-docker.yml +44 -0
  7. data/.github/workflows/ci.yml +93 -0
  8. data/.github/workflows/rubocop.yml +16 -0
  9. data/.gitignore +13 -0
  10. data/.rubocop.yml +22 -0
  11. data/.rubocop_todo.yml +1081 -0
  12. data/CHANGES.txt +237 -7
  13. data/DEVELOPMENT.md +23 -0
  14. data/Dockerfile +27 -0
  15. data/Dockerfile.openssl3 +17 -0
  16. data/Gemfile +13 -0
  17. data/Gemfile.noed25519 +12 -0
  18. data/Gemfile.norbnacl +12 -0
  19. data/ISSUE_TEMPLATE.md +30 -0
  20. data/Manifest +4 -5
  21. data/README.md +298 -0
  22. data/Rakefile +125 -74
  23. data/SECURITY.md +4 -0
  24. data/appveyor.yml +58 -0
  25. data/docker-compose.yml +23 -0
  26. data/lib/net/ssh/authentication/agent.rb +279 -18
  27. data/lib/net/ssh/authentication/certificate.rb +183 -0
  28. data/lib/net/ssh/authentication/constants.rb +17 -15
  29. data/lib/net/ssh/authentication/ed25519.rb +186 -0
  30. data/lib/net/ssh/authentication/ed25519_loader.rb +31 -0
  31. data/lib/net/ssh/authentication/key_manager.rb +86 -39
  32. data/lib/net/ssh/authentication/methods/abstract.rb +67 -48
  33. data/lib/net/ssh/authentication/methods/hostbased.rb +34 -37
  34. data/lib/net/ssh/authentication/methods/keyboard_interactive.rb +13 -13
  35. data/lib/net/ssh/authentication/methods/none.rb +16 -19
  36. data/lib/net/ssh/authentication/methods/password.rb +27 -17
  37. data/lib/net/ssh/authentication/methods/publickey.rb +96 -55
  38. data/lib/net/ssh/authentication/pageant.rb +471 -367
  39. data/lib/net/ssh/authentication/pub_key_fingerprint.rb +43 -0
  40. data/lib/net/ssh/authentication/session.rb +131 -121
  41. data/lib/net/ssh/buffer.rb +399 -300
  42. data/lib/net/ssh/buffered_io.rb +154 -150
  43. data/lib/net/ssh/config.rb +308 -185
  44. data/lib/net/ssh/connection/channel.rb +635 -613
  45. data/lib/net/ssh/connection/constants.rb +29 -29
  46. data/lib/net/ssh/connection/event_loop.rb +123 -0
  47. data/lib/net/ssh/connection/keepalive.rb +55 -51
  48. data/lib/net/ssh/connection/session.rb +620 -551
  49. data/lib/net/ssh/connection/term.rb +125 -123
  50. data/lib/net/ssh/errors.rb +101 -99
  51. data/lib/net/ssh/key_factory.rb +197 -105
  52. data/lib/net/ssh/known_hosts.rb +214 -127
  53. data/lib/net/ssh/loggable.rb +50 -49
  54. data/lib/net/ssh/packet.rb +83 -79
  55. data/lib/net/ssh/prompt.rb +50 -81
  56. data/lib/net/ssh/proxy/command.rb +105 -90
  57. data/lib/net/ssh/proxy/errors.rb +12 -10
  58. data/lib/net/ssh/proxy/http.rb +82 -79
  59. data/lib/net/ssh/proxy/https.rb +50 -0
  60. data/lib/net/ssh/proxy/jump.rb +54 -0
  61. data/lib/net/ssh/proxy/socks4.rb +2 -6
  62. data/lib/net/ssh/proxy/socks5.rb +14 -17
  63. data/lib/net/ssh/service/forward.rb +370 -317
  64. data/lib/net/ssh/test/channel.rb +145 -136
  65. data/lib/net/ssh/test/extensions.rb +131 -110
  66. data/lib/net/ssh/test/kex.rb +34 -32
  67. data/lib/net/ssh/test/local_packet.rb +46 -44
  68. data/lib/net/ssh/test/packet.rb +89 -70
  69. data/lib/net/ssh/test/remote_packet.rb +32 -30
  70. data/lib/net/ssh/test/script.rb +156 -142
  71. data/lib/net/ssh/test/socket.rb +49 -48
  72. data/lib/net/ssh/test.rb +82 -77
  73. data/lib/net/ssh/transport/algorithms.rb +462 -359
  74. data/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +117 -0
  75. data/lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb +17 -0
  76. data/lib/net/ssh/transport/cipher_factory.rb +122 -99
  77. data/lib/net/ssh/transport/constants.rb +32 -24
  78. data/lib/net/ssh/transport/ctr.rb +42 -22
  79. data/lib/net/ssh/transport/hmac/abstract.rb +81 -63
  80. data/lib/net/ssh/transport/hmac/md5.rb +0 -2
  81. data/lib/net/ssh/transport/hmac/md5_96.rb +0 -2
  82. data/lib/net/ssh/transport/hmac/none.rb +0 -2
  83. data/lib/net/ssh/transport/hmac/ripemd160.rb +0 -2
  84. data/lib/net/ssh/transport/hmac/sha1.rb +0 -2
  85. data/lib/net/ssh/transport/hmac/sha1_96.rb +0 -2
  86. data/lib/net/ssh/transport/hmac/sha2_256.rb +7 -11
  87. data/lib/net/ssh/transport/hmac/sha2_256_96.rb +4 -8
  88. data/lib/net/ssh/transport/hmac/sha2_256_etm.rb +12 -0
  89. data/lib/net/ssh/transport/hmac/sha2_512.rb +6 -9
  90. data/lib/net/ssh/transport/hmac/sha2_512_96.rb +4 -8
  91. data/lib/net/ssh/transport/hmac/sha2_512_etm.rb +12 -0
  92. data/lib/net/ssh/transport/hmac.rb +14 -12
  93. data/lib/net/ssh/transport/identity_cipher.rb +54 -44
  94. data/lib/net/ssh/transport/kex/abstract.rb +130 -0
  95. data/lib/net/ssh/transport/kex/abstract5656.rb +72 -0
  96. data/lib/net/ssh/transport/kex/curve25519_sha256.rb +39 -0
  97. data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +30 -0
  98. data/lib/net/ssh/transport/kex/diffie_hellman_group14_sha1.rb +33 -40
  99. data/lib/net/ssh/transport/kex/diffie_hellman_group14_sha256.rb +11 -0
  100. data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +119 -213
  101. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +53 -61
  102. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha256.rb +5 -9
  103. data/lib/net/ssh/transport/kex/ecdh_sha2_nistp256.rb +36 -90
  104. data/lib/net/ssh/transport/kex/ecdh_sha2_nistp384.rb +18 -10
  105. data/lib/net/ssh/transport/kex/ecdh_sha2_nistp521.rb +18 -10
  106. data/lib/net/ssh/transport/kex.rb +15 -12
  107. data/lib/net/ssh/transport/key_expander.rb +24 -20
  108. data/lib/net/ssh/transport/openssl.rb +161 -124
  109. data/lib/net/ssh/transport/openssl_cipher_extensions.rb +8 -0
  110. data/lib/net/ssh/transport/packet_stream.rb +246 -185
  111. data/lib/net/ssh/transport/server_version.rb +55 -56
  112. data/lib/net/ssh/transport/session.rb +306 -255
  113. data/lib/net/ssh/transport/state.rb +178 -176
  114. data/lib/net/ssh/verifiers/accept_new.rb +33 -0
  115. data/lib/net/ssh/verifiers/accept_new_or_local_tunnel.rb +33 -0
  116. data/lib/net/ssh/verifiers/always.rb +58 -0
  117. data/lib/net/ssh/verifiers/never.rb +19 -0
  118. data/lib/net/ssh/version.rb +55 -53
  119. data/lib/net/ssh.rb +111 -47
  120. data/net-ssh-public_cert.pem +18 -18
  121. data/net-ssh.gemspec +38 -205
  122. data/support/ssh_tunnel_bug.rb +5 -5
  123. data.tar.gz.sig +0 -0
  124. metadata +173 -118
  125. metadata.gz.sig +0 -0
  126. data/.travis.yml +0 -18
  127. data/README.rdoc +0 -182
  128. data/lib/net/ssh/authentication/agent/java_pageant.rb +0 -85
  129. data/lib/net/ssh/authentication/agent/socket.rb +0 -178
  130. data/lib/net/ssh/ruby_compat.rb +0 -46
  131. data/lib/net/ssh/verifiers/lenient.rb +0 -30
  132. data/lib/net/ssh/verifiers/null.rb +0 -12
  133. data/lib/net/ssh/verifiers/secure.rb +0 -52
  134. data/lib/net/ssh/verifiers/strict.rb +0 -24
  135. data/setup.rb +0 -1585
  136. data/support/arcfour_check.rb +0 -20
  137. data/test/README.txt +0 -18
  138. data/test/authentication/methods/common.rb +0 -28
  139. data/test/authentication/methods/test_abstract.rb +0 -51
  140. data/test/authentication/methods/test_hostbased.rb +0 -114
  141. data/test/authentication/methods/test_keyboard_interactive.rb +0 -121
  142. data/test/authentication/methods/test_none.rb +0 -41
  143. data/test/authentication/methods/test_password.rb +0 -95
  144. data/test/authentication/methods/test_publickey.rb +0 -148
  145. data/test/authentication/test_agent.rb +0 -232
  146. data/test/authentication/test_key_manager.rb +0 -240
  147. data/test/authentication/test_session.rb +0 -107
  148. data/test/common.rb +0 -125
  149. data/test/configs/auth_off +0 -5
  150. data/test/configs/auth_on +0 -4
  151. data/test/configs/empty +0 -0
  152. data/test/configs/eqsign +0 -3
  153. data/test/configs/exact_match +0 -8
  154. data/test/configs/host_plus +0 -10
  155. data/test/configs/multihost +0 -4
  156. data/test/configs/negative_match +0 -6
  157. data/test/configs/nohost +0 -19
  158. data/test/configs/numeric_host +0 -4
  159. data/test/configs/proxy_remote_user +0 -2
  160. data/test/configs/send_env +0 -2
  161. data/test/configs/substitutes +0 -8
  162. data/test/configs/wild_cards +0 -14
  163. data/test/connection/test_channel.rb +0 -487
  164. data/test/connection/test_session.rb +0 -564
  165. data/test/integration/README.txt +0 -17
  166. data/test/integration/Vagrantfile +0 -12
  167. data/test/integration/common.rb +0 -63
  168. data/test/integration/playbook.yml +0 -56
  169. data/test/integration/test_forward.rb +0 -637
  170. data/test/integration/test_id_rsa_keys.rb +0 -96
  171. data/test/integration/test_proxy.rb +0 -93
  172. data/test/known_hosts/github +0 -1
  173. data/test/known_hosts/github_hash +0 -1
  174. data/test/manual/test_pageant.rb +0 -37
  175. data/test/start/test_connection.rb +0 -53
  176. data/test/start/test_options.rb +0 -57
  177. data/test/start/test_transport.rb +0 -28
  178. data/test/start/test_user_nil.rb +0 -27
  179. data/test/test_all.rb +0 -12
  180. data/test/test_buffer.rb +0 -433
  181. data/test/test_buffered_io.rb +0 -63
  182. data/test/test_config.rb +0 -268
  183. data/test/test_key_factory.rb +0 -191
  184. data/test/test_known_hosts.rb +0 -66
  185. data/test/transport/hmac/test_md5.rb +0 -41
  186. data/test/transport/hmac/test_md5_96.rb +0 -27
  187. data/test/transport/hmac/test_none.rb +0 -34
  188. data/test/transport/hmac/test_ripemd160.rb +0 -36
  189. data/test/transport/hmac/test_sha1.rb +0 -36
  190. data/test/transport/hmac/test_sha1_96.rb +0 -27
  191. data/test/transport/hmac/test_sha2_256.rb +0 -37
  192. data/test/transport/hmac/test_sha2_256_96.rb +0 -27
  193. data/test/transport/hmac/test_sha2_512.rb +0 -37
  194. data/test/transport/hmac/test_sha2_512_96.rb +0 -27
  195. data/test/transport/kex/test_diffie_hellman_group14_sha1.rb +0 -13
  196. data/test/transport/kex/test_diffie_hellman_group1_sha1.rb +0 -150
  197. data/test/transport/kex/test_diffie_hellman_group_exchange_sha1.rb +0 -96
  198. data/test/transport/kex/test_diffie_hellman_group_exchange_sha256.rb +0 -19
  199. data/test/transport/kex/test_ecdh_sha2_nistp256.rb +0 -161
  200. data/test/transport/kex/test_ecdh_sha2_nistp384.rb +0 -38
  201. data/test/transport/kex/test_ecdh_sha2_nistp521.rb +0 -38
  202. data/test/transport/test_algorithms.rb +0 -328
  203. data/test/transport/test_cipher_factory.rb +0 -443
  204. data/test/transport/test_hmac.rb +0 -34
  205. data/test/transport/test_identity_cipher.rb +0 -40
  206. data/test/transport/test_packet_stream.rb +0 -1762
  207. data/test/transport/test_server_version.rb +0 -74
  208. data/test/transport/test_session.rb +0 -331
  209. data/test/transport/test_state.rb +0 -181
  210. data/test/verifiers/test_secure.rb +0 -40
@@ -1,251 +1,323 @@
1
- module Net; module SSH
2
-
3
- # The Net::SSH::Config class is used to parse OpenSSH configuration files,
4
- # and translates that syntax into the configuration syntax that Net::SSH
5
- # understands. This lets Net::SSH scripts read their configuration (to
6
- # some extent) from OpenSSH configuration files (~/.ssh/config, /etc/ssh_config,
7
- # and so forth).
8
- #
9
- # Only a subset of OpenSSH configuration options are understood:
10
- #
11
- # * ChallengeResponseAuthentication => maps to the :auth_methods option challenge-response (then coleasced into keyboard-interactive)
12
- # * KbdInteractiveAuthentication => maps to the :auth_methods keyboard-interactive
13
- # * Ciphers => maps to the :encryption option
14
- # * Compression => :compression
15
- # * CompressionLevel => :compression_level
16
- # * ConnectTimeout => maps to the :timeout option
17
- # * ForwardAgent => :forward_agent
18
- # * GlobalKnownHostsFile => :global_known_hosts_file
19
- # * HostBasedAuthentication => maps to the :auth_methods option
20
- # * HostKeyAlgorithms => maps to :host_key option
21
- # * HostKeyAlias => :host_key_alias
22
- # * HostName => :host_name
23
- # * IdentityFile => maps to the :keys option
24
- # * IdentitiesOnly => :keys_only
25
- # * Macs => maps to the :hmac option
26
- # * PasswordAuthentication => maps to the :auth_methods option password
27
- # * Port => :port
28
- # * PreferredAuthentications => maps to the :auth_methods option
29
- # * ProxyCommand => maps to the :proxy option
30
- # * PubKeyAuthentication => maps to the :auth_methods option
31
- # * RekeyLimit => :rekey_limit
32
- # * User => :user
33
- # * UserKnownHostsFile => :user_known_hosts_file
34
- # * NumberOfPasswordPrompts => :number_of_password_prompts
35
- #
36
- # Note that you will never need to use this class directly--you can control
37
- # whether the OpenSSH configuration files are read by passing the :config
38
- # option to Net::SSH.start. (They are, by default.)
39
- class Config
40
- class << self
41
- @@default_files = %w(~/.ssh/config /etc/ssh_config /etc/ssh/ssh_config)
42
- # The following defaults follow the openssh client ssh_config defaults.
43
- # http://lwn.net/Articles/544640/
44
- # "hostbased" is off and "none" is not supported but we allow it since
45
- # it's used by some clients to query the server for allowed auth methods
46
- @@default_auth_methods = %w(none publickey password keyboard-interactive)
47
-
48
- # Returns an array of locations of OpenSSH configuration files
49
- # to parse by default.
50
- def default_files
51
- @@default_files
52
- end
1
+ module Net
2
+ module SSH
3
+ # The Net::SSH::Config class is used to parse OpenSSH configuration files,
4
+ # and translates that syntax into the configuration syntax that Net::SSH
5
+ # understands. This lets Net::SSH scripts read their configuration (to
6
+ # some extent) from OpenSSH configuration files (~/.ssh/config, /etc/ssh_config,
7
+ # and so forth).
8
+ #
9
+ # Only a subset of OpenSSH configuration options are understood:
10
+ #
11
+ # * ChallengeResponseAuthentication => maps to the :auth_methods option challenge-response (then coleasced into keyboard-interactive)
12
+ # * KbdInteractiveAuthentication => maps to the :auth_methods keyboard-interactive
13
+ # * CertificateFile => maps to the :keycerts option
14
+ # * Ciphers => maps to the :encryption option
15
+ # * Compression => :compression
16
+ # * CompressionLevel => :compression_level
17
+ # * ConnectTimeout => maps to the :timeout option
18
+ # * ForwardAgent => :forward_agent
19
+ # * GlobalKnownHostsFile => :global_known_hosts_file
20
+ # * HostBasedAuthentication => maps to the :auth_methods option
21
+ # * HostKeyAlgorithms => maps to :host_key option
22
+ # * HostKeyAlias => :host_key_alias
23
+ # * HostName => :host_name
24
+ # * IdentityFile => maps to the :keys option
25
+ # * IdentityAgent => :identity_agent
26
+ # * IdentitiesOnly => :keys_only
27
+ # * CheckHostIP => :check_host_ip
28
+ # * Macs => maps to the :hmac option
29
+ # * PasswordAuthentication => maps to the :auth_methods option password
30
+ # * Port => :port
31
+ # * PreferredAuthentications => maps to the :auth_methods option
32
+ # * ProxyCommand => maps to the :proxy option
33
+ # * ProxyJump => maps to the :proxy option
34
+ # * PubKeyAuthentication => maps to the :auth_methods option
35
+ # * RekeyLimit => :rekey_limit
36
+ # * StrictHostKeyChecking => :verify_host_key
37
+ # * User => :user
38
+ # * UserKnownHostsFile => :user_known_hosts_file
39
+ # * NumberOfPasswordPrompts => :number_of_password_prompts
40
+ # * FingerprintHash => :fingerprint_hash
41
+ #
42
+ # Note that you will never need to use this class directly--you can control
43
+ # whether the OpenSSH configuration files are read by passing the :config
44
+ # option to Net::SSH.start. (They are, by default.)
45
+ class Config
46
+ class << self
47
+ @@default_files = %w[~/.ssh/config /etc/ssh_config /etc/ssh/ssh_config]
48
+ # The following defaults follow the openssh client ssh_config defaults.
49
+ # http://lwn.net/Articles/544640/
50
+ # "hostbased" is off and "none" is not supported but we allow it since
51
+ # it's used by some clients to query the server for allowed auth methods
52
+ @@default_auth_methods = %w[none publickey password keyboard-interactive]
53
53
 
54
- def default_auth_methods
55
- @@default_auth_methods
56
- end
54
+ # Returns an array of locations of OpenSSH configuration files
55
+ # to parse by default.
56
+ def default_files
57
+ @@default_files.clone
58
+ end
57
59
 
58
- # Loads the configuration data for the given +host+ from all of the
59
- # given +files+ (defaulting to the list of files returned by
60
- # #default_files), translates the resulting hash into the options
61
- # recognized by Net::SSH, and returns them.
62
- def for(host, files=default_files)
63
- translate(files.inject({}) { |settings, file|
64
- load(file, host, settings)
65
- })
66
- end
60
+ def default_auth_methods
61
+ @@default_auth_methods.clone
62
+ end
67
63
 
68
- # Load the OpenSSH configuration settings in the given +file+ for the
69
- # given +host+. If +settings+ is given, the options are merged into
70
- # that hash, with existing values taking precedence over newly parsed
71
- # ones. Returns a hash containing the OpenSSH options. (See
72
- # #translate for how to convert the OpenSSH options into Net::SSH
73
- # options.)
74
- def load(path, host, settings={})
75
- file = File.expand_path(path)
76
- return settings unless File.readable?(file)
77
-
78
- globals = {}
79
- matched_host = nil
80
- seen_host = false
81
- IO.foreach(file) do |line|
82
- next if line =~ /^\s*(?:#.*)?$/
83
-
84
- if line =~ /^\s*(\S+)\s*=(.*)$/
85
- key, value = $1, $2
86
- else
87
- key, value = line.strip.split(/\s+/, 2)
88
- end
64
+ # Loads the configuration data for the given +host+ from all of the
65
+ # given +files+ (defaulting to the list of files returned by
66
+ # #default_files), translates the resulting hash into the options
67
+ # recognized by Net::SSH, and returns them.
68
+ def for(host, files = expandable_default_files)
69
+ translate(files.inject({}) { |settings, file|
70
+ load(file, host, settings)
71
+ })
72
+ end
89
73
 
90
- # silently ignore malformed entries
91
- next if value.nil?
74
+ # Load the OpenSSH configuration settings in the given +file+ for the
75
+ # given +host+. If +settings+ is given, the options are merged into
76
+ # that hash, with existing values taking precedence over newly parsed
77
+ # ones. Returns a hash containing the OpenSSH options. (See
78
+ # #translate for how to convert the OpenSSH options into Net::SSH
79
+ # options.)
80
+ def load(path, host, settings = {}, base_dir = nil)
81
+ file = File.expand_path(path)
82
+ base_dir ||= File.dirname(file)
83
+ return settings unless File.readable?(file)
92
84
 
93
- key.downcase!
94
- value = $1 if value =~ /^"(.*)"$/
85
+ globals = {}
86
+ block_matched = false
87
+ block_seen = false
88
+ IO.foreach(file) do |line|
89
+ next if line =~ /^\s*(?:#.*)?$/
95
90
 
96
- value = case value.strip
97
- when /^\d+$/ then value.to_i
98
- when /^no$/i then false
99
- when /^yes$/i then true
100
- else value
91
+ if line =~ /^\s*(\S+)\s*=(.*)$/
92
+ key, value = $1, $2
93
+ else
94
+ key, value = line.strip.split(/\s+/, 2)
101
95
  end
102
96
 
103
- if key == 'host'
104
- # Support "Host host1 host2 hostN".
105
- # See http://github.com/net-ssh/net-ssh/issues#issue/6
106
- negative_hosts, positive_hosts = value.to_s.split(/\s+/).partition { |h| h.start_with?('!') }
97
+ # silently ignore malformed entries
98
+ next if value.nil?
107
99
 
108
- # Check for negative patterns first. If the host matches, that overrules any other positive match.
109
- # The host substring code is used to strip out the starting "!" so the regexp will be correct.
110
- negative_match = negative_hosts.select { |h| host =~ pattern2regex(h[1..-1]) }.first
100
+ key.downcase!
101
+ value = unquote(value)
111
102
 
112
- if negative_match
113
- matched_host = nil
114
- else
115
- matched_host = positive_hosts.select { |h| host =~ pattern2regex(h) }.first
103
+ value = case value.strip
104
+ when /^\d+$/ then value.to_i
105
+ when /^no$/i then false
106
+ when /^yes$/i then true
107
+ else value
108
+ end
109
+
110
+ if key == 'host'
111
+ # Support "Host host1 host2 hostN".
112
+ # See http://github.com/net-ssh/net-ssh/issues#issue/6
113
+ negative_hosts, positive_hosts = value.to_s.split(/\s+/).partition { |h| h.start_with?('!') }
114
+
115
+ # Check for negative patterns first. If the host matches, that overrules any other positive match.
116
+ # The host substring code is used to strip out the starting "!" so the regexp will be correct.
117
+ negative_matched = negative_hosts.any? { |h| host =~ pattern2regex(h[1..-1]) }
118
+
119
+ if negative_matched
120
+ block_matched = false
121
+ else
122
+ block_matched = positive_hosts.any? { |h| host =~ pattern2regex(h) }
123
+ end
124
+
125
+ block_seen = true
126
+ settings[key] = host
127
+ elsif key == 'match'
128
+ block_matched = eval_match_conditions(value, host, settings)
129
+ block_seen = true
130
+ elsif !block_seen
131
+ case key
132
+ when 'identityfile', 'certificatefile'
133
+ (globals[key] ||= []) << value
134
+ when 'include'
135
+ included_file_paths(base_dir, value).each do |file_path|
136
+ globals = load(file_path, host, globals, base_dir)
137
+ end
138
+ else
139
+ globals[key] = value unless settings.key?(key)
140
+ end
141
+ elsif block_matched
142
+ case key
143
+ when 'identityfile', 'certificatefile'
144
+ (settings[key] ||= []) << value
145
+ when 'include'
146
+ included_file_paths(base_dir, value).each do |file_path|
147
+ settings = load(file_path, host, settings, base_dir)
148
+ end
149
+ else
150
+ settings[key] = value unless settings.key?(key)
151
+ end
116
152
  end
117
153
 
118
- seen_host = true
119
- settings[key] = host
120
- elsif !seen_host
121
- if key == 'identityfile'
122
- (globals[key] ||= []) << value
123
- else
124
- globals[key] = value unless settings.key?(key)
154
+ # ProxyCommand and ProxyJump override each other so they need to be tracked togeather
155
+ %w[proxyjump proxycommand].each do |proxy_key|
156
+ if (proxy_value = settings.delete(proxy_key))
157
+ settings['proxy'] ||= [proxy_key, proxy_value]
158
+ end
125
159
  end
126
- elsif !matched_host.nil?
127
- if key == 'identityfile'
128
- (settings[key] ||= []) << value
160
+ end
161
+
162
+ globals.merge(settings) do |key, oldval, newval|
163
+ case key
164
+ when 'identityfile', 'certificatefile'
165
+ oldval + newval
129
166
  else
130
- settings[key] = value unless settings.key?(key)
167
+ newval
131
168
  end
132
169
  end
133
170
  end
134
171
 
135
- settings = globals.merge(settings) if globals
172
+ # Given a hash of OpenSSH configuration options, converts them into
173
+ # a hash of Net::SSH options. Unrecognized options are ignored. The
174
+ # +settings+ hash must have Strings for keys, all downcased, and
175
+ # the returned hash will have Symbols for keys.
176
+ def translate(settings)
177
+ auth_methods = default_auth_methods.clone
178
+ (auth_methods << 'challenge-response').uniq!
179
+ ret = settings.each_with_object({ auth_methods: auth_methods }) do |(key, value), hash|
180
+ translate_config_key(hash, key.to_sym, value, settings)
181
+ end
182
+ merge_challenge_response_with_keyboard_interactive(ret)
183
+ end
136
184
 
137
- return settings
138
- end
185
+ # Filters default_files down to the files that are expandable.
186
+ def expandable_default_files
187
+ default_files.keep_if do |path|
188
+ File.expand_path(path)
189
+ true
190
+ rescue ArgumentError
191
+ false
192
+ end
193
+ end
194
+
195
+ private
196
+
197
+ def translate_verify_host_key(value)
198
+ case value
199
+ when false
200
+ :never
201
+ when true
202
+ :always
203
+ when 'accept-new'
204
+ :accept_new
205
+ end
206
+ end
207
+
208
+ def translate_keepalive(hash, value)
209
+ if value && value.to_i > 0
210
+ hash[:keepalive] = true
211
+ hash[:keepalive_interval] = value.to_i
212
+ else
213
+ hash[:keepalive] = false
214
+ end
215
+ end
139
216
 
140
- # Given a hash of OpenSSH configuration options, converts them into
141
- # a hash of Net::SSH options. Unrecognized options are ignored. The
142
- # +settings+ hash must have Strings for keys, all downcased, and
143
- # the returned hash will have Symbols for keys.
144
- def translate(settings)
145
- auth_methods = default_auth_methods.clone
146
- (auth_methods << 'challenge-response').uniq!
147
- ret = settings.inject({:auth_methods=>auth_methods}) do |hash, (key, value)|
217
+ TRANSLATE_CONFIG_KEY_RENAME_MAP = {
218
+ bindaddress: :bind_address,
219
+ compression: :compression,
220
+ compressionlevel: :compression_level,
221
+ certificatefile: :keycerts,
222
+ connecttimeout: :timeout,
223
+ forwardagent: :forward_agent,
224
+ identitiesonly: :keys_only,
225
+ identityagent: :identity_agent,
226
+ globalknownhostsfile: :global_known_hosts_file,
227
+ hostkeyalias: :host_key_alias,
228
+ identityfile: :keys,
229
+ fingerprinthash: :fingerprint_hash,
230
+ port: :port,
231
+ user: :user,
232
+ userknownhostsfile: :user_known_hosts_file,
233
+ checkhostip: :check_host_ip
234
+ }.freeze
235
+ def translate_config_key(hash, key, value, settings)
148
236
  case key
149
- when 'bindaddress' then
150
- hash[:bind_address] = value
151
- when 'ciphers' then
237
+ when :stricthostkeychecking
238
+ hash[:verify_host_key] = translate_verify_host_key(value)
239
+ when :ciphers
152
240
  hash[:encryption] = value.split(/,/)
153
- when 'compression' then
154
- hash[:compression] = value
155
- when 'compressionlevel' then
156
- hash[:compression_level] = value
157
- when 'connecttimeout' then
158
- hash[:timeout] = value
159
- when 'forwardagent' then
160
- hash[:forward_agent] = value
161
- when 'identitiesonly' then
162
- hash[:keys_only] = value
163
- when 'globalknownhostsfile'
164
- hash[:global_known_hosts_file] = value
165
- when 'hostbasedauthentication' then
241
+ when :hostbasedauthentication
166
242
  if value
167
243
  (hash[:auth_methods] << "hostbased").uniq!
168
244
  else
169
245
  hash[:auth_methods].delete("hostbased")
170
246
  end
171
- when 'hostkeyalgorithms' then
247
+ when :hostkeyalgorithms
172
248
  hash[:host_key] = value.split(/,/)
173
- when 'hostkeyalias' then
174
- hash[:host_key_alias] = value
175
- when 'hostname' then
249
+ when :hostname
176
250
  hash[:host_name] = value.gsub(/%h/, settings['host'])
177
- when 'identityfile' then
178
- hash[:keys] = value
179
- when 'macs' then
251
+ when :macs
180
252
  hash[:hmac] = value.split(/,/)
181
- when 'serveralivecountmax'
253
+ when :serveralivecountmax
182
254
  hash[:keepalive_maxcount] = value.to_i if value
183
- when 'serveraliveinterval'
184
- if value && value.to_i > 0
185
- hash[:keepalive] = true
186
- hash[:keepalive_interval] = value.to_i
187
- else
188
- hash[:keepalive] = false
189
- end
190
- when 'passwordauthentication'
255
+ when :serveraliveinterval
256
+ translate_keepalive(hash, value)
257
+ when :passwordauthentication
191
258
  if value
192
259
  (hash[:auth_methods] << 'password').uniq!
193
260
  else
194
261
  hash[:auth_methods].delete('password')
195
262
  end
196
- when 'challengeresponseauthentication'
263
+ when :challengeresponseauthentication
197
264
  if value
198
265
  (hash[:auth_methods] << 'challenge-response').uniq!
199
266
  else
200
267
  hash[:auth_methods].delete('challenge-response')
201
268
  end
202
- when 'kbdinteractiveauthentication'
269
+ when :kbdinteractiveauthentication
203
270
  if value
204
271
  (hash[:auth_methods] << 'keyboard-interactive').uniq!
205
272
  else
206
273
  hash[:auth_methods].delete('keyboard-interactive')
207
274
  end
208
- when 'port'
209
- hash[:port] = value
210
- when 'preferredauthentications'
275
+ when :preferredauthentications
211
276
  hash[:auth_methods] = value.split(/,/) # TODO we should place to preferred_auth_methods rather than auth_methods
212
- when 'proxycommand'
213
- if value and !(value =~ /^none$/)
214
- require 'net/ssh/proxy/command'
215
- hash[:proxy] = Net::SSH::Proxy::Command.new(value)
277
+ when :proxy
278
+ if (proxy = setup_proxy(*value))
279
+ hash[:proxy] = proxy
216
280
  end
217
- when 'pubkeyauthentication'
281
+ when :pubkeyauthentication
218
282
  if value
219
283
  (hash[:auth_methods] << 'publickey').uniq!
220
284
  else
221
285
  hash[:auth_methods].delete('publickey')
222
286
  end
223
- when 'rekeylimit'
287
+ when :rekeylimit
224
288
  hash[:rekey_limit] = interpret_size(value)
225
- when 'user'
226
- hash[:user] = value
227
- when 'userknownhostsfile'
228
- hash[:user_known_hosts_file] = value
229
- when 'sendenv'
289
+ when :sendenv
230
290
  multi_send_env = value.to_s.split(/\s+/)
231
291
  hash[:send_env] = multi_send_env.map { |e| Regexp.new pattern2regex(e).source, false }
232
- when 'numberofpasswordprompts'
292
+ when :setenv
293
+ hash[:set_env] = Shellwords.split(value.to_s).map { |e| e.split '=', 2 }.to_h
294
+ when :numberofpasswordprompts
233
295
  hash[:number_of_password_prompts] = value.to_i
296
+ when *TRANSLATE_CONFIG_KEY_RENAME_MAP.keys
297
+ hash[TRANSLATE_CONFIG_KEY_RENAME_MAP[key]] = value
234
298
  end
235
- hash
236
299
  end
237
- merge_challenge_response_with_keyboard_interactive(ret)
238
- end
239
300
 
240
- private
301
+ def setup_proxy(type, value)
302
+ case type
303
+ when 'proxycommand'
304
+ if value !~ /^none$/
305
+ require 'net/ssh/proxy/command'
306
+ Net::SSH::Proxy::Command.new(value)
307
+ end
308
+ when 'proxyjump'
309
+ require 'net/ssh/proxy/jump'
310
+ Net::SSH::Proxy::Jump.new(value)
311
+ end
312
+ end
241
313
 
242
314
  # Converts an ssh_config pattern into a regex for matching against
243
315
  # host names.
244
316
  def pattern2regex(pattern)
245
317
  tail = pattern
246
- prefix = ""
318
+ prefix = String.new
247
319
  while !tail.empty? do
248
- head,sep,tail = tail.partition(/[\*\?]/)
320
+ head, sep, tail = tail.partition(/[\*\?]/)
249
321
  prefix = prefix + Regexp.quote(head)
250
322
  case sep
251
323
  when '*'
@@ -269,7 +341,7 @@ module Net; module SSH
269
341
  else size.to_i
270
342
  end
271
343
  end
272
-
344
+
273
345
  def merge_challenge_response_with_keyboard_interactive(hash)
274
346
  if hash[:auth_methods].include?('challenge-response')
275
347
  hash[:auth_methods].delete('challenge-response')
@@ -277,7 +349,58 @@ module Net; module SSH
277
349
  end
278
350
  hash
279
351
  end
352
+
353
+ def included_file_paths(base_dir, config_paths)
354
+ tokenize_config_value(config_paths).flat_map do |path|
355
+ Dir.glob(File.expand_path(path, base_dir)).select { |f| File.file?(f) }
356
+ end
357
+ end
358
+
359
+ # Tokenize string into tokens.
360
+ # A token is a word or a quoted sequence of words, separated by whitespaces.
361
+ def tokenize_config_value(str)
362
+ str.scan(/([^"\s]+)?(?:"([^"]+)")?\s*/).map(&:join)
363
+ end
364
+
365
+ def eval_match_conditions(condition, host, settings)
366
+ # Not using `\s` for whitespace matching as canonical
367
+ # ssh_config parser implementation (OpenSSH) has specific character set.
368
+ # Ref: https://github.com/openssh/openssh-portable/blob/2581333d564d8697837729b3d07d45738eaf5a54/misc.c#L237-L239
369
+ conditions = condition.split(/[ \t\r\n]+|(?<!=)=(?!=)/).reject(&:empty?)
370
+ return true if conditions == ["all"]
371
+
372
+ conditions = conditions.each_slice(2)
373
+ condition_matches = []
374
+ conditions.each do |(kind, exprs)|
375
+ exprs = unquote(exprs)
376
+
377
+ case kind.downcase
378
+ when "all"
379
+ raise "all cannot be mixed with other conditions"
380
+ when "host"
381
+ if exprs.start_with?('!')
382
+ negated = true
383
+ exprs = exprs[1..-1]
384
+ else
385
+ negated = false
386
+ end
387
+ condition_met = false
388
+ exprs.split(",").each do |expr|
389
+ condition_met = condition_met || host =~ pattern2regex(expr)
390
+ end
391
+ condition_matches << (true && negated ^ condition_met)
392
+ # else
393
+ # warn "net-ssh: Unsupported expr in Match block: #{kind}"
394
+ end
395
+ end
396
+
397
+ !condition_matches.empty? && condition_matches.all?
398
+ end
399
+
400
+ def unquote(string)
401
+ string =~ /^"(.*)"$/ ? Regexp.last_match(1) : string
402
+ end
403
+ end
280
404
  end
281
405
  end
282
-
283
- end; end
406
+ end