require 'puppet/ssl/openssl_loader'
require 'puppet/ssl/host'
require 'puppet/ssl/validator'
require 'puppet/network/http'
require 'uri'
require 'date'
require 'time'
module Puppet::Network::HTTP
# This will be raised if too many redirects happen for a given HTTP request
class RedirectionLimitExceededException < Puppet::Error ; end
# This class provides simple methods for issuing various types of HTTP
# requests. It's interface is intended to mirror Ruby's Net::HTTP
# object, but it provides a few important bits of additional
# functionality. Notably:
#
# * Any HTTPS requests made using this class will use Puppet's SSL
# certificate configuration for their authentication, and
# * Provides some useful error handling for any SSL errors that occur
# during a request.
# @api public
class Connection
OPTION_DEFAULTS = {
:use_ssl => true,
:verify => nil, # Puppet::SSL::Validator is deprecated
:verifier => nil,
:redirect_limit => 10,
}
# Creates a new HTTP client connection to `host`:`port`.
# @param host [String] the host to which this client will connect to
# @param port [Integer] the port to which this client will connect to
# @param options [Hash] options influencing the properties of the created
# connection,
# @option options [Boolean] :use_ssl true to connect with SSL, false
# otherwise, defaults to true
# @option options [#setup_connection] :verify An object that will configure
# any verification to do on the connection
# @option options [Integer] :redirect_limit the number of allowed
# redirections, defaults to 10 passing any other option in the options
# hash results in a Puppet::Error exception
#
# @note the HTTP connection itself happens lazily only when {#request}, or
# one of the {#get}, {#post}, {#delete}, {#head} or {#put} is called
# @note The correct way to obtain a connection is to use one of the factory
# methods on {Puppet::Network::HttpPool}
# @api private
def initialize(host, port, options = {})
@host = host
@port = port
unknown_options = options.keys - OPTION_DEFAULTS.keys
raise Puppet::Error, _("Unrecognized option(s): %{opts}") % { opts: unknown_options.map(&:inspect).sort.join(', ') } unless unknown_options.empty?
options = OPTION_DEFAULTS.merge(options)
@use_ssl = options[:use_ssl]
if @use_ssl
if options[:verifier]
unless options[:verifier].is_a?(Puppet::SSL::Verifier)
raise ArgumentError, _("Expected an instance of Puppet::SSL::Verifier but was passed a %{klass}") % { klass: options[:verifier].class }
end
@verifier = options[:verifier]
else
@verifier = Puppet::SSL::VerifierAdapter.new(options[:verify])
end
end
@redirect_limit = options[:redirect_limit]
@site = Puppet::Network::HTTP::Site.new(@use_ssl ? 'https' : 'http', host, port)
@pool = Puppet.lookup(:http_pool)
end
# @!macro [new] common_options
# @param options [Hash] options influencing the request made. Any
# options not recognized by this class will be ignored - no error will
# be thrown.
# @option options [Hash{Symbol => String}] :basic_auth The basic auth
# :username and :password to use for the request, :metric_id Ignored
# by this class - used by Puppet Server only. The metric id by which
# to track metrics on requests.
# @param path [String]
# @param headers [Hash{String => String}]
# @!macro common_options
# @api public
def get(path, headers = {}, options = {})
do_request(Net::HTTP::Get.new(path, headers), options)
end
# @param path [String]
# @param data [String]
# @param headers [Hash{String => String}]
# @!macro common_options
# @api public
def post(path, data, headers = nil, options = {})
request = Net::HTTP::Post.new(path, headers)
request.body = data
do_request(request, options)
end
# @param path [String]
# @param headers [Hash{String => String}]
# @!macro common_options
# @api public
def head(path, headers = {}, options = {})
do_request(Net::HTTP::Head.new(path, headers), options)
end
# @param path [String]
# @param headers [Hash{String => String}]
# @!macro common_options
# @api public
def delete(path, headers = {'Depth' => 'Infinity'}, options = {})
do_request(Net::HTTP::Delete.new(path, headers), options)
end
# @param path [String]
# @param data [String]
# @param headers [Hash{String => String}]
# @!macro common_options
# @api public
def put(path, data, headers = nil, options = {})
request = Net::HTTP::Put.new(path, headers)
request.body = data
do_request(request, options)
end
def request(method, *args)
self.send(method, *args)
end
# TODO: These are proxies for the Net::HTTP#request_* methods, which are
# almost the same as the "get", "post", etc. methods that we've ported above,
# but they are able to accept a code block and will yield to it, which is
# necessary to stream responses, e.g. file content. For now
# we're not funneling these proxy implementations through our #request
# method above, so they will not inherit the same error handling. In the
# future we may want to refactor these so that they are funneled through
# that method and do inherit the error handling.
def request_get(*args, &block)
with_connection(@site) do |http|
resp = http.request_get(*args, &block)
Puppet.debug("HTTP GET #{@site}#{args.first.split('?').first} returned #{resp.code} #{resp.message}")
resp
end
end
def request_head(*args, &block)
with_connection(@site) do |http|
resp = http.request_head(*args, &block)
Puppet.debug("HTTP HEAD #{@site}#{args.first.split('?').first} returned #{resp.code} #{resp.message}")
resp
end
end
def request_post(*args, &block)
with_connection(@site) do |http|
resp = http.request_post(*args, &block)
Puppet.debug("HTTP POST #{@site}#{args.first.split('?').first} returned #{resp.code} #{resp.message}")
resp
end
end
# end of Net::HTTP#request_* proxies
# The address to connect to.
def address
@site.host
end
# The port to connect to.
def port
@site.port
end
# Whether to use ssl
def use_ssl?
@site.use_ssl?
end
# @api private
def verifier
@verifier
end
private
def do_request(request, options)
current_request = request
current_site = @site
response = nil
0.upto(@redirect_limit) do |redirection|
return response if response
with_connection(current_site) do |connection|
apply_options_to(current_request, options)
current_response = execute_request(connection, current_request)
case current_response.code.to_i
when 301, 302, 307
# handle redirection
location = URI.parse(current_response['location'])
current_site = current_site.move_to(location)
# update to the current request path
current_request = current_request.class.new(location.path)
current_request.body = request.body
request.each do |header, value|
current_request[header] = value
end
when 429, 503
if connection.started?
Puppet.debug("Closing connection for #{current_site}")
connection.finish
end
response = handle_retry_after(current_response)
else
response = current_response
end
end
# and try again...
end
raise RedirectionLimitExceededException, _("Too many HTTP redirections for %{host}:%{port}") % { host: @host, port: @port }
end
# Handles the Retry-After header of a HTTPResponse
#
# This method checks the response for a Retry-After header and handles
# it by sleeping for the indicated number of seconds. The response is
# returned unmodified if no Retry-After header is present.
#
# @param response [Net::HTTPResponse] A response received from the
# HTTP client.
#
# @return [nil] Sleeps and returns nil if the response contained a
# Retry-After header that indicated the request should be retried.
# @return [Net::HTTPResponse] Returns the `response` unmodified if
# no Retry-After header was present or the Retry-After header could
# not be parsed as an integer or RFC 2822 date.
def handle_retry_after(response)
retry_after = response['Retry-After']
return response if retry_after.nil?
retry_sleep = parse_retry_after_header(retry_after)
# Recover remote hostname if Net::HTTPResponse was generated by a
# method that fills in the uri attribute.
#
server_hostname = if response.uri.is_a?(URI)
response.uri.host
else
# TRANSLATORS: Used in the phrase:
# "Received a response from the remote server."
_('the remote server')
end
if retry_sleep.nil?
Puppet.err(_('Received a %{status_code} response from %{server_hostname}, but the Retry-After header value of "%{retry_after}" could not be converted to an integer or RFC 2822 date.') %
{status_code: response.code,
server_hostname: server_hostname,
retry_after: retry_after.inspect})
return response
end
# Cap maximum sleep at the run interval of the Puppet agent.
retry_sleep = [retry_sleep, Puppet[:runinterval]].min
Puppet.warning(_('Received a %{status_code} response from %{server_hostname}. Sleeping for %{retry_sleep} seconds before retrying the request.') %
{status_code: response.code,
server_hostname: server_hostname,
retry_sleep: retry_sleep})
::Kernel.sleep(retry_sleep)
return nil
end
# Parse the value of a Retry-After header
#
# Parses a string containing an Integer or RFC 2822 datestamp and returns
# an integer number of seconds before a request can be retried.
#
# @param header_value [String] The value of the Retry-After header.
#
# @return [Integer] Number of seconds to wait before retrying the
# request. Will be equal to 0 for the case of date that has already
# passed.
# @return [nil] Returns `nil` when the `header_value` can't be
# parsed as an Integer or RFC 2822 date.
def parse_retry_after_header(header_value)
retry_after = begin
Integer(header_value)
rescue TypeError, ArgumentError
begin
DateTime.rfc2822(header_value)
rescue ArgumentError
return nil
end
end
case retry_after
when Integer
retry_after
when DateTime
sleep = (retry_after.to_time - DateTime.now.to_time).to_i
(sleep > 0) ? sleep : 0
end
end
def apply_options_to(request, options)
request["User-Agent"] = Puppet[:http_user_agent]
if options[:basic_auth]
request.basic_auth(options[:basic_auth][:user], options[:basic_auth][:password])
end
end
def execute_request(connection, request)
start = Time.now
resp = connection.request(request)
Puppet.debug("HTTP #{request.method.upcase} #{@site}#{request.path.split('?').first} returned #{resp.code} #{resp.message}")
resp
rescue => exception
elapsed = (Time.now - start).to_f.round(3)
uri = [@site.addr, request.path.split('?')[0]].join('/')
case exception
when EOFError
Puppet.log_exception(exception, _('request %{uri} interrupted after %{elapsed} seconds') % {uri: uri, elapsed: elapsed})
when Timeout::Error
Puppet.log_exception(exception, _('request %{uri} timed out after %{elapsed} seconds') % {uri: uri, elapsed: elapsed})
else
Puppet.log_exception(exception, _('request %{uri} failed: %{msg}') % {uri: uri, msg: exception.message})
end
raise exception
end
def with_connection(site, &block)
Puppet.deprecation_warning(_('Puppet::Network::HTTP::Connection is deprecated. Please use Puppet::Network::HTTP::ConnectionAdapter instead.'))
response = nil
@pool.with_connection(site, @verifier) do |conn|
response = yield conn
end
response
end
end
end
Copyright 2K16 - 2K18 Indonesian Hacker Rulez