Skip to content

Commit 681ca65

Browse files
authored
service/sqs: Add support for validating message checksums by default (#1748)
Adds support for the SQS client to automatically validate message checksums for SendMessage, SendMessageBatch, and ReceiveMessage. This brings the v2 SDK up to speed with the v1 SDK's behavior. A DisableMessageChecksumValidation parameter has been added to the Options struct for SQS package. Setting this to true will disable the checksum validation. This can be set when creating a client, or per operation call.
1 parent 7ece169 commit 681ca65

9 files changed

Lines changed: 831 additions & 0 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "131fe156-ee06-40ff-85a5-ba09e3cda44c",
3+
"type": "feature",
4+
"description": "Adds support for the SQS client to automatically validate message checksums for SendMessage, SendMessageBatch, and ReceiveMessage. A DisableMessageChecksumValidation parameter has been added to the Options struct for SQS package. Setting this to true will disable the checksum validation. This can be set when creating a client, or per operation call.",
5+
"modules": [
6+
"service/sqs"
7+
]
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package software.amazon.smithy.aws.go.codegen.customization;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.Map;
6+
import java.util.Set;
7+
import java.util.logging.Logger;
8+
import software.amazon.smithy.codegen.core.SymbolProvider;
9+
import software.amazon.smithy.go.codegen.GoCodegenPlugin;
10+
import software.amazon.smithy.go.codegen.GoSettings;
11+
import software.amazon.smithy.go.codegen.SymbolUtils;
12+
import software.amazon.smithy.go.codegen.integration.ConfigField;
13+
import software.amazon.smithy.go.codegen.integration.GoIntegration;
14+
import software.amazon.smithy.go.codegen.integration.MiddlewareRegistrar;
15+
import software.amazon.smithy.go.codegen.integration.RuntimeClientPlugin;
16+
import software.amazon.smithy.model.Model;
17+
import software.amazon.smithy.model.shapes.OperationShape;
18+
import software.amazon.smithy.model.shapes.ServiceShape;
19+
import software.amazon.smithy.model.shapes.ShapeId;
20+
import software.amazon.smithy.utils.MapUtils;
21+
import software.amazon.smithy.utils.SetUtils;
22+
23+
public class SQSValidateMessageChecksum implements GoIntegration {
24+
private static final Logger LOGGER = Logger.getLogger(SQSValidateMessageChecksum.class.getName());
25+
26+
/**
27+
* Map of service shape to Set of operation shapes that need to have this
28+
* customization.
29+
*/
30+
public static final Map<ShapeId, Set<ShapeId>> SERVICE_TO_OPERATION_MAP = MapUtils.of(
31+
ShapeId.from("com.amazonaws.sqs#AmazonSQS"), SetUtils.of(
32+
ShapeId.from("com.amazonaws.sqs#SendMessage"),
33+
ShapeId.from("com.amazonaws.sqs#SendMessageBatch"),
34+
ShapeId.from("com.amazonaws.sqs#ReceiveMessage")
35+
)
36+
);
37+
static final String DISABLE_MESSAGE_CHECKSUM_VALIDATION_OPTION_NAME = "DisableMessageChecksumValidation";
38+
39+
private final List<RuntimeClientPlugin> runtimeClientPlugins = new ArrayList<>();
40+
41+
/**
42+
* Builds the set of runtime plugs used by the customization.
43+
*
44+
* @param settings codegen settings
45+
* @param model api model
46+
*/
47+
@Override
48+
public void processFinalizedModel(GoSettings settings, Model model) {
49+
ShapeId serviceId = settings.getService();
50+
if (!SERVICE_TO_OPERATION_MAP.containsKey(serviceId)) {
51+
return;
52+
}
53+
54+
ServiceShape service = settings.getService(model);
55+
56+
// Add option to disable message checksum validation
57+
runtimeClientPlugins.add(RuntimeClientPlugin.builder()
58+
.servicePredicate((m, s) -> s.equals(service))
59+
.addConfigField(ConfigField.builder()
60+
.name(DISABLE_MESSAGE_CHECKSUM_VALIDATION_OPTION_NAME)
61+
.type(SymbolUtils.createValueSymbolBuilder("bool")
62+
.putProperty(SymbolUtils.GO_UNIVERSE_TYPE, true).build())
63+
.documentation("Allows you to disable the client's validation of "
64+
+ "response message checksums. Enabled by default. "
65+
+ "Used by SendMessage, SendMessageBatch, and ReceiveMessage.")
66+
.build())
67+
.build());
68+
69+
for (ShapeId operationId : SERVICE_TO_OPERATION_MAP.get(serviceId)) {
70+
final OperationShape operation = model.expectShape(operationId, OperationShape.class);
71+
72+
// Create a symbol provider because one is not available in this call.
73+
SymbolProvider symbolProvider = GoCodegenPlugin.createSymbolProvider(model, settings);
74+
75+
String helperFuncName = addMiddlewareFuncName(symbolProvider.toSymbol(operation).getName());
76+
77+
runtimeClientPlugins.add(RuntimeClientPlugin.builder()
78+
.servicePredicate((m, s) -> s.equals(service))
79+
.operationPredicate((m, s, o) -> o.equals(operation))
80+
.registerMiddleware(MiddlewareRegistrar.builder()
81+
.resolvedFunction(SymbolUtils.createValueSymbolBuilder(helperFuncName)
82+
.build())
83+
.useClientOptions()
84+
.build())
85+
.build());
86+
}
87+
}
88+
89+
String addMiddlewareFuncName(String operationName) {
90+
return "addValidate" + operationName + "Checksum";
91+
}
92+
93+
/**
94+
* Returns the list of runtime client plugins added by this customization
95+
*
96+
* @return runtime client plugins
97+
*/
98+
@Override
99+
public List<RuntimeClientPlugin> getClientPlugins() {
100+
return runtimeClientPlugins;
101+
}
102+
}

codegen/smithy-aws-go-codegen/src/main/resources/META-INF/services/software.amazon.smithy.go.codegen.integration.GoIntegration

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,5 @@ software.amazon.smithy.aws.go.codegen.RequestResponseLogging
4545
software.amazon.smithy.aws.go.codegen.customization.S3AddPutObjectUnseekableBodyDoc
4646
software.amazon.smithy.aws.go.codegen.customization.BackfillEc2UnboxedToBoxedShapes
4747
software.amazon.smithy.aws.go.codegen.customization.AdjustAwsRestJsonContentType
48+
software.amazon.smithy.aws.go.codegen.customization.SQSValidateMessageChecksum
4849
software.amazon.smithy.aws.go.codegen.EndpointDiscoveryGenerator

service/sqs/api_client.go

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

service/sqs/api_op_ReceiveMessage.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

service/sqs/api_op_SendMessage.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

service/sqs/api_op_SendMessageBatch.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
package sqs
2+
3+
import (
4+
"context"
5+
"crypto/md5"
6+
"encoding/hex"
7+
"fmt"
8+
"strings"
9+
10+
"github.com/aws/aws-sdk-go-v2/aws"
11+
sqstypes "github.com/aws/aws-sdk-go-v2/service/sqs/types"
12+
"github.com/aws/smithy-go/middleware"
13+
)
14+
15+
// addValidateSendMessageChecksum adds the ValidateMessageChecksum middleware
16+
// to the stack configured for the SendMessage Operation.
17+
func addValidateSendMessageChecksum(stack *middleware.Stack, o Options) error {
18+
return addValidateMessageChecksum(stack, o, validateSendMessageChecksum)
19+
}
20+
21+
// validateSendMessageChecksum validates the SendMessage operation's input
22+
// message payload MD5 checksum matches that returned by the API.
23+
//
24+
// The input and output types must match the SendMessage operation.
25+
func validateSendMessageChecksum(input, output interface{}) error {
26+
in, ok := input.(*SendMessageInput)
27+
if !ok {
28+
return fmt.Errorf("wrong input type, expect %T, got %T", in, input)
29+
}
30+
out, ok := output.(*SendMessageOutput)
31+
if !ok {
32+
return fmt.Errorf("wrong output type, expect %T, got %T", out, output)
33+
}
34+
35+
// Nothing to validate if the members aren't populated.
36+
if in.MessageBody == nil || out.MD5OfMessageBody == nil {
37+
return nil
38+
}
39+
40+
if err := validateMessageChecksum(*in.MessageBody, *out.MD5OfMessageBody); err != nil {
41+
return messageChecksumError{
42+
MessageID: aws.ToString(out.MessageId),
43+
Err: err,
44+
}
45+
}
46+
return nil
47+
}
48+
49+
// addValidateSendMessageBatchChecksum adds the ValidateMessagechecksum
50+
// middleware to the stack configured for the SendMessageBatch operation.
51+
func addValidateSendMessageBatchChecksum(stack *middleware.Stack, o Options) error {
52+
return addValidateMessageChecksum(stack, o, validateSendMessageBatchChecksum)
53+
}
54+
55+
// validateSendMessageBatchChecksum validates the SendMessageBatch operation's
56+
// input messages body MD5 checksum matches those returned by the API.
57+
//
58+
// The input and output types must match the SendMessageBatch operation.
59+
func validateSendMessageBatchChecksum(input, output interface{}) error {
60+
in, ok := input.(*SendMessageBatchInput)
61+
if !ok {
62+
return fmt.Errorf("wrong input type, expect %T, got %T", in, input)
63+
}
64+
out, ok := output.(*SendMessageBatchOutput)
65+
if !ok {
66+
return fmt.Errorf("wrong output type, expect %T, got %T", out, output)
67+
}
68+
69+
outEntries := map[string]sqstypes.SendMessageBatchResultEntry{}
70+
for _, e := range out.Successful {
71+
outEntries[*e.Id] = e
72+
}
73+
74+
var failedMessageErrs []messageChecksumError
75+
for _, inEntry := range in.Entries {
76+
outEntry, ok := outEntries[*inEntry.Id]
77+
// Nothing to validate if the members aren't populated.
78+
if !ok || inEntry.MessageBody == nil || outEntry.MD5OfMessageBody == nil {
79+
continue
80+
}
81+
82+
if err := validateMessageChecksum(*inEntry.MessageBody, *outEntry.MD5OfMessageBody); err != nil {
83+
failedMessageErrs = append(failedMessageErrs, messageChecksumError{
84+
MessageID: aws.ToString(outEntry.MessageId),
85+
Err: err,
86+
})
87+
}
88+
}
89+
90+
if len(failedMessageErrs) != 0 {
91+
return batchMessageChecksumError{
92+
Errs: failedMessageErrs,
93+
}
94+
}
95+
96+
return nil
97+
}
98+
99+
// addValidateReceiveMessageChecksum adds the ValidateMessagechecksum
100+
// middleware to the stack configured for the ReceiveMessage operation.
101+
func addValidateReceiveMessageChecksum(stack *middleware.Stack, o Options) error {
102+
return addValidateMessageChecksum(stack, o, validateReceiveMessageChecksum)
103+
}
104+
105+
// validateReceiveMessageChecksum validates the ReceiveMessage operation's
106+
// input messages body MD5 checksum matches those returned by the API.
107+
//
108+
// The input and output types must match the ReceiveMessage operation.
109+
func validateReceiveMessageChecksum(_, output interface{}) error {
110+
out, ok := output.(*ReceiveMessageOutput)
111+
if !ok {
112+
return fmt.Errorf("wrong output type, expect %T, got %T", out, output)
113+
}
114+
115+
var failedMessageErrs []messageChecksumError
116+
for _, msg := range out.Messages {
117+
// Nothing to validate if the members aren't populated.
118+
if msg.Body == nil || msg.MD5OfBody == nil {
119+
continue
120+
}
121+
122+
if err := validateMessageChecksum(*msg.Body, *msg.MD5OfBody); err != nil {
123+
failedMessageErrs = append(failedMessageErrs, messageChecksumError{
124+
MessageID: aws.ToString(msg.MessageId),
125+
Err: err,
126+
})
127+
}
128+
}
129+
130+
if len(failedMessageErrs) != 0 {
131+
return batchMessageChecksumError{
132+
Errs: failedMessageErrs,
133+
}
134+
}
135+
136+
return nil
137+
}
138+
139+
// messageChecksumValidator provides the function signature for the operation's
140+
// validator.
141+
type messageChecksumValidator func(input, output interface{}) error
142+
143+
// addValidateMessageChecksum adds the ValidateMessageChecksum middleware to
144+
// the stack with the passed in validator specified.
145+
func addValidateMessageChecksum(stack *middleware.Stack, o Options, validate messageChecksumValidator) error {
146+
if o.DisableMessageChecksumValidation {
147+
return nil
148+
}
149+
150+
m := validateMessageChecksumMiddleware{
151+
validate: validate,
152+
}
153+
err := stack.Initialize.Add(m, middleware.Before)
154+
if err != nil {
155+
return fmt.Errorf("failed to add %s middleware, %w", m.ID(), err)
156+
}
157+
158+
return nil
159+
}
160+
161+
// validateMessageChecksumMiddleware provides the Initialize middleware for
162+
// validating an operation's message checksum is validate. Needs to b
163+
// configured with the operation's validator.
164+
type validateMessageChecksumMiddleware struct {
165+
validate messageChecksumValidator
166+
}
167+
168+
// ID returns the Middleware ID.
169+
func (validateMessageChecksumMiddleware) ID() string { return "SQSValidateMessageChecksum" }
170+
171+
// HandleInitialize implements the InitializeMiddleware interface providing a
172+
// middleware that will validate an operation's message checksum based on
173+
// calling the validate member.
174+
func (m validateMessageChecksumMiddleware) HandleInitialize(
175+
ctx context.Context, input middleware.InitializeInput, next middleware.InitializeHandler,
176+
) (
177+
out middleware.InitializeOutput, meta middleware.Metadata, err error,
178+
) {
179+
out, meta, err = next.HandleInitialize(ctx, input)
180+
if err != nil {
181+
return out, meta, err
182+
}
183+
184+
err = m.validate(input.Parameters, out.Result)
185+
if err != nil {
186+
return out, meta, fmt.Errorf("message checksum validation failed, %w", err)
187+
}
188+
189+
return out, meta, nil
190+
}
191+
192+
// validateMessageChecksum compares the MD5 checksums of value parameter with
193+
// the expected MD5 value. Returns an error if the computed checksum does not
194+
// match the expected value.
195+
func validateMessageChecksum(value, expect string) error {
196+
msum := md5.Sum([]byte(value))
197+
sum := hex.EncodeToString(msum[:])
198+
if sum != expect {
199+
return fmt.Errorf("expected MD5 checksum %s, got %s", expect, sum)
200+
}
201+
202+
return nil
203+
}
204+
205+
// messageChecksumError provides an error type for invalid message checksums.
206+
type messageChecksumError struct {
207+
MessageID string
208+
Err error
209+
}
210+
211+
func (e messageChecksumError) Error() string {
212+
prefix := "message"
213+
if e.MessageID != "" {
214+
prefix += " " + e.MessageID
215+
}
216+
return fmt.Sprintf("%s has invalid checksum, %v", prefix, e.Err.Error())
217+
}
218+
219+
// batchMessageChecksumError provides an error type for a collection of invalid
220+
// message checksum errors.
221+
type batchMessageChecksumError struct {
222+
Errs []messageChecksumError
223+
}
224+
225+
func (e batchMessageChecksumError) Error() string {
226+
var w strings.Builder
227+
fmt.Fprintf(&w, "message checksum errors")
228+
229+
for _, err := range e.Errs {
230+
fmt.Fprintf(&w, "\n\t%s", err.Error())
231+
}
232+
233+
return w.String()
234+
}

0 commit comments

Comments
 (0)