net-ssh 4.1.0 → 6.1.0

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 (111) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/.gitignore +5 -0
  5. data/.rubocop.yml +8 -2
  6. data/.rubocop_todo.yml +405 -552
  7. data/.travis.yml +23 -22
  8. data/CHANGES.txt +112 -1
  9. data/Gemfile +1 -7
  10. data/{Gemfile.norbnacl → Gemfile.noed25519} +1 -1
  11. data/Manifest +4 -5
  12. data/README.md +287 -0
  13. data/Rakefile +40 -29
  14. data/appveyor.yml +12 -6
  15. data/lib/net/ssh.rb +68 -32
  16. data/lib/net/ssh/authentication/agent.rb +234 -222
  17. data/lib/net/ssh/authentication/certificate.rb +175 -164
  18. data/lib/net/ssh/authentication/constants.rb +17 -14
  19. data/lib/net/ssh/authentication/ed25519.rb +162 -141
  20. data/lib/net/ssh/authentication/ed25519_loader.rb +32 -29
  21. data/lib/net/ssh/authentication/key_manager.rb +40 -9
  22. data/lib/net/ssh/authentication/methods/abstract.rb +53 -47
  23. data/lib/net/ssh/authentication/methods/hostbased.rb +32 -33
  24. data/lib/net/ssh/authentication/methods/keyboard_interactive.rb +1 -1
  25. data/lib/net/ssh/authentication/methods/none.rb +10 -10
  26. data/lib/net/ssh/authentication/methods/password.rb +13 -13
  27. data/lib/net/ssh/authentication/methods/publickey.rb +56 -55
  28. data/lib/net/ssh/authentication/pageant.rb +468 -465
  29. data/lib/net/ssh/authentication/pub_key_fingerprint.rb +43 -0
  30. data/lib/net/ssh/authentication/session.rb +130 -122
  31. data/lib/net/ssh/buffer.rb +345 -312
  32. data/lib/net/ssh/buffered_io.rb +163 -163
  33. data/lib/net/ssh/config.rb +316 -238
  34. data/lib/net/ssh/connection/channel.rb +670 -650
  35. data/lib/net/ssh/connection/constants.rb +30 -26
  36. data/lib/net/ssh/connection/event_loop.rb +108 -105
  37. data/lib/net/ssh/connection/keepalive.rb +54 -50
  38. data/lib/net/ssh/connection/session.rb +682 -671
  39. data/lib/net/ssh/connection/term.rb +180 -176
  40. data/lib/net/ssh/errors.rb +101 -99
  41. data/lib/net/ssh/key_factory.rb +195 -108
  42. data/lib/net/ssh/known_hosts.rb +161 -152
  43. data/lib/net/ssh/loggable.rb +57 -55
  44. data/lib/net/ssh/packet.rb +82 -78
  45. data/lib/net/ssh/prompt.rb +55 -53
  46. data/lib/net/ssh/proxy/command.rb +104 -89
  47. data/lib/net/ssh/proxy/errors.rb +12 -8
  48. data/lib/net/ssh/proxy/http.rb +93 -91
  49. data/lib/net/ssh/proxy/https.rb +42 -39
  50. data/lib/net/ssh/proxy/jump.rb +50 -47
  51. data/lib/net/ssh/proxy/socks4.rb +0 -2
  52. data/lib/net/ssh/proxy/socks5.rb +11 -12
  53. data/lib/net/ssh/service/forward.rb +370 -317
  54. data/lib/net/ssh/test.rb +83 -77
  55. data/lib/net/ssh/test/channel.rb +146 -142
  56. data/lib/net/ssh/test/extensions.rb +150 -146
  57. data/lib/net/ssh/test/kex.rb +35 -31
  58. data/lib/net/ssh/test/local_packet.rb +48 -44
  59. data/lib/net/ssh/test/packet.rb +87 -84
  60. data/lib/net/ssh/test/remote_packet.rb +35 -31
  61. data/lib/net/ssh/test/script.rb +173 -171
  62. data/lib/net/ssh/test/socket.rb +59 -55
  63. data/lib/net/ssh/transport/algorithms.rb +430 -364
  64. data/lib/net/ssh/transport/cipher_factory.rb +95 -91
  65. data/lib/net/ssh/transport/constants.rb +33 -25
  66. data/lib/net/ssh/transport/ctr.rb +33 -11
  67. data/lib/net/ssh/transport/hmac.rb +15 -13
  68. data/lib/net/ssh/transport/hmac/abstract.rb +82 -63
  69. data/lib/net/ssh/transport/hmac/sha2_256.rb +7 -11
  70. data/lib/net/ssh/transport/hmac/sha2_256_96.rb +4 -8
  71. data/lib/net/ssh/transport/hmac/sha2_256_etm.rb +12 -0
  72. data/lib/net/ssh/transport/hmac/sha2_512.rb +6 -9
  73. data/lib/net/ssh/transport/hmac/sha2_512_96.rb +4 -8
  74. data/lib/net/ssh/transport/hmac/sha2_512_etm.rb +12 -0
  75. data/lib/net/ssh/transport/identity_cipher.rb +55 -51
  76. data/lib/net/ssh/transport/kex.rb +14 -13
  77. data/lib/net/ssh/transport/kex/abstract.rb +123 -0
  78. data/lib/net/ssh/transport/kex/abstract5656.rb +72 -0
  79. data/lib/net/ssh/transport/kex/curve25519_sha256.rb +38 -0
  80. data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +30 -0
  81. data/lib/net/ssh/transport/kex/diffie_hellman_group14_sha1.rb +33 -40
  82. data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +112 -217
  83. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +53 -62
  84. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha256.rb +5 -9
  85. data/lib/net/ssh/transport/kex/ecdh_sha2_nistp256.rb +36 -90
  86. data/lib/net/ssh/transport/kex/ecdh_sha2_nistp384.rb +18 -10
  87. data/lib/net/ssh/transport/kex/ecdh_sha2_nistp521.rb +18 -10
  88. data/lib/net/ssh/transport/key_expander.rb +29 -25
  89. data/lib/net/ssh/transport/openssl.rb +116 -116
  90. data/lib/net/ssh/transport/packet_stream.rb +223 -190
  91. data/lib/net/ssh/transport/server_version.rb +64 -66
  92. data/lib/net/ssh/transport/session.rb +306 -257
  93. data/lib/net/ssh/transport/state.rb +198 -196
  94. data/lib/net/ssh/verifiers/accept_new.rb +35 -0
  95. data/lib/net/ssh/verifiers/accept_new_or_local_tunnel.rb +34 -0
  96. data/lib/net/ssh/verifiers/always.rb +56 -0
  97. data/lib/net/ssh/verifiers/never.rb +21 -0
  98. data/lib/net/ssh/version.rb +55 -53
  99. data/net-ssh-public_cert.pem +18 -19
  100. data/net-ssh.gemspec +12 -11
  101. data/support/ssh_tunnel_bug.rb +2 -2
  102. metadata +86 -75
  103. metadata.gz.sig +0 -0
  104. data/Gemfile.norbnacl.lock +0 -41
  105. data/README.rdoc +0 -169
  106. data/lib/net/ssh/ruby_compat.rb +0 -24
  107. data/lib/net/ssh/verifiers/lenient.rb +0 -30
  108. data/lib/net/ssh/verifiers/null.rb +0 -12
  109. data/lib/net/ssh/verifiers/secure.rb +0 -52
  110. data/lib/net/ssh/verifiers/strict.rb +0 -24
  111. data/support/arcfour_check.rb +0 -20
