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

System attributes md5 for sqs #3014

Merged
merged 5 commits into from
Apr 29, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions gems/aws-sdk-sqs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
Unreleased Changes
------------------

* Feature - Handle System Message Attributes MD5 verification.

1.71.0 (2024-04-25)
------------------

Expand Down
119 changes: 84 additions & 35 deletions gems/aws-sdk-sqs/lib/aws-sdk-sqs/plugins/md5s.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ module Aws
module SQS
module Plugins
class Md5s < Seahorse::Client::Plugin

# @api private
class Handler < Seahorse::Client::Handler
def call(context)
Expand All @@ -26,80 +25,105 @@ def call(context)
'String' => 1,
'Binary' => 2,
'Number' => 1
}
}.freeze

DATA_TYPE = /\A(String|Binary|Number)(\..+)?\z/
DATA_TYPE = /\A(String|Binary|Number)(\..+)?\z/.freeze

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)
system_attributes = context.params[:message_system_attributes]
validate_single_message(body, attributes, system_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]
system_attributes = entry[:message_system_attributes]
message_response = response.successful.select { |r| r.id == id }[0]
unless message_response.nil?
validate_single_message(body, attributes, message_response)
validate_single_message(body, attributes, system_attributes, message_response)
end
end
end

def validate_single_message(body, attributes, response)
def validate_single_message(body, attributes, system_attributes, response)
validate_body(body, response)
unless attributes.nil? || attributes.empty?
validate_attributes(attributes, response)
end
unless system_attributes.nil? || system_attributes.empty?
validate_system_attributes(system_attributes, response)
end
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
return unless calculated_md5 != returned_md5

error_message = mismatch_error_message(
'message body',
calculated_md5,
returned_md5,
response
)
raise Aws::Errors::ChecksumError, error_message
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 attributes',
calculated_md5,
returned_md5,
response)
raise Aws::Errors::ChecksumError, error_message
end
return unless returned_md5 != calculated_md5

error_message = mismatch_error_message(
'message attributes',
calculated_md5,
returned_md5,
response
)
raise Aws::Errors::ChecksumError, error_message
end

def validate_system_attributes(system_attributes, response)
calculated_md5 = md5_of_message_system_attributes(system_attributes)
returned_md5 = response.md5_of_message_system_attributes
return unless returned_md5 != calculated_md5

error_message = mismatch_error_message(
'message system attributes',
calculated_md5,
returned_md5,
response
)
raise Aws::Errors::ChecksumError, error_message
end

def md5_of_message_body(message_body)
OpenSSL::Digest::MD5.hexdigest(message_body)
end

# MD5 of Message Attributes and System Attributes are effectively
# the same calculation. However, keeping these as two methods because
# they are modeled as two different shapes.
###
jterapin marked this conversation as resolved.
Show resolved Hide resolved
def md5_of_message_attributes(message_attributes)
encoded = { }
encoded = {}
message_attributes.each do |name, attribute|
name = name.to_s
encoded[name] = String.new
data_type_without_label = DATA_TYPE.match(attribute[:data_type])[1]
encoded[name] << encode_length_and_bytes(name) <<
encode_length_and_bytes(attribute[:data_type]) <<
[TRANSPORT_TYPE_ENCODINGS[data_type_without_label]].pack('C'.freeze)
encode_length_and_bytes(attribute[:data_type]) <<
[TRANSPORT_TYPE_ENCODINGS[data_type_without_label]].pack('C')

if attribute[:string_value] != nil
if !attribute[:string_value].nil?
encoded[name] << encode_length_and_string(attribute[:string_value])
elsif attribute[:binary_value] != nil
elsif !attribute[:binary_value].nil?
encoded[name] << encode_length_and_bytes(attribute[:binary_value])
end
end
Expand All @@ -110,14 +134,38 @@ def md5_of_message_attributes(message_attributes)
OpenSSL::Digest::MD5.hexdigest(buffer)
end

def md5_of_message_system_attributes(message_system_attributes)
encoded = {}
message_system_attributes.each do |name, attribute|
name = name.to_s
encoded[name] = String.new
data_type_without_label = DATA_TYPE.match(attribute[:data_type])[1]
encoded[name] << encode_length_and_bytes(name) <<
encode_length_and_bytes(attribute[:data_type]) <<
[TRANSPORT_TYPE_ENCODINGS[data_type_without_label]].pack('C')

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)
[bytes.bytesize, bytes].pack('L>a*')
end

def mismatch_error_message(section, local_md5, returned_md5, response)
Expand Down Expand Up @@ -154,13 +202,14 @@ def mismatch_error_message(section, local_md5, returned_md5, response)
end

def add_handlers(handlers, config)
if config.verify_checksums
handlers.add(Handler, {
priority: 10 ,
step: :validate,
operations: [:send_message, :send_message_batch]
})
end
return unless config.verify_checksums

