Skip to content

Commit

Permalink
Verify MD5 checksums returned by SQS
Browse files Browse the repository at this point in the history
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
tawan committed Feb 19, 2016
1 parent 17f8934 commit 7d68859
Show file tree
Hide file tree
Showing 4 changed files with 335 additions and 0 deletions.
1 change: 1 addition & 0 deletions aws-sdk-core/lib/aws-sdk-core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ module Plugins
autoload :S3SseCpk, 'aws-sdk-core/plugins/s3_sse_cpk'
autoload :S3UrlEncodedKeys, 'aws-sdk-core/plugins/s3_url_encoded_keys'
autoload :SQSQueueUrls, 'aws-sdk-core/plugins/sqs_queue_urls'
autoload :SQSMd5s, 'aws-sdk-core/plugins/sqs_md5s'
autoload :StubResponses, 'aws-sdk-core/plugins/stub_responses'
autoload :SWFReadTimeouts, 'aws-sdk-core/plugins/swf_read_timeouts'
autoload :UserAgent, 'aws-sdk-core/plugins/user_agent'
Expand Down
1 change: 1 addition & 0 deletions aws-sdk-core/lib/aws-sdk-core/api/customizations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ def apply_plugins(client_class)

plugins('sqs', add: %w(
Aws::Plugins::SQSQueueUrls
Aws::Plugins::SQSMd5s
))

plugins('swf', add: %w(
Expand Down
148 changes: 148 additions & 0 deletions aws-sdk-core/lib/aws-sdk-core/plugins/sqs_md5s.rb
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.

Copy link
@zwang80

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
185 changes: 185 additions & 0 deletions aws-sdk-core/spec/aws/plugins/sqs_md5s_spec.rb
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

0 comments on commit 7d68859

Please sign in to comment.