class Puppet::HTTP::Client

The HTTP client provides methods for making `GET`, `POST`, etc requests to HTTP(S) servers. It also provides methods for resolving Puppetserver REST service endpoints using SRV records and settings (such as `server_list`, `server`, `ca_server`, etc). Once a service endpoint has been resolved, there are methods for making REST requests (such as getting a node, sending facts, etc).

The client uses persistent HTTP connections by default unless the `Connection: close` header is specified and supports streaming response bodies.

By default the client only trusts the Puppet CA for HTTPS connections. However, if the `include_system_store` request option is set to true, then Puppet will trust certificates in the puppet-agent CA bundle.

@example To access the HTTP client:

client = Puppet.runtime[:http]

@example To make an HTTP GET request:

response = client.get(URI("http://www.example.com"))

@example To make an HTTPS GET request, trusting the puppet CA and certs in Puppet's CA bundle:

response = client.get(URI("https://www.example.com"), options: { include_system_store: true })

@example To use a URL containing special characters, such as spaces:

response = client.get(URI(Puppet::Util.uri_encode("https://www.example.com/path to file")))

@example To pass query parameters:

response = client.get(URI("https://www.example.com"), query: {'q' => 'puppet'})

@example To pass custom headers:

response = client.get(URI("https://www.example.com"), headers: {'Accept-Content' => 'application/json'})

@example To check if the response is successful (2xx):

response = client.get(URI("http://www.example.com"))
puts response.success?

@example To get the response code and reason:

response = client.get(URI("http://www.example.com"))
unless response.success?
  puts "HTTP #{response.code} #{response.reason}"
 end

@example To read response headers:

response = client.get(URI("http://www.example.com"))
puts response['Content-Type']

@example To stream the response body:

client.get(URI("http://www.example.com")) do |response|
  if response.success?
    response.read_body do |data|
      puts data
    end
  end
end

@example To handle exceptions:

begin
  client.get(URI("https://www.example.com"))
rescue Puppet::HTTP::ResponseError => e
  puts "HTTP #{e.response.code} #{e.response.reason}"
rescue Puppet::HTTP::ConnectionError => e
  puts "Connection error #{e.message}"
rescue Puppet::SSL::SSLError => e
  puts "SSL error #{e.message}"
rescue Puppet::HTTP::HTTPError => e
  puts "General HTTP error #{e.message}"
end

@example To route to the `:puppet` service:

session = client.create_session
service = session.route_to(:puppet)

@example To make a node request:

node = service.get_node(Puppet[:certname], environment: 'production')

@example To submit facts:

facts = Puppet::Indirection::Facts.indirection.find(Puppet[:certname])
service.put_facts(Puppet[:certname], environment: 'production', facts: facts)

@example To submit a report to the `:report` service:

report = Puppet::Transaction::Report.new
service = session.route_to(:report)
service.put_report(Puppet[:certname], report, environment: 'production')

@api public

Attributes

pool[R]

Public Class Methods

new(pool: Puppet::HTTP::Pool.new(Puppet[:http_keepalive_timeout]), ssl_context: nil, system_ssl_context: nil, redirect_limit: 10, retry_limit: 100) click to toggle source

Create a new http client instance. Use `Puppet.runtime` to get the current client instead of creating an instance of this class.

@param [Puppet::HTTP::Pool] pool pool of persistent Net::HTTP

connections

@param [Puppet::SSL::SSLContext] ssl_context ssl context to be used for

connections

@param [Puppet::SSL::SSLContext] system_ssl_context the system ssl context

used if :include_system_store is set to true

@param [Integer] redirect_limit default number of HTTP redirections to allow

in a given request. Can also be specified per-request.

@param [Integer] retry_limit number of HTTP retries allowed in a given

request
    # File lib/puppet/http/client.rb
105 def initialize(pool: Puppet::HTTP::Pool.new(Puppet[:http_keepalive_timeout]), ssl_context: nil, system_ssl_context: nil, redirect_limit: 10, retry_limit: 100)
106   @pool = pool
107   @default_headers = {
108     'X-Puppet-Version' => Puppet.version,
109     'User-Agent' => Puppet[:http_user_agent],
110   }.freeze
111   @default_ssl_context = ssl_context
112   @default_system_ssl_context = system_ssl_context
113   @default_redirect_limit = redirect_limit
114   @retry_after_handler = Puppet::HTTP::RetryAfterHandler.new(retry_limit, Puppet[:runinterval])
115 end

Public Instance Methods

close() click to toggle source

Close persistent connections in the pool.

@return [void]

@api public

    # File lib/puppet/http/client.rb
302 def close
303   @pool.close
304   @default_ssl_context = nil
305   @default_system_ssl_context = nil
306 end
connect(uri, options: {}) { |http| ... } click to toggle source

