diff options
Diffstat (limited to 'lib/rubygems/remote_fetcher.rb')
-rw-r--r-- | lib/rubygems/remote_fetcher.rb | 266 |
1 files changed, 259 insertions, 7 deletions
diff --git a/lib/rubygems/remote_fetcher.rb b/lib/rubygems/remote_fetcher.rb index 6abd6bd9db..86bad9de41 100644 --- a/lib/rubygems/remote_fetcher.rb +++ b/lib/rubygems/remote_fetcher.rb @@ -1,7 +1,7 @@ require 'rubygems' -require 'rubygems/request' -require 'rubygems/uri_formatter' require 'rubygems/user_interaction' +require 'thread' +require 'uri' require 'resolv' ## @@ -72,7 +72,18 @@ class Gem::RemoteFetcher Socket.do_not_reverse_lookup = true - @proxy = proxy + @connections = {} + @connections_mutex = Mutex.new + @requests = Hash.new 0 + @proxy_uri = + case proxy + when :no_proxy then nil + when nil then get_proxy_from_env + when URI::HTTP then proxy + else URI.parse(proxy) + end + @user_agent = user_agent + @env_no_proxy = get_no_proxy_from_env @dns = dns end @@ -191,7 +202,7 @@ class Gem::RemoteFetcher source_uri.path end - source_path = Gem::UriFormatter.new(source_path).unescape + source_path = unescape source_path begin FileUtils.cp source_path, local_gem_path unless @@ -310,6 +321,128 @@ class Gem::RemoteFetcher response['content-length'].to_i end + def escape(str) + return unless str + @uri_parser ||= uri_escaper + @uri_parser.escape str + end + + def unescape(str) + return unless str + @uri_parser ||= uri_escaper + @uri_parser.unescape str + end + + def uri_escaper + URI::Parser.new + rescue NameError + URI + end + + ## + # Returns list of no_proxy entries (if any) from the environment + + def get_no_proxy_from_env + env_no_proxy = ENV['no_proxy'] || ENV['NO_PROXY'] + + return [] if env_no_proxy.nil? or env_no_proxy.empty? + + env_no_proxy.split(/\s*,\s*/) + end + + ## + # Returns an HTTP proxy URI if one is set in the environment variables. + + def get_proxy_from_env + env_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY'] + + return nil if env_proxy.nil? or env_proxy.empty? + + uri = URI.parse(normalize_uri(env_proxy)) + + if uri and uri.user.nil? and uri.password.nil? then + # Probably we have http_proxy_* variables? + uri.user = escape(ENV['http_proxy_user'] || ENV['HTTP_PROXY_USER']) + uri.password = escape(ENV['http_proxy_pass'] || ENV['HTTP_PROXY_PASS']) + end + + uri + end + + ## + # Normalize the URI by adding "https://" if it is missing. + + def normalize_uri(uri) + (uri =~ /^(https?|ftp|file):/i) ? uri : "http://#{uri}" + end + + ## + # Creates or an HTTP connection based on +uri+, or retrieves an existing + # connection, using a proxy if needed. + + def connection_for(uri) + net_http_args = [uri.host, uri.port] + + if @proxy_uri and not no_proxy?(uri.host) then + net_http_args += [ + @proxy_uri.host, + @proxy_uri.port, + @proxy_uri.user, + @proxy_uri.password + ] + end + + connection_id = [Thread.current.object_id, *net_http_args].join ':' + + connection = @connections_mutex.synchronize do + @connections[connection_id] ||= Net::HTTP.new(*net_http_args) + @connections[connection_id] + end + + if https?(uri) and not connection.started? then + configure_connection_for_https(connection) + end + + connection.start unless connection.started? + + connection + rescue defined?(OpenSSL::SSL) ? OpenSSL::SSL::SSLError : Errno::EHOSTDOWN, + Errno::EHOSTDOWN => e + raise FetchError.new(e.message, uri) + end + + def configure_connection_for_https(connection) + require 'net/https' + connection.use_ssl = true + connection.verify_mode = + Gem.configuration.ssl_verify_mode || OpenSSL::SSL::VERIFY_PEER + store = OpenSSL::X509::Store.new + if Gem.configuration.ssl_ca_cert + if File.directory? Gem.configuration.ssl_ca_cert + store.add_path Gem.configuration.ssl_ca_cert + else + store.add_file Gem.configuration.ssl_ca_cert + end + else + store.set_default_paths + add_rubygems_trusted_certs(store) + end + connection.cert_store = store + rescue LoadError => e + raise unless (e.respond_to?(:path) && e.path == 'openssl') || + e.message =~ / -- openssl$/ + + raise Gem::Exception.new( + 'Unable to require openssl, install OpenSSL and rebuild ruby (preferred) or use non-HTTPS sources') + end + + def add_rubygems_trusted_certs(store) + pattern = File.expand_path("./ssl_certs/*.pem", File.dirname(__FILE__)) + Dir.glob(pattern).each do |ssl_cert_file| + store.add_file ssl_cert_file + end + end + def correct_for_windows_path(path) if path[0].chr == '/' && path[1].chr =~ /[a-z]/i && path[2].chr == ':' path = path[1..-1] @@ -318,17 +451,136 @@ class Gem::RemoteFetcher end end + def no_proxy? host + host = host.downcase + @env_no_proxy.each do |pattern| + pattern = pattern.downcase + return true if host[-pattern.length, pattern.length ] == pattern + end + return false + end + ## # Performs a Net::HTTP request of type +request_class+ on +uri+ returning # a Net::HTTP response object. request maintains a table of persistent # connections to reduce connect overhead. def request(uri, request_class, last_modified = nil) - request = Gem::Request.new uri, request_class, last_modified, @proxy + request = request_class.new uri.request_uri + + unless uri.nil? || uri.user.nil? || uri.user.empty? then + request.basic_auth uri.user, uri.password + end + + request.add_field 'User-Agent', @user_agent + request.add_field 'Connection', 'keep-alive' + request.add_field 'Keep-Alive', '30' + + if last_modified then + last_modified = last_modified.utc + request.add_field 'If-Modified-Since', last_modified.rfc2822 + end + + yield request if block_given? + + connection = connection_for uri + + retried = false + bad_response = false + + begin + @requests[connection.object_id] += 1 + + say "#{request.method} #{uri}" if + Gem.configuration.really_verbose + + file_name = File.basename(uri.path) + # perform download progress reporter only for gems + if request.response_body_permitted? && file_name =~ /\.gem$/ + reporter = ui.download_reporter + response = connection.request(request) do |incomplete_response| + if Net::HTTPOK === incomplete_response + reporter.fetch(file_name, incomplete_response.content_length) + downloaded = 0 + data = '' + + incomplete_response.read_body do |segment| + data << segment + downloaded += segment.length + reporter.update(downloaded) + end + reporter.done + if incomplete_response.respond_to? :body= + incomplete_response.body = data + else + incomplete_response.instance_variable_set(:@body, data) + end + end + end + else + response = connection.request request + end + + say "#{response.code} #{response.message}" if + Gem.configuration.really_verbose + + rescue Net::HTTPBadResponse + say "bad response" if Gem.configuration.really_verbose + + reset connection + + raise FetchError.new('too many bad responses', uri) if bad_response + + bad_response = true + retry + # HACK work around EOFError bug in Net::HTTP + # NOTE Errno::ECONNABORTED raised a lot on Windows, and make impossible + # to install gems. + rescue EOFError, Timeout::Error, + Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE + + requests = @requests[connection.object_id] + say "connection reset after #{requests} requests, retrying" if + Gem.configuration.really_verbose + + raise FetchError.new('too many connection resets', uri) if retried + + reset connection + + retried = true + retry + end - request.fetch do |req| - yield req if block_given? + response + end + + ## + # Resets HTTP connection +connection+. + + def reset(connection) + @requests.delete connection.object_id + + connection.finish + connection.start + end + + def user_agent + ua = "RubyGems/#{Gem::VERSION} #{Gem::Platform.local}" + + ruby_version = RUBY_VERSION + ruby_version += 'dev' if RUBY_PATCHLEVEL == -1 + + ua << " Ruby/#{ruby_version} (#{RUBY_RELEASE_DATE}" + if RUBY_PATCHLEVEL >= 0 then + ua << " patchlevel #{RUBY_PATCHLEVEL}" + elsif defined?(RUBY_REVISION) then + ua << " revision #{RUBY_REVISION}" end + ua << ")" + + ua << " #{RUBY_ENGINE}" if defined?(RUBY_ENGINE) and RUBY_ENGINE != 'ruby' + + ua end def https?(uri) |