|
| 1 | +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"). You |
| 4 | +# may not use this file except in compliance with the License. A copy of |
| 5 | +# the License is located at |
| 6 | +# |
| 7 | +# http://aws.amazon.com/apache2.0/ |
| 8 | +# |
| 9 | +# or in the "license" file accompanying this file. This file is |
| 10 | +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF |
| 11 | +# ANY KIND, either express or implied. See the License for the specific |
| 12 | +# language governing permissions and limitations under the License. |
| 13 | + |
| 14 | +import os |
| 15 | +import platform |
| 16 | +from string import ascii_letters, digits |
| 17 | +from typing import NamedTuple, Optional, Self |
| 18 | + |
| 19 | +from smithy_http.aio.crt import HAS_CRT |
| 20 | + |
| 21 | +_USERAGENT_ALLOWED_CHARACTERS = ascii_letters + digits + "!$%&'*+-.^_`|~" |
| 22 | +_USERAGENT_ALLOWED_OS_NAMES = ( |
| 23 | + "windows", |
| 24 | + "linux", |
| 25 | + "macos", |
| 26 | + "android", |
| 27 | + "ios", |
| 28 | + "watchos", |
| 29 | + "tvos", |
| 30 | + "other", |
| 31 | +) |
| 32 | +_USERAGENT_PLATFORM_NAME_MAPPINGS = {"darwin": "macos"} |
| 33 | +_USERAGENT_SDK_NAME = "aws-sdk-python" |
| 34 | + |
| 35 | + |
| 36 | +class UserAgent: |
| 37 | + def __init__( |
| 38 | + self, |
| 39 | + platform_name, |
| 40 | + platform_version, |
| 41 | + platform_machine, |
| 42 | + python_version, |
| 43 | + python_implementation, |
| 44 | + execution_env, |
| 45 | + crt_version=None, |
| 46 | + ) -> None: |
| 47 | + self._platform_name = platform_name |
| 48 | + self._platform_version = platform_version |
| 49 | + self._platform_machine = platform_machine |
| 50 | + self._python_version = python_version |
| 51 | + self._python_implementation = python_implementation |
| 52 | + self._execution_env = execution_env |
| 53 | + self._crt_version = crt_version |
| 54 | + |
| 55 | + # Components that can be added with ``set_config`` |
| 56 | + self._user_agent_suffix = None |
| 57 | + self._user_agent_app_id = None |
| 58 | + self._sdk_version = None |
| 59 | + |
| 60 | + @classmethod |
| 61 | + def from_environment(cls) -> Self: |
| 62 | + crt_version = None |
| 63 | + if HAS_CRT: |
| 64 | + crt_version = _get_crt_version() or "Unknown" |
| 65 | + return cls( |
| 66 | + platform_name=platform.system(), |
| 67 | + platform_version=platform.release(), |
| 68 | + platform_machine=platform.machine(), |
| 69 | + python_version=platform.python_version(), |
| 70 | + python_implementation=platform.python_implementation(), |
| 71 | + execution_env=os.environ.get("AWS_EXECUTION_ENV"), |
| 72 | + crt_version=crt_version, |
| 73 | + ) |
| 74 | + |
| 75 | + def with_config( |
| 76 | + self, |
| 77 | + ua_suffix: str | None = None, |
| 78 | + ua_app_id: str | None = None, |
| 79 | + sdk_version: str | None = None, |
| 80 | + ) -> Self: |
| 81 | + self._user_agent_suffix = ua_suffix |
| 82 | + self._user_agent_app_id = ua_app_id |
| 83 | + self._sdk_version = sdk_version |
| 84 | + return self |
| 85 | + |
| 86 | + def to_string(self): |
| 87 | + """Build User-Agent header string from the object's properties.""" |
| 88 | + components = [ |
| 89 | + *self._build_sdk_metadata(), |
| 90 | + UserAgentComponent("ua", "2.0"), |
| 91 | + *self._build_os_metadata(), |
| 92 | + *self._build_architecture_metadata(), |
| 93 | + *self._build_language_metadata(), |
| 94 | + *self._build_execution_env_metadata(), |
| 95 | + *self._build_feature_metadata(), |
| 96 | + *self._build_app_id(), |
| 97 | + *self._build_suffix(), |
| 98 | + ] |
| 99 | + |
| 100 | + return " ".join([comp.to_string() for comp in components]) |
| 101 | + |
| 102 | + def _build_sdk_metadata(self): |
| 103 | + """Build the SDK name and version component of the User-Agent header. |
| 104 | +
|
| 105 | + Includes CRT version if available. |
| 106 | + """ |
| 107 | + sdk_md = [] |
| 108 | + sdk_md.append(UserAgentComponent(_USERAGENT_SDK_NAME, self._sdk_version)) |
| 109 | + |
| 110 | + if self._crt_version is not None: |
| 111 | + sdk_md.append(UserAgentComponent("md", "awscrt", self._crt_version)) |
| 112 | + |
| 113 | + return sdk_md |
| 114 | + |
| 115 | + def _build_os_metadata(self): |
| 116 | + """Build the OS/platform components of the User-Agent header string. |
| 117 | +
|
| 118 | + For recognized platform names that match or map to an entry in the list |
| 119 | + of standardized OS names, a single component with prefix "os" is |
| 120 | + returned. Otherwise, one component "os/other" is returned and a second |
| 121 | + with prefix "md" and the raw platform name. |
| 122 | +
|
| 123 | + String representations of example return values: |
| 124 | + * ``os/macos#10.13.6`` |
| 125 | + * ``os/linux`` |
| 126 | + * ``os/other`` |
| 127 | + * ``os/other md/foobar#1.2.3`` |
| 128 | + """ |
| 129 | + if self._platform_name is None: |
| 130 | + return [UserAgentComponent("os", "other")] |
| 131 | + |
| 132 | + plt_name_lower = self._platform_name.lower() |
| 133 | + if plt_name_lower in _USERAGENT_ALLOWED_OS_NAMES: |
| 134 | + os_family = plt_name_lower |
| 135 | + elif plt_name_lower in _USERAGENT_PLATFORM_NAME_MAPPINGS: |
| 136 | + os_family = _USERAGENT_PLATFORM_NAME_MAPPINGS[plt_name_lower] |
| 137 | + else: |
| 138 | + os_family = None |
| 139 | + |
| 140 | + if os_family is not None: |
| 141 | + return [UserAgentComponent("os", os_family, self._platform_version)] |
| 142 | + else: |
| 143 | + return [ |
| 144 | + UserAgentComponent("os", "other"), |
| 145 | + UserAgentComponent("md", self._platform_name, self._platform_version), |
| 146 | + ] |
| 147 | + |
| 148 | + def _build_architecture_metadata(self): |
| 149 | + """Build architecture component of the User-Agent header string. |
| 150 | +
|
| 151 | + Returns the machine type with prefix "md" and name "arch", if one is available. |
| 152 | + Common values include "x86_64", "arm64", "i386". |
| 153 | + """ |
| 154 | + if self._platform_machine: |
| 155 | + return [UserAgentComponent("md", "arch", self._platform_machine.lower())] |
| 156 | + return [] |
| 157 | + |
| 158 | + def _build_language_metadata(self): |
| 159 | + """Build the language components of the User-Agent header string. |
| 160 | +
|
| 161 | + Returns the Python version in a component with prefix "lang" and name |
| 162 | + "python". The Python implementation (e.g. CPython, PyPy) is returned as |
| 163 | + separate metadata component with prefix "md" and name "pyimpl". |
| 164 | +
|
| 165 | + String representation of an example return value: |
| 166 | + ``lang/python#3.10.4 md/pyimpl#CPython`` |
| 167 | + """ |
| 168 | + lang_md = [ |
| 169 | + UserAgentComponent("lang", "python", self._python_version), |
| 170 | + ] |
| 171 | + if self._python_implementation: |
| 172 | + lang_md.append( |
| 173 | + UserAgentComponent("md", "pyimpl", self._python_implementation) |
| 174 | + ) |
| 175 | + return lang_md |
| 176 | + |
| 177 | + def _build_execution_env_metadata(self): |
| 178 | + """Build the execution environment component of the User-Agent header. |
| 179 | +
|
| 180 | + Returns a single component prefixed with "exec-env", usually sourced from the |
| 181 | + environment variable AWS_EXECUTION_ENV. |
| 182 | + """ |
| 183 | + if self._execution_env: |
| 184 | + return [UserAgentComponent("exec-env", self._execution_env)] |
| 185 | + else: |
| 186 | + return [] |
| 187 | + |
| 188 | + def _build_feature_metadata(self): |
| 189 | + """Build the features components of the User-Agent header string. |
| 190 | +
|
| 191 | + TODO: These should be sourced from property bag set on context. |
| 192 | + """ |
| 193 | + return [] |
| 194 | + |
| 195 | + def _build_app_id(self): |
| 196 | + """Build app component of the User-Agent header string.""" |
| 197 | + if self._user_agent_app_id: |
| 198 | + return [UserAgentComponent("app", self._user_agent_app_id)] |
| 199 | + else: |
| 200 | + return [] |
| 201 | + |
| 202 | + def _build_suffix(self): |
| 203 | + if self._user_agent_suffix: |
| 204 | + return [RawStringUserAgentComponent(self._user_agent_suffix)] |
| 205 | + else: |
| 206 | + return [] |
| 207 | + |
| 208 | + |
| 209 | +def sanitize_user_agent_string_component(raw_str, allow_hash): |
| 210 | + """Replaces all not allowed characters in the string with a dash ("-"). |
| 211 | +
|
| 212 | + Allowed characters are ASCII alphanumerics and ``!$%&'*+-.^_`|~``. If |
| 213 | + ``allow_hash`` is ``True``, "#"``" is also allowed. |
| 214 | +
|
| 215 | + :type raw_str: str |
| 216 | + :param raw_str: The input string to be sanitized. |
| 217 | +
|
| 218 | + :type allow_hash: bool |
| 219 | + :param allow_hash: Whether "#" is considered an allowed character. |
| 220 | + """ |
| 221 | + return "".join( |
| 222 | + c if c in _USERAGENT_ALLOWED_CHARACTERS or (allow_hash and c == "#") else "-" |
| 223 | + for c in raw_str |
| 224 | + ) |
| 225 | + |
| 226 | + |
| 227 | +class UserAgentComponent(NamedTuple): |
| 228 | + """Component of a User-Agent header string in the standard format. |
| 229 | +
|
| 230 | + Each component consists of a prefix, a name, and a value. In the string |
| 231 | + representation these are combined in the format ``prefix/name#value``. |
| 232 | +
|
| 233 | + This class is considered private and is subject to abrupt breaking changes. |
| 234 | + """ |
| 235 | + |
| 236 | + prefix: str |
| 237 | + name: str |
| 238 | + value: Optional[str] = None |
| 239 | + |
| 240 | + def to_string(self): |
| 241 | + """Create string like 'prefix/name#value' from a UserAgentComponent.""" |
| 242 | + clean_prefix = sanitize_user_agent_string_component( |
| 243 | + self.prefix, allow_hash=True |
| 244 | + ) |
| 245 | + clean_name = sanitize_user_agent_string_component(self.name, allow_hash=False) |
| 246 | + if self.value is None or self.value == "": |
| 247 | + return f"{clean_prefix}/{clean_name}" |
| 248 | + clean_value = sanitize_user_agent_string_component(self.value, allow_hash=True) |
| 249 | + return f"{clean_prefix}/{clean_name}#{clean_value}" |
| 250 | + |
| 251 | + |
| 252 | +class RawStringUserAgentComponent: |
| 253 | + """UserAgentComponent interface wrapper around ``str``. |
| 254 | +
|
| 255 | + Use for User-Agent header components that are not constructed from prefix+name+value |
| 256 | + but instead are provided as strings. No sanitization is performed. |
| 257 | + """ |
| 258 | + |
| 259 | + def __init__(self, value): |
| 260 | + self._value = value |
| 261 | + |
| 262 | + def to_string(self): |
| 263 | + return self._value |
| 264 | + |
| 265 | + |
| 266 | +def _get_crt_version(): |
| 267 | + """This function is considered private and is subject to abrupt breaking changes.""" |
| 268 | + try: |
| 269 | + import awscrt |
| 270 | + |
| 271 | + return awscrt.__version__ |
| 272 | + except AttributeError: |
| 273 | + return None |
0 commit comments