This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class Fetcher
|
||||
class Base
|
||||
attr_reader :downloader
|
||||
attr_reader :display_uri
|
||||
attr_reader :remote
|
||||
attr_reader :gem_remote_fetcher
|
||||
|
||||
def initialize(downloader, remote, display_uri, gem_remote_fetcher)
|
||||
raise "Abstract class" if self.class == Base
|
||||
@downloader = downloader
|
||||
@remote = remote
|
||||
@display_uri = display_uri
|
||||
@gem_remote_fetcher = gem_remote_fetcher
|
||||
end
|
||||
|
||||
def remote_uri
|
||||
@remote.uri
|
||||
end
|
||||
|
||||
def fetch_uri
|
||||
@fetch_uri ||= if remote_uri.host == "rubygems.org"
|
||||
uri = remote_uri.dup
|
||||
uri.host = "index.rubygems.org"
|
||||
uri
|
||||
else
|
||||
remote_uri
|
||||
end
|
||||
end
|
||||
|
||||
def available?
|
||||
true
|
||||
end
|
||||
|
||||
def api_fetcher?
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def log_specs(&block)
|
||||
if Bundler.ui.debug?
|
||||
Bundler.ui.debug yield
|
||||
else
|
||||
Bundler.ui.info ".", false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,129 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "base"
|
||||
require_relative "../worker"
|
||||
|
||||
module Bundler
|
||||
autoload :CompactIndexClient, File.expand_path("../compact_index_client", __dir__)
|
||||
|
||||
class Fetcher
|
||||
class CompactIndex < Base
|
||||
def self.compact_index_request(method_name)
|
||||
method = instance_method(method_name)
|
||||
undef_method(method_name)
|
||||
define_method(method_name) do |*args, &blk|
|
||||
method.bind(self).call(*args, &blk)
|
||||
rescue NetworkDownError, CompactIndexClient::Updater::MismatchedChecksumError => e
|
||||
raise HTTPError, e.message
|
||||
rescue AuthenticationRequiredError, BadAuthenticationError
|
||||
# Fail since we got a 401 from the server.
|
||||
raise
|
||||
rescue HTTPError => e
|
||||
Bundler.ui.trace(e)
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def specs(gem_names)
|
||||
specs_for_names(gem_names)
|
||||
end
|
||||
compact_index_request :specs
|
||||
|
||||
def specs_for_names(gem_names)
|
||||
gem_info = []
|
||||
complete_gems = []
|
||||
remaining_gems = gem_names.dup
|
||||
|
||||
until remaining_gems.empty?
|
||||
log_specs { "Looking up gems #{remaining_gems.inspect}" }
|
||||
|
||||
deps = begin
|
||||
parallel_compact_index_client.dependencies(remaining_gems)
|
||||
rescue TooManyRequestsError
|
||||
@bundle_worker&.stop
|
||||
@bundle_worker = nil # reset it. Not sure if necessary
|
||||
serial_compact_index_client.dependencies(remaining_gems)
|
||||
end
|
||||
next_gems = deps.flat_map {|d| d[3].flat_map(&:first) }.uniq
|
||||
deps.each {|dep| gem_info << dep }
|
||||
complete_gems.concat(deps.map(&:first)).uniq!
|
||||
remaining_gems = next_gems - complete_gems
|
||||
end
|
||||
@bundle_worker&.stop
|
||||
@bundle_worker = nil # reset it. Not sure if necessary
|
||||
|
||||
gem_info
|
||||
end
|
||||
|
||||
def available?
|
||||
unless SharedHelpers.md5_available?
|
||||
Bundler.ui.debug("FIPS mode is enabled, bundler can't use the CompactIndex API")
|
||||
return nil
|
||||
end
|
||||
# Read info file checksums out of /versions, so we can know if gems are up to date
|
||||
compact_index_client.update_and_parse_checksums!
|
||||
rescue CompactIndexClient::Updater::MismatchedChecksumError => e
|
||||
Bundler.ui.debug(e.message)
|
||||
nil
|
||||
end
|
||||
compact_index_request :available?
|
||||
|
||||
def api_fetcher?
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def compact_index_client
|
||||
@compact_index_client ||=
|
||||
SharedHelpers.filesystem_access(cache_path) do
|
||||
CompactIndexClient.new(cache_path, client_fetcher)
|
||||
end
|
||||
end
|
||||
|
||||
def parallel_compact_index_client
|
||||
compact_index_client.execution_mode = lambda do |inputs, &blk|
|
||||
func = lambda {|object, _index| blk.call(object) }
|
||||
worker = bundle_worker(func)
|
||||
inputs.each {|input| worker.enq(input) }
|
||||
inputs.map { worker.deq }
|
||||
end
|
||||
|
||||
compact_index_client
|
||||
end
|
||||
|
||||
def serial_compact_index_client
|
||||
compact_index_client.sequential_execution_mode!
|
||||
compact_index_client
|
||||
end
|
||||
|
||||
def bundle_worker(func = nil)
|
||||
@bundle_worker ||= begin
|
||||
worker_name = "Compact Index (#{display_uri.host})"
|
||||
Bundler::Worker.new(Bundler.settings.processor_count, worker_name, func)
|
||||
end
|
||||
@bundle_worker.tap do |worker|
|
||||
worker.instance_variable_set(:@func, func) if func
|
||||
end
|
||||
end
|
||||
|
||||
def cache_path
|
||||
Bundler.user_cache.join("compact_index", remote.cache_slug)
|
||||
end
|
||||
|
||||
def client_fetcher
|
||||
ClientFetcher.new(self, Bundler.ui)
|
||||
end
|
||||
|
||||
ClientFetcher = Struct.new(:fetcher, :ui) do
|
||||
def call(path, headers)
|
||||
fetcher.downloader.fetch(fetcher.fetch_uri + path, headers)
|
||||
rescue NetworkDownError => e
|
||||
raise unless Bundler.feature_flag.allow_offline_install? && headers["If-None-Match"]
|
||||
ui.warn "Using the cached data for the new index because of a network error: #{e}"
|
||||
Gem::Net::HTTPNotModified.new(nil, nil, nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,78 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "base"
|
||||
require "cgi"
|
||||
|
||||
module Bundler
|
||||
class Fetcher
|
||||
class Dependency < Base
|
||||
def available?
|
||||
@available ||= fetch_uri.scheme != "file" && downloader.fetch(dependency_api_uri)
|
||||
rescue NetworkDownError => e
|
||||
raise HTTPError, e.message
|
||||
rescue AuthenticationRequiredError
|
||||
# Fail since we got a 401 from the server.
|
||||
raise
|
||||
rescue HTTPError
|
||||
false
|
||||
end
|
||||
|
||||
def api_fetcher?
|
||||
true
|
||||
end
|
||||
|
||||
def specs(gem_names, full_dependency_list = [], last_spec_list = [])
|
||||
query_list = gem_names.uniq - full_dependency_list
|
||||
|
||||
log_specs { "Query List: #{query_list.inspect}" }
|
||||
|
||||
return last_spec_list if query_list.empty?
|
||||
|
||||
spec_list, deps_list = Bundler::Retry.new("dependency api", FAIL_ERRORS).attempts do
|
||||
dependency_specs(query_list)
|
||||
end
|
||||
|
||||
returned_gems = spec_list.map(&:first).uniq
|
||||
specs(deps_list, full_dependency_list + returned_gems, spec_list + last_spec_list)
|
||||
rescue MarshalError, HTTPError, GemspecError
|
||||
Bundler.ui.info "" unless Bundler.ui.debug? # new line now that the dots are over
|
||||
Bundler.ui.debug "could not fetch from the dependency API, trying the full index"
|
||||
nil
|
||||
end
|
||||
|
||||
def dependency_specs(gem_names)
|
||||
Bundler.ui.debug "Query Gemcutter Dependency Endpoint API: #{gem_names.join(",")}"
|
||||
|
||||
gem_list = unmarshalled_dep_gems(gem_names)
|
||||
get_formatted_specs_and_deps(gem_list)
|
||||
end
|
||||
|
||||
def unmarshalled_dep_gems(gem_names)
|
||||
gem_list = []
|
||||
gem_names.each_slice(Source::Rubygems::API_REQUEST_SIZE) do |names|
|
||||
marshalled_deps = downloader.fetch(dependency_api_uri(names)).body
|
||||
gem_list.concat(Bundler.safe_load_marshal(marshalled_deps))
|
||||
end
|
||||
gem_list
|
||||
end
|
||||
|
||||
def get_formatted_specs_and_deps(gem_list)
|
||||
deps_list = []
|
||||
spec_list = []
|
||||
|
||||
gem_list.each do |s|
|
||||
deps_list.concat(s[:dependencies].map(&:first))
|
||||
deps = s[:dependencies].map {|n, d| [n, d.split(", ")] }
|
||||
spec_list.push([s[:name], s[:number], s[:platform], deps])
|
||||
end
|
||||
[spec_list, deps_list]
|
||||
end
|
||||
|
||||
def dependency_api_uri(gem_names = [])
|
||||
uri = fetch_uri + "api/v1/dependencies"
|
||||
uri.query = "gems=#{CGI.escape(gem_names.sort.join(","))}" if gem_names.any?
|
||||
uri
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,91 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class Fetcher
|
||||
class Downloader
|
||||
attr_reader :connection
|
||||
attr_reader :redirect_limit
|
||||
|
||||
def initialize(connection, redirect_limit)
|
||||
@connection = connection
|
||||
@redirect_limit = redirect_limit
|
||||
end
|
||||
|
||||
def fetch(uri, headers = {}, counter = 0)
|
||||
raise HTTPError, "Too many redirects" if counter >= redirect_limit
|
||||
|
||||
filtered_uri = URICredentialsFilter.credential_filtered_uri(uri)
|
||||
|
||||
response = request(uri, headers)
|
||||
Bundler.ui.debug("HTTP #{response.code} #{response.message} #{filtered_uri}")
|
||||
|
||||
case response
|
||||
when Gem::Net::HTTPSuccess, Gem::Net::HTTPNotModified
|
||||
response
|
||||
when Gem::Net::HTTPRedirection
|
||||
new_uri = Gem::URI.parse(response["location"])
|
||||
if new_uri.host == uri.host
|
||||
new_uri.user = uri.user
|
||||
new_uri.password = uri.password
|
||||
end
|
||||
fetch(new_uri, headers, counter + 1)
|
||||
when Gem::Net::HTTPRequestedRangeNotSatisfiable
|
||||
new_headers = headers.dup
|
||||
new_headers.delete("Range")
|
||||
new_headers["Accept-Encoding"] = "gzip"
|
||||
fetch(uri, new_headers)
|
||||
when Gem::Net::HTTPRequestEntityTooLarge
|
||||
raise FallbackError, response.body
|
||||
when Gem::Net::HTTPTooManyRequests
|
||||
raise TooManyRequestsError, response.body
|
||||
when Gem::Net::HTTPUnauthorized
|
||||
raise BadAuthenticationError, uri.host if uri.userinfo
|
||||
raise AuthenticationRequiredError, uri.host
|
||||
when Gem::Net::HTTPForbidden
|
||||
raise AuthenticationForbiddenError, uri.host
|
||||
when Gem::Net::HTTPNotFound
|
||||
raise FallbackError, "Gem::Net::HTTPNotFound: #{filtered_uri}"
|
||||
else
|
||||
message = "Gem::#{response.class.name.gsub(/\AGem::/, "")}"
|
||||
message += ": #{response.body}" unless response.body.empty?
|
||||
raise HTTPError, message
|
||||
end
|
||||
end
|
||||
|
||||
def request(uri, headers)
|
||||
validate_uri_scheme!(uri)
|
||||
|
||||
filtered_uri = URICredentialsFilter.credential_filtered_uri(uri)
|
||||
|
||||
Bundler.ui.debug "HTTP GET #{filtered_uri}"
|
||||
req = Gem::Net::HTTP::Get.new uri.request_uri, headers
|
||||
if uri.user
|
||||
user = CGI.unescape(uri.user)
|
||||
password = uri.password ? CGI.unescape(uri.password) : nil
|
||||
req.basic_auth(user, password)
|
||||
end
|
||||
connection.request(uri, req)
|
||||
rescue OpenSSL::SSL::SSLError
|
||||
raise CertificateFailureError.new(uri)
|
||||
rescue *HTTP_ERRORS => e
|
||||
Bundler.ui.trace e
|
||||
if e.is_a?(SocketError) || e.message.to_s.include?("host down:")
|
||||
raise NetworkDownError, "Could not reach host #{uri.host}. Check your network " \
|
||||
"connection and try again."
|
||||
else
|
||||
raise HTTPError, "Network error while fetching #{filtered_uri}" \
|
||||
" (#{e})"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_uri_scheme!(uri)
|
||||
return if /\Ahttps?\z/.match?(uri.scheme)
|
||||
raise InvalidOption,
|
||||
"The request uri `#{uri}` has an invalid scheme (`#{uri.scheme}`). " \
|
||||
"Did you mean `http` or `https`?"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rubygems/remote_fetcher"
|
||||
|
||||
module Bundler
|
||||
class Fetcher
|
||||
class GemRemoteFetcher < Gem::RemoteFetcher
|
||||
def request(*args)
|
||||
super do |req|
|
||||
req.delete("User-Agent") if headers["User-Agent"]
|
||||
yield req if block_given?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "base"
|
||||
|
||||
module Bundler
|
||||
class Fetcher
|
||||
class Index < Base
|
||||
def specs(_gem_names)
|
||||
Bundler.rubygems.fetch_all_remote_specs(remote, gem_remote_fetcher)
|
||||
rescue Gem::RemoteFetcher::FetchError => e
|
||||
case e.message
|
||||
when /certificate verify failed/
|
||||
raise CertificateFailureError.new(display_uri)
|
||||
when /401/
|
||||
raise BadAuthenticationError, remote_uri if remote_uri.userinfo
|
||||
raise AuthenticationRequiredError, remote_uri
|
||||
when /403/
|
||||
raise AuthenticationForbiddenError, remote_uri
|
||||
else
|
||||
raise HTTPError, "Could not fetch specs from #{display_uri} due to underlying error <#{e.message}>"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user