Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/botocore/useragent.py: 29%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

187 statements  

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 logging 

27import os 

28import platform 

29from copy import copy 

30from string import ascii_letters, digits 

31from typing import NamedTuple, Optional 

32 

33from botocore import __version__ as botocore_version 

34from botocore.compat import HAS_CRT 

35from botocore.context import get_context 

36 

37logger = logging.getLogger(__name__) 

38 

39 

40_USERAGENT_ALLOWED_CHARACTERS = ascii_letters + digits + "!$%&'*+-.^_`|~," 

41_USERAGENT_ALLOWED_OS_NAMES = ( 

42 'windows', 

43 'linux', 

44 'macos', 

45 'android', 

46 'ios', 

47 'watchos', 

48 'tvos', 

49 'other', 

50) 

51_USERAGENT_PLATFORM_NAME_MAPPINGS = {'darwin': 'macos'} 

52# The name by which botocore is identified in the User-Agent header. While most 

53# AWS SDKs follow a naming pattern of "aws-sdk-*", botocore and boto3 continue 

54# using their existing values. Uses uppercase "B" with all other characters 

55# lowercase. 

56_USERAGENT_SDK_NAME = 'Botocore' 

57_USERAGENT_FEATURE_MAPPINGS = { 

58 'WAITER': 'B', 

59 'PAGINATOR': 'C', 

60 "RETRY_MODE_LEGACY": "D", 

61 "RETRY_MODE_STANDARD": "E", 

62 "RETRY_MODE_ADAPTIVE": "F", 

63 'S3_TRANSFER': 'G', 

64 'GZIP_REQUEST_COMPRESSION': 'L', 

65 'PROTOCOL_RPC_V2_CBOR': 'M', 

66 'ENDPOINT_OVERRIDE': 'N', 

67 'ACCOUNT_ID_MODE_PREFERRED': 'P', 

68 'ACCOUNT_ID_MODE_DISABLED': 'Q', 

69 'ACCOUNT_ID_MODE_REQUIRED': 'R', 

70 'SIGV4A_SIGNING': 'S', 

71 'RESOLVED_ACCOUNT_ID': 'T', 

72 'FLEXIBLE_CHECKSUMS_REQ_CRC32': 'U', 

73 'FLEXIBLE_CHECKSUMS_REQ_CRC32C': 'V', 

74 'FLEXIBLE_CHECKSUMS_REQ_CRC64': 'W', 

75 'FLEXIBLE_CHECKSUMS_REQ_SHA1': 'X', 

76 'FLEXIBLE_CHECKSUMS_REQ_SHA256': 'Y', 

77 'FLEXIBLE_CHECKSUMS_REQ_WHEN_SUPPORTED': 'Z', 

78 'FLEXIBLE_CHECKSUMS_REQ_WHEN_REQUIRED': 'a', 

79 'FLEXIBLE_CHECKSUMS_RES_WHEN_SUPPORTED': 'b', 

80 'FLEXIBLE_CHECKSUMS_RES_WHEN_REQUIRED': 'c', 

81 'CREDENTIALS_HTTP': 'z', 

82 'CREDENTIALS_IMDS': '0', 

83 'BEARER_SERVICE_ENV_VARS': '3', 

84} 

85 

86 

87def register_feature_id(feature_id): 

88 """Adds metric value to the current context object's ``features`` set. 

89 

90 :type feature_id: str 

91 :param feature_id: The name of the feature to register. Value must be a key 

92 in the ``_USERAGENT_FEATURE_MAPPINGS`` dict. 

93 """ 

94 ctx = get_context() 

95 if ctx is None: 

96 # Never register features outside the scope of a 

97 # ``botocore.context.start_as_current_context`` context manager. 

98 # Otherwise, the context variable won't be reset and features will 

99 # bleed into all subsequent requests. Return instead of raising an 

100 # exception since this function could be invoked in a public interface. 

101 return 

102 if val := _USERAGENT_FEATURE_MAPPINGS.get(feature_id): 

103 ctx.features.add(val) 

104 

105 

106def sanitize_user_agent_string_component(raw_str, allow_hash): 

107 """Replaces all not allowed characters in the string with a dash ("-"). 

108 

109 Allowed characters are ASCII alphanumerics and ``!$%&'*+-.^_`|~,``. If 

110 ``allow_hash`` is ``True``, "#"``" is also allowed. 

111 

112 :type raw_str: str 

113 :param raw_str: The input string to be sanitized. 

114 

115 :type allow_hash: bool 

116 :param allow_hash: Whether "#" is considered an allowed character. 

117 """ 