@@ -1,144 +1,145 @@
1
1
  require 'net/ssh/buffer'
2
2
  require 'net/ssh/loggable'
3
- require 'net/ssh/ruby_compat'
4
3
 
5
- module Net; module SSH
6
-
7
- # This module is used to extend sockets and other IO objects, to allow
8
- # them to be buffered for both read and write. This abstraction makes it
9
- # quite easy to write a select-based event loop
10
- # (see Net::SSH::Connection::Session#listen_to).
11
- #
12
- # The general idea is that instead of calling #read directly on an IO that
13
- # has been extended with this module, you call #fill (to add pending input
14
- # to the internal read buffer), and then #read_available (to read from that
15
- # buffer). Likewise, you don't call #write directly, you call #enqueue to
16
- # add data to the write buffer, and then #send_pending or #wait_for_pending_sends
17
- # to actually send the data across the wire.
18
- #
19
- # In this way you can easily use the object as an argument to IO.select,
20
- # calling #fill when it is available for read, or #send_pending when it is
21
- # available for write, and then call #enqueue and #read_available during
22
- # the idle times.
23
- #
24
- # socket = TCPSocket.new(address, port)
25
- # socket.extend(Net::SSH::BufferedIo)
26
- #
27
- # ssh.listen_to(socket)
28
- #
29
- # ssh.loop do
30
- # if socket.available > 0
31
- # puts socket.read_available
32
- # socket.enqueue("response\n")
33
- # end
34
- # end
35
- #
36
- # Note that this module must be used to extend an instance, and should not
37
- # be included in a class. If you do want to use it via an include, then you
38
- # must make sure to invoke the private #initialize_buffered_io method in
39
- # your class' #initialize method:
40
- #
41
- # class Foo < IO
42
- # include Net::SSH::BufferedIo
43
- #
44
- # def initialize
45
- # initialize_buffered_io
46
- # # ...
47
- # end
48
- # end
49
- module BufferedIo
50
- include Loggable
51
-
52
- # Called when the #extend is called on an object, with this module as the
53
- # argument. It ensures that the modules instance variables are all properly
54
- # initialized.
55
- def self.extended(object) #:nodoc:
56
- # need to use __send__ because #send is overridden in Socket
57
- object.__send__(:initialize_buffered_io)
58
- end
59
-
60
- # Tries to read up to +n+ bytes of data from the remote end, and appends
61
- # the data to the input buffer. It returns the number of bytes read, or 0
62
- # if no data was available to be read.
63
- def fill(n=8192)
64
- input.consume!
65
- data = recv(n)
66
- debug { "read #{data.length} bytes" }
67
- input.append(data)
68
- return data.length
69
- rescue EOFError => e
70
- @input_errors << e
71
- return 0
72
- end
73
-
74
- # Read up to +length+ bytes from the input buffer. If +length+ is nil,
75
- # all available data is read from the buffer. (See #available.)
76
- def read_available(length=nil)
77
- input.read(length || available)
78
- end
79
-
80
- # Returns the number of bytes available to be read from the input buffer.
81
- # (See #read_available.)
82
- def available
83
- input.available
84
- end
85
-
86
- # Enqueues data in the output buffer, to be written when #send_pending
87
- # is called. Note that the data is _not_ sent immediately by this method!
88
- def enqueue(data)
89
- output.append(data)
90
- end
91
-
92
- # Returns +true+ if there is data waiting in the output buffer, and
93
- # +false+ otherwise.
94
- def pending_write?
95
- output.length > 0
96
- end
97
-
98
- # Sends as much of the pending output as possible. Returns +true+ if any
99
- # data was sent, and +false+ otherwise.
100
- def send_pending
101
- if output.length > 0
102
- sent = send(output.to_s, 0)
103
- debug { "sent #{sent} bytes" }
104
- output.consume!(sent)
105
- return sent > 0
106
- else
107
- return false
4
+ module Net
5
+ module SSH
6
+
7
+ # This module is used to extend sockets and other IO objects, to allow
8
+ # them to be buffered for both read and write. This abstraction makes it
9
+ # quite easy to write a select-based event loop
10
+ # (see Net::SSH::Connection::Session#listen_to).
11
+ #
12
+ # The general idea is that instead of calling #read directly on an IO that
13
+ # has been extended with this module, you call #fill (to add pending input
14
+ # to the internal read buffer), and then #read_available (to read from that
15
+ # buffer). Likewise, you don't call #write directly, you call #enqueue to
16
+ # add data to the write buffer, and then #send_pending or #wait_for_pending_sends
17
+ # to actually send the data across the wire.
18
+ #
19
+ # In this way you can easily use the object as an argument to IO.select,
20
+ # calling #fill when it is available for read, or #send_pending when it is
21
+ # available for write, and then call #enqueue and #read_available during
22
+ # the idle times.
23
+ #
24
+ # socket = TCPSocket.new(address, port)
25
+ # socket.extend(Net::SSH::BufferedIo)
26
+ #
27
+ # ssh.listen_to(socket)
28
+ #
29
+ # ssh.loop do
30
+ # if socket.available > 0
31
+ # puts socket.read_available
32
+ # socket.enqueue("response\n")
33
+ # end
34
+ # end
35
+ #
36
+ # Note that this module must be used to extend an instance, and should not
37
+ # be included in a class. If you do want to use it via an include, then you
38
+ # must make sure to invoke the private #initialize_buffered_io method in
39
+ # your class' #initialize method:
40
+ #
41
+ # class Foo < IO
42
+ # include Net::SSH::BufferedIo
43
+ #
44
+ # def initialize
45
+ # initialize_buffered_io
46
+ # # ...
47
+ # end
48
+ # end
49
+ module BufferedIo
50
+ include Loggable
51
+
52
+ # Called when the #extend is called on an object, with this module as the
53
+ # argument. It ensures that the modules instance variables are all properly
54
+ # initialized.
55
+ def self.extended(object) #:nodoc:
56
+ # need to use __send__ because #send is overridden in Socket
57
+ object.__send__(:initialize_buffered_io)
108
58
  end