handlers.add(
Handler,
priority: 10,
step: :validate,
operations: %i[send_message send_message_batch]
)
end
end
end
Expand Down
46 changes: 41 additions & 5 deletions gems/aws-sdk-sqs/spec/client/verify_checksums_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,25 @@ class Client
}
}}

let(:message_system_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')
}
}}

it 'is enabled by default' do
client = Client.new(
region: 'us-east-1',
Expand All @@ -52,13 +71,15 @@ class Client

describe '#send_message' do
let(:md5_of_attributes) { '756d7f4338696745d063b420a2f7e502' }
let(:md5_of_system_attributes) { '756d7f4338696745d063b420a2f7e502' }

before(:each) do
response_body = <<-JSON
{
"MD5OfMessageAttributes": "#{md5_of_attributes}",
"MD5OfMessageBody": "900150983cd24fb0d6963f7d28e17f72",
"MessageId": "5fea7756-0ea4-451a-a703-a558b933e274"
"MessageId": "5fea7756-0ea4-451a-a703-a558b933e274",
"MD5OfMessageBody": "900150983cd24fb0d6963f7d28e17f72",
"MD5OfMessageAttributes": "#{md5_of_attributes}",
"MD5OfMessageSystemAttributes": "#{md5_of_system_attributes}"
}
JSON

Expand All @@ -77,6 +98,7 @@ class Client
queue_url:'https://queue.url',
message_body: message_body,
message_attributes: message_attributes,
message_system_attributes: message_system_attributes
)
}.not_to raise_error
end
Expand All @@ -87,17 +109,22 @@ class Client
queue_url:'https://queue.url',
message_body: message_body,
message_attributes: {},
message_system_attributes: {}
)
}.not_to raise_error
end

context 'when data types have custom labels' do
let(:md5_of_attributes) { '5b7ef6c8a8d46001c7cdadaeea917aa4' }
let(:md5_of_system_attributes) { '5b7ef6c8a8d46001c7cdadaeea917aa4' }

before(:each) do
message_attributes.keys.each do |attribute_name|
message_attributes[attribute_name][:data_type] += '.test'
end
message_system_attributes.keys.each do |attribute_name|
message_system_attributes[attribute_name][:data_type] += '.test'
end
end

it 'does not raise an error if checksums match' do
Expand All @@ -106,6 +133,7 @@ class Client
queue_url:'https://queue.url',
message_body: message_body,
message_attributes: message_attributes,
message_system_attributes: message_system_attributes
)
}.not_to raise_error
end
Expand All @@ -117,17 +145,20 @@ class Client
queue_url:'https://queue.url',
message_body: message_body + 'junk',
message_attributes: message_attributes,
message_system_attributes: message_system_attributes
)
}.to raise_error(Aws::Errors::ChecksumError)
end

it 'raises when the md5 checksums do not match for the body' do
message_attributes['ccc'][:string_value] += 'junk'
message_system_attributes['ccc'][:string_value] += 'junk'
expect {
client.send_message(
queue_url:'https://queue.url',
message_body: message_body,
message_attributes: message_attributes,
message_system_attributes: message_system_attributes
)
}.to raise_error(Aws::Errors::ChecksumError)
end
Expand All @@ -146,14 +177,14 @@ class Client
queue_url:'https://queue.url',
message_body: message_body,
message_attributes: message_attributes,
message_system_attributes: message_system_attributes
)
}.to raise_error(Aws::SQS::Errors::ServiceError)
end

end

describe '#send_message_batch' do

before(:each) do
client.handle(step: :send) do |context|
context.http_response.signal_done(
Expand All @@ -165,7 +196,8 @@ class Client
{
"Id": "msg-id",
"MD5OfMessageBody": "900150983cd24fb0d6963f7d28e17f72",
"MD5OfMessageAttributes": "756d7f4338696745d063b420a2f7e502"
"MD5OfMessageAttributes": "756d7f4338696745d063b420a2f7e502",
"MD5OfMessageSystemAttributes": "756d7f4338696745d063b420a2f7e502"
}
]
}
Expand All @@ -183,6 +215,7 @@ class Client
id: 'msg-id',
message_body: message_body,
message_attributes: message_attributes,
message_system_attributes: message_system_attributes
}
]
)
Expand All @@ -198,6 +231,7 @@ class Client
id: 'msg-id',
message_body: message_body + 'junk',
message_attributes: message_attributes,
message_system_attributes: message_system_attributes
}
]
)
Expand All @@ -214,6 +248,7 @@ class Client
id: 'msg-id',
message_body: message_body,
message_attributes: message_attributes,
message_system_attributes: message_system_attributes
}
]
)
Expand Down Expand Up @@ -245,6 +280,7 @@ class Client
id: 'msg-id',
message_body: message_body,
message_attributes: message_attributes,
message_system_attributes: message_system_attributes
}
]
)
Expand Down