118 return ''.join( 

119 c 

120 if c in _USERAGENT_ALLOWED_CHARACTERS or (allow_hash and c == '#') 

121 else '-' 

122 for c in raw_str 

123 ) 

124 

125 

126class UserAgentComponentSizeConfig: 

127 """ 

128 Configures the max size of a built user agent string component and the 

129 delimiter used to truncate the string if the size is above the max. 

130 """ 

131 

132 def __init__(self, max_size_in_bytes: int, delimiter: str): 

133 self.max_size_in_bytes = max_size_in_bytes 

134 self.delimiter = delimiter 

135 self._validate_input() 

136 

137 def _validate_input(self): 

138 if self.max_size_in_bytes < 1: 

139 raise ValueError( 

140 f'Invalid `max_size_in_bytes`: {self.max_size_in_bytes}. ' 

141 'Value must be a positive integer.' 

142 ) 

143 

144 

145class UserAgentComponent(NamedTuple): 

146 """ 

147 Component of a Botocore User-Agent header string in the standard format. 

148 

149 Each component consists of a prefix, a name, a value, and a size_config. 

150 In the string representation these are combined in the format 

151 ``prefix/name#value``. 

152 

153 ``size_config`` configures the max size and truncation strategy for the 

154 built user agent string component. 

155 

156 This class is considered private and is subject to abrupt breaking changes. 

157 """ 

158 

159 prefix: str 

160 name: str 

161 value: Optional[str] = None 

162 size_config: Optional[UserAgentComponentSizeConfig] = None 

163 

164 def to_string(self): 

165 """Create string like 'prefix/name#value' from a UserAgentComponent.""" 

166 clean_prefix = sanitize_user_agent_string_component( 

167 self.prefix, allow_hash=True 

168 ) 

169 clean_name = sanitize_user_agent_string_component( 

170 self.name, allow_hash=False 

171 ) 

172 if self.value is None or self.value == '': 

173 clean_string = f'{clean_prefix}/{clean_name}' 

174 else: 

175 clean_value = sanitize_user_agent_string_component( 

176 self.value, allow_hash=True 

177 ) 

178 clean_string = f'{clean_prefix}/{clean_name}#{clean_value}' 

179 if self.size_config is not None: 

180 clean_string = self._truncate_string( 

181 clean_string, 

182 self.size_config.max_size_in_bytes, 

183 self.size_config.delimiter, 

184 ) 

185 return clean_string 

186 

187 def _truncate_string(self, string, max_size, delimiter): 

188 """ 

189 Pop ``delimiter``-separated values until encoded string is less than or 

190 equal to ``max_size``. 

191 """ 

192 orig = string 

193 while len(string.encode('utf-8')) > max_size: 

194 parts = string.split(delimiter) 

195 parts.pop() 

196 string = delimiter.join(parts) 

197 

198 if string == '': 

199 logger.debug( 

200 "User agent component `%s` could not be truncated to " 

201 "`%s` bytes with delimiter " 

202 "`%s` without losing all contents. " 

203 "Value will be omitted from user agent string.", 

204 orig, 

205 max_size, 

206 delimiter, 

207 ) 

208 return string 

209 

210 

211class RawStringUserAgentComponent: 

212 """ 

213 UserAgentComponent interface wrapper around ``str``. 

214 

215 Use for User-Agent header components that are not constructed from 

216 prefix+name+value but instead are provided as strings. No sanitization is 

217 performed. 

218 """ 

219 

220 def __init__(self, value): 

221 self._value = value 

222 

223 def to_string(self): 

224 return self._value 

225 

226 

227# This is not a public interface and is subject to abrupt breaking changes. 

228# Any usage is not advised or supported in external code bases. 

229try: 

230 from botocore.customizations.useragent import modify_components 

231except ImportError: 

232 # Default implementation that returns unmodified User-Agent components. 

233 def modify_components(components): 

234 return components 

235 

236 

237class UserAgentString: 

238 """ 

239 Generator for AWS SDK User-Agent header strings. 

240 

241 The User-Agent header format contains information from session, client, and 

242 request context. ``UserAgentString`` provides methods for collecting the 

243 information and ``to_string`` for assembling it into the standardized 

244 string format. 

245 

246 Example usage: 

247 

248 ua_session = UserAgentString.from_environment() 

249 ua_session.set_session_config(...) 

250 ua_client = ua_session.with_client_config(Config(...)) 

251 ua_string = ua_request.to_string() 

252 

253 For testing or when information from all sources is available at the same 

254 time, the methods can be chained: 

255 

256 ua_string = ( 

257 UserAgentString 

258 .from_environment() 

259 .set_session_config(...) 

260 .with_client_config(Config(...)) 

261 .to_string() 

262 ) 

263 

264 """ 