109
- end
110
-
111
- # Calls #send_pending repeatedly, if necessary, blocking until the output
112
- # buffer is empty.
113
- def wait_for_pending_sends
114
- send_pending
115
- while output.length > 0
116
- result = Net::SSH::Compat.io_select(nil, [self]) or next
117
- next unless result[1].any?
59
+
60
+ # Tries to read up to +n+ bytes of data from the remote end, and appends
61
+ # the data to the input buffer. It returns the number of bytes read, or 0
62
+ # if no data was available to be read.
63
+ def fill(n=8192)
64
+ input.consume!
65
+ data = recv(n)
66
+ debug { "read #{data.length} bytes" }
67
+ input.append(data)
68
+ return data.length
69
+ rescue EOFError => e
70
+ @input_errors << e
71
+ return 0
72
+ end
73
+
74
+ # Read up to +length+ bytes from the input buffer. If +length+ is nil,
75
+ # all available data is read from the buffer. (See #available.)
76
+ def read_available(length=nil)
77
+ input.read(length || available)
78
+ end
79
+
80
+ # Returns the number of bytes available to be read from the input buffer.
81
+ # (See #read_available.)
82
+ def available
83
+ input.available
84
+ end
85
+
86
+ # Enqueues data in the output buffer, to be written when #send_pending
87
+ # is called. Note that the data is _not_ sent immediately by this method!
88
+ def enqueue(data)
89
+ output.append(data)
90
+ end
91
+
92
+ # Returns +true+ if there is data waiting in the output buffer, and
93
+ # +false+ otherwise.
94
+ def pending_write?
95
+ output.length > 0
96
+ end
97
+
98
+ # Sends as much of the pending output as possible. Returns +true+ if any
99
+ # data was sent, and +false+ otherwise.
100
+ def send_pending
101
+ if output.length > 0
102
+ sent = send(output.to_s, 0)
103
+ debug { "sent #{sent} bytes" }
104
+ output.consume!(sent)
105
+ return sent > 0
106
+ else
107
+ return false
108
+ end
109
+ end
110
+
111
+ # Calls #send_pending repeatedly, if necessary, blocking until the output
112
+ # buffer is empty.
113
+ def wait_for_pending_sends
118
114
  send_pending
115
+ while output.length > 0
116
+ result = IO.select(nil, [self]) or next
117
+ next unless result[1].any?
118
+ send_pending
119
+ end
119
120
  end
120
- end
121
-
122
- public # these methods are primarily for use in tests
123
-
121
+
122
+ public # these methods are primarily for use in tests
123
+
124
124
  def write_buffer #:nodoc:
125
125
  output.to_s
126
126
  end
127
-
127
+
128
128
  def read_buffer #:nodoc:
129
129
  input.to_s
130
130
  end
131
-
132
- private
133
-
131
+
132
+ private
133
+
134
134
  #--
