From 8d2492066b4996dfb73aba7496ca8831299be7f7 Mon Sep 17 00:00:00 2001 From: Anna-Karin Salander Date: Fri, 20 Sep 2024 09:43:09 -0700 Subject: [PATCH] Update user agent construction and deprecate old SdkUserAgent class (#5596) * Update user agent construction and deprecate old SdkUserAgent class * Add japicmp exclusion for USER_REGION * Removing validation of user agent prefix and user agent suffix from testing, since client no longer depends on those values to be an empty string * Reverting setting default values for prefix/suffix params and some other changes * Updating codegen tests and fixing internal marker bug * Small log name change * Unused import --- .../feature-AWSSDKforJavav2-e846f47.json | 6 + .../amazon/awssdk/spotbugs-suppressions.xml | 1 + .../imds/internal/RequestMarshaller.java | 4 +- .../builder/SdkDefaultClientBuilder.java | 50 +++-- .../core/client/config/SdkClientOption.java | 3 + .../config/SdkClientOptionValidation.java | 1 - .../pipeline/stages/ApplyUserAgentStage.java | 160 ++++++---------- .../useragent/DefaultSystemUserAgent.java | 146 +++++++++++++++ .../SdkClientUserAgentProperties.java | 43 +++++ .../useragent/SdkUserAgentBuilder.java | 127 +++++++++++++ .../internal/useragent/UserAgentConstant.java | 125 +++++++++++++ .../useragent/UserAgentLangValues.java | 91 +++++++++ .../amazon/awssdk/core/util/SdkUserAgent.java | 124 +------------ .../awssdk/core/util/SystemUserAgent.java | 59 ++++++ .../core/http/AmazonHttpClientTest.java | 79 ++++---- .../stages/ApplyUserAgentStageTest.java | 84 +++++++-- .../useragent/SdkUserAgentBuilderTest.java | 172 ++++++++++++++++++ .../useragent/SystemUserAgentTest.java | 132 ++++++++++++++ .../awssdk/core/util/SdkUserAgentTest.java | 32 +--- pom.xml | 2 + .../InternalUserAgentTest.java | 7 +- .../ClientDefaultsModeTestSuite.java | 6 +- .../awssdk/utils/JavaSystemSetting.java | 2 +- 23 files changed, 1134 insertions(+), 322 deletions(-) create mode 100644 .changes/next-release/feature-AWSSDKforJavav2-e846f47.json create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/DefaultSystemUserAgent.java create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/SdkClientUserAgentProperties.java create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/SdkUserAgentBuilder.java create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/UserAgentConstant.java create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/UserAgentLangValues.java create mode 100644 core/sdk-core/src/main/java/software/amazon/awssdk/core/util/SystemUserAgent.java create mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/useragent/SdkUserAgentBuilderTest.java create mode 100644 core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/useragent/SystemUserAgentTest.java diff --git a/.changes/next-release/feature-AWSSDKforJavav2-e846f47.json b/.changes/next-release/feature-AWSSDKforJavav2-e846f47.json new file mode 100644 index 000000000000..a00bcafb4221 --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-e846f47.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Refactoring the user agent string format to be more consistent across SDKs" +} diff --git a/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml b/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml index 0642bb0f7bce..b1f14527e131 100644 --- a/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml +++ b/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml @@ -280,6 +280,7 @@ + diff --git a/core/imds/src/main/java/software/amazon/awssdk/imds/internal/RequestMarshaller.java b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/RequestMarshaller.java index 04358583ef3e..516def7dd985 100644 --- a/core/imds/src/main/java/software/amazon/awssdk/imds/internal/RequestMarshaller.java +++ b/core/imds/src/main/java/software/amazon/awssdk/imds/internal/RequestMarshaller.java @@ -18,7 +18,7 @@ import java.net.URI; import java.time.Duration; import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.core.util.SdkUserAgent; +import software.amazon.awssdk.core.util.SystemUserAgent; import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.http.SdkHttpMethod; @@ -69,7 +69,7 @@ public SdkHttpFullRequest createDataRequest(String path, String token, Duration private SdkHttpFullRequest.Builder defaulttHttpBuilder() { return SdkHttpFullRequest.builder() - .putHeader(USER_AGENT, SdkUserAgent.create().userAgent()) + .putHeader(USER_AGENT, SystemUserAgent.getOrCreate().userAgentString()) .putHeader(ACCEPT, "*/*") .putHeader(CONNECTION, "keep-alive"); } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java index 44847e35c1da..7dd7e99a0c90 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java @@ -48,6 +48,10 @@ import static software.amazon.awssdk.core.client.config.SdkClientOption.RETRY_STRATEGY; import static software.amazon.awssdk.core.client.config.SdkClientOption.SCHEDULED_EXECUTOR_SERVICE; import static software.amazon.awssdk.core.client.config.SdkClientOption.SYNC_HTTP_CLIENT; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.HTTP; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.INTERNAL_METADATA_MARKER; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.IO; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.RETRY_MODE; import static software.amazon.awssdk.utils.CollectionUtils.mergeLists; import static software.amazon.awssdk.utils.Validate.paramNotNull; @@ -71,6 +75,7 @@ import software.amazon.awssdk.annotations.SdkProtectedApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; import software.amazon.awssdk.core.ClientEndpointProvider; +import software.amazon.awssdk.core.ClientType; import software.amazon.awssdk.core.CompressionConfiguration; import software.amazon.awssdk.core.SdkPlugin; import software.amazon.awssdk.core.SdkSystemSetting; @@ -82,13 +87,14 @@ import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; import software.amazon.awssdk.core.internal.http.loader.DefaultSdkAsyncHttpClientBuilder; import software.amazon.awssdk.core.internal.http.loader.DefaultSdkHttpClientBuilder; -import software.amazon.awssdk.core.internal.http.pipeline.stages.ApplyUserAgentStage; import software.amazon.awssdk.core.internal.http.pipeline.stages.CompressRequestStage; import software.amazon.awssdk.core.internal.interceptor.HttpChecksumValidationInterceptor; import software.amazon.awssdk.core.internal.retry.SdkDefaultRetryStrategy; +import software.amazon.awssdk.core.internal.useragent.SdkClientUserAgentProperties; +import software.amazon.awssdk.core.internal.useragent.SdkUserAgentBuilder; import software.amazon.awssdk.core.retry.RetryMode; import software.amazon.awssdk.core.retry.RetryPolicy; -import software.amazon.awssdk.core.util.SdkUserAgent; +import software.amazon.awssdk.core.util.SystemUserAgent; import software.amazon.awssdk.http.ExecutableHttpRequest; import software.amazon.awssdk.http.HttpExecuteRequest; import software.amazon.awssdk.http.SdkHttpClient; @@ -108,8 +114,10 @@ import software.amazon.awssdk.utils.Either; import software.amazon.awssdk.utils.Lazy; import software.amazon.awssdk.utils.OptionalUtils; +import software.amazon.awssdk.utils.StringUtils; import software.amazon.awssdk.utils.ThreadFactoryBuilder; import software.amazon.awssdk.utils.Validate; +import software.amazon.awssdk.utils.http.SdkHttpUtils; /** * An SDK-internal implementation of the methods in {@link SdkClientBuilder}, {@link SdkAsyncClientBuilder} and @@ -283,7 +291,7 @@ private SdkClientConfiguration mergeGlobalDefaults(SdkClientConfiguration config .lazyOption(PROFILE_FILE, conf -> conf.get(PROFILE_FILE_SUPPLIER).get()) .option(PROFILE_NAME, ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow()) - .option(USER_AGENT_PREFIX, SdkUserAgent.create().userAgent()) + .option(USER_AGENT_PREFIX, "") .option(USER_AGENT_SUFFIX, "") .option(CRC32_FROM_COMPRESSED_DATA_ENABLED, false) .option(CONFIGURED_COMPRESSION_CONFIGURATION, @@ -384,7 +392,8 @@ protected SdkClientConfiguration invokePlugins(SdkClientConfiguration config) { return config; } - private String resolveRetryMode(RetryPolicy retryPolicy, RetryStrategy retryStrategy) { + //TODO (useragent): Refactor this as part of moving value to business metrics (UA 2.1) + private static String resolveRetryMode(RetryPolicy retryPolicy, RetryStrategy retryStrategy) { if (retryPolicy != null) { return retryPolicy.retryMode().toString(); } @@ -401,13 +410,32 @@ private String resolveRetryMode(RetryPolicy retryPolicy, RetryStrategy retryStra } private String resolveClientUserAgent(LazyValueSource config) { - String retryMode = resolveRetryMode(config.get(RETRY_POLICY), config.get(RETRY_STRATEGY)); - return ApplyUserAgentStage.resolveClientUserAgent(config.get(USER_AGENT_PREFIX), - config.get(INTERNAL_USER_AGENT), - config.get(CLIENT_TYPE), - config.get(SYNC_HTTP_CLIENT), - config.get(ASYNC_HTTP_CLIENT), - retryMode); + SdkClientUserAgentProperties clientProperties = new SdkClientUserAgentProperties(); + + ClientType clientType = config.get(CLIENT_TYPE); + ClientType resolvedClientType = clientType == null ? ClientType.UNKNOWN : config.get(CLIENT_TYPE); + + clientProperties.putProperty(RETRY_MODE, StringUtils.lowerCase(resolveRetryMode(config.get(RETRY_POLICY), + config.get(RETRY_STRATEGY)))); + clientProperties.putProperty(INTERNAL_METADATA_MARKER, StringUtils.trimToEmpty(config.get(INTERNAL_USER_AGENT))); + clientProperties.putProperty(IO, StringUtils.lowerCase(resolvedClientType.name())); + clientProperties.putProperty(HTTP, SdkHttpUtils.urlEncode(clientName(resolvedClientType, + config.get(SYNC_HTTP_CLIENT), + config.get(ASYNC_HTTP_CLIENT)))); + + return SdkUserAgentBuilder.buildClientUserAgentString(SystemUserAgent.getOrCreate(), clientProperties); + } + + private static String clientName(ClientType clientType, SdkHttpClient syncHttpClient, SdkAsyncHttpClient asyncHttpClient) { + if (clientType == SYNC) { + return syncHttpClient == null ? "null" : syncHttpClient.clientName(); + } + + if (clientType == ASYNC) { + return asyncHttpClient == null ? "null" : asyncHttpClient.clientName(); + } + + return ClientType.UNKNOWN.name(); } private RetryStrategy resolveRetryStrategy(LazyValueSource config) { diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java index 5c740cd7b01a..2b24f36dc10f 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java @@ -30,6 +30,7 @@ import software.amazon.awssdk.core.ServiceConfiguration; import software.amazon.awssdk.core.interceptor.ExecutionAttributes; import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.core.internal.useragent.SdkClientUserAgentProperties; import software.amazon.awssdk.core.retry.RetryMode; import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.endpoints.EndpointProvider; @@ -266,6 +267,8 @@ public final class SdkClientOption extends ClientOption { /** * A user agent prefix that is specific to the client (agnostic of the request). + * + * Not currently in use, since the introduction of {@link SdkClientUserAgentProperties} */ public static final SdkClientOption CLIENT_USER_AGENT = new SdkClientOption<>(String.class); diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOptionValidation.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOptionValidation.java index aab55738c828..2075c72e7b87 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOptionValidation.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOptionValidation.java @@ -45,7 +45,6 @@ private static void validateClientOptions(SdkClientConfiguration c) { require("overrideConfiguration.additionalHttpHeaders", c.option(SdkClientOption.ADDITIONAL_HTTP_HEADERS)); require("overrideConfiguration.executionInterceptors", c.option(SdkClientOption.EXECUTION_INTERCEPTORS)); require("overrideConfiguration.retryStrategy", c.option(SdkClientOption.RETRY_STRATEGY)); - require("overrideConfiguration.advancedOption[USER_AGENT_PREFIX]", c.option(SdkAdvancedClientOption.USER_AGENT_PREFIX)); require("overrideConfiguration.advancedOption[USER_AGENT_SUFFIX]", diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApplyUserAgentStage.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApplyUserAgentStage.java index cd21119d28ab..3ac3a43152b3 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApplyUserAgentStage.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApplyUserAgentStage.java @@ -15,15 +15,19 @@ package software.amazon.awssdk.core.internal.http.pipeline.stages; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.AUTH_SOURCE; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.CONFIG_METADATA; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.SLASH; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.SPACE; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.appendSpaceAndField; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.uaPair; +import static software.amazon.awssdk.utils.StringUtils.trim; + import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; -import java.util.function.BinaryOperator; -import java.util.function.UnaryOperator; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.core.ApiName; -import software.amazon.awssdk.core.ClientType; -import software.amazon.awssdk.core.SdkSystemSetting; import software.amazon.awssdk.core.SelectedAuthScheme; import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; @@ -34,15 +38,11 @@ import software.amazon.awssdk.core.internal.http.RequestExecutionContext; import software.amazon.awssdk.core.internal.http.pipeline.MutableRequestToRequestPipeline; import software.amazon.awssdk.core.internal.useragent.IdentityProviderNameMapping; -import software.amazon.awssdk.core.util.SdkUserAgent; -import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.SdkHttpFullRequest; -import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import software.amazon.awssdk.identity.spi.Identity; import software.amazon.awssdk.utils.CompletableFutureUtils; import software.amazon.awssdk.utils.Logger; import software.amazon.awssdk.utils.StringUtils; -import software.amazon.awssdk.utils.http.SdkHttpUtils; /** * A stage for adding the user agent header to the request, after retrieving the current string @@ -55,104 +55,64 @@ public class ApplyUserAgentStage implements MutableRequestToRequestPipeline { private static final Logger log = Logger.loggerFor(ApplyUserAgentStage.class); - private static final String COMMA = ", "; - private static final String SLASH = "/"; - private static final String SPACE = " "; - private static final String HASH = "#"; - private static final String IO = "io"; - private static final String HTTP = "http"; - private static final String CONFIG = "cfg"; - private static final String RETRY_MODE = "retry-mode"; - private static final String AUTH_HEADER = "auth-source"; - private static final String AWS_EXECUTION_ENV_PREFIX = "exec-env/"; - - private static final BinaryOperator API_NAMES = (name, version) -> name + "/" + version; - private static final BinaryOperator CONFIG_METADATA = (param, name) -> CONFIG + SLASH + param + HASH + name; - private static final UnaryOperator AUTH_CONFIG = name -> CONFIG_METADATA.apply(AUTH_HEADER, name); - private final SdkClientConfiguration clientConfig; public ApplyUserAgentStage(HttpClientDependencies dependencies) { this.clientConfig = dependencies.clientConfiguration(); } - public static String resolveClientUserAgent(String userAgentPrefix, - String internalUserAgent, - ClientType clientType, - SdkHttpClient syncHttpClient, - SdkAsyncHttpClient asyncHttpClient, - String retryMode) { - String awsExecutionEnvironment = SdkSystemSetting.AWS_EXECUTION_ENV.getStringValue().orElse(null); - - StringBuilder userAgent = new StringBuilder(128); - - userAgent.append(StringUtils.trimToEmpty(userAgentPrefix)); - - String systemUserAgent = SdkUserAgent.create().userAgent(); - if (!systemUserAgent.equals(userAgentPrefix)) { - userAgent.append(COMMA).append(systemUserAgent); - } - - String trimmedInternalUserAgent = StringUtils.trimToEmpty(internalUserAgent); - if (!trimmedInternalUserAgent.isEmpty()) { - userAgent.append(SPACE).append(trimmedInternalUserAgent); - } - - if (!StringUtils.isEmpty(awsExecutionEnvironment)) { - userAgent.append(SPACE).append(AWS_EXECUTION_ENV_PREFIX).append(awsExecutionEnvironment.trim()); - } - - if (clientType == null) { - clientType = ClientType.UNKNOWN; - } - - userAgent.append(SPACE) - .append(IO) - .append("/") - .append(StringUtils.lowerCase(clientType.name())); - - userAgent.append(SPACE) - .append(HTTP) - .append("/") - .append(SdkHttpUtils.urlEncode(clientName(clientType, syncHttpClient, asyncHttpClient))) - .append(SPACE) - .append(CONFIG) - .append("/") - .append(RETRY_MODE) - .append("/") - .append(StringUtils.lowerCase(retryMode)); - - return userAgent.toString(); - } - @Override - public SdkHttpFullRequest.Builder execute(SdkHttpFullRequest.Builder request, RequestExecutionContext context) - throws Exception { - return request.putHeader(HEADER_USER_AGENT, getUserAgent(clientConfig, context)); + public SdkHttpFullRequest.Builder execute(SdkHttpFullRequest.Builder request, + RequestExecutionContext context) throws Exception { + String headerValue = finalizeUserAgent(context); + return request.putHeader(HEADER_USER_AGENT, headerValue); } - private String getUserAgent(SdkClientConfiguration config, RequestExecutionContext context) { + /** + * The final value sent in the user agent header consists of + *
    + *
  1. an optional user provided prefix
  2. + *
  3. SDK user agent values (governed by a common specification)
  4. + *
  5. an optional set of API names, expressed as name/version pairs
  6. + *
  7. an optional user provided suffix
  8. + *
+ *

+ * In general, usage of the optional values is discouraged since they do not follow a specification and can make + * the user agent too long. + *

+ * The SDK user agent values are constructed from static system values, client level values and request level + * values. This method adds request level values directly after the retrieved SDK client user agent string. + */ + private String finalizeUserAgent(RequestExecutionContext context) { String clientUserAgent = clientConfig.option(SdkClientOption.CLIENT_USER_AGENT); if (clientUserAgent == null) { log.warn(() -> "Client user agent configuration is missing, so request user agent will be incomplete."); clientUserAgent = ""; } - StringBuilder userAgent = new StringBuilder(clientUserAgent); - //additional cfg information - identityProviderName(context.executionAttributes()) - .ifPresent(providerName -> userAgent.append(SPACE).append(AUTH_CONFIG.apply(providerName))); + StringBuilder javaUserAgent = new StringBuilder(); + + String userPrefix = trim(clientConfig.option(SdkAdvancedClientOption.USER_AGENT_PREFIX)); + if (!StringUtils.isEmpty(userPrefix)) { + javaUserAgent.append(userPrefix).append(SPACE); + } + + javaUserAgent.append(clientUserAgent); + + //add remaining SDK user agent properties + identityProviderName(context.executionAttributes()).ifPresent( + authSource -> appendSpaceAndField(javaUserAgent, CONFIG_METADATA, uaPair(AUTH_SOURCE, authSource))); - //request API names - requestApiNames(context.requestConfig().apiNames()).ifPresent(userAgent::append); + //treat ApiNames as an opaque set of values because it may contain user values + Optional apiNames = requestApiNames(context.requestConfig().apiNames()); + apiNames.ifPresent(javaUserAgent::append); - //suffix - String userDefinedSuffix = config.option(SdkAdvancedClientOption.USER_AGENT_SUFFIX); - if (!StringUtils.isEmpty(userDefinedSuffix)) { - userAgent.append(COMMA).append(userDefinedSuffix.trim()); + String userSuffix = trim(clientConfig.option(SdkAdvancedClientOption.USER_AGENT_SUFFIX)); + if (!StringUtils.isEmpty(userSuffix)) { + javaUserAgent.append(SPACE).append(userSuffix); } - return userAgent.toString(); + return javaUserAgent.toString(); } private static Optional identityProviderName(ExecutionAttributes executionAttributes) { @@ -170,26 +130,24 @@ private static Optional providerNameFromIdentity(Se return identity.providerName().flatMap(IdentityProviderNameMapping::mapFrom); } + /** + * This structure is used for external users as well as for internal tracking of features. + * It's not governed by a specification. + * Internal usage should be migrated to business metrics or another designated metadata field, + * leaving these values to be completely user-set, in which case the result would in most cases be empty. + *

+ * Currently tracking these SDK values (remove from list as they're migrated): + * PAGINATED/sdk-version, hll/s3Multipart, hll/ddb-enh, hll/cw-mp, hll/waiter, hll/cross-region, ft/s3-transfer + */ private Optional requestApiNames(List requestApiNames) { if (requestApiNames.isEmpty()) { return Optional.empty(); } StringBuilder concatenatedNames = new StringBuilder(); requestApiNames.forEach(apiName -> concatenatedNames.append(SPACE) - .append(API_NAMES.apply(apiName.name(), - apiName.version()))); + .append(apiName.name()) + .append(SLASH) + .append(apiName.version())); return Optional.of(concatenatedNames.toString()); } - - private static String clientName(ClientType clientType, SdkHttpClient syncHttpClient, SdkAsyncHttpClient asyncHttpClient) { - if (clientType == ClientType.SYNC) { - return syncHttpClient == null ? "null" : syncHttpClient.clientName(); - } - - if (clientType == ClientType.ASYNC) { - return asyncHttpClient == null ? "null" : asyncHttpClient.clientName(); - } - - return ClientType.UNKNOWN.name(); - } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/DefaultSystemUserAgent.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/DefaultSystemUserAgent.java new file mode 100644 index 000000000000..cac3777ed80b --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/DefaultSystemUserAgent.java @@ -0,0 +1,146 @@ +/* + * Copyright 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.core.internal.useragent; + +import static software.amazon.awssdk.core.internal.useragent.SdkUserAgentBuilder.buildSystemUserAgentString; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.sanitizeInput; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.uaPair; +import static software.amazon.awssdk.core.internal.useragent.UserAgentLangValues.getAdditionalJvmLanguages; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import software.amazon.awssdk.annotations.SdkProtectedApi; +import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.core.util.SystemUserAgent; +import software.amazon.awssdk.core.util.VersionInfo; +import software.amazon.awssdk.utils.JavaSystemSetting; +import software.amazon.awssdk.utils.SystemSetting; + +/** + * Common system level user agent properties that can either be accessed as a string or as individual values. + * The former is useful when making generic calls, for instance to local endpoints when resolving identity, while + * the latter is when incorporating this information into a user agent header in an SDK request. + */ +@ThreadSafe +@SdkProtectedApi +public final class DefaultSystemUserAgent implements SystemUserAgent { + + private static volatile DefaultSystemUserAgent instance; + + private final String sdkVersion; + private final String osMetadata; + private final String langMetadata; + private final String envMetadata; + private final String vmMetadata; + private final String vendorMetadata; + private final Optional languageTagMetadata; + private final List additionalJvmLanguages; + private final String systemUserAgent; + + private DefaultSystemUserAgent() { + sdkVersion = VersionInfo.SDK_VERSION; + osMetadata = uaPair(systemSetting(JavaSystemSetting.OS_NAME), systemSetting(JavaSystemSetting.OS_VERSION)); + langMetadata = uaPair("java", systemSetting(JavaSystemSetting.JAVA_VERSION)); + envMetadata = systemSetting(SdkSystemSetting.AWS_EXECUTION_ENV); + vmMetadata = uaPair(systemSetting(JavaSystemSetting.JAVA_VM_NAME), systemSetting(JavaSystemSetting.JAVA_VM_VERSION)); + vendorMetadata = uaPair("vendor", systemSetting(JavaSystemSetting.JAVA_VENDOR)); + languageTagMetadata = getLanguageTagMetadata(); + additionalJvmLanguages = getAdditionalJvmLanguages(); + systemUserAgent = getUserAgent(); + } + + public static DefaultSystemUserAgent getOrCreate() { + if (instance == null) { + synchronized (DefaultSystemUserAgent.class) { + if (instance == null) { + instance = new DefaultSystemUserAgent(); + } + } + } + + return instance; + } + + /** + * A generic user agent string to be used when communicating with backend services. + * This string contains Java, OS and region information but does not contain client and request + * specific values. + */ + @Override + public String userAgentString() { + return systemUserAgent; + } + + @Override + public String sdkVersion() { + return sdkVersion; + } + + @Override + public String osMetadata() { + return osMetadata; + } + + @Override + public String langMetadata() { + return langMetadata; + } + + @Override + public String envMetadata() { + return envMetadata; + } + + @Override + public String vmMetadata() { + return vmMetadata; + } + + @Override + public String vendorMetadata() { + return vendorMetadata; + } + + @Override + public Optional languageTagMetadata() { + return languageTagMetadata; + } + + @Override + public List additionalJvmLanguages() { + return Collections.unmodifiableList(additionalJvmLanguages); + } + + private String getUserAgent() { + return buildSystemUserAgentString(this); + } + + private String systemSetting(SystemSetting systemSetting) { + return sanitizeInput(systemSetting.getStringValue().orElse(null)); + } + + private Optional getLanguageTagMetadata() { + Optional language = JavaSystemSetting.USER_LANGUAGE.getStringValue(); + Optional country = JavaSystemSetting.USER_COUNTRY.getStringValue(); + String value = null; + if (language.isPresent() && country.isPresent()) { + value = sanitizeInput(language.get()) + "_" + sanitizeInput(country.get()); + } + return Optional.ofNullable(value); + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/SdkClientUserAgentProperties.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/SdkClientUserAgentProperties.java new file mode 100644 index 000000000000..95b2739ff201 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/SdkClientUserAgentProperties.java @@ -0,0 +1,43 @@ +/* + * Copyright 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.core.internal.useragent; + +import java.util.HashMap; +import java.util.Map; +import software.amazon.awssdk.annotations.NotThreadSafe; +import software.amazon.awssdk.annotations.SdkProtectedApi; + +/** + * Represents AWS SDK user agent client values. + */ +@NotThreadSafe +@SdkProtectedApi +public final class SdkClientUserAgentProperties { + + private final Map properties; + + public SdkClientUserAgentProperties() { + this.properties = new HashMap<>(32); + } + + public String getProperty(String property) { + return properties.get(property); + } + + public void putProperty(String property, String value) { + properties.put(property, value); + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/SdkUserAgentBuilder.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/SdkUserAgentBuilder.java new file mode 100644 index 000000000000..213653cbd307 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/SdkUserAgentBuilder.java @@ -0,0 +1,127 @@ +/* + * Copyright 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.core.internal.useragent; + +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.CONFIG_METADATA; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.ENV_METADATA; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.HTTP; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.INTERNAL_METADATA_MARKER; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.IO; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.JAVA_SDK_METADATA; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.LANG_METADATA; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.METADATA; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.OS_METADATA; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.RETRY_MODE; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.UA_METADATA; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.appendFieldAndSpace; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.appendNonEmptyField; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.uaPair; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.uaPairOrNull; + +import software.amazon.awssdk.annotations.SdkProtectedApi; +import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.core.util.SystemUserAgent; +import software.amazon.awssdk.utils.StringUtils; + +/** + * Responsible for building user agent strings for different use cases. + */ +@ThreadSafe +@SdkProtectedApi +public final class SdkUserAgentBuilder { + + private SdkUserAgentBuilder() { + } + + /** + * Constructs a string representation of an SDK client user agent string, based on system and client data. + * Note that request level values must be added separately. + */ + public static String buildClientUserAgentString(SystemUserAgent systemValues, + SdkClientUserAgentProperties userAgentProperties) { + StringBuilder uaString = new StringBuilder(255); + + appendNonEmptyField(uaString, JAVA_SDK_METADATA, systemValues.sdkVersion()); + appendAdditionalSdkMetadata(uaString, userAgentProperties); + + String internalMarkerValue = userAgentProperties.getProperty(INTERNAL_METADATA_MARKER); + if (!StringUtils.isEmpty(internalMarkerValue)) { + appendFieldAndSpace(uaString, METADATA, INTERNAL_METADATA_MARKER); + } + + appendNonEmptyField(uaString, UA_METADATA, "2.0"); + appendNonEmptyField(uaString, OS_METADATA, systemValues.osMetadata()); + appendNonEmptyField(uaString, LANG_METADATA, systemValues.langMetadata()); + appendAdditionalJvmMetadata(uaString, systemValues); + + String envMetadata = systemValues.envMetadata(); + if (!isEmptyOrUnknown(envMetadata)) { + appendFieldAndSpace(uaString, ENV_METADATA, envMetadata); + } + + String retryMode = userAgentProperties.getProperty(RETRY_MODE); + if (!StringUtils.isEmpty(retryMode)) { + appendFieldAndSpace(uaString, CONFIG_METADATA, uaPair(RETRY_MODE, retryMode)); + } + + removeFinalWhitespace(uaString); + return uaString.toString(); + } + + /** + * Constructs a string representation of system user agent values only, that can be used for any backend calls. + */ + public static String buildSystemUserAgentString(SystemUserAgent systemValues) { + StringBuilder uaString = new StringBuilder(128); + + appendNonEmptyField(uaString, JAVA_SDK_METADATA, systemValues.sdkVersion()); + appendNonEmptyField(uaString, OS_METADATA, systemValues.osMetadata()); + appendNonEmptyField(uaString, LANG_METADATA, systemValues.langMetadata()); + appendAdditionalJvmMetadata(uaString, systemValues); + + String envMetadata = systemValues.envMetadata(); + if (!isEmptyOrUnknown(envMetadata)) { + appendFieldAndSpace(uaString, ENV_METADATA, systemValues.envMetadata()); + } + + removeFinalWhitespace(uaString); + return uaString.toString(); + } + + private static void removeFinalWhitespace(StringBuilder builder) { + if (builder.length() > 0 && builder.charAt(builder.length() - 1) == ' ') { + builder.deleteCharAt(builder.length() - 1); + } + } + + private static boolean isEmptyOrUnknown(String envMetadata) { + return StringUtils.isEmpty(envMetadata) || envMetadata.equalsIgnoreCase("unknown"); + } + + private static void appendAdditionalSdkMetadata(StringBuilder builder, SdkClientUserAgentProperties userAgentProperties) { + appendNonEmptyField(builder, METADATA, uaPairOrNull(IO, userAgentProperties.getProperty(IO))); + appendNonEmptyField(builder, METADATA, uaPairOrNull(HTTP, userAgentProperties.getProperty(HTTP))); + } + + private static void appendAdditionalJvmMetadata(StringBuilder builder, SystemUserAgent systemProperties) { + appendNonEmptyField(builder, METADATA, systemProperties.vmMetadata()); + appendNonEmptyField(builder, METADATA, systemProperties.vendorMetadata()); + systemProperties.languageTagMetadata().ifPresent(value -> appendFieldAndSpace(builder, METADATA, value)); + for (String lang : systemProperties.additionalJvmLanguages()) { + appendNonEmptyField(builder, METADATA, lang); + } + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/UserAgentConstant.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/UserAgentConstant.java new file mode 100644 index 000000000000..179f54df965b --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/UserAgentConstant.java @@ -0,0 +1,125 @@ +/* + * Copyright 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.core.internal.useragent; + +import static software.amazon.awssdk.utils.StringUtils.trim; + +import java.util.regex.Pattern; +import software.amazon.awssdk.annotations.SdkProtectedApi; +import software.amazon.awssdk.utils.StringUtils; + +@SdkProtectedApi +public final class UserAgentConstant { + + //Known SDK metadata tags/names + public static final String API_METADATA = "api"; + public static final String OS_METADATA = "os"; + public static final String LANG_METADATA = "lang"; + public static final String UA_METADATA = "ua"; + public static final String ENV_METADATA = "exec-env"; + public static final String JAVA_SDK_METADATA = "aws-sdk-java"; + public static final String FEATURE_METADATA = "ft"; + public static final String CONFIG_METADATA = "cfg"; + public static final String FRAMEWORK_METADATA = "lib"; + public static final String METADATA = "md"; + public static final String INTERNAL_METADATA_MARKER = "internal"; + + //Separators used in SDK user agent + public static final String SLASH = "/"; + public static final String HASH = "#"; + public static final String SPACE = " "; + + //Java user agent tags/names + public static final String IO = "io"; + public static final String HTTP = "http"; + public static final String RETRY_MODE = "retry-mode"; + public static final String AUTH_SOURCE = "auth-source"; + + /** Disallowed characters in the user agent token: @see RFC 7230 */ + private static final String UA_DENYLIST_REGEX = "[() ,/:;<=>?@\\[\\]{}\\\\]"; + private static final Pattern UA_DENYLIST_PATTERN = Pattern.compile(UA_DENYLIST_REGEX); + private static final String UNKNOWN = "unknown"; + + private UserAgentConstant() { + } + + /** + * According to specifications, the SDK user agent consists of metadata fields separated by RWS characters - in + * this implementation, space. + * Each field is represented by the name of the field, as specified and the contents of the field, separated + * with a '/' (SLASH). Contents can be a single token, a specified value or a uaPair. + */ + public static String field(String name, String value) { + return concat(name, trim(value), SLASH); + } + + /** + * According to specifications, an SDK user agent pair is a name, value pair concatenated with a '#' (HASH). + */ + public static String uaPair(String name, String value) { + return concat(name, value, HASH); + } + + /** + * According to specifications, an SDK user agent pair is a name, value pair concatenated with a '#' (HASH). + */ + public static String uaPairOrNull(String name, String value) { + return value != null ? uaPair(name, value) : null; + } + + /** + * Add a metadata field to the string builder, followed by space. If 'value' can be empty, use + * {@link #appendNonEmptyField(StringBuilder, String, String)} instead. + */ + public static void appendFieldAndSpace(StringBuilder builder, String name, String value) { + builder.append(name).append(SLASH).append(value); + builder.append(SPACE); + } + + /** + * Add a metadata field to the string builder, preceded by space. If 'value' can be empty, use + * {@link #appendNonEmptyField(StringBuilder, String, String)} instead. + */ + public static void appendSpaceAndField(StringBuilder builder, String name, String value) { + builder.append(SPACE); + builder.append(name).append(SLASH).append(value); + } + + /** + * Add a metadata field to the string builder only if 'value' is non-empty. + * Also see {@link #appendFieldAndSpace(StringBuilder, String, String)} + */ + public static void appendNonEmptyField(StringBuilder builder, String name, String value) { + if (!StringUtils.isEmpty(value)) { + appendFieldAndSpace(builder, name, value); + } + } + + /** + * Replace any spaces, parentheses in the input with underscores. + */ + public static String sanitizeInput(String input) { + return input == null ? UNKNOWN : UA_DENYLIST_PATTERN.matcher(input).replaceAll("_"); + } + + /** + * Concatenates two values with the specified separator, if the second value is not null/empty, otherwise + * returns the first value. + */ + public static String concat(String prefix, String suffix, String separator) { + return suffix != null && !suffix.isEmpty() ? prefix + separator + suffix : prefix; + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/UserAgentLangValues.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/UserAgentLangValues.java new file mode 100644 index 000000000000..8082510b80a1 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/UserAgentLangValues.java @@ -0,0 +1,91 @@ +/* + * Copyright 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.core.internal.useragent; + +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.concat; + +import java.util.Arrays; +import java.util.List; +import java.util.jar.JarInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.utils.IoUtils; + +@SdkInternalApi +public final class UserAgentLangValues { + + private static final Logger log = LoggerFactory.getLogger(UserAgentLangValues.class); + + private UserAgentLangValues() { + } + + public static List getAdditionalJvmLanguages() { + return Arrays.asList(scalaVersion(), kotlinVersion()); + } + + /** + * Attempt to determine if Scala is on the classpath and if so what version is in use. + * Does this by looking for a known Scala class (scala.util.Properties) and then calling + * a static method on that class via reflection to determine the versionNumberString. + * + * @return Scala version if any, else empty string + */ + public static String scalaVersion() { + String scalaVersion = ""; + try { + Class scalaProperties = Class.forName("scala.util.Properties"); + scalaVersion = "scala"; + String version = (String) scalaProperties.getMethod("versionNumberString").invoke(null); + scalaVersion = concat(scalaVersion, version, "/"); + } catch (ClassNotFoundException e) { + //Ignore + } catch (Exception e) { + if (log.isTraceEnabled()) { + log.trace("Exception attempting to get Scala version.", e); + } + } + return scalaVersion; + } + + /** + * Attempt to determine if Kotlin is on the classpath and if so what version is in use. + * Does this by looking for a known Kotlin class (kotlin.Unit) and then loading the Manifest + * from that class' JAR to determine the Kotlin version. + * + * @return Kotlin version if any, else empty string + */ + public static String kotlinVersion() { + String kotlinVersion = ""; + JarInputStream kotlinJar = null; + try { + Class kotlinUnit = Class.forName("kotlin.Unit"); + kotlinVersion = "kotlin"; + kotlinJar = new JarInputStream(kotlinUnit.getProtectionDomain().getCodeSource().getLocation().openStream()); + String version = kotlinJar.getManifest().getMainAttributes().getValue("Implementation-Version"); + kotlinVersion = concat(kotlinVersion, version, "/"); + } catch (ClassNotFoundException e) { + //Ignore + } catch (Exception e) { + if (log.isTraceEnabled()) { + log.trace("Exception attempting to get Kotlin version.", e); + } + } finally { + IoUtils.closeQuietly(kotlinJar, log); + } + return kotlinVersion; + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/util/SdkUserAgent.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/util/SdkUserAgent.java index bbaa7dd22531..602c4d94c6fe 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/util/SdkUserAgent.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/util/SdkUserAgent.java @@ -15,50 +15,20 @@ package software.amazon.awssdk.core.util; -import java.util.Optional; -import java.util.jar.JarInputStream; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import software.amazon.awssdk.annotations.SdkProtectedApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; import software.amazon.awssdk.annotations.ThreadSafe; -import software.amazon.awssdk.utils.IoUtils; -import software.amazon.awssdk.utils.JavaSystemSetting; -import software.amazon.awssdk.utils.StringUtils; /** - * Utility class for accessing AWS SDK versioning information. + * Utility class for accessing AWS SDK versioning information. Deprecated in favor of {@link SystemUserAgent}. */ @ThreadSafe @SdkProtectedApi +@Deprecated public final class SdkUserAgent { - private static final String UA_STRING = "aws-sdk-{platform}/{version} {os.name}/{os.version} {java.vm.name}/{java.vm" - + ".version} Java/{java.version}{language.and.region}{additional.languages} " - + "vendor/{java.vendor}"; - - /** Disallowed characters in the user agent token: @see RFC 7230 */ - private static final String UA_DENYLIST_REGEX = "[() ,/:;<=>?@\\[\\]{}\\\\]"; - - /** Shared logger for any issues while loading version information. */ - private static final Logger log = LoggerFactory.getLogger(SdkUserAgent.class); - private static final String UNKNOWN = "unknown"; private static volatile SdkUserAgent instance; - private static final String[] USER_AGENT_SEARCH = { - "{platform}", - "{version}", - "{os.name}", - "{os.version}", - "{java.vm.name}", - "{java.vm.version}", - "{java.version}", - "{java.vendor}", - "{additional.languages}", - "{language.and.region}" - }; - - /** User Agent info. */ private String userAgent; private SdkUserAgent() { @@ -97,94 +67,6 @@ private void initializeUserAgent() { @SdkTestInternalApi String getUserAgent() { - Optional language = JavaSystemSetting.USER_LANGUAGE.getStringValue(); - Optional region = JavaSystemSetting.USER_REGION.getStringValue(); - String languageAndRegion = ""; - if (language.isPresent() && region.isPresent()) { - languageAndRegion = " (" + sanitizeInput(language.get()) + "_" + sanitizeInput(region.get()) + ")"; - } - - return StringUtils.replaceEach(UA_STRING, USER_AGENT_SEARCH, new String[] { - "java", - VersionInfo.SDK_VERSION, - sanitizeInput(JavaSystemSetting.OS_NAME.getStringValue().orElse(null)), - sanitizeInput(JavaSystemSetting.OS_VERSION.getStringValue().orElse(null)), - sanitizeInput(JavaSystemSetting.JAVA_VM_NAME.getStringValue().orElse(null)), - sanitizeInput(JavaSystemSetting.JAVA_VM_VERSION.getStringValue().orElse(null)), - sanitizeInput(JavaSystemSetting.JAVA_VERSION.getStringValue().orElse(null)), - sanitizeInput(JavaSystemSetting.JAVA_VENDOR.getStringValue().orElse(null)), - getAdditionalJvmLanguages(), - languageAndRegion, - }); - } - - /** - * Replace any spaces, parentheses in the input with underscores. - * - * @param input the input - * @return the input with spaces replaced by underscores - */ - private static String sanitizeInput(String input) { - return input == null ? UNKNOWN : input.replaceAll(UA_DENYLIST_REGEX, "_"); - } - - private static String getAdditionalJvmLanguages() { - return concat(concat("", scalaVersion(), " "), kotlinVersion(), " "); - } - - /** - * Attempt to determine if Scala is on the classpath and if so what version is in use. - * Does this by looking for a known Scala class (scala.util.Properties) and then calling - * a static method on that class via reflection to determine the versionNumberString. - * - * @return Scala version if any, else empty string - */ - private static String scalaVersion() { - String scalaVersion = ""; - try { - Class scalaProperties = Class.forName("scala.util.Properties"); - scalaVersion = "scala"; - String version = (String) scalaProperties.getMethod("versionNumberString").invoke(null); - scalaVersion = concat(scalaVersion, version, "/"); - } catch (ClassNotFoundException e) { - //Ignore - } catch (Exception e) { - if (log.isTraceEnabled()) { - log.trace("Exception attempting to get Scala version.", e); - } - } - return scalaVersion; - } - - /** - * Attempt to determine if Kotlin is on the classpath and if so what version is in use. - * Does this by looking for a known Kotlin class (kotlin.Unit) and then loading the Manifest - * from that class' JAR to determine the Kotlin version. - * - * @return Kotlin version if any, else empty string - */ - private static String kotlinVersion() { - String kotlinVersion = ""; - JarInputStream kotlinJar = null; - try { - Class kotlinUnit = Class.forName("kotlin.Unit"); - kotlinVersion = "kotlin"; - kotlinJar = new JarInputStream(kotlinUnit.getProtectionDomain().getCodeSource().getLocation().openStream()); - String version = kotlinJar.getManifest().getMainAttributes().getValue("Implementation-Version"); - kotlinVersion = concat(kotlinVersion, version, "/"); - } catch (ClassNotFoundException e) { - //Ignore - } catch (Exception e) { - if (log.isTraceEnabled()) { - log.trace("Exception attempting to get Kotlin version.", e); - } - } finally { - IoUtils.closeQuietly(kotlinJar, log); - } - return kotlinVersion; - } - - private static String concat(String prefix, String suffix, String separator) { - return suffix != null && !suffix.isEmpty() ? prefix + separator + suffix : prefix; + return SystemUserAgent.getOrCreate().userAgentString(); } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/util/SystemUserAgent.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/util/SystemUserAgent.java new file mode 100644 index 000000000000..7425ecb23406 --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/util/SystemUserAgent.java @@ -0,0 +1,59 @@ +/* + * Copyright 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.core.util; + +import java.util.List; +import java.util.Optional; +import software.amazon.awssdk.annotations.SdkProtectedApi; +import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.core.internal.useragent.DefaultSystemUserAgent; + +/** + * Common system level user agent properties that can either be accessed as a string or as individual values. + * The former is useful when making generic calls, for instance to local endpoints when resolving identity, while + * the latter is when incorporating this information into a user agent header in an SDK request. + */ +@ThreadSafe +@SdkProtectedApi +public interface SystemUserAgent { + + static SystemUserAgent getOrCreate() { + return DefaultSystemUserAgent.getOrCreate(); + } + + /** + * A generic user agent string to be used when communicating with backend services. + * This string contains Java, OS and region information but does not contain client and request + * specific values. + */ + String userAgentString(); + + String sdkVersion(); + + String osMetadata(); + + String langMetadata(); + + String envMetadata(); + + String vmMetadata(); + + String vendorMetadata(); + + Optional languageTagMetadata(); + + List additionalJvmLanguages(); +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/http/AmazonHttpClientTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/http/AmazonHttpClientTest.java index 859a2ac4eaca..c307a2f9ba4f 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/http/AmazonHttpClientTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/http/AmazonHttpClientTest.java @@ -20,10 +20,12 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.HTTP; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.IO; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.RETRY_MODE; import static software.amazon.awssdk.core.internal.util.ResponseHandlerTestUtils.combinedSyncResponseHandler; import java.io.IOException; -import java.net.URI; import java.util.concurrent.ExecutorService; import org.junit.Assert; @@ -41,15 +43,18 @@ import software.amazon.awssdk.core.client.config.SdkClientOption; import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.core.internal.http.AmazonSyncHttpClient; -import software.amazon.awssdk.core.internal.http.pipeline.stages.ApplyUserAgentStage; import software.amazon.awssdk.core.internal.http.timers.ClientExecutionAndRequestTimerTestUtils; +import software.amazon.awssdk.core.internal.useragent.SdkClientUserAgentProperties; +import software.amazon.awssdk.core.internal.useragent.SdkUserAgentBuilder; import software.amazon.awssdk.core.retry.RetryMode; -import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.core.util.SystemUserAgent; import software.amazon.awssdk.http.ExecutableHttpRequest; import software.amazon.awssdk.http.HttpExecuteRequest; import software.amazon.awssdk.http.HttpExecuteResponse; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.utils.StringUtils; +import software.amazon.awssdk.utils.http.SdkHttpUtils; import utils.HttpTestUtils; import utils.ValidSdkObjects; @@ -81,13 +86,11 @@ public void testRetryIoExceptionFromExecute() throws Exception { when(abortableCallable.call()).thenThrow(ioException); - ExecutionContext context = ClientExecutionAndRequestTimerTestUtils.executionContext(null); - try { client.requestExecutionBuilder() .request(ValidSdkObjects.sdkHttpFullRequest().build()) .originalRequest(NoopTestRequest.builder().build()) - .executionContext(context) + .executionContext(executionContext()) .execute(combinedSyncResponseHandler(null, null)); Assert.fail("No exception when request repeatedly fails!"); @@ -101,18 +104,16 @@ public void testRetryIoExceptionFromExecute() throws Exception { @Test public void testRetryIoExceptionFromHandler() throws Exception { - final IOException exception = new IOException("BOOM"); + IOException exception = new IOException("BOOM"); HttpResponseHandler mockHandler = mock(HttpResponseHandler.class); when(mockHandler.handle(any(), any())).thenThrow(exception); - ExecutionContext context = ClientExecutionAndRequestTimerTestUtils.executionContext(null); - try { client.requestExecutionBuilder() .request(ValidSdkObjects.sdkHttpFullRequest().build()) .originalRequest(NoopTestRequest.builder().build()) - .executionContext(context) + .executionContext(executionContext()) .execute(combinedSyncResponseHandler(mockHandler, null)); Assert.fail("No exception when request repeatedly fails!"); @@ -132,13 +133,17 @@ public void testUserAgentPrefixAndSuffixAreAdded() { HttpResponseHandler handler = mock(HttpResponseHandler.class); - String clientUserAgent = - ApplyUserAgentStage.resolveClientUserAgent(prefix, "", ClientType.SYNC, sdkHttpClient, null, - RetryMode.STANDARD.toString()); + SdkClientUserAgentProperties userAgentProperties = new SdkClientUserAgentProperties(); + userAgentProperties.putProperty(RETRY_MODE, RetryMode.STANDARD.toString()); + userAgentProperties.putProperty(IO, ClientType.SYNC.name()); + userAgentProperties.putProperty(HTTP, SdkHttpUtils.urlEncode(sdkHttpClient.clientName())); + String clientUserAgent = SdkUserAgentBuilder.buildClientUserAgentString(SystemUserAgent.getOrCreate(), + userAgentProperties); SdkClientConfiguration config = HttpTestUtils.testClientConfiguration().toBuilder() - .option(SdkAdvancedClientOption.USER_AGENT_SUFFIX, suffix) .option(SdkClientOption.CLIENT_USER_AGENT, clientUserAgent) + .option(SdkAdvancedClientOption.USER_AGENT_PREFIX, prefix) + .option(SdkAdvancedClientOption.USER_AGENT_SUFFIX, suffix) .option(SdkClientOption.SYNC_HTTP_CLIENT, sdkHttpClient) .build(); AmazonSyncHttpClient client = new AmazonSyncHttpClient(config); @@ -146,14 +151,14 @@ public void testUserAgentPrefixAndSuffixAreAdded() { client.requestExecutionBuilder() .request(ValidSdkObjects.sdkHttpFullRequest().build()) .originalRequest(NoopTestRequest.builder().build()) - .executionContext(ClientExecutionAndRequestTimerTestUtils.executionContext(null)) + .executionContext(executionContext()) .execute(combinedSyncResponseHandler(handler, null)); ArgumentCaptor httpRequestCaptor = ArgumentCaptor.forClass(HttpExecuteRequest.class); verify(sdkHttpClient).prepareRequest(httpRequestCaptor.capture()); - final String userAgent = httpRequestCaptor.getValue().httpRequest().firstMatchingHeader("User-Agent") - .orElseThrow(() -> new AssertionError("User-Agent header was not found")); + String userAgent = httpRequestCaptor.getValue().httpRequest().firstMatchingHeader("User-Agent") + .orElseThrow(() -> new AssertionError("User-Agent header was not found")); Assert.assertTrue(userAgent.startsWith(prefix)); Assert.assertTrue(userAgent.endsWith(suffix)); @@ -163,39 +168,43 @@ public void testUserAgentPrefixAndSuffixAreAdded() { public void testUserAgentContainsHttpClientInfo() { HttpResponseHandler handler = mock(HttpResponseHandler.class); - String clientUserAgent = - ApplyUserAgentStage.resolveClientUserAgent(null, null, ClientType.SYNC, sdkHttpClient, null, - RetryMode.STANDARD.toString()); + SdkClientUserAgentProperties userAgentProperties = new SdkClientUserAgentProperties(); + userAgentProperties.putProperty(IO, StringUtils.lowerCase(ClientType.SYNC.name())); + userAgentProperties.putProperty(HTTP, SdkHttpUtils.urlEncode(sdkHttpClient.clientName())); + String clientUserAgent = SdkUserAgentBuilder.buildClientUserAgentString(SystemUserAgent.getOrCreate(), + userAgentProperties); + SdkClientConfiguration config = HttpTestUtils.testClientConfiguration().toBuilder() + .option(SdkClientOption.CLIENT_USER_AGENT, clientUserAgent) .option(SdkClientOption.SYNC_HTTP_CLIENT, sdkHttpClient) .option(SdkClientOption.CLIENT_TYPE, ClientType.SYNC) - .option(SdkClientOption.CLIENT_USER_AGENT, clientUserAgent) .build(); AmazonSyncHttpClient client = new AmazonSyncHttpClient(config); client.requestExecutionBuilder() .request(ValidSdkObjects.sdkHttpFullRequest().build()) .originalRequest(NoopTestRequest.builder().build()) - .executionContext(ClientExecutionAndRequestTimerTestUtils.executionContext(null)) + .executionContext(executionContext()) .execute(combinedSyncResponseHandler(handler, null)); ArgumentCaptor httpRequestCaptor = ArgumentCaptor.forClass(HttpExecuteRequest.class); verify(sdkHttpClient).prepareRequest(httpRequestCaptor.capture()); - final String userAgent = httpRequestCaptor.getValue().httpRequest().firstMatchingHeader("User-Agent") - .orElseThrow(() -> new AssertionError("User-Agent header was not found")); + String userAgent = httpRequestCaptor.getValue().httpRequest().firstMatchingHeader("User-Agent") + .orElseThrow(() -> new AssertionError("User-Agent header was not found")); - Assert.assertTrue(userAgent.contains("io/sync")); - Assert.assertTrue(userAgent.contains("http/UNKNOWN")); + Assert.assertTrue(userAgent.contains("io#sync")); + Assert.assertTrue(userAgent.contains("http#UNKNOWN")); } @Test public void testUserAgentContainsRetryModeInfo() { HttpResponseHandler handler = mock(HttpResponseHandler.class); - String clientUserAgent = - ApplyUserAgentStage.resolveClientUserAgent(null, null, ClientType.SYNC, sdkHttpClient, null, - RetryMode.STANDARD.toString()); + SdkClientUserAgentProperties userAgentProperties = new SdkClientUserAgentProperties(); + userAgentProperties.putProperty(RETRY_MODE, RetryMode.STANDARD.toString().toLowerCase()); + String clientUserAgent = SdkUserAgentBuilder.buildClientUserAgentString(SystemUserAgent.getOrCreate(), + userAgentProperties); SdkClientConfiguration config = HttpTestUtils.testClientConfiguration().toBuilder() .option(SdkClientOption.CLIENT_USER_AGENT, clientUserAgent) @@ -206,16 +215,16 @@ public void testUserAgentContainsRetryModeInfo() { client.requestExecutionBuilder() .request(ValidSdkObjects.sdkHttpFullRequest().build()) .originalRequest(NoopTestRequest.builder().build()) - .executionContext(ClientExecutionAndRequestTimerTestUtils.executionContext(null)) + .executionContext(executionContext()) .execute(combinedSyncResponseHandler(handler, null)); ArgumentCaptor httpRequestCaptor = ArgumentCaptor.forClass(HttpExecuteRequest.class); verify(sdkHttpClient).prepareRequest(httpRequestCaptor.capture()); - final String userAgent = httpRequestCaptor.getValue().httpRequest().firstMatchingHeader("User-Agent") - .orElseThrow(() -> new AssertionError("User-Agent header was not found")); + String userAgent = httpRequestCaptor.getValue().httpRequest().firstMatchingHeader("User-Agent") + .orElseThrow(() -> new AssertionError("User-Agent header was not found")); - Assert.assertTrue(userAgent.contains("cfg/retry-mode/standard")); + Assert.assertTrue(userAgent.contains("cfg/retry-mode#standard")); } @Test @@ -232,6 +241,10 @@ public void closeClient_shouldCloseDependencies() { verify(executor).shutdown(); } + private ExecutionContext executionContext() { + return ClientExecutionAndRequestTimerTestUtils.executionContext(null); + } + private void stubSuccessfulResponse() throws Exception { when(abortableCallable.call()).thenReturn(HttpExecuteResponse.builder().response(SdkHttpResponse.builder() .statusCode(200) diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApplyUserAgentStageTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApplyUserAgentStageTest.java index 50b72aa58031..0eaee0e9500d 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApplyUserAgentStageTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApplyUserAgentStageTest.java @@ -16,7 +16,13 @@ package software.amazon.awssdk.core.internal.http.pipeline.stages; import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.core.client.config.SdkAdvancedClientOption.USER_AGENT_PREFIX; +import static software.amazon.awssdk.core.client.config.SdkAdvancedClientOption.USER_AGENT_SUFFIX; import static software.amazon.awssdk.core.internal.http.pipeline.stages.ApplyUserAgentStage.HEADER_USER_AGENT; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.HTTP; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.IO; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.RETRY_MODE; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.SPACE; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -35,6 +41,9 @@ import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute; import software.amazon.awssdk.core.internal.http.HttpClientDependencies; import software.amazon.awssdk.core.internal.http.RequestExecutionContext; +import software.amazon.awssdk.core.internal.useragent.SdkClientUserAgentProperties; +import software.amazon.awssdk.core.internal.useragent.SdkUserAgentBuilder; +import software.amazon.awssdk.core.util.SystemUserAgent; import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.http.auth.spi.scheme.AuthSchemeOption; import software.amazon.awssdk.http.auth.spi.signer.HttpSigner; @@ -49,7 +58,6 @@ public class ApplyUserAgentStageTest { (HttpSigner) Mockito.mock(HttpSigner.class), AuthSchemeOption.builder().schemeId("mock").build()); - private static final String SDK_UA_STRING = "aws-sdk-java/version vendor/unknown"; private static final String PROVIDER_SOURCE = "ProcessCredentialsProvider"; private static final AwsCredentialsIdentity IDENTITY_WITHOUT_SOURCE = AwsCredentialsIdentity.create("akid", "secret"); @@ -59,38 +67,49 @@ public class ApplyUserAgentStageTest { .providerName(PROVIDER_SOURCE).build(); @Test - public void when_noAdditionalDataIsPresent_outputStringEqualsInputString() throws Exception { - String clientBuildTimeUserAgentString = SDK_UA_STRING; - - ApplyUserAgentStage stage = new ApplyUserAgentStage(dependenciesWithUserAgent(clientBuildTimeUserAgentString)); + public void when_noAdditionalDataIsPresent_userAgentOnlyHasSdkValues() throws Exception { + ApplyUserAgentStage stage = new ApplyUserAgentStage(dependencies(clientUserAgent())); RequestExecutionContext ctx = requestExecutionContext(executionAttributes(IDENTITY_WITHOUT_SOURCE), noOpRequest()); SdkHttpFullRequest.Builder request = stage.execute(SdkHttpFullRequest.builder(), ctx); List userAgentHeaders = request.headers().get(HEADER_USER_AGENT); assertThat(userAgentHeaders).isNotNull().hasSize(1); - assertThat(userAgentHeaders.get(0)).isEqualTo(SDK_UA_STRING); + String userAgentString = userAgentHeaders.get(0); + assertThat(userAgentString).startsWith("aws-sdk-java").endsWith("cfg/retry-mode#standard"); } @Test - public void when_identityContainsProvider_authSourceIsPresent() throws Exception { - String clientBuildTimeUserAgentString = SDK_UA_STRING; + public void when_userPrefixIsPresent_itIsAddedToUserAgent() throws Exception { + String prefix = "Some completely opaque user prefix"; + ApplyUserAgentStage stage = new ApplyUserAgentStage(dependencies(clientUserAgent(), prefix, null)); - ApplyUserAgentStage stage = new ApplyUserAgentStage(dependenciesWithUserAgent(clientBuildTimeUserAgentString)); + RequestExecutionContext ctx = requestExecutionContext(executionAttributes(IDENTITY_WITHOUT_SOURCE), noOpRequest()); + SdkHttpFullRequest.Builder request = stage.execute(SdkHttpFullRequest.builder(), ctx); - RequestExecutionContext ctx = requestExecutionContext(executionAttributes(IDENTITY_WITH_SOURCE), noOpRequest()); + List userAgentHeaders = request.headers().get(HEADER_USER_AGENT); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + String userAgentString = userAgentHeaders.get(0); + assertThat(userAgentString).startsWith(prefix + SPACE).endsWith("cfg/retry-mode#standard"); + } + + @Test + public void when_userSuffixIsPresent_itIsAddedToUserAgent() throws Exception { + String suffix = "Some completely opaque user suffix"; + ApplyUserAgentStage stage = new ApplyUserAgentStage(dependencies(clientUserAgent(), null, suffix)); + + RequestExecutionContext ctx = requestExecutionContext(executionAttributes(IDENTITY_WITHOUT_SOURCE), noOpRequest()); SdkHttpFullRequest.Builder request = stage.execute(SdkHttpFullRequest.builder(), ctx); List userAgentHeaders = request.headers().get(HEADER_USER_AGENT); assertThat(userAgentHeaders).isNotNull().hasSize(1); - assertThat(userAgentHeaders.get(0)).contains("auth-source#proc"); + String userAgentString = userAgentHeaders.get(0); + assertThat(userAgentString).startsWith("aws-sdk-java").endsWith(SPACE + suffix); } @Test public void when_requestContainsApiName_apiNamesArePresent() throws Exception { - String clientBuildTimeUserAgentString = SDK_UA_STRING; - - ApplyUserAgentStage stage = new ApplyUserAgentStage(dependenciesWithUserAgent(clientBuildTimeUserAgentString)); + ApplyUserAgentStage stage = new ApplyUserAgentStage(dependencies(clientUserAgent())); RequestExecutionContext ctx = requestExecutionContext(executionAttributes(IDENTITY_WITH_SOURCE), requestWithApiName("myLib", "1.0")); @@ -101,15 +120,44 @@ public void when_requestContainsApiName_apiNamesArePresent() throws Exception { assertThat(userAgentHeaders.get(0)).contains("myLib/1.0"); } - private static HttpClientDependencies dependenciesWithUserAgent(String userAgent) { - SdkClientConfiguration clientConfiguration = SdkClientConfiguration.builder() - .option(SdkClientOption.CLIENT_USER_AGENT, userAgent) - .build(); + @Test + public void when_identityContainsProvider_authSourceIsPresent() throws Exception { + ApplyUserAgentStage stage = new ApplyUserAgentStage(dependencies(clientUserAgent())); + + RequestExecutionContext ctx = requestExecutionContext(executionAttributes(IDENTITY_WITH_SOURCE), noOpRequest()); + SdkHttpFullRequest.Builder request = stage.execute(SdkHttpFullRequest.builder(), ctx); + + List userAgentHeaders = request.headers().get(HEADER_USER_AGENT); + assertThat(userAgentHeaders).isNotNull().hasSize(1); + assertThat(userAgentHeaders.get(0)).contains("auth-source#proc"); + } + + private static HttpClientDependencies dependencies(String clientUserAgent) { + return dependencies(clientUserAgent, null, null); + } + + private static HttpClientDependencies dependencies(String clientUserAgent, String prefix, String suffix) { + SdkClientConfiguration clientConfiguration = + SdkClientConfiguration.builder() + .option(SdkClientOption.CLIENT_USER_AGENT, clientUserAgent) + .option(USER_AGENT_PREFIX, prefix) + .option(USER_AGENT_SUFFIX, suffix) + .build(); return HttpClientDependencies.builder() .clientConfiguration(clientConfiguration) .build(); } + private String clientUserAgent() { + SdkClientUserAgentProperties clientProperties = new SdkClientUserAgentProperties(); + + clientProperties.putProperty(RETRY_MODE, "standard"); + clientProperties.putProperty(IO, "async"); + clientProperties.putProperty(HTTP, "netty"); + + return SdkUserAgentBuilder.buildClientUserAgentString(SystemUserAgent.getOrCreate(), clientProperties); + } + private static SdkRequest noOpRequest() { return requestWithOverrideConfig(null); } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/useragent/SdkUserAgentBuilderTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/useragent/SdkUserAgentBuilderTest.java new file mode 100644 index 000000000000..d8f9343843a8 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/useragent/SdkUserAgentBuilderTest.java @@ -0,0 +1,172 @@ +/* + * Copyright 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.core.internal.useragent; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.HTTP; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.INTERNAL_METADATA_MARKER; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.IO; +import static software.amazon.awssdk.core.internal.useragent.UserAgentConstant.RETRY_MODE; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.core.util.SystemUserAgent; + +class SdkUserAgentBuilderTest { + + @ParameterizedTest(name = "{index} - {0}") + @MethodSource("inputValues") + void sdkUserAgentStringValidation(String description, String expected, SdkClientUserAgentProperties requestUserAgent, + SystemUserAgent systemUserAgent) { + String userAgent = SdkUserAgentBuilder.buildClientUserAgentString(systemUserAgent, requestUserAgent); + assertThat(userAgent).isEqualTo(expected); + } + + private static Stream inputValues() { + SystemUserAgent standardValuesSysAgent = + customSysAgent("2.26.22-SNAPSHOT", "Mac_OS_X#14.6.1", "java#21.0.2", null, + "OpenJDK_64-Bit_Server_VM#21.0.2+13-LTS", null, "en_US", null); + SystemUserAgent maximalSysAgent = + customSysAgent("2.26.22-SNAPSHOT", "Mac_OS_X#14.6.1", "java#21.0.2", "lambda", + "OpenJDK_64-Bit_Server_VM#21.0.2+13-LTS", "vendor#Amazon.com_Inc.", "en_US", + Arrays.asList("Kotlin", "Scala")); + + SdkClientUserAgentProperties minimalProperties = sdkProperties(null, null, null, null); + SdkClientUserAgentProperties maximalProperties = sdkProperties("standard", "arbitrary", "async", "Netty"); + + return Stream.of( + Arguments.of("default sysagent, empty requestvalues", + "aws-sdk-java/2.26.22-SNAPSHOT ua/2.0 os/Mac_OS_X#14.6.1 lang/java#21.0.2 md/OpenJDK_64-Bit_Server_VM#21.0.2+13-LTS md/en_US", + minimalProperties, + standardValuesSysAgent), + Arguments.of("standard sysagent, request values - retry", + "aws-sdk-java/2.26.22-SNAPSHOT ua/2.0 os/Mac_OS_X#14.6.1 lang/java#21.0.2 " + + "md/OpenJDK_64-Bit_Server_VM#21.0.2+13-LTS md/vendor#Amazon.com_Inc. md/en_US md/Kotlin md/Scala " + + "exec-env/lambda cfg/retry-mode#standard", + sdkProperties("standard", null, null, null), + maximalSysAgent), + Arguments.of("standard sysagent, request values - internalMarker", + "aws-sdk-java/2.26.22-SNAPSHOT md/internal ua/2.0 os/Mac_OS_X#14.6.1 lang/java#21.0.2 " + + "md/OpenJDK_64-Bit_Server_VM#21.0.2+13-LTS md/vendor#Amazon.com_Inc. md/en_US md/Kotlin md/Scala exec-env/lambda", + sdkProperties(null, "arbitrary", null, null), + maximalSysAgent), + Arguments.of("standard sysagent, request values - io", + "aws-sdk-java/2.26.22-SNAPSHOT md/io#async ua/2.0 os/Mac_OS_X#14.6.1 lang/java#21.0.2 " + + "md/OpenJDK_64-Bit_Server_VM#21.0.2+13-LTS md/vendor#Amazon.com_Inc. md/en_US md/Kotlin md/Scala exec-env/lambda", + sdkProperties(null, null, "async", null), + maximalSysAgent), + Arguments.of("standard sysagent, request values - http", + "aws-sdk-java/2.26.22-SNAPSHOT md/http#Apache ua/2.0 os/Mac_OS_X#14.6.1 lang/java#21.0.2 " + + "md/OpenJDK_64-Bit_Server_VM#21.0.2+13-LTS md/vendor#Amazon.com_Inc. md/en_US md/Kotlin md/Scala exec-env/lambda", + sdkProperties(null, null, null, "Apache"), + maximalSysAgent), + Arguments.of("standard sysagent, request values - authSource", + "aws-sdk-java/2.26.22-SNAPSHOT ua/2.0 os/Mac_OS_X#14.6.1 lang/java#21.0.2 " + + "md/OpenJDK_64-Bit_Server_VM#21.0.2+13-LTS md/vendor#Amazon.com_Inc. md/en_US md/Kotlin md/Scala " + + "exec-env/lambda", + sdkProperties(null, null, null, null), + maximalSysAgent), + Arguments.of("standard sysagent, request values - maximal", + "aws-sdk-java/2.26.22-SNAPSHOT md/io#async md/http#Netty md/internal ua/2.0 os/Mac_OS_X#14.6.1 " + + "lang/java#21.0.2 " + + "md/OpenJDK_64-Bit_Server_VM#21.0.2+13-LTS md/vendor#Amazon.com_Inc. md/en_US md/Kotlin md/Scala " + + "exec-env/lambda cfg/retry-mode#standard", + maximalProperties, + maximalSysAgent) + ); + } + + private static SdkClientUserAgentProperties sdkProperties(String retryMode, String internalMarker, String io, String http) { + SdkClientUserAgentProperties properties = new SdkClientUserAgentProperties(); + + if (retryMode != null) { + properties.putProperty(RETRY_MODE, retryMode); + } + + if (internalMarker != null) { + properties.putProperty(INTERNAL_METADATA_MARKER, internalMarker); + } + + if (io != null) { + properties.putProperty(IO, io); + } + + if (http != null) { + properties.putProperty(HTTP, http); + } + + return properties; + } + + private static SystemUserAgent customSysAgent(String sdkVersion, String osMetadata, + String langMetadata, String envMetadata, String vmMetadata, + String vendorMetadata, String languageTagMetadata, + List additionalJvmLanguages) { + return new SystemUserAgent() { + @Override + public String userAgentString() { + return null; + } + + @Override + public String sdkVersion() { + return sdkVersion; + } + + @Override + public String osMetadata() { + return osMetadata; + } + + @Override + public String langMetadata() { + return langMetadata; + } + + @Override + public String envMetadata() { + return envMetadata; + } + + @Override + public String vmMetadata() { + return vmMetadata; + } + + @Override + public String vendorMetadata() { + return vendorMetadata; + } + + @Override + public Optional languageTagMetadata() { + return Optional.ofNullable(languageTagMetadata); + } + + @Override + public List additionalJvmLanguages() { + return additionalJvmLanguages == null ? Collections.emptyList() : additionalJvmLanguages; + } + }; + } + +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/useragent/SystemUserAgentTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/useragent/SystemUserAgentTest.java new file mode 100644 index 000000000000..7be2f614e03d --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/useragent/SystemUserAgentTest.java @@ -0,0 +1,132 @@ +/* + * Copyright 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.core.internal.useragent; + +import static org.assertj.core.api.Assertions.assertThat; +import static software.amazon.awssdk.core.internal.useragent.SdkUserAgentBuilder.buildSystemUserAgentString; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.core.util.SystemUserAgent; + +class SystemUserAgentTest { + + @Test + void when_userAgent_IsRetrived_itIsTheSameObject() { + SystemUserAgent userAgent = SystemUserAgent.getOrCreate(); + SystemUserAgent userAgentCopy = SystemUserAgent.getOrCreate(); + assertThat(userAgent).isEqualTo(userAgentCopy); + } + + @Test + void when_defaultUserAgent_IsRetrived_itIsTheSameObject() { + DefaultSystemUserAgent userAgent = DefaultSystemUserAgent.getOrCreate(); + DefaultSystemUserAgent userAgentCopy = DefaultSystemUserAgent.getOrCreate(); + assertThat(userAgent).isEqualTo(userAgentCopy); + } + + @Test + void when_defaultSystemUserAgent_isGenerated_itHasTheRightFormat() { + SystemUserAgent userAgent = DefaultSystemUserAgent.getOrCreate(); + String[] userAgentFields = userAgent.userAgentString().split(" "); + assertThat(userAgentFields.length).isGreaterThan(0); + assertThat(Arrays.stream(userAgentFields).allMatch(field -> field.contains("/"))).isTrue(); + } + + @ParameterizedTest(name = "{index} - {0}") + @MethodSource("inputValues") + void when_systemAgentValues_areCustomized_resultingStringIsExpected(String description, String expected, + SystemUserAgent systemUserAgent) { + assertThat(systemUserAgent.userAgentString()).isEqualTo(expected); + } + + private static Stream inputValues() { + return Stream.of( + Arguments.of("Minimal system agent", + "", + customSysAgent(null, null, null, null, null, null, null, null)), + Arguments.of("System agent no vendor, env", + "aws-sdk-java/2.26.22-SNAPSHOT os/Mac_OS_X#14.6.1 lang/java#21.0.2 md/OpenJDK_64-Bit_Server_VM#21.0.2+13-LTS md/en_US md/Scala", + customSysAgent("2.26.22-SNAPSHOT", "Mac_OS_X#14.6.1", "java#21.0.2", "unknown", + "OpenJDK_64-Bit_Server_VM#21.0.2+13-LTS", null, "en_US", + Arrays.asList("Scala"))), + Arguments.of("Maximal system agent", + "aws-sdk-java/2.26.22-SNAPSHOT os/Mac_OS_X#14.6.1 lang/java#21.0.2 md/OpenJDK_64-Bit_Server_VM#21.0.2+13-LTS md/vendor#Amazon.com_Inc. md/en_US md/Kotlin md/Scala exec-env/lambda", + customSysAgent("2.26.22-SNAPSHOT", "Mac_OS_X#14.6.1", "java#21.0.2", "lambda", + "OpenJDK_64-Bit_Server_VM#21.0.2+13-LTS", "vendor#Amazon.com_Inc.", "en_US", + Arrays.asList("Kotlin", "Scala"))) + ); + } + + private static SystemUserAgent customSysAgent(String sdkVersion, String osMetadata, + String langMetadata, String envMetadata, String vmMetadata, + String vendorMetadata, String languageTagMetadata, + List additionalJvmLanguages) { + return new SystemUserAgent() { + @Override + public String userAgentString() { + return buildSystemUserAgentString(this); + } + + @Override + public String sdkVersion() { + return sdkVersion; + } + + @Override + public String osMetadata() { + return osMetadata; + } + + @Override + public String langMetadata() { + return langMetadata; + } + + @Override + public String envMetadata() { + return envMetadata; + } + + @Override + public String vmMetadata() { + return vmMetadata; + } + + @Override + public String vendorMetadata() { + return vendorMetadata; + } + + @Override + public Optional languageTagMetadata() { + return Optional.ofNullable(languageTagMetadata); + } + + @Override + public List additionalJvmLanguages() { + return additionalJvmLanguages == null ? Collections.emptyList() : additionalJvmLanguages; + } + }; + } +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/util/SdkUserAgentTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/util/SdkUserAgentTest.java index 4c06a42e445f..fedd63fe854a 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/util/SdkUserAgentTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/util/SdkUserAgentTest.java @@ -16,37 +16,15 @@ package software.amazon.awssdk.core.util; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import java.util.Arrays; import org.junit.jupiter.api.Test; -import software.amazon.awssdk.utils.JavaSystemSetting; -public class SdkUserAgentTest { +class SdkUserAgentTest { @Test - public void userAgent() { - String userAgent = SdkUserAgent.create().userAgent(); - assertNotNull(userAgent); - Arrays.stream(userAgent.split(" ")).forEach(str -> assertThat(isValidInput(str)).isTrue()); - } - - @Test - public void userAgent_HasVendor() { - System.setProperty(JavaSystemSetting.JAVA_VENDOR.property(), "finks"); - String userAgent = SdkUserAgent.create().getUserAgent(); - System.clearProperty(JavaSystemSetting.JAVA_VENDOR.property()); - assertThat(userAgent).contains("vendor/finks"); - } - - @Test - public void userAgent_HasUnknownVendor() { - System.clearProperty(JavaSystemSetting.JAVA_VENDOR.property()); - String userAgent = SdkUserAgent.create().getUserAgent(); - assertThat(userAgent).contains("vendor/unknown"); - } - - private boolean isValidInput(String input) { - return input.startsWith("(") || input.contains("/") && input.split("/").length == 2; + void when_callingDeprecatedClass_valueIsCorrect() { + String userAgentStringThroughOldClass = SdkUserAgent.create().getUserAgent(); + String systemUserAgentString = SystemUserAgent.getOrCreate().userAgentString(); + assertThat(userAgentStringThroughOldClass).isEqualTo(systemUserAgentString); } } diff --git a/pom.xml b/pom.xml index 17aeebf6aa36..014ba2261917 100644 --- a/pom.xml +++ b/pom.xml @@ -676,6 +676,8 @@ *.internal.* software.amazon.awssdk.thirdparty.* software.amazon.awssdk.regions.* + + software.amazon.awssdk.utils.JavaSystemSetting true diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/customizeduseragent/InternalUserAgentTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/customizeduseragent/InternalUserAgentTest.java index 5e6ecce8205b..ccb2f28f8b10 100644 --- a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/customizeduseragent/InternalUserAgentTest.java +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/customizeduseragent/InternalUserAgentTest.java @@ -83,7 +83,6 @@ public void syncWithInternalUserAgent_shouldContainInternalUserAgent() { verifyUserAgent(); } - @Test public void asyncWithInternalUserAgent_shouldContainInternalUserAgent() { stubResponse(); @@ -95,7 +94,7 @@ public void asyncWithInternalUserAgent_shouldContainInternalUserAgent() { public void syncWithoutInternalUserAgent_shouldNotContainInternalUserAgent() { stubResponse(); clientWithoutInternalConfig.allTypes(SdkBuilder::build); - verifyNotContainUserAgent(); + verifyNotContainUserAgent(); } @Test @@ -106,11 +105,11 @@ public void asyncWithoutInternalUserAgent_shouldNotContainInternalUserAgent() { } private void verifyUserAgent() { - verify(postRequestedFor(anyUrl()).withHeader("user-agent", containing("md/foobar"))); + verify(postRequestedFor(anyUrl()).withHeader("user-agent", containing("md/internal"))); } private void verifyNotContainUserAgent() { - verify(postRequestedFor(anyUrl()).withHeader("user-agent", notMatching(".*md/foobar.*"))); + verify(postRequestedFor(anyUrl()).withHeader("user-agent", notMatching(".*md/internal.*"))); } private void stubResponse() { diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/defaultsmode/ClientDefaultsModeTestSuite.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/defaultsmode/ClientDefaultsModeTestSuite.java index 8042e0ccc321..68cc64d83a50 100644 --- a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/defaultsmode/ClientDefaultsModeTestSuite.java +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/defaultsmode/ClientDefaultsModeTestSuite.java @@ -50,7 +50,7 @@ public void legacyDefaultsMode_shouldUseLegacySetting() { ClientT client = clientBuilder().overrideConfiguration(o -> o.retryPolicy(RetryMode.LEGACY)).build(); callAllTypes(client); - WireMock.verify(postRequestedFor(anyUrl()).withHeader("User-Agent", containing("cfg/retry-mode/legacy"))); + WireMock.verify(postRequestedFor(anyUrl()).withHeader("User-Agent", containing("cfg/retry-mode#legacy"))); } @Test @@ -59,7 +59,7 @@ public void standardDefaultsMode_shouldApplyStandardDefaults() { ClientT client = clientBuilder().defaultsMode(DefaultsMode.STANDARD).build(); callAllTypes(client); - WireMock.verify(postRequestedFor(anyUrl()).withHeader("User-Agent", containing("cfg/retry-mode/standard"))); + WireMock.verify(postRequestedFor(anyUrl()).withHeader("User-Agent", containing("cfg/retry-mode#standard"))); } @Test @@ -69,7 +69,7 @@ public void retryModeOverridden_shouldTakePrecedence() { clientBuilder().defaultsMode(DefaultsMode.STANDARD).overrideConfiguration(o -> o.retryPolicy(RetryMode.LEGACY)).build(); callAllTypes(client); - WireMock.verify(postRequestedFor(anyUrl()).withHeader("User-Agent", containing("cfg/retry-mode/legacy"))); + WireMock.verify(postRequestedFor(anyUrl()).withHeader("User-Agent", containing("cfg/retry-mode#legacy"))); } private BuilderT clientBuilder() { diff --git a/utils/src/main/java/software/amazon/awssdk/utils/JavaSystemSetting.java b/utils/src/main/java/software/amazon/awssdk/utils/JavaSystemSetting.java index 67b88c0ba375..ecf2bf8578f4 100644 --- a/utils/src/main/java/software/amazon/awssdk/utils/JavaSystemSetting.java +++ b/utils/src/main/java/software/amazon/awssdk/utils/JavaSystemSetting.java @@ -33,7 +33,7 @@ public enum JavaSystemSetting implements SystemSetting { USER_HOME("user.home"), USER_LANGUAGE("user.language"), - USER_REGION("user.region"), + USER_COUNTRY("user.country"), USER_NAME("user.name"), SSL_KEY_STORE("javax.net.ssl.keyStore"),