-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Verify MD5 checksums returned by SQS
Verify MD5 checksums returned by SQS for received messages. * Verify checksum of message body. * Verify checksum of message attributes if present. Refer to #1085
- Loading branch information
Showing
4 changed files
with
335 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
require 'openssl' | ||
|
||
module Aws | ||
module Plugins | ||
|
||
# @seahorse.client.option [Boolean] :verify_checksums (true) | ||
# When `true` MD5 checksums will be computed for messages sent to | ||
# an SQS queue and matched against MD5 checksums returned by Amazon SQS. | ||
# `Aws::Errors::Checksum` errors are raised for cases where checksums do | ||
# not match. | ||
class SQSMd5s < Seahorse::Client::Plugin | ||
OPERATIONS_TO_VERIFY = [:send_message, :send_message_batch] | ||
|
||
# @api private | ||
class Handler < Seahorse::Client::Handler | ||
def call(context) | ||
@handler.call(context).on_success do |response| | ||
case context.operation_name | ||
when :send_message | ||
validate_send_message(context, response) | ||
when :send_message_batch | ||
validate_send_message_batch(context, response) | ||
end | ||
end | ||
end | ||
|
||
private | ||
|
||
TRANSPORT_TYPE_ENCODINGS = { | ||
'String' => 1, | ||
'Binary' => 2, | ||
'Number' => 1 | ||
} | ||
|
||
NORMALIZED_ENCODING = Encoding::UTF_8 | ||
|
||
def validate_send_message(context, response) | ||
body = context.params[:message_body] | ||
attributes = context.params[:message_attributes] | ||
validate_single_message(body, attributes, response) | ||
end | ||
|
||
def validate_send_message_batch(context, response) | ||
context.params[:entries].each do |entry| | ||
id = entry[:id] | ||
body = entry[:message_body] | ||
attributes = entry[:message_attributes] | ||
message_response = response.successful.select { |r| r.id == id }[0] | ||
unless message_response.nil? | ||
validate_single_message(body, attributes, message_response) | ||
end | ||
end | ||
end | ||
|
||
def validate_single_message(body, attributes, response) | ||
validate_body(body, response) | ||
validate_attributes(attributes, response) unless attributes.nil? | ||
end | ||
|
||
def validate_body(body, response) | ||
calculated_md5 = md5_of_message_body(body) | ||
returned_md5 = response.md5_of_message_body | ||
if calculated_md5 != returned_md5 | ||
error_message = mismatch_error_message( | ||
'message body', | ||
calculated_md5, | ||
returned_md5, | ||
response) | ||
raise Aws::Errors::ChecksumError, error_message | ||
end | ||
end | ||
|
||
def validate_attributes(attributes, response) | ||
calculated_md5 = md5_of_message_attributes(attributes) | ||
returned_md5 = response.md5_of_message_attributes | ||
if returned_md5 != calculated_md5 | ||
error_message = mismatch_error_message( | ||
'message atributes', | ||
calculated_md5, | ||
returned_md5, | ||
response) | ||
raise Aws::Errors::ChecksumError, error_message | ||
end | ||
end | ||
|
||
def md5_of_message_body(message_body) | ||
OpenSSL::Digest::MD5.hexdigest(message_body) | ||
end | ||
|
||
def md5_of_message_attributes(message_attributes) | ||
encoded = { } | ||
message_attributes.each do |name, attribute| | ||
name = name.to_s | ||
encoded[name] = String.new | ||
encoded[name] << encode_length_and_bytes(name) << | ||
encode_length_and_bytes(attribute[:data_type]) << | ||
[TRANSPORT_TYPE_ENCODINGS[attribute[:data_type]]].pack('C'.freeze) | ||
This comment has been minimized.
Sorry, something went wrong. |
||
|
||
if attribute[:string_value] != nil | ||
encoded[name] << encode_length_and_string(attribute[:string_value]) | ||
elsif attribute[:binary_value] != nil | ||
encoded[name] << encode_length_and_bytes(attribute[:binary_value]) | ||
end | ||
end | ||
|
||
buffer = encoded.keys.sort.reduce(String.new) do |string, name| | ||
string << encoded[name] | ||
end | ||
OpenSSL::Digest::MD5.hexdigest(buffer) | ||
end | ||
|
||
def encode_length_and_string(string) | ||
string = String.new(string) | ||
string.encode!(NORMALIZED_ENCODING) | ||
encode_length_and_bytes(string) | ||
end | ||
|
||
def encode_length_and_bytes(bytes) | ||
[bytes.bytesize, bytes].pack('L>a*'.freeze) | ||
end | ||
|
||
def mismatch_error_message(section, local_md5, returned_md5, response) | ||
m = "MD5 returned by SQS does not match " << | ||
"the calculation on the original request. (" | ||
|
||
if response.respond_to?(:id) && !response.id.nil? | ||
m << "Message ID: #{response.id}, " | ||
end | ||
|
||
m << "MD5 calculated by the #{section}: " << | ||
"'#{local_md5}', MD5 checksum returned: '#{returned_md5}')" | ||
end | ||
end | ||
|
||
option(:verify_checksums, true) | ||
|
||
def add_handlers(handlers, config) | ||
if config.verify_checksums | ||
handlers.add(Handler, { | ||
priority: 10 , | ||
step: :validate, | ||
operations: SQSMd5s::OPERATIONS_TO_VERIFY | ||
}) | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
# encoding: UTF-8 | ||
|
||
require 'spec_helper' | ||
|
||
module Aws | ||
module Plugins | ||
describe SQSMd5s do | ||
|
||
let(:plugin) { SQSMd5s.new } | ||
|
||
let(:config) { Seahorse::Client::Configuration.new } | ||
|
||
let(:built_config) { config.build!(verify_checksums: verify_checksums) } | ||
|
||
let(:handlers) { Seahorse::Client::HandlerList.new } | ||
|
||
it 'adds a :verify_checksums option that defaults to true' do | ||
plugin.add_options(config) | ||
expect(config.build!.verify_checksums).to be(true) | ||
end | ||
|
||
describe '#add_handlers' do | ||
|
||
before(:each) do | ||
plugin.add_options(config) | ||
plugin.add_handlers(handlers, built_config) | ||
end | ||
|
||
context 'when verify_checksums is true' do | ||
|
||
let(:verify_checksums) { true } | ||
|
||
it 'adds a handler for each verifiable operation' do | ||
SQSMd5s::OPERATIONS_TO_VERIFY.each do |operation| | ||
expect(handlers.for(operation).count).to eq(1) | ||
end | ||
end | ||
end | ||
|
||
context 'when verify_checksums is false' do | ||
|
||
let(:verify_checksums) { false } | ||
|
||
it 'does not add any handler' do | ||
expect(handlers.count).to eq(0) | ||
SQSMd5s::OPERATIONS_TO_VERIFY.each do |operation| | ||
expect(handlers.for(operation).count).to eq(0) | ||
end | ||
end | ||
end | ||
end | ||
|
||
describe SQSMd5s::Handler do | ||
|
||
let(:message_body) { 'abc' } | ||
|
||
let(:message_attributes) { | ||
{ | ||
'ccc' => { | ||
string_value: 'test', | ||
data_type: 'String' | ||
}, | ||
aaa: { | ||
binary_value: [ 2, 3, 4 ].pack('C*'), | ||
data_type: 'Binary' | ||
}, | ||
zzz: { | ||
data_type: 'Number', | ||
string_value: '0230.01' | ||
}, | ||
'öther_encodings' => { | ||
data_type: 'String', | ||
string_value: 'Tüst'.encode!('ISO-8859-1') | ||
} | ||
} | ||
} | ||
|
||
let(:response_data) { | ||
double( | ||
'response_data', | ||
md5_of_message_body: md5_of_message_body, | ||
md5_of_message_attributes: md5_of_message_attributes) | ||
} | ||
|
||
let(:built_config) { | ||
config.build!( | ||
verify_checksums: verify_checksums, | ||
response_data: response_data) | ||
} | ||
|
||
let(:params) { | ||
{ | ||
message_body: message_body, | ||
message_attributes: message_attributes | ||
} | ||
} | ||
|
||
let(:operation_name) { :send_message } | ||
|
||
let(:context) { | ||
Seahorse::Client::RequestContext.new( | ||
params: params, | ||
config: built_config, | ||
operation_name: operation_name) | ||
} | ||
|
||
let(:verify_checksums) { true } | ||
|
||
let(:status_code) { 200 } | ||
|
||
before(:each) do | ||
plugin.add_options(config) | ||
DummySendPlugin.new.add_options(config) | ||
plugin.add_handlers(handlers, built_config) | ||
handlers.add(DummySendPlugin::Handler, step: :send) | ||
handlers.for(operation_name).to_stack.call(context) | ||
context.http_response.status_code = status_code | ||
end | ||
|
||
context 'when calculated and returned MD5 digest match' do | ||
|
||
let(:md5_of_message_body) { '900150983cd24fb0d6963f7d28e17f72' } | ||
|
||
let(:md5_of_message_attributes) { '756d7f4338696745d063b420a2f7e502' } | ||
|
||
it 'returns response' do | ||
expect { context.http_response.signal_done }.not_to raise_error | ||
end | ||
|
||
context 'when messages sent in a batch' do | ||
|
||
let(:operation_name) { :send_message_batch } | ||
|
||
let(:params) { | ||
{ | ||
entries: [ | ||
{ | ||
id: '0', | ||
message_body: message_body, | ||
message_attributes: message_attributes | ||
} | ||
] | ||
} | ||
} | ||
|
||
let(:response_data) do | ||
entry = double( | ||
'response_data_0', | ||
id: '0', | ||
md5_of_message_body: md5_of_message_body, | ||
md5_of_message_attributes: md5_of_message_attributes) | ||
double('response_data', successful: [ entry ]) | ||
end | ||
|
||
it 'returns response' do | ||
expect { context.http_response.signal_done }.not_to raise_error | ||
end | ||
end | ||
end | ||
|
||
context 'when calculated and returned MD5 mismatch' do | ||
|
||
let(:md5_of_message_body) { 'a different MD5 digest' } | ||
|
||
let(:md5_of_message_attributes) { 'a different MD5 digest' } | ||
|
||
it 'raises error' do | ||
expect { context.http_response.signal_done }.to( | ||
raise_error(Aws::Errors::ChecksumError, /does not match/) | ||
) | ||
end | ||
|
||
context 'when request was not successful' do | ||
|
||
let(:status_code) { 500 } | ||
|
||
it 'doe not raise ChecksumError' do | ||
expect { context.http_response.signal_done }.not_to raise_error | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
#1103