1 /*
2  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License").
5  * You may not use this file except in compliance with the License.
6  * A copy of the License is located at
7  *
8  *  http://aws.amazon.com/apache2.0
9  *
10  * or in the "license" file accompanying this file. This file is distributed
11  * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12  * express or implied. See the License for the specific language governing
13  * permissions and limitations under the License.
14  */
15 
16 package software.amazon.awssdk.core.util;
17 
18 import java.util.Optional;
19 import java.util.jar.JarInputStream;
20 import org.slf4j.Logger;
21 import org.slf4j.LoggerFactory;
22 import software.amazon.awssdk.annotations.SdkProtectedApi;
23 import software.amazon.awssdk.annotations.SdkTestInternalApi;
24 import software.amazon.awssdk.annotations.ThreadSafe;
25 import software.amazon.awssdk.utils.IoUtils;
26 import software.amazon.awssdk.utils.JavaSystemSetting;
27 import software.amazon.awssdk.utils.StringUtils;
28 
29 /**
30  * Utility class for accessing AWS SDK versioning information.
31  */
32 @ThreadSafe
33 @SdkProtectedApi
34 public final class SdkUserAgent {
35 
36     private static final String UA_STRING = "aws-sdk-{platform}/{version} {os.name}/{os.version} {java.vm.name}/{java.vm"
37                                             + ".version} Java/{java.version}{language.and.region}{additional.languages} "
38                                             + "vendor/{java.vendor}";
39 
40     /** Disallowed characters in the user agent token: @see <a href="https://tools.ietf.org/html/rfc7230#section-3.2.6">RFC 7230</a> */
41     private static final String UA_DENYLIST_REGEX = "[() ,/:;<=>?@\\[\\]{}\\\\]";
42 
43     /** Shared logger for any issues while loading version information. */
44     private static final Logger log = LoggerFactory.getLogger(SdkUserAgent.class);
45     private static final String UNKNOWN = "unknown";
46     private static volatile SdkUserAgent instance;
47 
48     private static final String[] USER_AGENT_SEARCH = {
49         "{platform}",
50         "{version}",
51         "{os.name}",
52         "{os.version}",
53         "{java.vm.name}",
54         "{java.vm.version}",
55         "{java.version}",
56         "{java.vendor}",
57         "{additional.languages}",
58         "{language.and.region}"
59     };
60 
61     /** User Agent info. */
62     private String userAgent;
63 
SdkUserAgent()64     private SdkUserAgent() {
65         initializeUserAgent();
66     }
67 
create()68     public static SdkUserAgent create() {
69         if (instance == null) {
70             synchronized (SdkUserAgent.class) {
71                 if (instance == null) {
72                     instance = new SdkUserAgent();
73                 }
74             }
75         }
76 
77         return instance;
78     }
79 
80     /**
81      * @return Returns the User Agent string to be used when communicating with
82      *     the AWS services.  The User Agent encapsulates SDK, Java, OS and
83      *     region information.
84      */
userAgent()85     public String userAgent() {
86         return userAgent;
87     }
88 
89     /**
90      * Initializes the user agent string by loading a template from
91      * {@code InternalConfig} and filling in the detected version/platform
92      * info.
93      */
initializeUserAgent()94     private void initializeUserAgent() {
95         userAgent = getUserAgent();
96     }
97 
98     @SdkTestInternalApi
getUserAgent()99     String getUserAgent() {
100         Optional<String> language = JavaSystemSetting.USER_LANGUAGE.getStringValue();
101         Optional<String> region = JavaSystemSetting.USER_REGION.getStringValue();
102         String languageAndRegion = "";
103         if (language.isPresent() && region.isPresent()) {
104             languageAndRegion = " (" + sanitizeInput(language.get()) + "_" + sanitizeInput(region.get()) + ")";
105         }
106 
107         return StringUtils.replaceEach(UA_STRING, USER_AGENT_SEARCH, new String[] {
108             "java",
109             VersionInfo.SDK_VERSION,
110             sanitizeInput(JavaSystemSetting.OS_NAME.getStringValue().orElse(null)),
111             sanitizeInput(JavaSystemSetting.OS_VERSION.getStringValue().orElse(null)),
112             sanitizeInput(JavaSystemSetting.JAVA_VM_NAME.getStringValue().orElse(null)),
113             sanitizeInput(JavaSystemSetting.JAVA_VM_VERSION.getStringValue().orElse(null)),
114             sanitizeInput(JavaSystemSetting.JAVA_VERSION.getStringValue().orElse(null)),
115             sanitizeInput(JavaSystemSetting.JAVA_VENDOR.getStringValue().orElse(null)),
116             getAdditionalJvmLanguages(),
117             languageAndRegion,
118         });
119     }
120 
121     /**
122      * Replace any spaces, parentheses in the input with underscores.
123      *
124      * @param input the input
125      * @return the input with spaces replaced by underscores
126      */
sanitizeInput(String input)127     private static String sanitizeInput(String input) {
128         return input == null ? UNKNOWN : input.replaceAll(UA_DENYLIST_REGEX, "_");
129     }
130 
getAdditionalJvmLanguages()131     private static String getAdditionalJvmLanguages() {
132         return concat(concat("", scalaVersion(), " "), kotlinVersion(), " ");
133     }
134 
135     /**
136      * Attempt to determine if Scala is on the classpath and if so what version is in use.
137      * Does this by looking for a known Scala class (scala.util.Properties) and then calling
138      * a static method on that class via reflection to determine the versionNumberString.
139      *
140      * @return Scala version if any, else empty string
141      */
scalaVersion()142     private static String scalaVersion() {
143         String scalaVersion = "";
144         try {
145             Class<?> scalaProperties = Class.forName("scala.util.Properties");
146             scalaVersion = "scala";
147             String version = (String) scalaProperties.getMethod("versionNumberString").invoke(null);
148             scalaVersion = concat(scalaVersion, version, "/");
149         } catch (ClassNotFoundException e) {
150             //Ignore
151         } catch (Exception e) {
152             if (log.isTraceEnabled()) {
153                 log.trace("Exception attempting to get Scala version.", e);
154             }
155         }
156         return scalaVersion;
157     }
158 
159     /**
160      * Attempt to determine if Kotlin is on the classpath and if so what version is in use.
161      * Does this by looking for a known Kotlin class (kotlin.Unit) and then loading the Manifest
162      * from that class' JAR to determine the Kotlin version.
163      *
164      * @return Kotlin version if any, else empty string
165      */
kotlinVersion()166     private static String kotlinVersion() {
167         String kotlinVersion = "";
168         JarInputStream kotlinJar = null;
169         try {
170             Class<?> kotlinUnit = Class.forName("kotlin.Unit");
171             kotlinVersion = "kotlin";
172             kotlinJar = new JarInputStream(kotlinUnit.getProtectionDomain().getCodeSource().getLocation().openStream());
173             String version = kotlinJar.getManifest().getMainAttributes().getValue("Implementation-Version");
174             kotlinVersion = concat(kotlinVersion, version, "/");
175         } catch (ClassNotFoundException e) {
176             //Ignore
177         } catch (Exception e) {
178             if (log.isTraceEnabled()) {
179                 log.trace("Exception attempting to get Kotlin version.", e);
180             }
181         } finally {
182             IoUtils.closeQuietly(kotlinJar, log);
183         }
184         return kotlinVersion;
185     }
186 
concat(String prefix, String suffix, String separator)187     private static String concat(String prefix, String suffix, String separator) {
188         return suffix != null && !suffix.isEmpty() ? prefix + separator + suffix : prefix;
189     }
190 }
191