135
135
  # Can't use attr_reader here (after +private+) without incurring the
136
136
  # wrath of "ruby -w". We hates it.
137
137
  #++
138
-
138
+
139
139
  def input; @input; end
140
- def output; @output; end
141
140
 
141
+ def output; @output; end
142
+
142
143
  # Initializes the intput and output buffers for this object. This method
143
144
  # is called automatically when the module is mixed into an object via
144
145
  # Object#extend (see Net::SSH::BufferedIo.extended), but must be called
@@ -150,54 +151,53 @@ module Net; module SSH
150
151
  @output = Net::SSH::Buffer.new
151
152
  @output_errors = []
152
153
  end
153
- end
154
-
155
-
154
+ end
156
155
 
157
- # Fixes for two issues by Miklós Fazekas:
158
- #
159
- # * if client closes a forwarded connection, but the server is
160
- # reading, net-ssh terminates with IOError socket closed.
161
- # * if client force closes (RST) a forwarded connection, but
162
- # server is reading, net-ssh terminates with [an exception]
163
- #
164
- # See:
165
- #
166
- # http://net-ssh.lighthouseapp.com/projects/36253/tickets/7
167
- # http://github.com/net-ssh/net-ssh/tree/portfwfix
168
- #
169
- module ForwardedBufferedIo
170
- def fill(n=8192)
171
- begin
172
- super(n)
173
- rescue Errno::ECONNRESET => e
174
- debug { "connection was reset => shallowing exception:#{e}" }
175
- return 0
176
- rescue IOError => e
177
- if e.message =~ /closed/ then
156
+ # Fixes for two issues by Miklós Fazekas:
157
+ #
158
+ # * if client closes a forwarded connection, but the server is
159
+ # reading, net-ssh terminates with IOError socket closed.
160
+ # * if client force closes (RST) a forwarded connection, but
161
+ # server is reading, net-ssh terminates with [an exception]
162
+ #
163
+ # See:
164
+ #
165
+ # http://net-ssh.lighthouseapp.com/projects/36253/tickets/7
166
+ # http://github.com/net-ssh/net-ssh/tree/portfwfix
167
+ #
168
+ module ForwardedBufferedIo
169
+ def fill(n=8192)
170
+ begin
171
+ super(n)
172
+ rescue Errno::ECONNRESET => e
178
173
  debug { "connection was reset => shallowing exception:#{e}" }
179
174
  return 0
180
- else
181
- raise
175
+ rescue IOError => e
176
+ if e.message =~ /closed/ then
177
+ debug { "connection was reset => shallowing exception:#{e}" }
178
+ return 0
179
+ else
180
+ raise
181
+ end
182
182
  end
183
183
  end
184
- end
185
-
186
- def send_pending
187
- begin
188
- super
189
- rescue Errno::ECONNRESET => e
190
- debug { "connection was reset => shallowing exception:#{e}" }
191
- return 0
192
- rescue IOError => e
193
- if e.message =~ /closed/ then
184
+
185
+ def send_pending
186
+ begin
187
+ super
188
+ rescue Errno::ECONNRESET => e
194
189
  debug { "connection was reset => shallowing exception:#{e}" }
195
190
  return 0
196
- else
197
- raise
191
+ rescue IOError => e
192
+ if e.message =~ /closed/ then
193
+ debug { "connection was reset => shallowing exception:#{e}" }
194
+ return 0
195
+ else
196
+ raise
197
+ end
198
198
  end
199
199
  end
200
200
  end
201
- end
202
201
 
