Skip to content

Commit cb9930c

Browse files
authored
SnsMessageManager Impl (#6804)
* SnsMessageManager Impl Provides an implementation of the SnsMessageManager. This mostly just ties all the other classes implemented in previous PRs related to the message manager. * Review comments * Allowlist usage of http client builder outside core * Fix test
1 parent fba4644 commit cb9930c

File tree

16 files changed

+655
-37
lines changed

16 files changed

+655
-37
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "Amazon SNS Message Manager",
4+
"contributor": "",
5+
"description": "This change introduces the SNS Message Manager for 2.x, a library used to parse and validate messages received from SNS. This aims to provide the same functionality as [SnsMessageManager](https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/sns/message/SnsMessageManager.html) from 1.x."
6+
}

core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/loader/DefaultSdkHttpClientBuilder.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
package software.amazon.awssdk.core.internal.http.loader;
1717

18-
import software.amazon.awssdk.annotations.SdkInternalApi;
18+
import software.amazon.awssdk.annotations.SdkProtectedApi;
1919
import software.amazon.awssdk.core.exception.SdkClientException;
2020
import software.amazon.awssdk.http.SdkHttpClient;
2121
import software.amazon.awssdk.http.SdkHttpService;
@@ -24,7 +24,9 @@
2424
/**
2525
* Utility to load the default HTTP client factory and create an instance of {@link SdkHttpClient}.
2626
*/
27-
@SdkInternalApi
27+
// NOTE: This was previously @SdkInternalApi, which is why it's in the .internal. package. It was moved to a protected API to
28+
// allow usage outside of core for modules that need to use an HTTP client directly, such as sns-message-manager.
29+
@SdkProtectedApi
2830
public final class DefaultSdkHttpClientBuilder implements SdkHttpClient.Builder {
2931

3032
private static final SdkHttpServiceProvider<SdkHttpService> DEFAULT_CHAIN = new CachingSdkHttpServiceProvider<>(

services-custom/sns-message-manager/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@
9898
<artifactId>httpclient5</artifactId>
9999
<version>${httpcomponents.client5.version}</version>
100100
</dependency>
101+
<dependency>
102+
<groupId>software.amazon.awssdk</groupId>
103+
<artifactId>apache5-client</artifactId>
104+
<version>${project.version}</version>
105+
<scope>runtime</scope>
106+
</dependency>
101107
<dependency>
102108
<groupId>org.assertj</groupId>
103109
<artifactId>assertj-core</artifactId>
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.messagemanager.sns;
17+
18+
import java.io.InputStream;
19+
import software.amazon.awssdk.annotations.SdkPublicApi;
20+
import software.amazon.awssdk.http.SdkHttpClient;
21+
import software.amazon.awssdk.messagemanager.sns.internal.DefaultSnsMessageManager;
22+
import software.amazon.awssdk.messagemanager.sns.model.SnsMessage;
23+
import software.amazon.awssdk.regions.Region;
24+
import software.amazon.awssdk.utils.SdkAutoCloseable;
25+
26+
27+
/**
28+
* Message manager for validating SNS message signatures. Create an instance using {@link #builder()}.
29+
*
30+
* <p>This manager provides automatic validation of SNS message signatures received via HTTP/HTTPS endpoints,
31+
* ensuring that messages originate from Amazon SNS and have not been modified during transmission.
32+
* It supports both SignatureVersion1 (SHA1) and SignatureVersion2 (SHA256) as per AWS SNS standards.
33+
*
34+
* <p>The manager handles certificate retrieval, caching, and validation automatically, supporting different
35+
* AWS regions and partitions (aws, aws-gov, aws-cn).
36+
*
37+
* <p>Basic usage with default configuration:
38+
* <pre>
39+
* {@code
40+
* SnsMessageManager messageManager = SnsMessageManager.builder().build();
41+
*
42+
* try {
43+
* SnsMessage validatedMessage = messageManager.parseMessage(messageBody);
44+
* String messageContent = validatedMessage.message();
45+
* String topicArn = validatedMessage.topicArn();
46+
* // Process the validated message
47+
* } catch (SdkClientException e) {
48+
* // Handle validation failure
49+
* logger.error("SNS message validation failed: {}", e.getMessage());
50+
* }
51+
* }
52+
* </pre>
53+
*
54+
* <p>Advanced usage with custom HTTP client:
55+
* <pre>
56+
* {@code
57+
* SnsMessageManager messageManager = SnsMessageManager.builder()
58+
* .httpClient(ApacheHttpClient.create())
59+
* .build();
60+
* }
61+
* </pre>
62+
*
63+
* @see SnsMessage
64+
* @see Builder
65+
*/
66+
@SdkPublicApi
67+
public interface SnsMessageManager extends SdkAutoCloseable {
68+
69+
/**
70+
* Creates a builder for configuring and creating an {@link SnsMessageManager}.
71+
*
72+
* @return A new builder.
73+
*/
74+
static Builder builder() {
75+
return DefaultSnsMessageManager.builder();
76+
}
77+
78+
/**
79+
* Parses and validates an SNS message from a stream.
80+
* <p>
81+
* This method reads the JSON message payload, validates the signature, returns a parsed SNS message object with all
82+
* message attributes if validation succeeds.
83+
*/
84+
SnsMessage parseMessage(InputStream messageStream);
85+
86+
/**
87+
* Parses and validates an SNS message from a string.
88+
* <p>
89+
* This method reads the JSON message payload, validates the signature, returns a parsed SNS message object with all
90+
* message attributes if validation succeeds.
91+
*/
92+
SnsMessage parseMessage(String messageContent);
93+
94+
/**
95+
* Close this {@code SnsMessageManager}, releasing any resources it owned.
96+
* <p>
97+
* <b>Note:</b> if you provided your own {@link SdkHttpClient}, you must close it separately.
98+
*/
99+
@Override
100+
void close();
101+
102+
interface Builder {
103+
104+
/**
105+
* Sets the HTTP client to use for certificate retrieval. The caller is responsible for closing this HTTP client after
106+
* the {@code SnsMessageManager} is closed.
107+
*
108+
* @param httpClient The HTTP client to use for fetching signing certificates.
109+
* @return This builder for method chaining.
110+
*/
111+
Builder httpClient(SdkHttpClient httpClient);
112+
113+
/**
114+
* Sets the AWS region for certificate validation. This region must match the SNS region where the messages originate.
115+
*
116+
* @param region The AWS region where the SNS messages originate.
117+
* @return This builder for method chaining.
118+
*/
119+
Builder region(Region region);
120+
121+
/**
122+
* Builds an instance of {@link SnsMessageManager} based on the supplied configurations.
123+
*
124+
* @return An initialized SnsMessageManager ready to validate SNS messages.
125+
*/
126+
SnsMessageManager build();
127+
}
128+
}

services-custom/sns-message-manager/src/main/java/software/amazon/awssdk/messagemanager/sns/internal/CertificateRetriever.java

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import software.amazon.awssdk.http.SdkHttpRequest;
4141
import software.amazon.awssdk.utils.IoUtils;
4242
import software.amazon.awssdk.utils.Lazy;
43+
import software.amazon.awssdk.utils.SdkAutoCloseable;
4344
import software.amazon.awssdk.utils.Validate;
4445
import software.amazon.awssdk.utils.cache.lru.LruCache;
4546

@@ -49,7 +50,7 @@
4950
* This class retrieves the certificate used to sign a message, validates it, and caches them for future use.
5051
*/
5152
@SdkInternalApi
52-
public final class CertificateRetriever {
53+
public class CertificateRetriever implements SdkAutoCloseable {
5354
private static final Lazy<Pattern> X509_FORMAT = new Lazy<>(() ->
5455
Pattern.compile(
5556
"^[\\s]*-----BEGIN [A-Z]+-----\\n[A-Za-z\\d+\\/\\n]+[=]{0,2}\\n-----END [A-Z]+-----[\\s]*$"));
@@ -61,26 +62,31 @@ public final class CertificateRetriever {
6162
private final CertificateUrlValidator certUrlValidator;
6263
private final LruCache<URI, PublicKey> certificateCache;
6364

64-
public CertificateRetriever(SdkHttpClient httpClient, String certCommonName) {
65-
this(httpClient, certCommonName, new CertificateUrlValidator(certCommonName));
65+
public CertificateRetriever(SdkHttpClient httpClient, String certHost, String certCommonName) {
66+
this(httpClient, certCommonName, new CertificateUrlValidator(certHost));
6667
}
6768

6869
CertificateRetriever(SdkHttpClient httpClient, String certCommonName, CertificateUrlValidator certificateUrlValidator) {
6970
this.httpClient = Validate.paramNotNull(httpClient, "httpClient");
7071
this.certCommonName = Validate.paramNotNull(certCommonName, "certCommonName");
71-
this.certificateCache = LruCache.builder(this::getCertificate)
72+
this.certificateCache = LruCache.builder(this::fetchCertificate)
7273
.maxSize(10)
7374
.build();
7475
this.certUrlValidator = Validate.paramNotNull(certificateUrlValidator, "certificateUrlValidator");
7576
}
7677

77-
public byte[] retrieveCertificate(URI certificateUrl) {
78+
public PublicKey retrieveCertificate(URI certificateUrl) {
7879
Validate.paramNotNull(certificateUrl, "certificateUrl");
7980
certUrlValidator.validate(certificateUrl);
80-
return certificateCache.get(certificateUrl).getEncoded();
81+
return certificateCache.get(certificateUrl);
8182
}
8283

83-
private PublicKey getCertificate(URI certificateUrl) {
84+
@Override
85+
public void close() {
86+
httpClient.close();
87+
}
88+
89+
private PublicKey fetchCertificate(URI certificateUrl) {
8490
byte[] cert = fetchUrl(certificateUrl);
8591
validateCertificateData(cert);
8692
return createPublicKey(cert);

services-custom/sns-message-manager/src/main/java/software/amazon/awssdk/messagemanager/sns/internal/CertificateUrlValidator.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,18 @@
1818
import java.net.URI;
1919
import software.amazon.awssdk.annotations.SdkInternalApi;
2020
import software.amazon.awssdk.core.exception.SdkClientException;
21+
import software.amazon.awssdk.utils.Validate;
2122

2223
/**
2324
* Validates that the signing certificate URL is valid.
2425
*/
2526
@SdkInternalApi
2627
public class CertificateUrlValidator {
27-
private final String expectedCommonName;
28+
private final String certificateHost;
2829

29-
public CertificateUrlValidator(String expectedCommonName) {
30-
this.expectedCommonName = expectedCommonName;
30+
public CertificateUrlValidator(String certificateHost) {
31+
Validate.notBlank(certificateHost, "Expected certificate host cannot be null or empty");
32+
this.certificateHost = certificateHost;
3133
}
3234

3335
public void validate(URI certificateUrl) {
@@ -39,8 +41,8 @@ public void validate(URI certificateUrl) {
3941
throw SdkClientException.create("Certificate URL must use HTTPS");
4042
}
4143

42-
if (!expectedCommonName.equals(certificateUrl.getHost())) {
43-
throw SdkClientException.create("Certificate URL does not match expected host: " + expectedCommonName);
44+
if (!certificateHost.equals(certificateUrl.getHost())) {
45+
throw SdkClientException.create("Certificate URL does not match expected host: " + certificateHost);
4446
}
4547
}
4648
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.messagemanager.sns.internal;
17+
18+
import java.io.ByteArrayInputStream;
19+
import java.io.InputStream;
20+
import java.net.URI;
21+
import java.nio.charset.StandardCharsets;
22+
import java.security.PublicKey;
23+
import java.time.Duration;
24+
import software.amazon.awssdk.annotations.SdkInternalApi;
25+
import software.amazon.awssdk.annotations.SdkTestInternalApi;
26+
import software.amazon.awssdk.core.internal.http.loader.DefaultSdkHttpClientBuilder;
27+
import software.amazon.awssdk.http.SdkHttpClient;
28+
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
29+
import software.amazon.awssdk.messagemanager.sns.SnsMessageManager;
30+
import software.amazon.awssdk.messagemanager.sns.model.SnsMessage;
31+
import software.amazon.awssdk.regions.Region;
32+
import software.amazon.awssdk.utils.AttributeMap;
33+
import software.amazon.awssdk.utils.Validate;
34+
35+
@SdkInternalApi
36+
public final class DefaultSnsMessageManager implements SnsMessageManager {
37+
private static final AttributeMap HTTP_CLIENT_DEFAULTS =
38+
AttributeMap.builder()
39+
.put(SdkHttpConfigurationOption.CONNECTION_TIMEOUT, Duration.ofSeconds(10))
40+
.put(SdkHttpConfigurationOption.READ_TIMEOUT, Duration.ofSeconds(30))
41+
.build();
42+
43+
private final SnsMessageUnmarshaller unmarshaller;
44+
private final CertificateRetriever certRetriever;
45+
private final SignatureValidator signatureValidator;
46+
47+
private DefaultSnsMessageManager(BuilderImpl builder) {
48+
this.unmarshaller = new SnsMessageUnmarshaller();
49+
50+
SnsHostProvider hostProvider = new SnsHostProvider(builder.region);
51+
URI signingCertEndpoint = hostProvider.regionalEndpoint();
52+
String signingCertCommonName = hostProvider.signingCertCommonName();
53+
54+
SdkHttpClient httpClient = resolveHttpClient(builder);
55+
certRetriever = builder.certRetriever != null
56+
? builder.certRetriever
57+
: new CertificateRetriever(httpClient, signingCertEndpoint.getHost(), signingCertCommonName);
58+
59+
signatureValidator = new SignatureValidator();
60+
}
61+
62+
@Override
63+
public SnsMessage parseMessage(InputStream message) {
64+
Validate.notNull(message, "message cannot be null");
65+
66+
SnsMessage snsMessage = unmarshaller.unmarshall(message);
67+
PublicKey certificate = certRetriever.retrieveCertificate(snsMessage.signingCertUrl());
68+
69+
signatureValidator.validateSignature(snsMessage, certificate);
70+
71+
return snsMessage;
72+
}
73+
74+
@Override
75+
public SnsMessage parseMessage(String message) {
76+
Validate.notNull(message, "message cannot be null");
77+
return parseMessage(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)));
78+
}
79+
80+
@Override
81+
public void close() {
82+
certRetriever.close();
83+
}
84+
85+
public static Builder builder() {
86+
return new BuilderImpl();
87+
}
88+
89+
private static SdkHttpClient resolveHttpClient(BuilderImpl builder) {
90+
if (builder.httpClient != null) {
91+
return new UnmanagedSdkHttpClient(builder.httpClient);
92+
}
93+
94+
return new DefaultSdkHttpClientBuilder().buildWithDefaults(HTTP_CLIENT_DEFAULTS);
95+
}
96+
97+
static class BuilderImpl implements SnsMessageManager.Builder {
98+
private Region region;
99+
private SdkHttpClient httpClient;
100+
101+
// Testing only
102+
private CertificateRetriever certRetriever;
103+
104+
@Override
105+
public Builder httpClient(SdkHttpClient httpClient) {
106+
this.httpClient = httpClient;
107+
return this;
108+
}
109+
110+
@Override
111+
public Builder region(Region region) {
112+
this.region = region;
113+
return this;
114+
}
115+
116+
@SdkTestInternalApi
117+
Builder certificateRetriever(CertificateRetriever certificateRetriever) {
118+
this.certRetriever = certificateRetriever;
119+
return this;
120+
}
121+
122+
@Override
123+
public SnsMessageManager build() {
124+
return new DefaultSnsMessageManager(this);
125+
}
126+
}
127+
}

0 commit comments

Comments
 (0)