1# Copyright 2023 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"""
14NOTE: All classes and functions in this module are considered private and are
15subject to abrupt breaking changes. Please do not use them directly.
16
17To modify the User-Agent header sent by botocore, use one of these
18configuration options:
19* The ``AWS_SDK_UA_APP_ID`` environment variable.
20* The ``sdk_ua_app_id`` setting in the shared AWS config file.
21* The ``user_agent_appid`` field in the :py:class:`botocore.config.Config`.
22* The ``user_agent_extra`` field in the :py:class:`botocore.config.Config`.
23
24"""
25
26import os
27import platform
28from copy import copy
29from string import ascii_letters, digits
30from typing import NamedTuple, Optional
31
32from botocore import __version__ as botocore_version
33from botocore.compat import HAS_CRT
34
35_USERAGENT_ALLOWED_CHARACTERS = ascii_letters + digits + "!$%&'*+-.^_`|~"
36_USERAGENT_ALLOWED_OS_NAMES = (
37 'windows',
38 'linux',
39 'macos',
40 'android',
41 'ios',
42 'watchos',
43 'tvos',
44 'other',
45)
46_USERAGENT_PLATFORM_NAME_MAPPINGS = {'darwin': 'macos'}
47# The name by which botocore is identified in the User-Agent header. While most
48# AWS SDKs follow a naming pattern of "aws-sdk-*", botocore and boto3 continue
49# using their existing values. Uses uppercase "B" with all other characters
50# lowercase.
51_USERAGENT_SDK_NAME = 'Botocore'
52
53
54def sanitize_user_agent_string_component(raw_str, allow_hash):
55 """Replaces all not allowed characters in the string with a dash ("-").
56
57 Allowed characters are ASCII alphanumerics and ``!$%&'*+-.^_`|~``. If
58 ``allow_hash`` is ``True``, "#"``" is also allowed.
59
60 :type raw_str: str
61 :param raw_str: The input string to be sanitized.
62
63 :type allow_hash: bool
64 :param allow_hash: Whether "#" is considered an allowed character.
65 """
66 return ''.join(
67 c
68 if c in _USERAGENT_ALLOWED_CHARACTERS or (allow_hash and c == '#')
69 else '-'
70 for c in raw_str
71 )
72
73
74class UserAgentComponent(NamedTuple):
75 """
76 Component of a Botocore User-Agent header string in the standard format.
77
78 Each component consists of a prefix, a name, and a value. In the string
79 representation these are combined in the format ``prefix/name#value``.
80
81 This class is considered private and is subject to abrupt breaking changes.
82 """
83
84 prefix: str
85 name: str
86 value: Optional[str] = None
87
88 def to_string(self):
89 """Create string like 'prefix/name#value' from a UserAgentComponent."""
90 clean_prefix = sanitize_user_agent_string_component(
91 self.prefix, allow_hash=True
92 )
93 clean_name = sanitize_user_agent_string_component(
94 self.name, allow_hash=False
95 )
96 if self.value is None or self.value == '':
97 return f'{clean_prefix}/{clean_name}'
98 clean_value = sanitize_user_agent_string_component(
99 self.value, allow_hash=True
100 )
101 return f'{clean_prefix}/{clean_name}#{clean_value}'
102
103
104class RawStringUserAgentComponent:
105 """
106 UserAgentComponent interface wrapper around ``str``.
107
108 Use for User-Agent header components that are not constructed from
109 prefix+name+value but instead are provided as strings. No sanitization is
110 performed.
111 """
112
113 def __init__(self, value):
114 self._value = value
115
116 def to_string(self):
117 return self._value
118
119
120# This is not a public interface and is subject to abrupt breaking changes.
121# Any usage is not advised or supported in external code bases.
122try:
123 from botocore.customizations.useragent import modify_components
124except ImportError:
125 # Default implementation that returns unmodified User-Agent components.
126 def modify_components(components):
127 return components
128
129
130class UserAgentString:
131 """
132 Generator for AWS SDK User-Agent header strings.
133
134 The User-Agent header format contains information from session, client, and
135 request context. ``UserAgentString`` provides methods for collecting the
136 information and ``to_string`` for assembling it into the standardized
137 string format.
138
139 Example usage:
140
141 ua_session = UserAgentString.from_environment()
142 ua_session.set_session_config(...)
143 ua_client = ua_session.with_client_config(Config(...))
144 ua_string = ua_request.to_string()
145
146 For testing or when information from all sources is available at the same
147 time, the methods can be chained:
148
149 ua_string = (
150 UserAgentString
151 .from_environment()
152 .set_session_config(...)
153 .with_client_config(Config(...))
154 .to_string()
155 )
156
157 """
158
159 def __init__(
160 self,
161 platform_name,
162 platform_version,
163 platform_machine,
164 python_version,
165 python_implementation,
166 execution_env,
167 crt_version=None,
168 ):
169 """
170 :type platform_name: str
171 :param platform_name: Name of the operating system or equivalent
172 platform name. Should be sourced from :py:meth:`platform.system`.
173 :type platform_version: str
174 :param platform_version: Version of the operating system or equivalent
175 platform name. Should be sourced from :py:meth:`platform.version`.
176 :type platform_machine: str
177 :param platform_version: Processor architecture or machine type. For
178 example "x86_64". Should be sourced from :py:meth:`platform.machine`.
179 :type python_version: str
180 :param python_version: Version of the python implementation as str.
181 Should be sourced from :py:meth:`platform.python_version`.
182 :type python_implementation: str
183 :param python_implementation: Name of the python implementation.
184 Should be sourced from :py:meth:`platform.python_implementation`.
185 :type execution_env: str
186 :param execution_env: The value of the AWS execution environment.
187 Should be sourced from the ``AWS_EXECUTION_ENV` environment
188 variable.
189 :type crt_version: str
190 :param crt_version: Version string of awscrt package, if installed.
191 """
192 self._platform_name = platform_name
193 self._platform_version = platform_version
194 self._platform_machine = platform_machine
195 self._python_version = python_version
196 self._python_implementation = python_implementation
197 self._execution_env = execution_env
198 self._crt_version = crt_version
199
200 # Components that can be added with ``set_session_config()``
201 self._session_user_agent_name = None
202 self._session_user_agent_version = None
203 self._session_user_agent_extra = None
204
205 self._client_config = None
206 self._uses_paginator = None
207 self._uses_waiter = None
208 self._uses_resource = None
209
210 @classmethod
211 def from_environment(cls):
212 crt_version = None
213 if HAS_CRT:
214 crt_version = _get_crt_version() or 'Unknown'
215 return cls(
216 platform_name=platform.system(),
217 platform_version=platform.release(),
218 platform_machine=platform.machine(),
219 python_version=platform.python_version(),
220 python_implementation=platform.python_implementation(),
221 execution_env=os.environ.get('AWS_EXECUTION_ENV'),
222 crt_version=crt_version,
223 )
224
225 def set_session_config(
226 self,
227 session_user_agent_name,
228 session_user_agent_version,
229 session_user_agent_extra,
230 ):
231 """
232 Set the user agent configuration values that apply at session level.
233
234 :param user_agent_name: The user agent name configured in the
235 :py:class:`botocore.session.Session` object. For backwards
236 compatibility, this will always be at the beginning of the
237 User-Agent string, together with ``user_agent_version``.
238 :param user_agent_version: The user agent version configured in the
239 :py:class:`botocore.session.Session` object.
240 :param user_agent_extra: The user agent "extra" configured in the
241 :py:class:`botocore.session.Session` object.
242 """
243 self._session_user_agent_name = session_user_agent_name
244 self._session_user_agent_version = session_user_agent_version
245 self._session_user_agent_extra = session_user_agent_extra
246 return self
247
248 def with_client_config(self, client_config):
249 """
250 Create a copy with all original values and client-specific values.
251
252 :type client_config: botocore.config.Config
253 :param client_config: The client configuration object.
254 """
255 cp = copy(self)
256 cp._client_config = client_config
257 return cp
258
259 def to_string(self):
260 """
261 Build User-Agent header string from the object's properties.
262 """
263 config_ua_override = None
264 if self._client_config:
265 if hasattr(self._client_config, '_supplied_user_agent'):
266 config_ua_override = self._client_config._supplied_user_agent
267 else:
268 config_ua_override = self._client_config.user_agent
269
270 if config_ua_override is not None:
271 return self._build_legacy_ua_string(config_ua_override)
272
273 components = [
274 *self._build_sdk_metadata(),
275 RawStringUserAgentComponent('ua/2.0'),
276 *self._build_os_metadata(),
277 *self._build_architecture_metadata(),
278 *self._build_language_metadata(),
279 *self._build_execution_env_metadata(),
280 *self._build_feature_metadata(),
281 *self._build_config_metadata(),
282 *self._build_app_id(),
283 *self._build_extra(),
284 ]
285
286 components = modify_components(components)
287
288 return ' '.join([comp.to_string() for comp in components])
289
290 def _build_sdk_metadata(self):
291 """
292 Build the SDK name and version component of the User-Agent header.
293
294 For backwards-compatibility both session-level and client-level config
295 of custom tool names are honored. If this removes the Botocore
296 information from the start of the string, Botocore's name and version
297 are included as a separate field with "md" prefix.
298 """
299 sdk_md = []
300 if (
301 self._session_user_agent_name
302 and self._session_user_agent_version
303 and (
304 self._session_user_agent_name != _USERAGENT_SDK_NAME
305 or self._session_user_agent_version != botocore_version
306 )
307 ):
308 sdk_md.extend(
309 [
310 UserAgentComponent(
311 self._session_user_agent_name,
312 self._session_user_agent_version,
313 ),
314 UserAgentComponent(
315 'md', _USERAGENT_SDK_NAME, botocore_version
316 ),
317 ]
318 )
319 else:
320 sdk_md.append(
321 UserAgentComponent(_USERAGENT_SDK_NAME, botocore_version)
322 )
323
324 if self._crt_version is not None:
325 sdk_md.append(
326 UserAgentComponent('md', 'awscrt', self._crt_version)
327 )
328
329 return sdk_md
330
331 def _build_os_metadata(self):
332 """
333 Build the OS/platform components of the User-Agent header string.
334
335 For recognized platform names that match or map to an entry in the list
336 of standardized OS names, a single component with prefix "os" is
337 returned. Otherwise, one component "os/other" is returned and a second
338 with prefix "md" and the raw platform name.
339
340 String representations of example return values:
341 * ``os/macos#10.13.6``
342 * ``os/linux``
343 * ``os/other``
344 * ``os/other md/foobar#1.2.3``
345 """
346 if self._platform_name is None:
347 return [UserAgentComponent('os', 'other')]
348
349 plt_name_lower = self._platform_name.lower()
350 if plt_name_lower in _USERAGENT_ALLOWED_OS_NAMES:
351 os_family = plt_name_lower
352 elif plt_name_lower in _USERAGENT_PLATFORM_NAME_MAPPINGS:
353 os_family = _USERAGENT_PLATFORM_NAME_MAPPINGS[plt_name_lower]
354 else:
355 os_family = None
356
357 if os_family is not None:
358 return [
359 UserAgentComponent('os', os_family, self._platform_version)
360 ]
361 else:
362 return [
363 UserAgentComponent('os', 'other'),
364 UserAgentComponent(
365 'md', self._platform_name, self._platform_version
366 ),
367 ]
368
369 def _build_architecture_metadata(self):
370 """
371 Build architecture component of the User-Agent header string.
372
373 Returns the machine type with prefix "md" and name "arch", if one is
374 available. Common values include "x86_64", "arm64", "i386".
375 """
376 if self._platform_machine:
377 return [
378 UserAgentComponent(
379 'md', 'arch', self._platform_machine.lower()
380 )
381 ]
382 return []
383
384 def _build_language_metadata(self):
385 """
386 Build the language components of the User-Agent header string.
387
388 Returns the Python version in a component with prefix "lang" and name
389 "python". The Python implementation (e.g. CPython, PyPy) is returned as
390 separate metadata component with prefix "md" and name "pyimpl".
391
392 String representation of an example return value:
393 ``lang/python#3.10.4 md/pyimpl#CPython``
394 """
395 lang_md = [
396 UserAgentComponent('lang', 'python', self._python_version),
397 ]
398 if self._python_implementation:
399 lang_md.append(
400 UserAgentComponent('md', 'pyimpl', self._python_implementation)
401 )
402 return lang_md
403
404 def _build_execution_env_metadata(self):
405 """
406 Build the execution environment component of the User-Agent header.
407
408 Returns a single component prefixed with "exec-env", usually sourced
409 from the environment variable AWS_EXECUTION_ENV.
410 """
411 if self._execution_env:
412 return [UserAgentComponent('exec-env', self._execution_env)]
413 else:
414 return []
415
416 def _build_feature_metadata(self):
417 """
418 Build the features components of the User-Agent header string.
419
420 Botocore currently does not report any features. This may change in a
421 future version.
422 """
423 return []
424
425 def _build_config_metadata(self):
426 """
427 Build the configuration components of the User-Agent header string.
428
429 Returns a list of components with prefix "cfg" followed by the config
430 setting name and its value. Tracked configuration settings may be
431 added or removed in future versions.
432 """
433 if not self._client_config or not self._client_config.retries:
434 return []
435 retry_mode = self._client_config.retries.get('mode')
436 cfg_md = [UserAgentComponent('cfg', 'retry-mode', retry_mode)]
437 if self._client_config.endpoint_discovery_enabled:
438 cfg_md.append(UserAgentComponent('cfg', 'endpoint-discovery'))
439 return cfg_md
440
441 def _build_app_id(self):
442 """
443 Build app component of the User-Agent header string.
444
445 Returns a single component with prefix "app" and value sourced from the
446 ``user_agent_appid`` field in :py:class:`botocore.config.Config` or
447 the ``sdk_ua_app_id`` setting in the shared configuration file, or the
448 ``AWS_SDK_UA_APP_ID`` environment variable. These are the recommended
449 ways for apps built with Botocore to insert their identifer into the
450 User-Agent header.
451 """
452 if self._client_config and self._client_config.user_agent_appid:
453 return [
454 UserAgentComponent('app', self._client_config.user_agent_appid)
455 ]
456 else:
457 return []
458
459 def _build_extra(self):
460 """User agent string components based on legacy "extra" settings.
461
462 Creates components from the session-level and client-level
463 ``user_agent_extra`` setting, if present. Both are passed through
464 verbatim and should be appended at the end of the string.
465
466 Preferred ways to inject application-specific information into
467 botocore's User-Agent header string are the ``user_agent_appid` field
468 in :py:class:`botocore.config.Config`. The ``AWS_SDK_UA_APP_ID``
469 environment variable and the ``sdk_ua_app_id`` configuration file
470 setting are alternative ways to set the ``user_agent_appid`` config.
471 """
472 extra = []
473 if self._session_user_agent_extra:
474 extra.append(
475 RawStringUserAgentComponent(self._session_user_agent_extra)
476 )
477 if self._client_config and self._client_config.user_agent_extra:
478 extra.append(
479 RawStringUserAgentComponent(
480 self._client_config.user_agent_extra
481 )
482 )
483 return extra
484
485 def _build_legacy_ua_string(self, config_ua_override):
486 components = [config_ua_override]
487 if self._session_user_agent_extra:
488 components.append(self._session_user_agent_extra)
489 if self._client_config.user_agent_extra:
490 components.append(self._client_config.user_agent_extra)
491 return ' '.join(components)
492
493
494def _get_crt_version():
495 """
496 This function is considered private and is subject to abrupt breaking
497 changes.
498 """
499 try:
500 import awscrt
501
502 return awscrt.__version__
503 except AttributeError:
504 return None