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

Bugfix around message attributes on Publish(SNS), ReceiveMessages(SQS) #339

Conversation

kojisaikiAtSony
Copy link
Contributor

@kojisaikiAtSony kojisaikiAtSony commented Nov 13, 2024

We found 3 bugs around message attributes, as shown below:

  1. Received Message Attribute via XML is not decoded from base 64.
  2. DataType value was broken by caser unexpectedly.
    • The exact conditions have not been identified, but at a certain point when publish/receive messages are repeated, the DataType value String becomes a string like \u0000\u0000\u0000\u0000\u0000\u0000 due to the caser.
  3. MD5OfMessageAttributes is missing on the message with message attributes from SQS-ReceiveMessages.

Comment on lines -260 to +280
binaryValue := values.Get(fmt.Sprintf("Entries.%d.MessageAttributes.%d.Value.BinaryValue", entryIndex, attributeIndex))
encodedBinaryValue := values.Get(fmt.Sprintf("Entries.%d.MessageAttributes.%d.Value.BinaryValue", entryIndex, attributeIndex))

if r.Entries[entryIndex].MessageAttributes == nil {
r.Entries[entryIndex].MessageAttributes = make(map[string]MessageAttribute)
}

binaryValue, err := base64.StdEncoding.DecodeString(encodedBinaryValue)
if err != nil {
log.Warnf("Failed to base64 decode. %s may not have been base64 encoded.", encodedBinaryValue)
continue
}