265 

266 def __init__( 

267 self, 

268 platform_name, 

269 platform_version, 

270 platform_machine, 

271 python_version, 

272 python_implementation, 

273 execution_env, 

274 crt_version=None, 

275 ): 

276 """ 

277 :type platform_name: str 

278 :param platform_name: Name of the operating system or equivalent 

279 platform name. Should be sourced from :py:meth:`platform.system`. 

280 :type platform_version: str 

281 :param platform_version: Version of the operating system or equivalent 

282 platform name. Should be sourced from :py:meth:`platform.version`. 

283 :type platform_machine: str 

284 :param platform_version: Processor architecture or machine type. For 

285 example "x86_64". Should be sourced from :py:meth:`platform.machine`. 

286 :type python_version: str 

287 :param python_version: Version of the python implementation as str. 

288 Should be sourced from :py:meth:`platform.python_version`. 

289 :type python_implementation: str 

290 :param python_implementation: Name of the python implementation. 

291 Should be sourced from :py:meth:`platform.python_implementation`. 

292 :type execution_env: str 

293 :param execution_env: The value of the AWS execution environment. 

294 Should be sourced from the ``AWS_EXECUTION_ENV` environment 

295 variable. 

296 :type crt_version: str 

297 :param crt_version: Version string of awscrt package, if installed. 

298 """ 

299 self._platform_name = platform_name 

300 self._platform_version = platform_version 

301 self._platform_machine = platform_machine 

302 self._python_version = python_version 

303 self._python_implementation = python_implementation 

304 self._execution_env = execution_env 

305 self._crt_version = crt_version 

306 

307 # Components that can be added with ``set_session_config()`` 

308 self._session_user_agent_name = None 

309 self._session_user_agent_version = None 

310 self._session_user_agent_extra = None 

311 

312 self._client_config = None 

313 

314 # Component that can be set with ``set_client_features()`` 

315 self._client_features = None 

316 

317 @classmethod 

318 def from_environment(cls): 

319 crt_version = None 

320 if HAS_CRT: 

321 crt_version = _get_crt_version() or 'Unknown' 

322 return cls( 

323 platform_name=platform.system(), 

324 platform_version=platform.release(), 

325 platform_machine=platform.machine(), 

326 python_version=platform.python_version(), 

327 python_implementation=platform.python_implementation(), 

328 execution_env=os.environ.get('AWS_EXECUTION_ENV'), 

329 crt_version=crt_version, 

330 ) 

331 

332 def set_session_config( 

333 self, 

334 session_user_agent_name, 

335 session_user_agent_version, 

336 session_user_agent_extra, 

337 ): 

338 """ 

339 Set the user agent configuration values that apply at session level. 

340 

341 :param user_agent_name: The user agent name configured in the 

342 :py:class:`botocore.session.Session` object. For backwards 

343 compatibility, this will always be at the beginning of the 

344 User-Agent string, together with ``user_agent_version``. 

345 :param user_agent_version: The user agent version configured in the 

346 :py:class:`botocore.session.Session` object. 

347 :param user_agent_extra: The user agent "extra" configured in the 

348 :py:class:`botocore.session.Session` object. 

349 """ 

350 self._session_user_agent_name = session_user_agent_name 

351 self._session_user_agent_version = session_user_agent_version 

352 self._session_user_agent_extra = session_user_agent_extra 

353 return self 

354 

355 def set_client_features(self, features): 

356 """ 

357 Persist client-specific features registered before or during client 

358 creation. 

359 

360 :type features: Set[str] 

361 :param features: A set of client-specific features. 

362 """ 

363 self._client_features = features 

364 

365 def with_client_config(self, client_config): 

366 """ 

367 Create a copy with all original values and client-specific values. 

368 

369 :type client_config: botocore.config.Config 

370 :param client_config: The client configuration object. 

371 """ 

372 cp = copy(self) 

373 cp._client_config = client_config 

374 return cp 

375 

376 def to_string(self): 

377 """ 

378 Build User-Agent header string from the object's properties. 

379 """ 

380 config_ua_override = None 

381 if self._client_config: 

382 if hasattr(self._client_config, '_supplied_user_agent'): 

383 config_ua_override = self._client_config._supplied_user_agent 

384 else: 

385 config_ua_override = self._client_config.user_agent 

386 

