Merge pull request #7985 from chrisroberts/shell-provisioner/checksum

Add md5 and sha1 checksum support to Downloader.
This commit is contained in:
Chris Roberts 2016-11-14 13:19:22 -08:00 committed by GitHub
commit 0f720a4386
8 changed files with 249 additions and 3 deletions

View File

@ -340,6 +340,10 @@ module Vagrant
error_key(:downloader_interrupted) error_key(:downloader_interrupted)
end end
class DownloaderChecksumError < VagrantError
error_key(:downloader_checksum_error)
end
class EnvInval < VagrantError class EnvInval < VagrantError
error_key(:env_inval) error_key(:env_inval)
end end

View File

@ -1,7 +1,8 @@
require "uri" require "uri"
require "log4r" require "log4r"
require "digest/md5"
require "digest/sha1"
require "vagrant/util/busy" require "vagrant/util/busy"
require "vagrant/util/platform" require "vagrant/util/platform"
require "vagrant/util/subprocess" require "vagrant/util/subprocess"
@ -18,6 +19,12 @@ module Vagrant
# Vagrant/1.7.4 (+https://www.vagrantup.com; ruby2.1.0) # Vagrant/1.7.4 (+https://www.vagrantup.com; ruby2.1.0)
USER_AGENT = "Vagrant/#{VERSION} (+https://www.vagrantup.com; #{RUBY_ENGINE}#{RUBY_VERSION})".freeze 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 :source
attr_reader :destination attr_reader :destination
@ -52,6 +59,10 @@ module Vagrant
@ui = options[:ui] @ui = options[:ui]
@client_cert = options[:client_cert] @client_cert = options[:client_cert]
@location_trusted = options[:location_trusted] @location_trusted = options[:location_trusted]
@checksums = {
:md5 => options[:md5],
:sha1 => options[:sha1]
}
end end
# This executes the actual download, downloading the source file # This executes the actual download, downloading the source file
@ -161,6 +172,8 @@ module Vagrant
end end
end end
validate_download!(@source, @destination, @checksums)
# Everything succeeded # Everything succeeded
true true
end end
@ -178,6 +191,46 @@ module Vagrant
protected 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)
@logger.debug("Validating checksum (#{type}) for #{source}. " \
"expected: #{checksums[type]} actual: #{result}")
if checksums[type] != result
raise Errors::DownloaderChecksumError.new(
source: source,
path: path,
type: type,
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
digester.file(path)
digester.hexdigest
end
def execute_curl(options, subprocess_options, &data_proc) def execute_curl(options, subprocess_options, &data_proc)
options = options.dup options = options.dup
options << subprocess_options options << subprocess_options

View File

@ -5,6 +5,8 @@ module VagrantPlugins
class Config < Vagrant.plugin("2", :config) class Config < Vagrant.plugin("2", :config)
attr_accessor :inline attr_accessor :inline
attr_accessor :path attr_accessor :path
attr_accessor :md5
attr_accessor :sha1
attr_accessor :env attr_accessor :env
attr_accessor :upload_path attr_accessor :upload_path
attr_accessor :args attr_accessor :args
@ -19,6 +21,8 @@ module VagrantPlugins
@args = UNSET_VALUE @args = UNSET_VALUE
@inline = UNSET_VALUE @inline = UNSET_VALUE
@path = UNSET_VALUE @path = UNSET_VALUE
@md5 = UNSET_VALUE
@sha1 = UNSET_VALUE
@env = UNSET_VALUE @env = UNSET_VALUE
@upload_path = UNSET_VALUE @upload_path = UNSET_VALUE
@privileged = UNSET_VALUE @privileged = UNSET_VALUE
@ -33,6 +37,8 @@ module VagrantPlugins
@args = nil if @args == UNSET_VALUE @args = nil if @args == UNSET_VALUE
@inline = nil if @inline == UNSET_VALUE @inline = nil if @inline == UNSET_VALUE
@path = nil if @path == 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 @env = {} if @env == UNSET_VALUE
@upload_path = "/tmp/vagrant-shell" if @upload_path == UNSET_VALUE @upload_path = "/tmp/vagrant-shell" if @upload_path == UNSET_VALUE
@privileged = true if @privileged == UNSET_VALUE @privileged = true if @privileged == UNSET_VALUE

View File

@ -177,7 +177,12 @@ module VagrantPlugins
download_path.delete if download_path.file? download_path.delete if download_path.file?
begin 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) ext = File.extname(config.path)
script = download_path.read script = download_path.read
ensure ensure

View File

@ -734,6 +734,14 @@ en:
downloader_interrupted: |- downloader_interrupted: |-
The download was interrupted by an external signal. It did not The download was interrupted by an external signal. It did not
complete. complete.
downloader_checksum_error: |-
The calculated checksum of the requested file does not match the expected
checksum!
File source: %{source}
Checsum type: %{type}
Expected checksum: %{expected_checksum}
Calculated checksum: %{actual_checksum}
env_inval: |- env_inval: |-
Vagrant received an "EINVAL" error while attempting to set some Vagrant received an "EINVAL" error while attempting to set some
environment variables. This is usually caused by the total size of your environment variables. This is usually caused by the total size of your

View File

@ -2,14 +2,19 @@ require File.expand_path("../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/provisioners/shell/provisioner") require Vagrant.source_root.join("plugins/provisioners/shell/provisioner")
describe "Vagrant::Shell::Provisioner" do describe "Vagrant::Shell::Provisioner" do
include_context "unit"
let(:env){ isolated_environment }
let(:machine) { 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(:config, :vm, :communicator).and_return(:not_winrm)
machine.stub_chain(:communicate, :tap) {} 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 context "with a script that contains invalid us-ascii byte sequences" do
let(:config) { let(:config) {
@ -37,4 +42,67 @@ describe "Vagrant::Shell::Provisioner" do
}.not_to raise_error }.not_to raise_error
end end
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(:digest){ double("digest") }
before do
Vagrant::Util::Downloader.any_instance.should_receive(:execute_curl).and_return(true)
allow(digest).to receive(:file).and_return(digest)
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(:digest){ double("digest") }
before do
Vagrant::Util::Downloader.any_instance.should_receive(:execute_curl).and_return(true)
allow(digest).to receive(:file).and_return(digest)
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 end

View File

@ -93,6 +93,104 @@ describe Vagrant::Util::Downloader do
expect(subject.download!).to be_true expect(subject.download!).to be_true
end end
end end
context "with checksum" do
let(:checksum_expected_value){ 'MD5_CHECKSUM_VALUE' }
let(:checksum_invalid_value){ 'INVALID_VALUE' }
let(:digest){ double("digest") }
before do
allow(digest).to receive(:file).and_return(digest)
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 end
describe "#head" do describe "#head" do

View File

@ -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 enable auto-login for Windows as the user must be logged in for interactive
mode to work. 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> <a name="inline-scripts"></a>
## Inline Scripts ## Inline Scripts