Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 deletions lib/vagrant/util/downloader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ def download!
@logger.info(" -- Destination: #{@destination}")

retried = false
retried_auth = false
begin
# Get the command line args and the subprocess opts based
# on our downloader settings.
Expand All @@ -118,10 +119,24 @@ def download!
execute_curl(options, subprocess_options, &data_proc)
rescue Errors::DownloaderError => e
# If we already retried, raise it.
raise if retried
if retried && retried_auth
raise
end

@logger.error("Exit code: #{e.extra_data[:code]}")

# If a 401 is returned and we included an Authorization header,
# retry once without the Authorization header. This allows access
# to public resources when a stale/expired token is present.
if !retried_auth && Array(@headers).any? { |h| h =~ /^Authorization\b/i } &&
e.extra_data[:message].to_s.include?("401")
@logger.warn("Download received 401 with Authorization header present. Retrying without Authorization header.")
# Remove Authorization header for retry
@headers = Array(@headers).reject { |h| h =~ /^Authorization\b/i }
retried_auth = true
retry
end

# If its any error other than 33, it is an error.
raise if e.extra_data[:code].to_i != 33

Expand Down Expand Up @@ -156,8 +171,30 @@ def head
options << @source

@logger.info("HEAD: #{@source}")
result = execute_curl(options, subprocess_options)
result.stdout
begin
result = execute_curl(options, subprocess_options)
return result.stdout
rescue Errors::DownloaderError => e
# If a 401 is returned and we included an Authorization header,
# retry once without the Authorization header. This allows access
# to public resources when a stale/expired token is present.
if Array(@headers).any? { |h| h =~ /^Authorization\b/i } &&
e.extra_data[:message].to_s.include?("401")
@logger.warn("HEAD request received 401 with Authorization header present. Retrying without Authorization header.")
begin
# Remove Authorization header and retry
@headers = Array(@headers).reject { |h| h =~ /^Authorization\b/i }
options, subprocess_options = self.options
options.unshift("-I")
options << @source
result = execute_curl(options, subprocess_options)
return result.stdout
ensure
# Keep Authorization removed after a 401 to avoid repeated failures
end
end
raise
end
end

protected
Expand Down
71 changes: 71 additions & 0 deletions test/unit/vagrant/util/downloader_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,36 @@
end
end

context "when server returns 401 with Authorization header" do
let(:source) { "http://example.org/vagrant.box" }
let(:options) { {headers: ["Authorization: Bearer expired"]} }

let(:subprocess_401) do
double("subprocess_401").tap do |result|
allow(result).to receive(:exit_code).and_return(22)
allow(result).to receive(:stderr).and_return("curl: (22) The requested URL returned error: 401")
end
end

it "retries without the Authorization header and succeeds" do
first_call = ["-q", "--fail", "--location", "--max-redirs", "10",
"--verbose", "--user-agent", described_class::USER_AGENT,
"-H", "Authorization: Bearer expired",
"--output", destination, source, {}]
second_call = ["-q", "--fail", "--location", "--max-redirs", "10",
"--verbose", "--user-agent", described_class::USER_AGENT,
"--output", destination, source, {}]

expect(Vagrant::Util::Subprocess).to receive(:execute).
with("curl", *first_call).ordered.and_return(subprocess_401)

expect(Vagrant::Util::Subprocess).to receive(:execute).
with("curl", *second_call).ordered.and_return(subprocess_result)

expect(subject.download!).to be(true)
end
end

context "with UI" do
let(:ui) { Vagrant::UI::Silent.new }
let(:options) { {ui: ui} }
Expand Down Expand Up @@ -320,6 +350,47 @@

expect(subject.head).to eq("foo")
end

context "when server returns 401 with Authorization header" do
let(:source) { "http://example.org/metadata.json" }
let(:options) { {headers: ["Authorization: Bearer expired"]} }

let(:subprocess_401) do
double("subprocess_401").tap do |result|
allow(result).to receive(:exit_code).and_return(22)
allow(result).to receive(:stderr).and_return("curl: (22) The requested URL returned error: 401")
allow(result).to receive(:stdout).and_return("")
end
end

let(:subprocess_ok) do
double("subprocess_ok").tap do |result|
allow(result).to receive(:exit_code).and_return(0)
allow(result).to receive(:stderr).and_return("")
allow(result).to receive(:stdout).and_return("HTTP/1.1 200 OK\nContent-Type: application/json")
end
end

it "retries without the Authorization header and succeeds" do
# First attempt should include Authorization header and fail with 401
first_call = ["-q", "-I", "--fail", "--location", "--max-redirs", "10",
"--verbose", "--user-agent", described_class::USER_AGENT,
"-H", "Authorization: Bearer expired",
source, {}]
expect(Vagrant::Util::Subprocess).to receive(:execute).
with("curl", *first_call).ordered.and_return(subprocess_401)

# Second attempt should exclude Authorization header and succeed
second_call = ["-q", "-I", "--fail", "--location", "--max-redirs", "10",
"--verbose", "--user-agent", described_class::USER_AGENT,
source, {}]
expect(Vagrant::Util::Subprocess).to receive(:execute).
with("curl", *second_call).ordered.and_return(subprocess_ok)

# Should not raise and should return the successful output
expect(subject.head).to include("Content-Type: application/json")
end
end
end

describe "#options" do
Expand Down