Open a connection to the given URI. It is typically not necessary to call this method as the client will create connections as needed when a request is made.

@param [URI] uri the connection destination @param [Hash] options @option options [Puppet::SSL::SSLContext] :ssl_context (nil) ssl context to

be used for connections

@option options [Boolean] :include_system_store (false) if we should include

the system store for connection
    # File lib/puppet/http/client.rb
137 def connect(uri, options: {}, &block)
138   start = Time.now
139   verifier = nil
140   connected = false
141 
142   site = Puppet::HTTP::Site.from_uri(uri)
143   if site.use_ssl?
144     ssl_context = options.fetch(:ssl_context, nil)
145     include_system_store = options.fetch(:include_system_store, false)
146     ctx = resolve_ssl_context(ssl_context, include_system_store)
147     verifier = Puppet::SSL::Verifier.new(site.host, ctx)
148   end
149 
150   @pool.with_connection(site, verifier) do |http|
151     connected = true
152     if block_given?
153       yield http
154     end
155   end
156 rescue Net::OpenTimeout => e
157   raise_error(_("Request to %{uri} timed out connect operation after %{elapsed} seconds") % {uri: uri, elapsed: elapsed(start)}, e, connected)
158 rescue Net::ReadTimeout => e
159   raise_error(_("Request to %{uri} timed out read operation after %{elapsed} seconds") % {uri: uri, elapsed: elapsed(start)}, e, connected)
160 rescue EOFError => e
161   raise_error(_("Request to %{uri} interrupted after %{elapsed} seconds") % {uri: uri, elapsed: elapsed(start)}, e, connected)
162 rescue Puppet::SSL::SSLError
163   raise
164 rescue Puppet::HTTP::HTTPError
165   raise
166 rescue => e
167   raise_error(_("Request to %{uri} failed after %{elapsed} seconds: %{message}") %
168               {uri: uri, elapsed: elapsed(start), message: e.message}, e, connected)
169 end
create_session() click to toggle source

Create a new HTTP session. A session is the object through which services may be connected to and accessed.

@return [Puppet::HTTP::Session] the newly created HTTP session

@api public

    # File lib/puppet/http/client.rb
123 def create_session
124   Puppet::HTTP::Session.new(self, build_resolvers)
125 end
default_ssl_context() click to toggle source
    # File lib/puppet/http/client.rb
308 def default_ssl_context
309   cert = Puppet::X509::CertProvider.new
310   password = cert.load_private_key_password
311 
312   ssl = Puppet::SSL::SSLProvider.new
313   ctx = ssl.load_context(certname: Puppet[:certname], password: password)
314   ssl.print(ctx)
315   ctx
316 rescue => e
317   # TRANSLATORS: `message` is an already translated string of why SSL failed to initialize
318   Puppet.log_exception(e, _("Failed to initialize SSL: %{message}") % { message: e.message })
319   # TRANSLATORS: `puppet agent -t` is a command and should not be translated
320   Puppet.err(_("Run `puppet agent -t`"))
321   raise e
322 end
delete(url, headers: {}, params: {}, options: {}) click to toggle source

Submits a DELETE HTTP request to the given url.

@param [URI] url the location to submit the http request @param [Hash] headers merged with the default headers defined by the client @param [Hash] params encoded and set as the url query @!macro request_options

@return [Puppet::HTTP::Response] the response

@api public

    # File lib/puppet/http/client.rb
289 def delete(url, headers: {}, params: {}, options: {})
290   url = encode_query(url, params)
291 
292   request = Net::HTTP::Delete.new(url, @default_headers.merge(headers))
293 
294   execute_streaming(request, options: options)
295 end
get(url, headers: {}, params: {}, options: {}, &block) click to toggle source

Submits a GET HTTP request to the given url

@param [URI] url the location to submit the http request @param [Hash] headers merged with the default headers defined by the client @param [Hash] params encoded and set as the url query @!macro request_options

@yield [Puppet::HTTP::Response] if a block is given yields the response

@return [Puppet::HTTP::Response] the response

@api public

    # File lib/puppet/http/client.rb
199 def get(url, headers: {}, params: {}, options: {}, &block)
200   url = encode_query(url, params)
201 
202   request = Net::HTTP::Get.new(url, @default_headers.merge(headers))
203 
204   execute_streaming(request, options: options, &block)
205 end
head(url, headers: {}, params: {}, options: {}) click to toggle source

Submits a HEAD HTTP request to the given url

@param [URI] url the location to submit the http request @param [Hash] headers merged with the default headers defined by the client @param [Hash] params encoded and set as the url query @!macro request_options

@return [Puppet::HTTP::Response] the response

