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 'BEARER_SERVICE_ENV_VARS': '3', 

82} 

83 

84 

85def register_feature_id(feature_id): 

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

87 

88 :type feature_id: str 

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

90 in the ``_USERAGENT_FEATURE_MAPPINGS`` dict. 

91 """ 

92 ctx = get_context() 

93 if ctx is None: 

94 # Never register features outside the scope of a 

95 # ``botocore.context.start_as_current_context`` context manager. 

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

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

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

99 return 

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

101 ctx.features.add(val) 

102 

103 

104def sanitize_user_agent_string_component(raw_str, allow_hash): 

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

106 

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

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

109 

110 :type raw_str: str 

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

112 

113 :type allow_hash: bool 

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

115 """ 

116 return ''.join( 

117 c 

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

119 else '-' 

120 for c in raw_str 

121 ) 

122 

123 

124class UserAgentComponentSizeConfig: 

125 """ 

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

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

128 """ 

129 

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

131 self.max_size_in_bytes = max_size_in_bytes 

132 self.delimiter = delimiter 

133 self._validate_input() 

134 

135 def _validate_input(self): 

136 if self.max_size_in_bytes < 1: 

137 raise ValueError( 

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

139 'Value must be a positive integer.' 

140 ) 

141 

142 

143class UserAgentComponent(NamedTuple): 

144 """ 

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

146 

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

148 In the string representation these are combined in the format 

149 ``prefix/name#value``. 

150 

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

152 built user agent string component. 

153 

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

155 """ 

156 

157 prefix: str 

158 name: str 

159 value: Optional[str] = None 

160 size_config: Optional[UserAgentComponentSizeConfig] = None 

161 

162 def to_string(self): 

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

164 clean_prefix = sanitize_user_agent_string_component( 

165 self.prefix, allow_hash=True 

166 ) 

167 clean_name = sanitize_user_agent_string_component( 

168 self.name, allow_hash=False 

169 ) 

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

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

172 else: 

173 clean_value = sanitize_user_agent_string_component( 

174 self.value, allow_hash=True 

175 ) 

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

177 if self.size_config is not None: 

178 clean_string = self._truncate_string( 

179 clean_string, 

180 self.size_config.max_size_in_bytes, 

181 self.size_config.delimiter, 

182 ) 

183 return clean_string 

184 

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

186 """ 

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

188 equal to ``max_size``. 

189 """ 

190 orig = string 

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

192 parts = string.split(delimiter) 

193 parts.pop() 

194 string = delimiter.join(parts) 

195 

196 if string == '': 

197 logger.debug( 

198 f"User agent component `{orig}` could not be truncated to " 

199 f"`{max_size}` bytes with delimiter " 

200 f"`{delimiter}` without losing all contents. " 

201 f"Value will be omitted from user agent string." 

202 ) 

203 return string 

204 

205 

206class RawStringUserAgentComponent: 

207 """ 

208 UserAgentComponent interface wrapper around ``str``. 

209 

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

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

212 performed. 

213 """ 

214 

215 def __init__(self, value): 

216 self._value = value 

217 

218 def to_string(self): 

219 return self._value 

220 

221 

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

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

224try: 

225 from botocore.customizations.useragent import modify_components 

226except ImportError: 

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

228 def modify_components(components): 

229 return components 

230 

231 

232class UserAgentString: 

233 """ 

234 Generator for AWS SDK User-Agent header strings. 

235 

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

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

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

239 string format. 

240 

241 Example usage: 

242 

243 ua_session = UserAgentString.from_environment() 

244 ua_session.set_session_config(...) 

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

246 ua_string = ua_request.to_string() 

247 

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

249 time, the methods can be chained: 

250 

251 ua_string = ( 

252 UserAgentString 

253 .from_environment() 

254 .set_session_config(...) 

255 .with_client_config(Config(...)) 

256 .to_string() 

257 ) 

258 

259 """ 

260 

261 def __init__( 

262 self, 

263 platform_name, 

264 platform_version, 

265 platform_machine, 

266 python_version, 

267 python_implementation, 

268 execution_env, 

269 crt_version=None, 

270 ): 

