Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Content-Digest header in StagingsController #3558

Merged
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
29 changes: 21 additions & 8 deletions app/controllers/runtime/stagings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def upload_v3_app_buildpack_cache(stack_name, guid)
raise CloudController::Errors::ApiError.new_from_details('ResourceNotFound', 'App not found') if app_model.nil?
raise ApiError.new_from_details('StagingError', "malformed buildpack cache upload request for #{guid}") unless upload_path

check_file_md5
check_content_digest

upload_job = Jobs::V3::BuildpackCacheUpload.new(local_path: upload_path, app_guid: guid, stack_name: stack_name)
Jobs::Enqueuer.new(upload_job, queue: Jobs::Queues.local(config)).enqueue
Expand Down Expand Up @@ -113,7 +113,7 @@ def upload_droplet(guid)

raise ApiError.new_from_details('StagingError', "malformed droplet upload request for #{droplet.guid}") unless upload_path

check_file_md5
check_content_digest

logger.info 'v3-droplet.begin-upload', droplet_guid: droplet.guid

Expand Down Expand Up @@ -150,16 +150,29 @@ def params_tempfile
HashUtils.dig(params, 'file', :tempfile) || HashUtils.dig(params, 'upload', 'droplet', :tempfile)
end

def check_file_md5
CONTENT_DIGEST_REGEX = %r{\s*(?<Algorithm>[a-zA-Z0-9-]*)\s*=\s*:(?<Base64Digest>[a-zA-Z0-9+/=]*):\s*}

def check_content_digest
return if Rails.env.local?

digester = Digester.new(algorithm: OpenSSL::Digest::MD5, type: :base64digest)
file_md5 = digester.digest_path(upload_path)
header_md5 = env['HTTP_CONTENT_MD5']
content_digest = env['HTTP_CONTENT_DIGEST']
if content_digest.present?
result = content_digest.match(CONTENT_DIGEST_REGEX)
raise ApiError.new_from_details('StagingError', 'invalid content digest header format') if result.nil?

return unless header_md5.present? && file_md5 != header_md5
algorithm = result['Algorithm'].tr('-', '')
algorithm += '1' if algorithm == 'sha'
digest = result['Base64Digest']
else
algorithm = 'md5'
digest = env['HTTP_CONTENT_MD5']
return if digest.blank?
end
raise ApiError.new_from_details('StagingError', 'unsupported digest algorithm') unless %w[sha512 sha256 sha1 md5].include?(algorithm)

raise ApiError.new_from_details('StagingError', 'content md5 did not match')
digester = Digester.new(algorithm: OpenSSL::Digest.const_get(algorithm.upcase.to_sym), type: :base64digest)
path_digest = digester.digest_path(upload_path)
raise ApiError.new_from_details('StagingError', 'content digest did not match') if path_digest != digest
end
end
end
81 changes: 67 additions & 14 deletions spec/unit/controllers/runtime/stagings_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,79 @@
## NOTICE: Prefer request specs over controller specs as per ADR #0003 ##

module VCAP::CloudController
RSpec.shared_examples 'droplet staging error handling' do
RSpec.shared_examples 'check content digest' do
context 'when a content-md5 is specified' do
it 'returns a 400 if the value does not match the md5 of the body' do
post url, upload_req, 'HTTP_CONTENT_MD5' => 'the-wrong-md5'
expect(last_response.status).to eq(400)
end

it 'succeeds if the value matches the md5 of the body' do
content_md5 = digester.digest(file_content)
content_md5 = digester_md5.digest(file_content)
post url, upload_req, 'HTTP_CONTENT_MD5' => content_md5
expect(last_response.status).to eq(200)
end
end

context 'when a content digest sha1 is specified' do
it 'returns a 400 if the value does not match the sha1 of the body' do
post url, upload_req, 'HTTP_CONTENT_DIGEST' => 'sha=:the+wrong+sha1:'
expect(last_response.status).to eq(400)
end