203
- end; end
202
+ end
203
+ end
@@ -1,266 +1,300 @@
1
- module Net; module SSH
1
+ module Net
2
+ module SSH
2
3
 
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
- # * ProxyJump => maps to the :proxy option
31
- # * PubKeyAuthentication => maps to the :auth_methods option
32
- # * RekeyLimit => :rekey_limit
33
- # * User => :user
34
- # * UserKnownHostsFile => :user_known_hosts_file
35
- # * NumberOfPasswordPrompts => :number_of_password_prompts
36
- #
37
- # Note that you will never need to use this class directly--you can control
38
- # whether the OpenSSH configuration files are read by passing the :config
39
- # option to Net::SSH.start. (They are, by default.)
40
- class Config
41
- class << self
42
- @@default_files = %w(~/.ssh/config /etc/ssh_config /etc/ssh/ssh_config)
43
- # The following defaults follow the openssh client ssh_config defaults.
44
- # http://lwn.net/Articles/544640/
45
- # "hostbased" is off and "none" is not supported but we allow it since
46
- # it's used by some clients to query the server for allowed auth methods
47
- @@default_auth_methods = %w(none publickey password keyboard-interactive)
4
+ # The Net::SSH::Config class is used to parse OpenSSH configuration files,
5
+ # and translates that syntax into the configuration syntax that Net::SSH
6
+ # understands. This lets Net::SSH scripts read their configuration (to
7
+ # some extent) from OpenSSH configuration files (~/.ssh/config, /etc/ssh_config,
8
+ # and so forth).
9
+ #
10
+ # Only a subset of OpenSSH configuration options are understood:
11
+ #
12
+ # * ChallengeResponseAuthentication => maps to the :auth_methods option challenge-response (then coleasced into keyboard-interactive)
13
+ # * KbdInteractiveAuthentication => maps to the :auth_methods keyboard-interactive
14
+ # * CertificateFile => maps to the :keycerts option
15
+ # * Ciphers => maps to the :encryption option
16
+ # * Compression => :compression
17
+ # * CompressionLevel => :compression_level
18
+ # * ConnectTimeout => maps to the :timeout option
19
+ # * ForwardAgent => :forward_agent
20
+ # * GlobalKnownHostsFile => :global_known_hosts_file
21
+ # * HostBasedAuthentication => maps to the :auth_methods option
22
+ # * HostKeyAlgorithms => maps to :host_key option
23
+ # * HostKeyAlias => :host_key_alias
24
+ # * HostName => :host_name
25
+ # * IdentityFile => maps to the :keys option
26
+ # * IdentityAgent => :identity_agent
27
+ # * IdentitiesOnly => :keys_only
28
+ # * CheckHostIP => :check_host_ip
29
+ # * Macs => maps to the :hmac option
30
+ # * PasswordAuthentication => maps to the :auth_methods option password
31
+ # * Port => :port
32
+ # * PreferredAuthentications => maps to the :auth_methods option
33
+ # * ProxyCommand => maps to the :proxy option
34
+ # * ProxyJump => maps to the :proxy option
35
+ # * PubKeyAuthentication => maps to the :auth_methods option
36
+ # * RekeyLimit => :rekey_limit
37
+ # * StrictHostKeyChecking => :strict_host_key_checking
38
+ # * User => :user
39
+ # * UserKnownHostsFile => :user_known_hosts_file
40
+ # * NumberOfPasswordPrompts => :number_of_password_prompts
41
+ # * FingerprintHash => :fingerprint_hash
42
+ #
43
+ # Note that you will never need to use this class directly--you can control
44
+ # whether the OpenSSH configuration files are read by passing the :config
45
+ # option to Net::SSH.start. (They are, by default.)
46
+ class Config
47
+ class << self
48
+ @@default_files = %w[~/.ssh/config /etc/ssh_config /etc/ssh/ssh_config]
49
+ # The following defaults follow the openssh client ssh_config defaults.
50
+ # http://lwn.net/Articles/544640/
51
+ # "hostbased" is off and "none" is not supported but we allow it since
52
+ # it's used by some clients to query the server for allowed auth methods
53
+ @@default_auth_methods = %w[none publickey password keyboard-interactive]
48
54
 
49
- # Returns an array of locations of OpenSSH configuration files
50
- # to parse by default.
51
- def default_files
52
- @@default_files
53
- end
55
+ # Returns an array of locations of OpenSSH configuration files
56
+ # to parse by default.
57
+ def default_files
58
+ @@default_files.clone
59
+ end
54
60
 
55
- def default_auth_methods
56
- @@default_auth_methods
57
- end
61
+ def default_auth_methods
62
+ @@default_auth_methods.clone
63
+ end
58
64
 
59
- # Loads the configuration data for the given +host+ from all of the
60
- # given +files+ (defaulting to the list of files returned by
61
- # #default_files), translates the resulting hash into the options
62
- # recognized by Net::SSH, and returns them.
63
- def for(host, files=expandable_default_files)
64
- translate(files.inject({}) { |settings, file|
65
- load(file, host, settings)
66
- })
67
- end
65
+ # Loads the configuration data for the given +host+ from all of the
66
+ # given +files+ (defaulting to the list of files returned by
67
+ # #default_files), translates the resulting hash into the options
68
+ # recognized by Net::SSH, and returns them.
69
+ def for(host, files=expandable_default_files)
70
+ translate(files.inject({}) { |settings, file|
71
+ load(file, host, settings)
72
+ })
73
+ end
68
74
 
69
- # Load the OpenSSH configuration settings in the given +file+ for the
70
- # given +host+. If +settings+ is given, the options are merged into
71
- # that hash, with existing values taking precedence over newly parsed
72
- # ones. Returns a hash containing the OpenSSH options. (See
73
- # #translate for how to convert the OpenSSH options into Net::SSH
74
- # options.)
75
- def load(path, host, settings={})
76
- file = File.expand_path(path)
77
- base_dir = File.dirname(file)
78
- return settings unless File.readable?(file)
75
+ # Load the OpenSSH configuration settings in the given +file+ for the
76
+ # given +host+. If +settings+ is given, the options are merged into
77
+ # that hash, with existing values taking precedence over newly parsed
78
+ # ones. Returns a hash containing the OpenSSH options. (See
79
+ # #translate for how to convert the OpenSSH options into Net::SSH
80
+ # options.)
81
+ def load(path, host, settings={}, base_dir = nil)
82
+ file = File.expand_path(path)
83
+ base_dir ||= File.dirname(file)
84
+ return settings unless File.readable?(file)
79
85
 