r.Entries[entryIndex].MessageAttributes[name] = MessageAttribute{
DataType: dataType,
StringValue: stringValue,
BinaryValue: []byte(binaryValue),
BinaryValue: binaryValue,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same fix for bug 1.

Comment on lines -744 to +769
binaryValue := values.Get(fmt.Sprintf("MessageAttributes.entry.%d.Value.BinaryValue", i))
encodedBinaryValue := values.Get(fmt.Sprintf("MessageAttributes.entry.%d.Value.BinaryValue", i))
binaryValue, err := base64.StdEncoding.DecodeString(encodedBinaryValue)
if err != nil {
log.Warnf("Failed to base64 decode. %s may not have been base64 encoded.", encodedBinaryValue)
continue
}

if r.MessageAttributes == nil {
r.MessageAttributes = make(map[string]MessageAttribute)
}

attributes[name] = MessageAttribute{
DataType: caser.String(dataType), // capitalize
DataType: cases.Title(language.AmericanEnglish).String(dataType), // capitalize
StringValue: stringValue,
BinaryValue: []byte(binaryValue),
BinaryValue: binaryValue,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same fix for bug 1.

Comment on lines -208 to +218
binaryValue := values.Get(fmt.Sprintf("MessageAttribute.%d.Value.BinaryValue", i))
encodedBinaryValue := values.Get(fmt.Sprintf("MessageAttribute.%d.Value.BinaryValue", i))

binaryValue, err := base64.StdEncoding.DecodeString(encodedBinaryValue)
if err != nil {
log.Warnf("Failed to base64 decode. %s may not have been base64 encoded.", encodedBinaryValue)
continue
}

r.MessageAttributes[name] = MessageAttribute{
DataType: dataType,
StringValue: stringValue,
BinaryValue: []byte(binaryValue),
BinaryValue: binaryValue,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes Bug 1 "Received Message Attribute via XML is not decoded from base 64".
JSON requests has decoded from base 64 on JSON marshaller, but for XML, here SetAttributesFromForm does not have decoding step. So internal BinaryValue was still base 64 string (as binary) value.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really love this. I know it's more true to life, since that input is supposed to be encoded, but I'd prefer to make the decoding optional. It's a testing tool after all, and we're doing our best to cover the needs of all. It's not like we're throwing errors for invalid attributes or anything.

So maybe instead of your continue, the log is fine, but take the value as is and stuff it in the resulting attribute?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK! I will update it as the below 👍

  • Try decoding
    • Success: store it as the decoded raw value
    • Failure: log and store it as original requested value

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @kojisaikiAtSony - I'm looking at this again, and more of my previous thoughts are coming back to me. Looks to me like this change is decoding it, storing it internally, and then upon receiving the message, encoding it, and returning it. Double check me, is that what you're doing here?

If so, why do this at all? That's why I undid all this decoding/encoding business not long ago. It didn't seem like there was a point. Are you having trouble with the checksum or something because it's encoded? Where are you seeing the "Received Message Attribute via XML is not decoded from base 64" error, and how are you recreating it?

assert.Equal(t, []uint8("VmFsdWUy"), attr2.BinaryValue)
assert.Equal(t, []uint8("Value2"), attr2.BinaryValue)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Internal value should be raw (not base 64 encoded) value.

Comment on lines +87 to +89
if value.DataType == "Binary" {
value.BinaryValue = []byte(base64.StdEncoding.EncodeToString(value.BinaryValue))
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the related fix for Bug 1.

In the AWS query protocol, blobs are sent and received after being base64 encoded , so the binary value should be base64 encoded.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know about this. I understand that's part of the query protocol, but that whole thing is deprecated in AWS. Based on my tests this works, as is, for the existing sdks I've tried. What exactly isn't working for you?

Copy link
Contributor Author

@kojisaikiAtSony kojisaikiAtSony Nov 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you said this part is for the query protocol. The latest version of AWS SDK for Java also works without this part.

I thought that future versions of GOAWS still keep support the query protocol, so I added it.
Sorry, I don't remember clearly, but Is the policy that "users who need to use the query protocol should use older versions of GOAWS"? (It also makes sense)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, no, that's not the policy per say. I hadn't given it that much thought if I'm honest. But if you think about it like this - this is a tool for folks to run their own apps against instead AWS directly. So my main goal is to make it work in a performant/lightweight way and replicate as much of the AWS SQS/SNS "contract" as I can.

So with that in mind - we don't NEED to match what AWS does, as long as we respond the same way that it would. We just need to match their contracts right? At any rate we can't know what they're doing all the times as their docs are generally garbage in my opinion.

So, I don't care about things like security, and base64 encoding much, because, it's really up to the user here to worry about their input and output - because it's an app running where a user puts it. "Do what you will with it." so to speak, as long as an SDK doesn't fail on it - "garbage in garbage" out.

Here's what I need:
So all that said - this change is doing a couple of things. I need to know why we need them. The way I had it lately was working for me, based on the contract I had researched. What I'd like to see is what you're failing with, how you're recreating it, and why we think these changes will fix them.

Sorry - this is a lot for me to juggle long term and I need a little grooming/refining (in agile terms), before I let it in. Otherwise I'll be adrift if someone else complains about it or something similar later.

As always thank you for giving it so much work and thought. I really appreciate it and without you I wouldn't be able to keep this tool as current and viable as it is.

entry = "<MessageAttribute><Name>attr2</Name><Value><BinaryValue>binary-value</BinaryValue><DataType>Binary</DataType></Value></MessageAttribute>"
entry = "<MessageAttribute><Name>attr2</Name><Value><BinaryValue>YmluYXJ5LXZhbHVl</BinaryValue><DataType>Binary</DataType></Value></MessageAttribute>"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


snsClient.SetSubscriptionAttributes(context.TODO(), &sns.SetSubscriptionAttributesInput{
SubscriptionArn: subscribeResult.SubscriptionArn,
AttributeName: aws.String("RawMessageDelivery"),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new test pattern is for "RawMessageDelivery".

@@ -268,7 +268,7 @@ func Test_ReceiveMessageV1_xml_with_attributes(t *testing.T) {
assert.Equal(t, 1, len(receiveMessageResponse.Result.Messages))
assert.Equal(t, "MyTestMessage", receiveMessageResponse.Result.Messages[0].Body)
assert.Equal(t, "ad4883a84ad41c79aa3a373698c0d4e9", receiveMessageResponse.Result.Messages[0].MD5OfBody)
assert.Equal(t, "", receiveMessageResponse.Result.Messages[0].MD5OfMessageAttributes)
assert.Equal(t, "ae8770938aee44bc548cf65ac377e3bf", receiveMessageResponse.Result.Messages[0].MD5OfMessageAttributes)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

xml/json both should have hash for message attribute if the message attributes is not empty.

@@ -370,7 +372,7 @@ func TestSendMessageBatchV1_Xml_Success_including_attributes(t *testing.T) {
WithFormField("Entries.1.MessageBody", messageBody2).
WithFormField("Entries.1.MessageAttributes.1.Name", binaryAttributeKey).
WithFormField("Entries.1.MessageAttributes.1.Value.DataType", binaryType).
WithFormField("Entries.1.MessageAttributes.1.Value.BinaryValue", binaryValue).
WithFormField("Entries.1.MessageAttributes.1.Value.BinaryValue", binaryValueEncodeString).
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is also for blobs are sent and received after being base64 encoded.
BinaryValue in XML should be base 64 encoded.

Comment on lines -750 to +767
DataType: caser.String(dataType), // capitalize
DataType: cases.Title(language.AmericanEnglish).String(dataType), // capitalize
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the fix for Bug 2 "DataType value was broken by caser unexpectedly".

As I wrote in the description, I don't know the exact conditions under which this occurs, but since there was no need to make it a global variable, I changed it to a local definition and the problem was resolved. If you have any ideas, I would appreciate your comments.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to understand more about why you want to change this. If you don't know the exact conditions does that mean you can't recreate it? If so, what are we fixing, and beyond that how can we create a test that makes sure the issue is fixed?

In the short term, I like the pattern of it being global. If I need to uppercase anything else, I want to use the same instance of caser instead of creating new ones all over the place. That's why I made it global.

Can you provide a little more information before we move forward with this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I thought you said so 😅. It's a little creepy when the behavior isn't clear. Let's dive into the caser code and take a closer look 👍

Copy link
Owner

@Admiral-Piett Admiral-Piett left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need some more information on this one before we move forward. Having just done some significant work on this, I want to understand more about what's causing you problems before we change it. Also, I think there are a couple opportunities to be the "path of least resistance" here, and I think that's helpful. See my individualized comments for the specifics.

Finally - #338 @toastwaffle did some good work on combining the logic around parsing incoming MessageAttributes. It'll definitely affect your change, since you'd only need to do it once. Want to take a look at what they've got first? I think there's is pretty close and I think it makes sense to get that in before this, so we can just fix the bugs in 1 area, if you're ok with that.

Thanks for looking at this, as always!

Comment on lines -208 to +218
binaryValue := values.Get(fmt.Sprintf("MessageAttribute.%d.Value.BinaryValue", i))
encodedBinaryValue := values.Get(fmt.Sprintf("MessageAttribute.%d.Value.BinaryValue", i))

binaryValue, err := base64.StdEncoding.DecodeString(encodedBinaryValue)
if err != nil {
log.Warnf("Failed to base64 decode. %s may not have been base64 encoded.", encodedBinaryValue)
continue
}

r.MessageAttributes[name] = MessageAttribute{
DataType: dataType,
StringValue: stringValue,
BinaryValue: []byte(binaryValue),
BinaryValue: binaryValue,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really love this. I know it's more true to life, since that input is supposed to be encoded, but I'd prefer to make the decoding optional. It's a testing tool after all, and we're doing our best to cover the needs of all. It's not like we're throwing errors for invalid attributes or anything.

So maybe instead of your continue, the log is fine, but take the value as is and stuff it in the resulting attribute?

Comment on lines +87 to +89
if value.DataType == "Binary" {
value.BinaryValue = []byte(base64.StdEncoding.EncodeToString(value.BinaryValue))
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know about this. I understand that's part of the query protocol, but that whole thing is deprecated in AWS. Based on my tests this works, as is, for the existing sdks I've tried. What exactly isn't working for you?

Comment on lines +102 to +104
if r.MessageAttributes != nil {
e.EncodeElement(r.MD5OfMessageAttributes, xml.StartElement{Name: xml.Name{Local: "MD5OfMessageAttributes"}})
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why I left this out. I think there was a reason though. A lot of this had to do with the raw vs regular message handling. Can you tell me more about what your issues are on this?

@kojisaikiAtSony
Copy link
Contributor Author

kojisaikiAtSony commented Nov 19, 2024

Finally - #338 @toastwaffle did some good work on combining the logic around parsing incoming MessageAttributes.

The PR is very helpful! We'll rebase this once it is merged 👍

@kojisaikiAtSony
Copy link
Contributor Author

kojisaikiAtSony commented Nov 25, 2024

@Admiral-Piett
I'm really sorry for creating such a big and confusing PR without considering your burden 🙇
I will create separated, small PRs on each fixes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants