diff options
Diffstat (limited to 'lib/rubygems/request.rb')
| -rw-r--r-- | lib/rubygems/request.rb | 299 |
1 files changed, 299 insertions, 0 deletions
diff --git a/lib/rubygems/request.rb b/lib/rubygems/request.rb new file mode 100644 index 0000000000..e817ee5704 --- /dev/null +++ b/lib/rubygems/request.rb @@ -0,0 +1,299 @@ +# frozen_string_literal: true + +require_relative "vendored_net_http" +require_relative "user_interaction" +require_relative "uri_formatter" + +class Gem::Request + extend Gem::UserInteraction + include Gem::UserInteraction + + ### + # Legacy. This is used in tests. + def self.create_with_proxy(uri, request_class, last_modified, proxy) # :nodoc: + cert_files = get_cert_files + proxy ||= get_proxy_from_env(uri.scheme) + pool = ConnectionPools.new proxy_uri(proxy), cert_files + + new(uri, request_class, last_modified, pool.pool_for(uri)) + end + + def self.proxy_uri(proxy) # :nodoc: + require_relative "vendor/uri/lib/uri" + case proxy + when :no_proxy then nil + when Gem::URI::HTTP then proxy + else Gem::URI.parse(proxy) + end + end + + def initialize(uri, request_class, last_modified, pool) + @uri = uri + @request_class = request_class + @last_modified = last_modified + @requests = Hash.new(0).compare_by_identity + @user_agent = user_agent + + @connection_pool = pool + end + + def proxy_uri + @connection_pool.proxy_uri + end + + def cert_files + @connection_pool.cert_files + end + + def self.get_cert_files + pattern = File.expand_path("./ssl_certs/*/*.pem", __dir__) + Dir.glob(pattern) + end + + def self.configure_connection_for_https(connection, cert_files) + raise Gem::Exception.new("OpenSSL is not available. Install OpenSSL and rebuild Ruby (preferred) or use non-HTTPS sources") unless Gem::HAVE_OPENSSL + + 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_client_cert + pem = File.read Gem.configuration.ssl_client_cert + connection.cert = OpenSSL::X509::Certificate.new pem + connection.key = OpenSSL::PKey::RSA.new pem + end + + store.set_default_paths + cert_files.each do |ssl_cert_file| + store.add_file ssl_cert_file + end + 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 + end + connection.cert_store = store + + connection.verify_callback = proc do |preverify_ok, store_context| + verify_certificate store_context unless preverify_ok + + preverify_ok + end + + connection + end + + def self.verify_certificate(store_context) + depth = store_context.error_depth + error = store_context.error_string + number = store_context.error + cert = store_context.current_cert + + ui.alert_error "SSL verification error at depth #{depth}: #{error} (#{number})" + + extra_message = verify_certificate_message number, cert + + ui.alert_error extra_message if extra_message + end + + def self.verify_certificate_message(error_number, cert) + return unless cert + case error_number + when OpenSSL::X509::V_ERR_CERT_HAS_EXPIRED then + require "time" + "Certificate #{cert.subject} expired at #{cert.not_after.iso8601}" + when OpenSSL::X509::V_ERR_CERT_NOT_YET_VALID then + require "time" + "Certificate #{cert.subject} not valid until #{cert.not_before.iso8601}" + when OpenSSL::X509::V_ERR_CERT_REJECTED then + "Certificate #{cert.subject} is rejected" + when OpenSSL::X509::V_ERR_CERT_UNTRUSTED then + "Certificate #{cert.subject} is not trusted" + when OpenSSL::X509::V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT then + "Certificate #{cert.issuer} is not trusted" + when OpenSSL::X509::V_ERR_INVALID_CA then + "Certificate #{cert.subject} is an invalid CA certificate" + when OpenSSL::X509::V_ERR_INVALID_PURPOSE then + "Certificate #{cert.subject} has an invalid purpose" + when OpenSSL::X509::V_ERR_SELF_SIGNED_CERT_IN_CHAIN then + "Root certificate is not trusted (#{cert.subject})" + when OpenSSL::X509::V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY then + "You must add #{cert.issuer} to your local trusted store" + when + OpenSSL::X509::V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE then + "Cannot verify certificate issued by #{cert.issuer}" + end + end + + ## + # Creates or an HTTP connection based on +uri+, or retrieves an existing + # connection, using a proxy if needed. + + def connection_for(uri) + @connection_pool.checkout + rescue Gem::HAVE_OPENSSL ? OpenSSL::SSL::SSLError : Errno::EHOSTDOWN, + Errno::EHOSTDOWN => e + raise Gem::RemoteFetcher::FetchError.new(e.message, uri) + end + + def fetch + request = @request_class.new @uri.request_uri + + unless @uri.nil? || @uri.user.nil? || @uri.user.empty? + request.basic_auth Gem::UriFormatter.new(@uri.user).unescape, + Gem::UriFormatter.new(@uri.password).unescape + end + + request.add_field "User-Agent", @user_agent + request.add_field "Connection", "keep-alive" + request.add_field "Keep-Alive", "30" + + if @last_modified + require "time" + request.add_field "If-Modified-Since", @last_modified.httpdate + end + + yield request if block_given? + + perform_request request + end + + ## + # Returns a proxy URI for the given +scheme+ if one is set in the + # environment variables. + + def self.get_proxy_from_env(scheme = "http") + downcase_scheme = scheme.downcase + upcase_scheme = scheme.upcase + env_proxy = ENV["#{downcase_scheme}_proxy"] || ENV["#{upcase_scheme}_PROXY"] + + no_env_proxy = env_proxy.nil? || env_proxy.empty? + + if no_env_proxy + return ["https", "http"].include?(downcase_scheme) ? :no_proxy : get_proxy_from_env("http") + end + + require "uri" + uri = Gem::URI(Gem::UriFormatter.new(env_proxy).normalize) + + if uri && uri.user.nil? && uri.password.nil? + user = ENV["#{downcase_scheme}_proxy_user"] || ENV["#{upcase_scheme}_PROXY_USER"] + password = ENV["#{downcase_scheme}_proxy_pass"] || ENV["#{upcase_scheme}_PROXY_PASS"] + + uri.user = Gem::UriFormatter.new(user).escape + uri.password = Gem::UriFormatter.new(password).escape + end + + uri + end + + def perform_request(request) # :nodoc: + connection = connection_for @uri + + retried = false + bad_response = false + + begin + @requests[connection] += 1 + + verbose "#{request.method} #{Gem::Uri.redact(@uri)}" + + 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 Gem::Net::HTTPOK === incomplete_response + reporter.fetch(file_name, incomplete_response.content_length) + downloaded = 0 + data = String.new + + 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 + + verbose "#{response.code} #{response.message}" + rescue Gem::Net::HTTPBadResponse + verbose "bad response" + + reset connection + + raise Gem::RemoteFetcher::FetchError.new("too many bad responses", @uri) if bad_response + + bad_response = true + retry + rescue Gem::Net::HTTPFatalError + verbose "fatal error" + + raise Gem::RemoteFetcher::FetchError.new("fatal error", @uri) + # HACK: work around EOFError bug in Gem::Net::HTTP + # NOTE Errno::ECONNABORTED raised a lot on Windows, and make impossible + # to install gems. + rescue EOFError, Gem::Timeout::Error, + Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE + + requests = @requests[connection] + verbose "connection reset after #{requests} requests, retrying" + + raise Gem::RemoteFetcher::FetchError.new("too many connection resets", @uri) if retried + + reset connection + + retried = true + retry + end + + response + ensure + @connection_pool.checkin connection + end + + ## + # Resets HTTP connection +connection+. + + def reset(connection) + @requests.delete connection + + connection.finish + connection.start + end + + def user_agent + ua = "RubyGems/#{Gem::VERSION} #{Gem::Platform.local}".dup + + ruby_version = RUBY_VERSION + ruby_version += "dev" if RUBY_PATCHLEVEL == -1 + + ua << " Ruby/#{ruby_version} (#{RUBY_RELEASE_DATE}" + if RUBY_PATCHLEVEL >= 0 + ua << " patchlevel #{RUBY_PATCHLEVEL}" + else + ua << " revision #{RUBY_REVISION}" + end + ua << ")" + + ua << " #{RUBY_ENGINE}" if RUBY_ENGINE != "ruby" + + ua + end +end + +require_relative "request/http_pool" +require_relative "request/https_pool" +require_relative "request/connection_pools" |
