Skip to content

Commit

Permalink
Merge pull request #3558 from sap-contributions/stagings-controller-c…
Browse files Browse the repository at this point in the history
…ontent-digest

Support Content-Digest header in StagingsController
  • Loading branch information
philippthun authored Dec 19, 2023
2 parents ba4cf06 + a3605ca commit 13c55c6
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 22 deletions.
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

0 comments on commit 13c55c6

Please sign in to comment.