Add md5 and sha1 checksum support to Downloader.
Allows checksum validation on downloaded files via Util::Downloader using MD5 and/or SHA1 checksums. This also integrates checksum validation support with the shell provisioner for downloaded remote files.
This commit is contained in:
parent
e494320ba2
commit
da45ca707c
|
@ -340,6 +340,10 @@ module Vagrant
|
|||
error_key(:downloader_interrupted)
|
||||
end
|
||||
|
||||
class DownloaderChecksumError < VagrantError
|
||||
error_key(:downloader_checksum_error)
|
||||
end
|
||||
|
||||
class EnvInval < VagrantError
|
||||
error_key(:env_inval)
|
||||
end
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
require "uri"
|
||||
|
||||
require "log4r"
|
||||
|
||||
require "digest/md5"
|
||||
require "digest/sha1"
|
||||
require "vagrant/util/busy"
|
||||
require "vagrant/util/platform"
|
||||
require "vagrant/util/subprocess"
|
||||
|
@ -18,6 +19,12 @@ module Vagrant
|
|||
# Vagrant/1.7.4 (+https://www.vagrantup.com; ruby2.1.0)
|
||||
USER_AGENT = "Vagrant/#{VERSION} (+https://www.vagrantup.com; #{RUBY_ENGINE}#{RUBY_VERSION})".freeze
|
||||
|
||||
# Supported file checksum
|
||||
CHECKSUM_MAP = {
|
||||
:md5 => Digest::MD5,
|
||||
:sha1 => Digest::SHA1
|
||||
}.freeze
|
||||
|
||||
attr_reader :source
|
||||
attr_reader :destination
|
||||
|
||||
|
@ -52,6 +59,10 @@ module Vagrant
|
|||
@ui = options[:ui]
|
||||
@client_cert = options[:client_cert]
|
||||
@location_trusted = options[:location_trusted]
|
||||
@checksums = {
|
||||
:md5 => options[:md5],
|
||||
:sha1 => options[:sha1]
|
||||
}
|
||||
end
|
||||
|
||||
# This executes the actual download, downloading the source file
|
||||
|
@ -161,6 +172,8 @@ module Vagrant
|
|||
end
|
||||
end
|
||||
|
||||
validate_download!(@source, @destination, @checksums)
|
||||
|
||||
# Everything succeeded
|
||||
true
|
||||
end
|
||||
|
@ -178,6 +191,47 @@ module Vagrant
|
|||
|
||||
protected
|
||||
|
||||
# Apply any checksum validations based on provided
|
||||
# options content
|
||||
#
|
||||
# @param source [String] Source of file
|
||||
# @param path [String, Pathname] local file path
|
||||
# @param checksums [Hash] User provided options
|
||||
# @option checksums [String] :md5 Compare MD5 checksum
|
||||
# @option checksums [String] :sha1 Compare SHA1 checksum
|
||||
# @return [Boolean]
|
||||
def validate_download!(source, path, checksums)
|
||||
CHECKSUM_MAP.each do |type, klass|
|
||||
if checksums[type]
|
||||
result = checksum_file(klass, path)
|
||||
if checksums[type] != result
|
||||
raise Errors::DownloaderChecksumError.new(
|
||||
source: source,
|
||||
path: path,
|
||||
expected_checksum: checksums[type],
|
||||
actual_checksum: result
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
# Generate checksum on given file
|
||||
#
|
||||
# @param digest_class [Class] Digest class to use for generating checksum
|
||||
# @param path [String, Pathname] Path to file
|
||||
# @return [String] hexdigest result
|
||||
def checksum_file(digest_class, path)
|
||||
digester = digest_class.new
|
||||
File.open(path.to_s, 'rb') do |file|
|
||||
while(data = file.read(1024))
|
||||
digester << data
|
||||
end
|
||||
end
|
||||
digester.hexdigest
|
||||
end
|
||||
|
||||
def execute_curl(options, subprocess_options, &data_proc)
|
||||
options = options.dup
|
||||
options << subprocess_options
|
||||
|
|
|
@ -5,6 +5,8 @@ module VagrantPlugins
|
|||
class Config < Vagrant.plugin("2", :config)
|
||||
attr_accessor :inline
|
||||
attr_accessor :path
|
||||
attr_accessor :md5
|
||||
attr_accessor :sha1
|
||||
attr_accessor :env
|
||||
attr_accessor :upload_path
|
||||
attr_accessor :args
|
||||
|
@ -19,6 +21,8 @@ module VagrantPlugins
|
|||
@args = UNSET_VALUE
|
||||
@inline = UNSET_VALUE
|
||||
@path = UNSET_VALUE
|
||||
@md5 = UNSET_VALUE
|
||||
@sha1 = UNSET_VALUE
|
||||
@env = UNSET_VALUE
|
||||
@upload_path = UNSET_VALUE
|
||||
@privileged = UNSET_VALUE
|
||||
|
@ -33,6 +37,8 @@ module VagrantPlugins
|
|||
@args = nil if @args == UNSET_VALUE
|
||||
@inline = nil if @inline == UNSET_VALUE
|
||||
@path = nil if @path == UNSET_VALUE
|
||||
@md5 = nil if @md5 == UNSET_VALUE
|
||||
@sha1 = nil if @sha1 == UNSET_VALUE
|
||||
@env = {} if @env == UNSET_VALUE
|
||||
@upload_path = "/tmp/vagrant-shell" if @upload_path == UNSET_VALUE
|
||||
@privileged = true if @privileged == UNSET_VALUE
|
||||
|
|
|
@ -177,7 +177,12 @@ module VagrantPlugins
|
|||
download_path.delete if download_path.file?
|
||||
|
||||
begin
|
||||
Vagrant::Util::Downloader.new(config.path, download_path).download!
|
||||
Vagrant::Util::Downloader.new(
|
||||
config.path,
|
||||
download_path,
|
||||
md5: config.md5,
|
||||
sha1: config.sha1
|
||||
).download!
|
||||
ext = File.extname(config.path)
|
||||
script = download_path.read
|
||||
ensure
|
||||
|
|
|
@ -734,6 +734,13 @@ en:
|
|||
downloader_interrupted: |-
|
||||
The download was interrupted by an external signal. It did not
|
||||
complete.
|
||||
downloader_checksum_error: |-
|
||||
The calculated checksum of the requested file does not match the expected
|
||||
checksum!
|
||||
|
||||
File source: %{source}
|
||||
Expected checksum: %{expected_checksum}
|
||||
Calculated checksum: %{actual_checksum}
|
||||
env_inval: |-
|
||||
Vagrant received an "EINVAL" error while attempting to set some
|
||||
environment variables. This is usually caused by the total size of your
|
||||
|
|
|
@ -2,14 +2,19 @@ require File.expand_path("../../../../base", __FILE__)
|
|||
require Vagrant.source_root.join("plugins/provisioners/shell/provisioner")
|
||||
|
||||
describe "Vagrant::Shell::Provisioner" do
|
||||
include_context "unit"
|
||||
|
||||
let(:env){ isolated_environment }
|
||||
let(:machine) {
|
||||
double(:machine).tap { |machine|
|
||||
double(:machine, env: env, id: "ID").tap { |machine|
|
||||
machine.stub_chain(:config, :vm, :communicator).and_return(:not_winrm)
|
||||
machine.stub_chain(:communicate, :tap) {}
|
||||
}
|
||||
}
|
||||
|
||||
before do
|
||||
allow(env).to receive(:tmp_path).and_return(Pathname.new("/dev/null"))
|
||||
end
|
||||
|
||||
context "with a script that contains invalid us-ascii byte sequences" do
|
||||
let(:config) {
|
||||
|
@ -37,4 +42,73 @@ describe "Vagrant::Shell::Provisioner" do
|
|||
}.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context "with remote script" do
|
||||
|
||||
context "that does not have matching sha1 checksum" do
|
||||
let(:config) {
|
||||
double(
|
||||
:config,
|
||||
:args => "doesn't matter",
|
||||
:env => {},
|
||||
:upload_path => "arbitrary",
|
||||
:remote? => true,
|
||||
:path => "http://example.com/script.sh",
|
||||
:binary => false,
|
||||
:md5 => nil,
|
||||
:sha1 => 'EXPECTED_VALUE'
|
||||
)
|
||||
}
|
||||
|
||||
let(:file){ double("file") }
|
||||
let(:digest){ double("digest") }
|
||||
before do
|
||||
Vagrant::Util::Downloader.any_instance.should_receive(:execute_curl).and_return(true)
|
||||
expect(File).to receive(:open).with(%r{/dev/null/.+}, "rb").and_yield(file).once
|
||||
allow(File).to receive(:open).and_call_original
|
||||
expect(file).to receive(:read).and_return(nil)
|
||||
expect(Digest::SHA1).to receive(:new).and_return(digest)
|
||||
expect(digest).to receive(:hexdigest).and_return('INVALID_VALUE')
|
||||
end
|
||||
|
||||
it "should raise an exception" do
|
||||
vsp = VagrantPlugins::Shell::Provisioner.new(machine, config)
|
||||
|
||||
expect{ vsp.provision }.to raise_error(Vagrant::Errors::DownloaderChecksumError)
|
||||
end
|
||||
end
|
||||
|
||||
context "that does not have matching md5 checksum" do
|
||||
let(:config) {
|
||||
double(
|
||||
:config,
|
||||
:args => "doesn't matter",
|
||||
:env => {},
|
||||
:upload_path => "arbitrary",
|
||||
:remote? => true,
|
||||
:path => "http://example.com/script.sh",
|
||||
:binary => false,
|
||||
:md5 => 'EXPECTED_VALUE',
|
||||
:sha1 => nil
|
||||
)
|
||||
}
|
||||
|
||||
let(:file){ double("file") }
|
||||
let(:digest){ double("digest") }
|
||||
before do
|
||||
Vagrant::Util::Downloader.any_instance.should_receive(:execute_curl).and_return(true)
|
||||
expect(File).to receive(:open).with(%r{/dev/null/.+}, "rb").and_yield(file).once
|
||||
allow(File).to receive(:open).and_call_original
|
||||
expect(file).to receive(:read).and_return(nil)
|
||||
expect(Digest::MD5).to receive(:new).and_return(digest)
|
||||
expect(digest).to receive(:hexdigest).and_return('INVALID_VALUE')
|
||||
end
|
||||
|
||||
it "should raise an exception" do
|
||||
vsp = VagrantPlugins::Shell::Provisioner.new(machine, config)
|
||||
|
||||
expect{ vsp.provision }.to raise_error(Vagrant::Errors::DownloaderChecksumError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -93,6 +93,107 @@ describe Vagrant::Util::Downloader do
|
|||
expect(subject.download!).to be_true
|
||||
end
|
||||
end
|
||||
|
||||
context "with checksum" do
|
||||
let(:checksum_expected_value){ 'MD5_CHECKSUM_VALUE' }
|
||||
let(:checksum_invalid_value){ 'INVALID_VALUE' }
|
||||
let(:digest){ double("digest") }
|
||||
let(:file){ double("file") }
|
||||
|
||||
before do
|
||||
allow(digest).to receive(:<<)
|
||||
allow(File).to receive(:open).and_yield(file)
|
||||
allow(file).to receive(:read).and_return(nil)
|
||||
end
|
||||
|
||||
[Digest::MD5, Digest::SHA1].each do |klass|
|
||||
short_name = klass.to_s.split("::").last.downcase
|
||||
|
||||
context "using #{short_name} digest" do
|
||||
subject { described_class.new(source, destination, short_name.to_sym => checksum_expected_value) }
|
||||
|
||||
context "that matches expected value" do
|
||||
before do
|
||||
expect(klass).to receive(:new).and_return(digest)
|
||||
expect(digest).to receive(:hexdigest).and_return(checksum_expected_value)
|
||||
end
|
||||
|
||||
it "should not raise an exception" do
|
||||
expect(subject.download!).to be_true
|
||||
end
|
||||
end
|
||||
|
||||
context "that does not match expected value" do
|
||||
before do
|
||||
expect(klass).to receive(:new).and_return(digest)
|
||||
expect(digest).to receive(:hexdigest).and_return(checksum_invalid_value)
|
||||
end
|
||||
|
||||
it "should raise an exception" do
|
||||
expect{ subject.download! }.to raise_error(Vagrant::Errors::DownloaderChecksumError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "using both md5 and sha1 digests" do
|
||||
context "that both match expected values" do
|
||||
subject { described_class.new(source, destination, md5: checksum_expected_value, sha1: checksum_expected_value) }
|
||||
|
||||
before do
|
||||
expect(Digest::MD5).to receive(:new).and_return(digest)
|
||||
expect(Digest::SHA1).to receive(:new).and_return(digest)
|
||||
expect(digest).to receive(:hexdigest).and_return(checksum_expected_value).exactly(2).times
|
||||
end
|
||||
|
||||
it "should not raise an exception" do
|
||||
expect(subject.download!).to be_true
|
||||
end
|
||||
end
|
||||
|
||||
context "that only sha1 matches expected value" do
|
||||
subject { described_class.new(source, destination, md5: checksum_invalid_value, sha1: checksum_expected_value) }
|
||||
|
||||
before do
|
||||
allow(Digest::MD5).to receive(:new).and_return(digest)
|
||||
allow(Digest::SHA1).to receive(:new).and_return(digest)
|
||||
expect(digest).to receive(:hexdigest).and_return(checksum_expected_value).at_least(:once)
|
||||
end
|
||||
|
||||
it "should raise an exception" do
|
||||
expect{ subject.download! }.to raise_error(Vagrant::Errors::DownloaderChecksumError)
|
||||
end
|
||||
end
|
||||
|
||||
context "that only md5 matches expected value" do
|
||||
subject { described_class.new(source, destination, md5: checksum_expected_value, sha1: checksum_invalid_value) }
|
||||
|
||||
before do
|
||||
allow(Digest::MD5).to receive(:new).and_return(digest)
|
||||
allow(Digest::SHA1).to receive(:new).and_return(digest)
|
||||
expect(digest).to receive(:hexdigest).and_return(checksum_expected_value).at_least(:once)
|
||||
end
|
||||
|
||||
it "should raise an exception" do
|
||||
expect{ subject.download! }.to raise_error(Vagrant::Errors::DownloaderChecksumError)
|
||||
end
|
||||
end
|
||||
|
||||
context "that none match expected value" do
|
||||
subject { described_class.new(source, destination, md5: checksum_invalid_value, sha1: checksum_invalid_value) }
|
||||
|
||||
before do
|
||||
allow(Digest::MD5).to receive(:new).and_return(digest)
|
||||
allow(Digest::SHA1).to receive(:new).and_return(digest)
|
||||
expect(digest).to receive(:hexdigest).and_return(checksum_expected_value).at_least(:once)
|
||||
end
|
||||
|
||||
it "should raise an exception" do
|
||||
expect{ subject.download! }.to raise_error(Vagrant::Errors::DownloaderChecksumError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#head" do
|
||||
|
|
|
@ -79,6 +79,10 @@ The remainder of the available options are optional:
|
|||
enable auto-login for Windows as the user must be logged in for interactive
|
||||
mode to work.
|
||||
|
||||
* `md5` (string) - MD5 checksum used to validate remotely downloaded shell files.
|
||||
|
||||
* `sha1` (string) - SHA1 checksum used to validate remotely downloaded shell files.
|
||||
|
||||
<a name="inline-scripts"></a>
|
||||
## Inline Scripts
|
||||
|
||||
|
|
Loading…
Reference in New Issue