Skip to content

Commit

Permalink
Adding new logic to send user-agent in S3 Requests (#42)
Browse files Browse the repository at this point in the history
* Adding new logic to send user-agent in S3 Requests

With this commit, we will send s3connectorframework user-agent in
every S3 request to help with observability. This commit also
introduces ObjectClientConfiguration to let uses to prepend user-agent
with custom values.

* add asana ticket to todo
  • Loading branch information
fuatbasik authored Jun 14, 2024
1 parent a2617be commit d180d0b
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.amazon.connector.s3;

import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;

/** Configuration for {@link ObjectClient} */
@Getter
@Builder
@EqualsAndHashCode
public class ObjectClientConfiguration {
private static final String DEFAULT_USER_AGENT_PREFIX = null;

/** User Agent Prefix. {@link ObjectClientConfiguration#DEFAULT_USER_AGENT_PREFIX} by default. */
@Builder.Default private String userAgentPrefix = DEFAULT_USER_AGENT_PREFIX;

public static final ObjectClientConfiguration DEFAULT =
ObjectClientConfiguration.builder().build();

/**
* Construct {@link ObjectClientConfiguration}
*
* @param userAgentPrefix Prefix to prepend to ObjectClient's userAgent.
*/
@Builder
private ObjectClientConfiguration(String userAgentPrefix) {
this.userAgentPrefix = userAgentPrefix;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,37 @@

/** Object client, based on AWS SDK v2 */
public class S3SdkObjectClient implements ObjectClient, AutoCloseable {

public static final String HEADER_USER_AGENT = "User-Agent";

private S3AsyncClient s3AsyncClient = null;
private ObjectClientConfiguration objectClientConfiguration = null;
private final UserAgent userAgent;

/**
* Create an instance of a S3 client for interaction with Amazon S3 compatible object stores.
* Create an instance of a S3 client, with default configuration, for interaction with Amazon S3
* compatible object stores.
*
* @param s3AsyncClient Underlying client to be used for making requests to S3.
*/
public S3SdkObjectClient(@NonNull S3AsyncClient s3AsyncClient) {
this(s3AsyncClient, ObjectClientConfiguration.DEFAULT);
}

/**
* Create an instance of a S3 client, for interaction with Amazon S3 compatible object stores.
*
* @param s3AsyncClient Underlying client to be used for making requests to S3.
* @param objectClientConfiguration Configuration for object client.
*/
public S3SdkObjectClient(
@NonNull S3AsyncClient s3AsyncClient,
@NonNull ObjectClientConfiguration objectClientConfiguration) {

this.s3AsyncClient = s3AsyncClient;
this.objectClientConfiguration = objectClientConfiguration;
this.userAgent = new UserAgent();
this.userAgent.prepend(objectClientConfiguration.getUserAgentPrefix());
}

@Override
Expand All @@ -39,12 +61,17 @@ public void close() {
*/
@Override
public CompletableFuture<ObjectMetadata> headObject(HeadRequest headRequest) {
HeadObjectRequest.Builder builder =
HeadObjectRequest.builder().bucket(headRequest.getBucket()).key(headRequest.getKey());

// Add User-Agent header to the request.
builder.overrideConfiguration(
AwsRequestOverrideConfiguration.builder()
.putHeader(HEADER_USER_AGENT, this.userAgent.getUserAgent())
.build());

return s3AsyncClient
.headObject(
HeadObjectRequest.builder()
.bucket(headRequest.getBucket())
.key(headRequest.getKey())
.build())
.headObject(builder.build())
.thenApply(
headObjectResponse ->
ObjectMetadata.builder().contentLength(headObjectResponse.contentLength()).build());
Expand All @@ -67,7 +94,10 @@ public CompletableFuture<ObjectContent> getObject(GetRequest getRequest) {
// Temporarily adding range of data requested as a Referrer header to allow for easy analysis
// of access logs. This is similar to what the Auditing feature in S3A does.
builder.overrideConfiguration(
AwsRequestOverrideConfiguration.builder().putHeader("Referer", range).build());
AwsRequestOverrideConfiguration.builder()
.putHeader("Referer", range)
.putHeader(HEADER_USER_AGENT, this.userAgent.getUserAgent())
.build());
}

return s3AsyncClient
Expand Down
36 changes: 36 additions & 0 deletions object-client/src/main/java/com/amazon/connector/s3/UserAgent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.amazon.connector.s3;

import java.util.Objects;
import lombok.Getter;

/** User-Agent to be used by ObjectClients */
@Getter
public final class UserAgent {
// Hard-coded user-agent string
private static final String UA_STRING = "s3connectorframework";
// TODO: Get VersionInfo and append it to UA. We need to understand how to create (if we want a)
// global version (for InputStream and ObjectClient).
// https://app.asana.com/0/1206885953994785/1207481230403504/f
private static final String VERSION_INFO = null;
/**
* Disallowed characters in the user agent token: @see <a
* href="https://tools.ietf.org/html/rfc7230#section-3.2.6">RFC 7230</a>
*/
private static final String UA_DENYLIST_REGEX = "[() ,/:;<=>?@\\[\\]{}\\\\]";

private String userAgent = UA_STRING + ":" + VERSION_INFO;

/**
* Prepend hard-coded user-agent string with input string provided.
*
* @param userAgentPrefix to prepend the default user-agent string
*/
public void prepend(String userAgentPrefix) {
if (Objects.nonNull(userAgentPrefix))
this.userAgent = sanitizeInput(userAgentPrefix) + "/" + this.userAgent;
}

private static String sanitizeInput(String input) {
return input == null ? "unknown" : input.replaceAll(UA_DENYLIST_REGEX, "_");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ void testConstructorWithWrappedClient() {
assertNotNull(client);
}

@Test
void testConstructorWithConfiguration() {
ObjectClientConfiguration configuration = ObjectClientConfiguration.DEFAULT;
S3SdkObjectClient client = new S3SdkObjectClient(s3AsyncClient, configuration);
assertNotNull(client);
}

@Test
void testConstructorThrowsOnNullArgument() {
assertThrows(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.amazon.connector.s3;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;

public class UserAgentTest {

@Test
void testDefaultUserAgent() {
UserAgent agent = new UserAgent();
assertNotNull(agent.getUserAgent());
}

@Test
void testPrependUserAgent() {
UserAgent agent = new UserAgent();
agent.prepend("unit_test");
assertTrue(agent.getUserAgent().startsWith("unit_test"));
}

@Test
void testNullPrependIsNoop() {
UserAgent agent = new UserAgent();
String pre = agent.getUserAgent();
agent.prepend(null);
String pos = agent.getUserAgent();
assertEquals(pre, pos);
}

@Test
void testInvalidInputIsSanitised() {
UserAgent agent = new UserAgent();
agent.prepend("unit/test");
assertTrue(agent.getUserAgent().startsWith("unit_test"));
}
}

0 comments on commit d180d0b

Please sign in to comment.