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
Public Class Methods
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 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
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 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
# 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
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
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
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
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
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
# 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
# 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
# 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
# File lib/puppet/http/client.rb 450 def elapsed(start) 451 (Time.now - start).to_f.round(3) 452 end
# 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
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
# 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
# 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
# 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
# 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
# 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