271 """ 

272 :type platform_name: str 

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

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

275 :type platform_version: str 

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

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

278 :type platform_machine: str 

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

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

281 :type python_version: str 

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

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

284 :type python_implementation: str 

285 :param python_implementation: Name of the python implementation. 

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

287 :type execution_env: str 

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

289 Should be sourced from the ``AWS_EXECUTION_ENV` environment 

290 variable. 

291 :type crt_version: str 

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

293 """ 

294 self._platform_name = platform_name 

295 self._platform_version = platform_version 

296 self._platform_machine = platform_machine 

297 self._python_version = python_version 

298 self._python_implementation = python_implementation 

299 self._execution_env = execution_env 

300 self._crt_version = crt_version 

301 

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

303 self._session_user_agent_name = None 

304 self._session_user_agent_version = None 

305 self._session_user_agent_extra = None 

306 

307 self._client_config = None 

308 

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

310 self._client_features = None 

311 

312 @classmethod 

313 def from_environment(cls): 

314 crt_version = None 

315 if HAS_CRT: 

316 crt_version = _get_crt_version() or 'Unknown' 

317 return cls( 

318 platform_name=platform.system(), 

319 platform_version=platform.release(), 

320 platform_machine=platform.machine(), 

321 python_version=platform.python_version(), 

322 python_implementation=platform.python_implementation(), 

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

324 crt_version=crt_version, 

325 ) 

326 

327 def set_session_config( 

328 self, 

329 session_user_agent_name, 

330 session_user_agent_version, 

331 session_user_agent_extra, 

332 ): 

333 """ 

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

335 

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

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

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

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

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

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

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

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

344 """ 

345 self._session_user_agent_name = session_user_agent_name 

346 self._session_user_agent_version = session_user_agent_version 

347 self._session_user_agent_extra = session_user_agent_extra 

348 return self 

349 

350 def set_client_features(self, features): 

351 """ 

352 Persist client-specific features registered before or during client 

353 creation. 

354 

355 :type features: Set[str] 

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

357 """ 

358 self._client_features = features 

359 

360 def with_client_config(self, client_config): 

361 """ 

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

363 

364 :type client_config: botocore.config.Config 

365 :param client_config: The client configuration object. 

366 """ 

367 cp = copy(self) 

368 cp._client_config = client_config 

369 return cp 

370 

371 def to_string(self): 

372 """ 

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

374 """ 

375 config_ua_override = None 

376 if self._client_config: 

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

378 config_ua_override = self._client_config._supplied_user_agent 

379 else: 

380 config_ua_override = self._client_config.user_agent 

381 

382 if config_ua_override is not None: 

383 return self._build_legacy_ua_string(config_ua_override) 

384 

385 components = [ 

386 *self._build_sdk_metadata(), 

387 RawStringUserAgentComponent('ua/2.1'), 

388 *self._build_os_metadata(), 

389 *self._build_architecture_metadata(), 

390 *self._build_language_metadata(), 

391 *self._build_execution_env_metadata(), 

392 *self._build_feature_metadata(), 

393 *self._build_config_metadata(), 

394 *self._build_app_id(), 

395 *self._build_extra(), 

396 ] 

397 

398 components = modify_components(components) 

399 

400 return ' '.join( 

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

402 ) 

403 

404 def _build_sdk_metadata(self): 

405 """ 

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

407 

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

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

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

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

412 """ 

413 sdk_md = [] 

414 if ( 

415 self._session_user_agent_name 

416 and self._session_user_agent_version 

417 and ( 

418 self._session_user_agent_name != _USERAGENT_SDK_NAME 

419 or self._session_user_agent_version != botocore_version 

420 ) 

421 ): 

422 sdk_md.extend( 

423 [ 

424 UserAgentComponent( 

425 self._session_user_agent_name, 

426 self._session_user_agent_version, 

427 ), 

428 UserAgentComponent( 

429 'md', _USERAGENT_SDK_NAME, botocore_version 

430 ), 

431 ] 

432 ) 

433 else: 

434 sdk_md.append( 

435 UserAgentComponent(_USERAGENT_SDK_NAME, botocore_version) 

436 ) 

437 

438 if self._crt_version is not None: 

