File: client_spec.rb

package info (click to toggle)
puppet-agent 8.10.0-5
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 27,392 kB
  • sloc: ruby: 286,820; sh: 492; xml: 116; makefile: 88; cs: 68
file content (229 lines) | stat: -rw-r--r-- 9,660 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
require 'spec_helper'
require 'puppet_spec/https'
require 'puppet_spec/files'

describe Puppet::HTTP::Client, unless: Puppet::Util::Platform.jruby? do
  include PuppetSpec::Files
  include_context "https client"

  let(:wrong_hostname) { 'localhost' }
  let(:client) { Puppet::HTTP::Client.new }
  let(:ssl_provider) { Puppet::SSL::SSLProvider.new }
  let(:root_context) { ssl_provider.create_root_context(cacerts: [https_server.ca_cert], crls: [https_server.ca_crl]) }

  context "when verifying an HTTPS server" do
    it "connects over SSL" do
      https_server.start_server do |port|
        res = client.get(URI("https://127.0.0.1:#{port}"), options: {ssl_context: root_context})
        expect(res).to be_success
      end
    end

    it "raises connection error if we can't connect" do
      Puppet[:http_connect_timeout] = '0s'

      # get available port, but don't bind to it
      tcps = TCPServer.new("127.0.0.1", 0)
      port = tcps.connect_address.ip_port

      expect {
        client.get(URI("https://127.0.0.1:#{port}"), options: {ssl_context: root_context})
      }.to raise_error(Puppet::HTTP::ConnectionError, %r{^Request to https://127.0.0.1:#{port} timed out connect operation after .* seconds})
    end

    it "raises if the server's cert doesn't match the hostname we connected to" do
      https_server.start_server do |port|
        expect {
          client.get(URI("https://#{wrong_hostname}:#{port}"), options: {ssl_context: root_context})
        }.to raise_error { |err|
          expect(err).to be_instance_of(Puppet::SSL::CertMismatchError)
          expect(err.message).to match(/Server hostname '#{wrong_hostname}' did not match server certificate; expected one of (.+)/)

          md = err.message.match(/expected one of (.+)/)
          expect(md[1].split(', ')).to contain_exactly('127.0.0.1', 'DNS:127.0.0.1', 'DNS:127.0.0.2')
        }
      end
    end

    it "raises if the server's CA is unknown" do
      wrong_ca = cert_fixture('netlock-arany-utf8.pem')
      alt_context = ssl_provider.create_root_context(cacerts: [wrong_ca], revocation: false)

      https_server.start_server do |port|
        expect {
          client.get(URI("https://127.0.0.1:#{port}"), options: {ssl_context: alt_context})
        }.to raise_error(Puppet::SSL::CertVerifyError,
                         %r{certificate verify failed.* .self.signed certificate in certificate chain for CN=Test CA.})
      end
    end

    it "prints TLS protocol and ciphersuite in debug" do
      Puppet[:log_level] = 'debug'
      https_server.start_server do |port|
        client.get(URI("https://127.0.0.1:#{port}"), options: {ssl_context: root_context})
        # TLS version string can be TLSv1 or TLSv1.[1-3], but not TLSv1.0
        expect(@logs).to include(
          an_object_having_attributes(level: :debug, message: /Using TLSv1(\.[1-3])? with cipher .*/),
        )
      end
    end
  end

  context "with client certs" do
    let(:ctx_proc) {
      -> ctx {
        # configures the server to require the client to present a client cert
        ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
      }
    }

    let(:cert_file) do
      res = tmpfile('cert_file')
      File.write(res, https_server.ca_cert)
      res
    end

    it "mutually authenticates the connection using an explicit context" do
      client_context = ssl_provider.create_context(
        cacerts: [https_server.ca_cert], crls: [https_server.ca_crl],
        client_cert: https_server.server_cert, private_key: https_server.server_key
      )

      https_server.start_server(ctx_proc: ctx_proc) do |port|
        res = client.get(URI("https://127.0.0.1:#{port}"), options: {ssl_context: client_context})
        expect(res).to be_success
      end
    end

    it "mutually authenticates the connection when the client and server certs are issued from different CAs" do
      # this is the client cert's CA, key and cert
      Puppet[:localcacert] = fixtures('ssl/unknown-ca.pem')
      Puppet[:hostprivkey] = fixtures('ssl/unknown-127.0.0.1-key.pem')
      Puppet[:hostcert] = fixtures('ssl/unknown-127.0.0.1.pem')

      # this is the server cert's CA that the client needs in order to authenticate the server
      Puppet[:ssl_trust_store] = fixtures('ssl/ca.pem')

      # need to pass both the client and server CAs. The former is needed so the server can authenticate our client cert
      https_server = PuppetSpec::HTTPSServer.new(ca_cert: [cert_fixture('ca.pem'), cert_fixture('unknown-ca.pem')])
      https_server.start_server(ctx_proc: ctx_proc) do |port|
        res = client.get(URI("https://127.0.0.1:#{port}"), options: {include_system_store: true})
        expect(res).to be_success
      end
    end

    it "connects when the server's CA is in the system store and the connection is mutually authenticated using create_context" do
      Puppet::Util.withenv("SSL_CERT_FILE" => cert_file) do
        client_context = ssl_provider.create_context(
          cacerts: [], crls: [],
          client_cert: https_server.server_cert, private_key: https_server.server_key,
          revocation: false, include_system_store: true
        )
        https_server.start_server(ctx_proc: ctx_proc) do |port|
          res = client.get(URI("https://127.0.0.1:#{port}"), options: {ssl_context: client_context})
          expect(res).to be_success
        end
      end
    end

    it "connects when the server's CA is in the system store and the connection is mutually authenticated using load_context" do
      Puppet::Util.withenv("SSL_CERT_FILE" => cert_file) do
        client_context = ssl_provider.load_context(revocation: false, include_system_store: true)
        https_server.start_server(ctx_proc: ctx_proc) do |port|
          res = client.get(URI("https://127.0.0.1:#{port}"), options: {ssl_context: client_context})
          expect(res).to be_success
        end
      end
    end
  end

  context "with a system trust store" do
    it "connects when the client trusts the server's CA" do
      system_context = ssl_provider.create_system_context(cacerts: [https_server.ca_cert])

      https_server.start_server do |port|
        res = client.get(URI("https://127.0.0.1:#{port}"), options: {ssl_context: system_context})
        expect(res).to be_success
      end
    end

    it "connects when the server's CA is in the system store" do
      # create a temp cacert bundle
      cert_file = tmpfile('cert_file')
      File.write(cert_file, https_server.ca_cert)

      # override path to system cacert bundle, this must be done before
      # the SSLContext is created and the call to X509::Store.set_default_paths
      Puppet::Util.withenv("SSL_CERT_FILE" => cert_file) do
        system_context = ssl_provider.create_system_context(cacerts: [])
        https_server.start_server do |port|
          res = client.get(URI("https://127.0.0.1:#{port}"), options: {ssl_context: system_context})
          expect(res).to be_success
        end
      end
    end

    it "raises if the server's CA is not in the context or system store" do
      system_context = ssl_provider.create_system_context(cacerts: [cert_fixture('netlock-arany-utf8.pem')])

      https_server.start_server do |port|
        expect {
          client.get(URI("https://127.0.0.1:#{port}"), options: {ssl_context: system_context})
        }.to raise_error(Puppet::SSL::CertVerifyError,
                         %r{certificate verify failed.* .self.signed certificate in certificate chain for CN=Test CA.})
      end
    end
  end

  context 'ensure that retrying does not attempt to read the body after closing the connection' do
    let(:client) { Puppet::HTTP::Client.new(retry_limit: 1) }
    it 'raises a retry error instead' do
      response_proc = -> (req, res) {
        res['Retry-After'] = 1
        res.status = 503
      }

      https_server.start_server(response_proc: response_proc) do |port|
        uri = URI("https://127.0.0.1:#{port}")
        kwargs = {headers: {'Content-Type' => 'text/plain'}, options: {ssl_context: root_context}}
        expect{client.post(uri, '', **kwargs)}.to raise_error(Puppet::HTTP::TooManyRetryAfters)
      end
    end
  end

  context 'persistent connections' do
    it "detects when the server has closed the connection and reconnects" do
      Puppet[:http_debug] = true

      # advertise that we support keep-alive, but we don't really
      response_proc = -> (req, res) {
        res['Connection'] = 'Keep-Alive'
      }

      https_server.start_server(response_proc: response_proc) do |port|
        uri = URI("https://127.0.0.1:#{port}")
        kwargs = {headers: {'Content-Type' => 'text/plain'}, options: {ssl_context: root_context}}

        expect {
          expect(client.post(uri, '', **kwargs)).to be_success
          # the server closes its connection after each request, so posting
          # again will force ruby to detect that the remote side closed the
          # connection, and reconnect
          expect(client.post(uri, '', **kwargs)).to be_success
        }.to output(/Conn close because of EOF/).to_stderr
      end
    end
  end

  context 'ciphersuites' do
    it "does not connect when using an SSLv3 ciphersuite", :if => Puppet::Util::Package.versioncmp(OpenSSL::OPENSSL_LIBRARY_VERSION.split[1], '1.1.1e') > 0 do
      Puppet[:ciphers] = "DES-CBC3-SHA"

      https_server.start_server do |port|
        expect {
          client.get(URI("https://127.0.0.1:#{port}"), options: {ssl_context: root_context})
        }.to raise_error(Puppet::HTTP::ConnectionError, /no cipher match|sslv3 alert handshake failure/)
      end
    end
  end
end