diff --git a/lib/puppet/forge/repository.rb b/lib/puppet/forge/repository.rb index 7cdd12754..7badc4c28 100644 --- a/lib/puppet/forge/repository.rb +++ b/lib/puppet/forge/repository.rb @@ -1,165 +1,165 @@ require 'net/https' require 'zlib' require 'digest/sha1' require 'uri' require 'puppet/forge/errors' 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, Zlib::GzipFile::Error, ] # Instantiate a new repository instance rooted at the +url+. # The agent will report +consumer_version+ in the User-Agent to # the repository. def initialize(url, consumer_version) @uri = url.is_a?(::URI) ? url : ::URI.parse(url) @cache = Cache.new(self) @consumer_version = consumer_version end # Read HTTP proxy configurationm from Puppet's config file, or the # http_proxy environment variable. def http_proxy_env proxy_env = ENV["http_proxy"] || ENV["HTTP_PROXY"] || nil begin return URI.parse(proxy_env) if proxy_env rescue URI::InvalidURIError return nil end return nil end def http_proxy_host env = http_proxy_env if env and env.host then return env.host end if Puppet.settings[:http_proxy_host] == 'none' return nil end return Puppet.settings[:http_proxy_host] end def http_proxy_port env = http_proxy_env if env and env.port then return env.port end return Puppet.settings[:http_proxy_port] end # Return a Net::HTTPResponse read for this +request_path+. def make_http_request(request_path) - request = Net::HTTP::Get.new(request_path, { "User-Agent" => user_agent }) + request = Net::HTTP::Get.new(URI.escape(request_path), { "User-Agent" => user_agent }) if ! @uri.user.nil? && ! @uri.password.nil? request.basic_auth(@uri.user, @uri.password) end return read_response(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) http_object = get_http_object http_object.start do |http| http.request(request) 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(http_proxy_host, http_proxy_port) 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 proxy end # Return the local file name containing the data downloaded from the # repository at +release+ (e.g. "myuser-mymodule"). def retrieve(release) return cache.retrieve(@uri + release) end # Return the URI string for this repository. def to_s return @uri.to_s end # Return the cache key for this repository, this a hashed string based on # the URI. def cache_key return @cache_key ||= [ @uri.to_s.gsub(/[^[:alnum:]]+/, '_').sub(/_$/, ''), Digest::SHA1.hexdigest(@uri.to_s) ].join('-') end def user_agent "#{@consumer_version} Puppet/#{Puppet.version} (#{Facter.value(:operatingsystem)} #{Facter.value(:operatingsystemrelease)}) #{ruby_version}" end private :user_agent def ruby_version # the patchlevel is not available in ruby 1.8.5 patch = defined?(RUBY_PATCHLEVEL) ? "-p#{RUBY_PATCHLEVEL}" : "" "Ruby/#{RUBY_VERSION}#{patch} (#{RUBY_RELEASE_DATE}; #{RUBY_PLATFORM})" end private :ruby_version end end diff --git a/spec/unit/forge/repository_spec.rb b/spec/unit/forge/repository_spec.rb index 0f16fa951..a0b585b36 100644 --- a/spec/unit/forge/repository_spec.rb +++ b/spec/unit/forge/repository_spec.rb @@ -1,133 +1,143 @@ +# 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(:consumer_version) { "Test/1.0" } let(:repository) { Puppet::Forge::Repository.new('http://fake.com', consumer_version) } let(:ssl_repository) { Puppet::Forge::Repository.new('https://fake.com', consumer_version) } it "retrieve accesses the cache" do uri = URI.parse('http://some.url.com') repository.cache.expects(:retrieve).with(uri) repository.retrieve(uri) end describe 'http_proxy support' do after :each do ENV["http_proxy"] = nil end it "supports environment variable for port and host" do ENV["http_proxy"] = "http://test.com:8011" repository.http_proxy_host.should == "test.com" repository.http_proxy_port.should == 8011 end it "supports puppet configuration for port and host" do ENV["http_proxy"] = nil proxy_settings_of('test.com', 7456) repository.http_proxy_port.should == 7456 repository.http_proxy_host.should == "test.com" end it "uses environment variable before puppet settings" do ENV["http_proxy"] = "http://test1.com:8011" proxy_settings_of('test2.com', 7456) repository.http_proxy_host.should == "test1.com" repository.http_proxy_port.should == 8011 end 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 = "the http response" 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 = "the http response" 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 "the http response" 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 "the http response" 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 performs_an_http_request do |http| http.expects(:request).with() do |request| puppet_version = /Puppet\/\d+\..*/ os_info = /\(.*\)/ ruby_version = /Ruby\/\d+\.\d+\.\d+(-p\d+)? \(\d{4}-\d{2}-\d{2}; .*\)/ request["User-Agent"] =~ /^#{consumer_version} #{puppet_version} #{os_info} #{ruby_version}/ end end repository.make_http_request("the_path") 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) http = mock("http client") yield http proxy_class = mock("http proxy class") proxy = mock("http proxy") proxy_class.expects(:new).with("fake.com", 80).returns(proxy) proxy.expects(:start).yields(http).returns(result) Net::HTTP.expects(:Proxy).with("proxy", 1234).returns(proxy_class) end def performs_an_https_request(result = nil, &block) http = mock("http client") yield http proxy_class = mock("http proxy class") proxy = mock("http proxy") proxy_class.expects(:new).with("fake.com", 443).returns(proxy) proxy.expects(:start).yields(http).returns(result) proxy.expects(:use_ssl=).with(true) proxy.expects(:cert_store=) proxy.expects(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER) Net::HTTP.expects(:Proxy).with("proxy", 1234).returns(proxy_class) end end def proxy_settings_of(host, port) Puppet[:http_proxy_host] = host Puppet[:http_proxy_port] = port end end