diff --git a/lib/puppet/network/http.rb b/lib/puppet/network/http.rb index f4323e4bb..a6f6fce60 100644 --- a/lib/puppet/network/http.rb +++ b/lib/puppet/network/http.rb @@ -1,17 +1,18 @@ module Puppet::Network::HTTP HEADER_ENABLE_PROFILING = "X-Puppet-Profiling" HEADER_PUPPET_VERSION = "X-Puppet-Version" require 'puppet/network/http/issues' require 'puppet/network/http/error' require 'puppet/network/http/route' require 'puppet/network/http/api' require 'puppet/network/http/api/v1' require 'puppet/network/http/api/v2' require 'puppet/network/http/handler' require 'puppet/network/http/response' require 'puppet/network/http/request' require 'puppet/network/http/site' require 'puppet/network/http/session' + require 'puppet/network/http/factory' require 'puppet/network/http/memory_response' end diff --git a/lib/puppet/network/http/connection.rb b/lib/puppet/network/http/connection.rb index a2ce6eef3..83570e651 100644 --- a/lib/puppet/network/http/connection.rb +++ b/lib/puppet/network/http/connection.rb @@ -1,254 +1,217 @@ require 'net/https' require 'puppet/ssl/host' require 'puppet/ssl/configuration' require 'puppet/ssl/validator' require 'puppet/network/authentication' require 'uri' 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 include Puppet::Network::Authentication OPTION_DEFAULTS = { :use_ssl => true, :verify => nil, :redirect_limit => 10, } - @@openssl_initialized = false - # Creates a new HTTP client connection to `host`:`port`. # @param host [String] the host to which this client will connect to # @param port [Fixnum] 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 [Fixnum] :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): #{unknown_options.map(&:inspect).sort.join(', ')}" unless unknown_options.empty? options = OPTION_DEFAULTS.merge(options) @use_ssl = options[:use_ssl] @verify = options[:verify] @redirect_limit = options[:redirect_limit] + @factory = Puppet::Network::HTTP::Factory.new(@verify) end # @!macro [new] common_options # @param options [Hash] options influencing the request made # @option options [Hash{Symbol => String}] :basic_auth The basic auth # :username and :password to use for the request # @param path [String] # @param headers [Hash{String => String}] # @!macro common_options # @api public def get(path, headers = {}, options = {}) request_with_redirects(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 request_with_redirects(request, options) end # @param path [String] # @param headers [Hash{String => String}] # @!macro common_options # @api public def head(path, headers = {}, options = {}) request_with_redirects(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 = {}) request_with_redirects(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 request_with_redirects(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. 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) connection.request_get(*args, &block) end def request_head(*args, &block) connection.request_head(*args, &block) end def request_post(*args, &block) connection.request_post(*args, &block) end # end of Net::HTTP#request_* proxies def address connection.address end def port connection.port end def use_ssl? connection.use_ssl? end private def request_with_redirects(request, options) current_request = request @redirect_limit.times do |redirection| apply_options_to(current_request, options) response = execute_request(current_request) return response unless [301, 302, 307].include?(response.code.to_i) # handle the redirection location = URI.parse(response['location']) @connection = initialize_connection(location.host, location.port, location.scheme == 'https') # 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 # and try again... end raise RedirectionLimitExceededException, "Too many HTTP redirections for #{@host}:#{@port}" end def apply_options_to(request, options) if options[:basic_auth] request.basic_auth(options[:basic_auth][:user], options[:basic_auth][:password]) end end def connection @connection || initialize_connection(@host, @port, @use_ssl) end def execute_request(request) response = connection.request(request) # Check the peer certs and warn if they're nearing expiration. warn_if_near_expiration(*@verify.peer_certs) response rescue OpenSSL::SSL::SSLError => error if error.message.include? "certificate verify failed" msg = error.message msg << ": [" + @verify.verify_errors.join('; ') + "]" raise Puppet::Error, msg, error.backtrace elsif error.message =~ /hostname.*not match.*server certificate/ leaf_ssl_cert = @verify.peer_certs.last valid_certnames = [leaf_ssl_cert.name, *leaf_ssl_cert.subject_alt_names].uniq msg = valid_certnames.length > 1 ? "one of #{valid_certnames.join(', ')}" : valid_certnames.first msg = "Server hostname '#{connection.address}' did not match server certificate; expected #{msg}" raise Puppet::Error, msg, error.backtrace else raise end end def initialize_connection(host, port, use_ssl) - args = [host, port] - if Puppet[:http_proxy_host] == "none" - args << nil << nil - else - args << Puppet[:http_proxy_host] << Puppet[:http_proxy_port] - end - - @connection = create_connection(*args) - - # Pop open the http client a little; older versions of Net::HTTP(s) didn't - # give us a reader for ca_file... Grr... - class << @connection; attr_accessor :ca_file; end - - @connection.use_ssl = use_ssl - # Use configured timeout (#1176) - @connection.read_timeout = Puppet[:configtimeout] - @connection.open_timeout = Puppet[:configtimeout] - - cert_setup - - @connection - end - - # Use cert information from a Puppet client to set up the http object. - def cert_setup - # PUP-1411, make sure that openssl is initialized before we try to connect - if ! @@openssl_initialized - OpenSSL::SSL::SSLContext.new - @@openssl_initialized = true - end - - @verify.setup_connection(@connection) - end - - # This method largely exists for testing purposes, so that we can - # mock the actual HTTP connection. - def create_connection(*args) - Net::HTTP.new(*args) + site = Puppet::Network::HTTP::Site.new(use_ssl ? 'https' : 'http', host, port) + @factory.create_connection(site) end end end diff --git a/lib/puppet/network/http/factory.rb b/lib/puppet/network/http/factory.rb new file mode 100644 index 000000000..fa53a3929 --- /dev/null +++ b/lib/puppet/network/http/factory.rb @@ -0,0 +1,43 @@ +require 'openssl' +require 'net/http' + +class Puppet::Network::HTTP::Factory + @@openssl_initialized = false + + def initialize(verify) + @verify = verify + + # PUP-1411, make sure that openssl is initialized before we try to connect + if ! @@openssl_initialized + OpenSSL::SSL::SSLContext.new + @@openssl_initialized = true + end + end + + def create_connection(site) + Puppet.debug("Creating new connection for #{site}") + + args = [site.host, site.port] + if Puppet[:http_proxy_host] == "none" + args << nil << nil + else + args << Puppet[:http_proxy_host] << Puppet[:http_proxy_port] + end + + http = Net::HTTP.new(*args) + + # REMIND: this probably isn't needed anymore + # Pop open the http client a little; older versions of Net::HTTP(s) didn't + # give us a reader for ca_file... Grr... + class << http; attr_accessor :ca_file; end + + http.use_ssl = site.use_ssl? + # Use configured timeout (#1176) + http.read_timeout = Puppet[:configtimeout] + http.open_timeout = Puppet[:configtimeout] + + @verify.setup_connection(http) + + http + end +end diff --git a/lib/puppet/ssl.rb b/lib/puppet/ssl.rb index 596feb933..f22c82d0e 100644 --- a/lib/puppet/ssl.rb +++ b/lib/puppet/ssl.rb @@ -1,12 +1,13 @@ # Just to make the constants work out. require 'puppet' require 'openssl' module Puppet::SSL # :nodoc: CA_NAME = "ca" + require 'puppet/ssl/configuration' require 'puppet/ssl/host' require 'puppet/ssl/oids' require 'puppet/ssl/validator' require 'puppet/ssl/validator/no_validator' require 'puppet/ssl/validator/default_validator' end diff --git a/spec/unit/network/http/factory_spec.rb b/spec/unit/network/http/factory_spec.rb new file mode 100755 index 000000000..bf0344e16 --- /dev/null +++ b/spec/unit/network/http/factory_spec.rb @@ -0,0 +1,34 @@ +#! /usr/bin/env ruby +require 'spec_helper' +require 'puppet/network/http' +require 'puppet/ssl' + +describe Puppet::Network::HTTP::Factory do + before :each do + Puppet::SSL::Key.indirection.terminus_class = :memory + Puppet::SSL::CertificateRequest.indirection.terminus_class = :memory + end + + let(:site) { Puppet::Network::HTTP::Site.new('https', 'www.example.com', 443) } + + def create_connection(site) + verifier = Puppet::SSL::Validator::DefaultValidator.new + factory = Puppet::Network::HTTP::Factory.new(verifier) + + factory.create_connection(site) + end + + it 'creates connections for our site' do + conn = create_connection(site) + + expect(conn.use_ssl?).to be_true + expect(conn.address).to eq(site.host) + expect(conn.port).to eq(site.port) + end + + it 'creates connections that have not yet started' do + conn = create_connection(site) + + expect(conn).to_not be_started + end +end