439 sdk_md.append( 

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

441 ) 

442 

443 return sdk_md 

444 

445 def _build_os_metadata(self): 

446 """ 

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

448 

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

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

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

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

453 

454 String representations of example return values: 

455 * ``os/macos#10.13.6`` 

456 * ``os/linux`` 

457 * ``os/other`` 

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

459 """ 

460 if self._platform_name is None: 

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

462 

463 plt_name_lower = self._platform_name.lower() 

464 if plt_name_lower in _USERAGENT_ALLOWED_OS_NAMES: 

465 os_family = plt_name_lower 

466 elif plt_name_lower in _USERAGENT_PLATFORM_NAME_MAPPINGS: 

467 os_family = _USERAGENT_PLATFORM_NAME_MAPPINGS[plt_name_lower] 

468 else: 

469 os_family = None 

470 

471 if os_family is not None: 

472 return [ 

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

474 ] 

475 else: 

476 return [ 

477 UserAgentComponent('os', 'other'), 

478 UserAgentComponent( 

479 'md', self._platform_name, self._platform_version 

480 ), 

481 ] 

482 

483 def _build_architecture_metadata(self): 

484 """ 

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

486 

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

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

489 """ 

490 if self._platform_machine: 

491 return [ 

492 UserAgentComponent( 

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

494 ) 

495 ] 

496 return [] 

497 

498 def _build_language_metadata(self): 

499 """ 

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

501 

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

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

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

505 

506 String representation of an example return value: 

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

508 """ 

509 lang_md = [ 

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

511 ] 

512 if self._python_implementation: 

513 lang_md.append( 

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

515 ) 

516 return lang_md 

517 

518 def _build_execution_env_metadata(self): 

519 """ 

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

521 

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

523 from the environment variable AWS_EXECUTION_ENV. 

524 """ 

525 if self._execution_env: 

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

527 else: 

528 return [] 

529 

530 def _build_feature_metadata(self): 

531 """ 

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

533 

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

535 comma-separated metric values. 

536 """ 

537 ctx = get_context() 

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

539 client_features = self._client_features or set() 

540 features = client_features.union(context_features) 

541 if not features: 

542 return [] 

543 size_config = UserAgentComponentSizeConfig(1024, ',') 

544 return [ 

545 UserAgentComponent( 

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

547 ) 

548 ] 

549 

550 def _build_config_metadata(self): 

551 """ 

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

553 

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

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

556 added or removed in future versions. 

557 """ 

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

559 return [] 

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

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

562 if self._client_config.endpoint_discovery_enabled: 

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

564 return cfg_md 

565 

566 def _build_app_id(self): 

567 """ 

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

569 

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

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

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

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

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

575 User-Agent header. 

576 """ 

577 if self._client_config and self._client_config.user_agent_appid: 

578 return [ 

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

580 ] 

581 else: 

582 return [] 

583 

584 def _build_extra(self): 

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

586 

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

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

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

590 

591 Preferred ways to inject application-specific information into 

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

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

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

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

596 """ 

597 extra = [] 

598 if self._session_user_agent_extra: 

599 extra.append( 

600 RawStringUserAgentComponent(self._session_user_agent_extra) 

601 ) 

602 if self._client_config and self._client_config.user_agent_extra: 

603 extra.append( 

604 RawStringUserAgentComponent( 

605 self._client_config.user_agent_extra 

606 ) 

607 ) 

608 return extra 

609 

610 def _build_legacy_ua_string(self, config_ua_override): 

611 components = [config_ua_override] 

612 if self._session_user_agent_extra: 

613 components.append(self._session_user_agent_extra) 

614 if self._client_config.user_agent_extra: 

615 components.append(self._client_config.user_agent_extra) 

616 return ' '.join(components) 

617 

618 def rebuild_and_replace_user_agent_handler( 

619 self, operation_name, request, **kwargs 

620 ): 

621 ua_string = self.to_string() 

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

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

624 

625 

626def _get_crt_version(): 

627 """ 

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

629 changes. 

630 """ 

631 try: 

632 import awscrt 

633 

634 return awscrt.__version__ 

635 except AttributeError: 

636 return None