diff --git a/lib/puppet/forge/repository.rb b/lib/puppet/forge/repository.rb index 957a013e7..969366c3a 100644 --- a/lib/puppet/forge/repository.rb +++ b/lib/puppet/forge/repository.rb @@ -1,183 +1,186 @@ require 'net/https' require 'digest/sha1' require 'uri' require 'puppet/util/http_proxy' require 'puppet/forge' require 'puppet/forge/errors' require 'puppet/network/http' class Puppet::Forge # = Repository # # This class is a file for accessing remote repositories with modules. class Repository include Puppet::Forge::Errors attr_reader :uri, :cache # List of Net::HTTP exceptions to catch NET_HTTP_EXCEPTIONS = [ EOFError, Errno::ECONNABORTED, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EINVAL, Errno::ETIMEDOUT, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, SocketError, ] if Puppet.features.zlib? NET_HTTP_EXCEPTIONS << Zlib::GzipFile::Error end # Instantiate a new repository instance rooted at the +url+. # The library will report +for_agent+ in the User-Agent to the repository. def initialize(host, for_agent) @host = host @agent = for_agent @cache = Cache.new(self) @uri = URI.parse(host) end # Return a Net::HTTPResponse read for this +path+. def make_http_request(path, io = nil) Puppet.debug "HTTP GET #{@host}#{path}" request = get_request_object(path) return read_response(request, io) end def forge_authorization if Puppet[:forge_authorization] Puppet[:forge_authorization] elsif Puppet.features.pe_license? PELicense.load_license_key.authorization_token end end def get_request_object(path) headers = { "User-Agent" => user_agent, } if Puppet.features.zlib? && RUBY_VERSION >= "1.9" headers = headers.merge({ "Accept-Encoding" => Puppet::Network::HTTP::Compression::ACCEPT_ENCODING }) end if forge_authorization headers = headers.merge({"Authorization" => forge_authorization}) end request = Net::HTTP::Get.new(URI.escape(path), headers) unless @uri.user.nil? || @uri.password.nil? || forge_authorization request.basic_auth(@uri.user, @uri.password) end return request end # Return a Net::HTTPResponse read from this HTTPRequest +request+. # # @param request [Net::HTTPRequest] request to make # @return [Net::HTTPResponse] response from request # @raise [Puppet::Forge::Errors::CommunicationError] if there is a network # related error # @raise [Puppet::Forge::Errors::SSLVerifyError] if there is a problem # verifying the remote SSL certificate def read_response(request, io = nil) http_object = get_http_object http_object.start do |http| response = http.request(request) if Puppet.features.zlib? if response && response.key?("content-encoding") case response["content-encoding"] when "gzip" response.body = Zlib::GzipReader.new(StringIO.new(response.read_body), :encoding => "ASCII-8BIT").read response.delete("content-encoding") when "deflate" response.body = Zlib::Inflate.inflate(response.read_body) response.delete("content-encoding") end end end io.write(response.body) if io.respond_to? :write response end rescue *NET_HTTP_EXCEPTIONS => e raise CommunicationError.new(:uri => @uri.to_s, :original => e) rescue OpenSSL::SSL::SSLError => e if e.message =~ /certificate verify failed/ raise SSLVerifyError.new(:uri => @uri.to_s, :original => e) else raise e end end # Return a Net::HTTP::Proxy object constructed from the settings provided # accessing the repository. # # This method optionally configures SSL correctly if the URI scheme is # 'https', including setting up the root certificate store so remote server # SSL certificates can be validated. # # @return [Net::HTTP::Proxy] object constructed from repo settings def get_http_object proxy_class = Net::HTTP::Proxy(Puppet::Util::HttpProxy.http_proxy_host, Puppet::Util::HttpProxy.http_proxy_port, Puppet::Util::HttpProxy.http_proxy_user, Puppet::Util::HttpProxy.http_proxy_password) proxy = proxy_class.new(@uri.host, @uri.port) if @uri.scheme == 'https' cert_store = OpenSSL::X509::Store.new cert_store.set_default_paths proxy.use_ssl = true proxy.verify_mode = OpenSSL::SSL::VERIFY_PEER proxy.cert_store = cert_store end if Puppet[:http_debug] proxy.set_debug_output($stderr) end + proxy.open_timeout = Puppet[:http_connect_timeout] + proxy.read_timeout = Puppet[:http_read_timeout] + proxy end # Return the local file name containing the data downloaded from the # repository at +release+ (e.g. "myuser-mymodule"). def retrieve(release) path = @host.chomp('/') + release return cache.retrieve(path) end # Return the URI string for this repository. def to_s "#<#{self.class} #{@host}>" end # Return the cache key for this repository, this a hashed string based on # the URI. def cache_key return @cache_key ||= [ @host.to_s.gsub(/[^[:alnum:]]+/, '_').sub(/_$/, ''), Digest::SHA1.hexdigest(@host.to_s) ].join('-').freeze end private def user_agent @user_agent ||= [ @agent, "Puppet/#{Puppet.version}", "Ruby/#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_PLATFORM})", ].join(' ').freeze end end end diff --git a/spec/unit/forge/repository_spec.rb b/spec/unit/forge/repository_spec.rb index a3f9d1a66..2dc9dd1f9 100644 --- a/spec/unit/forge/repository_spec.rb +++ b/spec/unit/forge/repository_spec.rb @@ -1,221 +1,224 @@ # encoding: utf-8 require 'spec_helper' require 'net/http' require 'puppet/forge/repository' require 'puppet/forge/cache' require 'puppet/forge/errors' describe Puppet::Forge::Repository do let(:agent) { "Test/1.0" } let(:repository) { Puppet::Forge::Repository.new('http://fake.com', agent) } let(:ssl_repository) { Puppet::Forge::Repository.new('https://fake.com', agent) } it "retrieve accesses the cache" do path = '/module/foo.tar.gz' repository.cache.expects(:retrieve) repository.retrieve(path) end it "retrieve merges forge URI and path specified" do host = 'http://fake.com/test' path = '/module/foo.tar.gz' uri = [ host, path ].join('') repository = Puppet::Forge::Repository.new(host, agent) repository.cache.expects(:retrieve).with(uri) repository.retrieve(path) end describe "making a request" do before :each do proxy_settings_of("proxy", 1234) end it "returns the result object from the request" do result = "#{Object.new}" performs_an_http_request result do |http| http.expects(:request).with(responds_with(:path, "the_path")) end repository.make_http_request("the_path").should == result end it 'returns the result object from a request with ssl' do result = "#{Object.new}" performs_an_https_request result do |http| http.expects(:request).with(responds_with(:path, "the_path")) end ssl_repository.make_http_request("the_path").should == result end it 'return a valid exception when there is an SSL verification problem' do performs_an_https_request "#{Object.new}" do |http| http.expects(:request).with(responds_with(:path, "the_path")).raises OpenSSL::SSL::SSLError.new("certificate verify failed") end expect { ssl_repository.make_http_request("the_path") }.to raise_error Puppet::Forge::Errors::SSLVerifyError, 'Unable to verify the SSL certificate at https://fake.com' end it 'return a valid exception when there is a communication problem' do performs_an_http_request "#{Object.new}" do |http| http.expects(:request).with(responds_with(:path, "the_path")).raises SocketError end expect { repository.make_http_request("the_path") }. to raise_error Puppet::Forge::Errors::CommunicationError, 'Unable to connect to the server at http://fake.com. Detail: SocketError.' end it "sets the user agent for the request" do path = 'the_path' request = repository.get_request_object(path) request['User-Agent'].should =~ /\b#{agent}\b/ request['User-Agent'].should =~ /\bPuppet\b/ request['User-Agent'].should =~ /\bRuby\b/ end it "Does not set Authorization header by default" do Puppet.features.stubs(:pe_license?).returns(false) Puppet[:forge_authorization] = nil request = repository.get_request_object("the_path") request['Authorization'].should == nil end it "Sets Authorization header from config" do token = 'bearer some token' Puppet[:forge_authorization] = token request = repository.get_request_object("the_path") request['Authorization'].should == token end it "escapes the received URI" do unescaped_uri = "héllo world !! ç à" performs_an_http_request do |http| http.expects(:request).with(responds_with(:path, URI.escape(unescaped_uri))) end repository.make_http_request(unescaped_uri) end def performs_an_http_request(result = nil, &block) proxy_args = ["proxy", 1234, nil, nil] mock_proxy(80, proxy_args, result, &block) end def performs_an_https_request(result = nil, &block) proxy_args = ["proxy", 1234, nil, nil] proxy = mock_proxy(443, proxy_args, result, &block) proxy.expects(:use_ssl=).with(true) proxy.expects(:cert_store=) proxy.expects(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER) end end describe "making a request against an authentiated proxy" do before :each do authenticated_proxy_settings_of("proxy", 1234, 'user1', 'password') end it "returns the result object from the request" do result = "#{Object.new}" performs_an_authenticated_http_request result do |http| http.expects(:request).with(responds_with(:path, "the_path")) end repository.make_http_request("the_path").should == result end it 'returns the result object from a request with ssl' do result = "#{Object.new}" performs_an_authenticated_https_request result do |http| http.expects(:request).with(responds_with(:path, "the_path")) end ssl_repository.make_http_request("the_path").should == result end it 'return a valid exception when there is an SSL verification problem' do performs_an_authenticated_https_request "#{Object.new}" do |http| http.expects(:request).with(responds_with(:path, "the_path")).raises OpenSSL::SSL::SSLError.new("certificate verify failed") end expect { ssl_repository.make_http_request("the_path") }.to raise_error Puppet::Forge::Errors::SSLVerifyError, 'Unable to verify the SSL certificate at https://fake.com' end it 'return a valid exception when there is a communication problem' do performs_an_authenticated_http_request "#{Object.new}" do |http| http.expects(:request).with(responds_with(:path, "the_path")).raises SocketError end expect { repository.make_http_request("the_path") }. to raise_error Puppet::Forge::Errors::CommunicationError, 'Unable to connect to the server at http://fake.com. Detail: SocketError.' end it "sets the user agent for the request" do path = 'the_path' request = repository.get_request_object(path) request['User-Agent'].should =~ /\b#{agent}\b/ request['User-Agent'].should =~ /\bPuppet\b/ request['User-Agent'].should =~ /\bRuby\b/ end it "escapes the received URI" do unescaped_uri = "héllo world !! ç à" performs_an_authenticated_http_request do |http| http.expects(:request).with(responds_with(:path, URI.escape(unescaped_uri))) end repository.make_http_request(unescaped_uri) end def performs_an_authenticated_http_request(result = nil, &block) proxy_args = ["proxy", 1234, 'user1', 'password'] mock_proxy(80, proxy_args, result, &block) end def performs_an_authenticated_https_request(result = nil, &block) proxy_args = ["proxy", 1234, 'user1', 'password'] proxy = mock_proxy(443, proxy_args, result, &block) proxy.expects(:use_ssl=).with(true) proxy.expects(:cert_store=) proxy.expects(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER) end end def proxy_settings_of(host, port) Puppet[:http_proxy_host] = host Puppet[:http_proxy_port] = port end def authenticated_proxy_settings_of(host, port, user, password) Puppet[:http_proxy_host] = host Puppet[:http_proxy_port] = port Puppet[:http_proxy_user] = user Puppet[:http_proxy_password] = password end def mock_proxy(port, proxy_args, result, &block) http = mock("http client") proxy = mock("http proxy") proxy_class = mock("http proxy class") Net::HTTP.expects(:Proxy).with(*proxy_args).returns(proxy_class) proxy_class.expects(:new).with("fake.com", port).returns(proxy) + proxy.expects(:open_timeout=) + proxy.expects(:read_timeout=) + proxy.expects(:start).yields(http).returns(result) yield http proxy end end