net-ssh 4.1.0 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
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