@api public

    # File lib/puppet/http/client.rb
217 def head(url, headers: {}, params: {}, options: {})
218   url = encode_query(url, params)
219 
220   request = Net::HTTP::Head.new(url, @default_headers.merge(headers))
221 
222   execute_streaming(request, options: options)
223 end
post(url, body, headers: {}, params: {}, options: {}, &block) click to toggle source

Submits a POST HTTP request to the given url

@param [URI] url the location to submit the http request @param [String] body the body of the POST request @param [Hash] headers merged with the default headers defined by the client. The

`Content-Type` header is required and should correspond to the type of data passed
as the `body` argument.

@param [Hash] params encoded and set as the url query @!macro request_options

@yield [Puppet::HTTP::Response] if a block is given yields the response

@return [Puppet::HTTP::Response] the response

@api public

    # File lib/puppet/http/client.rb
266 def post(url, body, headers: {}, params: {}, options: {}, &block)
267   raise ArgumentError, "'post' requires a string 'body' argument" unless body.is_a?(String)
268   url = encode_query(url, params)
269 
270   request = Net::HTTP::Post.new(url, @default_headers.merge(headers))
271   request.body = body
272   request.content_length = body.bytesize
273 
274   raise ArgumentError, "'post' requires a 'content-type' header" unless request['Content-Type']
275 
276   execute_streaming(request, options: options, &block)
277 end
put(url, body, headers: {}, params: {}, options: {}) click to toggle source

Submits a PUT HTTP request to the given url

@param [URI] url the location to submit the http request @param [String] body the body of the PUT request @param [Hash] headers merged with the default headers defined by the client. The

`Content-Type` header is required and should correspond to the type of data passed
as the `body` argument.

@param [Hash] params encoded and set as the url query @!macro request_options

@return [Puppet::HTTP::Response] the response

@api public

    # File lib/puppet/http/client.rb
238 def put(url, body, headers: {}, params: {}, options: {})
239   raise ArgumentError, "'put' requires a string 'body' argument" unless body.is_a?(String)
240   url = encode_query(url, params)
241 
242   request = Net::HTTP::Put.new(url, @default_headers.merge(headers))
243   request.body = body
244   request.content_length = body.bytesize
245 
246   raise ArgumentError, "'put' requires a 'content-type' header" unless request['Content-Type']
247 
248   execute_streaming(request, options: options)
249 end

Protected Instance Methods

encode_query(url, params) click to toggle source
    # File lib/puppet/http/client.rb
326 def encode_query(url, params)
327   return url if params.empty?
328 
329   url = url.dup
330   url.query = encode_params(params)
331   url
332 end

Private Instance Methods

apply_auth(request, basic_auth) click to toggle source
    # File lib/puppet/http/client.rb
485 def apply_auth(request, basic_auth)
486   if basic_auth
487     request.basic_auth(basic_auth[:user], basic_auth[:password])
488   end
489 end
build_resolvers() click to toggle source
    # File lib/puppet/http/client.rb
491 def build_resolvers
492   resolvers = []
493 
494   if Puppet[:use_srv_records]
495     resolvers << Puppet::HTTP::Resolver::SRV.new(self, domain: Puppet[:srv_domain])
496   end
497 
498   server_list_setting = Puppet.settings.setting(:server_list)
499   if server_list_setting.value && !server_list_setting.value.empty?
500     # use server list to resolve all services
501     services = Puppet::HTTP::Service::SERVICE_NAMES.dup
502 
503     # except if it's been explicitly set
504     if Puppet.settings.set_by_config?(:ca_server)
505       services.delete(:ca)
506     end
507 
508     if Puppet.settings.set_by_config?(:report_server)
509       services.delete(:report)
510     end
511 
512     resolvers << Puppet::HTTP::Resolver::ServerList.new(self, server_list_setting: server_list_setting, default_port: Puppet[:serverport], services: services)
513   end
514 
515   resolvers << Puppet::HTTP::Resolver::Settings.new(self)
516 
517   resolvers.freeze
518 end
elapsed(start) click to toggle source
    # File lib/puppet/http/client.rb
450 def elapsed(start)
451   (Time.now - start).to_f.round(3)
452 end
encode_params(params) click to toggle source
    # File lib/puppet/http/client.rb
443 def encode_params(params)
444   params = expand_into_parameters(params)
445   params.map do |key, value|
446     "#{key}=#{Puppet::Util.uri_query_encode(value.to_s)}"
447   end.join('&')
448 end
execute_streaming(request, options: {}) { |response| ... } click to toggle source

Connect or borrow a connection from the pool to the host and port associated with the request's URL. Then execute the HTTP request, retrying and following redirects as needed, and return the HTTP response. The response body will always be fully drained/consumed when this method returns.

