Skip to content

Add scheme to destination region endpoint #1572

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

Merged
merged 1 commit into from
Jan 8, 2020
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/next-release/bugfix-AmazonEC2-b80e04f.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "bugfix",
"category": "Amazon EC2",
"description": "Fix NPE when calling `CopySnapshot`. Fixes [#1564](https://github.com/aws/aws-sdk-java-v2/issues/1564)"
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
import static software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute.AWS_CREDENTIALS;

import java.net.URI;
import java.time.Clock;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.annotations.SdkTestInternalApi;
import software.amazon.awssdk.auth.signer.Aws4Signer;
import software.amazon.awssdk.auth.signer.params.Aws4PresignerParams;
import software.amazon.awssdk.awscore.util.AwsHostNameUtils;
Expand All @@ -40,7 +42,6 @@

/**
* ExecutionInterceptor that generates a pre-signed URL for copying encrypted snapshots
* TODO: Is this actually right? What if a different interceptor modifies the message? Should this be treated as a signer?
*/
@SdkInternalApi
public final class GeneratePreSignUrlInterceptor implements ExecutionInterceptor {
Expand All @@ -57,6 +58,17 @@ public final class GeneratePreSignUrlInterceptor implements ExecutionInterceptor

private static final CopySnapshotRequestMarshaller MARSHALLER = new CopySnapshotRequestMarshaller(PROTOCOL_FACTORY);

private final Clock testClock; // for testing only

public GeneratePreSignUrlInterceptor() {
testClock = null;
}

@SdkTestInternalApi
GeneratePreSignUrlInterceptor(Clock testClock) {
this.testClock = testClock;
}

@Override
public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) {
SdkHttpRequest request = context.httpRequest();
Expand Down Expand Up @@ -122,6 +134,7 @@ private Aws4PresignerParams getPresignerParams(ExecutionAttributes attributes, S
.signingRegion(Region.of(signingRegion))
.signingName(signingName)
.awsCredentials(attributes.getAttribute(AWS_CREDENTIALS))
.signingClockOverride(testClock)
.build();
}

Expand Down Expand Up @@ -152,6 +165,10 @@ private URI createEndpoint(String regionName, String serviceName) {
.build();
}

return Ec2Client.serviceMetadata().endpointFor(region);
URI endpoint = Ec2Client.serviceMetadata().endpointFor(region);
if (endpoint.getScheme() == null) {
return URI.create("https://" + endpoint);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we pull the client configured protocol in an interceptor?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I considered this, but given that this URL is used by EC2 and not consumed by the customer/client, it doesn't really make sense to be anything other than HTTPS. It's also the same behavior as V1 https://github.com/aws/aws-sdk-java/blob/7b1e5b87b0bf03456df9e77716b14731adf9a7a7/aws-java-sdk-ec2/src/main/java/com/amazonaws/services/ec2/model/transform/GeneratePreSignUrlRequestHandler.java#L145-L147

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 on pulling client configured protocol. Customers might use this against a mock server (maybe for testing purpose) which only supports HTTP. I think we should honor the customer configuration.

Copy link
Contributor Author

@dagnir dagnir Jan 6, 2020

Choose a reason for hiding this comment

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

I'm not sure I understand the use case. The presigned URL is explicitly used by the EC2 service to presumably pull the snapshot for cross region copying, it doesn't affect where or how the actual HTTP request being modified is sent.

Copy link
Contributor

Choose a reason for hiding this comment

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

I meant there might be some mockEc2 server library available for customers to test their applications. The mockEc2 server might have a similar copySnapshot function to mimic the real service's behavior. This use case might be rare or doesn't exist at all, but I still think it'd be better to honor the customer configuration.

Copy link
Contributor Author

@dagnir dagnir Jan 6, 2020

Choose a reason for hiding this comment

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

I disagree that it's important in this case; we've had the same behavior in v1 for a long time without issue, the same with RDS. Technically the protocol configured on the client specifies the protocol it speaks to service we're calling the API on anyway, I don't think it necessarily governs things like the presigned URL(s) it generates.

IMO it's also a potential dangerous if the customer happens to being using HTTP and now their EBS snapshots are also being transferred in plaintext.

}
return endpoint;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.awssdk.services.ec2.transform.internal;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import static software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute.AWS_CREDENTIALS;
import java.net.URI;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.core.interceptor.Context;
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
import software.amazon.awssdk.http.SdkHttpFullRequest;
import software.amazon.awssdk.http.SdkHttpMethod;
import software.amazon.awssdk.http.SdkHttpRequest;
import software.amazon.awssdk.services.ec2.model.CopySnapshotRequest;

@RunWith(MockitoJUnitRunner.class)
public class GeneratePreSignUrlInterceptorTest {
private static final GeneratePreSignUrlInterceptor INTERCEPTOR = new GeneratePreSignUrlInterceptor();

@Mock
private Context.ModifyHttpRequest mockContext;

@Test
public void copySnapshotRequest_httpsProtocolAddedToEndpoint() {
SdkHttpFullRequest request = SdkHttpFullRequest.builder()
.uri(URI.create("https://ec2.us-west-2.amazonaws.com"))
.method(SdkHttpMethod.POST)
.build();

CopySnapshotRequest ec2Request = CopySnapshotRequest.builder()
.sourceRegion("us-west-2")
.destinationRegion("us-east-2")
.build();

when(mockContext.httpRequest()).thenReturn(request);
when(mockContext.request()).thenReturn(ec2Request);

ExecutionAttributes attrs = new ExecutionAttributes();
attrs.putAttribute(AWS_CREDENTIALS, AwsBasicCredentials.create("foo", "bar"));

SdkHttpRequest modifiedRequest = INTERCEPTOR.modifyHttpRequest(mockContext, attrs);

String presignedUrl = modifiedRequest.rawQueryParameters().get("PresignedUrl").get(0);

assertThat(presignedUrl).startsWith("https://");
}

@Test
public void copySnapshotRequest_generatesCorrectPresignedUrl() {
// Expected URL was derived by first making a request to EC2 using
// valid credentials and a KMS encrypted snapshot and verifying that
// the snapshot was copied to the destination region, also encrypted.
// Then the same code was used to make a second request, changing only
// the credentials and snapshot ID.
String expectedPresignedUrl = "https://ec2.us-west-2.amazonaws.com?Action=CopySnapshot" +
"&Version=2016-11-15" +
"&DestinationRegion=us-east-1" +
"&SourceRegion=us-west-2" +
"&SourceSnapshotId=SNAPSHOT_ID" +
"&X-Amz-Algorithm=AWS4-HMAC-SHA256" +
"&X-Amz-Date=20200107T205609Z" +
"&X-Amz-SignedHeaders=host" +
"&X-Amz-Expires=604800" +
"&X-Amz-Credential=akid%2F20200107%2Fus-west-2%2Fec2%2Faws4_request" +
"&X-Amz-Signature=c1f5e34834292a86ff2b46b5e97cebaf2967b09641b4e2e60a382a37d137a03b";

ZoneId utcZone = ZoneId.of("UTC").normalized();

// Same signing date as the one used for the request above
Instant signingInstant = ZonedDateTime.of(2020, 1, 7, 20, 56, 9, 0, utcZone).toInstant();
Clock signingClock = Clock.fixed(signingInstant, utcZone);

GeneratePreSignUrlInterceptor interceptor = new GeneratePreSignUrlInterceptor(signingClock);

// These details don't really affect the test as they're not used in generating the signed URL
SdkHttpFullRequest request = SdkHttpFullRequest.builder()
.uri(URI.create("https://ec2.us-west-2.amazonaws.com"))
.method(SdkHttpMethod.POST)
.build();

CopySnapshotRequest ec2Request = CopySnapshotRequest.builder()
.sourceRegion("us-west-2")
.destinationRegion("us-east-1")
.sourceSnapshotId("SNAPSHOT_ID")
.build();

when(mockContext.httpRequest()).thenReturn(request);
when(mockContext.request()).thenReturn(ec2Request);

ExecutionAttributes attrs = new ExecutionAttributes();
attrs.putAttribute(AWS_CREDENTIALS, AwsBasicCredentials.create("akid", "skid"));

SdkHttpRequest modifiedRequest = interceptor.modifyHttpRequest(mockContext, attrs);

String generatedPresignedUrl = modifiedRequest.rawQueryParameters().get("PresignedUrl").get(0);

assertThat(generatedPresignedUrl).isEqualTo(expectedPresignedUrl);
}
}