387 if config_ua_override is not None: 

388 return self._build_legacy_ua_string(config_ua_override) 

389 

390 components = [ 

391 *self._build_sdk_metadata(), 

392 RawStringUserAgentComponent('ua/2.1'), 

393 *self._build_os_metadata(), 

394 *self._build_architecture_metadata(), 

395 *self._build_language_metadata(), 

396 *self._build_execution_env_metadata(), 

397 *self._build_feature_metadata(), 

398 *self._build_config_metadata(), 

399 *self._build_app_id(), 

400 *self._build_extra(), 

401 ] 

402 

403 components = modify_components(components) 

404 

405 return ' '.join( 

406 [comp.to_string() for comp in components if comp.to_string()] 

407 ) 

408 

409 def _build_sdk_metadata(self): 

410 """ 

411 Build the SDK name and version component of the User-Agent header. 

412 

413 For backwards-compatibility both session-level and client-level config 

414 of custom tool names are honored. If this removes the Botocore 

415 information from the start of the string, Botocore's name and version 

416 are included as a separate field with "md" prefix. 

417 """ 

418 sdk_md = [] 

419 if ( 

420 self._session_user_agent_name 

421 and self._session_user_agent_version 

422 and ( 

423 self._session_user_agent_name != _USERAGENT_SDK_NAME 

424 or self._session_user_agent_version != botocore_version 

425 ) 

426 ): 

427 sdk_md.extend( 

428 [ 

429 UserAgentComponent( 

430 self._session_user_agent_name, 

431 self._session_user_agent_version, 

432 ), 

433 UserAgentComponent( 

434 'md', _USERAGENT_SDK_NAME, botocore_version 

435 ), 

436 ] 

437 ) 

438 else: 

439 sdk_md.append( 

440 UserAgentComponent(_USERAGENT_SDK_NAME, botocore_version) 

441 ) 

442 

443 if self._crt_version is not None: 

444 sdk_md.append( 

445 UserAgentComponent('md', 'awscrt', self._crt_version) 

446 ) 

447 

448 return sdk_md 

449 

450 def _build_os_metadata(self): 

451 """ 

452 Build the OS/platform components of the User-Agent header string. 

453 

454 For recognized platform names that match or map to an entry in the list 

455 of standardized OS names, a single component with prefix "os" is 

456 returned. Otherwise, one component "os/other" is returned and a second 

457 with prefix "md" and the raw platform name. 

458 

459 String representations of example return values: 

460 * ``os/macos#10.13.6`` 

461 * ``os/linux`` 

462 * ``os/other`` 

463 * ``os/other md/foobar#1.2.3`` 

464 """ 

465 if self._platform_name is None: 

466 return [UserAgentComponent('os', 'other')] 

467 

468 plt_name_lower = self._platform_name.lower() 

469 if plt_name_lower in _USERAGENT_ALLOWED_OS_NAMES: 

470 os_family = plt_name_lower 

471 elif plt_name_lower in _USERAGENT_PLATFORM_NAME_MAPPINGS: 

472 os_family = _USERAGENT_PLATFORM_NAME_MAPPINGS[plt_name_lower] 

473 else: 

474 os_family = None 

475 

476 if os_family is not None: 

477 return [ 

478 UserAgentComponent('os', os_family, self._platform_version) 

479 ] 

480 else: 

481 return [ 

482 UserAgentComponent('os', 'other'), 

483 UserAgentComponent( 

484 'md', self._platform_name, self._platform_version 

485 ), 

486 ] 

487 

488 def _build_architecture_metadata(self): 

489 """ 

490 Build architecture component of the User-Agent header string. 

491 

492 Returns the machine type with prefix "md" and name "arch", if one is 

493 available. Common values include "x86_64", "arm64", "i386". 

494 """ 

495 if self._platform_machine: 

496 return [ 

497 UserAgentComponent( 

498 'md', 'arch', self._platform_machine.lower() 

499 ) 

500 ] 

501 return [] 

502 

503 def _build_language_metadata(self): 

504 """ 

505 Build the language components of the User-Agent header string. 

506 

507 Returns the Python version in a component with prefix "lang" and name 

508 "python". The Python implementation (e.g. CPython, PyPy) is returned as 

509 separate metadata component with prefix "md" and name "pyimpl". 

510 

511 String representation of an example return value: 

512 ``lang/python#3.10.4 md/pyimpl#CPython`` 

513 """ 

514 lang_md = [ 

515 UserAgentComponent('lang', 'python', self._python_version), 

516 ] 

517 if self._python_implementation: 