If a block is provided, then the response will be yielded to the caller, allowing the response body to be streamed.

If the request/response did not result in an exception and the caller did not ask for the connection to be closed (via Connection: close), then the connection will be returned to the pool.

@yieldparam [Puppet::HTTP::Response] response The final response, after following redirects and retrying @return [Puppet::HTTP::Response]

    # File lib/puppet/http/client.rb
351 def execute_streaming(request, options: {}, &block)
352   redirector = Puppet::HTTP::Redirector.new(options.fetch(:redirect_limit, @default_redirect_limit))
353 
354   basic_auth = options.fetch(:basic_auth, nil)
355   unless basic_auth
356     if request.uri.user && request.uri.password
357       basic_auth = { user: request.uri.user, password: request.uri.password }
358     end
359   end
360 
361   redirects = 0
362   retries = 0
363   response = nil
364   done = false
365 
366   while !done do
367     connect(request.uri, options: options) do |http|
368       apply_auth(request, basic_auth) if redirects.zero?
369 
370       # don't call return within the `request` block
371       http.request(request) do |nethttp|
372         response = Puppet::HTTP::ResponseNetHTTP.new(request.uri, nethttp)
373         begin
374           Puppet.debug("HTTP #{request.method.upcase} #{request.uri} returned #{response.code} #{response.reason}")
375 
376           if redirector.redirect?(request, response)
377             request = redirector.redirect_to(request, response, redirects)
378             redirects += 1
379             next
380           elsif @retry_after_handler.retry_after?(request, response)
381             interval = @retry_after_handler.retry_after_interval(request, response, retries)
382             retries += 1
383             if interval
384               if http.started?
385                 Puppet.debug("Closing connection for #{Puppet::HTTP::Site.from_uri(request.uri)}")
386                 http.finish
387               end
388               Puppet.warning(_("Sleeping for %{interval} seconds before retrying the request") % { interval: interval })
389               ::Kernel.sleep(interval)
390               next
391             end
392           end
393 
394           if block_given?
395             yield response
396           else
397             response.body
398           end
399         ensure
400           # we need to make sure the response body is fully consumed before
401           # the connection is put back in the pool, otherwise the response
402           # for one request could leak into a future response.
403           response.drain
404         end
405 
406         done = true
407       end
408     end
409   end
410 
411   response
412 end
expand_into_parameters(data) click to toggle source
    # File lib/puppet/http/client.rb
414 def expand_into_parameters(data)
415   data.inject([]) do |params, key_value|
416     key, value = key_value
417 
418     expanded_value = case value
419                      when Array
420                        value.collect { |val| [key, val] }
421                      else
422                        [key_value]
423                      end
424 
425     params.concat(expand_primitive_types_into_parameters(expanded_value))
426   end
427 end
expand_primitive_types_into_parameters(data) click to toggle source
    # File lib/puppet/http/client.rb
429 def expand_primitive_types_into_parameters(data)
430   data.inject([]) do |params, key_value|
431     key, value = key_value
432     case value
433     when nil
434       params
435     when true, false, String, Symbol, Integer, Float
436       params << [key, value]
437     else
438       raise Puppet::HTTP::SerializationError, _("HTTP REST queries cannot handle values of type '%{klass}'") % { klass: value.class }
439     end
440   end
441 end
raise_error(message, cause, connected) click to toggle source
    # File lib/puppet/http/client.rb
454 def raise_error(message, cause, connected)
455   if connected
456     raise Puppet::HTTP::HTTPError.new(message, cause)
457   else
458     raise Puppet::HTTP::ConnectionError.new(message, cause)
459   end
460 end
resolve_ssl_context(ssl_context, include_system_store) click to toggle source
    # File lib/puppet/http/client.rb
462 def resolve_ssl_context(ssl_context, include_system_store)
463   if ssl_context
464     raise Puppet::HTTP::HTTPError, "The ssl_context and include_system_store parameters are mutually exclusive" if include_system_store
465     ssl_context
466   elsif include_system_store
467     system_ssl_context
468   else
469     @default_ssl_context || Puppet.lookup(:ssl_context)
470   end
471 end
system_ssl_context() click to toggle source
    # File lib/puppet/http/client.rb
473 def system_ssl_context
474   return @default_system_ssl_context if @default_system_ssl_context
475 
476   cert_provider = Puppet::X509::CertProvider.new
477   cacerts = cert_provider.load_cacerts || []
478 
479   ssl = Puppet::SSL::SSLProvider.new
480   @default_system_ssl_context = ssl.create_system_context(cacerts: cacerts, include_client_cert: true)
481   ssl.print(@default_system_ssl_context)
482   @default_system_ssl_context
483 end