[rubygems/rubygems] User bundler UA when downloading gems
Gem::RemoteFetcher uses Gem::Request, which adds the RubyGems UA. Gem::RemoteFetcher is used to download gems, as well as the full index. We would like the bundler UA to be used whenever bundler is making requests. This PR also avoids unsafely mutating the headers hash on the shared `Gem::RemoteFetcher.fetcher` instance, which could cause corruption or incorrect headers when making parallel requests. Instead, we create one remote fetcher per rubygems remote, which is similar to the connection segregation bundler is already doing https://github.com/rubygems/rubygems/commit/f0e8dacdec
This commit is contained in:
parent
536649f819
commit
b69bbf588a
@ -844,7 +844,7 @@ module Bundler
|
|||||||
return unless SharedHelpers.md5_available?
|
return unless SharedHelpers.md5_available?
|
||||||
|
|
||||||
latest = Fetcher::CompactIndex.
|
latest = Fetcher::CompactIndex.
|
||||||
new(nil, Source::Rubygems::Remote.new(Bundler::URI("https://rubygems.org")), nil).
|
new(nil, Source::Rubygems::Remote.new(Bundler::URI("https://rubygems.org")), nil, nil).
|
||||||
send(:compact_index_client).
|
send(:compact_index_client).
|
||||||
instance_variable_get(:@cache).
|
instance_variable_get(:@cache).
|
||||||
dependencies("bundler").
|
dependencies("bundler").
|
||||||
|
@ -204,6 +204,16 @@ module Bundler
|
|||||||
fetchers.first.api_fetcher?
|
fetchers.first.api_fetcher?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def gem_remote_fetcher
|
||||||
|
@gem_remote_fetcher ||= begin
|
||||||
|
require_relative "fetcher/gem_remote_fetcher"
|
||||||
|
fetcher = GemRemoteFetcher.new Gem.configuration[:http_proxy]
|
||||||
|
fetcher.headers["User-Agent"] = user_agent
|
||||||
|
fetcher.headers["X-Gemfile-Source"] = @remote.original_uri.to_s if @remote.original_uri
|
||||||
|
fetcher
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def available_fetchers
|
def available_fetchers
|
||||||
@ -218,7 +228,7 @@ module Bundler
|
|||||||
end
|
end
|
||||||
|
|
||||||
def fetchers
|
def fetchers
|
||||||
@fetchers ||= available_fetchers.map {|f| f.new(downloader, @remote, uri) }.drop_while {|f| !f.available? }
|
@fetchers ||= available_fetchers.map {|f| f.new(downloader, @remote, uri, gem_remote_fetcher) }.drop_while {|f| !f.available? }
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_specs(gem_names)
|
def fetch_specs(gem_names)
|
||||||
|
@ -6,12 +6,14 @@ module Bundler
|
|||||||
attr_reader :downloader
|
attr_reader :downloader
|
||||||
attr_reader :display_uri
|
attr_reader :display_uri
|
||||||
attr_reader :remote
|
attr_reader :remote
|
||||||
|
attr_reader :gem_remote_fetcher
|
||||||
|
|
||||||
def initialize(downloader, remote, display_uri)
|
def initialize(downloader, remote, display_uri, gem_remote_fetcher)
|
||||||
raise "Abstract class" if self.class == Base
|
raise "Abstract class" if self.class == Base
|
||||||
@downloader = downloader
|
@downloader = downloader
|
||||||
@remote = remote
|
@remote = remote
|
||||||
@display_uri = display_uri
|
@display_uri = display_uri
|
||||||
|
@gem_remote_fetcher = gem_remote_fetcher
|
||||||
end
|
end
|
||||||
|
|
||||||
def remote_uri
|
def remote_uri
|
||||||
|
16
lib/bundler/fetcher/gem_remote_fetcher.rb
Normal file
16
lib/bundler/fetcher/gem_remote_fetcher.rb
Normal file
@ -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
|
@ -6,7 +6,7 @@ module Bundler
|
|||||||
class Fetcher
|
class Fetcher
|
||||||
class Index < Base
|
class Index < Base
|
||||||
def specs(_gem_names)
|
def specs(_gem_names)
|
||||||
Bundler.rubygems.fetch_all_remote_specs(remote)
|
Bundler.rubygems.fetch_all_remote_specs(remote, gem_remote_fetcher)
|
||||||
rescue Gem::RemoteFetcher::FetchError => e
|
rescue Gem::RemoteFetcher::FetchError => e
|
||||||
case e.message
|
case e.message
|
||||||
when /certificate verify failed/
|
when /certificate verify failed/
|
||||||
|
@ -467,11 +467,9 @@ module Bundler
|
|||||||
Gem::Specification.all = specs
|
Gem::Specification.all = specs
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_specs(remote, name)
|
def fetch_specs(remote, name, fetcher)
|
||||||
require "rubygems/remote_fetcher"
|
require "rubygems/remote_fetcher"
|
||||||
path = remote.uri.to_s + "#{name}.#{Gem.marshal_version}.gz"
|
path = remote.uri.to_s + "#{name}.#{Gem.marshal_version}.gz"
|
||||||
fetcher = gem_remote_fetcher
|
|
||||||
fetcher.headers = { "X-Gemfile-Source" => remote.original_uri.to_s } if remote.original_uri
|
|
||||||
string = fetcher.fetch_path(path)
|
string = fetcher.fetch_path(path)
|
||||||
specs = Bundler.safe_load_marshal(string)
|
specs = Bundler.safe_load_marshal(string)
|
||||||
raise MarshalError, "Specs #{name} from #{remote} is expected to be an Array but was unexpected class #{specs.class}" unless specs.is_a?(Array)
|
raise MarshalError, "Specs #{name} from #{remote} is expected to be an Array but was unexpected class #{specs.class}" unless specs.is_a?(Array)
|
||||||
@ -481,18 +479,16 @@ module Bundler
|
|||||||
raise unless name == "prerelease_specs"
|
raise unless name == "prerelease_specs"
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_all_remote_specs(remote)
|
def fetch_all_remote_specs(remote, gem_remote_fetcher)
|
||||||
specs = fetch_specs(remote, "specs")
|
specs = fetch_specs(remote, "specs", gem_remote_fetcher)
|
||||||
pres = fetch_specs(remote, "prerelease_specs") || []
|
pres = fetch_specs(remote, "prerelease_specs", gem_remote_fetcher) || []
|
||||||
|
|
||||||
specs.concat(pres)
|
specs.concat(pres)
|
||||||
end
|
end
|
||||||
|
|
||||||
def download_gem(spec, uri, cache_dir)
|
def download_gem(spec, uri, cache_dir, fetcher)
|
||||||
require "rubygems/remote_fetcher"
|
require "rubygems/remote_fetcher"
|
||||||
uri = Bundler.settings.mirror_for(uri)
|
uri = Bundler.settings.mirror_for(uri)
|
||||||
fetcher = gem_remote_fetcher
|
|
||||||
fetcher.headers = { "X-Gemfile-Source" => spec.remote.original_uri.to_s } if spec.remote.original_uri
|
|
||||||
Bundler::Retry.new("download gem from #{uri}").attempts do
|
Bundler::Retry.new("download gem from #{uri}").attempts do
|
||||||
gem_file_name = spec.file_name
|
gem_file_name = spec.file_name
|
||||||
local_gem_path = File.join cache_dir, gem_file_name
|
local_gem_path = File.join cache_dir, gem_file_name
|
||||||
@ -519,11 +515,6 @@ module Bundler
|
|||||||
raise Bundler::HTTPError, "Could not download gem from #{uri} due to underlying error <#{e.message}>"
|
raise Bundler::HTTPError, "Could not download gem from #{uri} due to underlying error <#{e.message}>"
|
||||||
end
|
end
|
||||||
|
|
||||||
def gem_remote_fetcher
|
|
||||||
require "rubygems/remote_fetcher"
|
|
||||||
Gem::RemoteFetcher.fetcher
|
|
||||||
end
|
|
||||||
|
|
||||||
def build(spec, skip_validation = false)
|
def build(spec, skip_validation = false)
|
||||||
require "rubygems/package"
|
require "rubygems/package"
|
||||||
Gem::Package.build(spec, skip_validation)
|
Gem::Package.build(spec, skip_validation)
|
||||||
|
@ -255,11 +255,15 @@ module Bundler
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetchers
|
def remote_fetchers
|
||||||
@fetchers ||= remotes.map do |uri|
|
@remote_fetchers ||= remotes.to_h do |uri|
|
||||||
remote = Source::Rubygems::Remote.new(uri)
|
remote = Source::Rubygems::Remote.new(uri)
|
||||||
Bundler::Fetcher.new(remote)
|
[remote, Bundler::Fetcher.new(remote)]
|
||||||
end
|
end.freeze
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetchers
|
||||||
|
@fetchers ||= remote_fetchers.values.freeze
|
||||||
end
|
end
|
||||||
|
|
||||||
def double_check_for(unmet_dependency_names)
|
def double_check_for(unmet_dependency_names)
|
||||||
@ -480,7 +484,8 @@ module Bundler
|
|||||||
def download_gem(spec, download_cache_path, previous_spec = nil)
|
def download_gem(spec, download_cache_path, previous_spec = nil)
|
||||||
uri = spec.remote.uri
|
uri = spec.remote.uri
|
||||||
Bundler.ui.confirm("Fetching #{version_message(spec, previous_spec)}")
|
Bundler.ui.confirm("Fetching #{version_message(spec, previous_spec)}")
|
||||||
Bundler.rubygems.download_gem(spec, uri, download_cache_path)
|
gem_remote_fetcher = remote_fetchers.fetch(spec.remote).gem_remote_fetcher
|
||||||
|
Bundler.rubygems.download_gem(spec, uri, download_cache_path, gem_remote_fetcher)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns the global cache path of the calling Rubygems::Source object.
|
# Returns the global cache path of the calling Rubygems::Source object.
|
||||||
|
@ -4,15 +4,16 @@ RSpec.describe Bundler::Fetcher::Base do
|
|||||||
let(:downloader) { double(:downloader) }
|
let(:downloader) { double(:downloader) }
|
||||||
let(:remote) { double(:remote) }
|
let(:remote) { double(:remote) }
|
||||||
let(:display_uri) { "http://sample_uri.com" }
|
let(:display_uri) { "http://sample_uri.com" }
|
||||||
|
let(:gem_remote_fetcher) { nil }
|
||||||
|
|
||||||
class TestClass < described_class; end
|
class TestClass < described_class; end
|
||||||
|
|
||||||
subject { TestClass.new(downloader, remote, display_uri) }
|
subject { TestClass.new(downloader, remote, display_uri, gem_remote_fetcher) }
|
||||||
|
|
||||||
describe "#initialize" do
|
describe "#initialize" do
|
||||||
context "with the abstract Base class" do
|
context "with the abstract Base class" do
|
||||||
it "should raise an error" do
|
it "should raise an error" do
|
||||||
expect { described_class.new(downloader, remote, display_uri) }.to raise_error(RuntimeError, "Abstract class")
|
expect { described_class.new(downloader, remote, display_uri, gem_remote_fetcher) }.to raise_error(RuntimeError, "Abstract class")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -7,7 +7,8 @@ RSpec.describe Bundler::Fetcher::CompactIndex do
|
|||||||
let(:downloader) { double(:downloader) }
|
let(:downloader) { double(:downloader) }
|
||||||
let(:display_uri) { Bundler::URI("http://sampleuri.com") }
|
let(:display_uri) { Bundler::URI("http://sampleuri.com") }
|
||||||
let(:remote) { double(:remote, :cache_slug => "lsjdf", :uri => display_uri) }
|
let(:remote) { double(:remote, :cache_slug => "lsjdf", :uri => display_uri) }
|
||||||
let(:compact_index) { described_class.new(downloader, remote, display_uri) }
|
let(:gem_remote_fetcher) { nil }
|
||||||
|
let(:compact_index) { described_class.new(downloader, remote, display_uri, gem_remote_fetcher) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(compact_index).to receive(:log_specs) {}
|
allow(compact_index).to receive(:log_specs) {}
|
||||||
|
@ -4,8 +4,9 @@ RSpec.describe Bundler::Fetcher::Dependency do
|
|||||||
let(:downloader) { double(:downloader) }
|
let(:downloader) { double(:downloader) }
|
||||||
let(:remote) { double(:remote, :uri => Bundler::URI("http://localhost:5000")) }
|
let(:remote) { double(:remote, :uri => Bundler::URI("http://localhost:5000")) }
|
||||||
let(:display_uri) { "http://sample_uri.com" }
|
let(:display_uri) { "http://sample_uri.com" }
|
||||||
|
let(:gem_remote_fetcher) { nil }
|
||||||
|
|
||||||
subject { described_class.new(downloader, remote, display_uri) }
|
subject { described_class.new(downloader, remote, display_uri, gem_remote_fetcher) }
|
||||||
|
|
||||||
describe "#available?" do
|
describe "#available?" do
|
||||||
let(:dependency_api_uri) { double(:dependency_api_uri) }
|
let(:dependency_api_uri) { double(:dependency_api_uri) }
|
||||||
|
@ -8,8 +8,9 @@ RSpec.describe Bundler::Fetcher::Index do
|
|||||||
let(:display_uri) { "http://sample_uri.com" }
|
let(:display_uri) { "http://sample_uri.com" }
|
||||||
let(:rubygems) { double(:rubygems) }
|
let(:rubygems) { double(:rubygems) }
|
||||||
let(:gem_names) { %w[foo bar] }
|
let(:gem_names) { %w[foo bar] }
|
||||||
|
let(:gem_remote_fetcher) { nil }
|
||||||
|
|
||||||
subject { described_class.new(downloader, remote, display_uri) }
|
subject { described_class.new(downloader, remote, display_uri, gem_remote_fetcher) }
|
||||||
|
|
||||||
before { allow(Bundler).to receive(:rubygems).and_return(rubygems) }
|
before { allow(Bundler).to receive(:rubygems).and_return(rubygems) }
|
||||||
|
|
||||||
|
@ -46,14 +46,12 @@ RSpec.describe Bundler::RubygemsIntegration do
|
|||||||
let(:fetcher) { double("gem_remote_fetcher") }
|
let(:fetcher) { double("gem_remote_fetcher") }
|
||||||
|
|
||||||
it "successfully downloads gem with retries" do
|
it "successfully downloads gem with retries" do
|
||||||
expect(Bundler.rubygems).to receive(:gem_remote_fetcher).and_return(fetcher)
|
|
||||||
expect(fetcher).to receive(:headers=).with({ "X-Gemfile-Source" => "https://foo.bar" })
|
|
||||||
expect(Bundler::Retry).to receive(:new).with("download gem from #{uri}/").
|
expect(Bundler::Retry).to receive(:new).with("download gem from #{uri}/").
|
||||||
and_return(bundler_retry)
|
and_return(bundler_retry)
|
||||||
expect(bundler_retry).to receive(:attempts).and_yield
|
expect(bundler_retry).to receive(:attempts).and_yield
|
||||||
expect(fetcher).to receive(:cache_update_path)
|
expect(fetcher).to receive(:cache_update_path)
|
||||||
|
|
||||||
Bundler.rubygems.download_gem(spec, uri, cache_dir)
|
Bundler.rubygems.download_gem(spec, uri, cache_dir, fetcher)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -68,11 +66,9 @@ RSpec.describe Bundler::RubygemsIntegration do
|
|||||||
let(:remote_with_mirror) { double("remote", :uri => uri, :original_uri => orig_uri) }
|
let(:remote_with_mirror) { double("remote", :uri => uri, :original_uri => orig_uri) }
|
||||||
|
|
||||||
it "sets the 'X-Gemfile-Source' header containing the original source" do
|
it "sets the 'X-Gemfile-Source' header containing the original source" do
|
||||||
expect(Bundler.rubygems).to receive(:gem_remote_fetcher).twice.and_return(fetcher)
|
|
||||||
expect(fetcher).to receive(:headers=).with({ "X-Gemfile-Source" => "http://zombo.com" }).twice
|
|
||||||
expect(fetcher).to receive(:fetch_path).with(uri + "specs.4.8.gz").and_return(specs_response)
|
expect(fetcher).to receive(:fetch_path).with(uri + "specs.4.8.gz").and_return(specs_response)
|
||||||
expect(fetcher).to receive(:fetch_path).with(uri + "prerelease_specs.4.8.gz").and_return(prerelease_specs_response)
|
expect(fetcher).to receive(:fetch_path).with(uri + "prerelease_specs.4.8.gz").and_return(prerelease_specs_response)
|
||||||
result = Bundler.rubygems.fetch_all_remote_specs(remote_with_mirror)
|
result = Bundler.rubygems.fetch_all_remote_specs(remote_with_mirror, fetcher)
|
||||||
expect(result).to eq(%w[specs prerelease_specs])
|
expect(result).to eq(%w[specs prerelease_specs])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -81,11 +77,9 @@ RSpec.describe Bundler::RubygemsIntegration do
|
|||||||
let(:remote_no_mirror) { double("remote", :uri => uri, :original_uri => nil) }
|
let(:remote_no_mirror) { double("remote", :uri => uri, :original_uri => nil) }
|
||||||
|
|
||||||
it "does not set the 'X-Gemfile-Source' header" do
|
it "does not set the 'X-Gemfile-Source' header" do
|
||||||
expect(Bundler.rubygems).to receive(:gem_remote_fetcher).twice.and_return(fetcher)
|
|
||||||
expect(fetcher).to_not receive(:headers=)
|
|
||||||
expect(fetcher).to receive(:fetch_path).with(uri + "specs.4.8.gz").and_return(specs_response)
|
expect(fetcher).to receive(:fetch_path).with(uri + "specs.4.8.gz").and_return(specs_response)
|
||||||
expect(fetcher).to receive(:fetch_path).with(uri + "prerelease_specs.4.8.gz").and_return(prerelease_specs_response)
|
expect(fetcher).to receive(:fetch_path).with(uri + "prerelease_specs.4.8.gz").and_return(prerelease_specs_response)
|
||||||
result = Bundler.rubygems.fetch_all_remote_specs(remote_no_mirror)
|
result = Bundler.rubygems.fetch_all_remote_specs(remote_no_mirror, fetcher)
|
||||||
expect(result).to eq(%w[specs prerelease_specs])
|
expect(result).to eq(%w[specs prerelease_specs])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -95,9 +89,8 @@ RSpec.describe Bundler::RubygemsIntegration do
|
|||||||
let(:unexpected_specs_response) { Marshal.dump(3) }
|
let(:unexpected_specs_response) { Marshal.dump(3) }
|
||||||
|
|
||||||
it "raises a MarshalError error" do
|
it "raises a MarshalError error" do
|
||||||
expect(Bundler.rubygems).to receive(:gem_remote_fetcher).once.and_return(fetcher)
|
|
||||||
expect(fetcher).to receive(:fetch_path).with(uri + "specs.4.8.gz").and_return(unexpected_specs_response)
|
expect(fetcher).to receive(:fetch_path).with(uri + "specs.4.8.gz").and_return(unexpected_specs_response)
|
||||||
expect { Bundler.rubygems.fetch_all_remote_specs(remote_no_mirror) }.to raise_error(Bundler::MarshalError, /unexpected class/i)
|
expect { Bundler.rubygems.fetch_all_remote_specs(remote_no_mirror, fetcher) }.to raise_error(Bundler::MarshalError, /unexpected class/i)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -17,7 +17,7 @@ RSpec.describe "fetching dependencies with a mirrored source", :realworld => tru
|
|||||||
@t.join
|
@t.join
|
||||||
end
|
end
|
||||||
|
|
||||||
it "sets the 'X-Gemfile-Source' header and bundles successfully" do
|
it "sets the 'X-Gemfile-Source' and 'User-Agent' headers and bundles successfully" do
|
||||||
gemfile <<-G
|
gemfile <<-G
|
||||||
source "#{mirror}"
|
source "#{mirror}"
|
||||||
gem 'weakling'
|
gem 'weakling'
|
||||||
|
@ -4,7 +4,7 @@ require_relative "helpers/endpoint"
|
|||||||
|
|
||||||
class EndpointMirrorSource < Endpoint
|
class EndpointMirrorSource < Endpoint
|
||||||
get "/gems/:id" do
|
get "/gems/:id" do
|
||||||
if request.env["HTTP_X_GEMFILE_SOURCE"] == "https://server.example.org/"
|
if request.env["HTTP_X_GEMFILE_SOURCE"] == "https://server.example.org/" && request.env["HTTP_USER_AGENT"].start_with?("bundler")
|
||||||
File.binread("#{gem_repo1}/gems/#{params[:id]}")
|
File.binread("#{gem_repo1}/gems/#{params[:id]}")
|
||||||
else
|
else
|
||||||
halt 500
|
halt 500
|
||||||
|
Loading…
x
Reference in New Issue
Block a user