518 lang_md.append( 

519 UserAgentComponent('md', 'pyimpl', self._python_implementation) 

520 ) 

521 return lang_md 

522 

523 def _build_execution_env_metadata(self): 

524 """ 

525 Build the execution environment component of the User-Agent header. 

526 

527 Returns a single component prefixed with "exec-env", usually sourced 

528 from the environment variable AWS_EXECUTION_ENV. 

529 """ 

530 if self._execution_env: 

531 return [UserAgentComponent('exec-env', self._execution_env)] 

532 else: 

533 return [] 

534 

535 def _build_feature_metadata(self): 

536 """ 

537 Build the features component of the User-Agent header string. 

538 

539 Returns a single component with prefix "m" followed by a list of 

540 comma-separated metric values. 

541 """ 

542 ctx = get_context() 

543 context_features = set() if ctx is None else ctx.features 

544 client_features = self._client_features or set() 

545 features = client_features.union(context_features) 

546 if not features: 

547 return [] 

548 size_config = UserAgentComponentSizeConfig(1024, ',') 

549 return [ 

550 UserAgentComponent( 

551 'm', ','.join(features), size_config=size_config 

552 ) 

553 ] 

554 

555 def _build_config_metadata(self): 

556 """ 

557 Build the configuration components of the User-Agent header string. 

558 

559 Returns a list of components with prefix "cfg" followed by the config 

560 setting name and its value. Tracked configuration settings may be 

561 added or removed in future versions. 

562 """ 

563 if not self._client_config or not self._client_config.retries: 

564 return [] 

565 retry_mode = self._client_config.retries.get('mode') 

566 cfg_md = [UserAgentComponent('cfg', 'retry-mode', retry_mode)] 

567 if self._client_config.endpoint_discovery_enabled: 

568 cfg_md.append(UserAgentComponent('cfg', 'endpoint-discovery')) 

569 return cfg_md 

570 

571 def _build_app_id(self): 

572 """ 

573 Build app component of the User-Agent header string. 

574 

575 Returns a single component with prefix "app" and value sourced from the 

576 ``user_agent_appid`` field in :py:class:`botocore.config.Config` or 

577 the ``sdk_ua_app_id`` setting in the shared configuration file, or the 

578 ``AWS_SDK_UA_APP_ID`` environment variable. These are the recommended 

579 ways for apps built with Botocore to insert their identifer into the 

580 User-Agent header. 

581 """ 

582 if self._client_config and self._client_config.user_agent_appid: 

583 return [ 

584 UserAgentComponent('app', self._client_config.user_agent_appid) 

585 ] 

586 else: 

587 return [] 

588 

589 def _build_extra(self): 

590 """User agent string components based on legacy "extra" settings. 

591 

592 Creates components from the session-level and client-level 

593 ``user_agent_extra`` setting, if present. Both are passed through 

594 verbatim and should be appended at the end of the string. 

595 

596 Preferred ways to inject application-specific information into 

597 botocore's User-Agent header string are the ``user_agent_appid` field 

598 in :py:class:`botocore.config.Config`. The ``AWS_SDK_UA_APP_ID`` 

599 environment variable and the ``sdk_ua_app_id`` configuration file 

600 setting are alternative ways to set the ``user_agent_appid`` config. 

601 """ 

602 extra = [] 

603 if self._session_user_agent_extra: 

604 extra.append( 

605 RawStringUserAgentComponent(self._session_user_agent_extra) 

606 ) 

607 if self._client_config and self._client_config.user_agent_extra: 

608 extra.append( 

609 RawStringUserAgentComponent( 

610 self._client_config.user_agent_extra 

611 ) 

612 ) 

613 return extra 

614 

615 def _build_legacy_ua_string(self, config_ua_override): 

616 components = [config_ua_override] 

617 if self._session_user_agent_extra: 

618 components.append(self._session_user_agent_extra) 

619 if self._client_config.user_agent_extra: 

620 components.append(self._client_config.user_agent_extra) 

621 return ' '.join(components) 

622 

623 def rebuild_and_replace_user_agent_handler( 

624 self, operation_name, request, **kwargs 

625 ): 

626 ua_string = self.to_string() 

627 if request.headers.get('User-Agent'): 

628 request.headers.replace_header('User-Agent', ua_string) 

629 

630 

631def _get_crt_version(): 

632 """ 

633 This function is considered private and is subject to abrupt breaking 

634 changes. 

635 """ 

636 try: 

637 import awscrt 

638 

639 return awscrt.__version__ 

640 except AttributeError: 

641 return None