80
- globals = {}
81
- host_matched = false
82
- seen_host = false
83
- IO.foreach(file) do |line|
84
- next if line =~ /^\s*(?:#.*)?$/
86
+ globals = {}
87
+ block_matched = false
88
+ block_seen = false
89
+ IO.foreach(file) do |line|
90
+ next if line =~ /^\s*(?:#.*)?$/
85
91
 
86
- if line =~ /^\s*(\S+)\s*=(.*)$/
87
- key, value = $1, $2
88
- else
89
- key, value = line.strip.split(/\s+/, 2)
90
- end
92
+ if line =~ /^\s*(\S+)\s*=(.*)$/
93
+ key, value = $1, $2
94
+ else
95
+ key, value = line.strip.split(/\s+/, 2)
96
+ end
91
97
 
92
- # silently ignore malformed entries
93
- next if value.nil?
98
+ # silently ignore malformed entries
99
+ next if value.nil?
94
100
 
95
- key.downcase!
96
- value = $1 if value =~ /^"(.*)"$/
101
+ key.downcase!
102
+ value = unquote(value)
97
103
 
98
- value = case value.strip
99
- when /^\d+$/ then value.to_i
100
- when /^no$/i then false
101
- when /^yes$/i then true
102
- else value
103
- end
104
+ value = case value.strip
105
+ when /^\d+$/ then value.to_i
106
+ when /^no$/i then false
107
+ when /^yes$/i then true
108
+ else value
109
+ end
104
110
 
105
- if key == 'host'
106
- # Support "Host host1 host2 hostN".
107
- # See http://github.com/net-ssh/net-ssh/issues#issue/6
108
- negative_hosts, positive_hosts = value.to_s.split(/\s+/).partition { |h| h.start_with?('!') }
111
+ if key == 'host'
112
+ # Support "Host host1 host2 hostN".
113
+ # See http://github.com/net-ssh/net-ssh/issues#issue/6
114
+ negative_hosts, positive_hosts = value.to_s.split(/\s+/).partition { |h| h.start_with?('!') }
109
115
 
110
- # Check for negative patterns first. If the host matches, that overrules any other positive match.
111
- # The host substring code is used to strip out the starting "!" so the regexp will be correct.
112
- negative_matched = negative_hosts.any? { |h| host =~ pattern2regex(h[1..-1]) }
116
+ # Check for negative patterns first. If the host matches, that overrules any other positive match.
117
+ # The host substring code is used to strip out the starting "!" so the regexp will be correct.
118
+ negative_matched = negative_hosts.any? { |h| host =~ pattern2regex(h[1..-1]) }
113
119
 
114
- if negative_matched
115
- host_matched = false
116
- else
117
- host_matched = positive_hosts.any? { |h| host =~ pattern2regex(h) }
120
+ if negative_matched
121
+ block_matched = false
122
+ else
123
+ block_matched = positive_hosts.any? { |h| host =~ pattern2regex(h) }
124
+ end
125
+
126
+ block_seen = true
127
+ settings[key] = host
128
+ elsif key == 'match'
129
+ block_matched = eval_match_conditions(value, host, settings)
130
+ block_seen = true
131
+ elsif !block_seen
132
+ case key
133
+ when 'identityfile', 'certificatefile'
134
+ (globals[key] ||= []) << value
135
+ when 'include'
136
+ included_file_paths(base_dir, value).each do |file_path|
137
+ globals = load(file_path, host, globals, base_dir)
138
+ end
139
+ else
140
+ globals[key] = value unless settings.key?(key)
141
+ end
142
+ elsif block_matched
143
+ case key
144
+ when 'identityfile', 'certificatefile'
145
+ (settings[key] ||= []) << value
146
+ when 'include'
147
+ included_file_paths(base_dir, value).each do |file_path|
148
+ settings = load(file_path, host, settings, base_dir)
149
+ end
150
+ else
151
+ settings[key] = value unless settings.key?(key)
152
+ end
118
153
  end
119
154
 
120
- seen_host = true
121
- settings[key] = host
122
- elsif !seen_host
123
- case key
124
- when 'identityfile'
125
- (globals[key] ||= []) << value
126
- when 'include'
127
- included_file_paths(base_dir, value).each do |file_path|
128
- globals = load(file_path, host, globals)
155
+ # ProxyCommand and ProxyJump override each other so they need to be tracked togeather
156
+ %w[proxyjump proxycommand].each do |proxy_key|
157
+ if (proxy_value = settings.delete(proxy_key))
158
+ settings['proxy'] ||= [proxy_key, proxy_value]
129
159
  end
130
- else
131
- globals[key] = value unless settings.key?(key)
132
160
  end
133
- elsif host_matched
161
+ end
162
+
163
+ globals.merge(settings) do |key, oldval, newval|
134
164
  case key
135
- when 'identityfile'
136
- (settings[key] ||= []) << value
137
- when 'include'
138
- included_file_paths(base_dir, value).each do |file_path|
139
- settings = load(file_path, host, settings)
140
- end
165
+ when 'identityfile', 'certificatefile'
166
+ oldval + newval
141
167
  else
142
- settings[key] = value unless settings.key?(key)
168
+ newval
143
169
  end
144
170
  end
145
171
  end
146
172
 
147
- settings = globals.merge(settings) if globals
148
-
149
- return settings
150
- end
151
-
152
- # Given a hash of OpenSSH configuration options, converts them into
153
- # a hash of Net::SSH options. Unrecognized options are ignored. The
154
- # +settings+ hash must have Strings for keys, all downcased, and
155
- # the returned hash will have Symbols for keys.
156
- def translate(settings)
157
- auth_methods = default_auth_methods.clone
158
- (auth_methods << 'challenge-response').uniq!
159
- ret = settings.inject({auth_methods: auth_methods}) do |hash, (key, value)|
160
- translate_config_key(hash, key.to_sym, value, settings)
161
- hash
173
+ # Given a hash of OpenSSH configuration options, converts them into
174
+ # a hash of Net::SSH options. Unrecognized options are ignored. The
175
+ # +settings+ hash must have Strings for keys, all downcased, and
176
+ # the returned hash will have Symbols for keys.
177
+ def translate(settings)
178
+ auth_methods = default_auth_methods.clone
179
+ (auth_methods << 'challenge-response').uniq!
180
+ ret = settings.each_with_object({ auth_methods: auth_methods }) do |(key, value), hash|
181
+ translate_config_key(hash, key.to_sym, value, settings)
182
+ end
183
+ merge_challenge_response_with_keyboard_interactive(ret)
162
184
  end
163
- merge_challenge_response_with_keyboard_interactive(ret)
164
- end
165
185
 
166
- # Filters default_files down to the files that are expandable.
167
- def expandable_default_files
168
- default_files.keep_if do |path|
169
- begin
170
- File.expand_path(path)
171
- true
172
- rescue ArgumentError
173
- false
186
+ # Filters default_files down to the files that are expandable.
187
+ def expandable_default_files
188
+ default_files.keep_if do |path|
189
+ begin
190
+ File.expand_path(path)
191
+ true
192
+ rescue ArgumentError
193
+ false
194
+ end
174
195
  end
175
196
  end
176
- end
177
197
 
178
- private
198
+ private
179
199
 
200
+ TRANSLATE_CONFIG_KEY_RENAME_MAP = {
201
+ bindaddress: :bind_address,
202
+ compression: :compression,
203
+ compressionlevel: :compression_level,
204
+ certificatefile: :keycerts,
205
+ connecttimeout: :timeout,
206
+ forwardagent: :forward_agent,
207
+ identitiesonly: :keys_only,
208
+ identityagent: :identity_agent,
209
+ globalknownhostsfile: :global_known_hosts_file,
210
+ hostkeyalias: :host_key_alias,
211
+ identityfile: :keys,
212
+ fingerprinthash: :fingerprint_hash,
213
+ port: :port,
214
+ stricthostkeychecking: :strict_host_key_checking,
215
+ user: :user,
216
+ userknownhostsfile: :user_known_hosts_file,
217
+ checkhostip: :check_host_ip
218
+ }.freeze
180
219
  def translate_config_key(hash, key, value, settings)
181
- rename = {
182
- bindaddress: :bind_address,
183
- compression: :compression,
184
- compressionlevel: :compression_level,
185
- connecttimeout: :timeout,
186
- forwardagent: :forward_agent,
187
- identitiesonly: :keys_only,
188
- globalknownhostsfile: :global_known_hosts_file,
189
- hostkeyalias: :host_key_alias,
190
- identityfile: :keys,
191
- port: :port,
192
- user: :user,
193
- userknownhostsfile: :user_known_hosts_file
194
- }
195
220
  case key
196
- when :ciphers
197
- hash[:encryption] = value.split(/,/)
198
- when :hostbasedauthentication
199
- if value
200
- (hash[:auth_methods] << "hostbased").uniq!
201
- else
202
- hash[:auth_methods].delete("hostbased")
203
- end
204
- when :hostkeyalgorithms
205
- hash[:host_key] = value.split(/,/)
206
- when :hostname
207
- hash[:host_name] = value.gsub(/%h/, settings['host'])
208
- when :macs
209
- hash[:hmac] = value.split(/,/)
210
- when :serveralivecountmax
211
- hash[:keepalive_maxcount] = value.to_i if value
212
- when :serveraliveinterval
213
- if value && value.to_i > 0
214
- hash[:keepalive] = true
215
- hash[:keepalive_interval] = value.to_i
216
- else
217
- hash[:keepalive] = false
218
- end
219
- when :passwordauthentication
220
- if value
221
- (hash[:auth_methods] << 'password').uniq!
222
- else
223
- hash[:auth_methods].delete('password')
224
- end
225
- when :challengeresponseauthentication
226
- if value
227
- (hash[:auth_methods] << 'challenge-response').uniq!
228
- else
229
- hash[:auth_methods].delete('challenge-response')
230
- end
231
- when :kbdinteractiveauthentication
232
- if value
233
- (hash[:auth_methods] << 'keyboard-interactive').uniq!
234
- else
235
- hash[:auth_methods].delete('keyboard-interactive')
236
- end
237
- when :preferredauthentications
238
- hash[:auth_methods] = value.split(/,/) # TODO we should place to preferred_auth_methods rather than auth_methods
239
- when :proxycommand
240
- if value and !(value =~ /^none$/)
241
- require 'net/ssh/proxy/command'
242
- hash[:proxy] = Net::SSH::Proxy::Command.new(value)
243
- end
244
- when :proxyjump
245
- if value
246
- require 'net/ssh/proxy/jump'
247
- hash[:proxy] = Net::SSH::Proxy::Jump.new(value)
248
- end
249
- when :pubkeyauthentication
250
- if value
251
- (hash[:auth_methods] << 'publickey').uniq!
252
- else
253
- hash[:auth_methods].delete('publickey')
254
- end
255
- when :rekeylimit
256
- hash[:rekey_limit] = interpret_size(value)
257
- when :sendenv
258
- multi_send_env = value.to_s.split(/\s+/)
259
- hash[:send_env] = multi_send_env.map { |e| Regexp.new pattern2regex(e).source, false }
260
- when :numberofpasswordprompts
261
- hash[:number_of_password_prompts] = value.to_i
262
- when *rename.keys
263
- hash[rename[key]] = value
221
+ when :ciphers
222
+ hash[:encryption] = value.split(/,/)
223
+ when :hostbasedauthentication
224
+ if value
225
+ (hash[:auth_methods] << "hostbased").uniq!
226
+ else
227
+ hash[:auth_methods].delete("hostbased")
228
+ end
229
+ when :hostkeyalgorithms
230
+ hash[:host_key] = value.split(/,/)
231
+ when :hostname
232
+ hash[:host_name] = value.gsub(/%h/, settings['host'])
233
+ when :macs
234
+ hash[:hmac] = value.split(/,/)
235
+ when :serveralivecountmax
236
+ hash[:keepalive_maxcount] = value.to_i if value
237
+ when :serveraliveinterval
238
+ if value && value.to_i > 0
239
+ hash[:keepalive] = true
240
+ hash[:keepalive_interval] = value.to_i
241
+ else
242
+ hash[:keepalive] = false
243
+ end
244
+ when :passwordauthentication
245
+ if value
246
+ (hash[:auth_methods] << 'password').uniq!
247
+ else
248
+ hash[:auth_methods].delete('password')
249
+ end
250
+ when :challengeresponseauthentication
251
+ if value
252
+ (hash[:auth_methods] << 'challenge-response').uniq!
253
+ else
254
+ hash[:auth_methods].delete('challenge-response')
255
+ end
256
+ when :kbdinteractiveauthentication
257
+ if value
258
+ (hash[:auth_methods] << 'keyboard-interactive').uniq!
259
+ else
260
+ hash[:auth_methods].delete('keyboard-interactive')
261
+ end
262
+ when :preferredauthentications
263
+ hash[:auth_methods] = value.split(/,/) # TODO we should place to preferred_auth_methods rather than auth_methods
264
+ when :proxy
265
+ if (proxy = setup_proxy(*value))
266
+ hash[:proxy] = proxy
267
+ end
268
+ when :pubkeyauthentication
269
+ if value
270
+ (hash[:auth_methods] << 'publickey').uniq!
271
+ else
272
+ hash[:auth_methods].delete('publickey')
273
+ end
274
+ when :rekeylimit
275
+ hash[:rekey_limit] = interpret_size(value)
276
+ when :sendenv
277
+ multi_send_env = value.to_s.split(/\s+/)
278
+ hash[:send_env] = multi_send_env.map { |e| Regexp.new pattern2regex(e).source, false }
279
+ when :setenv
280
+ hash[:set_env] = Shellwords.split(value.to_s).map { |e| e.split '=', 2 }.to_h
281
+ when :numberofpasswordprompts
282
+ hash[:number_of_password_prompts] = value.to_i
283
+ when *TRANSLATE_CONFIG_KEY_RENAME_MAP.keys
284
+ hash[TRANSLATE_CONFIG_KEY_RENAME_MAP[key]] = value
285
+ end
286
+ end
287
+
288
+ def setup_proxy(type, value)
289
+ case type
290
+ when 'proxycommand'
291
+ if value !~ /^none$/
292
+ require 'net/ssh/proxy/command'
293
+ Net::SSH::Proxy::Command.new(value)
294
+ end
295
+ when 'proxyjump'
296
+ require 'net/ssh/proxy/jump'
297
+ Net::SSH::Proxy::Jump.new(value)
264
298
  end
265
299
  end
266
300
 
@@ -303,13 +337,57 @@ module Net; module SSH
303
337
  hash
304
338
  end
305
339
 
306
- def included_file_paths(base_dir, config_path)
307
- paths = Dir.glob(File.expand_path(config_path)).select { |f| File.file?(f) }
308
- paths += Dir.glob(File.join(base_dir, config_path)).select { |f| File.file?(f) }
309
- paths.uniq
340
+ def included_file_paths(base_dir, config_paths)
341
+ tokenize_config_value(config_paths).flat_map do |path|
342
+ Dir.glob(File.expand_path(path, base_dir)).select { |f| File.file?(f) }
343
+ end
344
+ end
345
+
346
+ # Tokenize string into tokens.
347
+ # A token is a word or a quoted sequence of words, separated by whitespaces.
348
+ def tokenize_config_value(str)
349
+ str.scan(/([^"\s]+)?(?:"([^"]+)")?\s*/).map(&:join)
310
350
  end
311
351
 
352
+ def eval_match_conditions(condition, host, settings)
353
+ # Not using `\s` for whitespace matching as canonical
354
+ # ssh_config parser implementation (OpenSSH) has specific character set.
355
+ # Ref: https://github.com/openssh/openssh-portable/blob/2581333d564d8697837729b3d07d45738eaf5a54/misc.c#L237-L239
356
+ conditions = condition.split(/[ \t\r\n]+|(?<!=)=(?!=)/).reject(&:empty?)
357
+ return true if conditions == ["all"]
358
+
359
+ conditions = conditions.each_slice(2)
360
+ condition_matches = []
361
+ conditions.each do |(kind,exprs)|
362
+ exprs = unquote(exprs)
363
+
364
+ case kind.downcase
365
+ when "all"
366
+ raise "all cannot be mixed with other conditions"
367
+ when "host"
368
+ if exprs.start_with?('!')
369
+ negated = true
370
+ exprs = exprs[1..-1]
371
+ else
372
+ negated = false
373
+ end
374
+ condition_met = false
375
+ exprs.split(",").each do |expr|
376
+ condition_met = condition_met || host =~ pattern2regex(expr)
377
+ end
378
+ condition_matches << (true && negated ^ condition_met)
379
+ # else
380
+ # warn "net-ssh: Unsupported expr in Match block: #{kind}"
381
+ end
382
+ end
383
+
384
+ !condition_matches.empty? && condition_matches.all?
385
+ end
386
+
387
+ def unquote(string)
388
+ string =~ /^"(.*)"$/ ? Regexp.last_match(1) : string
389
+ end
390
+ end
312
391
  end
313
392
  end
314
-
315
- end; end
393
+ end