it 'succeeds if the value matches the sha1 of the body' do
content_sha1 = digester_sha1.digest(file_content)
post url, upload_req, 'HTTP_CONTENT_DIGEST' => "sha=:#{content_sha1}:"
expect(last_response.status).to eq(200)
end
end

context 'when a content digest sha256 is specified' do
it 'returns a 400 if the value does not match the sha256 of the body' do
post url, upload_req, 'HTTP_CONTENT_DIGEST' => 'sha-256=:the+wrong+sha256:'
expect(last_response.status).to eq(400)
end

it 'succeeds if the value matches the sha256 of the body' do
content_sha256 = digester_sha256.digest(file_content)
post url, upload_req, 'HTTP_CONTENT_DIGEST' => "sha-256=:#{content_sha256}:"
expect(last_response.status).to eq(200)
end
end

context 'when a content digest sha512 is specified' do
it 'returns a 400 if the value does not match the sha512 of the body' do
post url, upload_req, 'HTTP_CONTENT_DIGEST' => 'sha-512=:the+wrong+sha512:'
expect(last_response.status).to eq(400)
end

it 'succeeds if the value matches the sha512 of the body' do
content_sha512 = digester_sha512.digest(file_content)
post url, upload_req, 'HTTP_CONTENT_DIGEST' => "sha-512=:#{content_sha512}:"
expect(last_response.status).to eq(200)
end
end

context 'when an invalid content digest format is specified' do
it 'returns a 400' do
post url, upload_req, 'HTTP_CONTENT_DIGEST' => 'D/I/G/E/S/T:'
expect(last_response.status).to eq(400)
expect(last_response.body).to include('invalid content digest header format')
end
end

context 'when an unsupported content digest algorithm is specified' do
it 'returns a 400' do
post url, upload_req, 'HTTP_CONTENT_DIGEST' => 'sha-42=:D/I/G/E/S/T:'
expect(last_response.status).to eq(400)
expect(last_response.body).to include('unsupported digest algorithm')
end
end
end

RSpec.shared_examples 'droplet staging error handling' do
include_examples 'check content digest'

context 'with an invalid app' do
it 'returns 404' do
post bad_droplet_url, upload_req
Expand Down Expand Up @@ -115,7 +174,10 @@ module VCAP::CloudController
let(:blobstore) do
CloudController::DependencyLocator.instance.droplet_blobstore
end
let(:digester) { Digester.new(algorithm: OpenSSL::Digest::MD5, type: :base64digest) }
let(:digester_md5) { Digester.new(algorithm: OpenSSL::Digest::MD5, type: :base64digest) }
let(:digester_sha1) { Digester.new(algorithm: OpenSSL::Digest::SHA1, type: :base64digest) }
let(:digester_sha256) { Digester.new(algorithm: OpenSSL::Digest::SHA256, type: :base64digest) }
let(:digester_sha512) { Digester.new(algorithm: OpenSSL::Digest::SHA512, type: :base64digest) }

let(:buildpack_cache_blobstore) do
CloudController::DependencyLocator.instance.buildpack_cache_blobstore
Expand Down Expand Up @@ -359,17 +421,8 @@ def create_test_blob
expect(last_response.status).to eq 200
end

context 'when a content-md5 is specified' do
it 'returns a 400 if the value does not match the md5 of the body' do
post "/internal/v4/buildpack_cache/#{stack}/#{app_model.guid}/upload", upload_req, 'HTTP_CONTENT_MD5' => 'the-wrong-md5'
expect(last_response.status).to eq(400)
end

it 'succeeds if the value matches the md5 of the body' do
content_md5 = digester.digest(file_content)
post "/internal/v4/buildpack_cache/#{stack}/#{app_model.guid}/upload", upload_req, 'HTTP_CONTENT_MD5' => content_md5
expect(last_response.status).to eq(200)
end
include_examples 'check content digest' do
let(:url) { "/internal/v4/buildpack_cache/#{stack}/#{app_model.guid}/upload" }
end
end

Expand Down